flyspace-cli 0.1.7 → 0.1.10
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-UYLVSUD6.js} +25 -17
- package/dist/cli.js +934 -266
- 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-UYLVSUD6.js";
|
|
154
156
|
import {
|
|
155
157
|
firstReplyBody,
|
|
156
158
|
getThread,
|
|
@@ -437,9 +439,23 @@ import { confirm as confirm2 } from "@inquirer/prompts";
|
|
|
437
439
|
|
|
438
440
|
// src/service.ts
|
|
439
441
|
import { execSync } from "child_process";
|
|
440
|
-
import { existsSync as existsSync3 } from "fs";
|
|
441
|
-
import { join as join2 } from "path";
|
|
442
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
443
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
442
444
|
import { homedir, platform } from "os";
|
|
445
|
+
function flyspaceHome(entry = process.argv[1]) {
|
|
446
|
+
let cliPath;
|
|
447
|
+
try {
|
|
448
|
+
cliPath = realpathSync(entry ?? "");
|
|
449
|
+
} catch {
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
const at = cliPath.indexOf("/lib/node_modules/flyspace-cli/");
|
|
453
|
+
if (at <= 0) return void 0;
|
|
454
|
+
const npmPrefix = cliPath.slice(0, at);
|
|
455
|
+
if (basename(npmPrefix) !== "cli") return void 0;
|
|
456
|
+
const home = dirname2(npmPrefix);
|
|
457
|
+
return existsSync3(join2(home, "bin", "flyspace")) && existsSync3(join2(home, "node", "bin", "node")) ? home : void 0;
|
|
458
|
+
}
|
|
443
459
|
function serviceLabel(profile) {
|
|
444
460
|
return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
|
|
445
461
|
}
|
|
@@ -466,6 +482,22 @@ function isServiceInstalled(profile) {
|
|
|
466
482
|
if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
|
|
467
483
|
return false;
|
|
468
484
|
}
|
|
485
|
+
function serviceBinaryMissing(file) {
|
|
486
|
+
let text;
|
|
487
|
+
try {
|
|
488
|
+
text = readFileSync3(file, "utf-8");
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
|
|
493
|
+
return !!bin && !existsSync3(bin);
|
|
494
|
+
}
|
|
495
|
+
function profileServiceFile(profile) {
|
|
496
|
+
return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
|
|
497
|
+
}
|
|
498
|
+
function hostServiceFile() {
|
|
499
|
+
return platform() === "darwin" ? hostPlistPath() : join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
|
|
500
|
+
}
|
|
469
501
|
function restartService(profile) {
|
|
470
502
|
const os = platform();
|
|
471
503
|
if (os === "darwin") {
|
|
@@ -497,10 +529,7 @@ function hostPlistPath() {
|
|
|
497
529
|
return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
|
|
498
530
|
}
|
|
499
531
|
function isHostServiceInstalled() {
|
|
500
|
-
if (platform() === "darwin") return existsSync3(
|
|
501
|
-
if (platform() === "linux") {
|
|
502
|
-
return existsSync3(join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`));
|
|
503
|
-
}
|
|
532
|
+
if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
|
|
504
533
|
return false;
|
|
505
534
|
}
|
|
506
535
|
function restartHostService() {
|
|
@@ -518,11 +547,31 @@ function restartHostService() {
|
|
|
518
547
|
}
|
|
519
548
|
return false;
|
|
520
549
|
}
|
|
550
|
+
function serviceActive(label2, unit) {
|
|
551
|
+
try {
|
|
552
|
+
if (platform() === "darwin") {
|
|
553
|
+
const out = execSync(`launchctl print gui/${process.getuid?.()}/${label2}`, { encoding: "utf-8", stdio: "pipe" });
|
|
554
|
+
return /^\s*state = running$/m.test(out);
|
|
555
|
+
}
|
|
556
|
+
if (platform() === "linux") {
|
|
557
|
+
execSync(`systemctl --user is-active --quiet ${unit}`, { stdio: "pipe" });
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
function isServiceRunning(profile) {
|
|
565
|
+
return serviceActive(serviceLabel(profile), systemdUnitName(profile));
|
|
566
|
+
}
|
|
567
|
+
function isHostServiceRunning() {
|
|
568
|
+
return serviceActive(HOST_SERVICE_LABEL, HOST_SYSTEMD_UNIT);
|
|
569
|
+
}
|
|
521
570
|
|
|
522
571
|
// src/commands/install.ts
|
|
523
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
572
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
|
|
524
573
|
import { execSync as execSync2 } from "child_process";
|
|
525
|
-
import { dirname as
|
|
574
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
526
575
|
import { homedir as homedir2, platform as platform2 } from "os";
|
|
527
576
|
function log(msg) {
|
|
528
577
|
console.log(`
|
|
@@ -543,12 +592,23 @@ function run(cmd, opts = {}) {
|
|
|
543
592
|
function detectClaudeBinDir() {
|
|
544
593
|
try {
|
|
545
594
|
const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
546
|
-
return claudePath ?
|
|
595
|
+
return claudePath ? dirname3(claudePath) : void 0;
|
|
547
596
|
} catch {
|
|
548
597
|
return void 0;
|
|
549
598
|
}
|
|
550
599
|
}
|
|
600
|
+
function detectNodeBinDir() {
|
|
601
|
+
const home = flyspaceHome();
|
|
602
|
+
return home ? join3(home, "node", "bin") : dirname3(process.execPath);
|
|
603
|
+
}
|
|
604
|
+
function nodeShimDirs() {
|
|
605
|
+
return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
|
|
606
|
+
(d) => existsSync4(d)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
551
609
|
function detectCustieBin() {
|
|
610
|
+
const home = flyspaceHome();
|
|
611
|
+
if (home) return join3(home, "bin", "flyspace");
|
|
552
612
|
const argv1 = process.argv[1];
|
|
553
613
|
if (argv1) {
|
|
554
614
|
return resolve3(argv1);
|
|
@@ -562,7 +622,7 @@ function detectCustieBin() {
|
|
|
562
622
|
function readEnvFile() {
|
|
563
623
|
if (!existsSync4(paths.CONFIG_FILE)) return {};
|
|
564
624
|
const vars = {};
|
|
565
|
-
for (const line of
|
|
625
|
+
for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
|
|
566
626
|
const trimmed = line.trim();
|
|
567
627
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
568
628
|
const eq = trimmed.indexOf("=");
|
|
@@ -604,18 +664,15 @@ function hostSpec() {
|
|
|
604
664
|
async function installMacOS(spec) {
|
|
605
665
|
log("Setting up macOS LaunchAgent...");
|
|
606
666
|
const custieBin = detectCustieBin();
|
|
607
|
-
const nodePath = process.execPath;
|
|
608
667
|
const claudeBinDir = detectClaudeBinDir();
|
|
609
668
|
const logDir = paths.LOG_DIR;
|
|
610
669
|
mkdirSync2(logDir, { recursive: true });
|
|
611
670
|
const envVars = spec.env;
|
|
612
671
|
const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
|
|
613
|
-
const asdfShims = join3(homedir2(), ".asdf", "shims");
|
|
614
|
-
const asdfBin = existsSync4(asdfShims) ? asdfShims : void 0;
|
|
615
672
|
const pathParts = [
|
|
616
|
-
|
|
673
|
+
detectNodeBinDir(),
|
|
617
674
|
claudeBinDir,
|
|
618
|
-
|
|
675
|
+
...nodeShimDirs(),
|
|
619
676
|
homebrewBin,
|
|
620
677
|
"/usr/local/bin",
|
|
621
678
|
"/usr/bin",
|
|
@@ -657,7 +714,7 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
|
657
714
|
</dict>
|
|
658
715
|
</plist>`;
|
|
659
716
|
const agentPlistPath = spec.plistPath;
|
|
660
|
-
mkdirSync2(
|
|
717
|
+
mkdirSync2(dirname3(agentPlistPath), { recursive: true });
|
|
661
718
|
try {
|
|
662
719
|
run(`launchctl unload "${agentPlistPath}"`, { silent: true });
|
|
663
720
|
} catch {
|
|
@@ -675,15 +732,15 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
|
675
732
|
async function installLinux(spec) {
|
|
676
733
|
log("Setting up systemd user service...");
|
|
677
734
|
const custieBin = detectCustieBin();
|
|
678
|
-
const nodePath = process.execPath;
|
|
679
735
|
const claudeBinDir = detectClaudeBinDir();
|
|
680
736
|
const logDir = paths.LOG_DIR;
|
|
681
737
|
mkdirSync2(logDir, { recursive: true });
|
|
682
738
|
const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
|
|
683
739
|
const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
|
|
684
740
|
const pathParts = [
|
|
685
|
-
|
|
741
|
+
detectNodeBinDir(),
|
|
686
742
|
claudeBinDir,
|
|
743
|
+
...nodeShimDirs(),
|
|
687
744
|
linuxbrewBin,
|
|
688
745
|
"/usr/local/bin",
|
|
689
746
|
"/usr/bin",
|
|
@@ -757,7 +814,7 @@ function listProfiles() {
|
|
|
757
814
|
}
|
|
758
815
|
return profiles;
|
|
759
816
|
}
|
|
760
|
-
function
|
|
817
|
+
function isServiceRunning2(profile) {
|
|
761
818
|
try {
|
|
762
819
|
if (platform3() === "darwin") {
|
|
763
820
|
const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
|
|
@@ -774,8 +831,8 @@ function isServiceRunning(profile) {
|
|
|
774
831
|
|
|
775
832
|
// src/auth/device-auth.ts
|
|
776
833
|
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
|
|
777
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
778
|
-
import { dirname as
|
|
834
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
835
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
779
836
|
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
780
837
|
var EXPIRY_SKEW_MS = 6e4;
|
|
781
838
|
var AuthApiError = class extends Error {
|
|
@@ -798,11 +855,11 @@ function ensureDeviceKey(path = deviceKeyPath()) {
|
|
|
798
855
|
let created = false;
|
|
799
856
|
if (!existsSync6(path)) {
|
|
800
857
|
const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
|
|
801
|
-
mkdirSync3(
|
|
858
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
802
859
|
writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
803
860
|
created = true;
|
|
804
861
|
}
|
|
805
|
-
const privateKey = createPrivateKey(
|
|
862
|
+
const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
|
|
806
863
|
const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
807
864
|
"base64"
|
|
808
865
|
);
|
|
@@ -811,13 +868,13 @@ function ensureDeviceKey(path = deviceKeyPath()) {
|
|
|
811
868
|
function loadCredentials(path = credentialsPath()) {
|
|
812
869
|
if (!existsSync6(path)) return null;
|
|
813
870
|
try {
|
|
814
|
-
return JSON.parse(
|
|
871
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
815
872
|
} catch {
|
|
816
873
|
return null;
|
|
817
874
|
}
|
|
818
875
|
}
|
|
819
876
|
function saveCredentials(creds, path = credentialsPath()) {
|
|
820
|
-
mkdirSync3(
|
|
877
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
821
878
|
writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
|
|
822
879
|
`, { mode: 384 });
|
|
823
880
|
}
|
|
@@ -957,12 +1014,12 @@ import {
|
|
|
957
1014
|
lstatSync,
|
|
958
1015
|
mkdirSync as mkdirSync4,
|
|
959
1016
|
readdirSync as readdirSync2,
|
|
960
|
-
readFileSync as
|
|
1017
|
+
readFileSync as readFileSync6,
|
|
961
1018
|
statSync as statSync2,
|
|
962
1019
|
symlinkSync
|
|
963
1020
|
} from "fs";
|
|
964
1021
|
import { homedir as homedir3 } from "os";
|
|
965
|
-
import { basename, join as join6, resolve as resolve4 } from "path";
|
|
1022
|
+
import { basename as basename2, join as join6, resolve as resolve4 } from "path";
|
|
966
1023
|
var MAX_JSONL_HEAD_BYTES = 64 * 1024;
|
|
967
1024
|
var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
|
|
968
1025
|
function flyspaceRoot() {
|
|
@@ -986,7 +1043,7 @@ function claudeLoggedIn() {
|
|
|
986
1043
|
if (process.env["ANTHROPIC_API_KEY"]) return true;
|
|
987
1044
|
if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
|
|
988
1045
|
try {
|
|
989
|
-
const raw = JSON.parse(
|
|
1046
|
+
const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
|
|
990
1047
|
if (raw.oauthAccount) return true;
|
|
991
1048
|
} catch {
|
|
992
1049
|
}
|
|
@@ -1027,7 +1084,7 @@ function jsonlFiles(dir, depth) {
|
|
|
1027
1084
|
function cwdFromTranscript(file) {
|
|
1028
1085
|
let head;
|
|
1029
1086
|
try {
|
|
1030
|
-
head =
|
|
1087
|
+
head = readFileSync6(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
|
|
1031
1088
|
} catch {
|
|
1032
1089
|
return null;
|
|
1033
1090
|
}
|
|
@@ -1044,7 +1101,7 @@ function cwdFromTranscript(file) {
|
|
|
1044
1101
|
}
|
|
1045
1102
|
function codexTrustedPaths() {
|
|
1046
1103
|
try {
|
|
1047
|
-
const toml =
|
|
1104
|
+
const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
|
|
1048
1105
|
return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
|
|
1049
1106
|
} catch {
|
|
1050
1107
|
return [];
|
|
@@ -1111,7 +1168,7 @@ function scanOnboarding() {
|
|
|
1111
1168
|
} else {
|
|
1112
1169
|
byPath.set(path, {
|
|
1113
1170
|
path,
|
|
1114
|
-
name:
|
|
1171
|
+
name: basename2(path),
|
|
1115
1172
|
sources: [source],
|
|
1116
1173
|
lastUsed: mtime,
|
|
1117
1174
|
sessions,
|
|
@@ -1514,7 +1571,7 @@ async function runAuthCmd(args) {
|
|
|
1514
1571
|
}
|
|
1515
1572
|
|
|
1516
1573
|
// src/version.ts
|
|
1517
|
-
var CLI_VERSION = true ? "0.1.
|
|
1574
|
+
var CLI_VERSION = true ? "0.1.10" : null;
|
|
1518
1575
|
function isOlderVersion(a, b) {
|
|
1519
1576
|
const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1520
1577
|
const [pa, pb] = [parse2(a), parse2(b)];
|
|
@@ -1559,15 +1616,16 @@ async function servedWorkspaces() {
|
|
|
1559
1616
|
return bound.workspaces;
|
|
1560
1617
|
}
|
|
1561
1618
|
function checkLatestVersion() {
|
|
1562
|
-
if (!CLI_VERSION) return;
|
|
1619
|
+
if (!CLI_VERSION) return true;
|
|
1563
1620
|
let latest = "";
|
|
1564
1621
|
try {
|
|
1565
1622
|
latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
|
|
1566
1623
|
} catch {
|
|
1567
|
-
return;
|
|
1624
|
+
return true;
|
|
1568
1625
|
}
|
|
1569
|
-
if (!latest || !isOlderVersion(CLI_VERSION, latest)) return;
|
|
1626
|
+
if (!latest || !isOlderVersion(CLI_VERSION, latest)) return true;
|
|
1570
1627
|
bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
|
|
1628
|
+
return false;
|
|
1571
1629
|
}
|
|
1572
1630
|
function loginArgs(args) {
|
|
1573
1631
|
const out = [];
|
|
@@ -1633,7 +1691,12 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1633
1691
|
}
|
|
1634
1692
|
let hostRunning = false;
|
|
1635
1693
|
if (args.includes("--start-host") && workspaces.length) {
|
|
1636
|
-
|
|
1694
|
+
for (const slug of workspaces) {
|
|
1695
|
+
if (!serviceBinaryMissing(profileServiceFile(slug))) continue;
|
|
1696
|
+
await runWithProfile(slug, runInstall);
|
|
1697
|
+
ok(`${slug} scheduler reinstalled on the new CLI path`);
|
|
1698
|
+
}
|
|
1699
|
+
if (!isHostServiceInstalled() || serviceBinaryMissing(hostServiceFile())) {
|
|
1637
1700
|
hostRunning = await installHostService();
|
|
1638
1701
|
if (hostRunning) ok(`Host running in the background ${DIM2}logs: ${tilde2(paths.LOG_DIR)}/host.log${RESET2}`);
|
|
1639
1702
|
} else {
|
|
@@ -1657,10 +1720,138 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1657
1720
|
if (!workspaces.length) process.exitCode = 1;
|
|
1658
1721
|
}
|
|
1659
1722
|
|
|
1723
|
+
// src/commands/doctor.ts
|
|
1724
|
+
import { execFile } from "child_process";
|
|
1725
|
+
import { tmpdir } from "os";
|
|
1726
|
+
import { promisify } from "util";
|
|
1727
|
+
var execAsync = promisify(execFile);
|
|
1728
|
+
var exec = (cmd, args, opts) => {
|
|
1729
|
+
const run3 = execAsync(cmd, args, opts);
|
|
1730
|
+
run3.child.stdin?.end();
|
|
1731
|
+
return run3;
|
|
1732
|
+
};
|
|
1733
|
+
var BOLD3 = "\x1B[1m";
|
|
1734
|
+
var DIM3 = "\x1B[2m";
|
|
1735
|
+
var GREEN3 = "\x1B[32m";
|
|
1736
|
+
var YELLOW3 = "\x1B[33m";
|
|
1737
|
+
var RESET3 = "\x1B[0m";
|
|
1738
|
+
var HELP3 = `
|
|
1739
|
+
Usage: flyspace doctor
|
|
1740
|
+
|
|
1741
|
+
Checks this Host end to end: CLI version, server reachability, device login,
|
|
1742
|
+
a live turn with each installed agent CLI, and the background services.
|
|
1743
|
+
Exits non-zero when any check fails.
|
|
1744
|
+
`;
|
|
1745
|
+
var LOGIN_HINTS = {
|
|
1746
|
+
claude: "claude auth login",
|
|
1747
|
+
codex: "codex login"
|
|
1748
|
+
};
|
|
1749
|
+
var tilde3 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
|
|
1750
|
+
var failed = false;
|
|
1751
|
+
var ok2 = (msg) => console.log(` ${GREEN3}\u2713${RESET3} ${msg}`);
|
|
1752
|
+
var bad2 = (msg, hint) => {
|
|
1753
|
+
failed = true;
|
|
1754
|
+
console.log(` ${YELLOW3}\u2717${RESET3} ${msg}${hint ? ` ${DIM3}${hint}${RESET3}` : ""}`);
|
|
1755
|
+
};
|
|
1756
|
+
function errorLine(err) {
|
|
1757
|
+
const e = err;
|
|
1758
|
+
if (e.killed) return "timed out after 90s";
|
|
1759
|
+
const lines = `${e.stdout ?? ""}
|
|
1760
|
+
${e.stderr ?? ""}`.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1761
|
+
const last = (re) => [...lines].reverse().find((l) => re.test(l));
|
|
1762
|
+
const line = last(/expired|unauthori[sz]ed|\b401\b|not logged in|authenticate/i) ?? last(/error|fail/i) ?? lines.at(-1) ?? e.message ?? "failed";
|
|
1763
|
+
return line.length > 160 ? `${line.slice(0, 157)}...` : line;
|
|
1764
|
+
}
|
|
1765
|
+
async function probe(id) {
|
|
1766
|
+
const env = { ...process.env };
|
|
1767
|
+
const opts = { cwd: tmpdir(), timeout: 9e4, encoding: "utf-8" };
|
|
1768
|
+
try {
|
|
1769
|
+
if (id === "claude") {
|
|
1770
|
+
const dir = pinnedClaudeConfigDir(process.env["CLAUDE_CONFIG_DIR"]);
|
|
1771
|
+
if (dir) env["CLAUDE_CONFIG_DIR"] = dir;
|
|
1772
|
+
else delete env["CLAUDE_CONFIG_DIR"];
|
|
1773
|
+
await exec("claude", ["-p", "Reply with OK", "--model", "haiku", "--max-turns", "1"], { ...opts, env });
|
|
1774
|
+
} else {
|
|
1775
|
+
await exec("codex", ["exec", "--skip-git-repo-check", "-s", "read-only", "Reply with OK"], { ...opts, env });
|
|
1776
|
+
}
|
|
1777
|
+
return null;
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
return errorLine(err);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function serverReachable() {
|
|
1783
|
+
try {
|
|
1784
|
+
const res = await fetch(`${apiUrl()}/api/health`, { signal: AbortSignal.timeout(1e4) });
|
|
1785
|
+
return res.ok ? null : `HTTP ${res.status}`;
|
|
1786
|
+
} catch (err) {
|
|
1787
|
+
return err.cause ? String(err.cause) : err.message;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function checkService(name, installed, file, running, fix) {
|
|
1791
|
+
if (!installed) bad2(`${name} not installed`, `run: ${fix}`);
|
|
1792
|
+
else if (serviceBinaryMissing(file)) bad2(`${name} points at a CLI that no longer exists`, `run: ${fix}`);
|
|
1793
|
+
else if (!running) bad2(`${name} not running`, `logs: ${tilde3(paths.LOG_DIR)}`);
|
|
1794
|
+
else ok2(`${name} running`);
|
|
1795
|
+
}
|
|
1796
|
+
async function runDoctor(args) {
|
|
1797
|
+
if (args.includes("-h") || args.includes("--help")) return void console.log(HELP3);
|
|
1798
|
+
failed = false;
|
|
1799
|
+
console.log(`
|
|
1800
|
+
${BOLD3}Flyspace Doctor${RESET3}
|
|
1801
|
+
`);
|
|
1802
|
+
if (checkLatestVersion()) ok2(`flyspace ${CLI_VERSION || "(dev build)"}`);
|
|
1803
|
+
else failed = true;
|
|
1804
|
+
const serverError = await serverReachable();
|
|
1805
|
+
if (serverError) bad2(`Can't reach ${apiUrl()}: ${serverError}`, "check the network or DNS");
|
|
1806
|
+
else ok2(`Server ${apiUrl()} reachable`);
|
|
1807
|
+
const creds = loadCredentials();
|
|
1808
|
+
let workspaces = [];
|
|
1809
|
+
if (!creds) {
|
|
1810
|
+
bad2("This device is not logged in", "run: flyspace setup");
|
|
1811
|
+
} else {
|
|
1812
|
+
const bound = await boundWorkspaces();
|
|
1813
|
+
workspaces = bound.workspaces;
|
|
1814
|
+
ok2(`Logged in as ${creds.userEmail} (device ${creds.deviceName})`);
|
|
1815
|
+
if (!bound.fresh) bad2("Couldn't refresh the device session; showing the workspaces saved at login");
|
|
1816
|
+
if (workspaces.length) ok2(`Serving ${workspaces.join(", ")}`);
|
|
1817
|
+
else bad2("Not serving any workspace", "a workspace admin has to add this Host");
|
|
1818
|
+
}
|
|
1819
|
+
const runners = detectRunners().filter((r) => r.installed);
|
|
1820
|
+
if (!runners.length) bad2("Neither claude nor codex is installed", "npm install -g @anthropic-ai/claude-code");
|
|
1821
|
+
const results = await Promise.all(runners.map((r) => probe(r.id)));
|
|
1822
|
+
runners.forEach((r, i) => {
|
|
1823
|
+
const error2 = results[i];
|
|
1824
|
+
if (error2) bad2(`${r.id} can't run a turn: ${error2}`, `run: ${LOGIN_HINTS[r.id]}`);
|
|
1825
|
+
else ok2(`${r.id} answered a test turn`);
|
|
1826
|
+
});
|
|
1827
|
+
if (workspaces.length) {
|
|
1828
|
+
checkService(
|
|
1829
|
+
"Host service",
|
|
1830
|
+
isHostServiceInstalled(),
|
|
1831
|
+
hostServiceFile(),
|
|
1832
|
+
isHostServiceRunning(),
|
|
1833
|
+
"flyspace setup --start-host"
|
|
1834
|
+
);
|
|
1835
|
+
for (const slug of workspaces) {
|
|
1836
|
+
checkService(
|
|
1837
|
+
`${slug} scheduler`,
|
|
1838
|
+
isServiceInstalled(slug),
|
|
1839
|
+
profileServiceFile(slug),
|
|
1840
|
+
isServiceRunning(slug),
|
|
1841
|
+
"flyspace setup --start-host"
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
console.log(failed ? "" : `
|
|
1846
|
+
${GREEN3}All good.${RESET3}
|
|
1847
|
+
`);
|
|
1848
|
+
if (failed) process.exitCode = 1;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1660
1851
|
// src/commands/uninstall.ts
|
|
1661
1852
|
import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
1662
1853
|
import { execSync as execSync5 } from "child_process";
|
|
1663
|
-
import { basename as
|
|
1854
|
+
import { basename as basename3, join as join8 } from "path";
|
|
1664
1855
|
import { homedir as homedir4, platform as platform5 } from "os";
|
|
1665
1856
|
function log2(msg) {
|
|
1666
1857
|
console.log(`
|
|
@@ -1689,7 +1880,7 @@ async function runUninstall() {
|
|
|
1689
1880
|
}
|
|
1690
1881
|
try {
|
|
1691
1882
|
unlinkSync2(plist);
|
|
1692
|
-
removed.push(
|
|
1883
|
+
removed.push(basename3(plist));
|
|
1693
1884
|
} catch {
|
|
1694
1885
|
}
|
|
1695
1886
|
}
|
|
@@ -1722,7 +1913,6 @@ async function runUninstall() {
|
|
|
1722
1913
|
|
|
1723
1914
|
// src/commands/profiles-cmd.ts
|
|
1724
1915
|
import { rmSync as rmSync3 } from "fs";
|
|
1725
|
-
var OPERATOR_NOTE = "This machine is an operator Host (it has Supabase credentials), so it serves every workspace, bound or not.";
|
|
1726
1916
|
var USAGE = `
|
|
1727
1917
|
Usage: flyspace profiles [list]
|
|
1728
1918
|
flyspace profiles remove <name>
|
|
@@ -1743,7 +1933,7 @@ async function listCmd() {
|
|
|
1743
1933
|
console.log("\n Profiles:\n");
|
|
1744
1934
|
console.log(` ${"NAME".padEnd(24)} ${"BOUND HERE".padEnd(12)} SCHEDULER`);
|
|
1745
1935
|
for (const profile of listProfiles()) {
|
|
1746
|
-
const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" :
|
|
1936
|
+
const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning2(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
|
|
1747
1937
|
const here = bound === null ? "-".padEnd(12) : bound.includes(profile) ? "yes".padEnd(12) : `\x1B[33m${"no".padEnd(12)}\x1B[0m`;
|
|
1748
1938
|
console.log(` ${profile.padEnd(24)} ${here} ${scheduler}`);
|
|
1749
1939
|
}
|
|
@@ -1752,10 +1942,7 @@ async function listCmd() {
|
|
|
1752
1942
|
if (bound?.length === 0) {
|
|
1753
1943
|
console.log("\n This Host is not bound to any workspace, so no scheduler runs here. Run: flyspace setup");
|
|
1754
1944
|
}
|
|
1755
|
-
if (
|
|
1756
|
-
console.log(`
|
|
1757
|
-
${OPERATOR_NOTE}`);
|
|
1758
|
-
} else if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
|
|
1945
|
+
if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
|
|
1759
1946
|
console.log("\n Profiles not bound here run nothing on this machine. Remove one with: flyspace profiles remove <name>");
|
|
1760
1947
|
}
|
|
1761
1948
|
console.log("");
|
|
@@ -1770,10 +1957,6 @@ async function removeCmd(name) {
|
|
|
1770
1957
|
console.error(` No local profile named "${name}".`);
|
|
1771
1958
|
process.exit(1);
|
|
1772
1959
|
}
|
|
1773
|
-
if (hasSupabaseCredentials()) {
|
|
1774
|
-
console.error(` ${OPERATOR_NOTE} Removing a profile would stop that workspace's schedules.`);
|
|
1775
|
-
process.exit(1);
|
|
1776
|
-
}
|
|
1777
1960
|
const bound = await boundHere();
|
|
1778
1961
|
if (bound === null) {
|
|
1779
1962
|
console.error(" This machine has no device login, so it serves every workspace; removing a profile would be undone.");
|
|
@@ -1803,6 +1986,8 @@ async function runProfiles(args = []) {
|
|
|
1803
1986
|
|
|
1804
1987
|
// src/commands/upgrade.ts
|
|
1805
1988
|
import { execSync as execSync6 } from "child_process";
|
|
1989
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
1990
|
+
import { join as join9 } from "path";
|
|
1806
1991
|
function restartInstalledProfiles() {
|
|
1807
1992
|
if (isHostServiceInstalled()) {
|
|
1808
1993
|
console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
|
|
@@ -1822,19 +2007,34 @@ Restarting ${installed.length} instance(s)...`);
|
|
|
1822
2007
|
}
|
|
1823
2008
|
}
|
|
1824
2009
|
}
|
|
2010
|
+
function npmInstallCommand() {
|
|
2011
|
+
let prefix;
|
|
2012
|
+
try {
|
|
2013
|
+
const entry = realpathSync2(process.argv[1] ?? "");
|
|
2014
|
+
const at = entry.indexOf(`/lib/node_modules/${"flyspace-cli"}/`);
|
|
2015
|
+
if (at > 0) prefix = entry.slice(0, at);
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
const global = `-g${prefix ? ` --prefix "${prefix}"` : ""}`;
|
|
2019
|
+
return `npm install ${global} --ignore-scripts ${"flyspace-cli"}@latest && npm rebuild ${global} better-sqlite3`;
|
|
2020
|
+
}
|
|
2021
|
+
function npmEnv() {
|
|
2022
|
+
const home = flyspaceHome();
|
|
2023
|
+
return home ? { ...process.env, PATH: `${join9(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
|
|
2024
|
+
}
|
|
1825
2025
|
async function runUpgrade() {
|
|
1826
|
-
const current = "0.1.
|
|
2026
|
+
const current = "0.1.10";
|
|
1827
2027
|
console.log(`Current version: ${current}`);
|
|
1828
2028
|
console.log("Checking for updates...\n");
|
|
1829
2029
|
try {
|
|
1830
|
-
const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
|
|
2030
|
+
const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", env: npmEnv() }).trim();
|
|
1831
2031
|
if (latest === current) {
|
|
1832
2032
|
console.log(`Already on the latest version (${current}).`);
|
|
1833
2033
|
return;
|
|
1834
2034
|
}
|
|
1835
2035
|
console.log(`New version available: ${latest}`);
|
|
1836
2036
|
console.log("Upgrading...\n");
|
|
1837
|
-
execSync6(
|
|
2037
|
+
execSync6(npmInstallCommand(), { stdio: "inherit", env: npmEnv() });
|
|
1838
2038
|
console.log(`
|
|
1839
2039
|
Upgraded from ${current} to ${latest}.`);
|
|
1840
2040
|
restartInstalledProfiles();
|
|
@@ -1843,7 +2043,7 @@ Upgraded from ${current} to ${latest}.`);
|
|
|
1843
2043
|
} catch (err) {
|
|
1844
2044
|
console.error("Upgrade failed:", err.message);
|
|
1845
2045
|
console.error(`
|
|
1846
|
-
You can upgrade manually with:
|
|
2046
|
+
You can upgrade manually with: ${npmInstallCommand()}`);
|
|
1847
2047
|
process.exit(1);
|
|
1848
2048
|
}
|
|
1849
2049
|
}
|
|
@@ -2195,7 +2395,7 @@ async function runAutomationCmd(args) {
|
|
|
2195
2395
|
|
|
2196
2396
|
// src/commands/usage.ts
|
|
2197
2397
|
import { existsSync as existsSync9 } from "fs";
|
|
2198
|
-
var
|
|
2398
|
+
var HELP4 = `
|
|
2199
2399
|
Usage: flyspace usage [options]
|
|
2200
2400
|
|
|
2201
2401
|
Show agent token usage and notional cost for this workspace.
|
|
@@ -2317,7 +2517,7 @@ function printExplanation(explained) {
|
|
|
2317
2517
|
async function runUsage(args) {
|
|
2318
2518
|
const parsed = parseArgs(args);
|
|
2319
2519
|
if (parsed === "help") {
|
|
2320
|
-
console.log(
|
|
2520
|
+
console.log(HELP4);
|
|
2321
2521
|
return;
|
|
2322
2522
|
}
|
|
2323
2523
|
const { days, by, explain, json } = parsed;
|
|
@@ -2437,11 +2637,11 @@ async function runRestart() {
|
|
|
2437
2637
|
// src/commands/logs.ts
|
|
2438
2638
|
import { spawn as spawn4 } from "child_process";
|
|
2439
2639
|
import { existsSync as existsSync10 } from "fs";
|
|
2440
|
-
import { join as
|
|
2640
|
+
import { join as join10 } from "path";
|
|
2441
2641
|
async function runLogs(args) {
|
|
2442
2642
|
const showErrors = args.includes("--error") || args.includes("-e");
|
|
2443
2643
|
const filename = showErrors ? "custie-error.log" : "custie.log";
|
|
2444
|
-
const logFile =
|
|
2644
|
+
const logFile = join10(paths.LOG_DIR, filename);
|
|
2445
2645
|
if (!existsSync10(logFile)) {
|
|
2446
2646
|
console.error(`Log file not found: ${logFile}`);
|
|
2447
2647
|
console.error("Is the service installed? Run `flyspace install` first.");
|
|
@@ -2463,9 +2663,9 @@ async function runLogs(args) {
|
|
|
2463
2663
|
|
|
2464
2664
|
// src/commands/slack.ts
|
|
2465
2665
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2466
|
-
import { existsSync as existsSync27, readFileSync as
|
|
2666
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
2467
2667
|
import { resolve as resolve7 } from "path";
|
|
2468
|
-
import { tmpdir as
|
|
2668
|
+
import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
|
|
2469
2669
|
import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
|
|
2470
2670
|
import { WebClient as WebClient3 } from "@slack/web-api";
|
|
2471
2671
|
|
|
@@ -2520,7 +2720,7 @@ async function emailForToken(token, type) {
|
|
|
2520
2720
|
|
|
2521
2721
|
// src/dashboard/profile-config.ts
|
|
2522
2722
|
import { homedir as homedir5 } from "os";
|
|
2523
|
-
import { join as
|
|
2723
|
+
import { join as join11 } from "path";
|
|
2524
2724
|
|
|
2525
2725
|
// src/dashboard/profile-config-cache.ts
|
|
2526
2726
|
var cache = /* @__PURE__ */ new Map();
|
|
@@ -2539,7 +2739,7 @@ function profileEnv(key) {
|
|
|
2539
2739
|
function profileClaudeConfigDir() {
|
|
2540
2740
|
const config = cachedProfileConfig();
|
|
2541
2741
|
if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
|
|
2542
|
-
return config.env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
2742
|
+
return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join11(homedir5(), ".claude");
|
|
2543
2743
|
}
|
|
2544
2744
|
function profileOauthToken() {
|
|
2545
2745
|
const env = cachedProfileConfig()?.env ?? {};
|
|
@@ -3993,8 +4193,8 @@ async function deliverPasswordResetLink(email, redirectTo) {
|
|
|
3993
4193
|
// src/dashboard/server.ts
|
|
3994
4194
|
import { existsSync as existsSync26 } from "fs";
|
|
3995
4195
|
import { createServer } from "net";
|
|
3996
|
-
import { join as
|
|
3997
|
-
import { Hono as
|
|
4196
|
+
import { join as join23 } from "path";
|
|
4197
|
+
import { Hono as Hono33 } from "hono";
|
|
3998
4198
|
import { serve } from "@hono/node-server";
|
|
3999
4199
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4000
4200
|
|
|
@@ -4003,6 +4203,10 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as w
|
|
|
4003
4203
|
async function boundWorkspaces2() {
|
|
4004
4204
|
return (await boundWorkspaces())?.workspaces ?? null;
|
|
4005
4205
|
}
|
|
4206
|
+
async function servedWorkspaces2() {
|
|
4207
|
+
const bound = await boundWorkspaces2();
|
|
4208
|
+
return new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
|
|
4209
|
+
}
|
|
4006
4210
|
async function reconcileProfile(profile) {
|
|
4007
4211
|
return runWithProfile(profile, async () => {
|
|
4008
4212
|
const log5 = [];
|
|
@@ -4028,8 +4232,7 @@ async function reconcileProfile(profile) {
|
|
|
4028
4232
|
});
|
|
4029
4233
|
}
|
|
4030
4234
|
async function reconcileAll() {
|
|
4031
|
-
const
|
|
4032
|
-
const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
|
|
4235
|
+
const slugs = await servedWorkspaces2();
|
|
4033
4236
|
const results = /* @__PURE__ */ new Map();
|
|
4034
4237
|
for (const slug of slugs) {
|
|
4035
4238
|
try {
|
|
@@ -4042,8 +4245,7 @@ async function reconcileAll() {
|
|
|
4042
4245
|
return results;
|
|
4043
4246
|
}
|
|
4044
4247
|
async function ensureServices() {
|
|
4045
|
-
const
|
|
4046
|
-
const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
|
|
4248
|
+
const slugs = await servedWorkspaces2();
|
|
4047
4249
|
slugs.delete(DEFAULT_PROFILE);
|
|
4048
4250
|
const installed = [];
|
|
4049
4251
|
for (const slug of slugs) {
|
|
@@ -4192,11 +4394,11 @@ var requireDocsAccess = async (c, next) => {
|
|
|
4192
4394
|
};
|
|
4193
4395
|
|
|
4194
4396
|
// src/dashboard/api.ts
|
|
4195
|
-
import { Hono as
|
|
4397
|
+
import { Hono as Hono30 } from "hono";
|
|
4196
4398
|
|
|
4197
4399
|
// src/dashboard/public-share-routes.ts
|
|
4198
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4199
|
-
import { join as
|
|
4400
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
4401
|
+
import { join as join13 } from "path";
|
|
4200
4402
|
import { Hono as Hono3 } from "hono";
|
|
4201
4403
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
4202
4404
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
@@ -4336,15 +4538,15 @@ async function createDocShare(input2) {
|
|
|
4336
4538
|
if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
|
|
4337
4539
|
return rowToShare(data);
|
|
4338
4540
|
}
|
|
4339
|
-
async function updateDocShareSnapshot(id,
|
|
4541
|
+
async function updateDocShareSnapshot(id, snapshot3, features) {
|
|
4340
4542
|
const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
|
|
4341
|
-
title:
|
|
4342
|
-
content:
|
|
4343
|
-
asset_keys:
|
|
4543
|
+
title: snapshot3.title,
|
|
4544
|
+
content: snapshot3.content,
|
|
4545
|
+
asset_keys: snapshot3.assetKeys,
|
|
4344
4546
|
// Re-detected with the snapshot: a re-publish can carry a rewritten
|
|
4345
4547
|
// article, and one written in another language than last time is exactly
|
|
4346
4548
|
// the case a stale value would get wrong.
|
|
4347
|
-
source_lang:
|
|
4549
|
+
source_lang: snapshot3.sourceLang,
|
|
4348
4550
|
audio_enabled: features.audioEnabled,
|
|
4349
4551
|
ask_enabled: features.askEnabled,
|
|
4350
4552
|
social_enabled: features.socialEnabled,
|
|
@@ -4661,12 +4863,12 @@ function injectShareHead(html, head) {
|
|
|
4661
4863
|
|
|
4662
4864
|
// src/dashboard/app-icon.ts
|
|
4663
4865
|
import { createHash as createHash2 } from "crypto";
|
|
4664
|
-
import { readFileSync as
|
|
4665
|
-
import { join as
|
|
4866
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
4867
|
+
import { join as join12 } from "path";
|
|
4666
4868
|
import sharp from "sharp";
|
|
4667
4869
|
var PLATE = "#0A0A0F";
|
|
4668
|
-
var MARK_FILE =
|
|
4669
|
-
var MARK_SVG_FILE =
|
|
4870
|
+
var MARK_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
|
|
4871
|
+
var MARK_SVG_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
|
|
4670
4872
|
var LAYOUT = {
|
|
4671
4873
|
standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
|
|
4672
4874
|
maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
|
|
@@ -4689,7 +4891,7 @@ function roundedPlate(size, fill = PLATE) {
|
|
|
4689
4891
|
}
|
|
4690
4892
|
async function renderBadge(box) {
|
|
4691
4893
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
4692
|
-
const raster = await sharp(
|
|
4894
|
+
const raster = await sharp(readFileSync7(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4693
4895
|
return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4694
4896
|
}
|
|
4695
4897
|
async function sampleBackground(logo) {
|
|
@@ -4744,7 +4946,7 @@ async function renderAppIcon({
|
|
|
4744
4946
|
if (hit) return hit;
|
|
4745
4947
|
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,
|
|
4746
4948
|
// either on its usual dark plate or, for a favicon, trimmed and bare.
|
|
4747
|
-
(transparent ? await renderBadge(size) : await sharp(
|
|
4949
|
+
(transparent ? await renderBadge(size) : await sharp(readFileSync7(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
|
|
4748
4950
|
cache3.set(key, out);
|
|
4749
4951
|
return out;
|
|
4750
4952
|
}
|
|
@@ -5962,7 +6164,7 @@ var shellCache = null;
|
|
|
5962
6164
|
function readShell(path) {
|
|
5963
6165
|
const { mtimeMs } = statSync3(path);
|
|
5964
6166
|
if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
|
|
5965
|
-
const html =
|
|
6167
|
+
const html = readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
|
|
5966
6168
|
shellCache = { mtimeMs, html };
|
|
5967
6169
|
return html;
|
|
5968
6170
|
}
|
|
@@ -5982,17 +6184,17 @@ function serveBuildFile(c, webDist, dir, name) {
|
|
|
5982
6184
|
if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
|
|
5983
6185
|
return c.text("not found", 404);
|
|
5984
6186
|
}
|
|
5985
|
-
const path =
|
|
6187
|
+
const path = join13(webDist, dir, name);
|
|
5986
6188
|
if (!existsSync12(path)) return c.text("not found", 404);
|
|
5987
6189
|
c.header("Content-Type", type);
|
|
5988
6190
|
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
5989
6191
|
if (ext === ".css") {
|
|
5990
|
-
return c.body(
|
|
6192
|
+
return c.body(readFileSync8(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
|
|
5991
6193
|
}
|
|
5992
6194
|
if (ext === ".js") {
|
|
5993
|
-
return c.body(
|
|
6195
|
+
return c.body(readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
|
|
5994
6196
|
}
|
|
5995
|
-
return c.body(new Uint8Array(
|
|
6197
|
+
return c.body(new Uint8Array(readFileSync8(path)));
|
|
5996
6198
|
}
|
|
5997
6199
|
var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
|
|
5998
6200
|
function servePublishedHtml(c, share) {
|
|
@@ -6128,7 +6330,7 @@ ${analytics}` : head);
|
|
|
6128
6330
|
const requested = c.req.param("lang");
|
|
6129
6331
|
const lang = canonicalLanguage(requested);
|
|
6130
6332
|
if (!isTranslatableLanguage(lang)) return next();
|
|
6131
|
-
const shell =
|
|
6333
|
+
const shell = join13(webDist, "index.html");
|
|
6132
6334
|
if (!existsSync12(shell)) return next();
|
|
6133
6335
|
const share = await getDocShare(c.req.param("id"));
|
|
6134
6336
|
if (!share) return next();
|
|
@@ -6164,7 +6366,7 @@ ${analytics}` : head);
|
|
|
6164
6366
|
const share = await getDocShare(c.req.param("id"));
|
|
6165
6367
|
if (!share) return next();
|
|
6166
6368
|
if (share.kind === "html") return servePublishedHtml(c, share);
|
|
6167
|
-
const shell =
|
|
6369
|
+
const shell = join13(webDist, "index.html");
|
|
6168
6370
|
if (!existsSync12(shell)) return next();
|
|
6169
6371
|
return readerPage(
|
|
6170
6372
|
c,
|
|
@@ -6754,9 +6956,49 @@ function createAdminRouter() {
|
|
|
6754
6956
|
return admin;
|
|
6755
6957
|
}
|
|
6756
6958
|
|
|
6959
|
+
// src/dashboard/agent-identities.ts
|
|
6960
|
+
var BOT_LABEL = "Workspace Agent";
|
|
6961
|
+
var BOT_DM_KEY = "claude";
|
|
6962
|
+
async function resolveAgentIdentities() {
|
|
6963
|
+
const settings = loadDashboardSettings();
|
|
6964
|
+
const { cwd: defaultCwd } = resolveChannelConfig("");
|
|
6965
|
+
const main2 = [
|
|
6966
|
+
{
|
|
6967
|
+
name: BOT_DM_KEY,
|
|
6968
|
+
label: BOT_LABEL,
|
|
6969
|
+
kind: "main",
|
|
6970
|
+
cwd: defaultCwd,
|
|
6971
|
+
model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
|
|
6972
|
+
...await loadBotIdentity()
|
|
6973
|
+
}
|
|
6974
|
+
];
|
|
6975
|
+
return Promise.all(
|
|
6976
|
+
main2.map(async ({ cwd, ...agent }) => {
|
|
6977
|
+
const dm = await ensureAgentDmChannel(agent.name, {
|
|
6978
|
+
name: agent.displayName ?? agent.label,
|
|
6979
|
+
cwd
|
|
6980
|
+
});
|
|
6981
|
+
return { ...agent, channelId: dm.id };
|
|
6982
|
+
})
|
|
6983
|
+
);
|
|
6984
|
+
}
|
|
6985
|
+
async function offlineAgentIdentities() {
|
|
6986
|
+
const dm = findChannelByAgentName(BOT_DM_KEY);
|
|
6987
|
+
if (!dm) return [];
|
|
6988
|
+
return [
|
|
6989
|
+
{
|
|
6990
|
+
name: BOT_DM_KEY,
|
|
6991
|
+
label: BOT_LABEL,
|
|
6992
|
+
kind: "main",
|
|
6993
|
+
channelId: dm.id,
|
|
6994
|
+
...await loadBotIdentity()
|
|
6995
|
+
}
|
|
6996
|
+
];
|
|
6997
|
+
}
|
|
6998
|
+
|
|
6757
6999
|
// src/dashboard/host-api.ts
|
|
6758
|
-
import { existsSync as existsSync21, readFileSync as
|
|
6759
|
-
import { join as
|
|
7000
|
+
import { existsSync as existsSync21, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
|
|
7001
|
+
import { join as join20 } from "path";
|
|
6760
7002
|
import sharp3 from "sharp";
|
|
6761
7003
|
|
|
6762
7004
|
// src/dashboard/channels-routes.ts
|
|
@@ -6792,6 +7034,11 @@ var featureFlags = {
|
|
|
6792
7034
|
title: "Dictation",
|
|
6793
7035
|
description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
|
|
6794
7036
|
stage: "dev"
|
|
7037
|
+
},
|
|
7038
|
+
"agent-memory": {
|
|
7039
|
+
title: "Agent memory",
|
|
7040
|
+
description: "Channel memory the agent reads every turn, saved by the agent and picked up from idle threads, with a Memory tab and an undo chip. While on, Claude's built-in auto memory is switched off so the two can be compared; turning this off keeps every memory.",
|
|
7041
|
+
stage: "beta"
|
|
6795
7042
|
}
|
|
6796
7043
|
};
|
|
6797
7044
|
var featureFlagIds = Object.keys(featureFlags);
|
|
@@ -6817,6 +7064,11 @@ async function listEnabledFeatureFlags(workspaceId) {
|
|
|
6817
7064
|
if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
|
|
6818
7065
|
return (data ?? []).map((r) => r.flag_id);
|
|
6819
7066
|
}
|
|
7067
|
+
async function listEnabledFeatureFlagsBySlug(slug) {
|
|
7068
|
+
const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
|
|
7069
|
+
if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
|
|
7070
|
+
return data ? listEnabledFeatureFlags(data.id) : [];
|
|
7071
|
+
}
|
|
6820
7072
|
async function enableFeatureFlag(workspaceId, flagId, userId) {
|
|
6821
7073
|
const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
|
|
6822
7074
|
{ workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
|
|
@@ -6994,8 +7246,8 @@ function createChannelWriteRouter() {
|
|
|
6994
7246
|
(id, position) => supabase.from("channels").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id)
|
|
6995
7247
|
)
|
|
6996
7248
|
);
|
|
6997
|
-
const
|
|
6998
|
-
if (
|
|
7249
|
+
const failed2 = results.find((r) => r.error);
|
|
7250
|
+
if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
|
|
6999
7251
|
await refreshChannelRegistry();
|
|
7000
7252
|
return c.json({ ok: true });
|
|
7001
7253
|
});
|
|
@@ -7053,20 +7305,20 @@ import {
|
|
|
7053
7305
|
openSync,
|
|
7054
7306
|
readSync,
|
|
7055
7307
|
readdirSync as readdirSync4,
|
|
7056
|
-
readFileSync as
|
|
7057
|
-
realpathSync,
|
|
7308
|
+
readFileSync as readFileSync11,
|
|
7309
|
+
realpathSync as realpathSync3,
|
|
7058
7310
|
renameSync,
|
|
7059
7311
|
rmSync as rmSync5,
|
|
7060
7312
|
statSync as statSync5,
|
|
7061
7313
|
writeFileSync as writeFileSync7
|
|
7062
7314
|
} from "fs";
|
|
7063
|
-
import { basename as
|
|
7315
|
+
import { basename as basename6, dirname as dirname5, extname, join as join18, relative, resolve as resolve5, sep } from "path";
|
|
7064
7316
|
import { Hono as Hono10 } from "hono";
|
|
7065
7317
|
import { streamSSE } from "hono/streaming";
|
|
7066
7318
|
|
|
7067
7319
|
// src/dashboard/docs-watcher.ts
|
|
7068
7320
|
import { existsSync as existsSync14, watch } from "fs";
|
|
7069
|
-
import { basename as
|
|
7321
|
+
import { basename as basename4, join as join14 } from "path";
|
|
7070
7322
|
var DEBOUNCE_MS = 200;
|
|
7071
7323
|
var MAX_PENDING = 200;
|
|
7072
7324
|
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
@@ -7102,12 +7354,12 @@ function flush(cwd) {
|
|
|
7102
7354
|
function watchDocs(cwd, listener) {
|
|
7103
7355
|
let entry = entries.get(cwd);
|
|
7104
7356
|
if (!entry) {
|
|
7105
|
-
const root =
|
|
7357
|
+
const root = basename4(cwd);
|
|
7106
7358
|
const watcher = watch(cwd, { recursive: true, persistent: false }, (_event, filename) => {
|
|
7107
7359
|
const current = entries.get(cwd);
|
|
7108
7360
|
if (!current || filename === null) return;
|
|
7109
7361
|
const rel = filename.split("\\").join("/");
|
|
7110
|
-
if (rel === root && !existsSync14(
|
|
7362
|
+
if (rel === root && !existsSync14(join14(cwd, rel))) return;
|
|
7111
7363
|
if (isIgnoredDocPath(rel)) return;
|
|
7112
7364
|
if (current.pending.size < MAX_PENDING) current.pending.add(rel);
|
|
7113
7365
|
if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
|
|
@@ -7344,15 +7596,15 @@ async function readOrQueueDocAudio(input2) {
|
|
|
7344
7596
|
}
|
|
7345
7597
|
|
|
7346
7598
|
// src/dashboard/artifact-export.ts
|
|
7347
|
-
import { readFileSync as
|
|
7599
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7348
7600
|
|
|
7349
7601
|
// src/dashboard/chat-agent.ts
|
|
7350
7602
|
import { homedir as homedir7 } from "os";
|
|
7351
7603
|
|
|
7352
7604
|
// src/mcp/config.ts
|
|
7353
7605
|
import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
7354
|
-
import { tmpdir } from "os";
|
|
7355
|
-
import { join as
|
|
7606
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
7607
|
+
import { join as join15 } from "path";
|
|
7356
7608
|
|
|
7357
7609
|
// src/mcp/crypto.ts
|
|
7358
7610
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
@@ -8038,8 +8290,8 @@ async function buildMcpConfig(profile, connectionIds) {
|
|
|
8038
8290
|
return { config, skipped };
|
|
8039
8291
|
}
|
|
8040
8292
|
function writeMcpConfigFile(config) {
|
|
8041
|
-
const dir = mkdtempSync(
|
|
8042
|
-
const path =
|
|
8293
|
+
const dir = mkdtempSync(join15(tmpdir2(), "flyspace-mcp-"));
|
|
8294
|
+
const path = join15(dir, "mcp-config.json");
|
|
8043
8295
|
writeFileSync6(path, JSON.stringify(config), { mode: 384 });
|
|
8044
8296
|
return {
|
|
8045
8297
|
path,
|
|
@@ -8060,22 +8312,22 @@ async function withMcpConfig(profile, connectionIds, fn) {
|
|
|
8060
8312
|
}
|
|
8061
8313
|
|
|
8062
8314
|
// src/claude/agents.ts
|
|
8063
|
-
import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as
|
|
8315
|
+
import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
8064
8316
|
import { homedir as homedir6 } from "os";
|
|
8065
|
-
import { basename as
|
|
8317
|
+
import { basename as basename5, join as join16 } from "path";
|
|
8066
8318
|
import yaml from "js-yaml";
|
|
8067
8319
|
function defaultConfigDir() {
|
|
8068
|
-
return
|
|
8320
|
+
return join16(homedir6(), ".claude");
|
|
8069
8321
|
}
|
|
8070
8322
|
function expandHome(p) {
|
|
8071
8323
|
if (p === "~") return homedir6();
|
|
8072
|
-
if (p.startsWith("~/")) return
|
|
8324
|
+
if (p.startsWith("~/")) return join16(homedir6(), p.slice(2));
|
|
8073
8325
|
return p;
|
|
8074
8326
|
}
|
|
8075
8327
|
function parseFrontmatter(file) {
|
|
8076
8328
|
let content;
|
|
8077
8329
|
try {
|
|
8078
|
-
content =
|
|
8330
|
+
content = readFileSync9(file, "utf-8");
|
|
8079
8331
|
} catch {
|
|
8080
8332
|
return {};
|
|
8081
8333
|
}
|
|
@@ -8099,17 +8351,17 @@ function collectFrom(root, source) {
|
|
|
8099
8351
|
return [];
|
|
8100
8352
|
}
|
|
8101
8353
|
return entries2.map((file) => {
|
|
8102
|
-
const path =
|
|
8354
|
+
const path = join16(root, file);
|
|
8103
8355
|
const fm = parseFrontmatter(path);
|
|
8104
|
-
const name = fm.name?.trim() ||
|
|
8356
|
+
const name = fm.name?.trim() || basename5(file, ".md");
|
|
8105
8357
|
return { name, description: (fm.description ?? "").trim(), source, path };
|
|
8106
8358
|
});
|
|
8107
8359
|
}
|
|
8108
8360
|
function listAgents(configDir2, cwd) {
|
|
8109
8361
|
const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
|
|
8110
8362
|
const collected = [
|
|
8111
|
-
...collectFrom(
|
|
8112
|
-
...collectFrom(
|
|
8363
|
+
...collectFrom(join16(cwd, ".claude", "agents"), "project"),
|
|
8364
|
+
...collectFrom(join16(base, "agents"), "user")
|
|
8113
8365
|
];
|
|
8114
8366
|
const byName = /* @__PURE__ */ new Map();
|
|
8115
8367
|
for (const agent of collected) {
|
|
@@ -8121,16 +8373,241 @@ function readAgentPersona(configDir2, cwd, name) {
|
|
|
8121
8373
|
const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
|
|
8122
8374
|
if (!agent) return void 0;
|
|
8123
8375
|
try {
|
|
8124
|
-
const content =
|
|
8376
|
+
const content = readFileSync9(agent.path, "utf-8");
|
|
8125
8377
|
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
|
|
8126
8378
|
} catch {
|
|
8127
8379
|
return void 0;
|
|
8128
8380
|
}
|
|
8129
8381
|
}
|
|
8130
8382
|
|
|
8383
|
+
// src/store/memory-store.ts
|
|
8384
|
+
var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
|
|
8385
|
+
var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
8386
|
+
var DESCRIPTION_MAX = 200;
|
|
8387
|
+
var BODY_MAX = 4e3;
|
|
8388
|
+
var MEMORY_INDEX_MAX_CHARS = 8e3;
|
|
8389
|
+
function isMemoryType(value) {
|
|
8390
|
+
return MEMORY_TYPES.includes(value);
|
|
8391
|
+
}
|
|
8392
|
+
function validateMemoryInput(input2) {
|
|
8393
|
+
if (!NAME_RE.test(input2.name))
|
|
8394
|
+
throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
|
|
8395
|
+
if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
|
|
8396
|
+
const description = input2.description.trim();
|
|
8397
|
+
if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
|
|
8398
|
+
throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
|
|
8399
|
+
}
|
|
8400
|
+
const body = input2.body.trim();
|
|
8401
|
+
if (!body || body.length > BODY_MAX) {
|
|
8402
|
+
throw new Error(`body must be 1 to ${BODY_MAX} characters`);
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
8405
|
+
function rowToMemory(row) {
|
|
8406
|
+
return {
|
|
8407
|
+
id: row["id"],
|
|
8408
|
+
channelId: row["channel_id"],
|
|
8409
|
+
name: row["name"],
|
|
8410
|
+
description: row["description"],
|
|
8411
|
+
type: row["type"],
|
|
8412
|
+
body: row["body"],
|
|
8413
|
+
sourceThreadId: row["source_thread_id"] ?? null,
|
|
8414
|
+
createdBy: row["created_by"],
|
|
8415
|
+
createdAt: row["created_at"],
|
|
8416
|
+
updatedAt: row["updated_at"]
|
|
8417
|
+
};
|
|
8418
|
+
}
|
|
8419
|
+
async function listChannelMemories(channelId) {
|
|
8420
|
+
const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
|
|
8421
|
+
if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
|
|
8422
|
+
return (data ?? []).map(rowToMemory);
|
|
8423
|
+
}
|
|
8424
|
+
async function getChannelMemory(channelId, name) {
|
|
8425
|
+
const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
|
|
8426
|
+
if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
|
|
8427
|
+
return data ? rowToMemory(data) : null;
|
|
8428
|
+
}
|
|
8429
|
+
async function saveChannelMemory(input2, actor) {
|
|
8430
|
+
validateMemoryInput(input2);
|
|
8431
|
+
const supabase = getSupabaseClient();
|
|
8432
|
+
const existing = await getChannelMemory(input2.channelId, input2.name);
|
|
8433
|
+
const fields = {
|
|
8434
|
+
description: input2.description.trim(),
|
|
8435
|
+
type: input2.type,
|
|
8436
|
+
body: input2.body.trim(),
|
|
8437
|
+
source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
|
|
8438
|
+
created_by: input2.createdBy
|
|
8439
|
+
};
|
|
8440
|
+
if (existing && sameContent(existing, fields)) {
|
|
8441
|
+
return { memory: existing, created: false, changed: false };
|
|
8442
|
+
}
|
|
8443
|
+
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();
|
|
8444
|
+
if (error2)
|
|
8445
|
+
throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
|
|
8446
|
+
const memory = rowToMemory(data);
|
|
8447
|
+
await logMemoryEvent({
|
|
8448
|
+
memory,
|
|
8449
|
+
action: existing ? "update" : "create",
|
|
8450
|
+
before: existing ? snapshot(existing) : null,
|
|
8451
|
+
after: snapshot(memory),
|
|
8452
|
+
actor,
|
|
8453
|
+
threadId: input2.sourceThreadId ?? null
|
|
8454
|
+
});
|
|
8455
|
+
return { memory, created: !existing, changed: true };
|
|
8456
|
+
}
|
|
8457
|
+
async function updateChannelMemory(channelId, memoryId, patch, actor) {
|
|
8458
|
+
const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
|
|
8459
|
+
if (!existing) return null;
|
|
8460
|
+
const { memory } = await saveChannelMemory(
|
|
8461
|
+
{
|
|
8462
|
+
channelId,
|
|
8463
|
+
name: existing.name,
|
|
8464
|
+
description: patch.description ?? existing.description,
|
|
8465
|
+
type: patch.type ?? existing.type,
|
|
8466
|
+
body: patch.body ?? existing.body,
|
|
8467
|
+
createdBy: existing.createdBy
|
|
8468
|
+
},
|
|
8469
|
+
actor
|
|
8470
|
+
);
|
|
8471
|
+
return memory;
|
|
8472
|
+
}
|
|
8473
|
+
async function searchChannelMemories(channelId, query) {
|
|
8474
|
+
const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
|
|
8475
|
+
if (!words.length) return [];
|
|
8476
|
+
let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
|
|
8477
|
+
for (const word of words) {
|
|
8478
|
+
request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
|
|
8479
|
+
}
|
|
8480
|
+
const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
|
|
8481
|
+
if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
|
|
8482
|
+
return (data ?? []).map(rowToMemory);
|
|
8483
|
+
}
|
|
8484
|
+
async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
|
|
8485
|
+
const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
|
|
8486
|
+
if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
|
|
8487
|
+
const deleted = (data ?? []).map(rowToMemory);
|
|
8488
|
+
for (const memory of deleted) {
|
|
8489
|
+
await logMemoryEvent({
|
|
8490
|
+
memory,
|
|
8491
|
+
action: "delete",
|
|
8492
|
+
before: snapshot(memory),
|
|
8493
|
+
after: null,
|
|
8494
|
+
actor,
|
|
8495
|
+
threadId: threadId ?? null
|
|
8496
|
+
});
|
|
8497
|
+
}
|
|
8498
|
+
return deleted.length > 0;
|
|
8499
|
+
}
|
|
8500
|
+
function snapshot(m) {
|
|
8501
|
+
return {
|
|
8502
|
+
description: m.description,
|
|
8503
|
+
type: m.type,
|
|
8504
|
+
body: m.body,
|
|
8505
|
+
sourceThreadId: m.sourceThreadId,
|
|
8506
|
+
createdBy: m.createdBy
|
|
8507
|
+
};
|
|
8508
|
+
}
|
|
8509
|
+
function sameContent(m, other) {
|
|
8510
|
+
return m.description === other.description && m.type === other.type && m.body === other.body;
|
|
8511
|
+
}
|
|
8512
|
+
async function logMemoryEvent(e) {
|
|
8513
|
+
const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
|
|
8514
|
+
channel_id: e.memory.channelId,
|
|
8515
|
+
memory_id: e.action === "delete" ? null : e.memory.id,
|
|
8516
|
+
thread_id: e.threadId,
|
|
8517
|
+
name: e.memory.name,
|
|
8518
|
+
action: e.action,
|
|
8519
|
+
before: e.before,
|
|
8520
|
+
after: e.after,
|
|
8521
|
+
actor: e.actor
|
|
8522
|
+
});
|
|
8523
|
+
if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
|
|
8524
|
+
}
|
|
8525
|
+
function rowToEvent(row) {
|
|
8526
|
+
return {
|
|
8527
|
+
id: row["id"],
|
|
8528
|
+
channelId: row["channel_id"],
|
|
8529
|
+
memoryId: row["memory_id"] ?? null,
|
|
8530
|
+
threadId: row["thread_id"] ?? null,
|
|
8531
|
+
name: row["name"],
|
|
8532
|
+
action: row["action"],
|
|
8533
|
+
before: row["before"] ?? null,
|
|
8534
|
+
after: row["after"] ?? null,
|
|
8535
|
+
actor: row["actor"],
|
|
8536
|
+
createdAt: row["created_at"],
|
|
8537
|
+
undoneAt: row["undone_at"] ?? null
|
|
8538
|
+
};
|
|
8539
|
+
}
|
|
8540
|
+
async function listThreadMemoryEvents(channelId, threadId) {
|
|
8541
|
+
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);
|
|
8542
|
+
if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
|
|
8543
|
+
return (data ?? []).map(rowToEvent);
|
|
8544
|
+
}
|
|
8545
|
+
async function undoMemoryEvent(channelId, eventId) {
|
|
8546
|
+
const supabase = getSupabaseClient();
|
|
8547
|
+
const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
|
|
8548
|
+
if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
|
|
8549
|
+
if (!data) return "not-found";
|
|
8550
|
+
const event = rowToEvent(data);
|
|
8551
|
+
if (event.undoneAt) return "already-undone";
|
|
8552
|
+
const current = await getChannelMemory(channelId, event.name);
|
|
8553
|
+
const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
|
|
8554
|
+
if (!unchangedSince) return "conflict";
|
|
8555
|
+
const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
|
|
8556
|
+
if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
|
|
8557
|
+
if (!(claimed.data ?? []).length) return "already-undone";
|
|
8558
|
+
const before = event.before;
|
|
8559
|
+
const restore = before ? {
|
|
8560
|
+
description: before.description,
|
|
8561
|
+
type: before.type,
|
|
8562
|
+
body: before.body,
|
|
8563
|
+
source_thread_id: before.sourceThreadId,
|
|
8564
|
+
created_by: before.createdBy
|
|
8565
|
+
} : null;
|
|
8566
|
+
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 });
|
|
8567
|
+
if (writeError) {
|
|
8568
|
+
await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
|
|
8569
|
+
throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
|
|
8570
|
+
}
|
|
8571
|
+
return "undone";
|
|
8572
|
+
}
|
|
8573
|
+
function isUuid(value) {
|
|
8574
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
8575
|
+
}
|
|
8576
|
+
function formatMemoryPrompt(channelId, memories) {
|
|
8577
|
+
const lines = [];
|
|
8578
|
+
let used = 0;
|
|
8579
|
+
let omitted = 0;
|
|
8580
|
+
for (const m of memories) {
|
|
8581
|
+
const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
|
|
8582
|
+
if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
|
|
8583
|
+
omitted++;
|
|
8584
|
+
continue;
|
|
8585
|
+
}
|
|
8586
|
+
lines.push(line);
|
|
8587
|
+
used += line.length + 1;
|
|
8588
|
+
}
|
|
8589
|
+
const index = lines.length ? lines.join("\n") + (omitted ? `
|
|
8590
|
+
- (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
|
|
8591
|
+
return [
|
|
8592
|
+
`## Channel memory`,
|
|
8593
|
+
``,
|
|
8594
|
+
`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.`,
|
|
8595
|
+
``,
|
|
8596
|
+
`- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
|
|
8597
|
+
`- Search: \`flyspace memory search --channel ${channelId} <words>\``,
|
|
8598
|
+
`- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
|
|
8599
|
+
`- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
|
|
8600
|
+
``,
|
|
8601
|
+
`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.`,
|
|
8602
|
+
``,
|
|
8603
|
+
`Index:`,
|
|
8604
|
+
index
|
|
8605
|
+
].join("\n");
|
|
8606
|
+
}
|
|
8607
|
+
|
|
8131
8608
|
// src/dashboard/rollover-summary.ts
|
|
8132
8609
|
import { spawn as spawn5 } from "child_process";
|
|
8133
|
-
import { tmpdir as
|
|
8610
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
8134
8611
|
var HAIKU_MODEL = "haiku";
|
|
8135
8612
|
var CJK_PATTERN = /[ -〿-ヿ㐀-䶿一-鿿豈--]/;
|
|
8136
8613
|
function estimateTokens(text) {
|
|
@@ -8193,7 +8670,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
|
|
|
8193
8670
|
// A stateless rewrite touches no files — tmpdir keeps it that way and
|
|
8194
8671
|
// avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
|
|
8195
8672
|
// unlike the channel's own cwd the main turn runs in.
|
|
8196
|
-
cwd:
|
|
8673
|
+
cwd: tmpdir3(),
|
|
8197
8674
|
env,
|
|
8198
8675
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8199
8676
|
});
|
|
@@ -8396,7 +8873,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
|
|
|
8396
8873
|
|
|
8397
8874
|
// src/dashboard/attachment-downloader.ts
|
|
8398
8875
|
import { writeFile } from "fs/promises";
|
|
8399
|
-
import { join as
|
|
8876
|
+
import { join as join17 } from "path";
|
|
8400
8877
|
import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
|
|
8401
8878
|
var debug = process.env["DEBUG"] === "true";
|
|
8402
8879
|
var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
|
|
@@ -8443,7 +8920,7 @@ async function downloadOne(key) {
|
|
|
8443
8920
|
if (bytes.length > sizeLimit) {
|
|
8444
8921
|
return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
|
|
8445
8922
|
}
|
|
8446
|
-
const localPath =
|
|
8923
|
+
const localPath = join17(paths.UPLOADS_DIR, lastSegment);
|
|
8447
8924
|
await writeFile(localPath, bytes);
|
|
8448
8925
|
if (debug)
|
|
8449
8926
|
console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
|
|
@@ -8989,6 +9466,18 @@ var ChatAgent = class {
|
|
|
8989
9466
|
thread.channel_id
|
|
8990
9467
|
);
|
|
8991
9468
|
if (docsAccess && docsAccess !== "write") channelConfig = { ...channelConfig, docsAccess };
|
|
9469
|
+
const agentMemory = await listEnabledFeatureFlagsBySlug(owningProfile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
|
|
9470
|
+
console.error("[chat-agent] feature flag lookup failed:", err.message);
|
|
9471
|
+
return false;
|
|
9472
|
+
});
|
|
9473
|
+
if (agentMemory) channelConfig = { ...channelConfig, disableAutoMemory: true };
|
|
9474
|
+
if (agentMemory && docsAccess !== "none" && isUuid(thread.channel_id)) {
|
|
9475
|
+
const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
|
|
9476
|
+
console.error("[chat-agent] memory lookup failed:", err.message);
|
|
9477
|
+
return void 0;
|
|
9478
|
+
});
|
|
9479
|
+
if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
|
|
9480
|
+
}
|
|
8992
9481
|
const store = this.openSessionStore();
|
|
8993
9482
|
try {
|
|
8994
9483
|
const channelKey = `chat:${thread.channel_id}`;
|
|
@@ -9396,7 +9885,9 @@ ${msg.body}` : msg.body;
|
|
|
9396
9885
|
signal,
|
|
9397
9886
|
mcpConfigPath,
|
|
9398
9887
|
...usageContext ? { usageContext } : {},
|
|
9399
|
-
...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {}
|
|
9888
|
+
...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {},
|
|
9889
|
+
...channelConfig.memoryPrompt ? { memoryPrompt: channelConfig.memoryPrompt } : {},
|
|
9890
|
+
...channelConfig.disableAutoMemory ? { disableAutoMemory: true } : {}
|
|
9400
9891
|
},
|
|
9401
9892
|
channelConfig.claudeConfigDir,
|
|
9402
9893
|
sessionId,
|
|
@@ -9433,6 +9924,7 @@ ${msg.body}` : msg.body;
|
|
|
9433
9924
|
channelConfig.prompt,
|
|
9434
9925
|
resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
|
|
9435
9926
|
),
|
|
9927
|
+
channelConfig.memoryPrompt,
|
|
9436
9928
|
docsAccessPromptNote(channelConfig.docsAccess)
|
|
9437
9929
|
].filter(Boolean).join("\n\n---\n\n"),
|
|
9438
9930
|
// Codex has no per-tool deny list, so any restricted requester gets
|
|
@@ -9710,7 +10202,7 @@ async function exportDocAsArtifact(channelId, docPath) {
|
|
|
9710
10202
|
);
|
|
9711
10203
|
const abs = resolveDocPath(channelConfig.cwd, docPath);
|
|
9712
10204
|
if (!abs) throw new Error(`doc not found: ${docPath}`);
|
|
9713
|
-
const content =
|
|
10205
|
+
const content = readFileSync10(abs, "utf8");
|
|
9714
10206
|
const title = extractTitle(abs, "md");
|
|
9715
10207
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
9716
10208
|
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).
|
|
@@ -9925,9 +10417,9 @@ function readHead(absPath) {
|
|
|
9925
10417
|
}
|
|
9926
10418
|
}
|
|
9927
10419
|
function extractTitle(absPath, kind) {
|
|
9928
|
-
if (kind === "url" || kind === "excalidraw") return
|
|
10420
|
+
if (kind === "url" || kind === "excalidraw") return basename6(absPath);
|
|
9929
10421
|
const head = readHead(absPath);
|
|
9930
|
-
if (!head) return
|
|
10422
|
+
if (!head) return basename6(absPath);
|
|
9931
10423
|
if (kind === "md") {
|
|
9932
10424
|
const m = head.match(/^#{1,3}\s+(.+?)\s*$/m);
|
|
9933
10425
|
if (m) return m[1].replace(/[*_`]/g, "").trim();
|
|
@@ -9935,7 +10427,7 @@ function extractTitle(absPath, kind) {
|
|
|
9935
10427
|
const t = head.match(/<title[^>]*>([^<]+)<\/title>/i) ?? head.match(/<h1[^>]*>([^<]+)<\/h1>/i);
|
|
9936
10428
|
if (t) return t[1].trim();
|
|
9937
10429
|
}
|
|
9938
|
-
return
|
|
10430
|
+
return basename6(absPath);
|
|
9939
10431
|
}
|
|
9940
10432
|
function resolveDirPath(root, rel) {
|
|
9941
10433
|
if (rel.includes("\0")) return null;
|
|
@@ -9967,7 +10459,7 @@ function probeDir(absDir, showHidden) {
|
|
|
9967
10459
|
return false;
|
|
9968
10460
|
}
|
|
9969
10461
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
9970
|
-
const abs =
|
|
10462
|
+
const abs = join18(dir, entry.name);
|
|
9971
10463
|
const kind = entryKind(entry, abs);
|
|
9972
10464
|
if (kind === "dir") {
|
|
9973
10465
|
if (SKIP_DIRS.has(entry.name)) hasOther = true;
|
|
@@ -9987,14 +10479,14 @@ function probeDir(absDir, showHidden) {
|
|
|
9987
10479
|
}
|
|
9988
10480
|
function readGitBranch(dir) {
|
|
9989
10481
|
try {
|
|
9990
|
-
let gitDir =
|
|
10482
|
+
let gitDir = join18(dir, ".git");
|
|
9991
10483
|
if (statSync5(gitDir).isFile()) {
|
|
9992
|
-
const pointer =
|
|
10484
|
+
const pointer = readFileSync11(gitDir, "utf8").trim();
|
|
9993
10485
|
const match = pointer.match(/^gitdir:\s*(.+)$/);
|
|
9994
10486
|
if (!match) return void 0;
|
|
9995
10487
|
gitDir = resolve5(dir, match[1]);
|
|
9996
10488
|
}
|
|
9997
|
-
const head =
|
|
10489
|
+
const head = readFileSync11(join18(gitDir, "HEAD"), "utf8").trim();
|
|
9998
10490
|
const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
9999
10491
|
if (branchMatch) return branchMatch[1];
|
|
10000
10492
|
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
|
|
@@ -10016,14 +10508,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
|
|
|
10016
10508
|
for (const entry of entries2) {
|
|
10017
10509
|
if (dirs.length + docs.length >= MAX_ENTRIES) break;
|
|
10018
10510
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10019
|
-
const abs =
|
|
10511
|
+
const abs = join18(absDir, entry.name);
|
|
10020
10512
|
const path = relative(root, abs).split(sep).join("/");
|
|
10021
10513
|
const kind = entryKind(entry, abs);
|
|
10022
10514
|
if (kind === "dir") {
|
|
10023
10515
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
10024
|
-
const
|
|
10025
|
-
if (!
|
|
10026
|
-
const isRepo = existsSync17(
|
|
10516
|
+
const probe3 = probeDir(abs, showHidden);
|
|
10517
|
+
if (!probe3.hasDocs && probe3.hasOther) continue;
|
|
10518
|
+
const isRepo = existsSync17(join18(abs, ".git"));
|
|
10027
10519
|
dirs.push({
|
|
10028
10520
|
path,
|
|
10029
10521
|
name: entry.name,
|
|
@@ -10059,7 +10551,7 @@ function flatDocs(root, {
|
|
|
10059
10551
|
for (const entry of entries2) {
|
|
10060
10552
|
if (hits.length >= MAX_FLAT_DOCS) return;
|
|
10061
10553
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10062
|
-
const abs =
|
|
10554
|
+
const abs = join18(dir, entry.name);
|
|
10063
10555
|
const kind = entryKind(entry, abs);
|
|
10064
10556
|
if (kind === "dir") {
|
|
10065
10557
|
if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
|
|
@@ -10123,7 +10615,7 @@ function resolveDownloadPath(root, rel) {
|
|
|
10123
10615
|
if (!rel || rel.includes("\0")) return null;
|
|
10124
10616
|
const abs = resolve5(root, rel);
|
|
10125
10617
|
if (abs !== root && !abs.startsWith(root + sep)) return null;
|
|
10126
|
-
if (!isListableFile(
|
|
10618
|
+
if (!isListableFile(basename6(abs))) return null;
|
|
10127
10619
|
const contentType = MEDIA_EXTENSIONS[extname(abs).toLowerCase()] ?? "application/octet-stream";
|
|
10128
10620
|
return { abs, contentType };
|
|
10129
10621
|
}
|
|
@@ -10139,7 +10631,7 @@ function uniqueName(absDir, name) {
|
|
|
10139
10631
|
let candidate = name;
|
|
10140
10632
|
for (let n = 1; n < 1e3; n++) {
|
|
10141
10633
|
try {
|
|
10142
|
-
statSync5(
|
|
10634
|
+
statSync5(join18(absDir, candidate));
|
|
10143
10635
|
} catch {
|
|
10144
10636
|
return candidate;
|
|
10145
10637
|
}
|
|
@@ -10161,7 +10653,7 @@ function saveUploadedFile(root, relDir, name, data) {
|
|
|
10161
10653
|
if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
|
|
10162
10654
|
if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
|
|
10163
10655
|
const final = uniqueName(absDir, safe);
|
|
10164
|
-
const abs =
|
|
10656
|
+
const abs = join18(absDir, final);
|
|
10165
10657
|
writeFileSync7(abs, data);
|
|
10166
10658
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
|
|
10167
10659
|
}
|
|
@@ -10175,7 +10667,7 @@ function createDocFolder(root, relDir, name) {
|
|
|
10175
10667
|
}
|
|
10176
10668
|
const safe = sanitizeUploadName(name);
|
|
10177
10669
|
if (!safe) return { status: "bad-name" };
|
|
10178
|
-
const abs =
|
|
10670
|
+
const abs = join18(absDir, safe);
|
|
10179
10671
|
try {
|
|
10180
10672
|
statSync5(abs);
|
|
10181
10673
|
return { status: "exists" };
|
|
@@ -10198,7 +10690,7 @@ function createDocFile(root, relDir, name, content) {
|
|
|
10198
10690
|
if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
|
|
10199
10691
|
if (SKIP_FILES.has(safe)) return { status: "bad-name" };
|
|
10200
10692
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
|
|
10201
|
-
const abs =
|
|
10693
|
+
const abs = join18(absDir, safe);
|
|
10202
10694
|
try {
|
|
10203
10695
|
writeFileSync7(abs, content, { encoding: "utf8", flag: "wx" });
|
|
10204
10696
|
} catch (err) {
|
|
@@ -10221,7 +10713,7 @@ function moveDocEntry(root, from, to) {
|
|
|
10221
10713
|
if (!isDir2 && !stat.isFile()) return { status: "not-found" };
|
|
10222
10714
|
const absTo = resolve5(root, to);
|
|
10223
10715
|
if (!absTo.startsWith(root + sep)) return { status: "bad-target" };
|
|
10224
|
-
const name = sanitizeUploadName(
|
|
10716
|
+
const name = sanitizeUploadName(basename6(absTo));
|
|
10225
10717
|
if (!name) return { status: "bad-target" };
|
|
10226
10718
|
if (!isDir2) {
|
|
10227
10719
|
const ext = extname(name).toLowerCase();
|
|
@@ -10229,19 +10721,19 @@ function moveDocEntry(root, from, to) {
|
|
|
10229
10721
|
return { status: "bad-target" };
|
|
10230
10722
|
}
|
|
10231
10723
|
}
|
|
10232
|
-
const absToDir =
|
|
10724
|
+
const absToDir = dirname5(absTo);
|
|
10233
10725
|
if (resolveDirPath(root, relative(root, absToDir)) === null) return { status: "bad-target" };
|
|
10234
10726
|
try {
|
|
10235
10727
|
if (!statSync5(absToDir).isDirectory()) return { status: "bad-target" };
|
|
10236
10728
|
} catch {
|
|
10237
10729
|
return { status: "bad-target" };
|
|
10238
10730
|
}
|
|
10239
|
-
const finalAbs =
|
|
10731
|
+
const finalAbs = join18(absToDir, name);
|
|
10240
10732
|
if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
|
|
10241
10733
|
return { status: "into-self" };
|
|
10242
10734
|
}
|
|
10243
10735
|
try {
|
|
10244
|
-
if (
|
|
10736
|
+
if (realpathSync3(finalAbs) !== realpathSync3(absFrom)) return { status: "exists" };
|
|
10245
10737
|
} catch {
|
|
10246
10738
|
}
|
|
10247
10739
|
renameSync(absFrom, finalAbs);
|
|
@@ -10298,10 +10790,10 @@ function createDocsRouter() {
|
|
|
10298
10790
|
}
|
|
10299
10791
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10300
10792
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10301
|
-
const data =
|
|
10793
|
+
const data = readFileSync11(resolved2.abs);
|
|
10302
10794
|
const headers = { "Content-Type": resolved2.contentType };
|
|
10303
10795
|
if (download) {
|
|
10304
|
-
const filename =
|
|
10796
|
+
const filename = basename6(resolved2.abs).replace(/"/g, "");
|
|
10305
10797
|
headers["Content-Disposition"] = `attachment; filename="${filename}"`;
|
|
10306
10798
|
}
|
|
10307
10799
|
return c.body(new Uint8Array(data), 200, headers);
|
|
@@ -10318,7 +10810,7 @@ function createDocsRouter() {
|
|
|
10318
10810
|
}
|
|
10319
10811
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10320
10812
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10321
|
-
return c.body(new Uint8Array(
|
|
10813
|
+
return c.body(new Uint8Array(readFileSync11(abs)), 200, {
|
|
10322
10814
|
"Content-Type": "text/html; charset=utf-8",
|
|
10323
10815
|
"Content-Security-Policy": HTML_SHARE_CSP,
|
|
10324
10816
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -10334,10 +10826,10 @@ function createDocsRouter() {
|
|
|
10334
10826
|
const files = form.getAll("file").filter((f) => f instanceof File);
|
|
10335
10827
|
if (files.length === 0) return c.json({ error: "missing file" }, 400);
|
|
10336
10828
|
const uploaded = [];
|
|
10337
|
-
const
|
|
10829
|
+
const failed2 = [];
|
|
10338
10830
|
for (const file of files) {
|
|
10339
10831
|
if (file.size > MAX_UPLOAD_BYTES) {
|
|
10340
|
-
|
|
10832
|
+
failed2.push({
|
|
10341
10833
|
name: file.name,
|
|
10342
10834
|
error: `larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)}MB`
|
|
10343
10835
|
});
|
|
@@ -10351,16 +10843,16 @@ function createDocsRouter() {
|
|
|
10351
10843
|
case "no-dir":
|
|
10352
10844
|
return c.json({ error: "folder not found" }, 404);
|
|
10353
10845
|
case "bad-type":
|
|
10354
|
-
|
|
10846
|
+
failed2.push({ name: file.name, error: "file type not allowed" });
|
|
10355
10847
|
break;
|
|
10356
10848
|
case "too-large":
|
|
10357
|
-
|
|
10849
|
+
failed2.push({ name: file.name, error: "file too large" });
|
|
10358
10850
|
break;
|
|
10359
10851
|
default:
|
|
10360
|
-
|
|
10852
|
+
failed2.push({ name: file.name, error: "invalid filename" });
|
|
10361
10853
|
}
|
|
10362
10854
|
}
|
|
10363
|
-
return c.json({ path: dir, uploaded, failed }, uploaded.length > 0 ? 201 : 400);
|
|
10855
|
+
return c.json({ path: dir, uploaded, failed: failed2 }, uploaded.length > 0 ? 201 : 400);
|
|
10364
10856
|
});
|
|
10365
10857
|
app.post("/:id/docs/folder", async (c) => {
|
|
10366
10858
|
const { cwd } = resolveChannelConfig(c.req.param("id"));
|
|
@@ -10500,10 +10992,10 @@ function createDocsRouter() {
|
|
|
10500
10992
|
// by; only docs get a parsed title.
|
|
10501
10993
|
kind: kind ?? "code",
|
|
10502
10994
|
lang: kind ? void 0 : CODE_EXTENSIONS[ext],
|
|
10503
|
-
title: kind ? extractTitle(abs, kind) :
|
|
10995
|
+
title: kind ? extractTitle(abs, kind) : basename6(abs),
|
|
10504
10996
|
mtime: stat.mtime.toISOString(),
|
|
10505
10997
|
tooLarge: tooLarge || void 0,
|
|
10506
|
-
content: tooLarge ? "" :
|
|
10998
|
+
content: tooLarge ? "" : readFileSync11(abs, "utf8")
|
|
10507
10999
|
});
|
|
10508
11000
|
});
|
|
10509
11001
|
app.put("/:id/docs/content", async (c) => {
|
|
@@ -10546,7 +11038,7 @@ function createDocsRouter() {
|
|
|
10546
11038
|
channel,
|
|
10547
11039
|
rel,
|
|
10548
11040
|
title: extractTitle(abs, "md"),
|
|
10549
|
-
content:
|
|
11041
|
+
content: readFileSync11(abs, "utf8")
|
|
10550
11042
|
};
|
|
10551
11043
|
};
|
|
10552
11044
|
app.get("/:id/docs/translation/:lang", async (c) => {
|
|
@@ -10743,7 +11235,7 @@ function relay(agent, pathOf) {
|
|
|
10743
11235
|
|
|
10744
11236
|
// src/dashboard/skills-routes.ts
|
|
10745
11237
|
import { existsSync as existsSync19, statSync as statSync7 } from "fs";
|
|
10746
|
-
import { dirname as
|
|
11238
|
+
import { dirname as dirname6 } from "path";
|
|
10747
11239
|
import { Hono as Hono12 } from "hono";
|
|
10748
11240
|
|
|
10749
11241
|
// src/dashboard/skill-mirror.ts
|
|
@@ -10769,7 +11261,7 @@ function ownSkills(scope, dir) {
|
|
|
10769
11261
|
return [{ scope, name, description, body, path, plugin: null, mtime }];
|
|
10770
11262
|
});
|
|
10771
11263
|
}
|
|
10772
|
-
function
|
|
11264
|
+
function snapshot2(scopes) {
|
|
10773
11265
|
const wants = (scope) => !scopes || scopes.has(scope);
|
|
10774
11266
|
const configDir2 = profileClaudeConfigDir();
|
|
10775
11267
|
const rows = [];
|
|
@@ -10804,7 +11296,7 @@ async function pushSkillScopes(scopes) {
|
|
|
10804
11296
|
if (!mirrorEnabled()) return;
|
|
10805
11297
|
const workspace = getProfile();
|
|
10806
11298
|
const supabase = getSupabaseClient();
|
|
10807
|
-
const rows =
|
|
11299
|
+
const rows = snapshot2(scopes ? new Set(scopes) : void 0);
|
|
10808
11300
|
let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
|
|
10809
11301
|
if (scopes) query = query.in("scope", scopes);
|
|
10810
11302
|
const { data, error: error2 } = await query;
|
|
@@ -10868,7 +11360,7 @@ function workspaceScope() {
|
|
|
10868
11360
|
return {
|
|
10869
11361
|
dir: workspaceSkillsDir(claudeConfigDir),
|
|
10870
11362
|
// `~/.claude` itself, which exists wherever the CLI has ever run.
|
|
10871
|
-
root:
|
|
11363
|
+
root: dirname6(workspaceSkillsDir(claudeConfigDir)),
|
|
10872
11364
|
cwd,
|
|
10873
11365
|
claudeConfigDir,
|
|
10874
11366
|
// Only plugin skills. Project skills belong to a specific channel's cwd, so
|
|
@@ -10998,7 +11490,7 @@ function mirrorBackend(agent) {
|
|
|
10998
11490
|
}
|
|
10999
11491
|
}
|
|
11000
11492
|
return {
|
|
11001
|
-
dir: own[0] ?
|
|
11493
|
+
dir: own[0] ? dirname6(dirname6(own[0].path)) : "",
|
|
11002
11494
|
skills: own.map(({ name, description, mtime }) => ({ name, description, mtime })),
|
|
11003
11495
|
inherited: inherited.sort(byName)
|
|
11004
11496
|
};
|
|
@@ -11084,37 +11576,11 @@ function registerMirroredSkillRoutes(api, agent) {
|
|
|
11084
11576
|
mountSkillRoutes(api, "/channels/:id/skill-files", channelRef, backend);
|
|
11085
11577
|
}
|
|
11086
11578
|
|
|
11087
|
-
// src/dashboard/agent-identities.ts
|
|
11088
|
-
var BOT_DM_KEY = "claude";
|
|
11089
|
-
async function resolveAgentIdentities() {
|
|
11090
|
-
const settings = loadDashboardSettings();
|
|
11091
|
-
const { cwd: defaultCwd } = resolveChannelConfig("");
|
|
11092
|
-
const main2 = [
|
|
11093
|
-
{
|
|
11094
|
-
name: BOT_DM_KEY,
|
|
11095
|
-
label: "Workspace Agent",
|
|
11096
|
-
kind: "main",
|
|
11097
|
-
cwd: defaultCwd,
|
|
11098
|
-
model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
|
|
11099
|
-
...await loadBotIdentity()
|
|
11100
|
-
}
|
|
11101
|
-
];
|
|
11102
|
-
return Promise.all(
|
|
11103
|
-
main2.map(async ({ cwd, ...agent }) => {
|
|
11104
|
-
const dm = await ensureAgentDmChannel(agent.name, {
|
|
11105
|
-
name: agent.displayName ?? agent.label,
|
|
11106
|
-
cwd
|
|
11107
|
-
});
|
|
11108
|
-
return { ...agent, channelId: dm.id };
|
|
11109
|
-
})
|
|
11110
|
-
);
|
|
11111
|
-
}
|
|
11112
|
-
|
|
11113
11579
|
// src/dashboard/providers.ts
|
|
11114
11580
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
11115
11581
|
import { existsSync as existsSync20 } from "fs";
|
|
11116
11582
|
import { homedir as homedir8 } from "os";
|
|
11117
|
-
import { join as
|
|
11583
|
+
import { join as join19, resolve as resolve6 } from "path";
|
|
11118
11584
|
var PROVIDER_LABELS = {
|
|
11119
11585
|
claude: "Claude Code",
|
|
11120
11586
|
codex: "Codex"
|
|
@@ -11132,7 +11598,7 @@ function binaryOnPath(bin) {
|
|
|
11132
11598
|
function claudeAuthAvailable() {
|
|
11133
11599
|
if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
|
|
11134
11600
|
const configDir2 = profileClaudeConfigDir() || resolve6(homedir8(), ".claude");
|
|
11135
|
-
if (existsSync20(
|
|
11601
|
+
if (existsSync20(join19(configDir2, ".credentials.json"))) return true;
|
|
11136
11602
|
if (process.platform !== "darwin") return false;
|
|
11137
11603
|
try {
|
|
11138
11604
|
execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
@@ -11143,7 +11609,7 @@ function claudeAuthAvailable() {
|
|
|
11143
11609
|
return false;
|
|
11144
11610
|
}
|
|
11145
11611
|
}
|
|
11146
|
-
function
|
|
11612
|
+
function probe2(runner) {
|
|
11147
11613
|
const label2 = PROVIDER_LABELS[runner];
|
|
11148
11614
|
const bin = runner === "codex" ? "codex" : "claude";
|
|
11149
11615
|
if (!binaryOnPath(bin)) {
|
|
@@ -11164,7 +11630,7 @@ function providerStatuses() {
|
|
|
11164
11630
|
const key = getProfile();
|
|
11165
11631
|
const hit = cache6.get(key);
|
|
11166
11632
|
if (hit && Date.now() - hit.at < TTL_MS2) return hit.statuses;
|
|
11167
|
-
const statuses = ["claude", "codex"].map(
|
|
11633
|
+
const statuses = ["claude", "codex"].map(probe2);
|
|
11168
11634
|
cache6.set(key, { at: Date.now(), statuses });
|
|
11169
11635
|
return statuses;
|
|
11170
11636
|
}
|
|
@@ -11275,7 +11741,7 @@ function registerHostRoutes(api) {
|
|
|
11275
11741
|
const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
|
|
11276
11742
|
const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
|
|
11277
11743
|
const filename = errors ? "custie-error.log" : "custie.log";
|
|
11278
|
-
const file =
|
|
11744
|
+
const file = join20(paths.LOG_DIR, filename);
|
|
11279
11745
|
const exists = existsSync21(file);
|
|
11280
11746
|
return c.json({
|
|
11281
11747
|
file,
|
|
@@ -11287,7 +11753,7 @@ function registerHostRoutes(api) {
|
|
|
11287
11753
|
}
|
|
11288
11754
|
function tailFile(file, limit) {
|
|
11289
11755
|
if (!existsSync21(file)) return [];
|
|
11290
|
-
const lines =
|
|
11756
|
+
const lines = readFileSync12(file, "utf-8").split("\n");
|
|
11291
11757
|
if (lines.at(-1) === "") lines.pop();
|
|
11292
11758
|
return lines.slice(-limit);
|
|
11293
11759
|
}
|
|
@@ -11399,8 +11865,8 @@ function createCategoryWriteRouter() {
|
|
|
11399
11865
|
(id, position) => supabase.from("categories").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).eq("profile", getProfile())
|
|
11400
11866
|
)
|
|
11401
11867
|
);
|
|
11402
|
-
const
|
|
11403
|
-
if (
|
|
11868
|
+
const failed2 = results.find((r) => r.error);
|
|
11869
|
+
if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
|
|
11404
11870
|
return c.json({ ok: true });
|
|
11405
11871
|
});
|
|
11406
11872
|
app.put("/:id", async (c) => {
|
|
@@ -14354,8 +14820,86 @@ function createChannelMembersRouter() {
|
|
|
14354
14820
|
return router;
|
|
14355
14821
|
}
|
|
14356
14822
|
|
|
14357
|
-
// src/dashboard/
|
|
14823
|
+
// src/dashboard/memories-routes.ts
|
|
14358
14824
|
import { Hono as Hono27 } from "hono";
|
|
14825
|
+
var requireChannelVisible = async (c, next) => {
|
|
14826
|
+
const channel = findChannel(c.req.param("id"));
|
|
14827
|
+
if (!channel || !canSeeChannel(channel, c.get("auth"))) {
|
|
14828
|
+
return c.json({ error: "channel not found" }, 404);
|
|
14829
|
+
}
|
|
14830
|
+
c.set("memoryChannelId", channel.id);
|
|
14831
|
+
await next();
|
|
14832
|
+
};
|
|
14833
|
+
var requireAgentMemory = async (c, next) => {
|
|
14834
|
+
if (!await isFeatureEnabled(c, "agent-memory")) {
|
|
14835
|
+
return c.json({ error: "feature not enabled" }, 403);
|
|
14836
|
+
}
|
|
14837
|
+
await next();
|
|
14838
|
+
};
|
|
14839
|
+
function createMemoriesRouter() {
|
|
14840
|
+
const router = new Hono27();
|
|
14841
|
+
for (const path of [
|
|
14842
|
+
"/:id/memories",
|
|
14843
|
+
"/:id/memories/*",
|
|
14844
|
+
"/:id/memory-events",
|
|
14845
|
+
"/:id/memory-events/*"
|
|
14846
|
+
]) {
|
|
14847
|
+
router.use(path, requireAgentMemory, requireChannelVisible, requireDocsAccess);
|
|
14848
|
+
}
|
|
14849
|
+
router.get("/:id/memories", async (c) => {
|
|
14850
|
+
return c.json({ memories: await listChannelMemories(c.get("memoryChannelId")) });
|
|
14851
|
+
});
|
|
14852
|
+
router.patch("/:id/memories/:memoryId", async (c) => {
|
|
14853
|
+
const memoryId = c.req.param("memoryId");
|
|
14854
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
14855
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14856
|
+
const patch = {};
|
|
14857
|
+
if (typeof body["description"] === "string") patch.description = body["description"];
|
|
14858
|
+
if (typeof body["body"] === "string") patch.body = body["body"];
|
|
14859
|
+
const type = body["type"];
|
|
14860
|
+
if (type !== void 0) {
|
|
14861
|
+
if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
|
|
14862
|
+
patch.type = type;
|
|
14863
|
+
}
|
|
14864
|
+
try {
|
|
14865
|
+
const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
|
|
14866
|
+
if (!memory) return c.json({ error: "memory not found" }, 404);
|
|
14867
|
+
return c.json({ memory });
|
|
14868
|
+
} catch (err) {
|
|
14869
|
+
return c.json({ error: err.message }, 400);
|
|
14870
|
+
}
|
|
14871
|
+
});
|
|
14872
|
+
router.delete("/:id/memories/:memoryId", async (c) => {
|
|
14873
|
+
const memoryId = c.req.param("memoryId");
|
|
14874
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
14875
|
+
const deleted = await deleteChannelMemory(c.get("memoryChannelId"), memoryId, "user");
|
|
14876
|
+
if (!deleted) return c.json({ error: "memory not found" }, 404);
|
|
14877
|
+
return c.json({ ok: true });
|
|
14878
|
+
});
|
|
14879
|
+
router.get("/:id/memory-events", async (c) => {
|
|
14880
|
+
const threadId = c.req.query("thread") ?? "";
|
|
14881
|
+
if (!isUuid(threadId)) return c.json({ error: "thread query parameter required" }, 400);
|
|
14882
|
+
return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
|
|
14883
|
+
});
|
|
14884
|
+
router.post("/:id/memory-events/:eventId/undo", async (c) => {
|
|
14885
|
+
const eventId = c.req.param("eventId");
|
|
14886
|
+
if (!isUuid(eventId)) return c.json({ error: "invalid event id" }, 400);
|
|
14887
|
+
const result = await undoMemoryEvent(c.get("memoryChannelId"), eventId);
|
|
14888
|
+
if (result === "not-found") return c.json({ error: "event not found" }, 404);
|
|
14889
|
+
if (result === "already-undone") return c.json({ error: "already undone" }, 409);
|
|
14890
|
+
if (result === "conflict") {
|
|
14891
|
+
return c.json(
|
|
14892
|
+
{ error: "The memory has changed since, so this can no longer be undone." },
|
|
14893
|
+
409
|
|
14894
|
+
);
|
|
14895
|
+
}
|
|
14896
|
+
return c.json({ ok: true });
|
|
14897
|
+
});
|
|
14898
|
+
return router;
|
|
14899
|
+
}
|
|
14900
|
+
|
|
14901
|
+
// src/dashboard/onboarding-routes.ts
|
|
14902
|
+
import { Hono as Hono28 } from "hono";
|
|
14359
14903
|
|
|
14360
14904
|
// src/dashboard/unique-channel-name.ts
|
|
14361
14905
|
function uniqueChannelName(raw, taken) {
|
|
@@ -14444,7 +14988,7 @@ async function buildGreeting(input2) {
|
|
|
14444
14988
|
// src/dashboard/onboarding-routes.ts
|
|
14445
14989
|
var MAX_FOLDERS = 20;
|
|
14446
14990
|
function createOnboardingRouter(agent) {
|
|
14447
|
-
const app = new
|
|
14991
|
+
const app = new Hono28();
|
|
14448
14992
|
app.get("/scan", async (c) => {
|
|
14449
14993
|
const ctx = await resolveAuthContext(c);
|
|
14450
14994
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -14689,6 +15233,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14689
15233
|
api.route("/channels", createDocShareWriteRouter(agent));
|
|
14690
15234
|
api.route("/onboarding", createOnboardingRouter(agent));
|
|
14691
15235
|
api.route("/channels", createChannelMembersRouter());
|
|
15236
|
+
api.route("/channels", createMemoriesRouter());
|
|
14692
15237
|
api.route("/docs", createDocShareListRouter());
|
|
14693
15238
|
api.get("/users", (c) => c.json({ users: listActiveUsers() }));
|
|
14694
15239
|
api.route("/categories", createCategoryWriteRouter());
|
|
@@ -14939,20 +15484,20 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14939
15484
|
// src/dashboard/device-auth-routes.ts
|
|
14940
15485
|
import { verify } from "crypto";
|
|
14941
15486
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
14942
|
-
import { Hono as
|
|
15487
|
+
import { Hono as Hono29 } from "hono";
|
|
14943
15488
|
|
|
14944
15489
|
// src/dashboard/agent-bridge.ts
|
|
14945
15490
|
import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
|
|
14946
|
-
import { existsSync as existsSync25, readFileSync as
|
|
15491
|
+
import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
|
|
14947
15492
|
import dotenv from "dotenv";
|
|
14948
15493
|
import { WebSocket, WebSocketServer } from "ws";
|
|
14949
15494
|
|
|
14950
15495
|
// src/dashboard/host-identity.ts
|
|
14951
15496
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
|
|
14952
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as
|
|
14953
|
-
import { dirname as
|
|
15497
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
15498
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
14954
15499
|
function hostPrivateKeyPath() {
|
|
14955
|
-
return process.env["FLYSPACE_HOST_KEY_PATH"] ||
|
|
15500
|
+
return process.env["FLYSPACE_HOST_KEY_PATH"] || join21(paths.BASE_CONFIG_DIR, "host.key");
|
|
14956
15501
|
}
|
|
14957
15502
|
function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
14958
15503
|
if (!existsSync24(path)) {
|
|
@@ -14960,7 +15505,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
|
14960
15505
|
`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.`
|
|
14961
15506
|
);
|
|
14962
15507
|
}
|
|
14963
|
-
return createPrivateKey2(
|
|
15508
|
+
return createPrivateKey2(readFileSync13(path, "utf8"));
|
|
14964
15509
|
}
|
|
14965
15510
|
function loadHostPublicKey() {
|
|
14966
15511
|
const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
|
|
@@ -14976,10 +15521,10 @@ function loadHostPublicKey() {
|
|
|
14976
15521
|
// src/dashboard/host-lock.ts
|
|
14977
15522
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
14978
15523
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
14979
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
14980
|
-
import { join as
|
|
15524
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
|
|
15525
|
+
import { join as join22 } from "path";
|
|
14981
15526
|
function hostLockPath() {
|
|
14982
|
-
return
|
|
15527
|
+
return join22(paths.BASE_DATA_DIR, "host.lock");
|
|
14983
15528
|
}
|
|
14984
15529
|
function isLiveFlyspace(pid) {
|
|
14985
15530
|
try {
|
|
@@ -14994,7 +15539,7 @@ function isLiveFlyspace(pid) {
|
|
|
14994
15539
|
}
|
|
14995
15540
|
function readLockPid(path) {
|
|
14996
15541
|
try {
|
|
14997
|
-
const pid = Number(JSON.parse(
|
|
15542
|
+
const pid = Number(JSON.parse(readFileSync14(path, "utf8")).pid);
|
|
14998
15543
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
14999
15544
|
} catch {
|
|
15000
15545
|
return null;
|
|
@@ -15035,13 +15580,13 @@ function publishHostEndpoint(port) {
|
|
|
15035
15580
|
const path = hostLockPath();
|
|
15036
15581
|
if (readLockPid(path) !== process.pid) return null;
|
|
15037
15582
|
const token = randomBytes5(32).toString("hex");
|
|
15038
|
-
const lock = JSON.parse(
|
|
15583
|
+
const lock = JSON.parse(readFileSync14(path, "utf8"));
|
|
15039
15584
|
writeFileSync9(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
15040
15585
|
return token;
|
|
15041
15586
|
}
|
|
15042
15587
|
function readHostEndpoint() {
|
|
15043
15588
|
try {
|
|
15044
|
-
const { pid, port, token } = JSON.parse(
|
|
15589
|
+
const { pid, port, token } = JSON.parse(readFileSync14(hostLockPath(), "utf8"));
|
|
15045
15590
|
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
15046
15591
|
return { port, token };
|
|
15047
15592
|
} catch {
|
|
@@ -15281,7 +15826,7 @@ async function userEmail(userId) {
|
|
|
15281
15826
|
|
|
15282
15827
|
// src/dashboard/doc-share-publisher.ts
|
|
15283
15828
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
15284
|
-
import { readFileSync as
|
|
15829
|
+
import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
|
|
15285
15830
|
import { extname as extname2, posix } from "path";
|
|
15286
15831
|
import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
|
|
15287
15832
|
|
|
@@ -15468,7 +16013,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
15468
16013
|
}
|
|
15469
16014
|
if (!stat.isFile()) return { status: "not-found" };
|
|
15470
16015
|
if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
|
|
15471
|
-
const source =
|
|
16016
|
+
const source = readFileSync15(abs, "utf8");
|
|
15472
16017
|
const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
|
|
15473
16018
|
if (kind === "excalidraw") {
|
|
15474
16019
|
try {
|
|
@@ -15548,7 +16093,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
15548
16093
|
let text = null;
|
|
15549
16094
|
try {
|
|
15550
16095
|
const size = statSync9(docAbs).size;
|
|
15551
|
-
if (size <= budget) text =
|
|
16096
|
+
if (size <= budget) text = readFileSync15(docAbs, "utf8");
|
|
15552
16097
|
} catch {
|
|
15553
16098
|
continue;
|
|
15554
16099
|
}
|
|
@@ -15565,7 +16110,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
15565
16110
|
let body;
|
|
15566
16111
|
try {
|
|
15567
16112
|
if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
|
|
15568
|
-
body =
|
|
16113
|
+
body = readFileSync15(resolved2.abs);
|
|
15569
16114
|
} catch {
|
|
15570
16115
|
continue;
|
|
15571
16116
|
}
|
|
@@ -15607,7 +16152,7 @@ async function publishDocShare(input2) {
|
|
|
15607
16152
|
...HTML_FEATURES,
|
|
15608
16153
|
brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
|
|
15609
16154
|
} : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
|
|
15610
|
-
const
|
|
16155
|
+
const snapshot3 = {
|
|
15611
16156
|
...built,
|
|
15612
16157
|
sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
|
|
15613
16158
|
};
|
|
@@ -15617,8 +16162,8 @@ async function publishDocShare(input2) {
|
|
|
15617
16162
|
const renamed = await setDocShareSlug(existing.id, slug);
|
|
15618
16163
|
if (!renamed) return { status: "not-found" };
|
|
15619
16164
|
}
|
|
15620
|
-
const share2 = await updateDocShareSnapshot(existing.id,
|
|
15621
|
-
const stale = existing.assetKeys.filter((key) => !
|
|
16165
|
+
const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
|
|
16166
|
+
const stale = existing.assetKeys.filter((key) => !snapshot3.assetKeys.includes(key));
|
|
15622
16167
|
await deleteShareAssets(stale);
|
|
15623
16168
|
if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
|
|
15624
16169
|
void queueShareAudio(share2);
|
|
@@ -15632,7 +16177,7 @@ async function publishDocShare(input2) {
|
|
|
15632
16177
|
channelId: input2.channelId,
|
|
15633
16178
|
sourcePath: input2.sourcePath,
|
|
15634
16179
|
createdBy: input2.createdBy,
|
|
15635
|
-
...
|
|
16180
|
+
...snapshot3,
|
|
15636
16181
|
...features
|
|
15637
16182
|
});
|
|
15638
16183
|
void queueShareAudio(share);
|
|
@@ -15747,8 +16292,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
|
|
|
15747
16292
|
const scope = table === "skill_files" && isWrite ? assigned : workspaces;
|
|
15748
16293
|
if (scope.length === 0) return deny(`This Host is not the assigned Host for any workspace it may write ${table} to.`);
|
|
15749
16294
|
if (method === "POST") {
|
|
15750
|
-
const
|
|
15751
|
-
if (!
|
|
16295
|
+
const ok3 = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
|
|
16296
|
+
if (!ok3) return deny(`Every ${table} row must belong to a workspace this Host is assigned.`);
|
|
15752
16297
|
}
|
|
15753
16298
|
if (method === "PATCH") {
|
|
15754
16299
|
const patch = bodyRows(req.body);
|
|
@@ -15762,8 +16307,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
|
|
|
15762
16307
|
if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
|
|
15763
16308
|
if (table === "threads") {
|
|
15764
16309
|
if (method === "POST") {
|
|
15765
|
-
const
|
|
15766
|
-
if (!
|
|
16310
|
+
const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
|
|
16311
|
+
if (!ok3) return deny("A thread must belong to a channel of the device workspaces.");
|
|
15767
16312
|
}
|
|
15768
16313
|
if (method === "PATCH") {
|
|
15769
16314
|
const patch = bodyRows(req.body);
|
|
@@ -15942,7 +16487,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
15942
16487
|
regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
|
|
15943
16488
|
listAgentRuns: () => chatAgent.runningThreadIds(),
|
|
15944
16489
|
listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
|
|
15945
|
-
listRunningProfiles: () => listProfiles().filter(
|
|
16490
|
+
listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
|
|
15946
16491
|
getAgentIdentities: () => resolveAgentIdentities(),
|
|
15947
16492
|
publishDocShare: (input2) => publishDocShare(input2),
|
|
15948
16493
|
listChannelSkills: (args) => listSkills(args.claudeConfigDir, args.cwd),
|
|
@@ -15959,14 +16504,14 @@ function localAgentCallbacks(chatAgent) {
|
|
|
15959
16504
|
}
|
|
15960
16505
|
var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
|
|
15961
16506
|
function buildHostConfigSnapshot() {
|
|
15962
|
-
const
|
|
16507
|
+
const snapshot3 = {};
|
|
15963
16508
|
for (const profile of listProfiles()) {
|
|
15964
|
-
|
|
15965
|
-
env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(
|
|
16509
|
+
snapshot3[profile] = runWithProfile(profile, () => ({
|
|
16510
|
+
env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync16(paths.CONFIG_FILE, "utf-8")) : {},
|
|
15966
16511
|
agentModel: loadDashboardSettings().agent
|
|
15967
16512
|
}));
|
|
15968
16513
|
}
|
|
15969
|
-
return
|
|
16514
|
+
return snapshot3;
|
|
15970
16515
|
}
|
|
15971
16516
|
function startLocalProfileConfigHydration() {
|
|
15972
16517
|
setProfileConfigCache(buildHostConfigSnapshot());
|
|
@@ -16208,7 +16753,8 @@ function remoteAgentCallbacks() {
|
|
|
16208
16753
|
// Host connected, showing nobody as running is a reasonable default
|
|
16209
16754
|
// rather than a false claim of certainty either way.
|
|
16210
16755
|
listRunningProfiles: async () => await callHost("listRunningProfiles", []) ?? [],
|
|
16211
|
-
|
|
16756
|
+
// `null` (no Host) falls back to `offlineAgentIdentities` in api.ts.
|
|
16757
|
+
getAgentIdentities: () => callHost("getAgentIdentities", []),
|
|
16212
16758
|
// Reading the doc and copying its assets can only happen where the
|
|
16213
16759
|
// channel's real folder is, so this always goes to the Host. `null` (no
|
|
16214
16760
|
// Host connected) surfaces as `host-unavailable` — see
|
|
@@ -16492,7 +17038,7 @@ async function boundWorkspaceIds(publicKey, userId) {
|
|
|
16492
17038
|
return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
|
|
16493
17039
|
}
|
|
16494
17040
|
function createDeviceAuthRouter() {
|
|
16495
|
-
const app = new
|
|
17041
|
+
const app = new Hono29();
|
|
16496
17042
|
app.post("/code", async (c) => {
|
|
16497
17043
|
const body = await readBody(c);
|
|
16498
17044
|
const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
|
|
@@ -16555,8 +17101,8 @@ function createDeviceAuthRouter() {
|
|
|
16555
17101
|
if (!device || device.revokedAt || !nonce || !signature) return c.json({ error: "invalid_grant" }, 401);
|
|
16556
17102
|
if (!await consumeChallenge(device.id, nonce)) return c.json({ error: "invalid_grant" }, 401);
|
|
16557
17103
|
const key = parseEd25519PublicKey(device.publicKey);
|
|
16558
|
-
const
|
|
16559
|
-
if (!
|
|
17104
|
+
const ok3 = key && verify(null, Buffer.from(nonce), key, Buffer.from(signature, "base64"));
|
|
17105
|
+
if (!ok3) return c.json({ error: "invalid_grant" }, 401);
|
|
16560
17106
|
const { token, expiresInS } = await issueAccessToken(device.id);
|
|
16561
17107
|
return c.json({ access_token: token, token_type: "Bearer", expires_in: expiresInS });
|
|
16562
17108
|
});
|
|
@@ -16656,7 +17202,7 @@ function createDeviceAuthRouter() {
|
|
|
16656
17202
|
|
|
16657
17203
|
// src/dashboard/api.ts
|
|
16658
17204
|
function createApiRouter(agent = {}, mode = "all") {
|
|
16659
|
-
const api = new
|
|
17205
|
+
const api = new Hono30();
|
|
16660
17206
|
api.use("*", async (c, next) => {
|
|
16661
17207
|
const known = await listKnownWorkspaces();
|
|
16662
17208
|
const fromHost = profileFromHost(trustedHost(c), known);
|
|
@@ -16703,7 +17249,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
16703
17249
|
if (mode === "data") {
|
|
16704
17250
|
api.get(
|
|
16705
17251
|
"/settings/agents",
|
|
16706
|
-
async (c) => c.json({ agents: await agent.getAgentIdentities?.() ??
|
|
17252
|
+
async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? await offlineAgentIdentities() })
|
|
16707
17253
|
);
|
|
16708
17254
|
api.get("/channels/:id/skills", async (c) => {
|
|
16709
17255
|
const { cwd, claudeConfigDir } = resolveChannelConfig(c.req.param("id"));
|
|
@@ -16767,7 +17313,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
16767
17313
|
}
|
|
16768
17314
|
|
|
16769
17315
|
// src/dashboard/app-icon-routes.ts
|
|
16770
|
-
import { Hono as
|
|
17316
|
+
import { Hono as Hono31 } from "hono";
|
|
16771
17317
|
var NEUTRAL_MANIFEST = {
|
|
16772
17318
|
id: "/",
|
|
16773
17319
|
name: "Flyspace",
|
|
@@ -16805,7 +17351,7 @@ async function withResolvedProfile2(c, next) {
|
|
|
16805
17351
|
return profile ? runWithProfile(profile, () => next()) : next();
|
|
16806
17352
|
}
|
|
16807
17353
|
function createAppIconRouter() {
|
|
16808
|
-
const app = new
|
|
17354
|
+
const app = new Hono31();
|
|
16809
17355
|
app.get("/manifest.json", withResolvedProfile2, async (c) => {
|
|
16810
17356
|
if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
|
|
16811
17357
|
const profile = getProfile();
|
|
@@ -16845,7 +17391,7 @@ function pngResponse(c, png) {
|
|
|
16845
17391
|
}
|
|
16846
17392
|
|
|
16847
17393
|
// src/dashboard/stripe-routes.ts
|
|
16848
|
-
import { Hono as
|
|
17394
|
+
import { Hono as Hono32 } from "hono";
|
|
16849
17395
|
function planStatusFor(status) {
|
|
16850
17396
|
const known = ["trialing", "active", "paused", "canceled"];
|
|
16851
17397
|
return known.includes(status) ? status : null;
|
|
@@ -16888,7 +17434,7 @@ async function handleCardAdded(session) {
|
|
|
16888
17434
|
}
|
|
16889
17435
|
}
|
|
16890
17436
|
function createStripeWebhookRouter() {
|
|
16891
|
-
const stripeRouter = new
|
|
17437
|
+
const stripeRouter = new Hono32();
|
|
16892
17438
|
stripeRouter.post("/webhook", async (c) => {
|
|
16893
17439
|
const secret = process.env["STRIPE_WEBHOOK_SECRET"];
|
|
16894
17440
|
const signature = c.req.header("stripe-signature");
|
|
@@ -16999,15 +17545,15 @@ function startHostStatusBroadcast() {
|
|
|
16999
17545
|
|
|
17000
17546
|
// src/dashboard/server.ts
|
|
17001
17547
|
var DEFAULT_DASHBOARD_PORT = 4747;
|
|
17002
|
-
var WEB_DIST =
|
|
17548
|
+
var WEB_DIST = join23(paths.PACKAGE_ROOT, "web", "dist");
|
|
17003
17549
|
var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
|
|
17004
17550
|
async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
|
|
17005
17551
|
for (let port = start; port < start + attempts; port++) {
|
|
17006
17552
|
const free = await new Promise((resolve8) => {
|
|
17007
|
-
const
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17553
|
+
const probe3 = createServer();
|
|
17554
|
+
probe3.once("error", () => resolve8(false));
|
|
17555
|
+
probe3.once("listening", () => probe3.close(() => resolve8(true)));
|
|
17556
|
+
probe3.listen(port, host);
|
|
17011
17557
|
});
|
|
17012
17558
|
if (free) return port;
|
|
17013
17559
|
}
|
|
@@ -17048,7 +17594,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17048
17594
|
const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
|
|
17049
17595
|
const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
|
|
17050
17596
|
};
|
|
17051
|
-
const app = new
|
|
17597
|
+
const app = new Hono33();
|
|
17052
17598
|
app.use("*", async (c, next) => {
|
|
17053
17599
|
await next();
|
|
17054
17600
|
if (c.req.path.startsWith("/assets/")) {
|
|
@@ -17073,14 +17619,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17073
17619
|
} else {
|
|
17074
17620
|
app.route("/share", createPublicShareAssetRouter(WEB_DIST));
|
|
17075
17621
|
app.route("/", createAppIconRouter());
|
|
17076
|
-
if (existsSync26(WEB_DIST) || existsSync26(
|
|
17622
|
+
if (existsSync26(WEB_DIST) || existsSync26(join23(paths.PACKAGE_ROOT, "web", "package.json"))) {
|
|
17077
17623
|
app.use("/*", serveStatic({ root: WEB_DIST }));
|
|
17078
17624
|
}
|
|
17079
17625
|
app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
|
|
17080
|
-
const spaFallback = serveStatic({ path:
|
|
17626
|
+
const spaFallback = serveStatic({ path: join23(WEB_DIST, "index.html") });
|
|
17081
17627
|
app.get("*", async (c, next) => {
|
|
17082
|
-
if (!existsSync26(
|
|
17083
|
-
const isCheckout = existsSync26(
|
|
17628
|
+
if (!existsSync26(join23(WEB_DIST, "index.html"))) {
|
|
17629
|
+
const isCheckout = existsSync26(join23(paths.PACKAGE_ROOT, "web", "package.json"));
|
|
17084
17630
|
return c.text(
|
|
17085
17631
|
isCheckout ? `Workspace UI build missing (${WEB_DIST}).
|
|
17086
17632
|
Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
|
|
@@ -17135,7 +17681,7 @@ function watchServices() {
|
|
|
17135
17681
|
|
|
17136
17682
|
// src/slack/image-uploader.ts
|
|
17137
17683
|
import { readFile } from "fs/promises";
|
|
17138
|
-
import { basename as
|
|
17684
|
+
import { basename as basename7 } from "path";
|
|
17139
17685
|
import sharp6 from "sharp";
|
|
17140
17686
|
var MAX_WIDTH = 800;
|
|
17141
17687
|
async function uploadImage(client2, { channel, filePath, threadTs, text }) {
|
|
@@ -17144,7 +17690,7 @@ async function uploadImage(client2, { channel, filePath, threadTs, text }) {
|
|
|
17144
17690
|
const base = {
|
|
17145
17691
|
channel_id: channel,
|
|
17146
17692
|
file: resized,
|
|
17147
|
-
filename:
|
|
17693
|
+
filename: basename7(filePath),
|
|
17148
17694
|
...text ? { initial_comment: text } : {}
|
|
17149
17695
|
};
|
|
17150
17696
|
const result = await client2.filesUploadV2(threadTs ? { ...base, thread_ts: threadTs } : base);
|
|
@@ -17518,7 +18064,7 @@ async function threadReplies(client2, nameOrId, ts2) {
|
|
|
17518
18064
|
async function postMessage(client2, args) {
|
|
17519
18065
|
const channelArg = getArg2(args, "--channel");
|
|
17520
18066
|
const textFilePath = getArg2(args, "--text-file");
|
|
17521
|
-
const text = textFilePath ?
|
|
18067
|
+
const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
|
|
17522
18068
|
const threadTs = getArg2(args, "--thread");
|
|
17523
18069
|
const markdown = args.includes("--markdown");
|
|
17524
18070
|
const broadcast = args.includes("--broadcast") && Boolean(threadTs);
|
|
@@ -17673,7 +18219,7 @@ async function ensureClaudeCodeReady() {
|
|
|
17673
18219
|
}
|
|
17674
18220
|
function getManifestYaml(botName) {
|
|
17675
18221
|
const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
|
|
17676
|
-
const raw =
|
|
18222
|
+
const raw = readFileSync17(manifestPath, "utf-8");
|
|
17677
18223
|
return raw.replace(/name: Custie/g, `name: ${botName}`);
|
|
17678
18224
|
}
|
|
17679
18225
|
async function stepPersonalise() {
|
|
@@ -17886,7 +18432,7 @@ async function stepBrowserTokens(botName) {
|
|
|
17886
18432
|
})
|
|
17887
18433
|
));
|
|
17888
18434
|
const DEBUG = process.argv.includes("--debug");
|
|
17889
|
-
const USER_DATA_DIR = resolve7(
|
|
18435
|
+
const USER_DATA_DIR = resolve7(tmpdir4(), "custie-playwright-profile");
|
|
17890
18436
|
let context;
|
|
17891
18437
|
try {
|
|
17892
18438
|
log4("Opening browser \u2014 a Slack login page will appear.\n");
|
|
@@ -18107,7 +18653,7 @@ function copyDefaultPrompt(botName) {
|
|
|
18107
18653
|
if (!existsSync27(paths.PROMPT_FILE)) {
|
|
18108
18654
|
const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
|
|
18109
18655
|
if (existsSync27(defaultPrompt)) {
|
|
18110
|
-
const content =
|
|
18656
|
+
const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
|
|
18111
18657
|
writeFileSync10(paths.PROMPT_FILE, content);
|
|
18112
18658
|
success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
|
|
18113
18659
|
}
|
|
@@ -18429,8 +18975,122 @@ async function runChatCmd(args) {
|
|
|
18429
18975
|
}
|
|
18430
18976
|
}
|
|
18431
18977
|
|
|
18432
|
-
// src/commands/
|
|
18978
|
+
// src/commands/memory.ts
|
|
18979
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
18433
18980
|
var USAGE5 = `
|
|
18981
|
+
Usage: flyspace memory <subcommand> --channel <name-or-id> [options]
|
|
18982
|
+
|
|
18983
|
+
A channel's persistent agent memory (docs/features/workspace-memory.md).
|
|
18984
|
+
|
|
18985
|
+
Subcommands:
|
|
18986
|
+
list Every memory in the channel (index form)
|
|
18987
|
+
get <name> One memory in full
|
|
18988
|
+
search <words...> Memories whose name, description or body match
|
|
18989
|
+
save --name <slug> --type <type> Create, or overwrite the memory with that name
|
|
18990
|
+
--description <one line>
|
|
18991
|
+
--body <text | -> "-" reads the body from stdin
|
|
18992
|
+
[--thread <id>] The thread the memory came from
|
|
18993
|
+
delete <name-or-id> [--thread <id>] Delete one memory
|
|
18994
|
+
|
|
18995
|
+
Types: ${MEMORY_TYPES.join(", ")}
|
|
18996
|
+
`;
|
|
18997
|
+
function getArg4(args, flag) {
|
|
18998
|
+
const idx = args.indexOf(flag);
|
|
18999
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
19000
|
+
}
|
|
19001
|
+
function positionals(args) {
|
|
19002
|
+
const out = [];
|
|
19003
|
+
for (let i = 0; i < args.length; i++) {
|
|
19004
|
+
if (args[i].startsWith("--")) i++;
|
|
19005
|
+
else out.push(args[i]);
|
|
19006
|
+
}
|
|
19007
|
+
return out;
|
|
19008
|
+
}
|
|
19009
|
+
function indexLine(m) {
|
|
19010
|
+
return `${m.name} (${m.type}, ${m.updatedAt.slice(0, 10)}) ${m.description}`;
|
|
19011
|
+
}
|
|
19012
|
+
async function runMemoryCmd(args) {
|
|
19013
|
+
loadEnvFiles();
|
|
19014
|
+
const subcommand = args[0];
|
|
19015
|
+
const rest = args.slice(1);
|
|
19016
|
+
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
19017
|
+
console.log(USAGE5);
|
|
19018
|
+
return;
|
|
19019
|
+
}
|
|
19020
|
+
const channelArg = getArg4(rest, "--channel");
|
|
19021
|
+
if (!channelArg) throw new Error("--channel <name-or-id> is required");
|
|
19022
|
+
const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
|
|
19023
|
+
const words = positionals(rest);
|
|
19024
|
+
switch (subcommand) {
|
|
19025
|
+
case "list": {
|
|
19026
|
+
const memories = await listChannelMemories(channel.id);
|
|
19027
|
+
console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
|
|
19028
|
+
break;
|
|
19029
|
+
}
|
|
19030
|
+
case "get": {
|
|
19031
|
+
const name = words[0];
|
|
19032
|
+
if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
|
|
19033
|
+
const memory = await getChannelMemory(channel.id, name);
|
|
19034
|
+
if (!memory) throw new Error(`No memory named ${name} in ${channel.name}.`);
|
|
19035
|
+
console.log(`# ${memory.name} (${memory.type})
|
|
19036
|
+
|
|
19037
|
+
${memory.description}
|
|
19038
|
+
|
|
19039
|
+
${memory.body}`);
|
|
19040
|
+
break;
|
|
19041
|
+
}
|
|
19042
|
+
case "search": {
|
|
19043
|
+
if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
|
|
19044
|
+
const hits = await searchChannelMemories(channel.id, words.join(" "));
|
|
19045
|
+
console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
|
|
19046
|
+
break;
|
|
19047
|
+
}
|
|
19048
|
+
case "save": {
|
|
19049
|
+
const name = getArg4(rest, "--name");
|
|
19050
|
+
const type = getArg4(rest, "--type");
|
|
19051
|
+
const description = getArg4(rest, "--description");
|
|
19052
|
+
const bodyArg = getArg4(rest, "--body");
|
|
19053
|
+
if (!name || !type || !description || bodyArg === void 0) {
|
|
19054
|
+
throw new Error(
|
|
19055
|
+
"Usage: flyspace memory save --channel <c> --name <slug> --type <type> --description <one line> --body <text | -> [--thread <id>]"
|
|
19056
|
+
);
|
|
19057
|
+
}
|
|
19058
|
+
if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
|
|
19059
|
+
const body = bodyArg === "-" ? readFileSync18(0, "utf-8") : bodyArg;
|
|
19060
|
+
const thread = getArg4(rest, "--thread");
|
|
19061
|
+
const { created, changed } = await saveChannelMemory(
|
|
19062
|
+
{
|
|
19063
|
+
channelId: channel.id,
|
|
19064
|
+
name,
|
|
19065
|
+
type,
|
|
19066
|
+
description,
|
|
19067
|
+
body,
|
|
19068
|
+
createdBy: "agent",
|
|
19069
|
+
...thread ? { sourceThreadId: thread } : {}
|
|
19070
|
+
},
|
|
19071
|
+
"agent"
|
|
19072
|
+
);
|
|
19073
|
+
const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
|
|
19074
|
+
console.log(`${verb} memory ${name} in ${channel.name}.`);
|
|
19075
|
+
break;
|
|
19076
|
+
}
|
|
19077
|
+
case "delete": {
|
|
19078
|
+
const ref = words[0];
|
|
19079
|
+
if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
|
|
19080
|
+
const deleted = await deleteChannelMemory(channel.id, ref, "agent", getArg4(rest, "--thread"));
|
|
19081
|
+
if (!deleted) throw new Error(`No memory ${ref} in ${channel.name}.`);
|
|
19082
|
+
console.log(`Deleted memory ${ref}.`);
|
|
19083
|
+
break;
|
|
19084
|
+
}
|
|
19085
|
+
default:
|
|
19086
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
19087
|
+
console.log(USAGE5);
|
|
19088
|
+
process.exit(1);
|
|
19089
|
+
}
|
|
19090
|
+
}
|
|
19091
|
+
|
|
19092
|
+
// src/commands/mcp.ts
|
|
19093
|
+
var USAGE6 = `
|
|
18434
19094
|
Usage: flyspace mcp <server>
|
|
18435
19095
|
|
|
18436
19096
|
Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
|
|
@@ -18447,7 +19107,7 @@ Usage: flyspace mcp <server>
|
|
|
18447
19107
|
async function runMcpCmd(args) {
|
|
18448
19108
|
const server = args[0];
|
|
18449
19109
|
if (!server || server === "-h" || server === "--help") {
|
|
18450
|
-
console.log(
|
|
19110
|
+
console.log(USAGE6.trim());
|
|
18451
19111
|
return;
|
|
18452
19112
|
}
|
|
18453
19113
|
restoreStdout();
|
|
@@ -18460,7 +19120,7 @@ async function runMcpCmd(args) {
|
|
|
18460
19120
|
}
|
|
18461
19121
|
default:
|
|
18462
19122
|
console.error(`Unknown MCP server: ${server}`);
|
|
18463
|
-
console.error(
|
|
19123
|
+
console.error(USAGE6.trim());
|
|
18464
19124
|
process.exit(1);
|
|
18465
19125
|
}
|
|
18466
19126
|
}
|
|
@@ -18469,7 +19129,7 @@ async function runMcpCmd(args) {
|
|
|
18469
19129
|
import { spawn as spawn6, spawnSync } from "child_process";
|
|
18470
19130
|
import { existsSync as existsSync28 } from "fs";
|
|
18471
19131
|
import { platform as platform7 } from "os";
|
|
18472
|
-
import { join as
|
|
19132
|
+
import { join as join24 } from "path";
|
|
18473
19133
|
function flagValue(args, name) {
|
|
18474
19134
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
18475
19135
|
if (eq) return eq.slice(`--${name}=`.length);
|
|
@@ -18484,8 +19144,8 @@ function openBrowser2(url) {
|
|
|
18484
19144
|
}
|
|
18485
19145
|
}
|
|
18486
19146
|
function ensureWebBuild() {
|
|
18487
|
-
if (existsSync28(
|
|
18488
|
-
if (!existsSync28(
|
|
19147
|
+
if (existsSync28(join24(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
|
|
19148
|
+
if (!existsSync28(join24(paths.PACKAGE_ROOT, "web", "package.json"))) return;
|
|
18489
19149
|
console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
|
|
18490
19150
|
const result = spawnSync("pnpm", ["--dir", "web", "build"], {
|
|
18491
19151
|
cwd: paths.PACKAGE_ROOT,
|
|
@@ -18536,7 +19196,7 @@ async function runDashboard(args) {
|
|
|
18536
19196
|
|
|
18537
19197
|
// src/commands/workspace.ts
|
|
18538
19198
|
import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
18539
|
-
var
|
|
19199
|
+
var USAGE7 = `
|
|
18540
19200
|
Usage: flyspace workspace <subcommand> [options]
|
|
18541
19201
|
|
|
18542
19202
|
Subcommands:
|
|
@@ -18553,7 +19213,7 @@ var USAGE6 = `
|
|
|
18553
19213
|
--no-invite Never send an invite email. Fails if the admin
|
|
18554
19214
|
has no existing account.
|
|
18555
19215
|
`;
|
|
18556
|
-
function
|
|
19216
|
+
function getArg5(args, flag) {
|
|
18557
19217
|
const idx = args.indexOf(flag);
|
|
18558
19218
|
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
18559
19219
|
}
|
|
@@ -18649,10 +19309,10 @@ async function provisionChannels(specs) {
|
|
|
18649
19309
|
async function handleCreate(args) {
|
|
18650
19310
|
const slug = args[0];
|
|
18651
19311
|
if (!slug || slug.startsWith("-")) fail2("Usage: flyspace workspace create <slug> --admin-email <email>");
|
|
18652
|
-
const adminEmail =
|
|
19312
|
+
const adminEmail = getArg5(args, "--admin-email");
|
|
18653
19313
|
if (!adminEmail?.includes("@")) fail2("--admin-email is required and must be an email address");
|
|
18654
|
-
const botName =
|
|
18655
|
-
const baseDomain =
|
|
19314
|
+
const botName = getArg5(args, "--bot-name") ?? slug;
|
|
19315
|
+
const baseDomain = getArg5(args, "--base-domain") ?? "flyspace.ai";
|
|
18656
19316
|
const allowInvite = !args.includes("--no-invite");
|
|
18657
19317
|
const channels = getAllArgs(args, "--channel").map(parseChannelSpec);
|
|
18658
19318
|
loadEnvFiles();
|
|
@@ -18675,13 +19335,13 @@ async function runWorkspaceCmd(args) {
|
|
|
18675
19335
|
await handleCreate(args.slice(1));
|
|
18676
19336
|
break;
|
|
18677
19337
|
default:
|
|
18678
|
-
console.log(
|
|
19338
|
+
console.log(USAGE7);
|
|
18679
19339
|
process.exit(args[0] ? 1 : 0);
|
|
18680
19340
|
}
|
|
18681
19341
|
}
|
|
18682
19342
|
|
|
18683
19343
|
// src/commands/reconcile.ts
|
|
18684
|
-
var
|
|
19344
|
+
var USAGE8 = `
|
|
18685
19345
|
Usage: flyspace reconcile [<workspace> | --all]
|
|
18686
19346
|
|
|
18687
19347
|
Repair a workspace's local filesystem state (profile dir, config.env,
|
|
@@ -18701,7 +19361,7 @@ function printLog(name, log5) {
|
|
|
18701
19361
|
}
|
|
18702
19362
|
async function runReconcileCmd(args) {
|
|
18703
19363
|
if (args.includes("-h") || args.includes("--help")) {
|
|
18704
|
-
console.log(
|
|
19364
|
+
console.log(USAGE8);
|
|
18705
19365
|
return;
|
|
18706
19366
|
}
|
|
18707
19367
|
loadEnvFiles();
|
|
@@ -18717,12 +19377,13 @@ async function runReconcileCmd(args) {
|
|
|
18717
19377
|
|
|
18718
19378
|
// src/cli.ts
|
|
18719
19379
|
var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
|
|
18720
|
-
var
|
|
19380
|
+
var USAGE9 = `
|
|
18721
19381
|
Usage: flyspace <command> [options]
|
|
18722
19382
|
|
|
18723
19383
|
Commands:
|
|
18724
19384
|
setup Set up this machine as a Host (login, agent CLIs, ~/Flyspace)
|
|
18725
19385
|
auth Log in, check status, or log out this device
|
|
19386
|
+
doctor Check this Host end to end (login, agent CLIs, services)
|
|
18726
19387
|
start Start the agent host (Slack only if configured)
|
|
18727
19388
|
install Install as a system service (launchd / systemd)
|
|
18728
19389
|
uninstall Remove the system service
|
|
@@ -18736,6 +19397,7 @@ var USAGE8 = `
|
|
|
18736
19397
|
usage Show agent token usage and cost for this workspace
|
|
18737
19398
|
slack Query Slack (channels, users, post messages)
|
|
18738
19399
|
chat Inspect dashboard chat channels and move threads between them
|
|
19400
|
+
memory Read and write a channel's persistent agent memory
|
|
18739
19401
|
profiles List local profiles (bound here? scheduler?) or remove one
|
|
18740
19402
|
workspace Provision a workspace (Supabase rows + local profile dirs)
|
|
18741
19403
|
reconcile Repair a workspace's local files against Supabase
|
|
@@ -18776,6 +19438,9 @@ async function main() {
|
|
|
18776
19438
|
case "auth":
|
|
18777
19439
|
await runAuthCmd(args.slice(1));
|
|
18778
19440
|
break;
|
|
19441
|
+
case "doctor":
|
|
19442
|
+
await runDoctor(args.slice(1));
|
|
19443
|
+
break;
|
|
18779
19444
|
case "start":
|
|
18780
19445
|
await runStart();
|
|
18781
19446
|
break;
|
|
@@ -18824,6 +19489,9 @@ async function main() {
|
|
|
18824
19489
|
case "chat":
|
|
18825
19490
|
await runChatCmd(args.slice(1));
|
|
18826
19491
|
break;
|
|
19492
|
+
case "memory":
|
|
19493
|
+
await runMemoryCmd(args.slice(1));
|
|
19494
|
+
break;
|
|
18827
19495
|
case "profile":
|
|
18828
19496
|
case "profiles":
|
|
18829
19497
|
await runProfiles(args.slice(1));
|
|
@@ -18836,12 +19504,12 @@ async function main() {
|
|
|
18836
19504
|
break;
|
|
18837
19505
|
case "-v":
|
|
18838
19506
|
case "--version":
|
|
18839
|
-
printRaw("0.1.
|
|
19507
|
+
printRaw("0.1.10");
|
|
18840
19508
|
break;
|
|
18841
19509
|
case "-h":
|
|
18842
19510
|
case "--help":
|
|
18843
19511
|
case void 0:
|
|
18844
|
-
printRaw(
|
|
19512
|
+
printRaw(USAGE9);
|
|
18845
19513
|
break;
|
|
18846
19514
|
case "xero":
|
|
18847
19515
|
if (false) {
|
|
@@ -18852,7 +19520,7 @@ async function main() {
|
|
|
18852
19520
|
// falls through
|
|
18853
19521
|
default:
|
|
18854
19522
|
printRaw(`Unknown command: ${command}`);
|
|
18855
|
-
printRaw(
|
|
19523
|
+
printRaw(USAGE9);
|
|
18856
19524
|
process.exit(1);
|
|
18857
19525
|
}
|
|
18858
19526
|
}
|