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.
@@ -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 { resolve as resolve2, sep } from "node:path";
75
- function normPath(p) {
76
- let r = resolve2(p);
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 (process.platform === "win32") {
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 resolve4 = this.pendingReq.get(requestId);
666
- if (resolve4) {
670
+ const resolve2 = this.pendingReq.get(requestId);
671
+ if (resolve2) {
667
672
  this.pendingReq.delete(requestId);
668
- resolve4(answer);
673
+ resolve2(answer);
669
674
  }
670
675
  }
671
676
  waitFor(requestId) {
672
- return new Promise((resolve4) => {
673
- this.pendingReq.set(requestId, resolve4);
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, resolve4] of this.pendingReq) {
679
- resolve4({ cancelled: true });
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 [, resolve4] of this.pendingReq) resolve4({ cancelled: true });
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 { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "node:path";
831
+ import path2 from "node:path";
726
832
  function which(name) {
727
- return new Promise((resolvePromise) => {
728
- const cmd = IS_WIN ? "where.exe" : "which";
729
- let out = "";
730
- let done = false;
731
- const finish = (v) => {
732
- if (!done) {
733
- done = true;
734
- resolvePromise(v);
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
- const p = spawn(cmd, [name], { windowsHide: true });
739
- const t = setTimeout(() => {
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 = join6(cmdDir, p);
903
+ p = P.join(cmdDir, p);
776
904
  }
777
- if (existsSync4(p)) return resolve3(p);
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
- async function findNode() {
782
- const hits = await which("node");
783
- for (const h of hits) if (/node\.exe$/i.test(h) || /node$/.test(h)) return h;
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
- async function resolveBinUncached(name) {
807
- const hits = await which(name);
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 exe = hits.find((h) => /\.exe$/i.test(h));
810
- if (exe && existsSync4(exe)) {
811
- return { ok: true, cmd: exe, prefixArgs: [], display: exe, via: "exe" };
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
- if (IS_WIN) {
814
- const shim = hits.find((h) => /\.(cmd|bat)$/i.test(h));
815
- if (shim && existsSync4(shim)) {
816
- const dir = dirname3(shim);
817
- if (name.toLowerCase() === "npx") {
818
- const node = join6(dir, "node.exe");
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 first = hits[0];
837
- if (existsSync4(first)) return { ok: true, cmd: first, prefixArgs: [], display: first, via: "path" };
838
- return { ok: false, reason: `\u627E\u4E0D\u5230\u53EF\u6267\u884C\u7684 ${name}` };
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 补当前 turnId
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
- // 首次 send 前才真正起 query(检测失败要尽快报错)
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
- if (this.o.binCmd) {
1312
- if (this.o.binPrefixArgs.length) {
1313
- options.pathToClaudeCodeExecutable = this.o.binPrefixArgs[this.o.binPrefixArgs.length - 1];
1314
- options.executable = "node";
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
- // ---- 授权(§7.2.4)----
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((resolve4) => {
1437
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve4(this.toPermissionResult(a, input)) });
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
- resolve4({ behavior: "deny", message: "\u5DF2\u53D6\u6D88", interrupt: true });
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((resolve4) => {
1469
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve4(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
- resolve4({ cancelled: true });
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((resolve4) => {
1492
- this.pending.set(requestId, { kind: "question", resolve: (a) => resolve4(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
- resolve4({ cancelled: true });
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 join7 } from "node:path";
1612
- import { homedir as homedir2 } from "node:os";
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(path) {
1633
- const text = await readFile5(path, "utf8").catch(() => "");
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 = join7(CLAUDE_ROOT, d);
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 path = join7(dirPath, f);
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(path)) {
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(path).catch(() => null);
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 path = join7(CLAUDE_ROOT, d, `${sessionId}.jsonl`);
1677
- if (!existsSync6(path)) continue;
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(path)) {
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 = join7(homedir2(), ".claude", "projects");
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
- import { spawn as spawn2 } from "node:child_process";
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 = spawn2(cmd, args, { windowsHide: true, env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" } });
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.kill();
1863
+ void killTree(p, { graceMs: 500 });
1717
1864
  } catch {
1718
1865
  }
1719
1866
  reject(new Error("timeout"));
1720
1867
  }, timeoutMs);
1721
- p.stdout.on("data", (c) => out += c.toString());
1722
- p.stderr.on("data", (c) => out += c.toString());
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.cmd, [...bin.prefixArgs, "--version"]);
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:~/.claude/projects 下按 cwd 匹配的 jsonl 会话
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 不支持真正插话 — manager 会排队(§7.2.2)
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 join8 } from "node:path";
1815
- import { homedir as homedir3 } from "node:os";
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 = join8(dir, name);
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 path of walk(CODEX_ROOT)) {
1833
- const m = /rollout-.*-([0-9a-f-]{36})\.jsonl$/i.exec(path);
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(path, "utf8").catch(() => "");
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(path).catch(() => null);
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 = join8(homedir3(), ".codex", "sessions");
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 spawn3 } from "node:child_process";
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(cmd, args, opts = {}) {
1990
- this.proc = spawn3(cmd, args, {
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
- try {
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 homedir4 } from "node:os";
2349
- import { basename, join as join9 } from "node:path";
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 path = join9(dir, f);
2529
+ const path3 = join8(dir, f);
2380
2530
  try {
2381
- const st = await stat5(path);
2531
+ const st = await stat5(path3);
2382
2532
  if (!st.isFile()) continue;
2383
- const raw = await readFile7(path, "utf8");
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 = join9(root, d, "SKILL.md");
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 = homedir4();
2577
+ const home = homedir5();
2428
2578
  const parts = await Promise.all([
2429
- opts?.skipHome ? Promise.resolve([]) : readPromptFiles(join9(home, ".codex", "prompts")),
2430
- readPromptFiles(join9(cwd, ".codex", "prompts")),
2431
- opts?.skipHome ? Promise.resolve([]) : readSkillDirs(join9(home, ".codex", "skills")),
2432
- readSkillDirs(join9(cwd, ".codex", "skills"))
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
- // 当前轮在 timeline 里的 turnId(send 时生成)
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
- this.o.binCmd,
2510
- [...this.o.binPrefixArgs, "app-server"],
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
- // ---- 服务端请求(§7.3.5 审批)----
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((resolve4) => {
2717
- this.pending.set(requestId, { kind: "permission", resolve: resolve4 });
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
- import { spawn as spawn4 } from "node:child_process";
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 = spawn4(cmd, args, { windowsHide: true });
2913
+ const p = spawnBin(r, args, { stdio: ["ignore", "pipe", "pipe"] });
2762
2914
  const t = setTimeout(() => {
2763
2915
  try {
2764
- p.kill();
2916
+ void killTree(p, { graceMs: 500 });
2765
2917
  } catch {
2766
2918
  }
2767
2919
  reject(new Error("timeout"));
2768
2920
  }, timeoutMs);
2769
- p.stdout.on("data", (c) => out += c.toString());
2770
- p.stderr.on("data", (c) => out += c.toString());
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(cmd, prefixArgs, cwd) {
2788
- const rpc = new CodexRpc(cmd, [...prefixArgs, "app-server"], { cwd });
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.cmd, [...bin.prefixArgs, "--version"]);
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.cmd, bin.prefixArgs, process.cwd());
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:~/.codex/sessions/**/rollout-*.jsonl 按 cwd 匹配
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 有,cost 无
3040
+ // tokenUsage yes, cost no
2887
3041
  effort: [],
2888
- // effort 值随模型而定 — detect 后由 model/list 填充
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 spawn5(r.cmd, [...r.prefixArgs, ...args], {
3078
+ return spawnBin(r, args, {
2926
3079
  cwd: o.cwd,
2927
3080
  stdio: ["pipe", "pipe", "pipe"],
2928
- windowsHide: true,
2929
- env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" }
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
- // 第一批命令到达(或确认没有)前 listCommands 等它,参考 Paseo 的 waitForInitialCommands
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:声明支持参数化模型选择器(同 Paseo),它才会把档位 / Fast 作为单独配置项交出来
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
- /** 当前生效的配置项:每次 set_config_option 的返回会带回最新一份(换模型后档位选项会变),没有就用 session/new 的 */
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 报的模式选项:优先 configOptions 里 category=mode 的 select,否则 ACP modes.availableModes */
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
- * 换模型 → 用返回的新配置项刷新档位选项 → 再下发档位(同 Paseo)。
3337
- * Cursor 以 parameterizedModelPicker 方式连接:模型只发基名(grok-4.6),档位 / Fast 是单独的配置项。
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 专用:Fast / Auto-review(会话已开时也可改) */
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((resolve4) => {
3381
- this.permPending.set(requestId, resolve4);
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
- // 发出这一轮的 prompt;读更新由常驻的 pumpUpdates() 负责(参考 Paseo:会话级统一处理 session/update)
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
- * 会话整个生命周期内持续读 session/update。
3434
- * 原来只在一轮对话里读:Agent 在 session/new 之后、第一条消息之前推的 available_commands_update 就没人收,
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 = spawn6(r.cmd, [...r.prefixArgs, ...args], { windowsHide: true, env: { ...process.env, NO_COLOR: "1" } });
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.kill();
3683
+ void killTree(p, { graceMs: 500 });
3532
3684
  } catch {
3533
3685
  }
3534
3686
  reject(new Error("timeout"));
3535
3687
  }, timeoutMs);
3536
- p.stdout.on("data", (c) => out += c.toString());
3537
- p.stderr.on("data", (c) => out += c.toString());
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
- // 每个模型自己的思考档位来自 cursor/list_available_models(同 Paseo)
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 homedir5 } from "node:os";
3938
- import { join as join10 } from "node:path";
3939
- async function readJson(path) {
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(path, "utf8"));
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(join10(homedir5(), ".config", "magpie", "applied.json")) ?? {};
4155
+ return await readJson(join9(homedir6(), ".config", "magpie", "applied.json")) ?? {};
3948
4156
  }
3949
4157
  async function readClaudeEnv() {
3950
- const s = await readJson(join10(homedir5(), ".claude", "settings.json"));
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 existsSync7, statSync as statSync2 } from "node:fs";
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 homedir6 } from "node:os";
4122
- import { dirname as dirname4, join as join11 } from "node:path";
4123
- import { execFile as execFile2 } from "node:child_process";
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
- execFile2(cmd, [proj.path], { windowsHide: true }, () => {
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 isDir = false;
4456
+ let isDir2 = false;
4246
4457
  try {
4247
- exists = existsSync7(p.path);
4248
- isDir = exists && statSync2(p.path).isDirectory();
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) => pickFolderWindows(p?.initial)
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 (!existsSync7(proj.path)) throw new ApiError("bad_path", "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728");
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 (!existsSync7(proj.path)) throw new ApiError("bad_path", "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728");
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 path = await projectPath(p.projectId);
4420
- const r = await svc.terminals.create(p.projectId, path, p.cols, p.rows);
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 = join11(homedir6(), "Downloads");
4768
+ const dir = join10(homedir7(), "Downloads");
4558
4769
  await mkdir2(dir, { recursive: true });
4559
- let dest = join11(dir, filename);
4560
- if (existsSync7(dest)) {
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 = join11(dir, `${safe}-${stamp2}.md`);
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
- execFile2("explorer.exe", ["/select," + filePath], { windowsHide: true }, () => {
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
- execFile2("open", ["-R", filePath], () => {
4788
+ execFile3("open", ["-R", filePath], () => {
4789
+ }).on("error", () => {
4577
4790
  });
4578
4791
  return;
4579
4792
  }
4580
- execFile2("xdg-open", [dirname4(filePath)], () => {
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
- init_pickFolderWin();
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 dirname5 } from "node:path";
4819
+ import { dirname as dirname4 } from "node:path";
4606
4820
  async function writeJsonAtomic(file, obj) {
4607
4821
  try {
4608
- await mkdir3(dirname5(file), { recursive: true });
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 dirname6 } from "node:path";
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(dirname6(file), { recursive: true });
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 existsSync11 } from "node:fs";
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 (!existsSync11(oldDir) && !existsSync11(oldSettingsPath)) {
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 (existsSync11(oldDir)) await cp(oldDir, join17(backup, "sessions"), { recursive: true }).catch(() => {
4908
+ if (existsSync12(oldDir)) await cp2(oldDir, join17(backup, "sessions"), { recursive: true }).catch(() => {
4695
4909
  });
4696
- if (existsSync11(oldSettingsPath)) await cp(oldSettingsPath, join17(backup, "settings.json")).catch(() => {
4910
+ if (existsSync12(oldSettingsPath)) await cp2(oldSettingsPath, join17(backup, "settings.json")).catch(() => {
4697
4911
  });
4698
- const oldSettings = existsSync11(oldSettingsPath) ? JSON.parse(await readFile16(oldSettingsPath, "utf8").catch(() => "{}")) : {};
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) && existsSync11(join17(oldDir, d, "meta.json")));
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 (!existsSync11(d)) continue;
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 (existsSync11(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
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(vitest/tsc)
5172
+ // src/util → daemon/dist/main.js (vitest/tsc)
4959
5173
  join(here, "main.js"),
4960
- // 已在 dist 目录
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 < 1 || port > 65535) {
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 >= 1 && envPort <= 65535 ? envPort : 3488;
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 checkOrigin(origin, port) {
5036
- if (origin === void 0 || origin === "null" || origin === "") return { ok: true };
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 { ok: true };
5039
- return { ok: false, code: 403, message: "\u6765\u6E90\u4E0D\u5141\u8BB8" };
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
- function preflightHeaders(origin) {
5073
- return {
5074
- "access-control-allow-origin": origin && origin !== "null" ? origin : "null",
5075
- "access-control-allow-headers": "x-agent-console, content-type",
5076
- "access-control-allow-methods": "GET, POST, OPTIONS",
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 originCheck = checkOrigin(req.headers.origin, cfg2.port);
5097
- if (!originCheck.ok) {
5098
- sendJson(res, originCheck.code, { ok: false, e: { code: "bad_origin", message: originCheck.message } });
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
- if (req.headers.origin) {
5104
- res.setHeader("access-control-allow-origin", preflightHeaders(req.headers.origin)["access-control-allow-origin"]);
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
- send(res, 204, "", preflightHeaders(req.headers.origin));
5109
- return;
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
- await routeApi(req, res, pathname, url);
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 (pathname.startsWith("/attachments/")) {
5121
- const tok = req.headers["x-agent-console"];
5122
- const tokQ = url.searchParams.get("t");
5123
- const okTok = typeof tok === "string" && tokenEqual(tok, cfg2.token) || tokQ && tokenEqual(tokQ, cfg2.token);
5124
- if (!okTok) {
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
- const id = decodeURIComponent(pathname.slice("/attachments/".length));
5129
- const a = await attachments.get(id);
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, "cache-control": "no-store" });
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
- let html = await readFile(htmlPath, "utf8");
5146
- html = html.replace("__TOKEN__", cfg2.token);
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 rel = normalize(decodeURIComponent(pathname)).replace(/^([\\/])+/, "");
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", "cache-control": "no-store" });
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
- const mime = MIME2[ext] ?? "text/plain";
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
- const p = join5(this.dir, f);
5245
- const s = await stat2(p);
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 join12 } from "node:path";
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 = join12(dataDir, "settings.json");
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 existsSync8, statSync as statSync3 } from "node:fs";
5317
- import { join as join13 } from "node:path";
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 = join13(dataDir, "projects.json");
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(path, name) {
5350
- if (!path || typeof path !== "string") throw new ApiError("bad_params", "\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
5351
- if (!existsSync8(path) || !statSync3(path).isDirectory()) {
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(path);
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() || path.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path,
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 join14 } from "node:path";
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 写串行化:并发 patch/appendEvent 互相覆盖(manager 里 setStatus/patch/provider_session_id 会并发跑)
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 = join14(dataDir, "threads");
5412
- this.trashDir = join14(dataDir, "trash", "threads");
5671
+ this.dir = join13(dataDir, "threads");
5672
+ this.trashDir = join13(dataDir, "trash", "threads");
5413
5673
  }
5414
5674
  tdir(id) {
5415
- return join14(this.dir, id);
5675
+ return join13(this.dir, id);
5416
5676
  }
5417
5677
  metaPath(id) {
5418
- return join14(this.tdir(id), "meta.json");
5678
+ return join13(this.tdir(id), "meta.json");
5419
5679
  }
5420
5680
  timelinePath(id) {
5421
- return join14(this.tdir(id), "timeline.jsonl");
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
- // 事件追加:在串行区内取 meta.seq+1 作为 seq,落盘 jsonl + 更新 meta。
5494
- // 传入的 seq 被忽略(保证单调递增);ts/threadId 缺省时自动补。
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), join14(this.trashDir, `${id}-${stamp2}`));
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
- // 看 socket 里真正还没发出去的字节(客户端卡住才会涨),而不是累计发送量
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
- const originCheck = checkOrigin(req.headers.origin, this.cfg.port);
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
- // 终端二进制帧:[op:1][terminalIndex:2 BE][payload]
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
- // 推入一个 delta;首个立即发,窗口内合并后 trailing 发
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
- // 完整事件到达前必须 flush,保证顺序(delta 在完整事件之前)
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 的完整事件到达前调用:flush 未发完的 delta
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
- /** 前端 thread.send 的 attachment.path 实际是上传 id,需解析到 dataDir/attachments 下的文件 */
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
- // 启动时调用:恢复 running/initializing 的对话为 closed(§5.7.6)
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
- for (const rt of this.runtimes.values()) void rt.session?.close().catch(() => {
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
- // 空闲退出判定(§5.2.7):任一 runtime 非 idle 即忙
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
- /** 仅尚无用户消息、且未在跑时可换 Agent */
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
- /** 斜杠命令:默认不建会话;load:true 时才为需会话的 Provider 建会话拉列表 */
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.listModels().catch(() => []);
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
- // 追加事件:seq 由 store 串行区分配,落盘,推送 thread:<id>
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
- // 推送但不落盘(coalescer delta 用;usage 已经由上面累加)。
6433
- // 不落盘的事件不占 seq 序号:seq=-1 让前端只按持久化 seq 去重,
6434
- // 否则 delta 把客户端的 maxSeq 顶上去,会把随后的持久化事件误丢。
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 execFile3 } from "node:child_process";
6453
- import { existsSync as existsSync9 } from "node:fs";
6454
- import { mkdir as mkdir6, readFile as readFile14, rename as rename4, stat as stat7 } from "node:fs/promises";
6455
- import { join as join15, normalize as normalize2, sep as sep2 } from "node:path";
6456
- var TIMEOUT = 15e3;
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
- execFile3("git", args, { cwd, timeout: TIMEOUT, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (e, stdout, stderr) => {
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
- res({ code, out: String(stdout ?? ""), err: String(stderr ?? (e?.message ?? "")) });
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(join15(projectPath, rel));
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 由调用方从 projectId 解析(service 不认识 store)
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 path = rec5.slice(3);
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
- // 统一 diff 文本;{tooLarge:true} 当 diff > 1MB
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 = join15(projectPath, rel);
6507
- if (!existsSync9(full)) return { error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
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
- // 已跟踪 → git restore;未跟踪 → 移到 trash/discarded/<ts>/<relpath>
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 = join15(projectPath, rel);
6540
- if (!existsSync9(full)) return { ok: false, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
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 = join15(dataDir, "trash", "discarded", stamp2, rel);
6543
- await mkdir6(join15(dest, ".."), { recursive: true });
6817
+ const dest = join14(dataDir, "trash", "discarded", stamp2, rel);
6818
+ await mkdir6(join14(dest, ".."), { recursive: true });
6544
6819
  try {
6545
- await rename4(full, dest);
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 { existsSync as existsSync10 } from "node:fs";
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(join16(projectPath, rel));
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
- // 缓存每个项目的 ignore 实例(.gitignore 变更由下一次 daemon 重启拾取;代价可接受)
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
- if (this.igCache.has(projectPath)) return this.igCache.get(projectPath);
6616
- const gi = join16(projectPath, ".gitignore");
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 (existsSync10(gi)) {
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 = join16(projectPath, rel);
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 names = await readdir5(abs).catch(() => null);
6636
- if (!names) return { error: "\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55" };
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 name of names) {
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
- const st = await stat8(join16(projectPath, childRel)).catch(() => null);
6643
- if (!st) continue;
6644
- const isDir = st.isDirectory();
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(isDir ? childRel + "/" : childRel)) continue;
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 = join16(projectPath, rel);
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
- return { text: buf.toString("utf8"), language: LANG[extname3(rel).toLowerCase()] ?? "plaintext" };
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 spawn7, execFileSync } from "node:child_process";
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 pickShell() {
6676
- if (process.platform === "win32") {
6677
- for (const s of ["pwsh.exe", "powershell.exe"]) {
6678
- try {
6679
- execFileSync("where.exe", [s], { stdio: "ignore", windowsHide: true });
6680
- return s;
6681
- } catch {
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
- return "powershell.exe";
7026
+ } catch {
6685
7027
  }
6686
- return process.env.SHELL || "/bin/bash";
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 = process.platform === "win32" ? ["-NoLogo"] : ["-i"];
6753
- const proc = spawn7(shell, args, { cwd, stdio: "pipe", windowsHide: true, env: process.env });
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
- proc.stdout.on("data", (d) => this.onData(term, d.toString("utf8")));
6756
- proc.stderr.on("data", (d) => this.onData(term, d.toString("utf8")));
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
- return { index: t.index, snapshot, simple: t.simple };
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
- // 只杀 shell 会把它启动的 npm run dev 之类留成孤儿 → Windows 用 taskkill /T 杀整棵树
6791
- killTree(pid, sync) {
6792
- if (process.platform !== "win32" || !pid) return;
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) execFileSync("taskkill", args, { windowsHide: true, stdio: "ignore", timeout: 5e3 });
6796
- else spawn7("taskkill", args, { windowsHide: true, stdio: "ignore" }).on("error", () => {
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
- // 输入帧:op 2=输入,3=尺寸
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
- server.close(() => process.exit(code));
6956
- setTimeout(() => process.exit(code), 3e3).unref();
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);