magpie-agent-console 0.1.0 → 1.0.0
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/README.md +120 -77
- package/THIRD-PARTY-NOTICES.md +20 -18
- package/app/app.js +50 -50
- package/app/index.html +12 -4
- package/daemon/dist/main.js +842 -403
- package/daemon/dist/main.js.map +1 -7
- package/index.mjs +386 -177
- package/lib/gate.d.mts +9 -0
- package/lib/gate.mjs +157 -0
- package/lib/pathscan.d.mts +15 -0
- package/lib/pathscan.mjs +152 -0
- package/package.json +33 -30
- package/page/boot.js +73 -22
- package/page/index.html +8 -1
package/daemon/dist/main.js
CHANGED
|
@@ -71,17 +71,22 @@ var init_log = __esm({
|
|
|
71
71
|
|
|
72
72
|
// daemon/src/util/paths.ts
|
|
73
73
|
import { realpathSync, existsSync as existsSync3 } from "node:fs";
|
|
74
|
-
import {
|
|
75
|
-
function normPath(p) {
|
|
76
|
-
let r =
|
|
74
|
+
import { posix, sep, win32 } from "node:path";
|
|
75
|
+
function normPath(p, platform = process.platform) {
|
|
76
|
+
let r = (platform === "win32" ? win32 : posix).resolve(p);
|
|
77
77
|
try {
|
|
78
|
-
r = realpathSync(r);
|
|
78
|
+
r = realpathSync.native(r);
|
|
79
79
|
} catch {
|
|
80
|
+
try {
|
|
81
|
+
r = realpathSync(r);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
80
84
|
}
|
|
81
|
-
if (
|
|
82
|
-
r = r.replace(/\//g, "\\").toLowerCase();
|
|
85
|
+
if (platform === "win32") {
|
|
86
|
+
r = r.replace(/\//g, "\\").replace(/^\\\\\?\\(?!UNC\\)/i, "").replace(/^\\\\\?\\UNC\\/i, "\\\\").toLowerCase();
|
|
83
87
|
} else {
|
|
84
88
|
r = r.replace(/\\/g, "/");
|
|
89
|
+
if (platform === "darwin") r = r.normalize("NFC").toLowerCase();
|
|
85
90
|
}
|
|
86
91
|
while (r.length > 3 && (r.endsWith("/") || r.endsWith("\\"))) r = r.slice(0, -1);
|
|
87
92
|
return r;
|
|
@@ -662,21 +667,21 @@ var init_mock = __esm({
|
|
|
662
667
|
}
|
|
663
668
|
}
|
|
664
669
|
respond(requestId, answer) {
|
|
665
|
-
const
|
|
666
|
-
if (
|
|
670
|
+
const resolve2 = this.pendingReq.get(requestId);
|
|
671
|
+
if (resolve2) {
|
|
667
672
|
this.pendingReq.delete(requestId);
|
|
668
|
-
|
|
673
|
+
resolve2(answer);
|
|
669
674
|
}
|
|
670
675
|
}
|
|
671
676
|
waitFor(requestId) {
|
|
672
|
-
return new Promise((
|
|
673
|
-
this.pendingReq.set(requestId,
|
|
677
|
+
return new Promise((resolve2) => {
|
|
678
|
+
this.pendingReq.set(requestId, resolve2);
|
|
674
679
|
});
|
|
675
680
|
}
|
|
676
681
|
async interrupt() {
|
|
677
682
|
this.interruptFlag = true;
|
|
678
|
-
for (const [id,
|
|
679
|
-
|
|
683
|
+
for (const [id, resolve2] of this.pendingReq) {
|
|
684
|
+
resolve2({ cancelled: true });
|
|
680
685
|
this.pendingReq.delete(id);
|
|
681
686
|
}
|
|
682
687
|
await sleep(120);
|
|
@@ -704,11 +709,11 @@ var init_mock = __esm({
|
|
|
704
709
|
}
|
|
705
710
|
async close() {
|
|
706
711
|
this.closed = true;
|
|
707
|
-
for (const [,
|
|
712
|
+
for (const [, resolve2] of this.pendingReq) resolve2({ cancelled: true });
|
|
708
713
|
this.pendingReq.clear();
|
|
709
714
|
this.q.close();
|
|
710
715
|
}
|
|
711
|
-
// resumeId
|
|
716
|
+
// With resumeId the first reply gets a "(已续接)" prefix
|
|
712
717
|
resumePrefix() {
|
|
713
718
|
if (this.resumed && this.turnNum === 1) return "\uFF08\u5DF2\u7EED\u63A5\uFF09";
|
|
714
719
|
return "";
|
|
@@ -718,70 +723,192 @@ var init_mock = __esm({
|
|
|
718
723
|
}
|
|
719
724
|
});
|
|
720
725
|
|
|
726
|
+
// lib/pathscan.mjs
|
|
727
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
728
|
+
import { homedir as homedir2 } from "node:os";
|
|
729
|
+
import path from "node:path";
|
|
730
|
+
function defaultExists(file, platform) {
|
|
731
|
+
try {
|
|
732
|
+
if (!statSync2(file).isFile()) return false;
|
|
733
|
+
if (platform !== "win32") accessSync(file, constants.X_OK);
|
|
734
|
+
return true;
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function envGet(env, name) {
|
|
740
|
+
if (env[name] !== void 0) return env[name];
|
|
741
|
+
const lower = name.toLowerCase();
|
|
742
|
+
for (const key of Object.keys(env)) if (key.toLowerCase() === lower) return env[key];
|
|
743
|
+
return void 0;
|
|
744
|
+
}
|
|
745
|
+
function extraBinDirs({ platform = process.platform, env = process.env, home = homedir2() } = {}) {
|
|
746
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
747
|
+
const dirs = [];
|
|
748
|
+
if (platform === "win32") {
|
|
749
|
+
const appData = envGet(env, "APPDATA");
|
|
750
|
+
const localAppData = envGet(env, "LOCALAPPDATA");
|
|
751
|
+
const programFiles = envGet(env, "ProgramFiles") || "C:\\Program Files";
|
|
752
|
+
const nvmSymlink = envGet(env, "NVM_SYMLINK");
|
|
753
|
+
if (home) dirs.push(p.join(home, ".local", "bin"));
|
|
754
|
+
if (appData) dirs.push(p.join(appData, "npm"));
|
|
755
|
+
if (nvmSymlink) dirs.push(nvmSymlink);
|
|
756
|
+
dirs.push(p.join(programFiles, "nodejs"));
|
|
757
|
+
if (localAppData) dirs.push(p.join(localAppData, "Programs", "nodejs"));
|
|
758
|
+
if (home) {
|
|
759
|
+
dirs.push(p.join(home, ".volta", "bin"));
|
|
760
|
+
dirs.push(p.join(home, ".bun", "bin"));
|
|
761
|
+
}
|
|
762
|
+
return dirs;
|
|
763
|
+
}
|
|
764
|
+
if (home) {
|
|
765
|
+
dirs.push(p.join(home, ".local", "bin"));
|
|
766
|
+
dirs.push(p.join(home, ".npm-global", "bin"));
|
|
767
|
+
dirs.push(p.join(home, ".volta", "bin"));
|
|
768
|
+
dirs.push(p.join(home, ".bun", "bin"));
|
|
769
|
+
dirs.push(p.join(home, ".claude", "local"));
|
|
770
|
+
}
|
|
771
|
+
if (platform === "darwin") dirs.push("/opt/homebrew/bin");
|
|
772
|
+
dirs.push("/usr/local/bin", "/usr/bin", "/bin");
|
|
773
|
+
return dirs;
|
|
774
|
+
}
|
|
775
|
+
function searchDirs({ platform = process.platform, env = process.env, home = homedir2(), extra = true } = {}) {
|
|
776
|
+
const sep4 = platform === "win32" ? ";" : ":";
|
|
777
|
+
const raw = String(envGet(env, "PATH") ?? "").split(sep4).map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
|
|
778
|
+
const all = extra ? [...raw, ...extraBinDirs({ platform, env, home })] : raw;
|
|
779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
780
|
+
const out = [];
|
|
781
|
+
for (const d of all) {
|
|
782
|
+
const key = platform === "win32" ? d.toLowerCase().replace(/[\\/]+$/, "") : d.replace(/\/+$/, "");
|
|
783
|
+
if (seen.has(key)) continue;
|
|
784
|
+
seen.add(key);
|
|
785
|
+
out.push(d);
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
function windowsExts(env) {
|
|
790
|
+
const list = String(envGet(env, "PATHEXT") || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter((e) => e.startsWith("."));
|
|
791
|
+
if (!list.includes(".ps1")) list.push(".ps1");
|
|
792
|
+
return list;
|
|
793
|
+
}
|
|
794
|
+
function findOnPath(name, { platform = process.platform, env = process.env, home = homedir2(), extra = true, exists } = {}) {
|
|
795
|
+
if (!name) return [];
|
|
796
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
797
|
+
const check = exists ?? ((file) => defaultExists(file, platform));
|
|
798
|
+
if (name.includes("/") || platform === "win32" && name.includes("\\")) {
|
|
799
|
+
return check(name) ? [name] : [];
|
|
800
|
+
}
|
|
801
|
+
let names = [name];
|
|
802
|
+
if (platform === "win32") {
|
|
803
|
+
const exts = windowsExts(env);
|
|
804
|
+
const ext = p.extname(name).toLowerCase();
|
|
805
|
+
names = ext && exts.includes(ext) ? [name] : exts.map((e) => name + e);
|
|
806
|
+
}
|
|
807
|
+
const hits = [];
|
|
808
|
+
for (const dir of searchDirs({ platform, env, home, extra })) {
|
|
809
|
+
for (const n of names) {
|
|
810
|
+
const file = p.join(dir, n);
|
|
811
|
+
if (check(file) && !hits.includes(file)) hits.push(file);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return hits;
|
|
815
|
+
}
|
|
816
|
+
function findNode(opts = {}) {
|
|
817
|
+
return findOnPath("node", opts).find((file) => isNodeExecutable(file)) ?? null;
|
|
818
|
+
}
|
|
819
|
+
function isNodeExecutable(file) {
|
|
820
|
+
return /^node(\.exe)?$/i.test(String(file || "").split(/[\\/]/).pop() ?? "");
|
|
821
|
+
}
|
|
822
|
+
var init_pathscan = __esm({
|
|
823
|
+
"lib/pathscan.mjs"() {
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
|
|
721
827
|
// daemon/src/util/bin.ts
|
|
722
|
-
import { spawn } from "node:child_process";
|
|
828
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
723
829
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
724
830
|
import { existsSync as existsSync4 } from "node:fs";
|
|
725
|
-
import
|
|
831
|
+
import path2 from "node:path";
|
|
726
832
|
function which(name) {
|
|
727
|
-
return
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
833
|
+
return findOnPath(name);
|
|
834
|
+
}
|
|
835
|
+
function escapeCmdCommand(cmd) {
|
|
836
|
+
return cmd.replace(CMD_META, "^$1");
|
|
837
|
+
}
|
|
838
|
+
function escapeCmdArgument(arg, doubleEscape) {
|
|
839
|
+
let a = String(arg);
|
|
840
|
+
a = a.replace(/(\\*)"/g, '$1$1\\"');
|
|
841
|
+
a = a.replace(/(\\*)$/, "$1$1");
|
|
842
|
+
a = `"${a}"`;
|
|
843
|
+
a = a.replace(CMD_META, "^$1");
|
|
844
|
+
if (doubleEscape) a = a.replace(CMD_META, "^$1");
|
|
845
|
+
return a;
|
|
846
|
+
}
|
|
847
|
+
function argvFor(r, args) {
|
|
848
|
+
if (r.via === "cmd-shim" && r.shim) {
|
|
849
|
+
const line = [escapeCmdCommand(r.shim), ...args.map((a) => escapeCmdArgument(a, true))].join(" ");
|
|
850
|
+
return { cmd: r.cmd, args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true };
|
|
851
|
+
}
|
|
852
|
+
return { cmd: r.cmd, args: [...r.prefixArgs, ...args] };
|
|
853
|
+
}
|
|
854
|
+
function reapOnExit() {
|
|
855
|
+
for (const pid of liveTrees) {
|
|
737
856
|
try {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
try {
|
|
741
|
-
p.kill();
|
|
742
|
-
} catch {
|
|
743
|
-
}
|
|
744
|
-
finish([]);
|
|
745
|
-
}, 4e3);
|
|
746
|
-
p.stdout.on("data", (c) => out += c.toString());
|
|
747
|
-
p.on("error", () => {
|
|
748
|
-
clearTimeout(t);
|
|
749
|
-
finish([]);
|
|
750
|
-
});
|
|
751
|
-
p.on("close", (code) => {
|
|
752
|
-
clearTimeout(t);
|
|
753
|
-
const lines = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
754
|
-
finish(code === 0 ? lines : []);
|
|
755
|
-
});
|
|
857
|
+
if (IS_WIN) execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 3e3 });
|
|
858
|
+
else process.kill(-pid, "SIGKILL");
|
|
756
859
|
} catch {
|
|
757
|
-
finish([]);
|
|
758
860
|
}
|
|
861
|
+
}
|
|
862
|
+
liveTrees.clear();
|
|
863
|
+
}
|
|
864
|
+
function trackTree(child) {
|
|
865
|
+
if (child.pid == null) return;
|
|
866
|
+
const pid = child.pid;
|
|
867
|
+
liveTrees.add(pid);
|
|
868
|
+
child.once("exit", () => liveTrees.delete(pid));
|
|
869
|
+
if (!exitHookInstalled) {
|
|
870
|
+
exitHookInstalled = true;
|
|
871
|
+
process.once("exit", reapOnExit);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function spawnBin(r, args, opts = {}) {
|
|
875
|
+
const a = argvFor(r, args);
|
|
876
|
+
const { group, ...rest } = opts;
|
|
877
|
+
const child = spawn(a.cmd, a.args, {
|
|
878
|
+
windowsHide: true,
|
|
879
|
+
...rest,
|
|
880
|
+
...a.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {},
|
|
881
|
+
...group && !IS_WIN ? { detached: true } : {}
|
|
759
882
|
});
|
|
883
|
+
if (group) trackTree(child);
|
|
884
|
+
return child;
|
|
760
885
|
}
|
|
761
|
-
function shimTarget(cmdText, cmdDir) {
|
|
886
|
+
function shimTarget(cmdText, cmdDir, deps = {}) {
|
|
887
|
+
const exists = deps.exists ?? existsSync4;
|
|
888
|
+
const P = deps.win ?? IS_WIN ? path2.win32 : path2;
|
|
762
889
|
const cands = [];
|
|
763
890
|
const re = /"([^"]+?\.(?:js|mjs|cjs|exe|cmd|bat))"/gi;
|
|
764
891
|
let m;
|
|
765
892
|
while (m = re.exec(cmdText)) cands.push(m[1]);
|
|
766
893
|
const re2 = /(?:%~?dp0%?)?[A-Za-z]:?[\\/\w.@-]*?\.(?:js|mjs|cjs|exe)\b/gi;
|
|
767
894
|
while (m = re2.exec(cmdText)) cands.push(m[0]);
|
|
895
|
+
const found = [];
|
|
768
896
|
for (const raw of cands) {
|
|
769
897
|
let p = raw.replace(/%~?dp0%?/gi, "");
|
|
770
898
|
p = p.replace(/%[A-Z_]+%/gi, "");
|
|
771
899
|
p = p.trim().replace(/^[\\/]+/, "");
|
|
772
900
|
if (!p || /%\w+%/.test(p) || !/\.(js|mjs|cjs|exe)$/i.test(p)) continue;
|
|
773
|
-
if (!isAbsolute(p)) {
|
|
901
|
+
if (!P.isAbsolute(p)) {
|
|
774
902
|
if (!cmdDir) continue;
|
|
775
|
-
p =
|
|
903
|
+
p = P.join(cmdDir, p);
|
|
776
904
|
}
|
|
777
|
-
if (
|
|
905
|
+
if (exists(p)) found.push(P.resolve(p));
|
|
778
906
|
}
|
|
779
|
-
return null;
|
|
907
|
+
return found.find((f) => /\.(js|mjs|cjs)$/i.test(f)) ?? found.find((f) => /\.exe$/i.test(f) && !isNodeExecutable(P.basename(f))) ?? null;
|
|
780
908
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
return hits[0] || process.execPath;
|
|
909
|
+
function findNode2() {
|
|
910
|
+
if (isNodeExecutable(process.execPath)) return process.execPath;
|
|
911
|
+
return findNode() ?? "node";
|
|
785
912
|
}
|
|
786
913
|
async function resolveBin(name) {
|
|
787
914
|
const hit = cache.get(name);
|
|
@@ -803,39 +930,47 @@ function clearBinCache(name) {
|
|
|
803
930
|
if (name) cache.delete(name);
|
|
804
931
|
else cache.clear();
|
|
805
932
|
}
|
|
806
|
-
|
|
807
|
-
|
|
933
|
+
function resolveBinUncached(name) {
|
|
934
|
+
return resolveBinWith(name);
|
|
935
|
+
}
|
|
936
|
+
async function resolveBinWith(name, deps = {}) {
|
|
937
|
+
const platform = deps.platform ?? process.platform;
|
|
938
|
+
const win = platform === "win32";
|
|
939
|
+
const P = win ? path2.win32 : path2.posix;
|
|
940
|
+
const exists = deps.exists ?? existsSync4;
|
|
941
|
+
const hits = (deps.find ?? which)(name);
|
|
808
942
|
if (!hits.length) return { ok: false, reason: `PATH \u91CC\u627E\u4E0D\u5230 ${name}` };
|
|
809
|
-
const
|
|
810
|
-
if (
|
|
811
|
-
|
|
943
|
+
const first = hits[0];
|
|
944
|
+
if (!win) return { ok: true, cmd: first, prefixArgs: [], display: first, via: "path" };
|
|
945
|
+
const ext = P.extname(first).toLowerCase();
|
|
946
|
+
if (ext === ".exe" || ext === ".com") return { ok: true, cmd: first, prefixArgs: [], display: first, via: "exe" };
|
|
947
|
+
if (ext === ".ps1") {
|
|
948
|
+
return {
|
|
949
|
+
ok: true,
|
|
950
|
+
cmd: "powershell.exe",
|
|
951
|
+
prefixArgs: ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", first],
|
|
952
|
+
display: first,
|
|
953
|
+
via: "ps1"
|
|
954
|
+
};
|
|
812
955
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const cli = join6(dir, "node_modules", "npm", "bin", "npx-cli.js");
|
|
820
|
-
if (existsSync4(node) && existsSync4(cli))
|
|
821
|
-
return { ok: true, cmd: node, prefixArgs: [cli], display: `${node} ${cli}`, via: "npx-shim" };
|
|
822
|
-
}
|
|
823
|
-
const text = await readFile3(shim, "utf8").catch(() => "");
|
|
824
|
-
const target = shimTarget(text, dir);
|
|
825
|
-
if (target && /\.(js|mjs|cjs)$/i.test(target)) {
|
|
826
|
-
const node = await findNode();
|
|
827
|
-
return { ok: true, cmd: node, prefixArgs: [target], display: `${node} ${target}`, via: "shim-js" };
|
|
828
|
-
}
|
|
829
|
-
if (target && /\.exe$/i.test(target)) {
|
|
830
|
-
return { ok: true, cmd: target, prefixArgs: [], display: target, via: "shim-exe" };
|
|
831
|
-
}
|
|
832
|
-
const comspec = process.env.ComSpec || "cmd.exe";
|
|
833
|
-
return { ok: true, cmd: comspec, prefixArgs: ["/d", "/s", "/c", shim], display: shim, via: "cmd-shim" };
|
|
834
|
-
}
|
|
956
|
+
const dir = P.dirname(first);
|
|
957
|
+
if (name.toLowerCase() === "npx") {
|
|
958
|
+
const node = P.join(dir, "node.exe");
|
|
959
|
+
const cli = P.join(dir, "node_modules", "npm", "bin", "npx-cli.js");
|
|
960
|
+
if (exists(node) && exists(cli))
|
|
961
|
+
return { ok: true, cmd: node, prefixArgs: [cli], display: `${node} ${cli}`, via: "npx-shim" };
|
|
835
962
|
}
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
963
|
+
const text = await (deps.readText ?? ((f) => readFile3(f, "utf8")))(first).catch(() => "");
|
|
964
|
+
const target = shimTarget(text, dir, { exists, win: true });
|
|
965
|
+
if (target && /\.(js|mjs|cjs)$/i.test(target)) {
|
|
966
|
+
const node = (deps.node ?? findNode2)();
|
|
967
|
+
return { ok: true, cmd: node, prefixArgs: [target], display: `${node} ${target}`, via: "shim-js" };
|
|
968
|
+
}
|
|
969
|
+
if (target && /\.exe$/i.test(target)) {
|
|
970
|
+
return { ok: true, cmd: target, prefixArgs: [], display: target, via: "shim-exe" };
|
|
971
|
+
}
|
|
972
|
+
const comspec = deps.comspec ?? (process.env.ComSpec || "cmd.exe");
|
|
973
|
+
return { ok: true, cmd: comspec, prefixArgs: [], display: first, via: "cmd-shim", shim: first };
|
|
839
974
|
}
|
|
840
975
|
function killTree(child, { graceMs = 3e3 } = {}) {
|
|
841
976
|
if (!child || child.pid == null) return Promise.resolve();
|
|
@@ -870,6 +1005,10 @@ function killTree(child, { graceMs = 3e3 } = {}) {
|
|
|
870
1005
|
} catch {
|
|
871
1006
|
}
|
|
872
1007
|
}
|
|
1008
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1009
|
+
finish();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
873
1012
|
const t = setTimeout(() => {
|
|
874
1013
|
try {
|
|
875
1014
|
process.kill(-pid, "SIGKILL");
|
|
@@ -888,12 +1027,16 @@ function killTree(child, { graceMs = 3e3 } = {}) {
|
|
|
888
1027
|
});
|
|
889
1028
|
});
|
|
890
1029
|
}
|
|
891
|
-
var IS_WIN, cache, CACHE_MS;
|
|
1030
|
+
var IS_WIN, cache, CACHE_MS, CMD_META, liveTrees, exitHookInstalled;
|
|
892
1031
|
var init_bin = __esm({
|
|
893
1032
|
"daemon/src/util/bin.ts"() {
|
|
1033
|
+
init_pathscan();
|
|
894
1034
|
IS_WIN = process.platform === "win32";
|
|
895
1035
|
cache = /* @__PURE__ */ new Map();
|
|
896
1036
|
CACHE_MS = 60 * 1e3;
|
|
1037
|
+
CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
1038
|
+
liveTrees = /* @__PURE__ */ new Set();
|
|
1039
|
+
exitHookInstalled = false;
|
|
897
1040
|
}
|
|
898
1041
|
});
|
|
899
1042
|
|
|
@@ -1092,7 +1235,7 @@ function mapSdkMessage(m, st, cwd) {
|
|
|
1092
1235
|
out.push({
|
|
1093
1236
|
type: "turn_ended",
|
|
1094
1237
|
turnId: "",
|
|
1095
|
-
// session
|
|
1238
|
+
// The session fills in the current turnId
|
|
1096
1239
|
status: interrupted ? "interrupted" : isError ? "failed" : "completed",
|
|
1097
1240
|
durationMs: typeof r.duration_ms === "number" ? r.duration_ms : void 0,
|
|
1098
1241
|
error: isError && !interrupted ? str(r.result) ?? subtype : void 0,
|
|
@@ -1255,6 +1398,14 @@ var init_attachmentRefs = __esm({
|
|
|
1255
1398
|
// daemon/src/providers/claude/session.ts
|
|
1256
1399
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1257
1400
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
1401
|
+
function claudeExecutable(o) {
|
|
1402
|
+
if (!o.binCmd) return null;
|
|
1403
|
+
if (o.binVia === "cmd-shim" || o.binVia === "ps1") return null;
|
|
1404
|
+
const last = o.binPrefixArgs[o.binPrefixArgs.length - 1];
|
|
1405
|
+
if (last && /\.(c|m)?js$/i.test(last)) return { path: last, node: true };
|
|
1406
|
+
if (o.binPrefixArgs.length) return null;
|
|
1407
|
+
return { path: o.binCmd, node: false };
|
|
1408
|
+
}
|
|
1258
1409
|
var EFFORTS, rid2, ClaudeSession;
|
|
1259
1410
|
var init_session = __esm({
|
|
1260
1411
|
"daemon/src/providers/claude/session.ts"() {
|
|
@@ -1289,7 +1440,7 @@ var init_session = __esm({
|
|
|
1289
1440
|
closed = false;
|
|
1290
1441
|
stderrTail = "";
|
|
1291
1442
|
pumpDone = null;
|
|
1292
|
-
//
|
|
1443
|
+
// The query is only started before the first send (a failed detection should be reported quickly)
|
|
1293
1444
|
ensureQuery() {
|
|
1294
1445
|
if (this.query && !this.needsRestart) return this.query;
|
|
1295
1446
|
this.abort = new AbortController();
|
|
@@ -1308,13 +1459,10 @@ var init_session = __esm({
|
|
|
1308
1459
|
this.stderrTail = (this.stderrTail + d).slice(-4096);
|
|
1309
1460
|
}
|
|
1310
1461
|
};
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
} else {
|
|
1316
|
-
options.pathToClaudeCodeExecutable = this.o.binCmd;
|
|
1317
|
-
}
|
|
1462
|
+
const exe = claudeExecutable(this.o);
|
|
1463
|
+
if (exe) {
|
|
1464
|
+
options.pathToClaudeCodeExecutable = exe.path;
|
|
1465
|
+
if (exe.node) options.executable = "node";
|
|
1318
1466
|
}
|
|
1319
1467
|
if (this.o.effort && EFFORTS.includes(this.o.effort)) {
|
|
1320
1468
|
options.effort = this.o.effort;
|
|
@@ -1406,7 +1554,7 @@ var init_session = __esm({
|
|
|
1406
1554
|
})();
|
|
1407
1555
|
return this.restarting;
|
|
1408
1556
|
}
|
|
1409
|
-
// ----
|
|
1557
|
+
// ---- permissions (§7.2.4) ----
|
|
1410
1558
|
canUseTool(toolName, input, opts) {
|
|
1411
1559
|
const requestId = "perm_" + rid2();
|
|
1412
1560
|
const { signal } = opts;
|
|
@@ -1433,12 +1581,12 @@ var init_session = __esm({
|
|
|
1433
1581
|
{ id: "deny", label: "\u62D2\u7EDD", kind: "reject_once" }
|
|
1434
1582
|
]
|
|
1435
1583
|
});
|
|
1436
|
-
return new Promise((
|
|
1437
|
-
this.pending.set(requestId, { kind: "permission", resolve: (a) =>
|
|
1584
|
+
return new Promise((resolve2) => {
|
|
1585
|
+
this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(this.toPermissionResult(a, input)) });
|
|
1438
1586
|
signal.addEventListener("abort", () => {
|
|
1439
1587
|
if (this.pending.delete(requestId)) {
|
|
1440
1588
|
this.emit({ type: "permission_resolved", requestId, cancelled: true });
|
|
1441
|
-
|
|
1589
|
+
resolve2({ behavior: "deny", message: "\u5DF2\u53D6\u6D88", interrupt: true });
|
|
1442
1590
|
}
|
|
1443
1591
|
}, { once: true });
|
|
1444
1592
|
});
|
|
@@ -1465,12 +1613,12 @@ var init_session = __esm({
|
|
|
1465
1613
|
{ id: "reject", label: "\u62D2\u7EDD", kind: "reject_once" }
|
|
1466
1614
|
]
|
|
1467
1615
|
});
|
|
1468
|
-
const answer = await new Promise((
|
|
1469
|
-
this.pending.set(requestId, { kind: "permission", resolve: (a) =>
|
|
1616
|
+
const answer = await new Promise((resolve2) => {
|
|
1617
|
+
this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(a) });
|
|
1470
1618
|
signal.addEventListener("abort", () => {
|
|
1471
1619
|
if (this.pending.delete(requestId)) {
|
|
1472
1620
|
this.emit({ type: "permission_resolved", requestId, cancelled: true });
|
|
1473
|
-
|
|
1621
|
+
resolve2({ cancelled: true });
|
|
1474
1622
|
}
|
|
1475
1623
|
}, { once: true });
|
|
1476
1624
|
});
|
|
@@ -1488,12 +1636,12 @@ var init_session = __esm({
|
|
|
1488
1636
|
}
|
|
1489
1637
|
async handleQuestion(requestId, input, signal) {
|
|
1490
1638
|
this.emit({ type: "question", requestId, questions: input.questions });
|
|
1491
|
-
const answer = await new Promise((
|
|
1492
|
-
this.pending.set(requestId, { kind: "question", resolve: (a) =>
|
|
1639
|
+
const answer = await new Promise((resolve2) => {
|
|
1640
|
+
this.pending.set(requestId, { kind: "question", resolve: (a) => resolve2(a) });
|
|
1493
1641
|
signal.addEventListener("abort", () => {
|
|
1494
1642
|
if (this.pending.delete(requestId)) {
|
|
1495
1643
|
this.emit({ type: "permission_resolved", requestId, cancelled: true });
|
|
1496
|
-
|
|
1644
|
+
resolve2({ cancelled: true });
|
|
1497
1645
|
}
|
|
1498
1646
|
}, { once: true });
|
|
1499
1647
|
});
|
|
@@ -1608,8 +1756,8 @@ var init_models = __esm({
|
|
|
1608
1756
|
// daemon/src/history/import.ts
|
|
1609
1757
|
import { readdir, readFile as readFile5, stat as stat3 } from "node:fs/promises";
|
|
1610
1758
|
import { existsSync as existsSync6 } from "node:fs";
|
|
1611
|
-
import { join as
|
|
1612
|
-
import { homedir as
|
|
1759
|
+
import { join as join6 } from "node:path";
|
|
1760
|
+
import { homedir as homedir3 } from "node:os";
|
|
1613
1761
|
function parseLine(line) {
|
|
1614
1762
|
let r;
|
|
1615
1763
|
try {
|
|
@@ -1629,8 +1777,8 @@ function parseLine(line) {
|
|
|
1629
1777
|
}
|
|
1630
1778
|
return { cwd, userText };
|
|
1631
1779
|
}
|
|
1632
|
-
async function* linesOf(
|
|
1633
|
-
const text = await readFile5(
|
|
1780
|
+
async function* linesOf(path3) {
|
|
1781
|
+
const text = await readFile5(path3, "utf8").catch(() => "");
|
|
1634
1782
|
for (const line of text.split("\n")) {
|
|
1635
1783
|
if (line.trim()) yield line;
|
|
1636
1784
|
}
|
|
@@ -1639,14 +1787,14 @@ async function listClaudeHistory(cwd) {
|
|
|
1639
1787
|
const out = [];
|
|
1640
1788
|
const dirs = await readdir(CLAUDE_ROOT).catch(() => []);
|
|
1641
1789
|
for (const d of dirs) {
|
|
1642
|
-
const dirPath =
|
|
1790
|
+
const dirPath = join6(CLAUDE_ROOT, d);
|
|
1643
1791
|
const files = (await readdir(dirPath).catch(() => [])).filter((f) => f.endsWith(".jsonl"));
|
|
1644
1792
|
for (const f of files) {
|
|
1645
|
-
const
|
|
1793
|
+
const path3 = join6(dirPath, f);
|
|
1646
1794
|
let match = false;
|
|
1647
1795
|
let title = "";
|
|
1648
1796
|
let count = 0;
|
|
1649
|
-
for await (const line of linesOf(
|
|
1797
|
+
for await (const line of linesOf(path3)) {
|
|
1650
1798
|
const p = parseLine(line);
|
|
1651
1799
|
if (!p) continue;
|
|
1652
1800
|
if (!match) {
|
|
@@ -1658,7 +1806,7 @@ async function listClaudeHistory(cwd) {
|
|
|
1658
1806
|
if (p.userText) count++;
|
|
1659
1807
|
}
|
|
1660
1808
|
if (!match) continue;
|
|
1661
|
-
const s = await stat3(
|
|
1809
|
+
const s = await stat3(path3).catch(() => null);
|
|
1662
1810
|
out.push({
|
|
1663
1811
|
sessionId: f.replace(/\.jsonl$/, ""),
|
|
1664
1812
|
title: title || "\uFF08\u65E0\u6807\u9898\uFF09",
|
|
@@ -1673,11 +1821,11 @@ async function listClaudeHistory(cwd) {
|
|
|
1673
1821
|
async function readClaudeHistory(sessionId, cwd) {
|
|
1674
1822
|
const dirs = await readdir(CLAUDE_ROOT).catch(() => []);
|
|
1675
1823
|
for (const d of dirs) {
|
|
1676
|
-
const
|
|
1677
|
-
if (!existsSync6(
|
|
1824
|
+
const path3 = join6(CLAUDE_ROOT, d, `${sessionId}.jsonl`);
|
|
1825
|
+
if (!existsSync6(path3)) continue;
|
|
1678
1826
|
const msgs = [];
|
|
1679
1827
|
let matched = false;
|
|
1680
|
-
for await (const line of linesOf(
|
|
1828
|
+
for await (const line of linesOf(path3)) {
|
|
1681
1829
|
const p = parseLine(line);
|
|
1682
1830
|
if (!matched) {
|
|
1683
1831
|
if (p?.cwd && samePath(p.cwd, cwd)) matched = true;
|
|
@@ -1699,27 +1847,26 @@ var CLAUDE_ROOT, rec2, str2;
|
|
|
1699
1847
|
var init_import = __esm({
|
|
1700
1848
|
"daemon/src/history/import.ts"() {
|
|
1701
1849
|
init_paths();
|
|
1702
|
-
CLAUDE_ROOT =
|
|
1850
|
+
CLAUDE_ROOT = join6(homedir3(), ".claude", "projects");
|
|
1703
1851
|
rec2 = (v) => v && typeof v === "object" ? v : {};
|
|
1704
1852
|
str2 = (v) => typeof v === "string" ? v : void 0;
|
|
1705
1853
|
}
|
|
1706
1854
|
});
|
|
1707
1855
|
|
|
1708
1856
|
// daemon/src/providers/claude/provider.ts
|
|
1709
|
-
|
|
1710
|
-
function runVersion(cmd, args, timeoutMs = 5e3) {
|
|
1857
|
+
function runVersion(r, args, timeoutMs = 5e3) {
|
|
1711
1858
|
return new Promise((resolvePromise, reject) => {
|
|
1712
1859
|
let out = "";
|
|
1713
|
-
const p =
|
|
1860
|
+
const p = spawnBin(r, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" } });
|
|
1714
1861
|
const t = setTimeout(() => {
|
|
1715
1862
|
try {
|
|
1716
|
-
p
|
|
1863
|
+
void killTree(p, { graceMs: 500 });
|
|
1717
1864
|
} catch {
|
|
1718
1865
|
}
|
|
1719
1866
|
reject(new Error("timeout"));
|
|
1720
1867
|
}, timeoutMs);
|
|
1721
|
-
p.stdout
|
|
1722
|
-
p.stderr
|
|
1868
|
+
p.stdout?.on("data", (c) => out += c.toString());
|
|
1869
|
+
p.stderr?.on("data", (c) => out += c.toString());
|
|
1723
1870
|
p.on("error", reject);
|
|
1724
1871
|
p.on("close", (code) => {
|
|
1725
1872
|
clearTimeout(t);
|
|
@@ -1737,7 +1884,7 @@ function createClaudeProvider() {
|
|
|
1737
1884
|
const bin = await resolveBin("claude");
|
|
1738
1885
|
if (!bin.ok) return { available: false, reason: bin.reason, installHint: INSTALL_HINT };
|
|
1739
1886
|
try {
|
|
1740
|
-
const version = await runVersion(bin
|
|
1887
|
+
const version = await runVersion(bin, ["--version"]);
|
|
1741
1888
|
return { available: true, bin: bin.display, version: version.trim().split("\n")[0] || "" };
|
|
1742
1889
|
} catch {
|
|
1743
1890
|
return { available: true, bin: bin.display, version: "" };
|
|
@@ -1757,10 +1904,11 @@ function createClaudeProvider() {
|
|
|
1757
1904
|
effort: o.effort,
|
|
1758
1905
|
resumeId: o.resumeId,
|
|
1759
1906
|
binCmd: bin.cmd,
|
|
1760
|
-
binPrefixArgs: bin.prefixArgs
|
|
1907
|
+
binPrefixArgs: bin.prefixArgs,
|
|
1908
|
+
binVia: bin.via
|
|
1761
1909
|
});
|
|
1762
1910
|
},
|
|
1763
|
-
// §7.6
|
|
1911
|
+
// §7.6: jsonl sessions under ~/.claude/projects matched by cwd
|
|
1764
1912
|
async listHistory(cwd) {
|
|
1765
1913
|
const items = await listClaudeHistory(cwd);
|
|
1766
1914
|
return items.map((i) => ({ provider: "claude", ...i, imported: false }));
|
|
@@ -1798,7 +1946,7 @@ var init_provider = __esm({
|
|
|
1798
1946
|
resume: true,
|
|
1799
1947
|
interrupt: true,
|
|
1800
1948
|
steer: false,
|
|
1801
|
-
// Claude
|
|
1949
|
+
// Claude doesn't support real steering — the manager queues instead (§7.2.2)
|
|
1802
1950
|
images: true,
|
|
1803
1951
|
plan: true,
|
|
1804
1952
|
questions: true,
|
|
@@ -1811,8 +1959,8 @@ var init_provider = __esm({
|
|
|
1811
1959
|
|
|
1812
1960
|
// daemon/src/history/codex.ts
|
|
1813
1961
|
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
1814
|
-
import { join as
|
|
1815
|
-
import { homedir as
|
|
1962
|
+
import { join as join7 } from "node:path";
|
|
1963
|
+
import { homedir as homedir4 } from "node:os";
|
|
1816
1964
|
function textOf(content, kind) {
|
|
1817
1965
|
if (!Array.isArray(content)) return "";
|
|
1818
1966
|
return content.filter((p) => p.type === kind || p.type === "input_text" || p.type === "output_text").map((p) => str3(p.text) ?? "").join("\n").trim();
|
|
@@ -1820,7 +1968,7 @@ function textOf(content, kind) {
|
|
|
1820
1968
|
async function* walk(dir, depth = 0) {
|
|
1821
1969
|
if (depth > 6) return;
|
|
1822
1970
|
for (const name of await readdir2(dir).catch(() => [])) {
|
|
1823
|
-
const p =
|
|
1971
|
+
const p = join7(dir, name);
|
|
1824
1972
|
const s = await stat4(p).catch(() => null);
|
|
1825
1973
|
if (!s) continue;
|
|
1826
1974
|
if (s.isDirectory()) yield* walk(p, depth + 1);
|
|
@@ -1829,11 +1977,11 @@ async function* walk(dir, depth = 0) {
|
|
|
1829
1977
|
}
|
|
1830
1978
|
async function listCodexHistory(cwd) {
|
|
1831
1979
|
const out = [];
|
|
1832
|
-
for await (const
|
|
1833
|
-
const m = /rollout-.*-([0-9a-f-]{36})\.jsonl$/i.exec(
|
|
1980
|
+
for await (const path3 of walk(CODEX_ROOT)) {
|
|
1981
|
+
const m = /rollout-.*-([0-9a-f-]{36})\.jsonl$/i.exec(path3);
|
|
1834
1982
|
const sessionId = m?.[1];
|
|
1835
1983
|
if (!sessionId) continue;
|
|
1836
|
-
const text = await readFile6(
|
|
1984
|
+
const text = await readFile6(path3, "utf8").catch(() => "");
|
|
1837
1985
|
let match = false;
|
|
1838
1986
|
let title = "";
|
|
1839
1987
|
let count = 0;
|
|
@@ -1864,7 +2012,7 @@ async function listCodexHistory(cwd) {
|
|
|
1864
2012
|
}
|
|
1865
2013
|
}
|
|
1866
2014
|
if (!match) continue;
|
|
1867
|
-
const s = await stat4(
|
|
2015
|
+
const s = await stat4(path3).catch(() => null);
|
|
1868
2016
|
out.push({ sessionId, title: title || "\uFF08\u65E0\u6807\u9898\uFF09", updatedAt: s ? Math.round(s.mtimeMs) : 0, messageCount: count });
|
|
1869
2017
|
}
|
|
1870
2018
|
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
@@ -1952,7 +2100,7 @@ var CODEX_ROOT, rec3, str3, WRAPPERS;
|
|
|
1952
2100
|
var init_codex = __esm({
|
|
1953
2101
|
"daemon/src/history/codex.ts"() {
|
|
1954
2102
|
init_paths();
|
|
1955
|
-
CODEX_ROOT =
|
|
2103
|
+
CODEX_ROOT = join7(homedir4(), ".codex", "sessions");
|
|
1956
2104
|
rec3 = (v) => v && typeof v === "object" ? v : {};
|
|
1957
2105
|
str3 = (v) => typeof v === "string" ? v : void 0;
|
|
1958
2106
|
WRAPPERS = /^\s*<(environment_context|user_instructions|skills_instructions|permissions instructions)>/i;
|
|
@@ -1960,11 +2108,12 @@ var init_codex = __esm({
|
|
|
1960
2108
|
});
|
|
1961
2109
|
|
|
1962
2110
|
// daemon/src/providers/codex/rpc.ts
|
|
1963
|
-
import { spawn as
|
|
2111
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1964
2112
|
var CodexRpcError, CodexProcessExitedError, CodexRpc;
|
|
1965
2113
|
var init_rpc = __esm({
|
|
1966
2114
|
"daemon/src/providers/codex/rpc.ts"() {
|
|
1967
2115
|
init_log();
|
|
2116
|
+
init_bin();
|
|
1968
2117
|
CodexRpcError = class extends Error {
|
|
1969
2118
|
code;
|
|
1970
2119
|
constructor(message, code) {
|
|
@@ -1986,13 +2135,17 @@ var init_rpc = __esm({
|
|
|
1986
2135
|
onServerRequest;
|
|
1987
2136
|
onNotification;
|
|
1988
2137
|
onExit;
|
|
1989
|
-
constructor(
|
|
1990
|
-
this.proc =
|
|
2138
|
+
constructor(argv, opts = {}) {
|
|
2139
|
+
this.proc = spawn2(argv.cmd, argv.args, {
|
|
1991
2140
|
cwd: opts.cwd,
|
|
1992
2141
|
env: opts.env ?? process.env,
|
|
1993
2142
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1994
|
-
windowsHide: true
|
|
2143
|
+
windowsHide: true,
|
|
2144
|
+
...argv.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {},
|
|
2145
|
+
// Non-Windows: its own process group, so after a close() timeout killTree also ends the commands codex started
|
|
2146
|
+
...process.platform !== "win32" ? { detached: true } : {}
|
|
1995
2147
|
});
|
|
2148
|
+
trackTree(this.proc);
|
|
1996
2149
|
this.proc.stdout.setEncoding("utf8");
|
|
1997
2150
|
this.proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
1998
2151
|
this.proc.stderr.setEncoding("utf8");
|
|
@@ -2107,10 +2260,7 @@ var init_rpc = __esm({
|
|
|
2107
2260
|
}
|
|
2108
2261
|
await new Promise((resolvePromise) => {
|
|
2109
2262
|
const t = setTimeout(() => {
|
|
2110
|
-
|
|
2111
|
-
this.proc.kill();
|
|
2112
|
-
} catch {
|
|
2113
|
-
}
|
|
2263
|
+
void killTree(this.proc, { graceMs: 1500 });
|
|
2114
2264
|
resolvePromise();
|
|
2115
2265
|
}, 3e3);
|
|
2116
2266
|
this.proc.on("close", () => {
|
|
@@ -2345,8 +2495,8 @@ var init_map2 = __esm({
|
|
|
2345
2495
|
|
|
2346
2496
|
// daemon/src/providers/codex/commands.ts
|
|
2347
2497
|
import { readdir as readdir3, readFile as readFile7, stat as stat5 } from "node:fs/promises";
|
|
2348
|
-
import { homedir as
|
|
2349
|
-
import { basename, join as
|
|
2498
|
+
import { homedir as homedir5 } from "node:os";
|
|
2499
|
+
import { basename, join as join8 } from "node:path";
|
|
2350
2500
|
function parseFrontMatter(raw) {
|
|
2351
2501
|
if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
|
|
2352
2502
|
const end = raw.indexOf("\n---\n", 4);
|
|
@@ -2376,11 +2526,11 @@ async function readPromptFiles(dir) {
|
|
|
2376
2526
|
}
|
|
2377
2527
|
for (const f of names) {
|
|
2378
2528
|
if (!f.endsWith(".md")) continue;
|
|
2379
|
-
const
|
|
2529
|
+
const path3 = join8(dir, f);
|
|
2380
2530
|
try {
|
|
2381
|
-
const st = await stat5(
|
|
2531
|
+
const st = await stat5(path3);
|
|
2382
2532
|
if (!st.isFile()) continue;
|
|
2383
|
-
const raw = await readFile7(
|
|
2533
|
+
const raw = await readFile7(path3, "utf8");
|
|
2384
2534
|
const { meta, body } = parseFrontMatter(raw);
|
|
2385
2535
|
const name = basename(f, ".md");
|
|
2386
2536
|
out.push({
|
|
@@ -2404,7 +2554,7 @@ async function readSkillDirs(root) {
|
|
|
2404
2554
|
return out;
|
|
2405
2555
|
}
|
|
2406
2556
|
for (const d of dirs) {
|
|
2407
|
-
const skillPath =
|
|
2557
|
+
const skillPath = join8(root, d, "SKILL.md");
|
|
2408
2558
|
try {
|
|
2409
2559
|
const st = await stat5(skillPath);
|
|
2410
2560
|
if (!st.isFile()) continue;
|
|
@@ -2424,12 +2574,12 @@ async function readSkillDirs(root) {
|
|
|
2424
2574
|
return out;
|
|
2425
2575
|
}
|
|
2426
2576
|
async function loadCodexSlashEntries(cwd, opts) {
|
|
2427
|
-
const home =
|
|
2577
|
+
const home = homedir5();
|
|
2428
2578
|
const parts = await Promise.all([
|
|
2429
|
-
opts?.skipHome ? Promise.resolve([]) : readPromptFiles(
|
|
2430
|
-
readPromptFiles(
|
|
2431
|
-
opts?.skipHome ? Promise.resolve([]) : readSkillDirs(
|
|
2432
|
-
readSkillDirs(
|
|
2579
|
+
opts?.skipHome ? Promise.resolve([]) : readPromptFiles(join8(home, ".codex", "prompts")),
|
|
2580
|
+
readPromptFiles(join8(cwd, ".codex", "prompts")),
|
|
2581
|
+
opts?.skipHome ? Promise.resolve([]) : readSkillDirs(join8(home, ".codex", "skills")),
|
|
2582
|
+
readSkillDirs(join8(cwd, ".codex", "skills"))
|
|
2433
2583
|
]);
|
|
2434
2584
|
const byName = /* @__PURE__ */ new Map();
|
|
2435
2585
|
for (const list of parts) {
|
|
@@ -2462,6 +2612,7 @@ var init_session2 = __esm({
|
|
|
2462
2612
|
init_attachmentRefs();
|
|
2463
2613
|
init_queue();
|
|
2464
2614
|
init_rpc();
|
|
2615
|
+
init_bin();
|
|
2465
2616
|
init_map2();
|
|
2466
2617
|
init_commands();
|
|
2467
2618
|
init_log();
|
|
@@ -2489,7 +2640,7 @@ var init_session2 = __esm({
|
|
|
2489
2640
|
started = null;
|
|
2490
2641
|
interruptWanted = false;
|
|
2491
2642
|
ourTurnId = null;
|
|
2492
|
-
//
|
|
2643
|
+
// The current turn's turnId in the timeline (generated on send)
|
|
2493
2644
|
staleCodexTurns = /* @__PURE__ */ new Set();
|
|
2494
2645
|
codexSlash = null;
|
|
2495
2646
|
emit(ev) {
|
|
@@ -2506,8 +2657,10 @@ var init_session2 = __esm({
|
|
|
2506
2657
|
if (this.started) return this.started;
|
|
2507
2658
|
const attempt = (async () => {
|
|
2508
2659
|
const rpc = new CodexRpc(
|
|
2509
|
-
|
|
2510
|
-
|
|
2660
|
+
argvFor(
|
|
2661
|
+
{ ok: true, cmd: this.o.binCmd, prefixArgs: this.o.binPrefixArgs, display: this.o.binCmd, via: this.o.binVia ?? "path", shim: this.o.binShim },
|
|
2662
|
+
["app-server"]
|
|
2663
|
+
),
|
|
2511
2664
|
{ cwd: this.o.cwd }
|
|
2512
2665
|
);
|
|
2513
2666
|
this.rpc = rpc;
|
|
@@ -2651,7 +2804,7 @@ var init_session2 = __esm({
|
|
|
2651
2804
|
threadId() {
|
|
2652
2805
|
return this.sessionThreadId ?? void 0;
|
|
2653
2806
|
}
|
|
2654
|
-
// ----
|
|
2807
|
+
// ---- server requests (§7.3.5 approvals) ----
|
|
2655
2808
|
async onServerRequest(method, params) {
|
|
2656
2809
|
switch (method) {
|
|
2657
2810
|
case "item/commandExecution/requestApproval": {
|
|
@@ -2713,8 +2866,8 @@ var init_session2 = __esm({
|
|
|
2713
2866
|
}
|
|
2714
2867
|
}
|
|
2715
2868
|
waitFor(requestId) {
|
|
2716
|
-
return new Promise((
|
|
2717
|
-
this.pending.set(requestId, { kind: "permission", resolve:
|
|
2869
|
+
return new Promise((resolve2) => {
|
|
2870
|
+
this.pending.set(requestId, { kind: "permission", resolve: resolve2 });
|
|
2718
2871
|
});
|
|
2719
2872
|
}
|
|
2720
2873
|
respond(requestId, answer) {
|
|
@@ -2754,20 +2907,19 @@ var init_session2 = __esm({
|
|
|
2754
2907
|
});
|
|
2755
2908
|
|
|
2756
2909
|
// daemon/src/providers/codex/provider.ts
|
|
2757
|
-
|
|
2758
|
-
function runVersion2(cmd, args, timeoutMs = 5e3) {
|
|
2910
|
+
function runVersion2(r, args, timeoutMs = 5e3) {
|
|
2759
2911
|
return new Promise((resolvePromise, reject) => {
|
|
2760
2912
|
let out = "";
|
|
2761
|
-
const p =
|
|
2913
|
+
const p = spawnBin(r, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2762
2914
|
const t = setTimeout(() => {
|
|
2763
2915
|
try {
|
|
2764
|
-
p
|
|
2916
|
+
void killTree(p, { graceMs: 500 });
|
|
2765
2917
|
} catch {
|
|
2766
2918
|
}
|
|
2767
2919
|
reject(new Error("timeout"));
|
|
2768
2920
|
}, timeoutMs);
|
|
2769
|
-
p.stdout
|
|
2770
|
-
p.stderr
|
|
2921
|
+
p.stdout?.on("data", (c) => out += c.toString());
|
|
2922
|
+
p.stderr?.on("data", (c) => out += c.toString());
|
|
2771
2923
|
p.on("error", reject);
|
|
2772
2924
|
p.on("close", (code) => {
|
|
2773
2925
|
clearTimeout(t);
|
|
@@ -2784,8 +2936,8 @@ function normalizeEfforts(raw) {
|
|
|
2784
2936
|
}
|
|
2785
2937
|
return out;
|
|
2786
2938
|
}
|
|
2787
|
-
async function fetchModels(
|
|
2788
|
-
const rpc = new CodexRpc(
|
|
2939
|
+
async function fetchModels(r, cwd) {
|
|
2940
|
+
const rpc = new CodexRpc(argvFor(r, ["app-server"]), { cwd });
|
|
2789
2941
|
try {
|
|
2790
2942
|
await rpc.request("initialize", {
|
|
2791
2943
|
clientInfo: { name: "agent-console", title: "Agent Console", version: "0" },
|
|
@@ -2822,7 +2974,7 @@ function createCodexProvider(version) {
|
|
|
2822
2974
|
return { available: false, reason: bin.reason, installHint: INSTALL_HINT2 };
|
|
2823
2975
|
}
|
|
2824
2976
|
try {
|
|
2825
|
-
const v = await runVersion2(bin
|
|
2977
|
+
const v = await runVersion2(bin, ["--version"]);
|
|
2826
2978
|
return { available: true, bin: bin.display, version: v.trim().split("\n")[0] || "" };
|
|
2827
2979
|
} catch (e) {
|
|
2828
2980
|
return { available: true, bin: bin.display, version: "", reason: String(e) };
|
|
@@ -2833,7 +2985,7 @@ function createCodexProvider(version) {
|
|
|
2833
2985
|
const bin = await resolveBin("codex");
|
|
2834
2986
|
if (!bin.ok) return [{ id: "", name: "\u9ED8\u8BA4" }];
|
|
2835
2987
|
try {
|
|
2836
|
-
const models2 = await fetchModels(bin
|
|
2988
|
+
const models2 = await fetchModels(bin, process.cwd());
|
|
2837
2989
|
modelCache = { at: Date.now(), models: models2 };
|
|
2838
2990
|
return models2;
|
|
2839
2991
|
} catch (e) {
|
|
@@ -2853,10 +3005,12 @@ function createCodexProvider(version) {
|
|
|
2853
3005
|
resumeId: o.resumeId,
|
|
2854
3006
|
binCmd: bin.cmd,
|
|
2855
3007
|
binPrefixArgs: bin.prefixArgs,
|
|
3008
|
+
binVia: bin.via,
|
|
3009
|
+
binShim: bin.shim,
|
|
2856
3010
|
version
|
|
2857
3011
|
});
|
|
2858
3012
|
},
|
|
2859
|
-
// §7.6
|
|
3013
|
+
// §7.6: ~/.codex/sessions/**/rollout-*.jsonl matched by cwd
|
|
2860
3014
|
async listHistory(cwd) {
|
|
2861
3015
|
const items = await listCodexHistory(cwd);
|
|
2862
3016
|
return items.map((i) => ({ provider: "codex", ...i, imported: false }));
|
|
@@ -2883,9 +3037,9 @@ var init_provider2 = __esm({
|
|
|
2883
3037
|
plan: true,
|
|
2884
3038
|
questions: true,
|
|
2885
3039
|
cost: false,
|
|
2886
|
-
// tokenUsage
|
|
3040
|
+
// tokenUsage yes, cost no
|
|
2887
3041
|
effort: [],
|
|
2888
|
-
// effort
|
|
3042
|
+
// effort values depend on the model — filled from model/list after detect
|
|
2889
3043
|
modes: ["plan", "default", "acceptEdits", "bypassPermissions"]
|
|
2890
3044
|
};
|
|
2891
3045
|
modelCache = null;
|
|
@@ -2917,16 +3071,15 @@ var init_catalog = __esm({
|
|
|
2917
3071
|
});
|
|
2918
3072
|
|
|
2919
3073
|
// daemon/src/providers/acp/spawn.ts
|
|
2920
|
-
import { spawn as spawn5 } from "node:child_process";
|
|
2921
3074
|
import { Readable, Writable } from "node:stream";
|
|
2922
3075
|
import { resolve as resolvePath } from "node:path";
|
|
2923
3076
|
import * as acp from "@agentclientprotocol/sdk";
|
|
2924
3077
|
function spawnResolved(r, args, o = {}) {
|
|
2925
|
-
return
|
|
3078
|
+
return spawnBin(r, args, {
|
|
2926
3079
|
cwd: o.cwd,
|
|
2927
3080
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2928
|
-
|
|
2929
|
-
|
|
3081
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
3082
|
+
group: true
|
|
2930
3083
|
});
|
|
2931
3084
|
}
|
|
2932
3085
|
function acpStreamFromChild(child) {
|
|
@@ -3148,7 +3301,7 @@ var init_session3 = __esm({
|
|
|
3148
3301
|
activeTurnId = null;
|
|
3149
3302
|
configOptions = null;
|
|
3150
3303
|
wakePump = null;
|
|
3151
|
-
//
|
|
3304
|
+
// listCommands waits for the first batch of commands (or confirmation there are none), following Paseo's waitForInitialCommands
|
|
3152
3305
|
commandsReady;
|
|
3153
3306
|
settleCommands;
|
|
3154
3307
|
async boot() {
|
|
@@ -3184,7 +3337,7 @@ var init_session3 = __esm({
|
|
|
3184
3337
|
this.ctx = this.conn.agent;
|
|
3185
3338
|
await this.ctx.request(methods2.agent.initialize, {
|
|
3186
3339
|
protocolVersion: PROTOCOL_VERSION,
|
|
3187
|
-
// Cursor
|
|
3340
|
+
// Cursor: declare support for the parameterized model picker (as Paseo does); only then does it expose levels / Fast as separate config options
|
|
3188
3341
|
clientCapabilities: this.o.flavor === "cursor" ? { _meta: { parameterizedModelPicker: true } } : {},
|
|
3189
3342
|
clientInfo: { name: "Agent Console", version: this.o.appVersion }
|
|
3190
3343
|
});
|
|
@@ -3232,7 +3385,7 @@ var init_session3 = __esm({
|
|
|
3232
3385
|
if (this.initErr) throw this.initErr;
|
|
3233
3386
|
if (!this.active) throw new Error("ACP \u4F1A\u8BDD\u672A\u5C31\u7EEA");
|
|
3234
3387
|
}
|
|
3235
|
-
/**
|
|
3388
|
+
/** The config options currently in effect: each set_config_option response returns the latest set (levels change after switching models); otherwise use session/new's */
|
|
3236
3389
|
currentConfig() {
|
|
3237
3390
|
return this.configOptions ?? this.active?.newSessionResponse.configOptions ?? [];
|
|
3238
3391
|
}
|
|
@@ -3245,14 +3398,14 @@ var init_session3 = __esm({
|
|
|
3245
3398
|
}
|
|
3246
3399
|
if (this.o.flavor === "cursor") this.modelConfigId = "model";
|
|
3247
3400
|
}
|
|
3248
|
-
//
|
|
3401
|
+
// Level options belong to the current model: recompute on every refresh (cleared when switching to a model without levels)
|
|
3249
3402
|
syncThoughtConfigId() {
|
|
3250
3403
|
this.thoughtConfigId = null;
|
|
3251
3404
|
const opts = this.currentConfig().filter((o) => o.type === "select");
|
|
3252
3405
|
const hit = opts.find((o) => o.category === "thought_level") ?? opts.find((o) => /thought|thinking|effort|reasoning/i.test(o.id ?? ""));
|
|
3253
3406
|
if (hit) this.thoughtConfigId = hit.id;
|
|
3254
3407
|
}
|
|
3255
|
-
/** set_config_option
|
|
3408
|
+
/** set_config_option, then refresh local state from the latest config options it returns */
|
|
3256
3409
|
async setOption(configId, value) {
|
|
3257
3410
|
if (!this.ctx || !this.active) return false;
|
|
3258
3411
|
try {
|
|
@@ -3278,7 +3431,7 @@ var init_session3 = __esm({
|
|
|
3278
3431
|
if (values.length && !values.includes(effort)) return;
|
|
3279
3432
|
await this.setOption(this.thoughtConfigId, effort);
|
|
3280
3433
|
}
|
|
3281
|
-
/** Agent
|
|
3434
|
+
/** Mode choices the Agent reports: prefer a category=mode select in configOptions, otherwise ACP modes.availableModes */
|
|
3282
3435
|
modeChoices() {
|
|
3283
3436
|
const opt = this.currentConfig().find(
|
|
3284
3437
|
(o) => o.type === "select" && (o.category === "mode" || o.id === "mode")
|
|
@@ -3333,8 +3486,8 @@ var init_session3 = __esm({
|
|
|
3333
3486
|
}
|
|
3334
3487
|
}
|
|
3335
3488
|
/**
|
|
3336
|
-
*
|
|
3337
|
-
* Cursor
|
|
3489
|
+
* Switch model → refresh level options from the new config options it returns → then apply the level (as Paseo does).
|
|
3490
|
+
* Cursor connects with parameterizedModelPicker: only the base model name is sent (grok-4.6); level / Fast are separate config options.
|
|
3338
3491
|
*/
|
|
3339
3492
|
async applySessionModel(model) {
|
|
3340
3493
|
if (!this.ctx || !this.active) return;
|
|
@@ -3368,7 +3521,7 @@ var init_session3 = __esm({
|
|
|
3368
3521
|
}
|
|
3369
3522
|
}
|
|
3370
3523
|
}
|
|
3371
|
-
/** Cursor
|
|
3524
|
+
/** Cursor only: Fast / Auto-review (can be changed while the session is open) */
|
|
3372
3525
|
async updateCursorOpts(fast, autoReview) {
|
|
3373
3526
|
this.cursorFast = fast;
|
|
3374
3527
|
void autoReview;
|
|
@@ -3377,8 +3530,8 @@ var init_session3 = __esm({
|
|
|
3377
3530
|
await this.applyCursorAutoReviewConfig(false);
|
|
3378
3531
|
}
|
|
3379
3532
|
waitPerm(requestId) {
|
|
3380
|
-
return new Promise((
|
|
3381
|
-
this.permPending.set(requestId,
|
|
3533
|
+
return new Promise((resolve2) => {
|
|
3534
|
+
this.permPending.set(requestId, resolve2);
|
|
3382
3535
|
});
|
|
3383
3536
|
}
|
|
3384
3537
|
respond(requestId, answer) {
|
|
@@ -3416,7 +3569,7 @@ var init_session3 = __esm({
|
|
|
3416
3569
|
}
|
|
3417
3570
|
void this.runTurn(text);
|
|
3418
3571
|
}
|
|
3419
|
-
//
|
|
3572
|
+
// Send this turn's prompt; reading updates is the job of the persistent pumpUpdates() (following Paseo: session/update is handled at session level)
|
|
3420
3573
|
async runTurn(text) {
|
|
3421
3574
|
this.activeTurnId = this.turnId;
|
|
3422
3575
|
this.wakePump?.();
|
|
@@ -3430,9 +3583,9 @@ var init_session3 = __esm({
|
|
|
3430
3583
|
this.running = false;
|
|
3431
3584
|
}
|
|
3432
3585
|
/**
|
|
3433
|
-
*
|
|
3434
|
-
*
|
|
3435
|
-
*
|
|
3586
|
+
* Read session/update for the whole lifetime of the session.
|
|
3587
|
+
* It used to read only during a turn: the available_commands_update an Agent pushes after session/new and before the first message went unread,
|
|
3588
|
+
* so the slash menu stayed empty. Now command updates are cached at any time; conversation content becomes timeline events only while a turn is in progress.
|
|
3436
3589
|
*/
|
|
3437
3590
|
async pumpUpdates() {
|
|
3438
3591
|
const session = this.active;
|
|
@@ -3521,20 +3674,19 @@ var init_session3 = __esm({
|
|
|
3521
3674
|
});
|
|
3522
3675
|
|
|
3523
3676
|
// daemon/src/providers/acp/provider.ts
|
|
3524
|
-
import { spawn as spawn6 } from "node:child_process";
|
|
3525
3677
|
function runVersion3(r, args, timeoutMs = 8e3) {
|
|
3526
3678
|
return new Promise((resolvePromise, reject) => {
|
|
3527
3679
|
let out = "";
|
|
3528
|
-
const p =
|
|
3680
|
+
const p = spawnBin(r, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, NO_COLOR: "1" } });
|
|
3529
3681
|
const t = setTimeout(() => {
|
|
3530
3682
|
try {
|
|
3531
|
-
p
|
|
3683
|
+
void killTree(p, { graceMs: 500 });
|
|
3532
3684
|
} catch {
|
|
3533
3685
|
}
|
|
3534
3686
|
reject(new Error("timeout"));
|
|
3535
3687
|
}, timeoutMs);
|
|
3536
|
-
p.stdout
|
|
3537
|
-
p.stderr
|
|
3688
|
+
p.stdout?.on("data", (c) => out += c.toString());
|
|
3689
|
+
p.stderr?.on("data", (c) => out += c.toString());
|
|
3538
3690
|
p.on("error", reject);
|
|
3539
3691
|
p.on("close", (code) => {
|
|
3540
3692
|
clearTimeout(t);
|
|
@@ -3603,6 +3755,7 @@ function createAcpProvider(cfg2) {
|
|
|
3603
3755
|
var acpCapabilities, cursorAcpCapabilities;
|
|
3604
3756
|
var init_provider3 = __esm({
|
|
3605
3757
|
"daemon/src/providers/acp/provider.ts"() {
|
|
3758
|
+
init_bin();
|
|
3606
3759
|
init_bin();
|
|
3607
3760
|
init_session3();
|
|
3608
3761
|
acpCapabilities = {
|
|
@@ -3802,7 +3955,7 @@ function acpConfigForEntry(entry, appVersion) {
|
|
|
3802
3955
|
acpArgs: ["acp"],
|
|
3803
3956
|
flavor: "cursor",
|
|
3804
3957
|
capabilities: cursorAcpCapabilities,
|
|
3805
|
-
//
|
|
3958
|
+
// Each model's own thinking levels come from cursor/list_available_models (as Paseo does)
|
|
3806
3959
|
fetchModels: cursorProbeModels(["acp"])
|
|
3807
3960
|
};
|
|
3808
3961
|
case "devin":
|
|
@@ -3932,22 +4085,77 @@ var init_pickFolderWin = __esm({
|
|
|
3932
4085
|
}
|
|
3933
4086
|
});
|
|
3934
4087
|
|
|
4088
|
+
// daemon/src/util/pickFolder.ts
|
|
4089
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
4090
|
+
import { existsSync as existsSync7, statSync as statSync3 } from "node:fs";
|
|
4091
|
+
function appleScriptString(s) {
|
|
4092
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
4093
|
+
}
|
|
4094
|
+
function isDir(p) {
|
|
4095
|
+
try {
|
|
4096
|
+
return !!p && existsSync7(p) && statSync3(p).isDirectory();
|
|
4097
|
+
} catch {
|
|
4098
|
+
return false;
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
function pickFolderCommand(platform, initial, find = (n) => findOnPath(n)[0] ?? null, env = process.env) {
|
|
4102
|
+
const init = isDir(initial) ? initial : void 0;
|
|
4103
|
+
if (platform === "darwin") {
|
|
4104
|
+
const prompt = appleScriptString("\u9009\u62E9\u9879\u76EE\u6587\u4EF6\u5939 / Choose a project folder");
|
|
4105
|
+
const loc = init ? ` default location (POSIX file ${appleScriptString(init)})` : "";
|
|
4106
|
+
return { cmd: "/usr/bin/osascript", args: ["-e", `POSIX path of (choose folder with prompt ${prompt}${loc})`] };
|
|
4107
|
+
}
|
|
4108
|
+
if (platform === "linux" || platform === "freebsd" || platform === "openbsd") {
|
|
4109
|
+
if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return null;
|
|
4110
|
+
const zenity = find("zenity");
|
|
4111
|
+
if (zenity) {
|
|
4112
|
+
const args = ["--file-selection", "--directory", "--title=\u9009\u62E9\u9879\u76EE\u6587\u4EF6\u5939 / Choose a project folder"];
|
|
4113
|
+
if (init) args.push(`--filename=${init.endsWith("/") ? init : init + "/"}`);
|
|
4114
|
+
return { cmd: zenity, args };
|
|
4115
|
+
}
|
|
4116
|
+
const kdialog = find("kdialog");
|
|
4117
|
+
if (kdialog) return { cmd: kdialog, args: ["--getexistingdirectory", init ?? env.HOME ?? "/"] };
|
|
4118
|
+
}
|
|
4119
|
+
return null;
|
|
4120
|
+
}
|
|
4121
|
+
function pickFolder(initial) {
|
|
4122
|
+
if (process.platform === "win32") return pickFolderWindows(initial);
|
|
4123
|
+
const c = pickFolderCommand(process.platform, initial);
|
|
4124
|
+
if (!c) return Promise.resolve({ available: false, path: null });
|
|
4125
|
+
return new Promise((resolvePromise) => {
|
|
4126
|
+
execFile2(c.cmd, c.args, { timeout: TIMEOUT, encoding: "utf8" }, (e, stdout) => {
|
|
4127
|
+
if (e) return resolvePromise({ available: true, path: null });
|
|
4128
|
+
let p = String(stdout ?? "").replace(/\r?\n$/, "").trim();
|
|
4129
|
+
if (p.length > 1) p = p.replace(/\/+$/, "");
|
|
4130
|
+
resolvePromise({ available: true, path: p || null });
|
|
4131
|
+
}).on("error", () => resolvePromise({ available: false, path: null }));
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
var TIMEOUT;
|
|
4135
|
+
var init_pickFolder = __esm({
|
|
4136
|
+
"daemon/src/util/pickFolder.ts"() {
|
|
4137
|
+
init_pathscan();
|
|
4138
|
+
init_pickFolderWin();
|
|
4139
|
+
TIMEOUT = 10 * 60 * 1e3;
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
|
|
3935
4143
|
// daemon/src/providers/magpieGateway.ts
|
|
3936
4144
|
import { readFile as readFile9 } from "node:fs/promises";
|
|
3937
|
-
import { homedir as
|
|
3938
|
-
import { join as
|
|
3939
|
-
async function readJson(
|
|
4145
|
+
import { homedir as homedir6 } from "node:os";
|
|
4146
|
+
import { join as join9 } from "node:path";
|
|
4147
|
+
async function readJson(path3) {
|
|
3940
4148
|
try {
|
|
3941
|
-
return JSON.parse(await readFile9(
|
|
4149
|
+
return JSON.parse(await readFile9(path3, "utf8"));
|
|
3942
4150
|
} catch {
|
|
3943
4151
|
return null;
|
|
3944
4152
|
}
|
|
3945
4153
|
}
|
|
3946
4154
|
async function readApplied() {
|
|
3947
|
-
return await readJson(
|
|
4155
|
+
return await readJson(join9(homedir6(), ".config", "magpie", "applied.json")) ?? {};
|
|
3948
4156
|
}
|
|
3949
4157
|
async function readClaudeEnv() {
|
|
3950
|
-
const s = await readJson(
|
|
4158
|
+
const s = await readJson(join9(homedir6(), ".claude", "settings.json"));
|
|
3951
4159
|
return s?.env ?? {};
|
|
3952
4160
|
}
|
|
3953
4161
|
async function gatewayBase() {
|
|
@@ -4116,11 +4324,11 @@ var init_infoCache = __esm({
|
|
|
4116
4324
|
|
|
4117
4325
|
// daemon/src/ws/methods.ts
|
|
4118
4326
|
import { z } from "zod";
|
|
4119
|
-
import { existsSync as
|
|
4327
|
+
import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
|
|
4120
4328
|
import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
|
|
4121
|
-
import { homedir as
|
|
4122
|
-
import { dirname as
|
|
4123
|
-
import { execFile as
|
|
4329
|
+
import { homedir as homedir7 } from "node:os";
|
|
4330
|
+
import { dirname as dirname3, join as join10, win32 as win32Path } from "node:path";
|
|
4331
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
4124
4332
|
function wrap(schema, fn) {
|
|
4125
4333
|
return (params, ctx) => {
|
|
4126
4334
|
const r = schema.safeParse(params ?? {});
|
|
@@ -4235,20 +4443,23 @@ function registerMethods(gw, svc) {
|
|
|
4235
4443
|
gw.method("projects.open", wrap(z.object({ id: z.string() }), async (p) => {
|
|
4236
4444
|
const proj = await svc.projects.get(p.id);
|
|
4237
4445
|
if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
4446
|
+
if (!existsSync8(proj.path)) throw new ApiError("bad_path", "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939");
|
|
4238
4447
|
const cmd = process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
4239
|
-
|
|
4448
|
+
const target = process.platform === "win32" ? win32Path.normalize(proj.path) : proj.path;
|
|
4449
|
+
execFile3(cmd, [target], { windowsHide: true }, () => {
|
|
4450
|
+
}).on("error", () => {
|
|
4240
4451
|
});
|
|
4241
4452
|
return { ok: true };
|
|
4242
4453
|
}));
|
|
4243
4454
|
gw.method("fs.check", wrap(z.object({ path: z.string().min(1) }), (p) => {
|
|
4244
4455
|
let exists = false;
|
|
4245
|
-
let
|
|
4456
|
+
let isDir2 = false;
|
|
4246
4457
|
try {
|
|
4247
|
-
exists =
|
|
4248
|
-
|
|
4458
|
+
exists = existsSync8(p.path);
|
|
4459
|
+
isDir2 = exists && statSync4(p.path).isDirectory();
|
|
4249
4460
|
} catch {
|
|
4250
4461
|
}
|
|
4251
|
-
return { exists, isDir };
|
|
4462
|
+
return { exists, isDir: isDir2 };
|
|
4252
4463
|
}));
|
|
4253
4464
|
gw.method("fs.recentDirs", wrap(zEmpty, async () => {
|
|
4254
4465
|
const s = await svc.settings.get();
|
|
@@ -4256,7 +4467,7 @@ function registerMethods(gw, svc) {
|
|
|
4256
4467
|
}));
|
|
4257
4468
|
gw.method("fs.pickFolder", wrap(
|
|
4258
4469
|
z.object({ initial: z.string().optional() }).passthrough().optional(),
|
|
4259
|
-
(p) =>
|
|
4470
|
+
(p) => pickFolder(p?.initial)
|
|
4260
4471
|
));
|
|
4261
4472
|
gw.method("threads.list", wrap(zEmpty, () => svc.threads.list()));
|
|
4262
4473
|
gw.method("threads.create", wrap(
|
|
@@ -4273,7 +4484,7 @@ function registerMethods(gw, svc) {
|
|
|
4273
4484
|
async (p) => {
|
|
4274
4485
|
const proj = await svc.projects.get(p.projectId);
|
|
4275
4486
|
if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
4276
|
-
if (!
|
|
4487
|
+
if (!existsSync8(proj.path)) throw new ApiError("bad_path", "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728");
|
|
4277
4488
|
if (!p.providerSessionId && !isProviderEnabled(await svc.settings.get(), p.provider)) {
|
|
4278
4489
|
throw new ApiError("bad_params", "\u8BE5 Agent \u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528");
|
|
4279
4490
|
}
|
|
@@ -4335,7 +4546,7 @@ function registerMethods(gw, svc) {
|
|
|
4335
4546
|
const projectPath = async (projectId) => {
|
|
4336
4547
|
const proj = await svc.projects.get(projectId);
|
|
4337
4548
|
if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
4338
|
-
if (!
|
|
4549
|
+
if (!existsSync8(proj.path)) throw new ApiError("bad_path", "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728");
|
|
4339
4550
|
return proj.path;
|
|
4340
4551
|
};
|
|
4341
4552
|
gw.method("git.status", wrap(
|
|
@@ -4416,8 +4627,8 @@ function registerMethods(gw, svc) {
|
|
|
4416
4627
|
gw.method("terminal.create", wrap(
|
|
4417
4628
|
z.object({ projectId: z.string(), cols: z.number().int().min(20).max(500).default(80), rows: z.number().int().min(5).max(200).default(24) }),
|
|
4418
4629
|
async (p) => {
|
|
4419
|
-
const
|
|
4420
|
-
const r = await svc.terminals.create(p.projectId,
|
|
4630
|
+
const path3 = await projectPath(p.projectId);
|
|
4631
|
+
const r = await svc.terminals.create(p.projectId, path3, p.cols, p.rows);
|
|
4421
4632
|
if ("error" in r) throw new ApiError("bad_params", r.error);
|
|
4422
4633
|
return r;
|
|
4423
4634
|
}
|
|
@@ -4554,12 +4765,12 @@ ${e.text}
|
|
|
4554
4765
|
const markdown = L.join("\n");
|
|
4555
4766
|
const safe = meta.title.replace(/[\\/:*?"<>|\s]+/g, "_").slice(0, 60) || "thread";
|
|
4556
4767
|
const filename = `${safe}.md`;
|
|
4557
|
-
const dir =
|
|
4768
|
+
const dir = join10(homedir7(), "Downloads");
|
|
4558
4769
|
await mkdir2(dir, { recursive: true });
|
|
4559
|
-
let dest =
|
|
4560
|
-
if (
|
|
4770
|
+
let dest = join10(dir, filename);
|
|
4771
|
+
if (existsSync8(dest)) {
|
|
4561
4772
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
4562
|
-
dest =
|
|
4773
|
+
dest = join10(dir, `${safe}-${stamp2}.md`);
|
|
4563
4774
|
}
|
|
4564
4775
|
await writeFile3(dest, markdown, "utf8");
|
|
4565
4776
|
revealInFileManager(dest);
|
|
@@ -4568,16 +4779,19 @@ ${e.text}
|
|
|
4568
4779
|
}
|
|
4569
4780
|
function revealInFileManager(filePath) {
|
|
4570
4781
|
if (process.platform === "win32") {
|
|
4571
|
-
|
|
4782
|
+
execFile3("explorer.exe", ["/select," + win32Path.normalize(filePath)], { windowsHide: true }, () => {
|
|
4783
|
+
}).on("error", () => {
|
|
4572
4784
|
});
|
|
4573
4785
|
return;
|
|
4574
4786
|
}
|
|
4575
4787
|
if (process.platform === "darwin") {
|
|
4576
|
-
|
|
4788
|
+
execFile3("open", ["-R", filePath], () => {
|
|
4789
|
+
}).on("error", () => {
|
|
4577
4790
|
});
|
|
4578
4791
|
return;
|
|
4579
4792
|
}
|
|
4580
|
-
|
|
4793
|
+
execFile3("xdg-open", [dirname3(filePath)], () => {
|
|
4794
|
+
}).on("error", () => {
|
|
4581
4795
|
});
|
|
4582
4796
|
}
|
|
4583
4797
|
var threadModeSchema, ApiError;
|
|
@@ -4586,7 +4800,7 @@ var init_methods = __esm({
|
|
|
4586
4800
|
init_registry();
|
|
4587
4801
|
init_catalog();
|
|
4588
4802
|
init_enabled();
|
|
4589
|
-
|
|
4803
|
+
init_pickFolder();
|
|
4590
4804
|
init_bin();
|
|
4591
4805
|
init_infoCache();
|
|
4592
4806
|
threadModeSchema = z.enum(["plan", "default", "acceptEdits", "bypassPermissions", "ask"]);
|
|
@@ -4602,10 +4816,10 @@ var init_methods = __esm({
|
|
|
4602
4816
|
|
|
4603
4817
|
// daemon/src/util/atomic.ts
|
|
4604
4818
|
import { writeFile as writeFile4, rename, mkdir as mkdir3 } from "node:fs/promises";
|
|
4605
|
-
import { dirname as
|
|
4819
|
+
import { dirname as dirname4 } from "node:path";
|
|
4606
4820
|
async function writeJsonAtomic(file, obj) {
|
|
4607
4821
|
try {
|
|
4608
|
-
await mkdir3(
|
|
4822
|
+
await mkdir3(dirname4(file), { recursive: true });
|
|
4609
4823
|
const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4610
4824
|
await writeFile4(tmp, JSON.stringify(obj, null, 2), "utf8");
|
|
4611
4825
|
await rename(tmp, file);
|
|
@@ -4622,7 +4836,7 @@ var init_atomic = __esm({
|
|
|
4622
4836
|
|
|
4623
4837
|
// daemon/src/util/jsonl.ts
|
|
4624
4838
|
import { appendFile, readFile as readFile12, mkdir as mkdir4, rename as rename2, writeFile as writeFile5 } from "node:fs/promises";
|
|
4625
|
-
import { dirname as
|
|
4839
|
+
import { dirname as dirname5 } from "node:path";
|
|
4626
4840
|
async function readJsonl(file) {
|
|
4627
4841
|
let text = "";
|
|
4628
4842
|
try {
|
|
@@ -4645,7 +4859,7 @@ function appendJsonl(file, obj) {
|
|
|
4645
4859
|
const prev = writers.get(file) ?? Promise.resolve();
|
|
4646
4860
|
const next = prev.then(async () => {
|
|
4647
4861
|
try {
|
|
4648
|
-
await mkdir4(
|
|
4862
|
+
await mkdir4(dirname5(file), { recursive: true });
|
|
4649
4863
|
await appendFile(file, JSON.stringify(obj) + "\n", "utf8");
|
|
4650
4864
|
} catch (e) {
|
|
4651
4865
|
if (e instanceof ApiError) throw e;
|
|
@@ -4672,8 +4886,8 @@ var legacy_exports = {};
|
|
|
4672
4886
|
__export(legacy_exports, {
|
|
4673
4887
|
migrateLegacy: () => migrateLegacy
|
|
4674
4888
|
});
|
|
4675
|
-
import { cp, mkdir as mkdir7, readdir as readdir6, readFile as readFile16 } from "node:fs/promises";
|
|
4676
|
-
import { existsSync as
|
|
4889
|
+
import { cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile16 } from "node:fs/promises";
|
|
4890
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
4677
4891
|
import { join as join17 } from "node:path";
|
|
4678
4892
|
function stamp() {
|
|
4679
4893
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -4685,17 +4899,17 @@ async function migrateLegacy(dataDir, stores) {
|
|
|
4685
4899
|
if (settings.v1Migrated) return { migrated: 0, skipped: true };
|
|
4686
4900
|
const oldDir = join17(dataDir, "sessions");
|
|
4687
4901
|
const oldSettingsPath = join17(dataDir, "settings.json");
|
|
4688
|
-
if (!
|
|
4902
|
+
if (!existsSync12(oldDir) && !existsSync12(oldSettingsPath)) {
|
|
4689
4903
|
await stores.settings.set({ v1Migrated: true });
|
|
4690
4904
|
return { migrated: 0, skipped: true };
|
|
4691
4905
|
}
|
|
4692
4906
|
const backup = join17(dataDir, "trash", `legacy-${stamp()}`);
|
|
4693
4907
|
await mkdir7(backup, { recursive: true });
|
|
4694
|
-
if (
|
|
4908
|
+
if (existsSync12(oldDir)) await cp2(oldDir, join17(backup, "sessions"), { recursive: true }).catch(() => {
|
|
4695
4909
|
});
|
|
4696
|
-
if (
|
|
4910
|
+
if (existsSync12(oldSettingsPath)) await cp2(oldSettingsPath, join17(backup, "settings.json")).catch(() => {
|
|
4697
4911
|
});
|
|
4698
|
-
const oldSettings =
|
|
4912
|
+
const oldSettings = existsSync12(oldSettingsPath) ? JSON.parse(await readFile16(oldSettingsPath, "utf8").catch(() => "{}")) : {};
|
|
4699
4913
|
const projectIdByPath = /* @__PURE__ */ new Map();
|
|
4700
4914
|
const lc = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
|
|
4701
4915
|
for (const p of oldSettings.projects ?? []) {
|
|
@@ -4709,7 +4923,7 @@ async function migrateLegacy(dataDir, stores) {
|
|
|
4709
4923
|
}
|
|
4710
4924
|
let sessionIds = [];
|
|
4711
4925
|
try {
|
|
4712
|
-
sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) &&
|
|
4926
|
+
sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync12(join17(oldDir, d, "meta.json")));
|
|
4713
4927
|
} catch {
|
|
4714
4928
|
}
|
|
4715
4929
|
const metas = [];
|
|
@@ -4729,7 +4943,7 @@ async function migrateLegacy(dataDir, stores) {
|
|
|
4729
4943
|
extraDirs.push(m.cwd);
|
|
4730
4944
|
}
|
|
4731
4945
|
for (const d of extraDirs) {
|
|
4732
|
-
if (!
|
|
4946
|
+
if (!existsSync12(d)) continue;
|
|
4733
4947
|
try {
|
|
4734
4948
|
const name = d.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? d;
|
|
4735
4949
|
const proj = await stores.projects.create(d, name);
|
|
@@ -4786,7 +5000,7 @@ async function migrateLegacy(dataDir, stores) {
|
|
|
4786
5000
|
seq: 0
|
|
4787
5001
|
};
|
|
4788
5002
|
const tdir = join17(threadsDir, meta.id);
|
|
4789
|
-
if (
|
|
5003
|
+
if (existsSync12(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
|
|
4790
5004
|
await mkdir7(join17(threadsDir, meta.id), { recursive: true });
|
|
4791
5005
|
const oldEvts = (await readFile16(join17(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
|
|
4792
5006
|
try {
|
|
@@ -4845,7 +5059,7 @@ async function migrateLegacy(dataDir, stores) {
|
|
|
4845
5059
|
case "permission.request":
|
|
4846
5060
|
case "permission.resolved":
|
|
4847
5061
|
break;
|
|
4848
|
-
//
|
|
5062
|
+
// Old permission cards are stale; don't recreate them
|
|
4849
5063
|
case "notice":
|
|
4850
5064
|
push({ type: "notice", level: e.level ?? "info", message: String(e.message ?? "") });
|
|
4851
5065
|
break;
|
|
@@ -4933,7 +5147,7 @@ var init_warm = __esm({
|
|
|
4933
5147
|
});
|
|
4934
5148
|
|
|
4935
5149
|
// daemon/src/main.ts
|
|
4936
|
-
import { mkdirSync as mkdirSync2, rmSync as rmSync2 } from "node:fs";
|
|
5150
|
+
import { mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
4937
5151
|
import { join as join18, dirname as dirname7 } from "node:path";
|
|
4938
5152
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4939
5153
|
|
|
@@ -4955,9 +5169,9 @@ function resolveDaemonMainJs() {
|
|
|
4955
5169
|
const here = dirname(self);
|
|
4956
5170
|
const candidates = [
|
|
4957
5171
|
join(here, "..", "..", "dist", "main.js"),
|
|
4958
|
-
// src/util → daemon/dist/main.js
|
|
5172
|
+
// src/util → daemon/dist/main.js (vitest/tsc)
|
|
4959
5173
|
join(here, "main.js"),
|
|
4960
|
-
//
|
|
5174
|
+
// Already in the dist directory
|
|
4961
5175
|
join(here, "..", "main.js")
|
|
4962
5176
|
];
|
|
4963
5177
|
for (const p of candidates) {
|
|
@@ -4989,6 +5203,7 @@ function parseConfig(argv) {
|
|
|
4989
5203
|
let port = null;
|
|
4990
5204
|
let data = null;
|
|
4991
5205
|
let token = null;
|
|
5206
|
+
let parentPid;
|
|
4992
5207
|
for (let i = 0; i < argv.length; i++) {
|
|
4993
5208
|
const a = argv[i];
|
|
4994
5209
|
if (a === "--port" && argv[i + 1]) port = Number(argv[++i]);
|
|
@@ -4997,10 +5212,12 @@ function parseConfig(argv) {
|
|
|
4997
5212
|
else if (a.startsWith("--data=")) data = a.slice(7);
|
|
4998
5213
|
else if (a === "--token" && argv[i + 1]) token = argv[++i];
|
|
4999
5214
|
else if (a.startsWith("--token=")) token = a.slice(8);
|
|
5215
|
+
else if (a === "--parent-pid" && argv[i + 1]) parentPid = Number(argv[++i]);
|
|
5216
|
+
else if (a.startsWith("--parent-pid=")) parentPid = Number(a.slice(13));
|
|
5000
5217
|
}
|
|
5001
|
-
if (port == null || !Number.isInteger(port) || port <
|
|
5218
|
+
if (port == null || !Number.isInteger(port) || port < 0 || port > 65535) {
|
|
5002
5219
|
const envPort = Number(process.env.AGENT_CONSOLE_PORT || "");
|
|
5003
|
-
port = Number.isInteger(envPort) && envPort >=
|
|
5220
|
+
port = Number.isInteger(envPort) && envPort >= 0 && envPort <= 65535 ? envPort : 3488;
|
|
5004
5221
|
}
|
|
5005
5222
|
const xdg = data ?? process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config");
|
|
5006
5223
|
const dataDir = resolve(join2(xdg, "magpie", "agent-console"));
|
|
@@ -5011,7 +5228,8 @@ function parseConfig(argv) {
|
|
|
5011
5228
|
token: token ?? process.env.AGENT_CONSOLE_TOKEN ?? randomBytes(16).toString("hex"),
|
|
5012
5229
|
version: pkgVersion(),
|
|
5013
5230
|
idleExitMinutes: Number.isFinite(idleEnv) && idleEnv > 0 ? idleEnv : 60,
|
|
5014
|
-
buildStamp: daemonBuildStamp()
|
|
5231
|
+
buildStamp: daemonBuildStamp(),
|
|
5232
|
+
parentPid: Number.isInteger(parentPid) ? parentPid : void 0
|
|
5015
5233
|
};
|
|
5016
5234
|
}
|
|
5017
5235
|
|
|
@@ -5032,11 +5250,12 @@ function checkHost(host, port) {
|
|
|
5032
5250
|
if (h === `127.0.0.1:${port}` || h === `localhost:${port}`) return { ok: true };
|
|
5033
5251
|
return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
|
|
5034
5252
|
}
|
|
5035
|
-
function
|
|
5036
|
-
if (origin === void 0 || origin === "
|
|
5253
|
+
function classifyOrigin(origin, port) {
|
|
5254
|
+
if (origin === void 0 || origin === "") return "none";
|
|
5255
|
+
if (origin === "null") return "opaque";
|
|
5037
5256
|
const o = origin.toLowerCase();
|
|
5038
|
-
if (o === `http://127.0.0.1:${port}` || o === `http://localhost:${port}`) return
|
|
5039
|
-
return
|
|
5257
|
+
if (o === `http://127.0.0.1:${port}` || o === `http://localhost:${port}`) return "self";
|
|
5258
|
+
return "foreign";
|
|
5040
5259
|
}
|
|
5041
5260
|
function tokenEqual(a, b) {
|
|
5042
5261
|
const ha = createHash("sha256").update(a).digest();
|
|
@@ -5062,6 +5281,13 @@ var MIME = {
|
|
|
5062
5281
|
};
|
|
5063
5282
|
var MAX_UPLOAD = 50 * 1024 * 1024;
|
|
5064
5283
|
var MAX_MSG = 200 * 1024;
|
|
5284
|
+
function safeDecode(s) {
|
|
5285
|
+
try {
|
|
5286
|
+
return decodeURIComponent(s);
|
|
5287
|
+
} catch {
|
|
5288
|
+
return null;
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5065
5291
|
function send(res, code, body, headers = {}) {
|
|
5066
5292
|
res.writeHead(code, headers);
|
|
5067
5293
|
res.end(body);
|
|
@@ -5069,14 +5295,11 @@ function send(res, code, body, headers = {}) {
|
|
|
5069
5295
|
function sendJson(res, code, obj, headers = {}) {
|
|
5070
5296
|
send(res, code, JSON.stringify(obj), { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...headers });
|
|
5071
5297
|
}
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
"access-control-max-age": "600"
|
|
5078
|
-
};
|
|
5079
|
-
}
|
|
5298
|
+
var STATIC_HEADERS = {
|
|
5299
|
+
"cache-control": "no-store",
|
|
5300
|
+
"x-content-type-options": "nosniff",
|
|
5301
|
+
"referrer-policy": "no-referrer"
|
|
5302
|
+
};
|
|
5080
5303
|
function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
|
|
5081
5304
|
const server = createServer(async (req, res) => {
|
|
5082
5305
|
try {
|
|
@@ -5093,46 +5316,57 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
|
|
|
5093
5316
|
sendJson(res, hostCheck.code, { ok: false, e: { code: "bad_host", message: hostCheck.message } });
|
|
5094
5317
|
return;
|
|
5095
5318
|
}
|
|
5096
|
-
const
|
|
5097
|
-
if (
|
|
5098
|
-
sendJson(res,
|
|
5319
|
+
const origin = classifyOrigin(req.headers.origin, cfg2.port);
|
|
5320
|
+
if (origin === "foreign") {
|
|
5321
|
+
sendJson(res, 403, { ok: false, e: { code: "bad_origin", message: "\u6765\u6E90\u4E0D\u5141\u8BB8" } });
|
|
5099
5322
|
return;
|
|
5100
5323
|
}
|
|
5101
5324
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${cfg2.port}`);
|
|
5102
5325
|
const pathname = url.pathname;
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
res.setHeader("vary", "origin");
|
|
5106
|
-
}
|
|
5326
|
+
const isApi = pathname.startsWith("/api/");
|
|
5327
|
+
const isAttachment = pathname.startsWith("/attachments/");
|
|
5107
5328
|
if (req.method === "OPTIONS") {
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
}
|
|
5111
|
-
if (pathname.startsWith("/api/")) {
|
|
5112
|
-
const tok = req.headers["x-agent-console"];
|
|
5113
|
-
if (typeof tok !== "string" || !tokenEqual(tok, cfg2.token)) {
|
|
5114
|
-
sendJson(res, 403, { ok: false, e: { code: "bad_token", message: "\u4EE4\u724C\u65E0\u6548" } });
|
|
5329
|
+
if (pathname !== "/api/upload" || origin === "none") {
|
|
5330
|
+
sendJson(res, 403, { ok: false, e: { code: "bad_preflight", message: "\u9884\u68C0\u4E0D\u5141\u8BB8" } });
|
|
5115
5331
|
return;
|
|
5116
5332
|
}
|
|
5117
|
-
|
|
5333
|
+
send(res, 204, "", {
|
|
5334
|
+
"access-control-allow-origin": origin === "opaque" ? "null" : `http://127.0.0.1:${cfg2.port}`,
|
|
5335
|
+
"access-control-allow-headers": "x-agent-console, content-type",
|
|
5336
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
5337
|
+
"access-control-max-age": "600",
|
|
5338
|
+
vary: "origin"
|
|
5339
|
+
});
|
|
5118
5340
|
return;
|
|
5119
5341
|
}
|
|
5120
|
-
if (
|
|
5121
|
-
const
|
|
5122
|
-
const
|
|
5123
|
-
const
|
|
5124
|
-
if (!
|
|
5342
|
+
if (isApi || isAttachment) {
|
|
5343
|
+
const headerToken = req.headers["x-agent-console"];
|
|
5344
|
+
const queryToken = isAttachment ? url.searchParams.get("t") : null;
|
|
5345
|
+
const validToken = typeof headerToken === "string" && tokenEqual(headerToken, cfg2.token) || typeof queryToken === "string" && tokenEqual(queryToken, cfg2.token);
|
|
5346
|
+
if (!validToken) {
|
|
5125
5347
|
sendJson(res, 403, { ok: false, e: { code: "bad_token", message: "\u4EE4\u724C\u65E0\u6548" } });
|
|
5126
5348
|
return;
|
|
5127
5349
|
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5350
|
+
if (origin === "opaque") {
|
|
5351
|
+
res.setHeader("access-control-allow-origin", "null");
|
|
5352
|
+
res.setHeader("vary", "origin");
|
|
5353
|
+
}
|
|
5354
|
+
if (isApi) {
|
|
5355
|
+
await routeApi(req, res, pathname, url);
|
|
5356
|
+
return;
|
|
5357
|
+
}
|
|
5358
|
+
const id = safeDecode(pathname.slice("/attachments/".length));
|
|
5359
|
+
const a = id === null ? null : await attachments.get(id);
|
|
5130
5360
|
if (!a) {
|
|
5131
5361
|
sendJson(res, 404, { ok: false, e: { code: "not_found", message: "\u9644\u4EF6\u4E0D\u5B58\u5728" } });
|
|
5132
5362
|
return;
|
|
5133
5363
|
}
|
|
5134
|
-
const data2 = await attachments.read(id);
|
|
5135
|
-
send(res, 200, data2 ?? Buffer.alloc(0), { "content-type": a.mime
|
|
5364
|
+
const data2 = await attachments.read(a.id);
|
|
5365
|
+
send(res, 200, data2 ?? Buffer.alloc(0), { ...STATIC_HEADERS, "content-type": a.mime });
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
if (origin === "opaque") {
|
|
5369
|
+
sendJson(res, 403, { ok: false, e: { code: "bad_origin", message: "\u6765\u6E90\u4E0D\u5141\u8BB8" } });
|
|
5136
5370
|
return;
|
|
5137
5371
|
}
|
|
5138
5372
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -5140,17 +5374,20 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
|
|
|
5140
5374
|
return;
|
|
5141
5375
|
}
|
|
5142
5376
|
if (pathname === "/" || pathname === "/index.html") {
|
|
5143
|
-
const htmlPath = join4(appDir, "index.html");
|
|
5144
5377
|
try {
|
|
5145
|
-
|
|
5146
|
-
html
|
|
5147
|
-
send(res, 200, html, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
5378
|
+
const html = await readFile(join4(appDir, "index.html"), "utf8");
|
|
5379
|
+
send(res, 200, html, { ...STATIC_HEADERS, "content-type": "text/html; charset=utf-8" });
|
|
5148
5380
|
} catch {
|
|
5149
|
-
send(res, 503, "app \u8FD8\u6CA1\u6784\u5EFA\uFF08\u7F3A app/index.html\uFF09", { "content-type": "text/plain; charset=utf-8" });
|
|
5381
|
+
send(res, 503, "app \u8FD8\u6CA1\u6784\u5EFA\uFF08\u7F3A app/index.html\uFF09", { ...STATIC_HEADERS, "content-type": "text/plain; charset=utf-8" });
|
|
5150
5382
|
}
|
|
5151
5383
|
return;
|
|
5152
5384
|
}
|
|
5153
|
-
const
|
|
5385
|
+
const decoded = safeDecode(pathname);
|
|
5386
|
+
if (decoded === null) {
|
|
5387
|
+
sendJson(res, 400, { ok: false, e: { code: "bad_path", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5" } });
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
const rel = normalize(decoded).replace(/^([\\/])+/, "");
|
|
5154
5391
|
const full = normalize(join4(appDir, rel));
|
|
5155
5392
|
if (!full.startsWith(normalize(appDir)) || rel.includes("..")) {
|
|
5156
5393
|
sendJson(res, 403, { ok: false, e: { code: "bad_path", message: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" } });
|
|
@@ -5161,7 +5398,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
|
|
|
5161
5398
|
return;
|
|
5162
5399
|
}
|
|
5163
5400
|
const data = await readFile(full);
|
|
5164
|
-
send(res, 200, data, { "content-type": MIME[extname(full).toLowerCase()] ?? "application/octet-stream"
|
|
5401
|
+
send(res, 200, data, { ...STATIC_HEADERS, "content-type": MIME[extname(full).toLowerCase()] ?? "application/octet-stream" });
|
|
5165
5402
|
}
|
|
5166
5403
|
async function routeApi(req, res, pathname, url) {
|
|
5167
5404
|
if (pathname === "/api/ping" && req.method === "GET") {
|
|
@@ -5174,12 +5411,20 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
|
|
|
5174
5411
|
sendJson(res, 400, { ok: false, e: { code: "bad_type", message: "\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B" } });
|
|
5175
5412
|
return;
|
|
5176
5413
|
}
|
|
5414
|
+
const declared = Number(req.headers["content-length"] ?? 0);
|
|
5415
|
+
if (declared > MAX_UPLOAD) {
|
|
5416
|
+
res.setHeader("connection", "close");
|
|
5417
|
+
sendJson(res, 413, { ok: false, e: { code: "too_large", message: "\u6587\u4EF6\u592A\u5927\uFF08\u4E0A\u9650 50MB\uFF09" } });
|
|
5418
|
+
req.resume();
|
|
5419
|
+
return;
|
|
5420
|
+
}
|
|
5177
5421
|
const chunks = [];
|
|
5178
5422
|
let size = 0;
|
|
5179
5423
|
for await (const c of req) {
|
|
5180
5424
|
const b = c;
|
|
5181
5425
|
size += b.length;
|
|
5182
5426
|
if (size > MAX_UPLOAD) {
|
|
5427
|
+
res.setHeader("connection", "close");
|
|
5183
5428
|
sendJson(res, 413, { ok: false, e: { code: "too_large", message: "\u6587\u4EF6\u592A\u5927\uFF08\u4E0A\u9650 50MB\uFF09" } });
|
|
5184
5429
|
return;
|
|
5185
5430
|
}
|
|
@@ -5211,11 +5456,14 @@ var MIME2 = {
|
|
|
5211
5456
|
".gif": "image/gif",
|
|
5212
5457
|
".webp": "image/webp"
|
|
5213
5458
|
};
|
|
5459
|
+
var TEXT_MIME = "text/plain; charset=utf-8";
|
|
5214
5460
|
var AttachmentStore = class {
|
|
5215
5461
|
constructor(dir) {
|
|
5216
5462
|
this.dir = dir;
|
|
5217
5463
|
}
|
|
5218
5464
|
dir;
|
|
5465
|
+
// id → file name: avoids a readdir of the whole directory on every lookup (slow with many attachments)
|
|
5466
|
+
files = /* @__PURE__ */ new Map();
|
|
5219
5467
|
safeExt(name) {
|
|
5220
5468
|
const ext = extname2(name).toLowerCase();
|
|
5221
5469
|
if (IMG_EXT.has(ext) || TEXT_EXT.has(ext)) return ext;
|
|
@@ -5232,25 +5480,37 @@ var AttachmentStore = class {
|
|
|
5232
5480
|
await mkdir(this.dir, { recursive: true });
|
|
5233
5481
|
const file = join5(this.dir, id + ext);
|
|
5234
5482
|
await writeFile(file, data);
|
|
5235
|
-
|
|
5483
|
+
this.files.set(id, id + ext);
|
|
5484
|
+
const mime = MIME2[ext] ?? TEXT_MIME;
|
|
5236
5485
|
return { id, name: name || "\u672A\u547D\u540D" + ext, mime, size: data.length, path: file };
|
|
5237
5486
|
}
|
|
5238
5487
|
async get(id) {
|
|
5239
5488
|
if (!validId(id)) return null;
|
|
5489
|
+
const known = this.files.get(id);
|
|
5490
|
+
if (known) {
|
|
5491
|
+
const hit = await this.describe(id, known);
|
|
5492
|
+
if (hit) return hit;
|
|
5493
|
+
this.files.delete(id);
|
|
5494
|
+
}
|
|
5240
5495
|
const { readdir: readdir7 } = await import("node:fs/promises");
|
|
5241
5496
|
try {
|
|
5242
5497
|
for (const f of await readdir7(this.dir)) {
|
|
5243
5498
|
if (f.startsWith(id + ".")) {
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
const ext = extname2(f).toLowerCase();
|
|
5247
|
-
return { id, name: f.slice(id.length + 1) || f, mime: MIME2[ext] ?? "text/plain", size: s.size, path: p };
|
|
5499
|
+
this.files.set(id, f);
|
|
5500
|
+
return await this.describe(id, f);
|
|
5248
5501
|
}
|
|
5249
5502
|
}
|
|
5250
5503
|
} catch {
|
|
5251
5504
|
}
|
|
5252
5505
|
return null;
|
|
5253
5506
|
}
|
|
5507
|
+
async describe(id, f) {
|
|
5508
|
+
const p = join5(this.dir, f);
|
|
5509
|
+
const s = await stat2(p).catch(() => null);
|
|
5510
|
+
if (!s?.isFile()) return null;
|
|
5511
|
+
const ext = extname2(f).toLowerCase();
|
|
5512
|
+
return { id, name: f.slice(id.length + 1) || f, mime: MIME2[ext] ?? TEXT_MIME, size: s.size, path: p };
|
|
5513
|
+
}
|
|
5254
5514
|
async read(id) {
|
|
5255
5515
|
const a = await this.get(id);
|
|
5256
5516
|
if (!a) return null;
|
|
@@ -5261,7 +5521,7 @@ var AttachmentStore = class {
|
|
|
5261
5521
|
// daemon/src/store/settings.ts
|
|
5262
5522
|
init_atomic();
|
|
5263
5523
|
import { readFile as readFile10 } from "node:fs/promises";
|
|
5264
|
-
import { join as
|
|
5524
|
+
import { join as join11 } from "node:path";
|
|
5265
5525
|
var DEFAULT_SETTINGS = {
|
|
5266
5526
|
defaultProvider: "claude",
|
|
5267
5527
|
defaultMode: "default",
|
|
@@ -5285,7 +5545,7 @@ var SettingsStore = class {
|
|
|
5285
5545
|
cache = null;
|
|
5286
5546
|
writing = Promise.resolve();
|
|
5287
5547
|
constructor(dataDir) {
|
|
5288
|
-
this.file =
|
|
5548
|
+
this.file = join11(dataDir, "settings.json");
|
|
5289
5549
|
}
|
|
5290
5550
|
async get() {
|
|
5291
5551
|
if (this.cache) return this.cache;
|
|
@@ -5313,8 +5573,8 @@ init_atomic();
|
|
|
5313
5573
|
init_paths();
|
|
5314
5574
|
init_methods();
|
|
5315
5575
|
import { readFile as readFile11 } from "node:fs/promises";
|
|
5316
|
-
import { existsSync as
|
|
5317
|
-
import { join as
|
|
5576
|
+
import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
|
|
5577
|
+
import { join as join12 } from "node:path";
|
|
5318
5578
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
5319
5579
|
function newId() {
|
|
5320
5580
|
return "p_" + Date.now().toString(36) + randomBytes3(3).toString("hex");
|
|
@@ -5324,7 +5584,7 @@ var ProjectsStore = class {
|
|
|
5324
5584
|
cache = null;
|
|
5325
5585
|
writing = Promise.resolve();
|
|
5326
5586
|
constructor(dataDir) {
|
|
5327
|
-
this.file =
|
|
5587
|
+
this.file = join12(dataDir, "projects.json");
|
|
5328
5588
|
}
|
|
5329
5589
|
async list() {
|
|
5330
5590
|
if (this.cache) return this.cache;
|
|
@@ -5346,19 +5606,19 @@ var ProjectsStore = class {
|
|
|
5346
5606
|
this.writing = this.writing.then(() => writeJsonAtomic(this.file, snap));
|
|
5347
5607
|
await this.writing;
|
|
5348
5608
|
}
|
|
5349
|
-
async create(
|
|
5350
|
-
if (!
|
|
5351
|
-
if (!
|
|
5609
|
+
async create(path3, name) {
|
|
5610
|
+
if (!path3 || typeof path3 !== "string") throw new ApiError("bad_params", "\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
5611
|
+
if (!existsSync9(path3) || !statSync5(path3).isDirectory()) {
|
|
5352
5612
|
throw new ApiError("bad_path", "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939");
|
|
5353
5613
|
}
|
|
5354
|
-
const np = normPath(
|
|
5614
|
+
const np = normPath(path3);
|
|
5355
5615
|
const list = await this.list();
|
|
5356
5616
|
const dup = list.find((p) => normPath(p.path) === np);
|
|
5357
5617
|
if (dup) return dup;
|
|
5358
5618
|
const proj = {
|
|
5359
5619
|
id: newId(),
|
|
5360
|
-
name: name?.trim() ||
|
|
5361
|
-
path,
|
|
5620
|
+
name: name?.trim() || path3.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path3,
|
|
5621
|
+
path: path3,
|
|
5362
5622
|
createdAt: Date.now()
|
|
5363
5623
|
};
|
|
5364
5624
|
list.push(proj);
|
|
@@ -5389,7 +5649,7 @@ init_paths();
|
|
|
5389
5649
|
init_methods();
|
|
5390
5650
|
init_i18n();
|
|
5391
5651
|
import { mkdir as mkdir5, rename as rename3, readdir as readdir4, readFile as readFile13 } from "node:fs/promises";
|
|
5392
|
-
import { join as
|
|
5652
|
+
import { join as join13 } from "node:path";
|
|
5393
5653
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
5394
5654
|
function newId2() {
|
|
5395
5655
|
return "t_" + Date.now().toString(36) + randomBytes4(3).toString("hex");
|
|
@@ -5399,7 +5659,7 @@ var ThreadsStore = class {
|
|
|
5399
5659
|
trashDir;
|
|
5400
5660
|
metas = /* @__PURE__ */ new Map();
|
|
5401
5661
|
loaded = false;
|
|
5402
|
-
// meta
|
|
5662
|
+
// Serialize meta writes: concurrent patch/appendEvent would overwrite each other (setStatus/patch/provider_session_id run concurrently in the manager)
|
|
5403
5663
|
writeQ = Promise.resolve();
|
|
5404
5664
|
serial(fn) {
|
|
5405
5665
|
const p = this.writeQ.then(fn, fn);
|
|
@@ -5408,17 +5668,17 @@ var ThreadsStore = class {
|
|
|
5408
5668
|
return p;
|
|
5409
5669
|
}
|
|
5410
5670
|
constructor(dataDir) {
|
|
5411
|
-
this.dir =
|
|
5412
|
-
this.trashDir =
|
|
5671
|
+
this.dir = join13(dataDir, "threads");
|
|
5672
|
+
this.trashDir = join13(dataDir, "trash", "threads");
|
|
5413
5673
|
}
|
|
5414
5674
|
tdir(id) {
|
|
5415
|
-
return
|
|
5675
|
+
return join13(this.dir, id);
|
|
5416
5676
|
}
|
|
5417
5677
|
metaPath(id) {
|
|
5418
|
-
return
|
|
5678
|
+
return join13(this.tdir(id), "meta.json");
|
|
5419
5679
|
}
|
|
5420
5680
|
timelinePath(id) {
|
|
5421
|
-
return
|
|
5681
|
+
return join13(this.tdir(id), "timeline.jsonl");
|
|
5422
5682
|
}
|
|
5423
5683
|
async loadAll() {
|
|
5424
5684
|
if (this.loaded) return;
|
|
@@ -5490,8 +5750,8 @@ var ThreadsStore = class {
|
|
|
5490
5750
|
return next;
|
|
5491
5751
|
});
|
|
5492
5752
|
}
|
|
5493
|
-
//
|
|
5494
|
-
//
|
|
5753
|
+
// Append an event: inside the serialized section take meta.seq+1 as seq, write the jsonl and update meta.
|
|
5754
|
+
// A passed-in seq is ignored (keeps it strictly increasing); ts/threadId are filled in when missing.
|
|
5495
5755
|
async appendEvent(id, ev) {
|
|
5496
5756
|
return this.serial(async () => {
|
|
5497
5757
|
const meta = await this.get(id);
|
|
@@ -5522,7 +5782,7 @@ var ThreadsStore = class {
|
|
|
5522
5782
|
await mkdir5(this.trashDir, { recursive: true });
|
|
5523
5783
|
for (let attempt = 0; ; attempt++) {
|
|
5524
5784
|
try {
|
|
5525
|
-
await rename3(this.tdir(id),
|
|
5785
|
+
await rename3(this.tdir(id), join13(this.trashDir, `${id}-${stamp2}`));
|
|
5526
5786
|
break;
|
|
5527
5787
|
} catch (e) {
|
|
5528
5788
|
const code = e?.code;
|
|
@@ -5566,7 +5826,7 @@ var Conn = class {
|
|
|
5566
5826
|
} catch {
|
|
5567
5827
|
}
|
|
5568
5828
|
}
|
|
5569
|
-
//
|
|
5829
|
+
// Look at bytes really still unsent in the socket (only grows when the client is stuck), not the cumulative amount sent
|
|
5570
5830
|
overflow(next) {
|
|
5571
5831
|
return this.ws.bufferedAmount + next > 8 * 1024 * 1024;
|
|
5572
5832
|
}
|
|
@@ -5607,8 +5867,7 @@ var Gateway = class {
|
|
|
5607
5867
|
socket.destroy();
|
|
5608
5868
|
return;
|
|
5609
5869
|
}
|
|
5610
|
-
|
|
5611
|
-
if (!originCheck.ok) {
|
|
5870
|
+
if (classifyOrigin(req.headers.origin, this.cfg.port) === "foreign") {
|
|
5612
5871
|
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
5613
5872
|
socket.destroy();
|
|
5614
5873
|
return;
|
|
@@ -5700,7 +5959,7 @@ var Gateway = class {
|
|
|
5700
5959
|
});
|
|
5701
5960
|
}
|
|
5702
5961
|
}
|
|
5703
|
-
//
|
|
5962
|
+
// Terminal binary frame: [op:1][terminalIndex:2 BE][payload]
|
|
5704
5963
|
onBinary(conn, buf) {
|
|
5705
5964
|
if (buf.length < 3) return;
|
|
5706
5965
|
const op = buf.readUInt8(0);
|
|
@@ -5756,7 +6015,7 @@ var Coalescer = class {
|
|
|
5756
6015
|
this.windowMs = windowMs;
|
|
5757
6016
|
this.onEmit = onEmit;
|
|
5758
6017
|
}
|
|
5759
|
-
//
|
|
6018
|
+
// Push a delta; the first goes out immediately, the rest are merged within the window and sent trailing
|
|
5760
6019
|
push(itemId, text) {
|
|
5761
6020
|
const acc = this.acc.get(itemId);
|
|
5762
6021
|
if (!acc) {
|
|
@@ -5766,7 +6025,7 @@ var Coalescer = class {
|
|
|
5766
6025
|
}
|
|
5767
6026
|
acc.text += text;
|
|
5768
6027
|
}
|
|
5769
|
-
//
|
|
6028
|
+
// Must flush before the complete event arrives to keep ordering (deltas before the complete event)
|
|
5770
6029
|
flush(itemId) {
|
|
5771
6030
|
const acc = this.acc.get(itemId);
|
|
5772
6031
|
if (!acc) return;
|
|
@@ -5777,7 +6036,7 @@ var Coalescer = class {
|
|
|
5777
6036
|
if (acc.text.length > 0) this.onEmit(itemId, acc.text);
|
|
5778
6037
|
this.acc.delete(itemId);
|
|
5779
6038
|
}
|
|
5780
|
-
// itemId
|
|
6039
|
+
// Call before an itemId's complete event arrives: flush the deltas not yet sent
|
|
5781
6040
|
flushItem(itemId) {
|
|
5782
6041
|
this.flush(itemId);
|
|
5783
6042
|
}
|
|
@@ -5810,14 +6069,14 @@ var AgentManager = class {
|
|
|
5810
6069
|
runtimes = /* @__PURE__ */ new Map();
|
|
5811
6070
|
recycleTimer = null;
|
|
5812
6071
|
slashCache = /* @__PURE__ */ new Map();
|
|
5813
|
-
/**
|
|
6072
|
+
/** attachment.path in the frontend's thread.send is really an upload id; resolve it to the file under dataDir/attachments */
|
|
5814
6073
|
resolveAttachments(refs) {
|
|
5815
6074
|
return resolveAttachmentRefs(this.attachments, refs);
|
|
5816
6075
|
}
|
|
5817
6076
|
attachmentLabels(refs) {
|
|
5818
6077
|
return refs.map((a) => ({ id: a.path, name: (a.name ?? "").trim() || a.path, mime: a.mime }));
|
|
5819
6078
|
}
|
|
5820
|
-
//
|
|
6079
|
+
// Called at startup: running/initializing conversations are recovered as closed (§5.7.6)
|
|
5821
6080
|
async recoverOnStart() {
|
|
5822
6081
|
for (const meta of await this.threads.list()) {
|
|
5823
6082
|
if (meta.status === "running" || meta.status === "initializing") {
|
|
@@ -5854,13 +6113,16 @@ var AgentManager = class {
|
|
|
5854
6113
|
this.recycleTimer = setInterval(() => void this.recycleIdle(), 6e4);
|
|
5855
6114
|
this.recycleTimer.unref?.();
|
|
5856
6115
|
}
|
|
6116
|
+
/** Close every session; the returned Promise resolves once each close finishes (or fails). Callers add their own timeout. */
|
|
5857
6117
|
shutdown() {
|
|
5858
6118
|
if (this.recycleTimer) clearInterval(this.recycleTimer);
|
|
5859
|
-
|
|
5860
|
-
});
|
|
6119
|
+
const all = [...this.runtimes.values()].map((rt) => rt.session?.close().catch(() => {
|
|
6120
|
+
}));
|
|
5861
6121
|
this.runtimes.clear();
|
|
6122
|
+
return Promise.all(all).then(() => {
|
|
6123
|
+
});
|
|
5862
6124
|
}
|
|
5863
|
-
//
|
|
6125
|
+
// Idle-exit check (§5.2.7): busy if any runtime is not idle
|
|
5864
6126
|
anyBusy() {
|
|
5865
6127
|
for (const rt of this.runtimes.values()) {
|
|
5866
6128
|
if (rt.status === "running" || rt.status === "initializing") return true;
|
|
@@ -5913,7 +6175,7 @@ var AgentManager = class {
|
|
|
5913
6175
|
flushDeltas(rt, itemId) {
|
|
5914
6176
|
for (const t of DELTA_TYPES) rt.coalescer.flushItem(deltaKey(t, itemId));
|
|
5915
6177
|
}
|
|
5916
|
-
// ----
|
|
6178
|
+
// ---- send ----
|
|
5917
6179
|
async send(threadId, input, behavior = "queue") {
|
|
5918
6180
|
const meta = await this.mustThread(threadId);
|
|
5919
6181
|
if (meta.archived) {
|
|
@@ -6006,7 +6268,7 @@ var AgentManager = class {
|
|
|
6006
6268
|
throw e;
|
|
6007
6269
|
}
|
|
6008
6270
|
}
|
|
6009
|
-
// ----
|
|
6271
|
+
// ---- event pump ----
|
|
6010
6272
|
async pumpEvents(threadId, session) {
|
|
6011
6273
|
const rt = this.rt(threadId);
|
|
6012
6274
|
let ended = false;
|
|
@@ -6126,7 +6388,7 @@ var AgentManager = class {
|
|
|
6126
6388
|
alive(_s) {
|
|
6127
6389
|
return true;
|
|
6128
6390
|
}
|
|
6129
|
-
// ---- respond
|
|
6391
|
+
// ---- respond (permission/question answers) ----
|
|
6130
6392
|
async respond(threadId, requestId, answer) {
|
|
6131
6393
|
const rt = this.rt(threadId);
|
|
6132
6394
|
const pending = rt.pending.get(requestId);
|
|
@@ -6231,7 +6493,7 @@ var AgentManager = class {
|
|
|
6231
6493
|
}
|
|
6232
6494
|
this.gw.publish("sidebar", { type: "thread.upsert", thread: next });
|
|
6233
6495
|
}
|
|
6234
|
-
/**
|
|
6496
|
+
/** The Agent can be switched only before any user message and while not running */
|
|
6235
6497
|
async setProvider(threadId, providerId) {
|
|
6236
6498
|
await this.mustThread(threadId);
|
|
6237
6499
|
const meta = await this.threads.get(threadId);
|
|
@@ -6276,7 +6538,7 @@ var AgentManager = class {
|
|
|
6276
6538
|
}
|
|
6277
6539
|
await this.setStatus(threadId, "closed");
|
|
6278
6540
|
}
|
|
6279
|
-
/**
|
|
6541
|
+
/** Slash commands: no session by default; only with load:true do we create a session for providers that need one to list them */
|
|
6280
6542
|
async listCommands(threadId, opts) {
|
|
6281
6543
|
let meta;
|
|
6282
6544
|
try {
|
|
@@ -6363,7 +6625,7 @@ var AgentManager = class {
|
|
|
6363
6625
|
if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
6364
6626
|
let model = meta.model;
|
|
6365
6627
|
if (meta.provider === "dsh" && model) {
|
|
6366
|
-
const available = await provider
|
|
6628
|
+
const available = await cachedModels(provider).catch(() => []);
|
|
6367
6629
|
if (available.length && !available.some((m) => m.id === model)) {
|
|
6368
6630
|
model = available.find((m) => /magpie/i.test(m.id))?.id ?? available.find((m) => m.isDefault)?.id ?? null;
|
|
6369
6631
|
await this.threads.patch(threadId, { model });
|
|
@@ -6393,7 +6655,7 @@ var AgentManager = class {
|
|
|
6393
6655
|
this.runtimes.delete(threadId);
|
|
6394
6656
|
await this.threads.delete(threadId);
|
|
6395
6657
|
}
|
|
6396
|
-
// ----
|
|
6658
|
+
// ---- internals ----
|
|
6397
6659
|
async mustThread(threadId) {
|
|
6398
6660
|
const meta = await this.threads.get(threadId);
|
|
6399
6661
|
if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
|
|
@@ -6424,14 +6686,14 @@ var AgentManager = class {
|
|
|
6424
6686
|
this.gw.publish("sidebar", { type: "thread.upsert", thread: meta });
|
|
6425
6687
|
this.gw.publish(`thread:${threadId}`, { type: "status", status });
|
|
6426
6688
|
}
|
|
6427
|
-
//
|
|
6689
|
+
// Append an event: seq is assigned in the store's serialized section, persisted, then pushed to thread:<id>
|
|
6428
6690
|
async appendRaw(threadId, ev) {
|
|
6429
6691
|
const full = await this.threads.appendEvent(threadId, { ...ev, ts: Date.now() });
|
|
6430
6692
|
this.gw.publish(`thread:${threadId}`, { type: "event", event: full });
|
|
6431
6693
|
}
|
|
6432
|
-
//
|
|
6433
|
-
//
|
|
6434
|
-
//
|
|
6694
|
+
// Push without persisting (for coalescer deltas; usage is already accumulated above).
|
|
6695
|
+
// Unpersisted events take no seq: seq=-1 so the frontend dedupes only by persisted seq,
|
|
6696
|
+
// otherwise deltas would push the client's maxSeq up and the following persisted events would be dropped by mistake.
|
|
6435
6697
|
async pushEvent(threadId, ev) {
|
|
6436
6698
|
const full = { ...ev, seq: -1, ts: Date.now(), threadId };
|
|
6437
6699
|
this.gw.publish(`thread:${threadId}`, { type: "event", event: full });
|
|
@@ -6449,29 +6711,41 @@ var AgentManager = class {
|
|
|
6449
6711
|
};
|
|
6450
6712
|
|
|
6451
6713
|
// daemon/src/git/service.ts
|
|
6452
|
-
import { execFile as
|
|
6453
|
-
import { existsSync as
|
|
6454
|
-
import { mkdir as mkdir6, readFile as readFile14, rename as rename4, stat as stat7 } from "node:fs/promises";
|
|
6455
|
-
import { join as
|
|
6456
|
-
var
|
|
6714
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
6715
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
6716
|
+
import { cp, mkdir as mkdir6, readFile as readFile14, rename as rename4, rm, stat as stat7 } from "node:fs/promises";
|
|
6717
|
+
import { join as join14, normalize as normalize2, sep as sep2 } from "node:path";
|
|
6718
|
+
var TIMEOUT2 = 15e3;
|
|
6457
6719
|
var DIFF_LIMIT = 1024 * 1024;
|
|
6458
6720
|
function runGit(cwd, args) {
|
|
6459
6721
|
return new Promise((res) => {
|
|
6460
|
-
|
|
6722
|
+
execFile4("git", ["-c", "core.quotepath=false", ...args], { cwd, timeout: TIMEOUT2, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (e, stdout, stderr) => {
|
|
6461
6723
|
const code = e && typeof e.code === "number" ? e.code : e ? 1 : 0;
|
|
6462
|
-
|
|
6724
|
+
const missing = e?.code === "ENOENT";
|
|
6725
|
+
res({ code, out: String(stdout ?? ""), err: missing ? "\u627E\u4E0D\u5230 git\uFF08\u8BF7\u5148\u5B89\u88C5 git \u5E76\u52A0\u5165 PATH\uFF09" : String(stderr || (e?.message ?? "")) });
|
|
6463
6726
|
});
|
|
6464
6727
|
});
|
|
6465
6728
|
}
|
|
6466
6729
|
function insidePath(projectPath, p) {
|
|
6467
6730
|
const rel = normalize2(p).replace(/\\/g, "/");
|
|
6468
|
-
const abs = normalize2(
|
|
6731
|
+
const abs = normalize2(join14(projectPath, rel));
|
|
6469
6732
|
const proj = normalize2(projectPath);
|
|
6470
6733
|
if (abs !== proj && !abs.startsWith(proj + sep2)) return null;
|
|
6471
6734
|
return rel.replace(/\\/g, "/");
|
|
6472
6735
|
}
|
|
6736
|
+
async function moveAcrossDevices(from, to, ops = {}) {
|
|
6737
|
+
const ren = ops.rename ?? rename4;
|
|
6738
|
+
try {
|
|
6739
|
+
await ren(from, to);
|
|
6740
|
+
} catch (e) {
|
|
6741
|
+
const code = e?.code;
|
|
6742
|
+
if (code !== "EXDEV") throw e;
|
|
6743
|
+
await (ops.cp ?? cp)(from, to, { recursive: true, errorOnExist: true, force: false, preserveTimestamps: true });
|
|
6744
|
+
await (ops.rm ?? rm)(from, { recursive: true, force: true });
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6473
6747
|
var GitService = class {
|
|
6474
|
-
// projectPath
|
|
6748
|
+
// projectPath is resolved from projectId by the caller (the service doesn't know about the store)
|
|
6475
6749
|
async status(projectPath) {
|
|
6476
6750
|
const inside = await runGit(projectPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
6477
6751
|
if (inside.code !== 0 || inside.out.trim() !== "true") return { isRepo: false };
|
|
@@ -6485,7 +6759,7 @@ var GitService = class {
|
|
|
6485
6759
|
if (!rec5 || rec5.length < 4) continue;
|
|
6486
6760
|
const index = rec5[0];
|
|
6487
6761
|
const worktree = rec5[1];
|
|
6488
|
-
let
|
|
6762
|
+
let path3 = rec5.slice(3);
|
|
6489
6763
|
let kind = "modified";
|
|
6490
6764
|
if (index === "?" && worktree === "?") kind = "untracked";
|
|
6491
6765
|
else if (index === "R" || worktree === "R") {
|
|
@@ -6493,18 +6767,18 @@ var GitService = class {
|
|
|
6493
6767
|
i++;
|
|
6494
6768
|
} else if (index === "A" || worktree === "A") kind = "added";
|
|
6495
6769
|
else if (index === "D" || worktree === "D") kind = "deleted";
|
|
6496
|
-
files.push({ path, index, worktree, kind });
|
|
6770
|
+
files.push({ path: path3, index, worktree, kind });
|
|
6497
6771
|
}
|
|
6498
6772
|
return { isRepo: true, branch: br.out.trim() || void 0, files };
|
|
6499
6773
|
}
|
|
6500
|
-
//
|
|
6774
|
+
// Unified diff text; {tooLarge:true} when the diff is > 1MB
|
|
6501
6775
|
async diff(projectPath, p) {
|
|
6502
6776
|
const rel = insidePath(projectPath, p);
|
|
6503
6777
|
if (!rel) return { error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
|
|
6504
6778
|
const ls = await runGit(projectPath, ["ls-files", "--error-unmatch", "--", rel]);
|
|
6505
6779
|
if (ls.code !== 0) {
|
|
6506
|
-
const full =
|
|
6507
|
-
if (!
|
|
6780
|
+
const full = join14(projectPath, rel);
|
|
6781
|
+
if (!existsSync10(full)) return { error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
|
|
6508
6782
|
const s = await stat7(full);
|
|
6509
6783
|
if (!s.isFile()) return { error: "\u4E0D\u662F\u6587\u4EF6" };
|
|
6510
6784
|
if (s.size > DIFF_LIMIT) return { tooLarge: true };
|
|
@@ -6525,10 +6799,11 @@ ${lines}` };
|
|
|
6525
6799
|
if (d.out.length > DIFF_LIMIT) return { tooLarge: true };
|
|
6526
6800
|
return { diff: d.out };
|
|
6527
6801
|
}
|
|
6528
|
-
//
|
|
6802
|
+
// Tracked → git restore; untracked → move to trash/discarded/<ts>/<relpath>
|
|
6529
6803
|
async discard(projectPath, p, dataDir) {
|
|
6530
6804
|
const rel = insidePath(projectPath, p);
|
|
6531
6805
|
if (!rel) return { ok: false, error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
|
|
6806
|
+
if (rel === "." || rel === "" || rel === "./") return { ok: false, error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
|
|
6532
6807
|
const ls = await runGit(projectPath, ["ls-files", "--error-unmatch", "--", rel]);
|
|
6533
6808
|
if (ls.code === 0) {
|
|
6534
6809
|
let r = await runGit(projectPath, ["restore", "--staged", "--worktree", "--", rel]);
|
|
@@ -6536,13 +6811,13 @@ ${lines}` };
|
|
|
6536
6811
|
if (r.code !== 0) return { ok: false, error: r.err.trim() || "\u653E\u5F03\u5931\u8D25" };
|
|
6537
6812
|
return { ok: true };
|
|
6538
6813
|
}
|
|
6539
|
-
const full =
|
|
6540
|
-
if (!
|
|
6814
|
+
const full = join14(projectPath, rel);
|
|
6815
|
+
if (!existsSync10(full)) return { ok: false, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
|
|
6541
6816
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
6542
|
-
const dest =
|
|
6543
|
-
await mkdir6(
|
|
6817
|
+
const dest = join14(dataDir, "trash", "discarded", stamp2, rel);
|
|
6818
|
+
await mkdir6(join14(dest, ".."), { recursive: true });
|
|
6544
6819
|
try {
|
|
6545
|
-
await
|
|
6820
|
+
await moveAcrossDevices(full, dest);
|
|
6546
6821
|
} catch (e) {
|
|
6547
6822
|
return { ok: false, error: "\u79FB\u52A8\u5931\u8D25\uFF1A" + String(e?.message ?? e) };
|
|
6548
6823
|
}
|
|
@@ -6552,9 +6827,21 @@ ${lines}` };
|
|
|
6552
6827
|
|
|
6553
6828
|
// daemon/src/files/service.ts
|
|
6554
6829
|
import { readdir as readdir5, readFile as readFile15, stat as stat8 } from "node:fs/promises";
|
|
6555
|
-
import {
|
|
6556
|
-
import { join as join16, normalize as normalize3, sep as sep3, extname as extname3 } from "node:path";
|
|
6830
|
+
import { join as join15, normalize as normalize3, sep as sep3, extname as extname3 } from "node:path";
|
|
6557
6831
|
import ignore from "ignore";
|
|
6832
|
+
function decodeText(buf) {
|
|
6833
|
+
try {
|
|
6834
|
+
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(buf);
|
|
6835
|
+
return { text, encoding: "utf8" };
|
|
6836
|
+
} catch {
|
|
6837
|
+
}
|
|
6838
|
+
try {
|
|
6839
|
+
const text = new TextDecoder("gbk", { fatal: true }).decode(buf);
|
|
6840
|
+
return { text, encoding: "gbk" };
|
|
6841
|
+
} catch {
|
|
6842
|
+
}
|
|
6843
|
+
return { text: new TextDecoder("utf-8").decode(buf), encoding: "utf8" };
|
|
6844
|
+
}
|
|
6558
6845
|
var MAX_READ = 1024 * 1024;
|
|
6559
6846
|
var MAX_PER_LEVEL = 2e3;
|
|
6560
6847
|
var ALWAYS_IGNORE = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
@@ -6603,49 +6890,59 @@ var LANG = {
|
|
|
6603
6890
|
};
|
|
6604
6891
|
function insidePath2(projectPath, p) {
|
|
6605
6892
|
const rel = normalize3(p || ".").replace(/\\/g, "/");
|
|
6606
|
-
const abs = normalize3(
|
|
6893
|
+
const abs = normalize3(join15(projectPath, rel));
|
|
6607
6894
|
const proj = normalize3(projectPath);
|
|
6608
6895
|
if (abs !== proj && !abs.startsWith(proj + sep3)) return null;
|
|
6609
6896
|
return rel;
|
|
6610
6897
|
}
|
|
6611
6898
|
var FileService = class {
|
|
6612
|
-
//
|
|
6899
|
+
// Cache an ignore instance per project (.gitignore changes are picked up on the next daemon restart; acceptable cost)
|
|
6900
|
+
// Invalidate by .gitignore mtime: an edited .gitignore takes effect the next time a directory is expanded
|
|
6613
6901
|
igCache = /* @__PURE__ */ new Map();
|
|
6614
6902
|
async igFor(projectPath) {
|
|
6615
|
-
|
|
6616
|
-
const
|
|
6903
|
+
const gi = join15(projectPath, ".gitignore");
|
|
6904
|
+
const st = await stat8(gi).catch(() => null);
|
|
6905
|
+
const mtime = st?.isFile() ? st.mtimeMs : -1;
|
|
6906
|
+
const hit = this.igCache.get(projectPath);
|
|
6907
|
+
if (hit && hit.mtime === mtime) return hit.ig;
|
|
6617
6908
|
let ig = null;
|
|
6618
|
-
if (
|
|
6909
|
+
if (mtime >= 0) {
|
|
6619
6910
|
try {
|
|
6620
6911
|
ig = ignore().add(await readFile15(gi, "utf8"));
|
|
6621
6912
|
} catch {
|
|
6622
6913
|
ig = null;
|
|
6623
6914
|
}
|
|
6624
6915
|
}
|
|
6625
|
-
this.igCache.set(projectPath, ig);
|
|
6916
|
+
this.igCache.set(projectPath, { mtime, ig });
|
|
6626
6917
|
return ig;
|
|
6627
6918
|
}
|
|
6628
6919
|
async tree(projectPath, dir) {
|
|
6629
6920
|
const rel = insidePath2(projectPath, dir);
|
|
6630
6921
|
if (rel === null) return { error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
|
|
6631
|
-
const abs =
|
|
6922
|
+
const abs = join15(projectPath, rel);
|
|
6632
6923
|
const s = await stat8(abs).catch(() => null);
|
|
6633
6924
|
if (!s?.isDirectory()) return { error: "\u76EE\u5F55\u4E0D\u5B58\u5728" };
|
|
6634
6925
|
const ig = await this.igFor(projectPath);
|
|
6635
|
-
const
|
|
6636
|
-
if (!
|
|
6926
|
+
const dirents = await readdir5(abs, { withFileTypes: true }).catch(() => null);
|
|
6927
|
+
if (!dirents) return { error: "\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55" };
|
|
6637
6928
|
const out = [];
|
|
6638
|
-
for (const
|
|
6929
|
+
for (const d of dirents) {
|
|
6639
6930
|
if (out.length >= MAX_PER_LEVEL) break;
|
|
6931
|
+
const name = d.name;
|
|
6640
6932
|
if (ALWAYS_IGNORE.has(name)) continue;
|
|
6641
6933
|
const childRel = rel === "." ? name : `${rel}/${name}`;
|
|
6642
|
-
|
|
6643
|
-
if (
|
|
6644
|
-
|
|
6934
|
+
let isDir2 = d.isDirectory();
|
|
6935
|
+
if (d.isSymbolicLink()) {
|
|
6936
|
+
const st = await stat8(join15(projectPath, childRel)).catch(() => null);
|
|
6937
|
+
if (!st) continue;
|
|
6938
|
+
isDir2 = st.isDirectory();
|
|
6939
|
+
} else if (!d.isFile() && !isDir2) {
|
|
6940
|
+
continue;
|
|
6941
|
+
}
|
|
6645
6942
|
if (ig) {
|
|
6646
|
-
if (ig.ignores(
|
|
6943
|
+
if (ig.ignores(isDir2 ? childRel + "/" : childRel)) continue;
|
|
6647
6944
|
}
|
|
6648
|
-
out.push({ name, path: childRel, isDir });
|
|
6945
|
+
out.push({ name, path: childRel, isDir: isDir2 });
|
|
6649
6946
|
}
|
|
6650
6947
|
out.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name, "zh-CN") : a.isDir ? -1 : 1);
|
|
6651
6948
|
return { entries: out };
|
|
@@ -6653,7 +6950,7 @@ var FileService = class {
|
|
|
6653
6950
|
async read(projectPath, p) {
|
|
6654
6951
|
const rel = insidePath2(projectPath, p);
|
|
6655
6952
|
if (rel === null) return { error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
|
|
6656
|
-
const abs =
|
|
6953
|
+
const abs = join15(projectPath, rel);
|
|
6657
6954
|
const s = await stat8(abs).catch(() => null);
|
|
6658
6955
|
if (!s) return { error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
|
|
6659
6956
|
if (!s.isFile()) return { error: "\u4E0D\u662F\u6587\u4EF6" };
|
|
@@ -6662,28 +6959,76 @@ var FileService = class {
|
|
|
6662
6959
|
if (!buf) return { error: "\u65E0\u6CD5\u8BFB\u53D6" };
|
|
6663
6960
|
const head = buf.subarray(0, 8192);
|
|
6664
6961
|
if (head.includes(0)) return { binary: true };
|
|
6665
|
-
|
|
6962
|
+
const { text, encoding } = decodeText(buf);
|
|
6963
|
+
return { text, encoding, language: LANG[extname3(rel).toLowerCase()] ?? "plaintext" };
|
|
6666
6964
|
}
|
|
6667
6965
|
};
|
|
6668
6966
|
|
|
6669
6967
|
// daemon/src/terminal/service.ts
|
|
6968
|
+
init_pathscan();
|
|
6670
6969
|
init_log();
|
|
6671
|
-
import { spawn as
|
|
6970
|
+
import { spawn as spawn3, execFileSync as execFileSync2 } from "node:child_process";
|
|
6971
|
+
import { accessSync as accessSync2, chmodSync, constants as fsc, existsSync as existsSync11, statSync as statSync6 } from "node:fs";
|
|
6972
|
+
import { createRequire as moduleRequire } from "node:module";
|
|
6973
|
+
import { release } from "node:os";
|
|
6974
|
+
import { dirname as dirname6, join as join16 } from "node:path";
|
|
6975
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6672
6976
|
var MAX_TERMS_PER_PROJECT = 8;
|
|
6673
6977
|
var BUF_MAX = 2 * 1024 * 1024;
|
|
6674
6978
|
var SNAP_LINES = 5e3;
|
|
6675
|
-
function
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6979
|
+
function isExecFile(file) {
|
|
6980
|
+
try {
|
|
6981
|
+
accessSync2(file, fsc.X_OK);
|
|
6982
|
+
return statSync6(file).isFile();
|
|
6983
|
+
} catch {
|
|
6984
|
+
return false;
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
function pickShell(o = {}) {
|
|
6988
|
+
const platform = o.platform ?? process.platform;
|
|
6989
|
+
const env = o.env ?? process.env;
|
|
6990
|
+
const find = o.find ?? ((n) => findOnPath(n, { platform, env })[0] ?? null);
|
|
6991
|
+
const isExec = o.isExec ?? isExecFile;
|
|
6992
|
+
if (platform === "win32") {
|
|
6993
|
+
for (const s of ["pwsh", "powershell"]) {
|
|
6994
|
+
const hit = find(s);
|
|
6995
|
+
if (hit) return hit;
|
|
6996
|
+
}
|
|
6997
|
+
const sysRoot = env.SystemRoot || env.SYSTEMROOT || env.windir || "C:\\Windows";
|
|
6998
|
+
const ps = `${sysRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
6999
|
+
if (isExec(ps)) return ps;
|
|
7000
|
+
return env.ComSpec || env.COMSPEC || "cmd.exe";
|
|
7001
|
+
}
|
|
7002
|
+
if (env.SHELL && env.SHELL.startsWith("/") && isExec(env.SHELL)) return env.SHELL;
|
|
7003
|
+
const order = platform === "darwin" ? ["/bin/zsh", "/bin/bash", "/bin/sh"] : ["/bin/bash", "/usr/bin/bash", "/bin/zsh", "/bin/sh"];
|
|
7004
|
+
for (const s of order) if (isExec(s)) return s;
|
|
7005
|
+
return "/bin/sh";
|
|
7006
|
+
}
|
|
7007
|
+
function simpleShellArgs(shell, platform = process.platform) {
|
|
7008
|
+
if (platform === "win32") {
|
|
7009
|
+
if (/(^|[\\/])cmd(\.exe)?$/i.test(shell)) return ["/Q"];
|
|
7010
|
+
return ["-NoLogo"];
|
|
7011
|
+
}
|
|
7012
|
+
return ["-i"];
|
|
7013
|
+
}
|
|
7014
|
+
function fixSpawnHelper() {
|
|
7015
|
+
if (process.platform !== "darwin") return;
|
|
7016
|
+
try {
|
|
7017
|
+
const req = moduleRequire(import.meta.url);
|
|
7018
|
+
const root = dirname6(req.resolve("node-pty/package.json"));
|
|
7019
|
+
for (const sub of [`prebuilds/darwin-${process.arch}`, "build/Release"]) {
|
|
7020
|
+
const f = join16(root, sub, "spawn-helper");
|
|
7021
|
+
if (existsSync11(f) && !isExecFile(f)) {
|
|
7022
|
+
chmodSync(f, 493);
|
|
7023
|
+
log("info", "\u5DF2\u4FEE\u590D node-pty spawn-helper \u7684\u53EF\u6267\u884C\u4F4D", { file: f });
|
|
6682
7024
|
}
|
|
6683
7025
|
}
|
|
6684
|
-
|
|
7026
|
+
} catch {
|
|
6685
7027
|
}
|
|
6686
|
-
|
|
7028
|
+
}
|
|
7029
|
+
function conptyInfo() {
|
|
7030
|
+
const build = Number(release().split(".")[2]);
|
|
7031
|
+
return { backend: "conpty", buildNumber: Number.isFinite(build) ? build : 0 };
|
|
6687
7032
|
}
|
|
6688
7033
|
var nextIndex = 1;
|
|
6689
7034
|
var rid5 = () => "t_" + Math.random().toString(36).slice(2, 10);
|
|
@@ -6699,6 +7044,7 @@ var TerminalService = class {
|
|
|
6699
7044
|
async loadPty() {
|
|
6700
7045
|
if (this.ptyLib !== void 0) return this.ptyLib;
|
|
6701
7046
|
try {
|
|
7047
|
+
fixSpawnHelper();
|
|
6702
7048
|
this.ptyLib = await import("node-pty");
|
|
6703
7049
|
} catch (e) {
|
|
6704
7050
|
log("warn", "node-pty \u52A0\u8F7D\u5931\u8D25\uFF0C\u7EC8\u7AEF\u9000\u5316\u65E0 PTY \u65B9\u6848", { err: String(e) });
|
|
@@ -6749,11 +7095,19 @@ var TerminalService = class {
|
|
|
6749
7095
|
}
|
|
6750
7096
|
if (!term.pty) {
|
|
6751
7097
|
term.simple = true;
|
|
6752
|
-
const args =
|
|
6753
|
-
const proc =
|
|
7098
|
+
const args = simpleShellArgs(shell);
|
|
7099
|
+
const proc = spawn3(shell, args, {
|
|
7100
|
+
cwd,
|
|
7101
|
+
stdio: "pipe",
|
|
7102
|
+
windowsHide: true,
|
|
7103
|
+
env: process.env,
|
|
7104
|
+
detached: process.platform !== "win32"
|
|
7105
|
+
});
|
|
6754
7106
|
term.proc = proc;
|
|
6755
|
-
|
|
6756
|
-
|
|
7107
|
+
const outDec = new StringDecoder("utf8");
|
|
7108
|
+
const errDec = new StringDecoder("utf8");
|
|
7109
|
+
proc.stdout.on("data", (d) => this.onData(term, outDec.write(d)));
|
|
7110
|
+
proc.stderr.on("data", (d) => this.onData(term, errDec.write(d)));
|
|
6757
7111
|
proc.on("exit", () => this.onExit(term));
|
|
6758
7112
|
proc.on("error", () => this.onExit(term));
|
|
6759
7113
|
}
|
|
@@ -6766,7 +7120,8 @@ var TerminalService = class {
|
|
|
6766
7120
|
if (!t) return { error: "\u7EC8\u7AEF\u4E0D\u5B58\u5728" };
|
|
6767
7121
|
const lines = t.buf.split("\n");
|
|
6768
7122
|
const snapshot = lines.length > SNAP_LINES ? lines.slice(-SNAP_LINES).join("\n") : t.buf;
|
|
6769
|
-
|
|
7123
|
+
const windowsPty = process.platform === "win32" && t.pty ? conptyInfo() : void 0;
|
|
7124
|
+
return { index: t.index, snapshot, simple: t.simple, ...windowsPty ? { windowsPty } : {} };
|
|
6770
7125
|
}
|
|
6771
7126
|
indexOf(id) {
|
|
6772
7127
|
return this.terms.get(id)?.index ?? null;
|
|
@@ -6786,21 +7141,41 @@ var TerminalService = class {
|
|
|
6786
7141
|
killAll() {
|
|
6787
7142
|
for (const t of this.terms.values()) this.killTerm(t, true);
|
|
6788
7143
|
}
|
|
6789
|
-
// ----
|
|
6790
|
-
//
|
|
6791
|
-
killTree(pid, sync) {
|
|
6792
|
-
if (
|
|
7144
|
+
// ---- internals ----
|
|
7145
|
+
// Killing only the shell would orphan the npm run dev etc. it started → on Windows taskkill /T kills the whole tree
|
|
7146
|
+
killTree(pid, sync, group) {
|
|
7147
|
+
if (!pid) return;
|
|
7148
|
+
if (process.platform !== "win32") {
|
|
7149
|
+
if (group) {
|
|
7150
|
+
try {
|
|
7151
|
+
process.kill(-pid, "SIGTERM");
|
|
7152
|
+
} catch {
|
|
7153
|
+
}
|
|
7154
|
+
const t = setTimeout(() => {
|
|
7155
|
+
try {
|
|
7156
|
+
process.kill(-pid, "SIGKILL");
|
|
7157
|
+
} catch {
|
|
7158
|
+
}
|
|
7159
|
+
}, 1500);
|
|
7160
|
+
t.unref?.();
|
|
7161
|
+
}
|
|
7162
|
+
return;
|
|
7163
|
+
}
|
|
6793
7164
|
const args = ["/pid", String(pid), "/T", "/F"];
|
|
6794
7165
|
try {
|
|
6795
|
-
if (sync)
|
|
6796
|
-
else
|
|
7166
|
+
if (sync) execFileSync2("taskkill", args, { windowsHide: true, stdio: "ignore", timeout: 5e3 });
|
|
7167
|
+
else spawn3("taskkill", args, { windowsHide: true, stdio: "ignore" }).on("error", () => {
|
|
6797
7168
|
});
|
|
6798
7169
|
} catch {
|
|
6799
7170
|
}
|
|
6800
7171
|
}
|
|
6801
7172
|
killTerm(t, sync = false) {
|
|
6802
7173
|
t.alive = false;
|
|
6803
|
-
this.killTree(t.pty?.pid ?? t.proc?.pid, sync);
|
|
7174
|
+
this.killTree(t.pty?.pid ?? t.proc?.pid, sync, !t.pty && !!t.proc);
|
|
7175
|
+
if (t.timer) {
|
|
7176
|
+
clearTimeout(t.timer);
|
|
7177
|
+
t.timer = null;
|
|
7178
|
+
}
|
|
6804
7179
|
try {
|
|
6805
7180
|
t.pty?.kill();
|
|
6806
7181
|
} catch {
|
|
@@ -6813,6 +7188,16 @@ var TerminalService = class {
|
|
|
6813
7188
|
this.byIndex.delete(t.index);
|
|
6814
7189
|
}
|
|
6815
7190
|
onExit(t) {
|
|
7191
|
+
if (t.timer) {
|
|
7192
|
+
clearTimeout(t.timer);
|
|
7193
|
+
t.timer = null;
|
|
7194
|
+
if (t.pend) {
|
|
7195
|
+
this.gw.publishTerminal(t.index, Buffer.from(t.pend, "utf8"));
|
|
7196
|
+
t.pend = "";
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
if (t.exited) return;
|
|
7200
|
+
t.exited = true;
|
|
6816
7201
|
t.alive = false;
|
|
6817
7202
|
this.terms.delete(t.id);
|
|
6818
7203
|
this.byIndex.delete(t.index);
|
|
@@ -6832,7 +7217,7 @@ var TerminalService = class {
|
|
|
6832
7217
|
}, 16);
|
|
6833
7218
|
}
|
|
6834
7219
|
}
|
|
6835
|
-
//
|
|
7220
|
+
// Input frames: op 2=input, 3=resize
|
|
6836
7221
|
onInput(conn, index, op, payload) {
|
|
6837
7222
|
const t = this.byIndex.get(index);
|
|
6838
7223
|
if (!t || !t.alive) return;
|
|
@@ -6851,7 +7236,41 @@ var TerminalService = class {
|
|
|
6851
7236
|
}
|
|
6852
7237
|
};
|
|
6853
7238
|
|
|
7239
|
+
// daemon/src/util/proc.ts
|
|
7240
|
+
init_pathscan();
|
|
7241
|
+
import { delimiter } from "node:path";
|
|
7242
|
+
function pidAlive(pid, kill = (p, s) => process.kill(p, s)) {
|
|
7243
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
7244
|
+
try {
|
|
7245
|
+
kill(pid, 0);
|
|
7246
|
+
return true;
|
|
7247
|
+
} catch (e) {
|
|
7248
|
+
return e?.code === "EPERM";
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
function augmentPath(env, nodeDir, platform = process.platform, home) {
|
|
7252
|
+
const win = platform === "win32";
|
|
7253
|
+
const sep4 = win ? ";" : platform === process.platform ? delimiter : ":";
|
|
7254
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
7255
|
+
const cur = (env[key] ?? "").split(sep4).filter(Boolean);
|
|
7256
|
+
const norm = (d) => win ? d.replace(/[\\/]+$/, "").toLowerCase() : d.replace(/\/+$/, "");
|
|
7257
|
+
const seen = new Set(cur.map(norm));
|
|
7258
|
+
const out = [...cur];
|
|
7259
|
+
for (const d of [nodeDir, ...extraBinDirs({ platform, env, home })]) {
|
|
7260
|
+
if (!d || seen.has(norm(d))) continue;
|
|
7261
|
+
seen.add(norm(d));
|
|
7262
|
+
out.push(d);
|
|
7263
|
+
}
|
|
7264
|
+
return out.join(sep4);
|
|
7265
|
+
}
|
|
7266
|
+
|
|
6854
7267
|
// daemon/src/main.ts
|
|
7268
|
+
var nodeMajor = Number(/^v?(\d+)/.exec(process.versions.node ?? "")?.[1] ?? 0);
|
|
7269
|
+
if (nodeMajor < 20) {
|
|
7270
|
+
console.error(`[agent-console] \u9700\u8981 Node.js 20 \u6216\u66F4\u9AD8\u7248\u672C\uFF0C\u5F53\u524D ${process.version}\uFF08${process.execPath}\uFF09`);
|
|
7271
|
+
process.exit(3);
|
|
7272
|
+
}
|
|
7273
|
+
process.env.PATH = augmentPath(process.env, dirname7(process.execPath));
|
|
6855
7274
|
var cfg = parseConfig(process.argv.slice(2));
|
|
6856
7275
|
process.on("uncaughtException", (e) => {
|
|
6857
7276
|
log("error", "uncaughtException", { err: String(e?.stack ?? e) });
|
|
@@ -6917,6 +7336,9 @@ async function main() {
|
|
|
6917
7336
|
log("error", `\u7AEF\u53E3 ${cfg.port} \u65E0\u6CD5\u76D1\u542C`, { err: String(e) });
|
|
6918
7337
|
process.exit(2);
|
|
6919
7338
|
});
|
|
7339
|
+
const bound = server.address();
|
|
7340
|
+
if (bound && typeof bound === "object") cfg.port = bound.port;
|
|
7341
|
+
process.stdout.write(JSON.stringify({ agentConsoleReady: true, port: cfg.port, pid: process.pid }) + "\n");
|
|
6920
7342
|
void (async () => {
|
|
6921
7343
|
const { warmProviderCache: warmProviderCache2 } = await Promise.resolve().then(() => (init_warm(), warm_exports));
|
|
6922
7344
|
await warmProviderCache2(await settings.get());
|
|
@@ -6930,7 +7352,9 @@ async function main() {
|
|
|
6930
7352
|
buildStamp: cfg.buildStamp,
|
|
6931
7353
|
startedAt: Date.now()
|
|
6932
7354
|
});
|
|
7355
|
+
chmodSync2(daemonJsonPath, 384);
|
|
6933
7356
|
log("info", `daemon \u5DF2\u542F\u52A8 \u7AEF\u53E3 ${cfg.port} \u6570\u636E\u76EE\u5F55 ${cfg.dataDir} \u7248\u672C ${cfg.version} pid ${process.pid}`);
|
|
7357
|
+
if (!cfg.parentPid) console.log(`[agent-console] \u6253\u5F00 http://127.0.0.1:${cfg.port}/#t=${cfg.token}`);
|
|
6934
7358
|
let idleSince = Date.now();
|
|
6935
7359
|
setInterval(() => {
|
|
6936
7360
|
const busy = gateway.connectionCount > 0 || manager.anyBusy() || terminals.count > 0;
|
|
@@ -6946,17 +7370,32 @@ async function main() {
|
|
|
6946
7370
|
shuttingDown = true;
|
|
6947
7371
|
log("info", "daemon \u5173\u95ED\u4E2D");
|
|
6948
7372
|
terminals.killAll();
|
|
6949
|
-
manager.shutdown();
|
|
6950
7373
|
gateway.closeAll();
|
|
6951
7374
|
try {
|
|
6952
7375
|
rmSync2(daemonJsonPath, { force: true });
|
|
6953
7376
|
} catch {
|
|
6954
7377
|
}
|
|
6955
|
-
|
|
6956
|
-
|
|
7378
|
+
const sessionsClosed = Promise.race([manager.shutdown(), new Promise((r) => setTimeout(r, 2500))]);
|
|
7379
|
+
void sessionsClosed.then(() => server.close(() => process.exit(code)));
|
|
7380
|
+
setTimeout(() => process.exit(code), 4e3).unref();
|
|
6957
7381
|
}
|
|
6958
7382
|
process.on("SIGINT", () => shutdown(0));
|
|
6959
7383
|
process.on("SIGTERM", () => shutdown(0));
|
|
7384
|
+
if (cfg.parentPid) {
|
|
7385
|
+
const parentPid = cfg.parentPid;
|
|
7386
|
+
const parentWatch = setInterval(() => {
|
|
7387
|
+
if (!pidAlive(parentPid)) {
|
|
7388
|
+
clearInterval(parentWatch);
|
|
7389
|
+
log("info", `\u7236\u8FDB\u7A0B ${parentPid} \u5DF2\u9000\u51FA\uFF0Cdaemon \u8DDF\u968F\u9000\u51FA`);
|
|
7390
|
+
shutdown(0);
|
|
7391
|
+
}
|
|
7392
|
+
}, 2e3);
|
|
7393
|
+
parentWatch.unref();
|
|
7394
|
+
process.stdin.on("end", () => shutdown(0));
|
|
7395
|
+
process.stdin.on("close", () => shutdown(0));
|
|
7396
|
+
process.stdin.on("error", () => shutdown(0));
|
|
7397
|
+
process.stdin.resume();
|
|
7398
|
+
}
|
|
6960
7399
|
}
|
|
6961
7400
|
main().catch((e) => {
|
|
6962
7401
|
console.error("[agent-console] \u542F\u52A8\u5931\u8D25", e);
|