flyspace-cli 0.1.6 → 0.1.9
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-BZYTXXF7.js → chunk-JG6LNX5D.js} +21 -17
- package/dist/cli.js +1006 -227
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
expandPath,
|
|
53
53
|
extractCategories,
|
|
54
54
|
findChannel,
|
|
55
|
+
findChannelByAgentName,
|
|
55
56
|
findChannelById,
|
|
56
57
|
findSkillDetail,
|
|
57
58
|
getAgentPaused,
|
|
@@ -106,6 +107,7 @@ import {
|
|
|
106
107
|
mcpStrictConfigEnabled,
|
|
107
108
|
mimetypeSizeLimit,
|
|
108
109
|
modelLabel,
|
|
110
|
+
pinnedClaudeConfigDir,
|
|
109
111
|
readSkillFile,
|
|
110
112
|
recentRunsForWorkspace,
|
|
111
113
|
refreshAutomationRegistry,
|
|
@@ -150,7 +152,7 @@ import {
|
|
|
150
152
|
warnOnBrokenChannelCwds,
|
|
151
153
|
workspaceSkillsDir,
|
|
152
154
|
writeSkillFile
|
|
153
|
-
} from "./chunk-
|
|
155
|
+
} from "./chunk-JG6LNX5D.js";
|
|
154
156
|
import {
|
|
155
157
|
firstReplyBody,
|
|
156
158
|
getThread,
|
|
@@ -430,13 +432,14 @@ async function runConfig(args) {
|
|
|
430
432
|
}
|
|
431
433
|
|
|
432
434
|
// src/commands/setup.ts
|
|
435
|
+
import { execSync as execSync4 } from "child_process";
|
|
433
436
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
434
437
|
import { join as join7 } from "path";
|
|
435
438
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
436
439
|
|
|
437
440
|
// src/service.ts
|
|
438
441
|
import { execSync } from "child_process";
|
|
439
|
-
import { existsSync as existsSync3 } from "fs";
|
|
442
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
440
443
|
import { join as join2 } from "path";
|
|
441
444
|
import { homedir, platform } from "os";
|
|
442
445
|
function serviceLabel(profile) {
|
|
@@ -465,6 +468,22 @@ function isServiceInstalled(profile) {
|
|
|
465
468
|
if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
|
|
466
469
|
return false;
|
|
467
470
|
}
|
|
471
|
+
function serviceBinaryMissing(file) {
|
|
472
|
+
let text;
|
|
473
|
+
try {
|
|
474
|
+
text = readFileSync3(file, "utf-8");
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
|
|
479
|
+
return !!bin && !existsSync3(bin);
|
|
480
|
+
}
|
|
481
|
+
function profileServiceFile(profile) {
|
|
482
|
+
return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
|
|
483
|
+
}
|
|
484
|
+
function hostServiceFile() {
|
|
485
|
+
return platform() === "darwin" ? hostPlistPath() : join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
|
|
486
|
+
}
|
|
468
487
|
function restartService(profile) {
|
|
469
488
|
const os = platform();
|
|
470
489
|
if (os === "darwin") {
|
|
@@ -496,10 +515,7 @@ function hostPlistPath() {
|
|
|
496
515
|
return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
|
|
497
516
|
}
|
|
498
517
|
function isHostServiceInstalled() {
|
|
499
|
-
if (platform() === "darwin") return existsSync3(
|
|
500
|
-
if (platform() === "linux") {
|
|
501
|
-
return existsSync3(join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`));
|
|
502
|
-
}
|
|
518
|
+
if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
|
|
503
519
|
return false;
|
|
504
520
|
}
|
|
505
521
|
function restartHostService() {
|
|
@@ -517,9 +533,29 @@ function restartHostService() {
|
|
|
517
533
|
}
|
|
518
534
|
return false;
|
|
519
535
|
}
|
|
536
|
+
function serviceActive(label2, unit) {
|
|
537
|
+
try {
|
|
538
|
+
if (platform() === "darwin") {
|
|
539
|
+
const out = execSync(`launchctl print gui/${process.getuid?.()}/${label2}`, { encoding: "utf-8", stdio: "pipe" });
|
|
540
|
+
return /^\s*state = running$/m.test(out);
|
|
541
|
+
}
|
|
542
|
+
if (platform() === "linux") {
|
|
543
|
+
execSync(`systemctl --user is-active --quiet ${unit}`, { stdio: "pipe" });
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
function isServiceRunning(profile) {
|
|
551
|
+
return serviceActive(serviceLabel(profile), systemdUnitName(profile));
|
|
552
|
+
}
|
|
553
|
+
function isHostServiceRunning() {
|
|
554
|
+
return serviceActive(HOST_SERVICE_LABEL, HOST_SYSTEMD_UNIT);
|
|
555
|
+
}
|
|
520
556
|
|
|
521
557
|
// src/commands/install.ts
|
|
522
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
558
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
|
|
523
559
|
import { execSync as execSync2 } from "child_process";
|
|
524
560
|
import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
|
|
525
561
|
import { homedir as homedir2, platform as platform2 } from "os";
|
|
@@ -547,6 +583,11 @@ function detectClaudeBinDir() {
|
|
|
547
583
|
return void 0;
|
|
548
584
|
}
|
|
549
585
|
}
|
|
586
|
+
function nodeShimDirs() {
|
|
587
|
+
return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
|
|
588
|
+
(d) => existsSync4(d)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
550
591
|
function detectCustieBin() {
|
|
551
592
|
const argv1 = process.argv[1];
|
|
552
593
|
if (argv1) {
|
|
@@ -561,7 +602,7 @@ function detectCustieBin() {
|
|
|
561
602
|
function readEnvFile() {
|
|
562
603
|
if (!existsSync4(paths.CONFIG_FILE)) return {};
|
|
563
604
|
const vars = {};
|
|
564
|
-
for (const line of
|
|
605
|
+
for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
|
|
565
606
|
const trimmed = line.trim();
|
|
566
607
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
567
608
|
const eq = trimmed.indexOf("=");
|
|
@@ -609,12 +650,10 @@ async function installMacOS(spec) {
|
|
|
609
650
|
mkdirSync2(logDir, { recursive: true });
|
|
610
651
|
const envVars = spec.env;
|
|
611
652
|
const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
|
|
612
|
-
const asdfShims = join3(homedir2(), ".asdf", "shims");
|
|
613
|
-
const asdfBin = existsSync4(asdfShims) ? asdfShims : void 0;
|
|
614
653
|
const pathParts = [
|
|
615
654
|
dirname2(nodePath),
|
|
616
655
|
claudeBinDir,
|
|
617
|
-
|
|
656
|
+
...nodeShimDirs(),
|
|
618
657
|
homebrewBin,
|
|
619
658
|
"/usr/local/bin",
|
|
620
659
|
"/usr/bin",
|
|
@@ -683,6 +722,7 @@ async function installLinux(spec) {
|
|
|
683
722
|
const pathParts = [
|
|
684
723
|
dirname2(nodePath),
|
|
685
724
|
claudeBinDir,
|
|
725
|
+
...nodeShimDirs(),
|
|
686
726
|
linuxbrewBin,
|
|
687
727
|
"/usr/local/bin",
|
|
688
728
|
"/usr/bin",
|
|
@@ -756,7 +796,7 @@ function listProfiles() {
|
|
|
756
796
|
}
|
|
757
797
|
return profiles;
|
|
758
798
|
}
|
|
759
|
-
function
|
|
799
|
+
function isServiceRunning2(profile) {
|
|
760
800
|
try {
|
|
761
801
|
if (platform3() === "darwin") {
|
|
762
802
|
const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
|
|
@@ -770,19 +810,10 @@ function isServiceRunning(profile) {
|
|
|
770
810
|
}
|
|
771
811
|
return false;
|
|
772
812
|
}
|
|
773
|
-
async function runProfiles() {
|
|
774
|
-
const profiles = listProfiles();
|
|
775
|
-
console.log("\n Profiles:\n");
|
|
776
|
-
for (const profile of profiles) {
|
|
777
|
-
const status = isServiceRunning(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
|
|
778
|
-
console.log(` ${profile.padEnd(20)} ${status}`);
|
|
779
|
-
}
|
|
780
|
-
console.log("\n Target a profile with --profile <name> on any command.\n");
|
|
781
|
-
}
|
|
782
813
|
|
|
783
814
|
// src/auth/device-auth.ts
|
|
784
815
|
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
|
|
785
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
816
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
786
817
|
import { dirname as dirname3, join as join5 } from "path";
|
|
787
818
|
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
788
819
|
var EXPIRY_SKEW_MS = 6e4;
|
|
@@ -810,7 +841,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
|
|
|
810
841
|
writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
811
842
|
created = true;
|
|
812
843
|
}
|
|
813
|
-
const privateKey = createPrivateKey(
|
|
844
|
+
const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
|
|
814
845
|
const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
815
846
|
"base64"
|
|
816
847
|
);
|
|
@@ -819,7 +850,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
|
|
|
819
850
|
function loadCredentials(path = credentialsPath()) {
|
|
820
851
|
if (!existsSync6(path)) return null;
|
|
821
852
|
try {
|
|
822
|
-
return JSON.parse(
|
|
853
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
823
854
|
} catch {
|
|
824
855
|
return null;
|
|
825
856
|
}
|
|
@@ -932,6 +963,17 @@ async function fetchWhoAmI(base, token) {
|
|
|
932
963
|
workspaces: toSlugs(json["workspaces"])
|
|
933
964
|
};
|
|
934
965
|
}
|
|
966
|
+
async function boundWorkspaces() {
|
|
967
|
+
const creds = loadCredentials();
|
|
968
|
+
if (!creds) return null;
|
|
969
|
+
try {
|
|
970
|
+
const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
|
|
971
|
+
saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
|
|
972
|
+
return { workspaces: me.workspaces, fresh: true };
|
|
973
|
+
} catch {
|
|
974
|
+
return { workspaces: creds.workspaces ?? [], fresh: false };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
935
977
|
async function revokeDevice(base, token) {
|
|
936
978
|
await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
|
|
937
979
|
}
|
|
@@ -954,7 +996,7 @@ import {
|
|
|
954
996
|
lstatSync,
|
|
955
997
|
mkdirSync as mkdirSync4,
|
|
956
998
|
readdirSync as readdirSync2,
|
|
957
|
-
readFileSync as
|
|
999
|
+
readFileSync as readFileSync6,
|
|
958
1000
|
statSync as statSync2,
|
|
959
1001
|
symlinkSync
|
|
960
1002
|
} from "fs";
|
|
@@ -983,7 +1025,7 @@ function claudeLoggedIn() {
|
|
|
983
1025
|
if (process.env["ANTHROPIC_API_KEY"]) return true;
|
|
984
1026
|
if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
|
|
985
1027
|
try {
|
|
986
|
-
const raw = JSON.parse(
|
|
1028
|
+
const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
|
|
987
1029
|
if (raw.oauthAccount) return true;
|
|
988
1030
|
} catch {
|
|
989
1031
|
}
|
|
@@ -1024,7 +1066,7 @@ function jsonlFiles(dir, depth) {
|
|
|
1024
1066
|
function cwdFromTranscript(file) {
|
|
1025
1067
|
let head;
|
|
1026
1068
|
try {
|
|
1027
|
-
head =
|
|
1069
|
+
head = readFileSync6(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
|
|
1028
1070
|
} catch {
|
|
1029
1071
|
return null;
|
|
1030
1072
|
}
|
|
@@ -1041,7 +1083,7 @@ function cwdFromTranscript(file) {
|
|
|
1041
1083
|
}
|
|
1042
1084
|
function codexTrustedPaths() {
|
|
1043
1085
|
try {
|
|
1044
|
-
const toml =
|
|
1086
|
+
const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
|
|
1045
1087
|
return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
|
|
1046
1088
|
} catch {
|
|
1047
1089
|
return [];
|
|
@@ -1510,6 +1552,16 @@ async function runAuthCmd(args) {
|
|
|
1510
1552
|
}
|
|
1511
1553
|
}
|
|
1512
1554
|
|
|
1555
|
+
// src/version.ts
|
|
1556
|
+
var CLI_VERSION = true ? "0.1.9" : null;
|
|
1557
|
+
function isOlderVersion(a, b) {
|
|
1558
|
+
const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1559
|
+
const [pa, pb] = [parse2(a), parse2(b)];
|
|
1560
|
+
if (pa.length !== 3 || pb.length !== 3 || [...pa, ...pb].some(Number.isNaN)) return false;
|
|
1561
|
+
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i];
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1513
1565
|
// src/commands/setup.ts
|
|
1514
1566
|
var BOLD2 = "\x1B[1m";
|
|
1515
1567
|
var DIM2 = "\x1B[2m";
|
|
@@ -1541,15 +1593,21 @@ var tilde2 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
|
|
|
1541
1593
|
var ok = (msg) => console.log(` ${GREEN2}\u2713${RESET2} ${msg}`);
|
|
1542
1594
|
var bad = (msg) => console.log(` ${YELLOW2}\u2717${RESET2} ${msg}`);
|
|
1543
1595
|
async function servedWorkspaces() {
|
|
1544
|
-
const
|
|
1596
|
+
const bound = await boundWorkspaces();
|
|
1597
|
+
if (!bound.fresh) console.log(` ${DIM2}Couldn't reach the server; using the workspaces saved at login.${RESET2}`);
|
|
1598
|
+
return bound.workspaces;
|
|
1599
|
+
}
|
|
1600
|
+
function checkLatestVersion() {
|
|
1601
|
+
if (!CLI_VERSION) return true;
|
|
1602
|
+
let latest = "";
|
|
1545
1603
|
try {
|
|
1546
|
-
|
|
1547
|
-
saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
|
|
1548
|
-
return me.workspaces;
|
|
1604
|
+
latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
|
|
1549
1605
|
} catch {
|
|
1550
|
-
|
|
1551
|
-
return creds.workspaces ?? [];
|
|
1606
|
+
return true;
|
|
1552
1607
|
}
|
|
1608
|
+
if (!latest || !isOlderVersion(CLI_VERSION, latest)) return true;
|
|
1609
|
+
bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
|
|
1610
|
+
return false;
|
|
1553
1611
|
}
|
|
1554
1612
|
function loginArgs(args) {
|
|
1555
1613
|
const out = [];
|
|
@@ -1574,6 +1632,7 @@ async function runSetup(args) {
|
|
|
1574
1632
|
console.log(`
|
|
1575
1633
|
${BOLD2}Flyspace Setup${RESET2}
|
|
1576
1634
|
`);
|
|
1635
|
+
checkLatestVersion();
|
|
1577
1636
|
const existing = loadCredentials();
|
|
1578
1637
|
let workspaces;
|
|
1579
1638
|
if (existing) {
|
|
@@ -1583,7 +1642,10 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1583
1642
|
console.log(` Device ${existing.deviceName}`);
|
|
1584
1643
|
console.log(` Workspaces ${workspaces.length ? workspaces.join(", ") : "(none)"}`);
|
|
1585
1644
|
const others = listProfiles().filter((p) => p !== "default" && !workspaces.includes(p));
|
|
1586
|
-
if (others.length)
|
|
1645
|
+
if (others.length) {
|
|
1646
|
+
console.log(` ${DIM2}Not bound here, nothing runs for them: ${others.join(", ")}${RESET2}`);
|
|
1647
|
+
console.log(` ${DIM2}See: flyspace profiles Clean up: flyspace profiles remove <name>${RESET2}`);
|
|
1648
|
+
}
|
|
1587
1649
|
console.log("");
|
|
1588
1650
|
const keep = args.includes("--yes") || !process.stdin.isTTY ? true : await confirm2({ message: "Keep using this account and device?", default: true });
|
|
1589
1651
|
if (!keep) {
|
|
@@ -1611,7 +1673,12 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1611
1673
|
}
|
|
1612
1674
|
let hostRunning = false;
|
|
1613
1675
|
if (args.includes("--start-host") && workspaces.length) {
|
|
1614
|
-
|
|
1676
|
+
for (const slug of workspaces) {
|
|
1677
|
+
if (!serviceBinaryMissing(profileServiceFile(slug))) continue;
|
|
1678
|
+
await runWithProfile(slug, runInstall);
|
|
1679
|
+
ok(`${slug} scheduler reinstalled on the new CLI path`);
|
|
1680
|
+
}
|
|
1681
|
+
if (!isHostServiceInstalled() || serviceBinaryMissing(hostServiceFile())) {
|
|
1615
1682
|
hostRunning = await installHostService();
|
|
1616
1683
|
if (hostRunning) ok(`Host running in the background ${DIM2}logs: ${tilde2(paths.LOG_DIR)}/host.log${RESET2}`);
|
|
1617
1684
|
} else {
|
|
@@ -1635,9 +1702,137 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1635
1702
|
if (!workspaces.length) process.exitCode = 1;
|
|
1636
1703
|
}
|
|
1637
1704
|
|
|
1705
|
+
// src/commands/doctor.ts
|
|
1706
|
+
import { execFile } from "child_process";
|
|
1707
|
+
import { tmpdir } from "os";
|
|
1708
|
+
import { promisify } from "util";
|
|
1709
|
+
var execAsync = promisify(execFile);
|
|
1710
|
+
var exec = (cmd, args, opts) => {
|
|
1711
|
+
const run3 = execAsync(cmd, args, opts);
|
|
1712
|
+
run3.child.stdin?.end();
|
|
1713
|
+
return run3;
|
|
1714
|
+
};
|
|
1715
|
+
var BOLD3 = "\x1B[1m";
|
|
1716
|
+
var DIM3 = "\x1B[2m";
|
|
1717
|
+
var GREEN3 = "\x1B[32m";
|
|
1718
|
+
var YELLOW3 = "\x1B[33m";
|
|
1719
|
+
var RESET3 = "\x1B[0m";
|
|
1720
|
+
var HELP3 = `
|
|
1721
|
+
Usage: flyspace doctor
|
|
1722
|
+
|
|
1723
|
+
Checks this Host end to end: CLI version, server reachability, device login,
|
|
1724
|
+
a live turn with each installed agent CLI, and the background services.
|
|
1725
|
+
Exits non-zero when any check fails.
|
|
1726
|
+
`;
|
|
1727
|
+
var LOGIN_HINTS = {
|
|
1728
|
+
claude: "claude auth login",
|
|
1729
|
+
codex: "codex login"
|
|
1730
|
+
};
|
|
1731
|
+
var tilde3 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
|
|
1732
|
+
var failed = false;
|
|
1733
|
+
var ok2 = (msg) => console.log(` ${GREEN3}\u2713${RESET3} ${msg}`);
|
|
1734
|
+
var bad2 = (msg, hint) => {
|
|
1735
|
+
failed = true;
|
|
1736
|
+
console.log(` ${YELLOW3}\u2717${RESET3} ${msg}${hint ? ` ${DIM3}${hint}${RESET3}` : ""}`);
|
|
1737
|
+
};
|
|
1738
|
+
function errorLine(err) {
|
|
1739
|
+
const e = err;
|
|
1740
|
+
if (e.killed) return "timed out after 90s";
|
|
1741
|
+
const lines = `${e.stdout ?? ""}
|
|
1742
|
+
${e.stderr ?? ""}`.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1743
|
+
const last = (re) => [...lines].reverse().find((l) => re.test(l));
|
|
1744
|
+
const line = last(/expired|unauthori[sz]ed|\b401\b|not logged in|authenticate/i) ?? last(/error|fail/i) ?? lines.at(-1) ?? e.message ?? "failed";
|
|
1745
|
+
return line.length > 160 ? `${line.slice(0, 157)}...` : line;
|
|
1746
|
+
}
|
|
1747
|
+
async function probe(id) {
|
|
1748
|
+
const env = { ...process.env };
|
|
1749
|
+
const opts = { cwd: tmpdir(), timeout: 9e4, encoding: "utf-8" };
|
|
1750
|
+
try {
|
|
1751
|
+
if (id === "claude") {
|
|
1752
|
+
const dir = pinnedClaudeConfigDir(process.env["CLAUDE_CONFIG_DIR"]);
|
|
1753
|
+
if (dir) env["CLAUDE_CONFIG_DIR"] = dir;
|
|
1754
|
+
else delete env["CLAUDE_CONFIG_DIR"];
|
|
1755
|
+
await exec("claude", ["-p", "Reply with OK", "--model", "haiku", "--max-turns", "1"], { ...opts, env });
|
|
1756
|
+
} else {
|
|
1757
|
+
await exec("codex", ["exec", "--skip-git-repo-check", "-s", "read-only", "Reply with OK"], { ...opts, env });
|
|
1758
|
+
}
|
|
1759
|
+
return null;
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
return errorLine(err);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function serverReachable() {
|
|
1765
|
+
try {
|
|
1766
|
+
const res = await fetch(`${apiUrl()}/api/health`, { signal: AbortSignal.timeout(1e4) });
|
|
1767
|
+
return res.ok ? null : `HTTP ${res.status}`;
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
return err.cause ? String(err.cause) : err.message;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function checkService(name, installed, file, running, fix) {
|
|
1773
|
+
if (!installed) bad2(`${name} not installed`, `run: ${fix}`);
|
|
1774
|
+
else if (serviceBinaryMissing(file)) bad2(`${name} points at a CLI that no longer exists`, `run: ${fix}`);
|
|
1775
|
+
else if (!running) bad2(`${name} not running`, `logs: ${tilde3(paths.LOG_DIR)}`);
|
|
1776
|
+
else ok2(`${name} running`);
|
|
1777
|
+
}
|
|
1778
|
+
async function runDoctor(args) {
|
|
1779
|
+
if (args.includes("-h") || args.includes("--help")) return void console.log(HELP3);
|
|
1780
|
+
failed = false;
|
|
1781
|
+
console.log(`
|
|
1782
|
+
${BOLD3}Flyspace Doctor${RESET3}
|
|
1783
|
+
`);
|
|
1784
|
+
if (checkLatestVersion()) ok2(`flyspace ${CLI_VERSION || "(dev build)"}`);
|
|
1785
|
+
else failed = true;
|
|
1786
|
+
const serverError = await serverReachable();
|
|
1787
|
+
if (serverError) bad2(`Can't reach ${apiUrl()}: ${serverError}`, "check the network or DNS");
|
|
1788
|
+
else ok2(`Server ${apiUrl()} reachable`);
|
|
1789
|
+
const creds = loadCredentials();
|
|
1790
|
+
let workspaces = [];
|
|
1791
|
+
if (!creds) {
|
|
1792
|
+
bad2("This device is not logged in", "run: flyspace setup");
|
|
1793
|
+
} else {
|
|
1794
|
+
const bound = await boundWorkspaces();
|
|
1795
|
+
workspaces = bound.workspaces;
|
|
1796
|
+
ok2(`Logged in as ${creds.userEmail} (device ${creds.deviceName})`);
|
|
1797
|
+
if (!bound.fresh) bad2("Couldn't refresh the device session; showing the workspaces saved at login");
|
|
1798
|
+
if (workspaces.length) ok2(`Serving ${workspaces.join(", ")}`);
|
|
1799
|
+
else bad2("Not serving any workspace", "a workspace admin has to add this Host");
|
|
1800
|
+
}
|
|
1801
|
+
const runners = detectRunners().filter((r) => r.installed);
|
|
1802
|
+
if (!runners.length) bad2("Neither claude nor codex is installed", "npm install -g @anthropic-ai/claude-code");
|
|
1803
|
+
const results = await Promise.all(runners.map((r) => probe(r.id)));
|
|
1804
|
+
runners.forEach((r, i) => {
|
|
1805
|
+
const error2 = results[i];
|
|
1806
|
+
if (error2) bad2(`${r.id} can't run a turn: ${error2}`, `run: ${LOGIN_HINTS[r.id]}`);
|
|
1807
|
+
else ok2(`${r.id} answered a test turn`);
|
|
1808
|
+
});
|
|
1809
|
+
if (workspaces.length) {
|
|
1810
|
+
checkService(
|
|
1811
|
+
"Host service",
|
|
1812
|
+
isHostServiceInstalled(),
|
|
1813
|
+
hostServiceFile(),
|
|
1814
|
+
isHostServiceRunning(),
|
|
1815
|
+
"flyspace setup --start-host"
|
|
1816
|
+
);
|
|
1817
|
+
for (const slug of workspaces) {
|
|
1818
|
+
checkService(
|
|
1819
|
+
`${slug} scheduler`,
|
|
1820
|
+
isServiceInstalled(slug),
|
|
1821
|
+
profileServiceFile(slug),
|
|
1822
|
+
isServiceRunning(slug),
|
|
1823
|
+
"flyspace setup --start-host"
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
console.log(failed ? "" : `
|
|
1828
|
+
${GREEN3}All good.${RESET3}
|
|
1829
|
+
`);
|
|
1830
|
+
if (failed) process.exitCode = 1;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1638
1833
|
// src/commands/uninstall.ts
|
|
1639
1834
|
import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
1640
|
-
import { execSync as
|
|
1835
|
+
import { execSync as execSync5 } from "child_process";
|
|
1641
1836
|
import { basename as basename2, join as join8 } from "path";
|
|
1642
1837
|
import { homedir as homedir4, platform as platform5 } from "os";
|
|
1643
1838
|
function log2(msg) {
|
|
@@ -1651,7 +1846,7 @@ function warn2(msg) {
|
|
|
1651
1846
|
console.log(`\x1B[33m!\x1B[0m ${msg}`);
|
|
1652
1847
|
}
|
|
1653
1848
|
function run2(cmd) {
|
|
1654
|
-
|
|
1849
|
+
execSync5(cmd, { encoding: "utf-8", stdio: "pipe" });
|
|
1655
1850
|
}
|
|
1656
1851
|
async function runUninstall() {
|
|
1657
1852
|
const os = platform5();
|
|
@@ -1698,8 +1893,82 @@ async function runUninstall() {
|
|
|
1698
1893
|
console.log("\n Config and data directories were NOT removed.");
|
|
1699
1894
|
}
|
|
1700
1895
|
|
|
1896
|
+
// src/commands/profiles-cmd.ts
|
|
1897
|
+
import { rmSync as rmSync3 } from "fs";
|
|
1898
|
+
var USAGE = `
|
|
1899
|
+
Usage: flyspace profiles [list]
|
|
1900
|
+
flyspace profiles remove <name>
|
|
1901
|
+
|
|
1902
|
+
list Every local profile, whether this Host is bound to it, and the
|
|
1903
|
+
state of its scheduler service.
|
|
1904
|
+
remove Unload a profile's scheduler and delete its local config. Only for
|
|
1905
|
+
profiles this Host is not bound to; the Workspace itself, its
|
|
1906
|
+
channel folders and its data directory are left alone.
|
|
1907
|
+
`;
|
|
1908
|
+
async function boundHere() {
|
|
1909
|
+
const bound = await boundWorkspaces();
|
|
1910
|
+
if (bound && !bound.fresh) console.log(" \x1B[2mCouldn't reach the server; using the workspaces saved at login.\x1B[0m");
|
|
1911
|
+
return bound?.workspaces ?? null;
|
|
1912
|
+
}
|
|
1913
|
+
async function listCmd() {
|
|
1914
|
+
const bound = await boundHere();
|
|
1915
|
+
console.log("\n Profiles:\n");
|
|
1916
|
+
console.log(` ${"NAME".padEnd(24)} ${"BOUND HERE".padEnd(12)} SCHEDULER`);
|
|
1917
|
+
for (const profile of listProfiles()) {
|
|
1918
|
+
const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning2(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
|
|
1919
|
+
const here = bound === null ? "-".padEnd(12) : bound.includes(profile) ? "yes".padEnd(12) : `\x1B[33m${"no".padEnd(12)}\x1B[0m`;
|
|
1920
|
+
console.log(` ${profile.padEnd(24)} ${here} ${scheduler}`);
|
|
1921
|
+
}
|
|
1922
|
+
const missing = (bound ?? []).filter((w) => !listProfiles().includes(w));
|
|
1923
|
+
for (const w of missing) console.log(` ${w.padEnd(24)} ${"yes".padEnd(12)} \x1B[33mno local profile yet\x1B[0m`);
|
|
1924
|
+
if (bound?.length === 0) {
|
|
1925
|
+
console.log("\n This Host is not bound to any workspace, so no scheduler runs here. Run: flyspace setup");
|
|
1926
|
+
}
|
|
1927
|
+
if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
|
|
1928
|
+
console.log("\n Profiles not bound here run nothing on this machine. Remove one with: flyspace profiles remove <name>");
|
|
1929
|
+
}
|
|
1930
|
+
console.log("");
|
|
1931
|
+
}
|
|
1932
|
+
async function removeCmd(name) {
|
|
1933
|
+
if (!name) return void console.log(USAGE);
|
|
1934
|
+
if (name === DEFAULT_PROFILE) {
|
|
1935
|
+
console.error(" The default profile cannot be removed.");
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
}
|
|
1938
|
+
if (!listProfiles().includes(name)) {
|
|
1939
|
+
console.error(` No local profile named "${name}".`);
|
|
1940
|
+
process.exit(1);
|
|
1941
|
+
}
|
|
1942
|
+
const bound = await boundHere();
|
|
1943
|
+
if (bound === null) {
|
|
1944
|
+
console.error(" This machine has no device login, so it serves every workspace; removing a profile would be undone.");
|
|
1945
|
+
process.exit(1);
|
|
1946
|
+
}
|
|
1947
|
+
if (bound.includes(name)) {
|
|
1948
|
+
console.error(` This Host is still bound to "${name}" on the server, so the Host would reinstall it within a minute.`);
|
|
1949
|
+
console.error(" Unbinding a Host from a workspace is not available yet.");
|
|
1950
|
+
process.exit(1);
|
|
1951
|
+
}
|
|
1952
|
+
const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
|
|
1953
|
+
if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
|
|
1954
|
+
rmSync3(configDir2, { recursive: true, force: true });
|
|
1955
|
+
console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
|
|
1956
|
+
console.log(`
|
|
1957
|
+
Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
|
|
1958
|
+
console.log(` The workspace and its server-side records are unchanged; this only cleaned up this machine.
|
|
1959
|
+
`);
|
|
1960
|
+
}
|
|
1961
|
+
async function runProfiles(args = []) {
|
|
1962
|
+
loadEnvFiles();
|
|
1963
|
+
const [sub, name] = args;
|
|
1964
|
+
if (sub === "remove" || sub === "rm") return removeCmd(name);
|
|
1965
|
+
if (!sub || sub === "list" || sub === "ls") return listCmd();
|
|
1966
|
+
console.log(USAGE);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1701
1969
|
// src/commands/upgrade.ts
|
|
1702
|
-
import { execSync as
|
|
1970
|
+
import { execSync as execSync6 } from "child_process";
|
|
1971
|
+
import { realpathSync } from "fs";
|
|
1703
1972
|
function restartInstalledProfiles() {
|
|
1704
1973
|
if (isHostServiceInstalled()) {
|
|
1705
1974
|
console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
|
|
@@ -1719,19 +1988,29 @@ Restarting ${installed.length} instance(s)...`);
|
|
|
1719
1988
|
}
|
|
1720
1989
|
}
|
|
1721
1990
|
}
|
|
1991
|
+
function npmInstallCommand() {
|
|
1992
|
+
let prefix;
|
|
1993
|
+
try {
|
|
1994
|
+
const entry = realpathSync(process.argv[1] ?? "");
|
|
1995
|
+
const at = entry.indexOf(`/lib/node_modules/${"flyspace-cli"}/`);
|
|
1996
|
+
if (at > 0) prefix = entry.slice(0, at);
|
|
1997
|
+
} catch {
|
|
1998
|
+
}
|
|
1999
|
+
return `npm install -g${prefix ? ` --prefix "${prefix}"` : ""} ${"flyspace-cli"}@latest`;
|
|
2000
|
+
}
|
|
1722
2001
|
async function runUpgrade() {
|
|
1723
|
-
const current = "0.1.
|
|
2002
|
+
const current = "0.1.9";
|
|
1724
2003
|
console.log(`Current version: ${current}`);
|
|
1725
2004
|
console.log("Checking for updates...\n");
|
|
1726
2005
|
try {
|
|
1727
|
-
const latest =
|
|
2006
|
+
const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
|
|
1728
2007
|
if (latest === current) {
|
|
1729
2008
|
console.log(`Already on the latest version (${current}).`);
|
|
1730
2009
|
return;
|
|
1731
2010
|
}
|
|
1732
2011
|
console.log(`New version available: ${latest}`);
|
|
1733
2012
|
console.log("Upgrading...\n");
|
|
1734
|
-
|
|
2013
|
+
execSync6(npmInstallCommand(), { stdio: "inherit" });
|
|
1735
2014
|
console.log(`
|
|
1736
2015
|
Upgraded from ${current} to ${latest}.`);
|
|
1737
2016
|
restartInstalledProfiles();
|
|
@@ -1740,14 +2019,14 @@ Upgraded from ${current} to ${latest}.`);
|
|
|
1740
2019
|
} catch (err) {
|
|
1741
2020
|
console.error("Upgrade failed:", err.message);
|
|
1742
2021
|
console.error(`
|
|
1743
|
-
You can upgrade manually with:
|
|
2022
|
+
You can upgrade manually with: ${npmInstallCommand()}`);
|
|
1744
2023
|
process.exit(1);
|
|
1745
2024
|
}
|
|
1746
2025
|
}
|
|
1747
2026
|
|
|
1748
2027
|
// src/commands/automation.ts
|
|
1749
2028
|
import { WebClient } from "@slack/web-api";
|
|
1750
|
-
var
|
|
2029
|
+
var USAGE2 = `
|
|
1751
2030
|
Usage: flyspace automation <subcommand> [options]
|
|
1752
2031
|
|
|
1753
2032
|
Subcommands:
|
|
@@ -2085,14 +2364,14 @@ async function runAutomationCmd(args) {
|
|
|
2085
2364
|
await handleRun(manager, args[1]);
|
|
2086
2365
|
break;
|
|
2087
2366
|
default:
|
|
2088
|
-
console.log(
|
|
2367
|
+
console.log(USAGE2);
|
|
2089
2368
|
break;
|
|
2090
2369
|
}
|
|
2091
2370
|
}
|
|
2092
2371
|
|
|
2093
2372
|
// src/commands/usage.ts
|
|
2094
2373
|
import { existsSync as existsSync9 } from "fs";
|
|
2095
|
-
var
|
|
2374
|
+
var HELP4 = `
|
|
2096
2375
|
Usage: flyspace usage [options]
|
|
2097
2376
|
|
|
2098
2377
|
Show agent token usage and notional cost for this workspace.
|
|
@@ -2214,7 +2493,7 @@ function printExplanation(explained) {
|
|
|
2214
2493
|
async function runUsage(args) {
|
|
2215
2494
|
const parsed = parseArgs(args);
|
|
2216
2495
|
if (parsed === "help") {
|
|
2217
|
-
console.log(
|
|
2496
|
+
console.log(HELP4);
|
|
2218
2497
|
return;
|
|
2219
2498
|
}
|
|
2220
2499
|
const { days, by, explain, json } = parsed;
|
|
@@ -2276,7 +2555,7 @@ function breakdown(store, by, days) {
|
|
|
2276
2555
|
}
|
|
2277
2556
|
|
|
2278
2557
|
// src/commands/stop.ts
|
|
2279
|
-
import { execSync as
|
|
2558
|
+
import { execSync as execSync7 } from "child_process";
|
|
2280
2559
|
import { platform as platform6 } from "os";
|
|
2281
2560
|
function log3(msg) {
|
|
2282
2561
|
console.log(`
|
|
@@ -2295,7 +2574,7 @@ async function runStop() {
|
|
|
2295
2574
|
const plist = plistPath(profile);
|
|
2296
2575
|
log3("Stopping Flyspace service...");
|
|
2297
2576
|
try {
|
|
2298
|
-
|
|
2577
|
+
execSync7(`launchctl unload "${plist}"`, { stdio: "pipe" });
|
|
2299
2578
|
success3("Service stopped. Run `flyspace restart` to start again.");
|
|
2300
2579
|
} catch {
|
|
2301
2580
|
warn3("Service is not currently loaded.");
|
|
@@ -2303,7 +2582,7 @@ async function runStop() {
|
|
|
2303
2582
|
} else if (os === "linux") {
|
|
2304
2583
|
log3("Stopping Flyspace service...");
|
|
2305
2584
|
try {
|
|
2306
|
-
|
|
2585
|
+
execSync7(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
|
|
2307
2586
|
success3("Service stopped. Run `flyspace restart` to start again.");
|
|
2308
2587
|
} catch {
|
|
2309
2588
|
warn3("Service is not currently running.");
|
|
@@ -2360,9 +2639,9 @@ async function runLogs(args) {
|
|
|
2360
2639
|
|
|
2361
2640
|
// src/commands/slack.ts
|
|
2362
2641
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2363
|
-
import { existsSync as existsSync27, readFileSync as
|
|
2642
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
2364
2643
|
import { resolve as resolve7 } from "path";
|
|
2365
|
-
import { tmpdir as
|
|
2644
|
+
import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
|
|
2366
2645
|
import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
|
|
2367
2646
|
import { WebClient as WebClient3 } from "@slack/web-api";
|
|
2368
2647
|
|
|
@@ -3891,22 +4170,18 @@ async function deliverPasswordResetLink(email, redirectTo) {
|
|
|
3891
4170
|
import { existsSync as existsSync26 } from "fs";
|
|
3892
4171
|
import { createServer } from "net";
|
|
3893
4172
|
import { join as join22 } from "path";
|
|
3894
|
-
import { Hono as
|
|
4173
|
+
import { Hono as Hono33 } from "hono";
|
|
3895
4174
|
import { serve } from "@hono/node-server";
|
|
3896
4175
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3897
4176
|
|
|
3898
4177
|
// src/reconcile.ts
|
|
3899
4178
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
3900
|
-
async function
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
return me.workspaces;
|
|
3907
|
-
} catch {
|
|
3908
|
-
return creds.workspaces ?? [];
|
|
3909
|
-
}
|
|
4179
|
+
async function boundWorkspaces2() {
|
|
4180
|
+
return (await boundWorkspaces())?.workspaces ?? null;
|
|
4181
|
+
}
|
|
4182
|
+
async function servedWorkspaces2() {
|
|
4183
|
+
const bound = await boundWorkspaces2();
|
|
4184
|
+
return new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
|
|
3910
4185
|
}
|
|
3911
4186
|
async function reconcileProfile(profile) {
|
|
3912
4187
|
return runWithProfile(profile, async () => {
|
|
@@ -3933,8 +4208,7 @@ async function reconcileProfile(profile) {
|
|
|
3933
4208
|
});
|
|
3934
4209
|
}
|
|
3935
4210
|
async function reconcileAll() {
|
|
3936
|
-
const
|
|
3937
|
-
const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
|
|
4211
|
+
const slugs = await servedWorkspaces2();
|
|
3938
4212
|
const results = /* @__PURE__ */ new Map();
|
|
3939
4213
|
for (const slug of slugs) {
|
|
3940
4214
|
try {
|
|
@@ -3947,8 +4221,7 @@ async function reconcileAll() {
|
|
|
3947
4221
|
return results;
|
|
3948
4222
|
}
|
|
3949
4223
|
async function ensureServices() {
|
|
3950
|
-
const
|
|
3951
|
-
const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
|
|
4224
|
+
const slugs = await servedWorkspaces2();
|
|
3952
4225
|
slugs.delete(DEFAULT_PROFILE);
|
|
3953
4226
|
const installed = [];
|
|
3954
4227
|
for (const slug of slugs) {
|
|
@@ -4097,10 +4370,10 @@ var requireDocsAccess = async (c, next) => {
|
|
|
4097
4370
|
};
|
|
4098
4371
|
|
|
4099
4372
|
// src/dashboard/api.ts
|
|
4100
|
-
import { Hono as
|
|
4373
|
+
import { Hono as Hono30 } from "hono";
|
|
4101
4374
|
|
|
4102
4375
|
// src/dashboard/public-share-routes.ts
|
|
4103
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4376
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
4104
4377
|
import { join as join12 } from "path";
|
|
4105
4378
|
import { Hono as Hono3 } from "hono";
|
|
4106
4379
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
@@ -4241,15 +4514,15 @@ async function createDocShare(input2) {
|
|
|
4241
4514
|
if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
|
|
4242
4515
|
return rowToShare(data);
|
|
4243
4516
|
}
|
|
4244
|
-
async function updateDocShareSnapshot(id,
|
|
4517
|
+
async function updateDocShareSnapshot(id, snapshot3, features) {
|
|
4245
4518
|
const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
|
|
4246
|
-
title:
|
|
4247
|
-
content:
|
|
4248
|
-
asset_keys:
|
|
4519
|
+
title: snapshot3.title,
|
|
4520
|
+
content: snapshot3.content,
|
|
4521
|
+
asset_keys: snapshot3.assetKeys,
|
|
4249
4522
|
// Re-detected with the snapshot: a re-publish can carry a rewritten
|
|
4250
4523
|
// article, and one written in another language than last time is exactly
|
|
4251
4524
|
// the case a stale value would get wrong.
|
|
4252
|
-
source_lang:
|
|
4525
|
+
source_lang: snapshot3.sourceLang,
|
|
4253
4526
|
audio_enabled: features.audioEnabled,
|
|
4254
4527
|
ask_enabled: features.askEnabled,
|
|
4255
4528
|
social_enabled: features.socialEnabled,
|
|
@@ -4566,7 +4839,7 @@ function injectShareHead(html, head) {
|
|
|
4566
4839
|
|
|
4567
4840
|
// src/dashboard/app-icon.ts
|
|
4568
4841
|
import { createHash as createHash2 } from "crypto";
|
|
4569
|
-
import { readFileSync as
|
|
4842
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
4570
4843
|
import { join as join11 } from "path";
|
|
4571
4844
|
import sharp from "sharp";
|
|
4572
4845
|
var PLATE = "#0A0A0F";
|
|
@@ -4594,7 +4867,7 @@ function roundedPlate(size, fill = PLATE) {
|
|
|
4594
4867
|
}
|
|
4595
4868
|
async function renderBadge(box) {
|
|
4596
4869
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
4597
|
-
const raster = await sharp(
|
|
4870
|
+
const raster = await sharp(readFileSync7(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4598
4871
|
return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4599
4872
|
}
|
|
4600
4873
|
async function sampleBackground(logo) {
|
|
@@ -4649,7 +4922,7 @@ async function renderAppIcon({
|
|
|
4649
4922
|
if (hit) return hit;
|
|
4650
4923
|
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,
|
|
4651
4924
|
// either on its usual dark plate or, for a favicon, trimmed and bare.
|
|
4652
|
-
(transparent ? await renderBadge(size) : await sharp(
|
|
4925
|
+
(transparent ? await renderBadge(size) : await sharp(readFileSync7(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
|
|
4653
4926
|
cache3.set(key, out);
|
|
4654
4927
|
return out;
|
|
4655
4928
|
}
|
|
@@ -5867,7 +6140,7 @@ var shellCache = null;
|
|
|
5867
6140
|
function readShell(path) {
|
|
5868
6141
|
const { mtimeMs } = statSync3(path);
|
|
5869
6142
|
if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
|
|
5870
|
-
const html =
|
|
6143
|
+
const html = readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
|
|
5871
6144
|
shellCache = { mtimeMs, html };
|
|
5872
6145
|
return html;
|
|
5873
6146
|
}
|
|
@@ -5892,12 +6165,12 @@ function serveBuildFile(c, webDist, dir, name) {
|
|
|
5892
6165
|
c.header("Content-Type", type);
|
|
5893
6166
|
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
5894
6167
|
if (ext === ".css") {
|
|
5895
|
-
return c.body(
|
|
6168
|
+
return c.body(readFileSync8(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
|
|
5896
6169
|
}
|
|
5897
6170
|
if (ext === ".js") {
|
|
5898
|
-
return c.body(
|
|
6171
|
+
return c.body(readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
|
|
5899
6172
|
}
|
|
5900
|
-
return c.body(new Uint8Array(
|
|
6173
|
+
return c.body(new Uint8Array(readFileSync8(path)));
|
|
5901
6174
|
}
|
|
5902
6175
|
var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
|
|
5903
6176
|
function servePublishedHtml(c, share) {
|
|
@@ -6659,8 +6932,48 @@ function createAdminRouter() {
|
|
|
6659
6932
|
return admin;
|
|
6660
6933
|
}
|
|
6661
6934
|
|
|
6935
|
+
// src/dashboard/agent-identities.ts
|
|
6936
|
+
var BOT_LABEL = "Workspace Agent";
|
|
6937
|
+
var BOT_DM_KEY = "claude";
|
|
6938
|
+
async function resolveAgentIdentities() {
|
|
6939
|
+
const settings = loadDashboardSettings();
|
|
6940
|
+
const { cwd: defaultCwd } = resolveChannelConfig("");
|
|
6941
|
+
const main2 = [
|
|
6942
|
+
{
|
|
6943
|
+
name: BOT_DM_KEY,
|
|
6944
|
+
label: BOT_LABEL,
|
|
6945
|
+
kind: "main",
|
|
6946
|
+
cwd: defaultCwd,
|
|
6947
|
+
model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
|
|
6948
|
+
...await loadBotIdentity()
|
|
6949
|
+
}
|
|
6950
|
+
];
|
|
6951
|
+
return Promise.all(
|
|
6952
|
+
main2.map(async ({ cwd, ...agent }) => {
|
|
6953
|
+
const dm = await ensureAgentDmChannel(agent.name, {
|
|
6954
|
+
name: agent.displayName ?? agent.label,
|
|
6955
|
+
cwd
|
|
6956
|
+
});
|
|
6957
|
+
return { ...agent, channelId: dm.id };
|
|
6958
|
+
})
|
|
6959
|
+
);
|
|
6960
|
+
}
|
|
6961
|
+
async function offlineAgentIdentities() {
|
|
6962
|
+
const dm = findChannelByAgentName(BOT_DM_KEY);
|
|
6963
|
+
if (!dm) return [];
|
|
6964
|
+
return [
|
|
6965
|
+
{
|
|
6966
|
+
name: BOT_DM_KEY,
|
|
6967
|
+
label: BOT_LABEL,
|
|
6968
|
+
kind: "main",
|
|
6969
|
+
channelId: dm.id,
|
|
6970
|
+
...await loadBotIdentity()
|
|
6971
|
+
}
|
|
6972
|
+
];
|
|
6973
|
+
}
|
|
6974
|
+
|
|
6662
6975
|
// src/dashboard/host-api.ts
|
|
6663
|
-
import { existsSync as existsSync21, readFileSync as
|
|
6976
|
+
import { existsSync as existsSync21, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
|
|
6664
6977
|
import { join as join19 } from "path";
|
|
6665
6978
|
import sharp3 from "sharp";
|
|
6666
6979
|
|
|
@@ -6899,8 +7212,8 @@ function createChannelWriteRouter() {
|
|
|
6899
7212
|
(id, position) => supabase.from("channels").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id)
|
|
6900
7213
|
)
|
|
6901
7214
|
);
|
|
6902
|
-
const
|
|
6903
|
-
if (
|
|
7215
|
+
const failed2 = results.find((r) => r.error);
|
|
7216
|
+
if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
|
|
6904
7217
|
await refreshChannelRegistry();
|
|
6905
7218
|
return c.json({ ok: true });
|
|
6906
7219
|
});
|
|
@@ -6958,10 +7271,10 @@ import {
|
|
|
6958
7271
|
openSync,
|
|
6959
7272
|
readSync,
|
|
6960
7273
|
readdirSync as readdirSync4,
|
|
6961
|
-
readFileSync as
|
|
6962
|
-
realpathSync,
|
|
7274
|
+
readFileSync as readFileSync11,
|
|
7275
|
+
realpathSync as realpathSync2,
|
|
6963
7276
|
renameSync,
|
|
6964
|
-
rmSync as
|
|
7277
|
+
rmSync as rmSync5,
|
|
6965
7278
|
statSync as statSync5,
|
|
6966
7279
|
writeFileSync as writeFileSync7
|
|
6967
7280
|
} from "fs";
|
|
@@ -7249,14 +7562,14 @@ async function readOrQueueDocAudio(input2) {
|
|
|
7249
7562
|
}
|
|
7250
7563
|
|
|
7251
7564
|
// src/dashboard/artifact-export.ts
|
|
7252
|
-
import { readFileSync as
|
|
7565
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7253
7566
|
|
|
7254
7567
|
// src/dashboard/chat-agent.ts
|
|
7255
7568
|
import { homedir as homedir7 } from "os";
|
|
7256
7569
|
|
|
7257
7570
|
// src/mcp/config.ts
|
|
7258
|
-
import { mkdtempSync, rmSync as
|
|
7259
|
-
import { tmpdir } from "os";
|
|
7571
|
+
import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
7572
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
7260
7573
|
import { join as join14 } from "path";
|
|
7261
7574
|
|
|
7262
7575
|
// src/mcp/crypto.ts
|
|
@@ -7943,12 +8256,12 @@ async function buildMcpConfig(profile, connectionIds) {
|
|
|
7943
8256
|
return { config, skipped };
|
|
7944
8257
|
}
|
|
7945
8258
|
function writeMcpConfigFile(config) {
|
|
7946
|
-
const dir = mkdtempSync(join14(
|
|
8259
|
+
const dir = mkdtempSync(join14(tmpdir2(), "flyspace-mcp-"));
|
|
7947
8260
|
const path = join14(dir, "mcp-config.json");
|
|
7948
8261
|
writeFileSync6(path, JSON.stringify(config), { mode: 384 });
|
|
7949
8262
|
return {
|
|
7950
8263
|
path,
|
|
7951
|
-
cleanup: () =>
|
|
8264
|
+
cleanup: () => rmSync4(dir, { recursive: true, force: true })
|
|
7952
8265
|
};
|
|
7953
8266
|
}
|
|
7954
8267
|
async function withMcpConfig(profile, connectionIds, fn) {
|
|
@@ -7965,7 +8278,7 @@ async function withMcpConfig(profile, connectionIds, fn) {
|
|
|
7965
8278
|
}
|
|
7966
8279
|
|
|
7967
8280
|
// src/claude/agents.ts
|
|
7968
|
-
import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as
|
|
8281
|
+
import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
7969
8282
|
import { homedir as homedir6 } from "os";
|
|
7970
8283
|
import { basename as basename4, join as join15 } from "path";
|
|
7971
8284
|
import yaml from "js-yaml";
|
|
@@ -7980,7 +8293,7 @@ function expandHome(p) {
|
|
|
7980
8293
|
function parseFrontmatter(file) {
|
|
7981
8294
|
let content;
|
|
7982
8295
|
try {
|
|
7983
|
-
content =
|
|
8296
|
+
content = readFileSync9(file, "utf-8");
|
|
7984
8297
|
} catch {
|
|
7985
8298
|
return {};
|
|
7986
8299
|
}
|
|
@@ -8026,16 +8339,241 @@ function readAgentPersona(configDir2, cwd, name) {
|
|
|
8026
8339
|
const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
|
|
8027
8340
|
if (!agent) return void 0;
|
|
8028
8341
|
try {
|
|
8029
|
-
const content =
|
|
8342
|
+
const content = readFileSync9(agent.path, "utf-8");
|
|
8030
8343
|
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
|
|
8031
8344
|
} catch {
|
|
8032
8345
|
return void 0;
|
|
8033
8346
|
}
|
|
8034
8347
|
}
|
|
8035
8348
|
|
|
8349
|
+
// src/store/memory-store.ts
|
|
8350
|
+
var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
|
|
8351
|
+
var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
8352
|
+
var DESCRIPTION_MAX = 200;
|
|
8353
|
+
var BODY_MAX = 4e3;
|
|
8354
|
+
var MEMORY_INDEX_MAX_CHARS = 8e3;
|
|
8355
|
+
function isMemoryType(value) {
|
|
8356
|
+
return MEMORY_TYPES.includes(value);
|
|
8357
|
+
}
|
|
8358
|
+
function validateMemoryInput(input2) {
|
|
8359
|
+
if (!NAME_RE.test(input2.name))
|
|
8360
|
+
throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
|
|
8361
|
+
if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
|
|
8362
|
+
const description = input2.description.trim();
|
|
8363
|
+
if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
|
|
8364
|
+
throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
|
|
8365
|
+
}
|
|
8366
|
+
const body = input2.body.trim();
|
|
8367
|
+
if (!body || body.length > BODY_MAX) {
|
|
8368
|
+
throw new Error(`body must be 1 to ${BODY_MAX} characters`);
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
function rowToMemory(row) {
|
|
8372
|
+
return {
|
|
8373
|
+
id: row["id"],
|
|
8374
|
+
channelId: row["channel_id"],
|
|
8375
|
+
name: row["name"],
|
|
8376
|
+
description: row["description"],
|
|
8377
|
+
type: row["type"],
|
|
8378
|
+
body: row["body"],
|
|
8379
|
+
sourceThreadId: row["source_thread_id"] ?? null,
|
|
8380
|
+
createdBy: row["created_by"],
|
|
8381
|
+
createdAt: row["created_at"],
|
|
8382
|
+
updatedAt: row["updated_at"]
|
|
8383
|
+
};
|
|
8384
|
+
}
|
|
8385
|
+
async function listChannelMemories(channelId) {
|
|
8386
|
+
const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
|
|
8387
|
+
if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
|
|
8388
|
+
return (data ?? []).map(rowToMemory);
|
|
8389
|
+
}
|
|
8390
|
+
async function getChannelMemory(channelId, name) {
|
|
8391
|
+
const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
|
|
8392
|
+
if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
|
|
8393
|
+
return data ? rowToMemory(data) : null;
|
|
8394
|
+
}
|
|
8395
|
+
async function saveChannelMemory(input2, actor) {
|
|
8396
|
+
validateMemoryInput(input2);
|
|
8397
|
+
const supabase = getSupabaseClient();
|
|
8398
|
+
const existing = await getChannelMemory(input2.channelId, input2.name);
|
|
8399
|
+
const fields = {
|
|
8400
|
+
description: input2.description.trim(),
|
|
8401
|
+
type: input2.type,
|
|
8402
|
+
body: input2.body.trim(),
|
|
8403
|
+
source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
|
|
8404
|
+
created_by: input2.createdBy
|
|
8405
|
+
};
|
|
8406
|
+
if (existing && sameContent(existing, fields)) {
|
|
8407
|
+
return { memory: existing, created: false, changed: false };
|
|
8408
|
+
}
|
|
8409
|
+
const { data, error: error2 } = existing ? await supabase.from("memories").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select("*").single() : await supabase.from("memories").insert({ ...fields, scope: "channel", channel_id: input2.channelId, name: input2.name }).select("*").single();
|
|
8410
|
+
if (error2)
|
|
8411
|
+
throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
|
|
8412
|
+
const memory = rowToMemory(data);
|
|
8413
|
+
await logMemoryEvent({
|
|
8414
|
+
memory,
|
|
8415
|
+
action: existing ? "update" : "create",
|
|
8416
|
+
before: existing ? snapshot(existing) : null,
|
|
8417
|
+
after: snapshot(memory),
|
|
8418
|
+
actor,
|
|
8419
|
+
threadId: input2.sourceThreadId ?? null
|
|
8420
|
+
});
|
|
8421
|
+
return { memory, created: !existing, changed: true };
|
|
8422
|
+
}
|
|
8423
|
+
async function updateChannelMemory(channelId, memoryId, patch, actor) {
|
|
8424
|
+
const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
|
|
8425
|
+
if (!existing) return null;
|
|
8426
|
+
const { memory } = await saveChannelMemory(
|
|
8427
|
+
{
|
|
8428
|
+
channelId,
|
|
8429
|
+
name: existing.name,
|
|
8430
|
+
description: patch.description ?? existing.description,
|
|
8431
|
+
type: patch.type ?? existing.type,
|
|
8432
|
+
body: patch.body ?? existing.body,
|
|
8433
|
+
createdBy: existing.createdBy
|
|
8434
|
+
},
|
|
8435
|
+
actor
|
|
8436
|
+
);
|
|
8437
|
+
return memory;
|
|
8438
|
+
}
|
|
8439
|
+
async function searchChannelMemories(channelId, query) {
|
|
8440
|
+
const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
|
|
8441
|
+
if (!words.length) return [];
|
|
8442
|
+
let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
|
|
8443
|
+
for (const word of words) {
|
|
8444
|
+
request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
|
|
8445
|
+
}
|
|
8446
|
+
const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
|
|
8447
|
+
if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
|
|
8448
|
+
return (data ?? []).map(rowToMemory);
|
|
8449
|
+
}
|
|
8450
|
+
async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
|
|
8451
|
+
const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
|
|
8452
|
+
if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
|
|
8453
|
+
const deleted = (data ?? []).map(rowToMemory);
|
|
8454
|
+
for (const memory of deleted) {
|
|
8455
|
+
await logMemoryEvent({
|
|
8456
|
+
memory,
|
|
8457
|
+
action: "delete",
|
|
8458
|
+
before: snapshot(memory),
|
|
8459
|
+
after: null,
|
|
8460
|
+
actor,
|
|
8461
|
+
threadId: threadId ?? null
|
|
8462
|
+
});
|
|
8463
|
+
}
|
|
8464
|
+
return deleted.length > 0;
|
|
8465
|
+
}
|
|
8466
|
+
function snapshot(m) {
|
|
8467
|
+
return {
|
|
8468
|
+
description: m.description,
|
|
8469
|
+
type: m.type,
|
|
8470
|
+
body: m.body,
|
|
8471
|
+
sourceThreadId: m.sourceThreadId,
|
|
8472
|
+
createdBy: m.createdBy
|
|
8473
|
+
};
|
|
8474
|
+
}
|
|
8475
|
+
function sameContent(m, other) {
|
|
8476
|
+
return m.description === other.description && m.type === other.type && m.body === other.body;
|
|
8477
|
+
}
|
|
8478
|
+
async function logMemoryEvent(e) {
|
|
8479
|
+
const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
|
|
8480
|
+
channel_id: e.memory.channelId,
|
|
8481
|
+
memory_id: e.action === "delete" ? null : e.memory.id,
|
|
8482
|
+
thread_id: e.threadId,
|
|
8483
|
+
name: e.memory.name,
|
|
8484
|
+
action: e.action,
|
|
8485
|
+
before: e.before,
|
|
8486
|
+
after: e.after,
|
|
8487
|
+
actor: e.actor
|
|
8488
|
+
});
|
|
8489
|
+
if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
|
|
8490
|
+
}
|
|
8491
|
+
function rowToEvent(row) {
|
|
8492
|
+
return {
|
|
8493
|
+
id: row["id"],
|
|
8494
|
+
channelId: row["channel_id"],
|
|
8495
|
+
memoryId: row["memory_id"] ?? null,
|
|
8496
|
+
threadId: row["thread_id"] ?? null,
|
|
8497
|
+
name: row["name"],
|
|
8498
|
+
action: row["action"],
|
|
8499
|
+
before: row["before"] ?? null,
|
|
8500
|
+
after: row["after"] ?? null,
|
|
8501
|
+
actor: row["actor"],
|
|
8502
|
+
createdAt: row["created_at"],
|
|
8503
|
+
undoneAt: row["undone_at"] ?? null
|
|
8504
|
+
};
|
|
8505
|
+
}
|
|
8506
|
+
async function listThreadMemoryEvents(channelId, threadId) {
|
|
8507
|
+
const { data, error: error2 } = await getSupabaseClient().from("memory_events").select("*").eq("channel_id", channelId).eq("thread_id", threadId).order("created_at", { ascending: false }).limit(50);
|
|
8508
|
+
if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
|
|
8509
|
+
return (data ?? []).map(rowToEvent);
|
|
8510
|
+
}
|
|
8511
|
+
async function undoMemoryEvent(channelId, eventId) {
|
|
8512
|
+
const supabase = getSupabaseClient();
|
|
8513
|
+
const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
|
|
8514
|
+
if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
|
|
8515
|
+
if (!data) return "not-found";
|
|
8516
|
+
const event = rowToEvent(data);
|
|
8517
|
+
if (event.undoneAt) return "already-undone";
|
|
8518
|
+
const current = await getChannelMemory(channelId, event.name);
|
|
8519
|
+
const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
|
|
8520
|
+
if (!unchangedSince) return "conflict";
|
|
8521
|
+
const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
|
|
8522
|
+
if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
|
|
8523
|
+
if (!(claimed.data ?? []).length) return "already-undone";
|
|
8524
|
+
const before = event.before;
|
|
8525
|
+
const restore = before ? {
|
|
8526
|
+
description: before.description,
|
|
8527
|
+
type: before.type,
|
|
8528
|
+
body: before.body,
|
|
8529
|
+
source_thread_id: before.sourceThreadId,
|
|
8530
|
+
created_by: before.createdBy
|
|
8531
|
+
} : null;
|
|
8532
|
+
const { error: writeError } = !restore ? await supabase.from("memories").delete().eq("id", current.id) : current ? await supabase.from("memories").update({ ...restore, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", current.id) : await supabase.from("memories").insert({ ...restore, scope: "channel", channel_id: channelId, name: event.name });
|
|
8533
|
+
if (writeError) {
|
|
8534
|
+
await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
|
|
8535
|
+
throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
|
|
8536
|
+
}
|
|
8537
|
+
return "undone";
|
|
8538
|
+
}
|
|
8539
|
+
function isUuid(value) {
|
|
8540
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
8541
|
+
}
|
|
8542
|
+
function formatMemoryPrompt(channelId, memories) {
|
|
8543
|
+
const lines = [];
|
|
8544
|
+
let used = 0;
|
|
8545
|
+
let omitted = 0;
|
|
8546
|
+
for (const m of memories) {
|
|
8547
|
+
const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
|
|
8548
|
+
if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
|
|
8549
|
+
omitted++;
|
|
8550
|
+
continue;
|
|
8551
|
+
}
|
|
8552
|
+
lines.push(line);
|
|
8553
|
+
used += line.length + 1;
|
|
8554
|
+
}
|
|
8555
|
+
const index = lines.length ? lines.join("\n") + (omitted ? `
|
|
8556
|
+
- (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
|
|
8557
|
+
return [
|
|
8558
|
+
`## Channel memory`,
|
|
8559
|
+
``,
|
|
8560
|
+
`This channel has a persistent memory shared by every thread in it and by every agent runner. Use it instead of any built-in memory directory of your own CLI.`,
|
|
8561
|
+
``,
|
|
8562
|
+
`- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
|
|
8563
|
+
`- Search: \`flyspace memory search --channel ${channelId} <words>\``,
|
|
8564
|
+
`- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
|
|
8565
|
+
`- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
|
|
8566
|
+
``,
|
|
8567
|
+
`Save when someone asks you to remember something, corrects you, or a decision is made that later threads will need. Save what is not derivable from the code, git history or docs: decisions and why, preferences, corrections, where external things live. Do not save one-off details of this conversation. Check the index first and update an existing memory rather than adding a near-duplicate. Write memories in English. Pass \`--thread\` with the thread id from the \`[context: ...]\` header, so the thread shows what changed and it can be undone there.`,
|
|
8568
|
+
``,
|
|
8569
|
+
`Index:`,
|
|
8570
|
+
index
|
|
8571
|
+
].join("\n");
|
|
8572
|
+
}
|
|
8573
|
+
|
|
8036
8574
|
// src/dashboard/rollover-summary.ts
|
|
8037
8575
|
import { spawn as spawn5 } from "child_process";
|
|
8038
|
-
import { tmpdir as
|
|
8576
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
8039
8577
|
var HAIKU_MODEL = "haiku";
|
|
8040
8578
|
var CJK_PATTERN = /[ -〿-ヿ㐀-䶿一-鿿豈--]/;
|
|
8041
8579
|
function estimateTokens(text) {
|
|
@@ -8098,7 +8636,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
|
|
|
8098
8636
|
// A stateless rewrite touches no files — tmpdir keeps it that way and
|
|
8099
8637
|
// avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
|
|
8100
8638
|
// unlike the channel's own cwd the main turn runs in.
|
|
8101
|
-
cwd:
|
|
8639
|
+
cwd: tmpdir3(),
|
|
8102
8640
|
env,
|
|
8103
8641
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8104
8642
|
});
|
|
@@ -8894,6 +9432,13 @@ var ChatAgent = class {
|
|
|
8894
9432
|
thread.channel_id
|
|
8895
9433
|
);
|
|
8896
9434
|
if (docsAccess && docsAccess !== "write") channelConfig = { ...channelConfig, docsAccess };
|
|
9435
|
+
if (docsAccess !== "none" && isUuid(thread.channel_id)) {
|
|
9436
|
+
const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
|
|
9437
|
+
console.error("[chat-agent] memory lookup failed:", err.message);
|
|
9438
|
+
return void 0;
|
|
9439
|
+
});
|
|
9440
|
+
if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
|
|
9441
|
+
}
|
|
8897
9442
|
const store = this.openSessionStore();
|
|
8898
9443
|
try {
|
|
8899
9444
|
const channelKey = `chat:${thread.channel_id}`;
|
|
@@ -9301,7 +9846,8 @@ ${msg.body}` : msg.body;
|
|
|
9301
9846
|
signal,
|
|
9302
9847
|
mcpConfigPath,
|
|
9303
9848
|
...usageContext ? { usageContext } : {},
|
|
9304
|
-
...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {}
|
|
9849
|
+
...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {},
|
|
9850
|
+
...channelConfig.memoryPrompt ? { memoryPrompt: channelConfig.memoryPrompt } : {}
|
|
9305
9851
|
},
|
|
9306
9852
|
channelConfig.claudeConfigDir,
|
|
9307
9853
|
sessionId,
|
|
@@ -9338,6 +9884,7 @@ ${msg.body}` : msg.body;
|
|
|
9338
9884
|
channelConfig.prompt,
|
|
9339
9885
|
resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
|
|
9340
9886
|
),
|
|
9887
|
+
channelConfig.memoryPrompt,
|
|
9341
9888
|
docsAccessPromptNote(channelConfig.docsAccess)
|
|
9342
9889
|
].filter(Boolean).join("\n\n---\n\n"),
|
|
9343
9890
|
// Codex has no per-tool deny list, so any restricted requester gets
|
|
@@ -9615,7 +10162,7 @@ async function exportDocAsArtifact(channelId, docPath) {
|
|
|
9615
10162
|
);
|
|
9616
10163
|
const abs = resolveDocPath(channelConfig.cwd, docPath);
|
|
9617
10164
|
if (!abs) throw new Error(`doc not found: ${docPath}`);
|
|
9618
|
-
const content =
|
|
10165
|
+
const content = readFileSync10(abs, "utf8");
|
|
9619
10166
|
const title = extractTitle(abs, "md");
|
|
9620
10167
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
9621
10168
|
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).
|
|
@@ -9894,12 +10441,12 @@ function readGitBranch(dir) {
|
|
|
9894
10441
|
try {
|
|
9895
10442
|
let gitDir = join17(dir, ".git");
|
|
9896
10443
|
if (statSync5(gitDir).isFile()) {
|
|
9897
|
-
const pointer =
|
|
10444
|
+
const pointer = readFileSync11(gitDir, "utf8").trim();
|
|
9898
10445
|
const match = pointer.match(/^gitdir:\s*(.+)$/);
|
|
9899
10446
|
if (!match) return void 0;
|
|
9900
10447
|
gitDir = resolve5(dir, match[1]);
|
|
9901
10448
|
}
|
|
9902
|
-
const head =
|
|
10449
|
+
const head = readFileSync11(join17(gitDir, "HEAD"), "utf8").trim();
|
|
9903
10450
|
const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
9904
10451
|
if (branchMatch) return branchMatch[1];
|
|
9905
10452
|
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
|
|
@@ -9926,8 +10473,8 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
|
|
|
9926
10473
|
const kind = entryKind(entry, abs);
|
|
9927
10474
|
if (kind === "dir") {
|
|
9928
10475
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
9929
|
-
const
|
|
9930
|
-
if (!
|
|
10476
|
+
const probe3 = probeDir(abs, showHidden);
|
|
10477
|
+
if (!probe3.hasDocs && probe3.hasOther) continue;
|
|
9931
10478
|
const isRepo = existsSync17(join17(abs, ".git"));
|
|
9932
10479
|
dirs.push({
|
|
9933
10480
|
path,
|
|
@@ -10146,7 +10693,7 @@ function moveDocEntry(root, from, to) {
|
|
|
10146
10693
|
return { status: "into-self" };
|
|
10147
10694
|
}
|
|
10148
10695
|
try {
|
|
10149
|
-
if (
|
|
10696
|
+
if (realpathSync2(finalAbs) !== realpathSync2(absFrom)) return { status: "exists" };
|
|
10150
10697
|
} catch {
|
|
10151
10698
|
}
|
|
10152
10699
|
renameSync(absFrom, finalAbs);
|
|
@@ -10164,7 +10711,7 @@ function deleteDocEntry(root, path) {
|
|
|
10164
10711
|
}
|
|
10165
10712
|
const isDir2 = stat.isDirectory();
|
|
10166
10713
|
if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
|
|
10167
|
-
|
|
10714
|
+
rmSync5(abs, { recursive: isDir2 });
|
|
10168
10715
|
return {
|
|
10169
10716
|
status: "ok",
|
|
10170
10717
|
path: relative(root, abs).split(sep).join("/"),
|
|
@@ -10203,7 +10750,7 @@ function createDocsRouter() {
|
|
|
10203
10750
|
}
|
|
10204
10751
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10205
10752
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10206
|
-
const data =
|
|
10753
|
+
const data = readFileSync11(resolved2.abs);
|
|
10207
10754
|
const headers = { "Content-Type": resolved2.contentType };
|
|
10208
10755
|
if (download) {
|
|
10209
10756
|
const filename = basename5(resolved2.abs).replace(/"/g, "");
|
|
@@ -10223,7 +10770,7 @@ function createDocsRouter() {
|
|
|
10223
10770
|
}
|
|
10224
10771
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10225
10772
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10226
|
-
return c.body(new Uint8Array(
|
|
10773
|
+
return c.body(new Uint8Array(readFileSync11(abs)), 200, {
|
|
10227
10774
|
"Content-Type": "text/html; charset=utf-8",
|
|
10228
10775
|
"Content-Security-Policy": HTML_SHARE_CSP,
|
|
10229
10776
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -10239,10 +10786,10 @@ function createDocsRouter() {
|
|
|
10239
10786
|
const files = form.getAll("file").filter((f) => f instanceof File);
|
|
10240
10787
|
if (files.length === 0) return c.json({ error: "missing file" }, 400);
|
|
10241
10788
|
const uploaded = [];
|
|
10242
|
-
const
|
|
10789
|
+
const failed2 = [];
|
|
10243
10790
|
for (const file of files) {
|
|
10244
10791
|
if (file.size > MAX_UPLOAD_BYTES) {
|
|
10245
|
-
|
|
10792
|
+
failed2.push({
|
|
10246
10793
|
name: file.name,
|
|
10247
10794
|
error: `larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)}MB`
|
|
10248
10795
|
});
|
|
@@ -10256,16 +10803,16 @@ function createDocsRouter() {
|
|
|
10256
10803
|
case "no-dir":
|
|
10257
10804
|
return c.json({ error: "folder not found" }, 404);
|
|
10258
10805
|
case "bad-type":
|
|
10259
|
-
|
|
10806
|
+
failed2.push({ name: file.name, error: "file type not allowed" });
|
|
10260
10807
|
break;
|
|
10261
10808
|
case "too-large":
|
|
10262
|
-
|
|
10809
|
+
failed2.push({ name: file.name, error: "file too large" });
|
|
10263
10810
|
break;
|
|
10264
10811
|
default:
|
|
10265
|
-
|
|
10812
|
+
failed2.push({ name: file.name, error: "invalid filename" });
|
|
10266
10813
|
}
|
|
10267
10814
|
}
|
|
10268
|
-
return c.json({ path: dir, uploaded, failed }, uploaded.length > 0 ? 201 : 400);
|
|
10815
|
+
return c.json({ path: dir, uploaded, failed: failed2 }, uploaded.length > 0 ? 201 : 400);
|
|
10269
10816
|
});
|
|
10270
10817
|
app.post("/:id/docs/folder", async (c) => {
|
|
10271
10818
|
const { cwd } = resolveChannelConfig(c.req.param("id"));
|
|
@@ -10408,7 +10955,7 @@ function createDocsRouter() {
|
|
|
10408
10955
|
title: kind ? extractTitle(abs, kind) : basename5(abs),
|
|
10409
10956
|
mtime: stat.mtime.toISOString(),
|
|
10410
10957
|
tooLarge: tooLarge || void 0,
|
|
10411
|
-
content: tooLarge ? "" :
|
|
10958
|
+
content: tooLarge ? "" : readFileSync11(abs, "utf8")
|
|
10412
10959
|
});
|
|
10413
10960
|
});
|
|
10414
10961
|
app.put("/:id/docs/content", async (c) => {
|
|
@@ -10451,7 +10998,7 @@ function createDocsRouter() {
|
|
|
10451
10998
|
channel,
|
|
10452
10999
|
rel,
|
|
10453
11000
|
title: extractTitle(abs, "md"),
|
|
10454
|
-
content:
|
|
11001
|
+
content: readFileSync11(abs, "utf8")
|
|
10455
11002
|
};
|
|
10456
11003
|
};
|
|
10457
11004
|
app.get("/:id/docs/translation/:lang", async (c) => {
|
|
@@ -10674,7 +11221,7 @@ function ownSkills(scope, dir) {
|
|
|
10674
11221
|
return [{ scope, name, description, body, path, plugin: null, mtime }];
|
|
10675
11222
|
});
|
|
10676
11223
|
}
|
|
10677
|
-
function
|
|
11224
|
+
function snapshot2(scopes) {
|
|
10678
11225
|
const wants = (scope) => !scopes || scopes.has(scope);
|
|
10679
11226
|
const configDir2 = profileClaudeConfigDir();
|
|
10680
11227
|
const rows = [];
|
|
@@ -10709,7 +11256,7 @@ async function pushSkillScopes(scopes) {
|
|
|
10709
11256
|
if (!mirrorEnabled()) return;
|
|
10710
11257
|
const workspace = getProfile();
|
|
10711
11258
|
const supabase = getSupabaseClient();
|
|
10712
|
-
const rows =
|
|
11259
|
+
const rows = snapshot2(scopes ? new Set(scopes) : void 0);
|
|
10713
11260
|
let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
|
|
10714
11261
|
if (scopes) query = query.in("scope", scopes);
|
|
10715
11262
|
const { data, error: error2 } = await query;
|
|
@@ -10989,32 +11536,6 @@ function registerMirroredSkillRoutes(api, agent) {
|
|
|
10989
11536
|
mountSkillRoutes(api, "/channels/:id/skill-files", channelRef, backend);
|
|
10990
11537
|
}
|
|
10991
11538
|
|
|
10992
|
-
// src/dashboard/agent-identities.ts
|
|
10993
|
-
var BOT_DM_KEY = "claude";
|
|
10994
|
-
async function resolveAgentIdentities() {
|
|
10995
|
-
const settings = loadDashboardSettings();
|
|
10996
|
-
const { cwd: defaultCwd } = resolveChannelConfig("");
|
|
10997
|
-
const main2 = [
|
|
10998
|
-
{
|
|
10999
|
-
name: BOT_DM_KEY,
|
|
11000
|
-
label: "Workspace Agent",
|
|
11001
|
-
kind: "main",
|
|
11002
|
-
cwd: defaultCwd,
|
|
11003
|
-
model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
|
|
11004
|
-
...await loadBotIdentity()
|
|
11005
|
-
}
|
|
11006
|
-
];
|
|
11007
|
-
return Promise.all(
|
|
11008
|
-
main2.map(async ({ cwd, ...agent }) => {
|
|
11009
|
-
const dm = await ensureAgentDmChannel(agent.name, {
|
|
11010
|
-
name: agent.displayName ?? agent.label,
|
|
11011
|
-
cwd
|
|
11012
|
-
});
|
|
11013
|
-
return { ...agent, channelId: dm.id };
|
|
11014
|
-
})
|
|
11015
|
-
);
|
|
11016
|
-
}
|
|
11017
|
-
|
|
11018
11539
|
// src/dashboard/providers.ts
|
|
11019
11540
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
11020
11541
|
import { existsSync as existsSync20 } from "fs";
|
|
@@ -11048,7 +11569,7 @@ function claudeAuthAvailable() {
|
|
|
11048
11569
|
return false;
|
|
11049
11570
|
}
|
|
11050
11571
|
}
|
|
11051
|
-
function
|
|
11572
|
+
function probe2(runner) {
|
|
11052
11573
|
const label2 = PROVIDER_LABELS[runner];
|
|
11053
11574
|
const bin = runner === "codex" ? "codex" : "claude";
|
|
11054
11575
|
if (!binaryOnPath(bin)) {
|
|
@@ -11069,7 +11590,7 @@ function providerStatuses() {
|
|
|
11069
11590
|
const key = getProfile();
|
|
11070
11591
|
const hit = cache6.get(key);
|
|
11071
11592
|
if (hit && Date.now() - hit.at < TTL_MS2) return hit.statuses;
|
|
11072
|
-
const statuses = ["claude", "codex"].map(
|
|
11593
|
+
const statuses = ["claude", "codex"].map(probe2);
|
|
11073
11594
|
cache6.set(key, { at: Date.now(), statuses });
|
|
11074
11595
|
return statuses;
|
|
11075
11596
|
}
|
|
@@ -11192,7 +11713,7 @@ function registerHostRoutes(api) {
|
|
|
11192
11713
|
}
|
|
11193
11714
|
function tailFile(file, limit) {
|
|
11194
11715
|
if (!existsSync21(file)) return [];
|
|
11195
|
-
const lines =
|
|
11716
|
+
const lines = readFileSync12(file, "utf-8").split("\n");
|
|
11196
11717
|
if (lines.at(-1) === "") lines.pop();
|
|
11197
11718
|
return lines.slice(-limit);
|
|
11198
11719
|
}
|
|
@@ -11304,8 +11825,8 @@ function createCategoryWriteRouter() {
|
|
|
11304
11825
|
(id, position) => supabase.from("categories").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).eq("profile", getProfile())
|
|
11305
11826
|
)
|
|
11306
11827
|
);
|
|
11307
|
-
const
|
|
11308
|
-
if (
|
|
11828
|
+
const failed2 = results.find((r) => r.error);
|
|
11829
|
+
if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
|
|
11309
11830
|
return c.json({ ok: true });
|
|
11310
11831
|
});
|
|
11311
11832
|
app.put("/:id", async (c) => {
|
|
@@ -14259,8 +14780,80 @@ function createChannelMembersRouter() {
|
|
|
14259
14780
|
return router;
|
|
14260
14781
|
}
|
|
14261
14782
|
|
|
14262
|
-
// src/dashboard/
|
|
14783
|
+
// src/dashboard/memories-routes.ts
|
|
14263
14784
|
import { Hono as Hono27 } from "hono";
|
|
14785
|
+
var requireChannelVisible = async (c, next) => {
|
|
14786
|
+
const channel = findChannel(c.req.param("id"));
|
|
14787
|
+
if (!channel || !canSeeChannel(channel, c.get("auth"))) {
|
|
14788
|
+
return c.json({ error: "channel not found" }, 404);
|
|
14789
|
+
}
|
|
14790
|
+
c.set("memoryChannelId", channel.id);
|
|
14791
|
+
await next();
|
|
14792
|
+
};
|
|
14793
|
+
function createMemoriesRouter() {
|
|
14794
|
+
const router = new Hono27();
|
|
14795
|
+
for (const path of [
|
|
14796
|
+
"/:id/memories",
|
|
14797
|
+
"/:id/memories/*",
|
|
14798
|
+
"/:id/memory-events",
|
|
14799
|
+
"/:id/memory-events/*"
|
|
14800
|
+
]) {
|
|
14801
|
+
router.use(path, requireChannelVisible, requireDocsAccess);
|
|
14802
|
+
}
|
|
14803
|
+
router.get("/:id/memories", async (c) => {
|
|
14804
|
+
return c.json({ memories: await listChannelMemories(c.get("memoryChannelId")) });
|
|
14805
|
+
});
|
|
14806
|
+
router.patch("/:id/memories/:memoryId", async (c) => {
|
|
14807
|
+
const memoryId = c.req.param("memoryId");
|
|
14808
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
14809
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14810
|
+
const patch = {};
|
|
14811
|
+
if (typeof body["description"] === "string") patch.description = body["description"];
|
|
14812
|
+
if (typeof body["body"] === "string") patch.body = body["body"];
|
|
14813
|
+
const type = body["type"];
|
|
14814
|
+
if (type !== void 0) {
|
|
14815
|
+
if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
|
|
14816
|
+
patch.type = type;
|
|
14817
|
+
}
|
|
14818
|
+
try {
|
|
14819
|
+
const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
|
|
14820
|
+
if (!memory) return c.json({ error: "memory not found" }, 404);
|
|
14821
|
+
return c.json({ memory });
|
|
14822
|
+
} catch (err) {
|
|
14823
|
+
return c.json({ error: err.message }, 400);
|
|
14824
|
+
}
|
|
14825
|
+
});
|
|
14826
|
+
router.delete("/:id/memories/:memoryId", async (c) => {
|
|
14827
|
+
const memoryId = c.req.param("memoryId");
|
|
14828
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
14829
|
+
const deleted = await deleteChannelMemory(c.get("memoryChannelId"), memoryId, "user");
|
|
14830
|
+
if (!deleted) return c.json({ error: "memory not found" }, 404);
|
|
14831
|
+
return c.json({ ok: true });
|
|
14832
|
+
});
|
|
14833
|
+
router.get("/:id/memory-events", async (c) => {
|
|
14834
|
+
const threadId = c.req.query("thread") ?? "";
|
|
14835
|
+
if (!isUuid(threadId)) return c.json({ error: "thread query parameter required" }, 400);
|
|
14836
|
+
return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
|
|
14837
|
+
});
|
|
14838
|
+
router.post("/:id/memory-events/:eventId/undo", async (c) => {
|
|
14839
|
+
const eventId = c.req.param("eventId");
|
|
14840
|
+
if (!isUuid(eventId)) return c.json({ error: "invalid event id" }, 400);
|
|
14841
|
+
const result = await undoMemoryEvent(c.get("memoryChannelId"), eventId);
|
|
14842
|
+
if (result === "not-found") return c.json({ error: "event not found" }, 404);
|
|
14843
|
+
if (result === "already-undone") return c.json({ error: "already undone" }, 409);
|
|
14844
|
+
if (result === "conflict") {
|
|
14845
|
+
return c.json(
|
|
14846
|
+
{ error: "The memory has changed since, so this can no longer be undone." },
|
|
14847
|
+
409
|
|
14848
|
+
);
|
|
14849
|
+
}
|
|
14850
|
+
return c.json({ ok: true });
|
|
14851
|
+
});
|
|
14852
|
+
return router;
|
|
14853
|
+
}
|
|
14854
|
+
|
|
14855
|
+
// src/dashboard/onboarding-routes.ts
|
|
14856
|
+
import { Hono as Hono28 } from "hono";
|
|
14264
14857
|
|
|
14265
14858
|
// src/dashboard/unique-channel-name.ts
|
|
14266
14859
|
function uniqueChannelName(raw, taken) {
|
|
@@ -14349,7 +14942,7 @@ async function buildGreeting(input2) {
|
|
|
14349
14942
|
// src/dashboard/onboarding-routes.ts
|
|
14350
14943
|
var MAX_FOLDERS = 20;
|
|
14351
14944
|
function createOnboardingRouter(agent) {
|
|
14352
|
-
const app = new
|
|
14945
|
+
const app = new Hono28();
|
|
14353
14946
|
app.get("/scan", async (c) => {
|
|
14354
14947
|
const ctx = await resolveAuthContext(c);
|
|
14355
14948
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -14594,6 +15187,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14594
15187
|
api.route("/channels", createDocShareWriteRouter(agent));
|
|
14595
15188
|
api.route("/onboarding", createOnboardingRouter(agent));
|
|
14596
15189
|
api.route("/channels", createChannelMembersRouter());
|
|
15190
|
+
api.route("/channels", createMemoriesRouter());
|
|
14597
15191
|
api.route("/docs", createDocShareListRouter());
|
|
14598
15192
|
api.get("/users", (c) => c.json({ users: listActiveUsers() }));
|
|
14599
15193
|
api.route("/categories", createCategoryWriteRouter());
|
|
@@ -14844,17 +15438,17 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14844
15438
|
// src/dashboard/device-auth-routes.ts
|
|
14845
15439
|
import { verify } from "crypto";
|
|
14846
15440
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
14847
|
-
import { Hono as
|
|
15441
|
+
import { Hono as Hono29 } from "hono";
|
|
14848
15442
|
|
|
14849
15443
|
// src/dashboard/agent-bridge.ts
|
|
14850
15444
|
import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
|
|
14851
|
-
import { existsSync as existsSync25, readFileSync as
|
|
15445
|
+
import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
|
|
14852
15446
|
import dotenv from "dotenv";
|
|
14853
15447
|
import { WebSocket, WebSocketServer } from "ws";
|
|
14854
15448
|
|
|
14855
15449
|
// src/dashboard/host-identity.ts
|
|
14856
15450
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
|
|
14857
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as
|
|
15451
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
14858
15452
|
import { dirname as dirname6, join as join20 } from "path";
|
|
14859
15453
|
function hostPrivateKeyPath() {
|
|
14860
15454
|
return process.env["FLYSPACE_HOST_KEY_PATH"] || join20(paths.BASE_CONFIG_DIR, "host.key");
|
|
@@ -14865,7 +15459,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
|
14865
15459
|
`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.`
|
|
14866
15460
|
);
|
|
14867
15461
|
}
|
|
14868
|
-
return createPrivateKey2(
|
|
15462
|
+
return createPrivateKey2(readFileSync13(path, "utf8"));
|
|
14869
15463
|
}
|
|
14870
15464
|
function loadHostPublicKey() {
|
|
14871
15465
|
const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
|
|
@@ -14881,7 +15475,7 @@ function loadHostPublicKey() {
|
|
|
14881
15475
|
// src/dashboard/host-lock.ts
|
|
14882
15476
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
14883
15477
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
14884
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
15478
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
|
|
14885
15479
|
import { join as join21 } from "path";
|
|
14886
15480
|
function hostLockPath() {
|
|
14887
15481
|
return join21(paths.BASE_DATA_DIR, "host.lock");
|
|
@@ -14899,7 +15493,7 @@ function isLiveFlyspace(pid) {
|
|
|
14899
15493
|
}
|
|
14900
15494
|
function readLockPid(path) {
|
|
14901
15495
|
try {
|
|
14902
|
-
const pid = Number(JSON.parse(
|
|
15496
|
+
const pid = Number(JSON.parse(readFileSync14(path, "utf8")).pid);
|
|
14903
15497
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
14904
15498
|
} catch {
|
|
14905
15499
|
return null;
|
|
@@ -14917,7 +15511,7 @@ function acquireHostLock() {
|
|
|
14917
15511
|
try {
|
|
14918
15512
|
writeFileSync9(path, body, { flag: "wx", mode: 384 });
|
|
14919
15513
|
const release2 = () => {
|
|
14920
|
-
if (readLockPid(path) === process.pid)
|
|
15514
|
+
if (readLockPid(path) === process.pid) rmSync6(path, { force: true });
|
|
14921
15515
|
};
|
|
14922
15516
|
process.once("exit", release2);
|
|
14923
15517
|
return release2;
|
|
@@ -14931,7 +15525,7 @@ function acquireHostLock() {
|
|
|
14931
15525
|
`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.`
|
|
14932
15526
|
);
|
|
14933
15527
|
}
|
|
14934
|
-
|
|
15528
|
+
rmSync6(path, { force: true });
|
|
14935
15529
|
}
|
|
14936
15530
|
}
|
|
14937
15531
|
throw new Error(`Could not take the Host lock at ${path}`);
|
|
@@ -14940,13 +15534,13 @@ function publishHostEndpoint(port) {
|
|
|
14940
15534
|
const path = hostLockPath();
|
|
14941
15535
|
if (readLockPid(path) !== process.pid) return null;
|
|
14942
15536
|
const token = randomBytes5(32).toString("hex");
|
|
14943
|
-
const lock = JSON.parse(
|
|
15537
|
+
const lock = JSON.parse(readFileSync14(path, "utf8"));
|
|
14944
15538
|
writeFileSync9(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
14945
15539
|
return token;
|
|
14946
15540
|
}
|
|
14947
15541
|
function readHostEndpoint() {
|
|
14948
15542
|
try {
|
|
14949
|
-
const { pid, port, token } = JSON.parse(
|
|
15543
|
+
const { pid, port, token } = JSON.parse(readFileSync14(hostLockPath(), "utf8"));
|
|
14950
15544
|
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
14951
15545
|
return { port, token };
|
|
14952
15546
|
} catch {
|
|
@@ -15149,16 +15743,35 @@ async function listWorkspaceDevices(workspaceId) {
|
|
|
15149
15743
|
if (error2) fail("listWorkspaceDevices", error2);
|
|
15150
15744
|
return (data ?? []).map((r) => toDevice(r.devices)).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
15151
15745
|
}
|
|
15746
|
+
async function touchDevice(deviceId) {
|
|
15747
|
+
const { error: error2 } = await getSupabaseClient().from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", deviceId);
|
|
15748
|
+
if (error2) fail("touchDevice", error2);
|
|
15749
|
+
}
|
|
15750
|
+
var STALE_HOST_MS = 14 * 24 * 60 * 6e4;
|
|
15751
|
+
var isStale = (lastSeenAt) => !!lastSeenAt && Date.now() - Date.parse(lastSeenAt) > STALE_HOST_MS;
|
|
15152
15752
|
async function listWorkspaceHosts(workspaceIds, exceptPublicKey) {
|
|
15153
15753
|
const hosts2 = /* @__PURE__ */ new Map();
|
|
15154
15754
|
if (!workspaceIds.length) return hosts2;
|
|
15155
|
-
const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
|
|
15755
|
+
const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at, last_seen_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
|
|
15156
15756
|
if (error2) fail("listWorkspaceHosts", error2);
|
|
15157
15757
|
for (const r of data ?? []) {
|
|
15158
|
-
if (r.devices.public_key !== exceptPublicKey
|
|
15758
|
+
if (r.devices.public_key !== exceptPublicKey && !isStale(r.devices.last_seen_at)) {
|
|
15759
|
+
hosts2.set(r.workspace_id, r.devices.name);
|
|
15760
|
+
}
|
|
15159
15761
|
}
|
|
15160
15762
|
return hosts2;
|
|
15161
15763
|
}
|
|
15764
|
+
async function releaseStaleHosts(workspaceIds, exceptPublicKey) {
|
|
15765
|
+
if (!workspaceIds.length) return;
|
|
15766
|
+
const db = getSupabaseClient();
|
|
15767
|
+
const { data, error: error2 } = await db.from("device_workspaces").select("device_id, workspace_id, devices!inner(public_key, last_seen_at)").in("workspace_id", workspaceIds);
|
|
15768
|
+
if (error2) fail("releaseStaleHosts", error2);
|
|
15769
|
+
for (const r of data ?? []) {
|
|
15770
|
+
if (r.devices.public_key === exceptPublicKey || !isStale(r.devices.last_seen_at)) continue;
|
|
15771
|
+
const { error: delError } = await db.from("device_workspaces").delete().eq("device_id", r.device_id).eq("workspace_id", r.workspace_id);
|
|
15772
|
+
if (delError) fail("releaseStaleHosts", delError);
|
|
15773
|
+
}
|
|
15774
|
+
}
|
|
15162
15775
|
async function userEmail(userId) {
|
|
15163
15776
|
const { data, error: error2 } = await getSupabaseClient().auth.admin.getUserById(userId);
|
|
15164
15777
|
if (error2) return null;
|
|
@@ -15167,7 +15780,7 @@ async function userEmail(userId) {
|
|
|
15167
15780
|
|
|
15168
15781
|
// src/dashboard/doc-share-publisher.ts
|
|
15169
15782
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
15170
|
-
import { readFileSync as
|
|
15783
|
+
import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
|
|
15171
15784
|
import { extname as extname2, posix } from "path";
|
|
15172
15785
|
import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
|
|
15173
15786
|
|
|
@@ -15354,7 +15967,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
15354
15967
|
}
|
|
15355
15968
|
if (!stat.isFile()) return { status: "not-found" };
|
|
15356
15969
|
if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
|
|
15357
|
-
const source =
|
|
15970
|
+
const source = readFileSync15(abs, "utf8");
|
|
15358
15971
|
const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
|
|
15359
15972
|
if (kind === "excalidraw") {
|
|
15360
15973
|
try {
|
|
@@ -15434,7 +16047,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
15434
16047
|
let text = null;
|
|
15435
16048
|
try {
|
|
15436
16049
|
const size = statSync9(docAbs).size;
|
|
15437
|
-
if (size <= budget) text =
|
|
16050
|
+
if (size <= budget) text = readFileSync15(docAbs, "utf8");
|
|
15438
16051
|
} catch {
|
|
15439
16052
|
continue;
|
|
15440
16053
|
}
|
|
@@ -15451,7 +16064,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
15451
16064
|
let body;
|
|
15452
16065
|
try {
|
|
15453
16066
|
if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
|
|
15454
|
-
body =
|
|
16067
|
+
body = readFileSync15(resolved2.abs);
|
|
15455
16068
|
} catch {
|
|
15456
16069
|
continue;
|
|
15457
16070
|
}
|
|
@@ -15493,7 +16106,7 @@ async function publishDocShare(input2) {
|
|
|
15493
16106
|
...HTML_FEATURES,
|
|
15494
16107
|
brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
|
|
15495
16108
|
} : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
|
|
15496
|
-
const
|
|
16109
|
+
const snapshot3 = {
|
|
15497
16110
|
...built,
|
|
15498
16111
|
sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
|
|
15499
16112
|
};
|
|
@@ -15503,8 +16116,8 @@ async function publishDocShare(input2) {
|
|
|
15503
16116
|
const renamed = await setDocShareSlug(existing.id, slug);
|
|
15504
16117
|
if (!renamed) return { status: "not-found" };
|
|
15505
16118
|
}
|
|
15506
|
-
const share2 = await updateDocShareSnapshot(existing.id,
|
|
15507
|
-
const stale = existing.assetKeys.filter((key) => !
|
|
16119
|
+
const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
|
|
16120
|
+
const stale = existing.assetKeys.filter((key) => !snapshot3.assetKeys.includes(key));
|
|
15508
16121
|
await deleteShareAssets(stale);
|
|
15509
16122
|
if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
|
|
15510
16123
|
void queueShareAudio(share2);
|
|
@@ -15518,7 +16131,7 @@ async function publishDocShare(input2) {
|
|
|
15518
16131
|
channelId: input2.channelId,
|
|
15519
16132
|
sourcePath: input2.sourcePath,
|
|
15520
16133
|
createdBy: input2.createdBy,
|
|
15521
|
-
...
|
|
16134
|
+
...snapshot3,
|
|
15522
16135
|
...features
|
|
15523
16136
|
});
|
|
15524
16137
|
void queueShareAudio(share);
|
|
@@ -15633,8 +16246,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
|
|
|
15633
16246
|
const scope = table === "skill_files" && isWrite ? assigned : workspaces;
|
|
15634
16247
|
if (scope.length === 0) return deny(`This Host is not the assigned Host for any workspace it may write ${table} to.`);
|
|
15635
16248
|
if (method === "POST") {
|
|
15636
|
-
const
|
|
15637
|
-
if (!
|
|
16249
|
+
const ok3 = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
|
|
16250
|
+
if (!ok3) return deny(`Every ${table} row must belong to a workspace this Host is assigned.`);
|
|
15638
16251
|
}
|
|
15639
16252
|
if (method === "PATCH") {
|
|
15640
16253
|
const patch = bodyRows(req.body);
|
|
@@ -15648,8 +16261,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
|
|
|
15648
16261
|
if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
|
|
15649
16262
|
if (table === "threads") {
|
|
15650
16263
|
if (method === "POST") {
|
|
15651
|
-
const
|
|
15652
|
-
if (!
|
|
16264
|
+
const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
|
|
16265
|
+
if (!ok3) return deny("A thread must belong to a channel of the device workspaces.");
|
|
15653
16266
|
}
|
|
15654
16267
|
if (method === "PATCH") {
|
|
15655
16268
|
const patch = bodyRows(req.body);
|
|
@@ -15828,7 +16441,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
15828
16441
|
regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
|
|
15829
16442
|
listAgentRuns: () => chatAgent.runningThreadIds(),
|
|
15830
16443
|
listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
|
|
15831
|
-
listRunningProfiles: () => listProfiles().filter(
|
|
16444
|
+
listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
|
|
15832
16445
|
getAgentIdentities: () => resolveAgentIdentities(),
|
|
15833
16446
|
publishDocShare: (input2) => publishDocShare(input2),
|
|
15834
16447
|
listChannelSkills: (args) => listSkills(args.claudeConfigDir, args.cwd),
|
|
@@ -15845,14 +16458,14 @@ function localAgentCallbacks(chatAgent) {
|
|
|
15845
16458
|
}
|
|
15846
16459
|
var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
|
|
15847
16460
|
function buildHostConfigSnapshot() {
|
|
15848
|
-
const
|
|
16461
|
+
const snapshot3 = {};
|
|
15849
16462
|
for (const profile of listProfiles()) {
|
|
15850
|
-
|
|
15851
|
-
env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(
|
|
16463
|
+
snapshot3[profile] = runWithProfile(profile, () => ({
|
|
16464
|
+
env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync16(paths.CONFIG_FILE, "utf-8")) : {},
|
|
15852
16465
|
agentModel: loadDashboardSettings().agent
|
|
15853
16466
|
}));
|
|
15854
16467
|
}
|
|
15855
|
-
return
|
|
16468
|
+
return snapshot3;
|
|
15856
16469
|
}
|
|
15857
16470
|
function startLocalProfileConfigHydration() {
|
|
15858
16471
|
setProfileConfigCache(buildHostConfigSnapshot());
|
|
@@ -15941,9 +16554,21 @@ async function revalidateDeviceScopes() {
|
|
|
15941
16554
|
continue;
|
|
15942
16555
|
}
|
|
15943
16556
|
const workspaces = (await listDeviceWorkspaces(device)).map((w) => w.slug);
|
|
15944
|
-
for (const scope of hosts
|
|
16557
|
+
for (const [ws, scope] of hosts) {
|
|
16558
|
+
if (scope.deviceId !== deviceId) continue;
|
|
16559
|
+
const changed = (scope.workspaces ?? []).join() !== workspaces.join();
|
|
16560
|
+
scope.workspaces = workspaces;
|
|
16561
|
+
if (changed) send(ws, { type: "workspaces", workspaces });
|
|
16562
|
+
}
|
|
16563
|
+
if (!device.lastSeenAt || Date.now() - Date.parse(device.lastSeenAt) > LAST_SEEN_RESOLUTION_MS) {
|
|
16564
|
+
await touchDevice(deviceId);
|
|
16565
|
+
}
|
|
15945
16566
|
}
|
|
15946
16567
|
}
|
|
16568
|
+
var LAST_SEEN_RESOLUTION_MS = 60 * 6e4;
|
|
16569
|
+
function minCliVersion() {
|
|
16570
|
+
return process.env["FLYSPACE_MIN_CLI_VERSION"]?.trim() || null;
|
|
16571
|
+
}
|
|
15947
16572
|
function attachHostSocket(server) {
|
|
15948
16573
|
const wss = new WebSocketServer({ server, path: HOST_SOCKET_PATH });
|
|
15949
16574
|
const legacyPublicKey = loadHostPublicKey();
|
|
@@ -15984,10 +16609,19 @@ function attachHostSocket(server) {
|
|
|
15984
16609
|
ws.close(1008, "bad signature");
|
|
15985
16610
|
return;
|
|
15986
16611
|
}
|
|
16612
|
+
const min = minCliVersion();
|
|
16613
|
+
if (min && msg.cliVersion && isOlderVersion(msg.cliVersion, min)) {
|
|
16614
|
+
console.error(`[host-socket] rejecting Host on CLI ${msg.cliVersion} (minimum ${min})`);
|
|
16615
|
+
send(ws, { type: "auth-failed", reason: "cli too old", minCliVersion: min });
|
|
16616
|
+
ws.close(1008, "cli too old");
|
|
16617
|
+
return;
|
|
16618
|
+
}
|
|
15987
16619
|
supersedeSameIdentity(scope2, ws);
|
|
15988
16620
|
lastAuthFailure = null;
|
|
15989
16621
|
addHost(ws, scope2);
|
|
15990
|
-
send(ws, { type: "auth-ok" });
|
|
16622
|
+
send(ws, scope2.workspaces ? { type: "auth-ok", workspaces: scope2.workspaces } : { type: "auth-ok" });
|
|
16623
|
+
if (scope2.deviceId) void touchDevice(scope2.deviceId).catch(() => {
|
|
16624
|
+
});
|
|
15991
16625
|
console.log(`[host-socket] Host authenticated (authPath: ${scope2.authPath})`);
|
|
15992
16626
|
if (scope2.workspaces) void markAgentConnected(scope2.workspaces);
|
|
15993
16627
|
})();
|
|
@@ -16073,7 +16707,8 @@ function remoteAgentCallbacks() {
|
|
|
16073
16707
|
// Host connected, showing nobody as running is a reasonable default
|
|
16074
16708
|
// rather than a false claim of certainty either way.
|
|
16075
16709
|
listRunningProfiles: async () => await callHost("listRunningProfiles", []) ?? [],
|
|
16076
|
-
|
|
16710
|
+
// `null` (no Host) falls back to `offlineAgentIdentities` in api.ts.
|
|
16711
|
+
getAgentIdentities: () => callHost("getAgentIdentities", []),
|
|
16077
16712
|
// Reading the doc and copying its assets can only happen where the
|
|
16078
16713
|
// channel's real folder is, so this always goes to the Host. `null` (no
|
|
16079
16714
|
// Host connected) surfaces as `host-unavailable` — see
|
|
@@ -16110,7 +16745,7 @@ function remoteAgentCallbacks() {
|
|
|
16110
16745
|
}
|
|
16111
16746
|
var RECONNECT_BASE_MS = 1e3;
|
|
16112
16747
|
var RECONNECT_MAX_MS = 3e4;
|
|
16113
|
-
function connectHostSocket(url, callbacks, onAuthenticated) {
|
|
16748
|
+
function connectHostSocket(url, callbacks, onAuthenticated, onWorkspaces) {
|
|
16114
16749
|
const credentials = loadCredentials();
|
|
16115
16750
|
const deviceId = credentials?.deviceId;
|
|
16116
16751
|
const privateKey = deviceId ? ensureDeviceKey().privateKey : loadHostPrivateKey();
|
|
@@ -16142,11 +16777,23 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
|
|
|
16142
16777
|
if (!msg) return;
|
|
16143
16778
|
if (msg.type === "challenge") {
|
|
16144
16779
|
const signature = signRaw(null, Buffer.from(msg.nonce, "base64"), privateKey).toString("base64");
|
|
16145
|
-
|
|
16780
|
+
const version = CLI_VERSION ? { cliVersion: CLI_VERSION } : {};
|
|
16781
|
+
send(ws, deviceId ? { type: "auth", signature, deviceId, ...version } : { type: "auth", signature, ...version });
|
|
16146
16782
|
return;
|
|
16147
16783
|
}
|
|
16148
16784
|
if (msg.type === "auth-failed") {
|
|
16149
16785
|
console.error(`[host-socket] auth rejected: ${msg.reason}`);
|
|
16786
|
+
if (msg.minCliVersion) {
|
|
16787
|
+
console.error(
|
|
16788
|
+
`[host-socket] This Host runs flyspace ${CLI_VERSION} but the Workspace needs ${msg.minCliVersion} or later.
|
|
16789
|
+
Upgrade, then restart the Host: flyspace upgrade`
|
|
16790
|
+
);
|
|
16791
|
+
stopped = true;
|
|
16792
|
+
}
|
|
16793
|
+
return;
|
|
16794
|
+
}
|
|
16795
|
+
if (msg.type === "workspaces") {
|
|
16796
|
+
onWorkspaces?.(msg.workspaces);
|
|
16150
16797
|
return;
|
|
16151
16798
|
}
|
|
16152
16799
|
if (msg.type === "auth-ok") {
|
|
@@ -16158,6 +16805,7 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
|
|
|
16158
16805
|
send(ws, { type: "host-config", configs: buildHostConfigSnapshot() });
|
|
16159
16806
|
}, HOST_CONFIG_PUSH_INTERVAL_MS);
|
|
16160
16807
|
onAuthenticated?.();
|
|
16808
|
+
if (msg.workspaces) onWorkspaces?.(msg.workspaces);
|
|
16161
16809
|
return;
|
|
16162
16810
|
}
|
|
16163
16811
|
if (msg.type === "db-result") {
|
|
@@ -16344,7 +16992,7 @@ async function boundWorkspaceIds(publicKey, userId) {
|
|
|
16344
16992
|
return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
|
|
16345
16993
|
}
|
|
16346
16994
|
function createDeviceAuthRouter() {
|
|
16347
|
-
const app = new
|
|
16995
|
+
const app = new Hono29();
|
|
16348
16996
|
app.post("/code", async (c) => {
|
|
16349
16997
|
const body = await readBody(c);
|
|
16350
16998
|
const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
|
|
@@ -16407,8 +17055,8 @@ function createDeviceAuthRouter() {
|
|
|
16407
17055
|
if (!device || device.revokedAt || !nonce || !signature) return c.json({ error: "invalid_grant" }, 401);
|
|
16408
17056
|
if (!await consumeChallenge(device.id, nonce)) return c.json({ error: "invalid_grant" }, 401);
|
|
16409
17057
|
const key = parseEd25519PublicKey(device.publicKey);
|
|
16410
|
-
const
|
|
16411
|
-
if (!
|
|
17058
|
+
const ok3 = key && verify(null, Buffer.from(nonce), key, Buffer.from(signature, "base64"));
|
|
17059
|
+
if (!ok3) return c.json({ error: "invalid_grant" }, 401);
|
|
16412
17060
|
const { token, expiresInS } = await issueAccessToken(device.id);
|
|
16413
17061
|
return c.json({ access_token: token, token_type: "Bearer", expires_in: expiresInS });
|
|
16414
17062
|
});
|
|
@@ -16500,6 +17148,7 @@ function createDeviceAuthRouter() {
|
|
|
16500
17148
|
if (!decided) {
|
|
16501
17149
|
return c.json({ error: "That code was already used." }, 409);
|
|
16502
17150
|
}
|
|
17151
|
+
await releaseStaleHosts(workspaceIds, pending3.publicKey);
|
|
16503
17152
|
return c.json({ ok: true });
|
|
16504
17153
|
});
|
|
16505
17154
|
return app;
|
|
@@ -16507,7 +17156,7 @@ function createDeviceAuthRouter() {
|
|
|
16507
17156
|
|
|
16508
17157
|
// src/dashboard/api.ts
|
|
16509
17158
|
function createApiRouter(agent = {}, mode = "all") {
|
|
16510
|
-
const api = new
|
|
17159
|
+
const api = new Hono30();
|
|
16511
17160
|
api.use("*", async (c, next) => {
|
|
16512
17161
|
const known = await listKnownWorkspaces();
|
|
16513
17162
|
const fromHost = profileFromHost(trustedHost(c), known);
|
|
@@ -16554,7 +17203,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
16554
17203
|
if (mode === "data") {
|
|
16555
17204
|
api.get(
|
|
16556
17205
|
"/settings/agents",
|
|
16557
|
-
async (c) => c.json({ agents: await agent.getAgentIdentities?.() ??
|
|
17206
|
+
async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? await offlineAgentIdentities() })
|
|
16558
17207
|
);
|
|
16559
17208
|
api.get("/channels/:id/skills", async (c) => {
|
|
16560
17209
|
const { cwd, claudeConfigDir } = resolveChannelConfig(c.req.param("id"));
|
|
@@ -16618,7 +17267,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
16618
17267
|
}
|
|
16619
17268
|
|
|
16620
17269
|
// src/dashboard/app-icon-routes.ts
|
|
16621
|
-
import { Hono as
|
|
17270
|
+
import { Hono as Hono31 } from "hono";
|
|
16622
17271
|
var NEUTRAL_MANIFEST = {
|
|
16623
17272
|
id: "/",
|
|
16624
17273
|
name: "Flyspace",
|
|
@@ -16656,7 +17305,7 @@ async function withResolvedProfile2(c, next) {
|
|
|
16656
17305
|
return profile ? runWithProfile(profile, () => next()) : next();
|
|
16657
17306
|
}
|
|
16658
17307
|
function createAppIconRouter() {
|
|
16659
|
-
const app = new
|
|
17308
|
+
const app = new Hono31();
|
|
16660
17309
|
app.get("/manifest.json", withResolvedProfile2, async (c) => {
|
|
16661
17310
|
if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
|
|
16662
17311
|
const profile = getProfile();
|
|
@@ -16696,7 +17345,7 @@ function pngResponse(c, png) {
|
|
|
16696
17345
|
}
|
|
16697
17346
|
|
|
16698
17347
|
// src/dashboard/stripe-routes.ts
|
|
16699
|
-
import { Hono as
|
|
17348
|
+
import { Hono as Hono32 } from "hono";
|
|
16700
17349
|
function planStatusFor(status) {
|
|
16701
17350
|
const known = ["trialing", "active", "paused", "canceled"];
|
|
16702
17351
|
return known.includes(status) ? status : null;
|
|
@@ -16739,7 +17388,7 @@ async function handleCardAdded(session) {
|
|
|
16739
17388
|
}
|
|
16740
17389
|
}
|
|
16741
17390
|
function createStripeWebhookRouter() {
|
|
16742
|
-
const stripeRouter = new
|
|
17391
|
+
const stripeRouter = new Hono32();
|
|
16743
17392
|
stripeRouter.post("/webhook", async (c) => {
|
|
16744
17393
|
const secret = process.env["STRIPE_WEBHOOK_SECRET"];
|
|
16745
17394
|
const signature = c.req.header("stripe-signature");
|
|
@@ -16855,10 +17504,10 @@ var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
|
|
|
16855
17504
|
async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
|
|
16856
17505
|
for (let port = start; port < start + attempts; port++) {
|
|
16857
17506
|
const free = await new Promise((resolve8) => {
|
|
16858
|
-
const
|
|
16859
|
-
|
|
16860
|
-
|
|
16861
|
-
|
|
17507
|
+
const probe3 = createServer();
|
|
17508
|
+
probe3.once("error", () => resolve8(false));
|
|
17509
|
+
probe3.once("listening", () => probe3.close(() => resolve8(true)));
|
|
17510
|
+
probe3.listen(port, host);
|
|
16862
17511
|
});
|
|
16863
17512
|
if (free) return port;
|
|
16864
17513
|
}
|
|
@@ -16872,7 +17521,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
16872
17521
|
const bootCallbacks = new Proxy({}, {
|
|
16873
17522
|
get: (_t, prop) => lateCallbacks?.[prop]
|
|
16874
17523
|
});
|
|
16875
|
-
const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.())) : void 0;
|
|
17524
|
+
const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.(), onBindingsChanged)) : void 0;
|
|
16876
17525
|
if (servesHost) {
|
|
16877
17526
|
await reconcileAll().catch(
|
|
16878
17527
|
(err) => console.error("[dashboard] reconcileAll failed:", err.message)
|
|
@@ -16899,7 +17548,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
16899
17548
|
const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
|
|
16900
17549
|
const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
|
|
16901
17550
|
};
|
|
16902
|
-
const app = new
|
|
17551
|
+
const app = new Hono33();
|
|
16903
17552
|
app.use("*", async (c, next) => {
|
|
16904
17553
|
await next();
|
|
16905
17554
|
if (c.req.path.startsWith("/assets/")) {
|
|
@@ -16947,7 +17596,7 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
|
|
|
16947
17596
|
if (mode === "host") relayToken = publishHostEndpoint(port);
|
|
16948
17597
|
if (mode === "data") attachHostSocket(server);
|
|
16949
17598
|
const stopHostStatusBroadcast = mode === "data" ? startHostStatusBroadcast() : void 0;
|
|
16950
|
-
const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.()) : void 0);
|
|
17599
|
+
const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.(), onBindingsChanged) : void 0);
|
|
16951
17600
|
const stopPushWatcher = earlyHostSocket ? () => {
|
|
16952
17601
|
} : startPushWatcher();
|
|
16953
17602
|
const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
@@ -16968,6 +17617,13 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
|
|
|
16968
17617
|
}
|
|
16969
17618
|
};
|
|
16970
17619
|
}
|
|
17620
|
+
function onBindingsChanged(workspaces) {
|
|
17621
|
+
const creds = loadCredentials();
|
|
17622
|
+
if (!creds || (creds.workspaces ?? []).join() === workspaces.join()) return;
|
|
17623
|
+
saveCredentials({ ...creds, workspaces });
|
|
17624
|
+
console.log(`[dashboard] now serving: ${workspaces.join(", ") || "(none)"}`);
|
|
17625
|
+
void reconcileAll().then(() => ensureServices()).catch((err) => console.error("[dashboard] applying new bindings failed:", err.message));
|
|
17626
|
+
}
|
|
16971
17627
|
var SERVICE_CHECK_MS = 6e4;
|
|
16972
17628
|
function watchServices() {
|
|
16973
17629
|
const tick = () => void ensureServices().then((slugs) => slugs.length && console.log(`[dashboard] installed scheduler service for: ${slugs.join(", ")}`)).catch((err) => console.error("[dashboard] ensureServices failed:", err.message));
|
|
@@ -17177,7 +17833,7 @@ Done. ${totalThreads} threads, ${totalReplies} messages upserted.`);
|
|
|
17177
17833
|
}
|
|
17178
17834
|
|
|
17179
17835
|
// src/commands/slack.ts
|
|
17180
|
-
var
|
|
17836
|
+
var USAGE3 = `
|
|
17181
17837
|
Usage: flyspace slack <subcommand> [options]
|
|
17182
17838
|
|
|
17183
17839
|
Subcommands:
|
|
@@ -17362,7 +18018,7 @@ async function threadReplies(client2, nameOrId, ts2) {
|
|
|
17362
18018
|
async function postMessage(client2, args) {
|
|
17363
18019
|
const channelArg = getArg2(args, "--channel");
|
|
17364
18020
|
const textFilePath = getArg2(args, "--text-file");
|
|
17365
|
-
const text = textFilePath ?
|
|
18021
|
+
const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
|
|
17366
18022
|
const threadTs = getArg2(args, "--thread");
|
|
17367
18023
|
const markdown = args.includes("--markdown");
|
|
17368
18024
|
const broadcast = args.includes("--broadcast") && Boolean(threadTs);
|
|
@@ -17517,7 +18173,7 @@ async function ensureClaudeCodeReady() {
|
|
|
17517
18173
|
}
|
|
17518
18174
|
function getManifestYaml(botName) {
|
|
17519
18175
|
const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
|
|
17520
|
-
const raw =
|
|
18176
|
+
const raw = readFileSync17(manifestPath, "utf-8");
|
|
17521
18177
|
return raw.replace(/name: Custie/g, `name: ${botName}`);
|
|
17522
18178
|
}
|
|
17523
18179
|
async function stepPersonalise() {
|
|
@@ -17730,7 +18386,7 @@ async function stepBrowserTokens(botName) {
|
|
|
17730
18386
|
})
|
|
17731
18387
|
));
|
|
17732
18388
|
const DEBUG = process.argv.includes("--debug");
|
|
17733
|
-
const USER_DATA_DIR = resolve7(
|
|
18389
|
+
const USER_DATA_DIR = resolve7(tmpdir4(), "custie-playwright-profile");
|
|
17734
18390
|
let context;
|
|
17735
18391
|
try {
|
|
17736
18392
|
log4("Opening browser \u2014 a Slack login page will appear.\n");
|
|
@@ -17951,7 +18607,7 @@ function copyDefaultPrompt(botName) {
|
|
|
17951
18607
|
if (!existsSync27(paths.PROMPT_FILE)) {
|
|
17952
18608
|
const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
|
|
17953
18609
|
if (existsSync27(defaultPrompt)) {
|
|
17954
|
-
const content =
|
|
18610
|
+
const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
|
|
17955
18611
|
writeFileSync10(paths.PROMPT_FILE, content);
|
|
17956
18612
|
success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
|
|
17957
18613
|
}
|
|
@@ -18112,13 +18768,13 @@ async function runSlackCmd(args) {
|
|
|
18112
18768
|
break;
|
|
18113
18769
|
}
|
|
18114
18770
|
default:
|
|
18115
|
-
console.log(
|
|
18771
|
+
console.log(USAGE3);
|
|
18116
18772
|
break;
|
|
18117
18773
|
}
|
|
18118
18774
|
}
|
|
18119
18775
|
|
|
18120
18776
|
// src/commands/chat.ts
|
|
18121
|
-
var
|
|
18777
|
+
var USAGE4 = `
|
|
18122
18778
|
Usage: flyspace chat <subcommand> [options]
|
|
18123
18779
|
|
|
18124
18780
|
Subcommands:
|
|
@@ -18264,17 +18920,131 @@ async function runChatCmd(args) {
|
|
|
18264
18920
|
case "-h":
|
|
18265
18921
|
case "--help":
|
|
18266
18922
|
case void 0:
|
|
18267
|
-
console.log(
|
|
18923
|
+
console.log(USAGE4);
|
|
18268
18924
|
break;
|
|
18269
18925
|
default:
|
|
18270
18926
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
18271
|
-
console.log(
|
|
18927
|
+
console.log(USAGE4);
|
|
18928
|
+
process.exit(1);
|
|
18929
|
+
}
|
|
18930
|
+
}
|
|
18931
|
+
|
|
18932
|
+
// src/commands/memory.ts
|
|
18933
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
18934
|
+
var USAGE5 = `
|
|
18935
|
+
Usage: flyspace memory <subcommand> --channel <name-or-id> [options]
|
|
18936
|
+
|
|
18937
|
+
A channel's persistent agent memory (docs/features/workspace-memory.md).
|
|
18938
|
+
|
|
18939
|
+
Subcommands:
|
|
18940
|
+
list Every memory in the channel (index form)
|
|
18941
|
+
get <name> One memory in full
|
|
18942
|
+
search <words...> Memories whose name, description or body match
|
|
18943
|
+
save --name <slug> --type <type> Create, or overwrite the memory with that name
|
|
18944
|
+
--description <one line>
|
|
18945
|
+
--body <text | -> "-" reads the body from stdin
|
|
18946
|
+
[--thread <id>] The thread the memory came from
|
|
18947
|
+
delete <name-or-id> [--thread <id>] Delete one memory
|
|
18948
|
+
|
|
18949
|
+
Types: ${MEMORY_TYPES.join(", ")}
|
|
18950
|
+
`;
|
|
18951
|
+
function getArg4(args, flag) {
|
|
18952
|
+
const idx = args.indexOf(flag);
|
|
18953
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
18954
|
+
}
|
|
18955
|
+
function positionals(args) {
|
|
18956
|
+
const out = [];
|
|
18957
|
+
for (let i = 0; i < args.length; i++) {
|
|
18958
|
+
if (args[i].startsWith("--")) i++;
|
|
18959
|
+
else out.push(args[i]);
|
|
18960
|
+
}
|
|
18961
|
+
return out;
|
|
18962
|
+
}
|
|
18963
|
+
function indexLine(m) {
|
|
18964
|
+
return `${m.name} (${m.type}, ${m.updatedAt.slice(0, 10)}) ${m.description}`;
|
|
18965
|
+
}
|
|
18966
|
+
async function runMemoryCmd(args) {
|
|
18967
|
+
loadEnvFiles();
|
|
18968
|
+
const subcommand = args[0];
|
|
18969
|
+
const rest = args.slice(1);
|
|
18970
|
+
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
18971
|
+
console.log(USAGE5);
|
|
18972
|
+
return;
|
|
18973
|
+
}
|
|
18974
|
+
const channelArg = getArg4(rest, "--channel");
|
|
18975
|
+
if (!channelArg) throw new Error("--channel <name-or-id> is required");
|
|
18976
|
+
const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
|
|
18977
|
+
const words = positionals(rest);
|
|
18978
|
+
switch (subcommand) {
|
|
18979
|
+
case "list": {
|
|
18980
|
+
const memories = await listChannelMemories(channel.id);
|
|
18981
|
+
console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
|
|
18982
|
+
break;
|
|
18983
|
+
}
|
|
18984
|
+
case "get": {
|
|
18985
|
+
const name = words[0];
|
|
18986
|
+
if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
|
|
18987
|
+
const memory = await getChannelMemory(channel.id, name);
|
|
18988
|
+
if (!memory) throw new Error(`No memory named ${name} in ${channel.name}.`);
|
|
18989
|
+
console.log(`# ${memory.name} (${memory.type})
|
|
18990
|
+
|
|
18991
|
+
${memory.description}
|
|
18992
|
+
|
|
18993
|
+
${memory.body}`);
|
|
18994
|
+
break;
|
|
18995
|
+
}
|
|
18996
|
+
case "search": {
|
|
18997
|
+
if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
|
|
18998
|
+
const hits = await searchChannelMemories(channel.id, words.join(" "));
|
|
18999
|
+
console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
|
|
19000
|
+
break;
|
|
19001
|
+
}
|
|
19002
|
+
case "save": {
|
|
19003
|
+
const name = getArg4(rest, "--name");
|
|
19004
|
+
const type = getArg4(rest, "--type");
|
|
19005
|
+
const description = getArg4(rest, "--description");
|
|
19006
|
+
const bodyArg = getArg4(rest, "--body");
|
|
19007
|
+
if (!name || !type || !description || bodyArg === void 0) {
|
|
19008
|
+
throw new Error(
|
|
19009
|
+
"Usage: flyspace memory save --channel <c> --name <slug> --type <type> --description <one line> --body <text | -> [--thread <id>]"
|
|
19010
|
+
);
|
|
19011
|
+
}
|
|
19012
|
+
if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
|
|
19013
|
+
const body = bodyArg === "-" ? readFileSync18(0, "utf-8") : bodyArg;
|
|
19014
|
+
const thread = getArg4(rest, "--thread");
|
|
19015
|
+
const { created, changed } = await saveChannelMemory(
|
|
19016
|
+
{
|
|
19017
|
+
channelId: channel.id,
|
|
19018
|
+
name,
|
|
19019
|
+
type,
|
|
19020
|
+
description,
|
|
19021
|
+
body,
|
|
19022
|
+
createdBy: "agent",
|
|
19023
|
+
...thread ? { sourceThreadId: thread } : {}
|
|
19024
|
+
},
|
|
19025
|
+
"agent"
|
|
19026
|
+
);
|
|
19027
|
+
const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
|
|
19028
|
+
console.log(`${verb} memory ${name} in ${channel.name}.`);
|
|
19029
|
+
break;
|
|
19030
|
+
}
|
|
19031
|
+
case "delete": {
|
|
19032
|
+
const ref = words[0];
|
|
19033
|
+
if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
|
|
19034
|
+
const deleted = await deleteChannelMemory(channel.id, ref, "agent", getArg4(rest, "--thread"));
|
|
19035
|
+
if (!deleted) throw new Error(`No memory ${ref} in ${channel.name}.`);
|
|
19036
|
+
console.log(`Deleted memory ${ref}.`);
|
|
19037
|
+
break;
|
|
19038
|
+
}
|
|
19039
|
+
default:
|
|
19040
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
19041
|
+
console.log(USAGE5);
|
|
18272
19042
|
process.exit(1);
|
|
18273
19043
|
}
|
|
18274
19044
|
}
|
|
18275
19045
|
|
|
18276
19046
|
// src/commands/mcp.ts
|
|
18277
|
-
var
|
|
19047
|
+
var USAGE6 = `
|
|
18278
19048
|
Usage: flyspace mcp <server>
|
|
18279
19049
|
|
|
18280
19050
|
Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
|
|
@@ -18291,7 +19061,7 @@ Usage: flyspace mcp <server>
|
|
|
18291
19061
|
async function runMcpCmd(args) {
|
|
18292
19062
|
const server = args[0];
|
|
18293
19063
|
if (!server || server === "-h" || server === "--help") {
|
|
18294
|
-
console.log(
|
|
19064
|
+
console.log(USAGE6.trim());
|
|
18295
19065
|
return;
|
|
18296
19066
|
}
|
|
18297
19067
|
restoreStdout();
|
|
@@ -18304,7 +19074,7 @@ async function runMcpCmd(args) {
|
|
|
18304
19074
|
}
|
|
18305
19075
|
default:
|
|
18306
19076
|
console.error(`Unknown MCP server: ${server}`);
|
|
18307
|
-
console.error(
|
|
19077
|
+
console.error(USAGE6.trim());
|
|
18308
19078
|
process.exit(1);
|
|
18309
19079
|
}
|
|
18310
19080
|
}
|
|
@@ -18380,7 +19150,7 @@ async function runDashboard(args) {
|
|
|
18380
19150
|
|
|
18381
19151
|
// src/commands/workspace.ts
|
|
18382
19152
|
import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
18383
|
-
var
|
|
19153
|
+
var USAGE7 = `
|
|
18384
19154
|
Usage: flyspace workspace <subcommand> [options]
|
|
18385
19155
|
|
|
18386
19156
|
Subcommands:
|
|
@@ -18397,7 +19167,7 @@ var USAGE5 = `
|
|
|
18397
19167
|
--no-invite Never send an invite email. Fails if the admin
|
|
18398
19168
|
has no existing account.
|
|
18399
19169
|
`;
|
|
18400
|
-
function
|
|
19170
|
+
function getArg5(args, flag) {
|
|
18401
19171
|
const idx = args.indexOf(flag);
|
|
18402
19172
|
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
18403
19173
|
}
|
|
@@ -18459,7 +19229,7 @@ async function bindThisHost(workspaceId) {
|
|
|
18459
19229
|
return;
|
|
18460
19230
|
}
|
|
18461
19231
|
await bindDeviceWorkspace(device.id, workspaceId, device.userId);
|
|
18462
|
-
console.log(` bound to Host "${device.name}" (
|
|
19232
|
+
console.log(` bound to Host "${device.name}" (a running Host picks it up within a minute)`);
|
|
18463
19233
|
}
|
|
18464
19234
|
function provisionLocal(botName) {
|
|
18465
19235
|
ensureDirs();
|
|
@@ -18493,10 +19263,10 @@ async function provisionChannels(specs) {
|
|
|
18493
19263
|
async function handleCreate(args) {
|
|
18494
19264
|
const slug = args[0];
|
|
18495
19265
|
if (!slug || slug.startsWith("-")) fail2("Usage: flyspace workspace create <slug> --admin-email <email>");
|
|
18496
|
-
const adminEmail =
|
|
19266
|
+
const adminEmail = getArg5(args, "--admin-email");
|
|
18497
19267
|
if (!adminEmail?.includes("@")) fail2("--admin-email is required and must be an email address");
|
|
18498
|
-
const botName =
|
|
18499
|
-
const baseDomain =
|
|
19268
|
+
const botName = getArg5(args, "--bot-name") ?? slug;
|
|
19269
|
+
const baseDomain = getArg5(args, "--base-domain") ?? "flyspace.ai";
|
|
18500
19270
|
const allowInvite = !args.includes("--no-invite");
|
|
18501
19271
|
const channels = getAllArgs(args, "--channel").map(parseChannelSpec);
|
|
18502
19272
|
loadEnvFiles();
|
|
@@ -18519,13 +19289,13 @@ async function runWorkspaceCmd(args) {
|
|
|
18519
19289
|
await handleCreate(args.slice(1));
|
|
18520
19290
|
break;
|
|
18521
19291
|
default:
|
|
18522
|
-
console.log(
|
|
19292
|
+
console.log(USAGE7);
|
|
18523
19293
|
process.exit(args[0] ? 1 : 0);
|
|
18524
19294
|
}
|
|
18525
19295
|
}
|
|
18526
19296
|
|
|
18527
19297
|
// src/commands/reconcile.ts
|
|
18528
|
-
var
|
|
19298
|
+
var USAGE8 = `
|
|
18529
19299
|
Usage: flyspace reconcile [<workspace> | --all]
|
|
18530
19300
|
|
|
18531
19301
|
Repair a workspace's local filesystem state (profile dir, config.env,
|
|
@@ -18545,7 +19315,7 @@ function printLog(name, log5) {
|
|
|
18545
19315
|
}
|
|
18546
19316
|
async function runReconcileCmd(args) {
|
|
18547
19317
|
if (args.includes("-h") || args.includes("--help")) {
|
|
18548
|
-
console.log(
|
|
19318
|
+
console.log(USAGE8);
|
|
18549
19319
|
return;
|
|
18550
19320
|
}
|
|
18551
19321
|
loadEnvFiles();
|
|
@@ -18561,12 +19331,13 @@ async function runReconcileCmd(args) {
|
|
|
18561
19331
|
|
|
18562
19332
|
// src/cli.ts
|
|
18563
19333
|
var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
|
|
18564
|
-
var
|
|
19334
|
+
var USAGE9 = `
|
|
18565
19335
|
Usage: flyspace <command> [options]
|
|
18566
19336
|
|
|
18567
19337
|
Commands:
|
|
18568
19338
|
setup Set up this machine as a Host (login, agent CLIs, ~/Flyspace)
|
|
18569
19339
|
auth Log in, check status, or log out this device
|
|
19340
|
+
doctor Check this Host end to end (login, agent CLIs, services)
|
|
18570
19341
|
start Start the agent host (Slack only if configured)
|
|
18571
19342
|
install Install as a system service (launchd / systemd)
|
|
18572
19343
|
uninstall Remove the system service
|
|
@@ -18580,7 +19351,8 @@ var USAGE7 = `
|
|
|
18580
19351
|
usage Show agent token usage and cost for this workspace
|
|
18581
19352
|
slack Query Slack (channels, users, post messages)
|
|
18582
19353
|
chat Inspect dashboard chat channels and move threads between them
|
|
18583
|
-
|
|
19354
|
+
memory Read and write a channel's persistent agent memory
|
|
19355
|
+
profiles List local profiles (bound here? scheduler?) or remove one
|
|
18584
19356
|
workspace Provision a workspace (Supabase rows + local profile dirs)
|
|
18585
19357
|
reconcile Repair a workspace's local files against Supabase
|
|
18586
19358
|
${XERO_USAGE} dashboard Start the read-only management dashboard (web UI)
|
|
@@ -18620,6 +19392,9 @@ async function main() {
|
|
|
18620
19392
|
case "auth":
|
|
18621
19393
|
await runAuthCmd(args.slice(1));
|
|
18622
19394
|
break;
|
|
19395
|
+
case "doctor":
|
|
19396
|
+
await runDoctor(args.slice(1));
|
|
19397
|
+
break;
|
|
18623
19398
|
case "start":
|
|
18624
19399
|
await runStart();
|
|
18625
19400
|
break;
|
|
@@ -18668,8 +19443,12 @@ async function main() {
|
|
|
18668
19443
|
case "chat":
|
|
18669
19444
|
await runChatCmd(args.slice(1));
|
|
18670
19445
|
break;
|
|
19446
|
+
case "memory":
|
|
19447
|
+
await runMemoryCmd(args.slice(1));
|
|
19448
|
+
break;
|
|
19449
|
+
case "profile":
|
|
18671
19450
|
case "profiles":
|
|
18672
|
-
await runProfiles();
|
|
19451
|
+
await runProfiles(args.slice(1));
|
|
18673
19452
|
break;
|
|
18674
19453
|
case "dashboard":
|
|
18675
19454
|
await runDashboard(args.slice(1));
|
|
@@ -18679,12 +19458,12 @@ async function main() {
|
|
|
18679
19458
|
break;
|
|
18680
19459
|
case "-v":
|
|
18681
19460
|
case "--version":
|
|
18682
|
-
printRaw("0.1.
|
|
19461
|
+
printRaw("0.1.9");
|
|
18683
19462
|
break;
|
|
18684
19463
|
case "-h":
|
|
18685
19464
|
case "--help":
|
|
18686
19465
|
case void 0:
|
|
18687
|
-
printRaw(
|
|
19466
|
+
printRaw(USAGE9);
|
|
18688
19467
|
break;
|
|
18689
19468
|
case "xero":
|
|
18690
19469
|
if (false) {
|
|
@@ -18695,7 +19474,7 @@ async function main() {
|
|
|
18695
19474
|
// falls through
|
|
18696
19475
|
default:
|
|
18697
19476
|
printRaw(`Unknown command: ${command}`);
|
|
18698
|
-
printRaw(
|
|
19477
|
+
printRaw(USAGE9);
|
|
18699
19478
|
process.exit(1);
|
|
18700
19479
|
}
|
|
18701
19480
|
}
|