deskbuddy 0.2.0-beta.13 → 0.2.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cli.js +2241 -359
  2. package/dist/gate.cjs +21 -13
  3. package/dist/hook.cjs +287 -9
  4. package/dist/ocTranslate.mjs +106 -0
  5. package/dist/opencode.mjs +100 -69
  6. package/hook/gate.cjs +21 -13
  7. package/hook/hook-debug.log +5 -0
  8. package/hook/hook.cjs +287 -9
  9. package/hook/ocTranslate.d.mts +7 -0
  10. package/hook/ocTranslate.mjs +106 -0
  11. package/hook/opencode.d.mts +4 -1
  12. package/hook/opencode.mjs +100 -69
  13. package/package.json +3 -2
  14. package/vendor/node-pty/LICENSE +69 -0
  15. package/vendor/node-pty/lib/conpty_console_list_agent.js +16 -0
  16. package/vendor/node-pty/lib/eventEmitter2.js +47 -0
  17. package/vendor/node-pty/lib/index.js +52 -0
  18. package/vendor/node-pty/lib/interfaces.js +7 -0
  19. package/vendor/node-pty/lib/shared/conout.js +11 -0
  20. package/vendor/node-pty/lib/terminal.js +190 -0
  21. package/vendor/node-pty/lib/types.js +7 -0
  22. package/vendor/node-pty/lib/unixTerminal.js +346 -0
  23. package/vendor/node-pty/lib/utils.js +39 -0
  24. package/vendor/node-pty/lib/windowsConoutConnection.js +125 -0
  25. package/vendor/node-pty/lib/windowsPtyAgent.js +320 -0
  26. package/vendor/node-pty/lib/windowsTerminal.js +199 -0
  27. package/vendor/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  28. package/vendor/node-pty/package.json +7 -0
  29. package/vendor/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  30. package/vendor/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  31. package/vendor/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  32. package/vendor/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  33. package/vendor/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  34. package/vendor/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  35. package/vendor/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  36. package/vendor/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  37. package/vendor/node-pty/prebuilds/win32-arm64/pty.node +0 -0
  38. package/vendor/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
  39. package/vendor/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
  40. package/vendor/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  41. package/vendor/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  42. package/vendor/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  43. package/vendor/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  44. package/vendor/node-pty/prebuilds/win32-x64/pty.node +0 -0
  45. package/vendor/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
  46. package/vendor/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
package/dist/cli.js CHANGED
@@ -61,6 +61,15 @@ function workbuddySettingsPath() {
61
61
  function qoderCnSettingsPath() {
62
62
  return join(process.env.DESKBUDDY_QODER_CN_DIR || join(homedir(), ".qoder-cn"), "settings.json");
63
63
  }
64
+ function qwenworkHome() {
65
+ return process.env.DESKBUDDY_QWENWORK_DIR || join(homedir(), ".QwenWorkCN");
66
+ }
67
+ function qwenworkSettingsPath() {
68
+ return join(qwenworkHome(), "settings.json");
69
+ }
70
+ function qianfanHome() {
71
+ return process.env.DESKBUDDY_QIANFAN_DIR || join(homedir(), ".qianfan");
72
+ }
64
73
  function qoderSettingsPath() {
65
74
  return join(process.env.DESKBUDDY_QODER_DIR || join(homedir(), ".qoder"), "settings.json");
66
75
  }
@@ -169,7 +178,7 @@ function atomicCopy(from, to) {
169
178
  function syncHookFile(opts = {}) {
170
179
  const target = opts.target ?? installedHookPath();
171
180
  const version = opts.version ?? packageVersion();
172
- const SIBLINGS = ["gate.cjs", "opencode.mjs", "wake.cjs", "launcher.cjs"];
181
+ const SIBLINGS = ["gate.cjs", "ocTranslate.mjs", "opencode.mjs", "wake.cjs", "launcher.cjs"];
173
182
  const siblingMissing = () => SIBLINGS.some((name) => {
174
183
  if (existsSync2(join2(dirname(target), name))) return false;
175
184
  try {
@@ -1007,25 +1016,57 @@ import { dirname as dirname5, join as join6, posix, win32 } from "path";
1007
1016
  import { chmodSync, existsSync as existsSync5, statSync } from "fs";
1008
1017
  import { createRequire as createRequire2 } from "module";
1009
1018
  import { dirname as dirname4, join as join5 } from "path";
1019
+ import { fileURLToPath as fileURLToPath3 } from "url";
1010
1020
  var req = createRequire2(import.meta.url);
1011
- function fixSpawnHelper() {
1021
+ var VENDORED_PTY_PLATFORMS = ["darwin-arm64", "darwin-x64", "win32-arm64", "win32-x64"];
1022
+ function hasVendoredPty(platform = process.platform, arch = process.arch) {
1023
+ return VENDORED_PTY_PLATFORMS.includes(`${platform}-${arch}`);
1024
+ }
1025
+ function vendoredPtyDir(from = dirname4(fileURLToPath3(import.meta.url))) {
1026
+ let dir = from;
1027
+ for (let i = 0; i < 4; i++) {
1028
+ const cand = join5(dir, "vendor", "node-pty");
1029
+ if (existsSync5(join5(cand, "package.json"))) return cand;
1030
+ const up = dirname4(dir);
1031
+ if (up === dir) break;
1032
+ dir = up;
1033
+ }
1034
+ return null;
1035
+ }
1036
+ function fixSpawnHelper(ptyDir) {
1012
1037
  if (process.platform !== "darwin") return;
1013
1038
  try {
1014
- const dir = join5(dirname4(req.resolve("node-pty/package.json")), "prebuilds", `darwin-${process.arch}`);
1015
- const helper = join5(dir, "spawn-helper");
1039
+ const helper = join5(ptyDir, "prebuilds", `darwin-${process.arch}`, "spawn-helper");
1016
1040
  if (existsSync5(helper) && (statSync(helper).mode & 73) === 0) chmodSync(helper, 493);
1017
1041
  } catch {
1018
1042
  }
1019
1043
  }
1044
+ function asPty(m) {
1045
+ return typeof m?.spawn === "function" ? m : null;
1046
+ }
1020
1047
  function loadPty() {
1048
+ if (hasVendoredPty()) {
1049
+ const dir = vendoredPtyDir();
1050
+ if (dir) {
1051
+ try {
1052
+ fixSpawnHelper(dir);
1053
+ const m = asPty(req(dir));
1054
+ if (m) return m;
1055
+ } catch {
1056
+ }
1057
+ }
1058
+ }
1021
1059
  try {
1022
- fixSpawnHelper();
1023
- const m = req("node-pty");
1024
- return typeof m?.spawn === "function" ? m : null;
1060
+ const dir = dirname4(req.resolve("node-pty/package.json"));
1061
+ fixSpawnHelper(dir);
1062
+ return asPty(req(dir));
1025
1063
  } catch {
1026
1064
  return null;
1027
1065
  }
1028
1066
  }
1067
+ function ptyMissingReason(platform = process.platform, arch = process.arch) {
1068
+ return hasVendoredPty(platform, arch) ? "\u5185\u7F6E\u7684 node-pty \u52A0\u8F7D\u5931\u8D25" : `${platform === "linux" ? "Linux" : `${platform}-${arch}`} \u4E0B\u6CA1\u6709\u5185\u7F6E node-pty \u9884\u7F16\u8BD1\uFF0C\u7A97\u53E3\u63A5\u7BA1\u4E0D\u53EF\u7528`;
1069
+ }
1029
1070
  function loadHeadless() {
1030
1071
  try {
1031
1072
  const m = req("@xterm/headless");
@@ -1123,17 +1164,32 @@ function npmInstallArgs(npmCli, dir, target, registry) {
1123
1164
  if (!REGISTRIES.includes(registry)) throw new Error(`\u4E0D\u5141\u8BB8\u7684\u6E90\uFF1A${registry}`);
1124
1165
  return [npmCli, "install", "--prefix", dir, "--no-audit", "--no-fund", "--omit=dev", `${PACKAGE_NAME}@${target}`, "--registry", registry];
1125
1166
  }
1126
- var defaultExec = (file, args, opts) => new Promise((resolve6, reject) => {
1167
+ function installEnv(execPath, base = process.env, platform = process.platform) {
1168
+ const env = { ...base };
1169
+ const win = platform === "win32";
1170
+ const key = (win ? Object.keys(env).find((k) => k.toUpperCase() === "PATH") : void 0) ?? "PATH";
1171
+ const nodeDir = (win ? win32 : posix).dirname(execPath);
1172
+ const sep3 = win ? ";" : ":";
1173
+ const rest = (env[key] ?? "").split(sep3).filter((p) => p && p !== nodeDir);
1174
+ env[key] = [nodeDir, ...rest].join(sep3);
1175
+ return { ...env, npm_config_update_notifier: "false", npm_config_fund: "false" };
1176
+ }
1177
+ var defaultExec = (file, args, opts) => new Promise((resolve7, reject) => {
1127
1178
  execFile2(file, args, { ...opts, maxBuffer: 16 * 1024 * 1024 }, (err, _stdout, stderr) => {
1128
1179
  if (err) {
1129
1180
  const tail = String(stderr || "").trim().split("\n").slice(-3).join(" | ");
1130
1181
  reject(new Error(`${err.message.split("\n")[0]}${tail ? ` \u2014 ${tail.slice(0, 300)}` : ""}`));
1131
- } else resolve6();
1182
+ } else resolve7();
1132
1183
  });
1133
1184
  });
1185
+ function ptyCandidatesIn(dir, platform = process.platform, arch = process.arch) {
1186
+ const pkg = join6(dir, "node_modules", PACKAGE_NAME);
1187
+ return [...hasVendoredPty(platform, arch) ? [join6(pkg, "vendor", "node-pty")] : [], join6(dir, "node_modules", "node-pty")];
1188
+ }
1134
1189
  function ptyLoadsIn(dir, execPath = process.execPath) {
1190
+ const script = "for(const p of process.argv.slice(1)){try{const m=require(p);if(typeof m.spawn==='function')process.exit(0)}catch{}}process.exit(3)";
1135
1191
  try {
1136
- execFileSync4(execPath, ["-e", "const m=require(process.argv[1]);if(typeof m.spawn!=='function')process.exit(3)", join6(dir, "node_modules", "node-pty")], {
1192
+ execFileSync4(execPath, ["-e", script, ...ptyCandidatesIn(dir)], {
1137
1193
  timeout: 3e4,
1138
1194
  windowsHide: true,
1139
1195
  shell: false,
@@ -1170,7 +1226,7 @@ async function installVersion(target, deps) {
1170
1226
  timeout: INSTALL_TIMEOUT_MS,
1171
1227
  shell: false,
1172
1228
  windowsHide: true,
1173
- env: { ...process.env, npm_config_update_notifier: "false", npm_config_fund: "false" }
1229
+ env: installEnv(execPath, process.env, platform)
1174
1230
  });
1175
1231
  } catch (e) {
1176
1232
  errors.push(`${registry} ${e.message}`);
@@ -1187,7 +1243,8 @@ async function installVersion(target, deps) {
1187
1243
  continue;
1188
1244
  }
1189
1245
  if (oldPtyOk() && !newPtyOk(dir)) {
1190
- errors.push(`${registry} \u88C5\u597D\u7684\u7248\u672C\u52A0\u8F7D\u4E0D\u4E86 node-pty`);
1246
+ const installed = ptyCandidatesIn(dir).some((p) => existsSync6(join6(p, "package.json")));
1247
+ errors.push(`${registry} \u88C5\u597D\u7684\u7248\u672C\u52A0\u8F7D\u4E0D\u4E86 node-pty\uFF08${installed ? "\u5305\u91CC\u6709\u4F46\u52A0\u8F7D\u5931\u8D25" : "\u5305\u91CC\u6CA1\u6709\uFF0Cnpm \u4E5F\u8DF3\u8FC7\u4E86\u53EF\u9009\u4F9D\u8D56"}\uFF09`);
1191
1248
  continue;
1192
1249
  }
1193
1250
  return cliPath;
@@ -1294,7 +1351,7 @@ var AutoUpdater = class {
1294
1351
  return "failed";
1295
1352
  }
1296
1353
  log2(`${target} \u5DF2\u88C5\u597D\u5E76\u6821\u9A8C\u901A\u8FC7\uFF0C\u7B49\u5B88\u62A4\u8FDB\u7A0B\u7A7A\u95F2\u540E\u5207\u6362`);
1297
- const sleep2 = this.#d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
1354
+ const sleep3 = this.#d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
1298
1355
  const deadline = this.#now() + (this.#d.idleMaxWaitMs ?? IDLE_MAX_WAIT_MS);
1299
1356
  let lastWhy = "";
1300
1357
  for (; ; ) {
@@ -1309,7 +1366,7 @@ var AutoUpdater = class {
1309
1366
  log2(`\u7B49\u4E86 24 \u5C0F\u65F6\u4ECD\u4E0D\u7A7A\u95F2\uFF0C\u653E\u5F03\u8FD9\u4E00\u8F6E\uFF0C\u4E0B\u6B21\u68C0\u67E5\u518D\u5224\u5B9A`);
1310
1367
  return "timeout";
1311
1368
  }
1312
- await sleep2(this.#d.idlePollMs ?? IDLE_POLL_MS);
1369
+ await sleep3(this.#d.idlePollMs ?? IDLE_POLL_MS);
1313
1370
  }
1314
1371
  if (this.#stopped) return "stopped";
1315
1372
  this.#d.beginDrain?.();
@@ -1342,7 +1399,7 @@ var AutoUpdater = class {
1342
1399
  // src/daemon/index.ts
1343
1400
  import { randomBytes as randomBytes7 } from "crypto";
1344
1401
  import { existsSync as existsSync13 } from "fs";
1345
- import { basename as basename7 } from "path";
1402
+ import { basename as basename8, dirname as dirname10, join as join26 } from "path";
1346
1403
  import WebSocket2 from "ws";
1347
1404
 
1348
1405
  // src/adapters/ocPlugin.ts
@@ -1407,6 +1464,31 @@ function processName(pid) {
1407
1464
  return null;
1408
1465
  }
1409
1466
  }
1467
+ function parentPid(pid) {
1468
+ if (WIN) {
1469
+ const pp = winProcs()?.find((p) => p.pid === pid)?.ppid ?? 0;
1470
+ return pp > 4 && pp !== pid ? pp : null;
1471
+ }
1472
+ try {
1473
+ const out = execFileSync5("ps", ["-p", String(pid), "-o", "ppid="], {
1474
+ encoding: "utf8",
1475
+ stdio: ["ignore", "pipe", "ignore"]
1476
+ });
1477
+ const ppid = Number(out.trim());
1478
+ return Number.isFinite(ppid) && ppid > 1 ? ppid : null;
1479
+ } catch {
1480
+ return null;
1481
+ }
1482
+ }
1483
+ function selfAndAncestors(pid, maxDepth = 8) {
1484
+ const out = [];
1485
+ let cur = pid;
1486
+ for (let i = 0; i <= maxDepth && cur != null && !out.includes(cur); i++) {
1487
+ out.push(cur);
1488
+ cur = parentPid(cur);
1489
+ }
1490
+ return out;
1491
+ }
1410
1492
  function isAlive(pid, expectName) {
1411
1493
  if (!Number.isFinite(pid) || pid <= 0) return false;
1412
1494
  try {
@@ -1440,6 +1522,7 @@ function writeEnded(s, now = Date.now()) {
1440
1522
  mkdirSync7(dir, { recursive: true, mode: 448 });
1441
1523
  const rec = { sessionId: s.sessionId, tool: s.tool, cwd: s.cwd ?? "", interactive: s.interactive === true, endedAt: now };
1442
1524
  if (Number.isInteger(s.turns) && s.turns >= 0) rec.turns = s.turns;
1525
+ if (typeof s.firstPrompt === "string" && s.firstPrompt) rec.firstPrompt = s.firstPrompt;
1443
1526
  const file = join8(dir, `${s.sessionId}.json`);
1444
1527
  const tmp = `${file}.${process.pid}.tmp`;
1445
1528
  writeFileSync6(tmp, JSON.stringify(rec), { encoding: "utf8", mode: 384 });
@@ -1562,6 +1645,12 @@ function readState(sessionId) {
1562
1645
  return null;
1563
1646
  }
1564
1647
  }
1648
+ function writeState(s) {
1649
+ mkdirSync8(stateDir(), { recursive: true });
1650
+ const tmp = statePath(s.sessionId) + ".tmp";
1651
+ writeFileSync7(tmp, JSON.stringify(s), "utf8");
1652
+ renameSync6(tmp, statePath(s.sessionId));
1653
+ }
1565
1654
  function retireState(s, now = Date.now()) {
1566
1655
  writeEnded(s, now);
1567
1656
  removeState(s.sessionId);
@@ -1572,8 +1661,26 @@ function removeState(sessionId) {
1572
1661
  } catch {
1573
1662
  }
1574
1663
  }
1575
- function readAliveStates(tool) {
1664
+ var STALE_NO_PID_MS = 2 * 60 * 60 * 1e3;
1665
+ var STALE_STATES = /* @__PURE__ */ new Set(["watch", "ask_approval"]);
1666
+ var STALE_NO_PID_LOG = "\u65E0\u8FDB\u7A0B\u53F7\u4E14 2 \u5C0F\u65F6\u65E0\u4E8B\u4EF6\uFF0C\u6309\u5DF2\u7ED3\u675F\u5904\u7406";
1667
+ function pendingApprovalSessions() {
1668
+ try {
1669
+ const recs = JSON.parse(readFileSync8(join9(deskbuddyHome(), "approvals.json"), "utf8"));
1670
+ return new Set(Array.isArray(recs) ? recs.filter((r) => r && r.status === "pending" && typeof r.sessionId === "string").map((r) => r.sessionId) : []);
1671
+ } catch {
1672
+ return /* @__PURE__ */ new Set();
1673
+ }
1674
+ }
1675
+ function isStaleNoPid(s, now, pending) {
1676
+ if (s.pid || !STALE_STATES.has(s.state) || !(now - s.updatedAt > STALE_NO_PID_MS)) return false;
1677
+ return !pending().has(s.sessionId);
1678
+ }
1679
+ function readAliveStates(tool, opts = {}) {
1576
1680
  if (!existsSync8(stateDir())) return [];
1681
+ const now = opts.now ?? Date.now();
1682
+ let pend = null;
1683
+ const pending = () => pend ??= pendingApprovalSessions();
1577
1684
  const alive = [];
1578
1685
  for (const file of readdirSync3(stateDir())) {
1579
1686
  if (!file.endsWith(".json")) continue;
@@ -1589,6 +1696,14 @@ function readAliveStates(tool) {
1589
1696
  retireState(s);
1590
1697
  continue;
1591
1698
  }
1699
+ if (isStaleNoPid(s, now, pending)) {
1700
+ try {
1701
+ opts.onStale?.(s);
1702
+ } catch {
1703
+ }
1704
+ retireState(s);
1705
+ continue;
1706
+ }
1592
1707
  alive.push(s);
1593
1708
  }
1594
1709
  return alive;
@@ -1812,7 +1927,13 @@ function parseApprovalFrame(frame) {
1812
1927
  if (!isValidId(approvalId)) return null;
1813
1928
  const decision = str(d.decision);
1814
1929
  if (decision !== "allow" && decision !== "deny" && decision !== "allow_always") return null;
1815
- return { instructionId, approvalId, decision, deviceName: cleanDeviceName(d.deviceName) };
1930
+ return {
1931
+ instructionId,
1932
+ approvalId,
1933
+ decision,
1934
+ deviceName: cleanDeviceName(d.deviceName),
1935
+ ...Array.isArray(d.answers) && d.answers.length <= 8 ? { answers: d.answers } : {}
1936
+ };
1816
1937
  }
1817
1938
  function cleanDeviceName(v) {
1818
1939
  return typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 32) : "";
@@ -1828,7 +1949,7 @@ import { join as join12 } from "path";
1828
1949
 
1829
1950
  // src/daemon/gate/settings.ts
1830
1951
  import { existsSync as existsSync10 } from "fs";
1831
- import { fileURLToPath as fileURLToPath3 } from "url";
1952
+ import { fileURLToPath as fileURLToPath4 } from "url";
1832
1953
  var GATE_HOOK_TIMEOUT_S = 7 * 24 * 60 * 60;
1833
1954
  function waitModeFor(tool) {
1834
1955
  return tool === "claude_code" ? "defer" : "hold";
@@ -1851,8 +1972,8 @@ function winPsPath(p) {
1851
1972
  }
1852
1973
  function defaultGateScript() {
1853
1974
  const candidates = [
1854
- fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
1855
- fileURLToPath3(new URL("../../../hook/gate.cjs", import.meta.url))
1975
+ fileURLToPath4(new URL("./gate.cjs", import.meta.url)),
1976
+ fileURLToPath4(new URL("../../../hook/gate.cjs", import.meta.url))
1856
1977
  ];
1857
1978
  return candidates.find((p) => existsSync10(p)) ?? candidates[0];
1858
1979
  }
@@ -1886,6 +2007,10 @@ import { execFileSync as execFileSync6 } from "child_process";
1886
2007
  import { accessSync, constants, existsSync as existsSync11, lstatSync, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
1887
2008
  import { homedir as homedir4 } from "os";
1888
2009
  import { delimiter, dirname as dirname7, join as join11, win32 as win323 } from "path";
2010
+ function qoderWorker(d, worker, appBin) {
2011
+ if (d.isExecutable(appBin)) return { file: appBin, prefixArgs: [worker], env: { ELECTRON_RUN_AS_NODE: "1" } };
2012
+ return { file: d.nodePath, prefixArgs: [worker] };
2013
+ }
1889
2014
  var defaultLocateDeps = () => ({
1890
2015
  home: homedir4(),
1891
2016
  path: process.env.PATH ?? "",
@@ -2084,14 +2209,14 @@ function locateWin(tool, d) {
2084
2209
  case "qoder_cn": {
2085
2210
  for (const dir of winAppDirs(d, "Qoder CN")) {
2086
2211
  const p = join11(dir, QODER_CN_WORKER_IN_WIN_APP);
2087
- if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
2212
+ if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder CN.exe"));
2088
2213
  }
2089
2214
  return null;
2090
2215
  }
2091
2216
  case "qoder": {
2092
2217
  for (const dir of winAppDirs(d, "Qoder")) {
2093
2218
  const p = join11(dir, QODER_WORKER_IN_WIN_APP);
2094
- if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
2219
+ if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder.exe"));
2095
2220
  }
2096
2221
  return plain("qodercli");
2097
2222
  }
@@ -2128,14 +2253,14 @@ function locate(tool, d = defaultLocateDeps()) {
2128
2253
  case "qoder_cn": {
2129
2254
  for (const dir of appDirs(d.home)) {
2130
2255
  const p = join11(dir, QODER_CN_WORKER_IN_APP);
2131
- if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
2256
+ if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder CN.app", "Contents", "MacOS", "Qoder CN"));
2132
2257
  }
2133
2258
  return null;
2134
2259
  }
2135
2260
  case "qoder": {
2136
2261
  for (const dir of appDirs(d.home)) {
2137
2262
  const p = join11(dir, QODER_WORKER_IN_APP);
2138
- if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
2263
+ if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder.app", "Contents", "MacOS", "Qoder"));
2139
2264
  }
2140
2265
  return plain("qodercli");
2141
2266
  }
@@ -2262,7 +2387,7 @@ function materialize(plan, tool, d = defaultLocateDeps()) {
2262
2387
  file: loc.file,
2263
2388
  args: [...loc.prefixArgs, ...plan.args],
2264
2389
  cwd: plan.cwd,
2265
- env: { ...plan.env, PATH: childPath(d) }
2390
+ env: { ...plan.env, ...loc.env, PATH: childPath(d) }
2266
2391
  };
2267
2392
  }
2268
2393
  function isDeferred(stdout7) {
@@ -2326,6 +2451,17 @@ function parseSessionId(tool, stdout7) {
2326
2451
  if (tool === "opencode") return parseOpencodeSessionId(stdout7);
2327
2452
  return parseClaudeSessionId(stdout7);
2328
2453
  }
2454
+ var FAILURE_TAIL_CHARS = 500;
2455
+ function failureTail(r, text) {
2456
+ let s = (r.stderr ?? "").trim() || (r.stdout ?? "").trim();
2457
+ if (!s) return "";
2458
+ const plain = [text, text.replace(/\s+/g, " ").trim()];
2459
+ const forms = [...plain, ...plain.map((t) => JSON.stringify(t).slice(1, -1))].filter((t, i, a) => t.length >= 2 && a.indexOf(t) === i);
2460
+ forms.sort((a, b) => b.length - a.length);
2461
+ for (const f of forms) s = s.split(f).join("<\u6307\u4EE4>");
2462
+ s = s.replace(/\s+/g, " ").trim();
2463
+ return s.length > FAILURE_TAIL_CHARS ? "\u2026" + s.slice(-FAILURE_TAIL_CHARS) : s;
2464
+ }
2329
2465
  function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2330
2466
  let remaining = limitMs;
2331
2467
  let startedAt = Date.now();
@@ -2341,7 +2477,7 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2341
2477
  }, Math.max(0, remaining));
2342
2478
  };
2343
2479
  let timedOut = false;
2344
- const done = new Promise((resolve6) => {
2480
+ const done = new Promise((resolve7) => {
2345
2481
  child = execFile3(
2346
2482
  plan.file,
2347
2483
  plan.args,
@@ -2353,16 +2489,18 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2353
2489
  // Windows:别给子进程弹黑框
2354
2490
  windowsHide: true
2355
2491
  },
2356
- (err, stdout7) => {
2492
+ (err, stdout7, stderr) => {
2357
2493
  finished = true;
2358
2494
  if (timer) clearTimeout(timer);
2359
2495
  const out = String(stdout7 ?? "");
2496
+ const errOut = String(stderr ?? "");
2360
2497
  if (err || timedOut) {
2361
- const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : err.message;
2362
- resolve6({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
2498
+ const e = err;
2499
+ const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : e && typeof e.code === "number" ? `\u9000\u51FA\u7801 ${e.code}` : e && e.signal ? `\u88AB\u4FE1\u53F7 ${String(e.signal)} \u7ED3\u675F` : `\u8D77\u4E0D\u6765\uFF08${String(e?.code ?? "unknown")}\uFF09`;
2500
+ resolve7({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, stderr: errOut, error: msg });
2363
2501
  return;
2364
2502
  }
2365
- resolve6({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
2503
+ resolve7({ ok: true, sessionId: parseSessionId(tool, out), stdout: out, stderr: errOut });
2366
2504
  }
2367
2505
  );
2368
2506
  child?.stdin?.end();
@@ -2370,6 +2508,9 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2370
2508
  });
2371
2509
  return {
2372
2510
  done,
2511
+ get pid() {
2512
+ return child?.pid;
2513
+ },
2373
2514
  pauseTimeout() {
2374
2515
  if (finished) return;
2375
2516
  if (paused++ === 0 && timer) {
@@ -2437,6 +2578,7 @@ var ApprovalStore = class _ApprovalStore {
2437
2578
  }
2438
2579
  }
2439
2580
  save() {
2581
+ for (const r of this.#records) if (r.status !== "pending" && r.toolInput !== void 0) delete r.toolInput;
2440
2582
  try {
2441
2583
  mkdirSync10(deskbuddyHome(), { recursive: true });
2442
2584
  const tmp = storePath() + ".tmp";
@@ -2492,7 +2634,9 @@ function serverBackend(client = api) {
2492
2634
  summary: r.summary,
2493
2635
  cwdName: basename5(r.cwd),
2494
2636
  source: r.mode === "window" ? "window" : "remote",
2495
- ...r.remember ? { remember: r.remember } : {}
2637
+ ...r.remember ? { remember: r.remember } : {},
2638
+ // 🔴 选择题只发展示用的题目和背景;原始 tool_input(r.toolInput)只留本机
2639
+ ...r.kind === "question" && r.questions ? { questions: r.questions, ...r.context ? { context: r.context } : {} } : {}
2496
2640
  }),
2497
2641
  cancel: (approvalId, reason) => client.approvalCancel(approvalId, reason),
2498
2642
  sync: async (ids) => {
@@ -2501,7 +2645,7 @@ function serverBackend(client = api) {
2501
2645
  for (const a of res?.approvals ?? []) {
2502
2646
  if (typeof a?.approvalId !== "string") continue;
2503
2647
  const d = mapStatus(a.status, a.decision);
2504
- if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName) });
2648
+ if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName), ...Array.isArray(a.answers) ? { answers: a.answers } : {} });
2505
2649
  }
2506
2650
  return out;
2507
2651
  }
@@ -2671,6 +2815,152 @@ function describe(toolName, toolInput, cwd) {
2671
2815
  return { kind: SHELL_TOOLS.has(toolName) ? "command" : "other", summary: truncate(detail ? `${toolName} ${detail}` : toolName) };
2672
2816
  }
2673
2817
 
2818
+ // src/daemon/gate/question.ts
2819
+ import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync4 } from "fs";
2820
+ var MAX_QUESTIONS = 4;
2821
+ var MAX_OPTIONS = 4;
2822
+ var HEADER_MAX = 12;
2823
+ var QUESTION_MAX = 150;
2824
+ var LABEL_MAX = 40;
2825
+ var DESC_MAX = 80;
2826
+ var SUMMARY_MAX2 = 200;
2827
+ var CONTEXT_MAX = 300;
2828
+ var OTHER_MAX = 200;
2829
+ function clean(v) {
2830
+ return typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ").replace(/\s+/g, " ").trim() : "";
2831
+ }
2832
+ function cut(s, max) {
2833
+ const a = Array.from(s);
2834
+ return a.length > max ? a.slice(0, max - 1).join("") + "\u2026" : s;
2835
+ }
2836
+ var RECOMMENDED_RE = /\s*\(Recommended\)\s*$/i;
2837
+ function rawQuestions(toolInput) {
2838
+ const ans = toolInput?.answers;
2839
+ if (ans !== void 0 && ans !== null) return null;
2840
+ const qs = toolInput?.questions;
2841
+ if (!Array.isArray(qs) || qs.length === 0 || qs.length > MAX_QUESTIONS) return null;
2842
+ const seen = /* @__PURE__ */ new Set();
2843
+ for (const q of qs) {
2844
+ if (!q || typeof q !== "object" || typeof q.question !== "string" || !q.question.trim()) return null;
2845
+ const kind = q.kind;
2846
+ if (kind !== void 0 && kind !== "choice") return null;
2847
+ if (seen.has(q.question)) return null;
2848
+ seen.add(q.question);
2849
+ if (!Array.isArray(q.options) || q.options.length < 2) return null;
2850
+ for (const o of q.options) if (!o || typeof o !== "object" || typeof o.label !== "string" || !o.label.trim()) return null;
2851
+ }
2852
+ return qs;
2853
+ }
2854
+ function questionView(toolInput) {
2855
+ const raw = rawQuestions(toolInput);
2856
+ if (!raw) return null;
2857
+ const questions = raw.map((q) => {
2858
+ const opts = q.options.slice(0, MAX_OPTIONS).map((o) => {
2859
+ const recommended = RECOMMENDED_RE.test(o.label);
2860
+ return {
2861
+ label: cut(clean(o.label.replace(RECOMMENDED_RE, "")), LABEL_MAX),
2862
+ description: cut(clean(o.description), DESC_MAX),
2863
+ recommended
2864
+ };
2865
+ });
2866
+ return {
2867
+ header: cut(clean(q.header), HEADER_MAX),
2868
+ question: cut(clean(q.question), QUESTION_MAX),
2869
+ multiSelect: q.multiSelect === true,
2870
+ options: opts,
2871
+ ...q.options.length > MAX_OPTIONS ? { truncatedOptions: true } : {}
2872
+ };
2873
+ });
2874
+ const first = questions[0];
2875
+ return { questions, summary: cut(first.question || first.header, SUMMARY_MAX2) };
2876
+ }
2877
+ function joinAnswerItems(items) {
2878
+ return items.map((s) => s.includes(", ") || s.includes('"') ? JSON.stringify(s) : s).join(", ");
2879
+ }
2880
+ function resolveAnswers(toolInput, answers) {
2881
+ const raw = rawQuestions(toolInput);
2882
+ if (!raw || !Array.isArray(answers) || answers.length !== raw.length) return null;
2883
+ const res = [];
2884
+ const done = /* @__PURE__ */ new Set();
2885
+ const ordered = [...answers].sort((x, y) => (x?.index ?? 0) - (y?.index ?? 0));
2886
+ for (const a of ordered) {
2887
+ if (!a || typeof a !== "object" || !Number.isInteger(a.index) || a.index < 0 || a.index >= raw.length || done.has(a.index)) return null;
2888
+ done.add(a.index);
2889
+ const q = raw[a.index];
2890
+ const shown = Math.min(q.options.length, MAX_OPTIONS);
2891
+ const sel = Array.isArray(a.selected) ? a.selected : [];
2892
+ if (!sel.every((i) => Number.isInteger(i) && i >= 0 && i < shown) || new Set(sel).size !== sel.length) return null;
2893
+ const other = clean(a.other);
2894
+ if (Array.from(other).length > OTHER_MAX) return null;
2895
+ if (other && q.custom === false) return null;
2896
+ const labels = [...sel].sort((x, y) => x - y).map((i) => q.options[i].label);
2897
+ if (q.multiSelect === true) {
2898
+ if (labels.length === 0 && !other) return null;
2899
+ const items = other ? [...labels, other] : labels;
2900
+ res.push({ question: q.question, items, joined: joinAnswerItems(items) });
2901
+ } else {
2902
+ if (labels.length === 1 && !other) res.push({ question: q.question, items: labels, joined: labels[0] });
2903
+ else if (labels.length === 0 && other) res.push({ question: q.question, items: [other], joined: other });
2904
+ else return null;
2905
+ }
2906
+ }
2907
+ return res;
2908
+ }
2909
+ function answersToUpdatedInput(toolInput, answers) {
2910
+ const r = resolveAnswers(toolInput, answers);
2911
+ if (!r) return null;
2912
+ const out = {};
2913
+ for (const x of r) out[x.question] = x.joined;
2914
+ return { ...toolInput, answers: out };
2915
+ }
2916
+ function answersSummary(updatedInput) {
2917
+ const a = updatedInput.answers;
2918
+ const s = Object.values(a ?? {}).join("\uFF1B");
2919
+ return cut(clean(s), 60);
2920
+ }
2921
+ var TAIL_BYTES = 256 * 1024;
2922
+ function lastAssistantText(transcriptPath) {
2923
+ if (typeof transcriptPath !== "string" || !transcriptPath) return "";
2924
+ let text = "";
2925
+ try {
2926
+ const size = statSync4(transcriptPath).size;
2927
+ const len = Math.min(size, TAIL_BYTES);
2928
+ const buf = Buffer.alloc(len);
2929
+ const fd = openSync3(transcriptPath, "r");
2930
+ try {
2931
+ readSync2(fd, buf, 0, len, size - len);
2932
+ } finally {
2933
+ closeSync3(fd);
2934
+ }
2935
+ text = buf.toString("utf8");
2936
+ } catch {
2937
+ return "";
2938
+ }
2939
+ const lines2 = text.split("\n");
2940
+ for (let i = lines2.length - 1; i >= 0; i--) {
2941
+ const line = lines2[i].trim();
2942
+ if (!line.startsWith("{")) continue;
2943
+ try {
2944
+ const m = JSON.parse(line);
2945
+ if (m.type === "user") {
2946
+ const c = m.message?.content;
2947
+ const toolResultOnly = Array.isArray(c) && c.length > 0 && c.every((x) => x?.type === "tool_result");
2948
+ if (!toolResultOnly) return "";
2949
+ continue;
2950
+ }
2951
+ if (m.type !== "assistant" || !Array.isArray(m.message?.content)) continue;
2952
+ const parts = m.message.content.filter((c) => c && c.type === "text" && typeof c.text === "string").map((c) => c.text);
2953
+ const body = parts.join("\n").trim();
2954
+ if (!body) continue;
2955
+ const paras = body.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
2956
+ return cut(clean(paras[paras.length - 1] ?? body), CONTEXT_MAX);
2957
+ } catch {
2958
+ continue;
2959
+ }
2960
+ }
2961
+ return "";
2962
+ }
2963
+
2674
2964
  // src/daemon/gate/procs.ts
2675
2965
  import { execFileSync as execFileSync8 } from "child_process";
2676
2966
  function listDescendants(rootPid) {
@@ -2785,7 +3075,17 @@ function windowNotice(decision, summary2, deviceName = "") {
2785
3075
  const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
2786
3076
  return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
2787
3077
  }
2788
- var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
3078
+ var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["ExitPlanMode"]);
3079
+ var QUESTION_TOOL = "AskUserQuestion";
3080
+ var QUESTION_TOOLS_HOSTS = /* @__PURE__ */ new Set(["claude_code", "qoder_cn", "qoder", "codex", "opencode", "qwenwork", "dumate"]);
3081
+ var QUESTION_SKIPPED = "\u7528\u6237\u5728\u8BBE\u5907\u4E0A\u9009\u62E9\u4E0D\u56DE\u7B54\u8FD9\u4E2A\u95EE\u9898\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u8BE2\u95EE\u6216\u6309\u4F60\u7684\u5224\u65AD\u7EE7\u7EED";
3082
+ var QUESTION_UNANSWERABLE = "\u8BBE\u5907\u672A\u7ED9\u51FA\u6709\u6548\u7B54\u6848\uFF08\u8FD9\u4E2A\u95EE\u9898\u6CA1\u6CD5\u5728\u8BBE\u5907\u4E0A\u56DE\u7B54\uFF09\uFF0C\u8BF7\u6309\u4F60\u7684\u5224\u65AD\u7EE7\u7EED\uFF0C\u6216\u5728\u7ED3\u679C\u91CC\u8BF4\u660E\u9700\u8981\u7528\u6237\u51B3\u5B9A\u7684\u4E8B";
3083
+ var REMOTE_QUESTION_TOOLS = /* @__PURE__ */ new Set(["workbuddy"]);
3084
+ var NO_NOTICE_TOOLS = /* @__PURE__ */ new Set(["qwenwork", "dumate"]);
3085
+ function questionNotice(summary2, deviceName = "") {
3086
+ const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
3087
+ return `\u2705 \u5DF2\u5728${who}\u4E0A\u56DE\u7B54\uFF1A${summary2}`;
3088
+ }
2789
3089
  var WINDOW_SETTLED_EVENTS = /* @__PURE__ */ new Set(["UserPromptSubmit", "Stop"]);
2790
3090
  var WINDOW_TOOL_DONE_EVENTS = /* @__PURE__ */ new Set(["PostToolUse", "PostToolUseFailure"]);
2791
3091
  var WINDOW_PROC_WATCH_MS = 10 * 60 * 1e3;
@@ -2823,7 +3123,10 @@ var ApprovalCoordinator = class {
2823
3123
  const toolName = typeof req2.hook.tool_name === "string" ? req2.hook.tool_name : "";
2824
3124
  const sessionId = typeof req2.hook.session_id === "string" && req2.hook.session_id || run2.sessionId || "";
2825
3125
  if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
2826
- const v = classify(toolName, req2.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
3126
+ const remoteQuestion = toolName === QUESTION_TOOL && REMOTE_QUESTION_TOOLS.has(run2.tool) && run2.mode === "hold";
3127
+ const qv = remoteQuestion ? questionView(req2.hook.tool_input) : null;
3128
+ if (remoteQuestion && !qv) return { decision: "deny", reason: QUESTION_UNANSWERABLE };
3129
+ const v = qv ? { action: "ask", kind: "question", summary: qv.summary } : classify(toolName, req2.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
2827
3130
  if (v.action === "auto") return { decision: "allow" };
2828
3131
  const fp = fingerprint(toolName, req2.hook.tool_input);
2829
3132
  const store = this.#d.store;
@@ -2848,13 +3151,17 @@ var ApprovalCoordinator = class {
2848
3151
  summary: v.summary,
2849
3152
  mode: run2.mode,
2850
3153
  status: "pending",
2851
- createdAt: this.#now()
3154
+ createdAt: this.#now(),
3155
+ ...qv ? (() => {
3156
+ const context = lastAssistantText(req2.hook.transcript_path);
3157
+ return { questions: qv.questions, toolInput: req2.hook.tool_input, ...context ? { context } : {} };
3158
+ })() : {}
2852
3159
  };
2853
3160
  store.add(rec);
2854
3161
  store.save();
2855
3162
  let early = null;
2856
3163
  let settle;
2857
- const decided = new Promise((resolve6) => settle = resolve6);
3164
+ const decided = new Promise((resolve7) => settle = resolve7);
2858
3165
  this.#waiters.set(rec.approvalId, (r) => {
2859
3166
  early = r;
2860
3167
  settle(r);
@@ -2878,8 +3185,8 @@ var ApprovalCoordinator = class {
2878
3185
  }
2879
3186
  run2.onWaitStart?.();
2880
3187
  try {
2881
- return await new Promise((resolve6) => {
2882
- void decided.then(resolve6);
3188
+ return await new Promise((resolve7) => {
3189
+ void decided.then(resolve7);
2883
3190
  const onAbort = () => {
2884
3191
  if (!this.#waiters.delete(rec.approvalId)) return;
2885
3192
  if (rec.status === "pending") {
@@ -2888,7 +3195,7 @@ var ApprovalCoordinator = class {
2888
3195
  store.save();
2889
3196
  void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
2890
3197
  }
2891
- resolve6({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
3198
+ resolve7({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
2892
3199
  };
2893
3200
  if (signal.aborted) onAbort();
2894
3201
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -2909,10 +3216,14 @@ var ApprovalCoordinator = class {
2909
3216
  const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
2910
3217
  const cwd = typeof hook.cwd === "string" ? hook.cwd : "";
2911
3218
  if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
3219
+ const isQuestion = toolName === QUESTION_TOOL;
3220
+ if (isQuestion && !QUESTION_TOOLS_HOSTS.has(tool)) return none;
3221
+ const qv = isQuestion ? questionView(hook.tool_input) : null;
3222
+ if (isQuestion && !qv) return none;
2912
3223
  for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
2913
3224
  const store = this.#d.store;
2914
- const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
2915
- const remember = REMEMBER_TOOLS.has(tool) ? pickRememberSuggestion(hook.permission_suggestions, { edit: EDIT_TOOLS2.has(toolName) || kind === "edit" }) : null;
3225
+ const { kind, summary: summary2 } = qv ? { kind: "question", summary: qv.summary } : describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
3226
+ const remember = !qv && REMEMBER_TOOLS.has(tool) ? pickRememberSuggestion(hook.permission_suggestions, { edit: EDIT_TOOLS2.has(toolName) || kind === "edit" }) : null;
2916
3227
  const rec = {
2917
3228
  approvalId: randomUUID3(),
2918
3229
  runId: "",
@@ -2925,7 +3236,11 @@ var ApprovalCoordinator = class {
2925
3236
  mode: "window",
2926
3237
  status: "pending",
2927
3238
  createdAt: this.#now(),
2928
- ...remember ? { remember: remember.text, rememberUpdates: remember.updates } : {}
3239
+ ...remember ? { remember: remember.text, rememberUpdates: remember.updates } : {},
3240
+ ...qv ? (() => {
3241
+ const context = lastAssistantText(hook.transcript_path);
3242
+ return { questions: qv.questions, toolInput: hook.tool_input, ...context ? { context } : {} };
3243
+ })() : {}
2929
3244
  };
2930
3245
  store.add(rec);
2931
3246
  store.save();
@@ -2940,7 +3255,7 @@ var ApprovalCoordinator = class {
2940
3255
  ...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
2941
3256
  });
2942
3257
  let settle;
2943
- const decided = new Promise((resolve6) => settle = resolve6);
3258
+ const decided = new Promise((resolve7) => settle = resolve7);
2944
3259
  this.#waiters.set(rec.approvalId, settle);
2945
3260
  try {
2946
3261
  await this.#d.backend.create(rec);
@@ -2956,11 +3271,11 @@ var ApprovalCoordinator = class {
2956
3271
  }
2957
3272
  this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
2958
3273
  if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
2959
- return new Promise((resolve6) => {
2960
- void decided.then(resolve6);
3274
+ return new Promise((resolve7) => {
3275
+ void decided.then(resolve7);
2961
3276
  const onAbort = () => {
2962
3277
  if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
2963
- resolve6(none);
3278
+ resolve7(none);
2964
3279
  };
2965
3280
  if (signal.aborted) onAbort();
2966
3281
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -2989,8 +3304,10 @@ var ApprovalCoordinator = class {
2989
3304
  }
2990
3305
  const fp = rec.fingerprint.slice(0, 16);
2991
3306
  const settled = (st.recent ?? []).some(
2992
- (e) => e.at > rec.createdAt && (WINDOW_SETTLED_EVENTS.has(e.e) || // 跑完的必须正是这一步:并行的同名工具不算(评审 #2)。老版 hook 没有 fp 时退回只比工具名
2993
- WINDOW_TOOL_DONE_EVENTS.has(e.e) && e.tool === w.toolName && (e.fp === void 0 || e.fp === fp))
3307
+ (e) => e.at > rec.createdAt && (WINDOW_SETTLED_EVENTS.has(e.e) || // 跑完的必须正是这一步:并行的同名工具不算(评审 #2)。老版 hook 没有 fp 时退回只比工具名。
3308
+ // 选择题只比工具名:答完后 PostToolUse 的 tool_input 带上了 answers,指纹变了(spec §3.5 / R2);
3309
+ // 同一会话同时只会有一道选择题(子代理用不了 AskUserQuestion,窗口里一次也只弹一个选择框)
3310
+ WINDOW_TOOL_DONE_EVENTS.has(e.e) && e.tool === w.toolName && (e.fp === void 0 || e.fp === fp || rec.kind === "question"))
2994
3311
  );
2995
3312
  if (settled) {
2996
3313
  this.#settleWindow(rec, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
@@ -3026,6 +3343,56 @@ var ApprovalCoordinator = class {
3026
3343
  }
3027
3344
  void this.#cancelRemote(rec.approvalId, reason);
3028
3345
  }
3346
+ /**
3347
+ * 选择题的答复:作答 → 用本地原始 tool_input 按下标拼 updatedInput 交回 hook;跳过 → deny(不带设备名)。
3348
+ * 🔴 答案拼不出来(下标越界、单选给了多个……)一律不表态,窗口里的选择框照常由人答(红线 R7)
3349
+ */
3350
+ #onQuestionDecision(rec, decision, deviceName, answers) {
3351
+ rec.decidedAt = this.#now();
3352
+ rec.consumed = true;
3353
+ this.#windows.delete(rec.approvalId);
3354
+ const waiter = this.#waiters.get(rec.approvalId);
3355
+ this.#waiters.delete(rec.approvalId);
3356
+ const remote = rec.mode !== "window";
3357
+ if (decision === "deny") {
3358
+ rec.status = "denied";
3359
+ this.#d.store.save();
3360
+ if (!waiter) return;
3361
+ if (remote) return waiter({ decision: "deny", reason: QUESTION_SKIPPED });
3362
+ const notice2 = `\u23ED \u5DF2\u5728${deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK}\u4E0A\u8DF3\u8FC7\uFF1A${rec.summary.slice(0, 60)}`;
3363
+ if (!NO_NOTICE_TOOLS.has(rec.tool)) {
3364
+ try {
3365
+ ;
3366
+ (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice2);
3367
+ } catch {
3368
+ }
3369
+ }
3370
+ waiter({ decision: "deny", reason: QUESTION_SKIPPED, notice: notice2 });
3371
+ return;
3372
+ }
3373
+ const resolved = resolveAnswers(rec.toolInput, answers);
3374
+ const updatedInput = resolved ? answersToUpdatedInput(rec.toolInput, answers) : null;
3375
+ if (!resolved || !updatedInput) {
3376
+ rec.status = "cancelled";
3377
+ this.#d.store.save();
3378
+ this.#d.log(`\u9009\u62E9\u9898 ${rec.approvalId} \u7684\u7B54\u6848\u5BF9\u4E0D\u4E0A\u9898\u76EE\uFF0C${remote ? "\u62D2\u7EDD" : "\u4E0D\u8868\u6001"}`);
3379
+ waiter?.(remote ? { decision: "deny", reason: QUESTION_UNANSWERABLE } : { decision: "none" });
3380
+ return;
3381
+ }
3382
+ rec.status = "approved";
3383
+ this.#d.store.save();
3384
+ if (!waiter) return;
3385
+ if (remote) return waiter({ decision: "allow", updatedInput });
3386
+ const notice = questionNotice(answersSummary(updatedInput), deviceName);
3387
+ if (!NO_NOTICE_TOOLS.has(rec.tool)) {
3388
+ try {
3389
+ ;
3390
+ (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
3391
+ } catch {
3392
+ }
3393
+ }
3394
+ waiter({ decision: "allow", notice, updatedInput, answerLists: resolved.map((r) => r.items) });
3395
+ }
3029
3396
  #readState(sessionId) {
3030
3397
  try {
3031
3398
  return (this.#d.readState ?? readState)(sessionId);
@@ -3033,14 +3400,18 @@ var ApprovalCoordinator = class {
3033
3400
  return null;
3034
3401
  }
3035
3402
  }
3036
- /** 设备上按了允许/拒绝(经服务端下发,或重连后 sync 补到) */
3037
- onDecision(approvalId, decision, deviceName = "") {
3403
+ /** 设备上按了允许/拒绝(经服务端下发,或重连后 sync 补到)。选择题的答案在 answers 里(下标) */
3404
+ onDecision(approvalId, decision, deviceName = "", answers) {
3038
3405
  const rec = this.#d.store.get(approvalId);
3039
3406
  if (!rec) {
3040
3407
  this.#d.log(`\u6536\u5230\u672A\u77E5\u6388\u6743 ${approvalId} \u7684\u51B3\u5B9A\uFF0C\u5FFD\u7565`);
3041
3408
  return;
3042
3409
  }
3043
3410
  if (rec.status !== "pending") return;
3411
+ if (rec.kind === "question") {
3412
+ this.#onQuestionDecision(rec, decision, deviceName, answers);
3413
+ return;
3414
+ }
3044
3415
  const updates = rememberUpdatesOf(rec);
3045
3416
  const always = decision === "allow_always" && rec.mode === "window" && updates !== null;
3046
3417
  if (decision === "allow_always") decision = "allow";
@@ -3054,10 +3425,12 @@ var ApprovalCoordinator = class {
3054
3425
  this.#d.store.save();
3055
3426
  if (rec.mode === "window") {
3056
3427
  const notice = windowNotice(decision, rec.summary, deviceName) + (always ? "\uFF08\u5DF2\u8BB0\u4F4F\uFF09" : "");
3057
- try {
3058
- ;
3059
- (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
3060
- } catch {
3428
+ if (!NO_NOTICE_TOOLS.has(rec.tool)) {
3429
+ try {
3430
+ ;
3431
+ (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
3432
+ } catch {
3433
+ }
3061
3434
  }
3062
3435
  waiter(
3063
3436
  decision === "allow" ? { decision: "allow", notice, ...always && updates ? { updatedPermissions: updates } : {} } : (
@@ -3085,6 +3458,16 @@ var ApprovalCoordinator = class {
3085
3458
  this.#d.store.save();
3086
3459
  if (decision === "allow") this.#d.resume(rec, "orphan");
3087
3460
  }
3461
+ /**
3462
+ * 认出某个后台进程其实在跑 sessionId 这个会话(它还没进过关卡、run 上没记会话号,例如按进程号认出来的新建任务):
3463
+ * 记上会话号并标成已被取代 —— 之后它再进关卡立即被拒,不会在 hold 上挂住、让排在它后面的回复等 7 天
3464
+ */
3465
+ supersedeRun(runId, sessionId) {
3466
+ const run2 = this.#runs.get(runId);
3467
+ if (!run2) return;
3468
+ if (!run2.sessionId) run2.sessionId = sessionId;
3469
+ run2.superseded = true;
3470
+ }
3088
3471
  /**
3089
3472
  * 同一会话来了新指令:等待中的那一步按"没批准"处理(spec E9,和窗口里弹框时直接打字一样)。
3090
3473
  * 🔴 必须在新指令【入队之前】调:hold 模式下旧进程还占着这个会话的串行队列,
@@ -3167,7 +3550,7 @@ var ApprovalCoordinator = class {
3167
3550
  const got = await this.#d.backend.sync(ids.slice(i, i + 50));
3168
3551
  for (const g of got) {
3169
3552
  if (g.decision === "cancelled") this.#markCancelled(g.approvalId);
3170
- else this.onDecision(g.approvalId, g.decision, g.deviceName);
3553
+ else this.onDecision(g.approvalId, g.decision, g.deviceName, g.answers);
3171
3554
  }
3172
3555
  } catch (e) {
3173
3556
  this.#d.log(`\u540C\u6B65\u6388\u6743\u72B6\u6001\u5931\u8D25\uFF1A${e.message}`);
@@ -3258,13 +3641,15 @@ function tokenEquals(a, b) {
3258
3641
  }
3259
3642
  var GateServer = class {
3260
3643
  #handler;
3644
+ #notify;
3261
3645
  #token = randomBytes3(32).toString("hex");
3262
3646
  #server = null;
3263
3647
  #inflight = 0;
3264
3648
  /** 自动升级排空中:所有请求直接 503(远程 gate → exit 2 拦下;窗口 gate → 退回窗口里本地确认) */
3265
3649
  refusing = false;
3266
- constructor(handler) {
3650
+ constructor(handler, notify = null) {
3267
3651
  this.#handler = handler;
3652
+ this.#notify = notify;
3268
3653
  }
3269
3654
  get token() {
3270
3655
  return this.#token;
@@ -3276,9 +3661,9 @@ var GateServer = class {
3276
3661
  /** 起监听并写 gate.json。返回实际端口 */
3277
3662
  async start() {
3278
3663
  const server = createServer((req2, res) => void this.#serve(req2, res));
3279
- await new Promise((resolve6, reject) => {
3664
+ await new Promise((resolve7, reject) => {
3280
3665
  server.once("error", reject);
3281
- server.listen(0, "127.0.0.1", () => resolve6());
3666
+ server.listen(0, "127.0.0.1", () => resolve7());
3282
3667
  });
3283
3668
  this.#server = server;
3284
3669
  const addr = server.address();
@@ -3299,7 +3684,7 @@ var GateServer = class {
3299
3684
  }
3300
3685
  const s = this.#server;
3301
3686
  this.#server = null;
3302
- if (s) await new Promise((resolve6) => s.close(() => resolve6()));
3687
+ if (s) await new Promise((resolve7) => s.close(() => resolve7()));
3303
3688
  }
3304
3689
  async #serve(req2, res) {
3305
3690
  const reply = (status, body2) => {
@@ -3329,6 +3714,14 @@ var GateServer = class {
3329
3714
  return reply(400);
3330
3715
  }
3331
3716
  if (!parsed.hook || typeof parsed.hook !== "object") return reply(400);
3717
+ if (parsed.mode === "notify") {
3718
+ if (typeof parsed.tool !== "string" || !/^[a-z_]{1,32}$/.test(parsed.tool)) return reply(400);
3719
+ try {
3720
+ this.#notify?.({ tool: parsed.tool, hook: parsed.hook });
3721
+ } catch {
3722
+ }
3723
+ return reply(200, { decision: "none" });
3724
+ }
3332
3725
  const window = parsed.mode === "window";
3333
3726
  if (!window && typeof parsed.runId !== "string") return reply(400);
3334
3727
  const ac = new AbortController();
@@ -3411,7 +3804,7 @@ function buildHandshakeUrl(cfg, opts = {}) {
3411
3804
  }
3412
3805
 
3413
3806
  // src/daemon/log.ts
3414
- import { appendFileSync, mkdirSync as mkdirSync12, statSync as statSync4, truncateSync } from "fs";
3807
+ import { appendFileSync, mkdirSync as mkdirSync12, statSync as statSync5, truncateSync } from "fs";
3415
3808
  import { join as join16 } from "path";
3416
3809
  var MAX_LOG_BYTES = 2 * 1024 * 1024;
3417
3810
  function logPath() {
@@ -3419,7 +3812,7 @@ function logPath() {
3419
3812
  }
3420
3813
  function rotateIfNeeded(path) {
3421
3814
  try {
3422
- if (statSync4(path).size > MAX_LOG_BYTES) truncateSync(path, 0);
3815
+ if (statSync5(path).size > MAX_LOG_BYTES) truncateSync(path, 0);
3423
3816
  } catch {
3424
3817
  }
3425
3818
  }
@@ -3468,118 +3861,1375 @@ var SessionQueue = class {
3468
3861
  });
3469
3862
  return next;
3470
3863
  }
3471
- /** 这个会话有没有在跑 / 在排队的任务 */
3472
- busy(sessionId) {
3473
- return this.#tails.has(sessionId || "");
3864
+ /** 这个会话有没有在跑 / 在排队的任务 */
3865
+ busy(sessionId) {
3866
+ return this.#tails.has(sessionId || "");
3867
+ }
3868
+ /** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
3869
+ get activeSessions() {
3870
+ return this.#tails.size;
3871
+ }
3872
+ /** 等所有会话都排空。只给测试和优雅退出用 */
3873
+ async drain() {
3874
+ while (this.#tails.size > 0) {
3875
+ await Promise.all([...this.#tails.values()]);
3876
+ }
3877
+ }
3878
+ };
3879
+
3880
+ // src/daemon/resolve.ts
3881
+ function resolveTarget(cmd, deps) {
3882
+ if (cmd.sessionId) {
3883
+ const sessionId = deps.alias ? deps.alias(cmd.sessionId) : cmd.sessionId;
3884
+ const state = deps.readState(sessionId);
3885
+ if (!state) return { ok: false, reason: "session_gone" };
3886
+ if (!state.cwd) return { ok: false, reason: "no_cwd" };
3887
+ if (deps.windowOpen?.(state)) return { ok: false, reason: "window_open" };
3888
+ return { ok: true, sessionId, cwd: state.cwd };
3889
+ }
3890
+ let dirCwd = "";
3891
+ if (cmd.dirRef) {
3892
+ dirCwd = deps.dirOf ? deps.dirOf(cmd.dirRef) : "";
3893
+ if (!dirCwd) return { ok: false, reason: "dir_gone" };
3894
+ }
3895
+ const cwd = dirCwd || deps.remoteCwd();
3896
+ if (!cwd) return { ok: false, reason: "no_remote_cwd" };
3897
+ return { ok: true, sessionId: "", cwd };
3898
+ }
3899
+ function failureMessage(reason) {
3900
+ switch (reason) {
3901
+ case "session_gone":
3902
+ return "\u8FD9\u4E2A\u4F1A\u8BDD\u5DF2\u7ECF\u4E0D\u5728\u4E86";
3903
+ case "no_cwd":
3904
+ return "\u8FD9\u4E2A\u4F1A\u8BDD\u9700\u8981\u66F4\u65B0 DeskBuddy \u540E\u624D\u80FD\u8FDC\u7A0B\u64CD\u4F5C";
3905
+ case "no_remote_cwd":
3906
+ return "\u8FD8\u6CA1\u8BBE\u7F6E\u8FDC\u7A0B\u4EFB\u52A1\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u8DD1\u4E00\u6B21 deskbuddy setup";
3907
+ case "window_open":
3908
+ return "\u8FD9\u4E2A\u4EFB\u52A1\u6B63\u5728\u7535\u8111\u4E0A\u6253\u5F00\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u7EE7\u7EED\uFF0C\u6216\u65B0\u5EFA\u4EFB\u52A1";
3909
+ case "dir_gone":
3910
+ return "\u7535\u8111\u4E0A\u627E\u4E0D\u5230\u8FD9\u4E2A\u76EE\u5F55\u4E86\uFF0C\u8BF7\u6362\u4E00\u4E2A\u76EE\u5F55";
3911
+ }
3912
+ }
3913
+
3914
+ // src/daemon/seen.ts
3915
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync11 } from "fs";
3916
+ import { join as join17 } from "path";
3917
+ var SEEN_CAPACITY = 200;
3918
+ function seenPath() {
3919
+ return join17(deskbuddyHome(), "seen.json");
3920
+ }
3921
+ var SeenIds = class _SeenIds {
3922
+ #ids = [];
3923
+ #set = /* @__PURE__ */ new Set();
3924
+ constructor(initial = []) {
3925
+ for (const id of initial) this.#push(id);
3926
+ }
3927
+ #push(id) {
3928
+ if (this.#set.has(id)) return;
3929
+ this.#ids.push(id);
3930
+ this.#set.add(id);
3931
+ while (this.#ids.length > SEEN_CAPACITY) {
3932
+ const dropped = this.#ids.shift();
3933
+ if (dropped !== void 0) this.#set.delete(dropped);
3934
+ }
3935
+ }
3936
+ has(id) {
3937
+ return this.#set.has(id);
3938
+ }
3939
+ /** 记下来。返回 true 表示这是**新的**(之前没见过) */
3940
+ remember(id) {
3941
+ if (this.#set.has(id)) return false;
3942
+ this.#push(id);
3943
+ return true;
3944
+ }
3945
+ snapshot() {
3946
+ return [...this.#ids];
3947
+ }
3948
+ static load() {
3949
+ try {
3950
+ const raw = JSON.parse(readFileSync12(seenPath(), "utf8"));
3951
+ return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
3952
+ } catch {
3953
+ return new _SeenIds();
3954
+ }
3955
+ }
3956
+ save() {
3957
+ try {
3958
+ mkdirSync13(deskbuddyHome(), { recursive: true });
3959
+ const tmp = seenPath() + ".tmp";
3960
+ writeFileSync11(tmp, JSON.stringify({ ids: this.#ids }), "utf8");
3961
+ renameSync9(tmp, seenPath());
3962
+ } catch {
3963
+ }
3964
+ }
3965
+ };
3966
+
3967
+ // src/daemon/qwenworkBridge.ts
3968
+ import { readFileSync as readFileSync13 } from "fs";
3969
+ import { homedir as homedir7 } from "os";
3970
+ import { basename as basename7, dirname as dirname9, join as join18, resolve as resolve3 } from "path";
3971
+ var SUBMIT_TOOL = "qwenwork_task_submit_response";
3972
+ var DETAIL_TOOL = "qwenwork_task_get_detail";
3973
+ var POLL_MS = 2e3;
3974
+ var REQUEST_TIMEOUT_MS = 5e3;
3975
+ var EARLY_WINDOW_MS = 10 * 1e3;
3976
+ var EARLY_RETRY_MS = 1e3;
3977
+ var EARLY_RETRIES = 3;
3978
+ var SEEN_TTL_MS = 60 * 60 * 1e3;
3979
+ var DENY_MESSAGE = "\u7528\u6237\u5728 DeskBuddy \u8BBE\u5907\u4E0A\u62D2\u7EDD\u4E86\u8FD9\u4E00\u6B65\uFF0C\u4E0D\u8981\u91CD\u8BD5\u8FD9\u4E2A\u64CD\u4F5C";
3980
+ var TERMINAL_STATUS = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "canceled", "error", "stopped", "aborted"]);
3981
+ var PENDING_STATES = /* @__PURE__ */ new Set(["call", "approval-requested", "awaiting-approval", "waiting-for-confirmation", "pending-confirmation"]);
3982
+ var DETAIL_LIMIT = 5;
3983
+ var NO_ID_DEDUPE_MS = 5 * 1e3;
3984
+ var TOOL_DONE_STATES = /* @__PURE__ */ new Set(["output-available", "output-error", "output-denied", "result", "error", "denied", "rejected"]);
3985
+ function isResolvedElsewhere(text) {
3986
+ return /already been resolved|no pending/i.test(text);
3987
+ }
3988
+ function isLoopbackHttp(url) {
3989
+ try {
3990
+ const u = new URL(url);
3991
+ return (u.protocol === "http:" || u.protocol === "https:") && ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
3992
+ } catch {
3993
+ return false;
3994
+ }
3995
+ }
3996
+ function parseMcpConf(raw) {
3997
+ if (!raw) return null;
3998
+ let c;
3999
+ try {
4000
+ c = JSON.parse(raw);
4001
+ } catch {
4002
+ return null;
4003
+ }
4004
+ const url = typeof c.url === "string" ? c.url : "";
4005
+ if (!url || !isLoopbackHttp(url)) return null;
4006
+ const h = c.headers && typeof c.headers === "object" ? c.headers : {};
4007
+ const key = typeof h["x-api-key"] === "string" && h["x-api-key"] ? h["x-api-key"] : typeof c.token === "string" ? c.token : "";
4008
+ if (!key) return null;
4009
+ return { url, key };
4010
+ }
4011
+ function sameDir(a, b) {
4012
+ return process.platform === "linux" ? a === b : a.toLowerCase() === b.toLowerCase();
4013
+ }
4014
+ function chatIdOf(cwd, root) {
4015
+ if (typeof cwd !== "string" || !cwd) return null;
4016
+ const dir = resolve3(cwd);
4017
+ if (!sameDir(dirname9(dir), resolve3(root))) return null;
4018
+ const id = basename7(dir);
4019
+ return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : null;
4020
+ }
4021
+ function submitAnswers(toolInput, lists) {
4022
+ const qs = toolInput?.questions;
4023
+ if (!Array.isArray(qs) || !Array.isArray(lists) || lists.length !== qs.length) return null;
4024
+ const out = {};
4025
+ for (let i = 0; i < qs.length; i++) {
4026
+ const q = qs[i] ?? {};
4027
+ const items = lists[i];
4028
+ if (typeof q.question !== "string" || !q.question || !Array.isArray(items) || items.length === 0) return null;
4029
+ if (!items.every((x) => typeof x === "string")) return null;
4030
+ out[q.question] = items.join(", ");
4031
+ }
4032
+ return out;
4033
+ }
4034
+ function parseRpcBody(text, contentType) {
4035
+ const tryJson = (s) => {
4036
+ try {
4037
+ const v = JSON.parse(s);
4038
+ return v && typeof v === "object" ? v : null;
4039
+ } catch {
4040
+ return null;
4041
+ }
4042
+ };
4043
+ if (!/event-stream/i.test(contentType)) return tryJson(text);
4044
+ let last = null;
4045
+ for (const line of text.split("\n")) {
4046
+ if (!line.startsWith("data:")) continue;
4047
+ const v = tryJson(line.slice(5).trim());
4048
+ if (v && (v.result !== void 0 || v.error !== void 0)) last = v;
4049
+ }
4050
+ return last;
4051
+ }
4052
+ var QwenWorkMcp = class {
4053
+ #read;
4054
+ #fetch;
4055
+ #log;
4056
+ /** 已核对过工具清单的那个 url + key */
4057
+ #checked = null;
4058
+ #session = "";
4059
+ #nextId = 1;
4060
+ constructor(read, fetchFn, log2) {
4061
+ this.#read = read;
4062
+ this.#fetch = fetchFn;
4063
+ this.#log = log2;
4064
+ }
4065
+ async #rpc(conf, method, params, notification = false) {
4066
+ const headers = {
4067
+ "content-type": "application/json",
4068
+ accept: "application/json, text/event-stream",
4069
+ "x-api-key": conf.key
4070
+ };
4071
+ if (this.#session) headers["mcp-session-id"] = this.#session;
4072
+ const msg = { jsonrpc: "2.0", method, ...params ? { params } : {} };
4073
+ if (!notification) msg.id = this.#nextId++;
4074
+ const ac = new AbortController();
4075
+ const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
4076
+ timer.unref?.();
4077
+ try {
4078
+ const r = await this.#fetch(conf.url, { method: "POST", headers, body: JSON.stringify(msg), signal: ac.signal });
4079
+ const sid = r.headers.get("mcp-session-id");
4080
+ if (sid) this.#session = sid;
4081
+ const text = await r.text();
4082
+ if (r.status === 202 && notification) return { ok: true, body: null };
4083
+ if (r.status < 200 || r.status >= 300) return { ok: false, message: `HTTP ${r.status}` };
4084
+ if (notification) return { ok: true, body: null };
4085
+ return { ok: true, body: parseRpcBody(text, r.headers.get("content-type") ?? "") };
4086
+ } catch (e) {
4087
+ return { ok: false, message: e?.name === "AbortError" ? "timeout" : String(e?.message ?? e) };
4088
+ } finally {
4089
+ clearTimeout(timer);
4090
+ }
4091
+ }
4092
+ /** 在途的握手(同一 url + key 只握一次) */
4093
+ #handshake = null;
4094
+ /** 连得上且有 submit_response。返回 config(给后续调用用),不行返回失败原因 */
4095
+ async ensure() {
4096
+ const conf = parseMcpConf(this.#read());
4097
+ if (!conf) return { ok: false, why: "no_config", message: "\u8BFB\u4E0D\u5230\u5343\u95EE\u529E\u516C\u7684 MCP \u914D\u7F6E\uFF08\u6216\u5730\u5740\u4E0D\u662F\u672C\u673A\u56DE\u73AF\uFF09" };
4098
+ const c = this.#checked;
4099
+ if (c && c.url === conf.url && c.key === conf.key) {
4100
+ return c.ok ? { ok: true, conf } : { ok: false, why: "unsupported", message: `\u5343\u95EE\u529E\u516C MCP \u6CA1\u6709 ${SUBMIT_TOOL}` };
4101
+ }
4102
+ const h = this.#handshake;
4103
+ if (h && h.url === conf.url && h.key === conf.key) return h.p;
4104
+ const p = this.#doHandshake(conf);
4105
+ this.#handshake = { url: conf.url, key: conf.key, p };
4106
+ try {
4107
+ return await p;
4108
+ } finally {
4109
+ if (this.#handshake?.p === p) this.#handshake = null;
4110
+ }
4111
+ }
4112
+ async #doHandshake(conf) {
4113
+ this.#session = "";
4114
+ const init = await this.#rpc(conf, "initialize", {
4115
+ protocolVersion: "2025-03-26",
4116
+ capabilities: {},
4117
+ clientInfo: { name: "deskbuddy", version: "1" }
4118
+ });
4119
+ if (!init.ok) return { ok: false, why: "network", message: `\u8FDE\u4E0D\u4E0A\u5343\u95EE\u529E\u516C MCP\uFF1A${init.message}` };
4120
+ if (!init.body || init.body.error) return { ok: false, why: "network", message: "\u5343\u95EE\u529E\u516C MCP \u521D\u59CB\u5316\u5931\u8D25" };
4121
+ await this.#rpc(conf, "notifications/initialized", void 0, true);
4122
+ const list = await this.#rpc(conf, "tools/list", {});
4123
+ if (!list.ok) return { ok: false, why: "network", message: `\u5343\u95EE\u529E\u516C MCP \u5DE5\u5177\u6E05\u5355\u53D6\u4E0D\u5230\uFF1A${list.message}` };
4124
+ const tools = list.body?.result?.tools ?? [];
4125
+ const ok = Array.isArray(tools) && tools.some((t) => t?.name === SUBMIT_TOOL);
4126
+ this.#checked = { url: conf.url, key: conf.key, ok };
4127
+ if (!ok) {
4128
+ this.#log(`\u5343\u95EE\u529E\u516C MCP \u6CA1\u6709 ${SUBMIT_TOOL}\uFF0C\u9009\u62E9\u9898 / \u6388\u6743\u4E0D\u63A8\u8BBE\u5907\uFF0C\u53EA\u4E0A\u62A5\u72B6\u6001`);
4129
+ return { ok: false, why: "unsupported", message: `\u5343\u95EE\u529E\u516C MCP \u6CA1\u6709 ${SUBMIT_TOOL}` };
4130
+ }
4131
+ return { ok: true, conf };
4132
+ }
4133
+ async call(name, args) {
4134
+ const e = await this.ensure();
4135
+ if (!e.ok) return e;
4136
+ const r = await this.#rpc(e.conf, "tools/call", { name, arguments: args });
4137
+ if (!r.ok) {
4138
+ this.#checked = null;
4139
+ return { ok: false, why: "network", message: r.message };
4140
+ }
4141
+ const body = r.body;
4142
+ if (!body) return { ok: false, why: "rpc", message: "\u56DE\u8BDD\u770B\u4E0D\u61C2" };
4143
+ if (body.error) {
4144
+ const m = body.error?.message;
4145
+ this.#checked = null;
4146
+ return { ok: false, why: "rpc", message: typeof m === "string" ? m : "error" };
4147
+ }
4148
+ const res = body.result ?? {};
4149
+ const content = Array.isArray(res.content) ? res.content : [];
4150
+ const text = content.map((c) => typeof c?.text === "string" ? c.text : "").join("\n");
4151
+ return { ok: true, text, isError: res.isError === true };
4152
+ }
4153
+ };
4154
+ var QwenWorkBridge = class {
4155
+ #d;
4156
+ #mcp;
4157
+ /** chatId → 正在等设备的那张卡(同一 chatId 同时只推一张) */
4158
+ #cards = /* @__PURE__ */ new Map();
4159
+ /** 推过的 (chatId, toolUseId) → 时间:同一道题 / 同一个授权框只推一次 */
4160
+ #seen = /* @__PURE__ */ new Map();
4161
+ /** 没带 toolUseId 的推送(只为日志限频) */
4162
+ #noIdLogged = /* @__PURE__ */ new Map();
4163
+ #timer = null;
4164
+ #polling = false;
4165
+ #stopped = false;
4166
+ constructor(deps) {
4167
+ this.#d = deps;
4168
+ const read = deps.readConfig ?? (() => readConfigFile(join18(qwenworkHome(), "mcp-adaptor.config")));
4169
+ const f = deps.fetch ?? globalThis.fetch;
4170
+ this.#mcp = new QwenWorkMcp(read, f, deps.log);
4171
+ }
4172
+ #now() {
4173
+ return this.#d.now ? this.#d.now() : Date.now();
4174
+ }
4175
+ /** 只给测试和诊断用 */
4176
+ get pending() {
4177
+ return this.#cards.size;
4178
+ }
4179
+ stop() {
4180
+ this.#stopped = true;
4181
+ if (this.#timer) clearInterval(this.#timer);
4182
+ this.#timer = null;
4183
+ this.#abortAll();
4184
+ }
4185
+ #abortAll() {
4186
+ for (const c of this.#cards.values()) c.ac.abort();
4187
+ this.#cards.clear();
4188
+ }
4189
+ /** hook 发来的 notify(GateServer mode=notify)。不抛错 */
4190
+ onNotify(req2) {
4191
+ void this.#onNotify(req2).catch((e) => this.#d.log(`\u5343\u95EE\u529E\u516C\u63A8\u5361\u51FA\u9519\uFF1A${e?.message ?? e}`));
4192
+ }
4193
+ async #onNotify(req2) {
4194
+ if (this.#stopped || req2.tool !== "qwenwork") return;
4195
+ if (this.#d.enabled && !this.#d.enabled()) return;
4196
+ const h = req2.hook;
4197
+ const event = h.hook_event_name;
4198
+ const sessionId = typeof h.session_id === "string" ? h.session_id : "";
4199
+ if (!sessionId) return;
4200
+ let kind;
4201
+ let toolName;
4202
+ let toolInput;
4203
+ let toolUseId;
4204
+ if (event === "PreToolUse" && h.tool_name === "AskUserQuestion") {
4205
+ kind = "question";
4206
+ toolName = "AskUserQuestion";
4207
+ toolInput = h.tool_input;
4208
+ toolUseId = typeof h.tool_use_id === "string" ? h.tool_use_id : "";
4209
+ } else if (event === "Notification" && h.notification_type === "permission_prompt") {
4210
+ const det = h.details && typeof h.details === "object" ? h.details : {};
4211
+ kind = "permission";
4212
+ toolName = typeof det.toolName === "string" ? det.toolName : "";
4213
+ toolInput = det.input && typeof det.input === "object" ? det.input : {};
4214
+ toolUseId = typeof det.toolUseId === "string" ? det.toolUseId : "";
4215
+ if (!toolName) return;
4216
+ } else return;
4217
+ const root = this.#d.workspaceRoot ?? join18(homedir7(), ".qwenworkcn", "workspace");
4218
+ const chatId = chatIdOf(h.cwd, root);
4219
+ if (!chatId) {
4220
+ this.#d.log("\u5343\u95EE\u529E\u516C\uFF1A\u8FD9\u9053\u9898 / \u6388\u6743\u4E0D\u5728\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF08\u53EF\u80FD\u662F\u5B50\u4EE3\u7406\uFF09\uFF0C\u4E0D\u63A8\u8BBE\u5907");
4221
+ return;
4222
+ }
4223
+ if (!toolUseId) {
4224
+ const fp = `${chatId}:${kind}`;
4225
+ const last = this.#noIdLogged.get(fp) ?? 0;
4226
+ if (this.#now() - last > NO_ID_DEDUPE_MS) this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${chatId.slice(0, 8)} \u7684${kind === "question" ? "\u9009\u62E9\u9898" : "\u6388\u6743"}\u6CA1\u5E26 toolUseId\uFF0C\u4E0D\u63A8\u8BBE\u5907`);
4227
+ this.#noIdLogged.set(fp, this.#now());
4228
+ return;
4229
+ }
4230
+ const key = `${chatId}:${toolUseId}`;
4231
+ this.#prune();
4232
+ if (this.#seen.has(key)) return;
4233
+ this.#seen.set(key, this.#now());
4234
+ this.#replaceOld(chatId, toolUseId);
4235
+ const e = await this.#mcp.ensure();
4236
+ if (!e.ok) {
4237
+ if (e.why !== "unsupported") this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A${e.message}\uFF0C\u8FD9\u4E00\u6761\u4E0D\u63A8\u8BBE\u5907`);
4238
+ return;
4239
+ }
4240
+ if (this.#stopped) return;
4241
+ this.#replaceOld(chatId, toolUseId);
4242
+ const card = { key, chatId, toolUseId, kind, toolInput, ac: new AbortController(), createdAt: this.#now() };
4243
+ this.#cards.set(chatId, card);
4244
+ this.#ensurePoll();
4245
+ const gateReq = {
4246
+ window: true,
4247
+ tool: "qwenwork",
4248
+ hook: {
4249
+ hook_event_name: "PermissionRequest",
4250
+ session_id: sessionId,
4251
+ cwd: h.cwd,
4252
+ tool_name: toolName,
4253
+ tool_input: toolInput,
4254
+ ...kind === "question" && typeof h.transcript_path === "string" ? { transcript_path: h.transcript_path } : {}
4255
+ }
4256
+ };
4257
+ let reply;
4258
+ try {
4259
+ reply = await this.#d.gate(gateReq, card.ac.signal);
4260
+ } catch {
4261
+ if (this.#cards.get(chatId) === card) this.#cards.delete(chatId);
4262
+ return;
4263
+ }
4264
+ if (this.#cards.get(chatId) !== card || card.ac.signal.aborted) return;
4265
+ this.#cards.delete(chatId);
4266
+ await this.#submit(card, reply);
4267
+ }
4268
+ #replaceOld(chatId, toolUseId) {
4269
+ const old = this.#cards.get(chatId);
4270
+ if (!old || old.toolUseId === toolUseId) return;
4271
+ this.#cards.delete(chatId);
4272
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${chatId.slice(0, 8)} \u51FA\u73B0\u65B0\u7684\u4E00\u6B65\uFF0C\u64A4\u6389\u65E7\u5361`);
4273
+ old.ac.abort();
4274
+ }
4275
+ /**
4276
+ * 提交前核对:界面上此刻在等的正是这张卡那一步(等待类状态的工具调用恰好一条、且 toolCallId 对得上)。
4277
+ * 对不上 / 判断不了一律返回原因(不提交)
4278
+ */
4279
+ async #verifyPending(card) {
4280
+ const r = await this.#mcp.call(DETAIL_TOOL, { chatId: card.chatId, limit: DETAIL_LIMIT });
4281
+ if (!r.ok) return `\u67E5\u4E0D\u5230\u4EFB\u52A1\u8BE6\u60C5\uFF08${r.why}\uFF09`;
4282
+ return pendingMismatch(r.text, card.toolUseId);
4283
+ }
4284
+ async #submit(card, reply) {
4285
+ let args = null;
4286
+ if (card.kind === "question") {
4287
+ if (reply.decision !== "allow") return;
4288
+ const answers = submitAnswers(card.toolInput, reply.answerLists);
4289
+ if (!answers) {
4290
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u7684\u8BBE\u5907\u7B54\u6848\u62FC\u4E0D\u51FA\u6765\uFF0C\u754C\u9762\u7559\u7ED9\u4EBA\u7B54`);
4291
+ return;
4292
+ }
4293
+ args = { chatId: card.chatId, action: "answer", answers };
4294
+ } else if (reply.decision === "allow") {
4295
+ args = { chatId: card.chatId, action: "approve" };
4296
+ } else if (reply.decision === "deny") {
4297
+ args = { chatId: card.chatId, action: "deny", message: DENY_MESSAGE };
4298
+ } else return;
4299
+ if (!card.toolUseId) return;
4300
+ for (let attempt = 0; ; attempt++) {
4301
+ const why = await this.#verifyPending(card);
4302
+ if (why) {
4303
+ const early = this.#now() - card.createdAt < EARLY_WINDOW_MS && attempt < EARLY_RETRIES && /没有在等/.test(why);
4304
+ if (early) {
4305
+ await sleep(this.#d.retryMs ?? EARLY_RETRY_MS);
4306
+ continue;
4307
+ }
4308
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} ${why}\uFF0C\u8BBE\u5907\u7B54\u590D\u4E0D\u56DE\u586B\uFF08\u6309\u7535\u8111\u5DF2\u5904\u7406\uFF09`);
4309
+ return;
4310
+ }
4311
+ const r = await this.#mcp.call(SUBMIT_TOOL, args);
4312
+ if (!r.ok) {
4313
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u56DE\u586B\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u5931\u8D25\uFF08${r.why}${r.message ? " " + r.message.slice(0, 80) : ""}\uFF09\uFF0C\u754C\u9762\u7559\u7ED9\u4EBA\u7B54`);
4314
+ return;
4315
+ }
4316
+ if (isResolvedElsewhere(r.text)) {
4317
+ const early = /no pending/i.test(r.text) && this.#now() - card.createdAt < EARLY_WINDOW_MS && attempt < EARLY_RETRIES;
4318
+ if (early) {
4319
+ await sleep(this.#d.retryMs ?? EARLY_RETRY_MS);
4320
+ continue;
4321
+ }
4322
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u7535\u8111\u4E0A\u5DF2\u5904\u7406\uFF0C\u8BBE\u5907\u7B54\u590D\u4E0D\u518D\u56DE\u586B`);
4323
+ return;
4324
+ }
4325
+ if (r.isError) {
4326
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u56DE\u586B\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u88AB\u62D2\uFF1A${r.text.slice(0, 80)}`);
4327
+ return;
4328
+ }
4329
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u5DF2\u6309\u8BBE\u5907\u7B54\u590D\u56DE\u586B\uFF08${card.kind}\uFF09`);
4330
+ return;
4331
+ }
4332
+ }
4333
+ #prune() {
4334
+ const now = this.#now();
4335
+ for (const [k, at] of this.#seen) if (now - at > SEEN_TTL_MS) this.#seen.delete(k);
4336
+ for (const [k, at] of this.#noIdLogged) if (now - at > NO_ID_DEDUPE_MS) this.#noIdLogged.delete(k);
4337
+ }
4338
+ #ensurePoll() {
4339
+ if (this.#timer || this.#stopped) return;
4340
+ this.#timer = setInterval(() => void this.poll(), this.#d.pollMs ?? POLL_MS);
4341
+ this.#timer.unref?.();
4342
+ }
4343
+ /**
4344
+ * 待答期间查 get_detail:任务已不在跑、或这一步已经有结果(电脑上答过 / 批过 / 拒过)就撤卡。
4345
+ * 看不出来就不动(最迟靠 coordinator.scanWindows 在 PostToolUse / Stop 时撤)。MCP 连不上 → 全部撤卡
4346
+ */
4347
+ async poll() {
4348
+ if (this.#polling) return;
4349
+ if (this.#cards.size === 0) {
4350
+ if (this.#timer) clearInterval(this.#timer);
4351
+ this.#timer = null;
4352
+ return;
4353
+ }
4354
+ this.#polling = true;
4355
+ try {
4356
+ for (const card of [...this.#cards.values()]) {
4357
+ if (this.#cards.get(card.chatId) !== card) continue;
4358
+ const r = await this.#mcp.call(DETAIL_TOOL, { chatId: card.chatId, limit: DETAIL_LIMIT });
4359
+ if (!r.ok) {
4360
+ if (r.why === "network" || r.why === "no_config") {
4361
+ this.#d.log(`\u5343\u95EE\u529E\u516C MCP \u8FDE\u4E0D\u4E0A\uFF08${r.message.slice(0, 80)}\uFF09\uFF0C\u64A4\u6389\u8BBE\u5907\u4E0A\u7B49\u7740\u7684\u5361`);
4362
+ this.#abortAll();
4363
+ return;
4364
+ }
4365
+ continue;
4366
+ }
4367
+ if (this.#cards.get(card.chatId) !== card) continue;
4368
+ const settled = detailSettled(r.text, card.toolUseId);
4369
+ if (settled) {
4370
+ this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} ${settled}\uFF0C\u64A4\u5361`);
4371
+ this.#cards.delete(card.chatId);
4372
+ card.ac.abort();
4373
+ }
4374
+ }
4375
+ } finally {
4376
+ this.#polling = false;
4377
+ }
4378
+ }
4379
+ };
4380
+ function toolCallsOf(d) {
4381
+ const msgs = Array.isArray(d.messages) ? d.messages : [];
4382
+ return msgs.flatMap((m) => Array.isArray(m?.toolCalls) ? m.toolCalls : []);
4383
+ }
4384
+ function pendingIds(d) {
4385
+ return toolCallsOf(d).filter((c) => typeof c?.state === "string" && PENDING_STATES.has(c.state)).map((c) => typeof c.toolCallId === "string" ? c.toolCallId : "");
4386
+ }
4387
+ function pendingMismatch(text, toolUseId) {
4388
+ if (!toolUseId) return "\u6CA1\u6709 toolUseId\uFF0C\u6838\u5BF9\u4E0D\u4E86";
4389
+ let d;
4390
+ try {
4391
+ d = JSON.parse(text);
4392
+ } catch {
4393
+ return "\u4EFB\u52A1\u8BE6\u60C5\u770B\u4E0D\u61C2";
4394
+ }
4395
+ if (!d || typeof d !== "object" || d.success === false) return "\u4EFB\u52A1\u8BE6\u60C5\u770B\u4E0D\u61C2";
4396
+ const status = typeof d.status === "string" ? d.status.toLowerCase() : "";
4397
+ if (status && TERMINAL_STATUS.has(status)) return `\u4EFB\u52A1\u5DF2${status}`;
4398
+ const ids = pendingIds(d);
4399
+ if (ids.length === 0) {
4400
+ const mine = toolCallsOf(d).find((c) => c?.toolCallId === toolUseId);
4401
+ const st = mine && typeof mine.state === "string" ? mine.state : "\u672A\u627E\u5230";
4402
+ return `\u754C\u9762\u4E0A\u6CA1\u6709\u5728\u7B49\u7684\u4E00\u6B65\uFF08\u8FD9\u4E00\u6B65\u72B6\u6001\uFF1A${st.slice(0, 32)}\uFF09`;
4403
+ }
4404
+ if (ids.length > 1) return "\u754C\u9762\u4E0A\u540C\u65F6\u6709\u591A\u6B65\u5728\u7B49\uFF0C\u5206\u4E0D\u6E05";
4405
+ if (ids[0] !== toolUseId) return "\u754C\u9762\u4E0A\u5728\u7B49\u7684\u5DF2\u662F\u522B\u7684\u4E00\u6B65";
4406
+ return "";
4407
+ }
4408
+ function detailSettled(text, toolUseId) {
4409
+ let d;
4410
+ try {
4411
+ d = JSON.parse(text);
4412
+ } catch {
4413
+ return "";
4414
+ }
4415
+ if (!d || typeof d !== "object" || d.success === false) return "";
4416
+ const status = typeof d.status === "string" ? d.status.toLowerCase() : "";
4417
+ if (status && TERMINAL_STATUS.has(status)) return `\u4EFB\u52A1\u5DF2${status}`;
4418
+ if (!toolUseId) return "";
4419
+ for (const c of toolCallsOf(d)) {
4420
+ if (c?.toolCallId !== toolUseId) continue;
4421
+ const st = typeof c.state === "string" ? c.state : "";
4422
+ if (TOOL_DONE_STATES.has(st)) return `\u8FD9\u4E00\u6B65\u5DF2\u662F ${st}`;
4423
+ break;
4424
+ }
4425
+ const ids = pendingIds(d);
4426
+ if (ids.length > 0 && !ids.includes(toolUseId)) return "\u754C\u9762\u4E0A\u5728\u7B49\u7684\u5DF2\u662F\u522B\u7684\u4E00\u6B65";
4427
+ return "";
4428
+ }
4429
+ function readConfigFile(p) {
4430
+ try {
4431
+ return readFileSync13(p, "utf8");
4432
+ } catch {
4433
+ return null;
4434
+ }
4435
+ }
4436
+ function sleep(ms) {
4437
+ return new Promise((r) => setTimeout(r, ms).unref?.());
4438
+ }
4439
+
4440
+ // src/daemon/dumateWatch.ts
4441
+ import { execFileSync as execFileSync9, spawn as spawn3 } from "child_process";
4442
+ import { readdirSync as readdirSync4, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
4443
+ import { request as httpRequest } from "http";
4444
+ import { join as join19 } from "path";
4445
+
4446
+ // hook/ocTranslate.mjs
4447
+ function toCcTool(tool, args) {
4448
+ const a = args && typeof args === "object" ? args : {};
4449
+ const s = (v) => typeof v === "string" ? v : "";
4450
+ switch (tool) {
4451
+ case "bash":
4452
+ return { tool_name: "Bash", tool_input: { command: s(a.command) } };
4453
+ case "read":
4454
+ return { tool_name: "Read", tool_input: { file_path: s(a.filePath) } };
4455
+ case "edit":
4456
+ return { tool_name: "Edit", tool_input: { file_path: s(a.filePath) } };
4457
+ case "write":
4458
+ return { tool_name: "Write", tool_input: { file_path: s(a.filePath) } };
4459
+ case "glob":
4460
+ return { tool_name: "Glob", tool_input: { pattern: s(a.pattern), path: s(a.path) } };
4461
+ case "grep":
4462
+ return { tool_name: "Grep", tool_input: { pattern: s(a.pattern), path: s(a.path) } };
4463
+ case "list":
4464
+ return { tool_name: "LS", tool_input: { path: s(a.path) } };
4465
+ case "webfetch":
4466
+ return { tool_name: "WebFetch", tool_input: { url: s(a.url) } };
4467
+ case "task":
4468
+ return { tool_name: "Task", tool_input: {} };
4469
+ case "todowrite":
4470
+ case "todoread":
4471
+ return { tool_name: "TodoWrite", tool_input: {} };
4472
+ case "patch":
4473
+ case "apply_patch":
4474
+ return { tool_name: "apply_patch", tool_input: { patch: s(a.patchText) || s(a.patch) || s(a.input) } };
4475
+ default:
4476
+ return { tool_name: String(tool || ""), tool_input: a };
4477
+ }
4478
+ }
4479
+ function askToCc(props) {
4480
+ const md = props && props.metadata || {};
4481
+ if (typeof md.command === "string" && md.command) return { tool_name: "Bash", tool_input: { command: md.command } };
4482
+ const file = md.filepath || md.filePath;
4483
+ if (typeof file === "string" && file) return { tool_name: "Edit", tool_input: { file_path: file } };
4484
+ const patterns = Array.isArray(props && props.patterns) ? props.patterns.join(" ") : "";
4485
+ return { tool_name: String(props && props.permission || "permission"), tool_input: { path: patterns } };
4486
+ }
4487
+ function isPlainQuestion(props, callTool) {
4488
+ if (callTool !== "question") return false;
4489
+ const qs = props && Array.isArray(props.questions) ? props.questions : [];
4490
+ return !qs.some((q) => q && (q.header === "Build Agent" || typeof q.question === "string" && /^Plan at .* is complete\./.test(q.question)));
4491
+ }
4492
+ function questionToCc(props) {
4493
+ const qs = props && Array.isArray(props.questions) ? props.questions : null;
4494
+ if (!qs || qs.length === 0) return null;
4495
+ return {
4496
+ tool_name: "AskUserQuestion",
4497
+ tool_input: {
4498
+ questions: qs.map((q) => ({
4499
+ question: q && q.question,
4500
+ header: q && q.header || "",
4501
+ multiSelect: !!(q && q.multiple),
4502
+ options: q && Array.isArray(q.options) ? q.options.map((o) => ({ label: o && o.label, description: o && o.description || "" })) : [],
4503
+ ...q && q.custom === false ? { custom: false } : {}
4504
+ }))
4505
+ }
4506
+ };
4507
+ }
4508
+ var OC_DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
4509
+ function rememberTitle(titles, info) {
4510
+ if (!info || typeof info.id !== "string" || !info.id || info.parentID) return;
4511
+ const t = typeof info.title === "string" ? info.title.trim() : "";
4512
+ if (!t || OC_DEFAULT_TITLE_RE.test(t)) return;
4513
+ titles.set(info.id, t.slice(0, 100));
4514
+ }
4515
+
4516
+ // src/daemon/dumateWatch.ts
4517
+ var ACTIVE_DIR_MS = 6 * 60 * 60 * 1e3;
4518
+ var MAX_POLL_DIRS = 12;
4519
+ var SESSION_LIST_MS = 10 * 1e3;
4520
+ var RESUBSCRIBE_MS = 5 * 1e3;
4521
+ var BUSY_WAIT_MS = 400;
4522
+ var QUESTION_TIMEOUT_MS = 120 * 1e3;
4523
+ var MISSING_GRACE_MS = 2 * 1e3;
4524
+ var REQUEST_TIMEOUT_MS2 = 5 * 1e3;
4525
+ var HOOK_KILL_MS = 10 * 1e3;
4526
+ var HANDLED_TTL_MS = 60 * 60 * 1e3;
4527
+ var SCAN_DIRS_MS = SESSION_LIST_MS;
4528
+ var DISCOVER_BACKOFF_MS = 10 * 1e3;
4529
+ var BUSY_DIR_MS = 10 * 60 * 1e3;
4530
+ var DAY_MS = 24 * 60 * 60 * 1e3;
4531
+ function questionDeadline(deadlineAt, timeoutMs, now) {
4532
+ if (typeof deadlineAt === "number" && Number.isFinite(deadlineAt) && deadlineAt > 0) {
4533
+ const ms = deadlineAt < 1e12 ? deadlineAt * 1e3 : deadlineAt;
4534
+ if (Math.abs(ms - now) <= DAY_MS) return ms;
4535
+ }
4536
+ if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 1e3 && timeoutMs <= DAY_MS) return now + timeoutMs;
4537
+ return now + QUESTION_TIMEOUT_MS;
4538
+ }
4539
+ var realCache = /* @__PURE__ */ new Map();
4540
+ function dirKey(p) {
4541
+ let r = realCache.get(p);
4542
+ if (r === void 0) {
4543
+ try {
4544
+ r = realpathSync3(p);
4545
+ } catch {
4546
+ r = p;
4547
+ }
4548
+ if (realCache.size > 2e3) realCache.clear();
4549
+ realCache.set(p, r);
4550
+ }
4551
+ const t = r.length > 1 ? r.replace(/\/+$/, "") : r;
4552
+ return process.platform === "linux" ? t : t.toLowerCase();
4553
+ }
4554
+ function parseDumateServe(psOutput, uid) {
4555
+ for (const line of psOutput.split("\n")) {
4556
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
4557
+ if (!m) continue;
4558
+ const [, pid, u, args] = m;
4559
+ if (Number(u) !== uid) continue;
4560
+ if (!/(^|\/)dumate-opencode\s+serve(\s|$)/.test(args)) continue;
4561
+ const port = args.match(/--port[= ](\d{1,5})(\s|$)/);
4562
+ if (!port) continue;
4563
+ const host = args.match(/--hostname[= ](\S+)/);
4564
+ if (host && !["127.0.0.1", "localhost", "::1"].includes(host[1])) continue;
4565
+ const n = Number(port[1]);
4566
+ if (n <= 0 || n > 65535) continue;
4567
+ return { pid: Number(pid), port: n };
4568
+ }
4569
+ return null;
4570
+ }
4571
+ function parseInappKey(envOutput) {
4572
+ const m = envOutput.match(/(?:^|\s)DUMATE_INAPP_KEY=([^\s]+)/);
4573
+ return m ? m[1] : null;
4574
+ }
4575
+ var keyCache = null;
4576
+ function defaultDiscover() {
4577
+ if (process.platform === "win32" || typeof process.getuid !== "function") return null;
4578
+ try {
4579
+ const ps = execFileSync9("ps", ["-A", "-ww", "-o", "pid=,uid=,args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
4580
+ const hit = parseDumateServe(ps, process.getuid());
4581
+ if (!hit) return null;
4582
+ if (keyCache && keyCache.pid === hit.pid && keyCache.port === hit.port) return keyCache;
4583
+ const env = execFileSync9("ps", ["eww", "-p", String(hit.pid), "-o", "command="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
4584
+ const key = parseInappKey(env);
4585
+ keyCache = key ? { ...hit, key } : null;
4586
+ return keyCache;
4587
+ } catch {
4588
+ return null;
4589
+ }
4590
+ }
4591
+ function headersFor(t, directory) {
4592
+ return {
4593
+ "X-Dumate-Inapp-Key": t.key,
4594
+ // 实测 DuMate 会 decodeURIComponent 这个头:编码后中文目录名也能放进 HTTP 头
4595
+ ...directory ? { "x-opencode-directory": encodeURIComponent(directory) } : {}
4596
+ };
4597
+ }
4598
+ var defaultHttp = async (t, req2) => {
4599
+ const ac = new AbortController();
4600
+ const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS2);
4601
+ timer.unref?.();
4602
+ try {
4603
+ const r = await fetch(`http://127.0.0.1:${t.port}${req2.path}`, {
4604
+ method: req2.method,
4605
+ headers: { ...headersFor(t, req2.directory), ...req2.body !== void 0 ? { "content-type": "application/json" } : {} },
4606
+ ...req2.body !== void 0 ? { body: JSON.stringify(req2.body) } : {},
4607
+ signal: ac.signal
4608
+ });
4609
+ const text = await r.text();
4610
+ let body = null;
4611
+ try {
4612
+ body = text ? JSON.parse(text) : null;
4613
+ } catch {
4614
+ body = text;
4615
+ }
4616
+ return { status: r.status, body };
4617
+ } catch {
4618
+ return { status: 0, body: null };
4619
+ } finally {
4620
+ clearTimeout(timer);
4621
+ }
4622
+ };
4623
+ var defaultSubscribe = (t, path, directory, onEvent, onEnd) => {
4624
+ let ended = false;
4625
+ const end = (status) => {
4626
+ if (ended) return;
4627
+ ended = true;
4628
+ onEnd(status);
4629
+ };
4630
+ const req2 = httpRequest(
4631
+ { host: "127.0.0.1", port: t.port, path, method: "GET", headers: { accept: "text/event-stream", ...headersFor(t, directory) } },
4632
+ (res) => {
4633
+ if (res.statusCode !== 200) {
4634
+ res.resume();
4635
+ end(res.statusCode ?? 0);
4636
+ return;
4637
+ }
4638
+ res.setEncoding("utf8");
4639
+ let buf = "";
4640
+ res.on("data", (chunk) => {
4641
+ buf += chunk;
4642
+ let i;
4643
+ while ((i = buf.indexOf("\n")) >= 0) {
4644
+ const line = buf.slice(0, i).replace(/\r$/, "");
4645
+ buf = buf.slice(i + 1);
4646
+ if (!line.startsWith("data:")) continue;
4647
+ try {
4648
+ const v = JSON.parse(line.slice(5).trim());
4649
+ if (v && typeof v === "object") onEvent(v);
4650
+ } catch {
4651
+ }
4652
+ }
4653
+ if (buf.length > 4 * 1024 * 1024) buf = "";
4654
+ });
4655
+ res.on("end", () => end(200));
4656
+ res.on("error", () => end(0));
4657
+ }
4658
+ );
4659
+ req2.on("error", () => end(0));
4660
+ req2.end();
4661
+ return () => {
4662
+ ended = true;
4663
+ req2.destroy();
4664
+ };
4665
+ };
4666
+ function scanSessionDirs(root, now, maxAgeMs = ACTIVE_DIR_MS) {
4667
+ const out = [];
4668
+ const list = (p) => {
4669
+ try {
4670
+ return readdirSync4(p, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join19(p, d.name));
4671
+ } catch {
4672
+ return [];
4673
+ }
4674
+ };
4675
+ for (const u of list(root))
4676
+ for (const day of list(u).sort().reverse().slice(0, 2))
4677
+ for (const dir of list(day)) {
4678
+ try {
4679
+ const at = statSync6(dir).mtimeMs;
4680
+ if (now - at <= maxAgeMs) out.push({ dir, at });
4681
+ } catch {
4682
+ }
4683
+ }
4684
+ return out.sort((a, b) => b.at - a.at).map((x) => x.dir);
4685
+ }
4686
+ var DumateWatch = class {
4687
+ #d;
4688
+ #target = null;
4689
+ #stopped = false;
4690
+ /** 订阅:'' = /global/event;否则按目录 */
4691
+ #subs = /* @__PURE__ */ new Map();
4692
+ #subEndedAt = 0;
4693
+ /** /global/event 不可用(404),退到按目录订阅 */
4694
+ #globalUnsupported = false;
4695
+ #questionUnsupported = false;
4696
+ #permissionUnsupported = false;
4697
+ #reconciling = false;
4698
+ #sessionListAt = 0;
4699
+ /** 目录 → 最近一次看到它活动的时间 */
4700
+ /** dirKey → 原写法 + 最近一次看到它活动的时间 */
4701
+ #dirs = /* @__PURE__ */ new Map();
4702
+ /** dirKey → 最近一次忙(busy / 工具 / 提问)的时间 */
4703
+ #busyDirs = /* @__PURE__ */ new Map();
4704
+ /** 当前这条 /global/event 已经收到过带目录的会话事件:之后只轮询有卡 / 最近忙过的目录 */
4705
+ #sseVerified = false;
4706
+ #scanAt = 0;
4707
+ #discoverMissAt = 0;
4708
+ #sessionDir = /* @__PURE__ */ new Map();
4709
+ #titles = /* @__PURE__ */ new Map();
4710
+ #children = /* @__PURE__ */ new Set();
4711
+ #userMsgs = /* @__PURE__ */ new Map();
4712
+ #promptedMsgs = /* @__PURE__ */ new Set();
4713
+ /** callID → 工具名 / cc 同构参数(授权卡用真实参数,选择题判来源) */
4714
+ #calls = /* @__PURE__ */ new Map();
4715
+ #callReported = /* @__PURE__ */ new Map();
4716
+ /** 本轮已报过 UserPromptSubmit 的会话 */
4717
+ #turnOpen = /* @__PURE__ */ new Set();
4718
+ /** 报过「进行中」类事件、还没报 Stop 的会话 */
4719
+ #active = /* @__PURE__ */ new Set();
4720
+ #busyTimers = /* @__PURE__ */ new Map();
4721
+ #lastUserText = /* @__PURE__ */ new Map();
4722
+ #cards = /* @__PURE__ */ new Map();
4723
+ /** 推过 / 不推的请求 id → 时间:同一个只处理一次 */
4724
+ #handled = /* @__PURE__ */ new Map();
4725
+ #reportChain = Promise.resolve();
4726
+ constructor(deps) {
4727
+ this.#d = deps;
4728
+ }
4729
+ #now() {
4730
+ return this.#d.now ? this.#d.now() : Date.now();
4731
+ }
4732
+ get connected() {
4733
+ return this.#target !== null;
4734
+ }
4735
+ /** 只给测试和诊断用 */
4736
+ get pending() {
4737
+ return this.#cards.size;
4738
+ }
4739
+ /** 定时调(2~3 秒):找进程、换了就重连、补订阅、轮询待答 */
4740
+ tick() {
4741
+ if (this.#stopped) return;
4742
+ if (this.#d.enabled && !this.#d.enabled()) {
4743
+ if (this.#target) this.#teardown("\u767E\u5EA6\u642D\u5B50\u5DF2\u505C\u7528");
4744
+ return;
4745
+ }
4746
+ if (!this.#target && this.#discoverMissAt && this.#now() - this.#discoverMissAt < DISCOVER_BACKOFF_MS) return;
4747
+ let t = null;
4748
+ try {
4749
+ t = (this.#d.discover ?? defaultDiscover)();
4750
+ } catch {
4751
+ t = null;
4752
+ }
4753
+ if (!t) {
4754
+ if (this.#target) this.#teardown("\u767E\u5EA6\u642D\u5B50\u5DF2\u9000\u51FA");
4755
+ this.#discoverMissAt = this.#now();
4756
+ return;
4757
+ }
4758
+ this.#discoverMissAt = 0;
4759
+ const cur = this.#target;
4760
+ if (!cur || cur.pid !== t.pid || cur.port !== t.port || cur.key !== t.key) {
4761
+ if (cur) this.#teardown("\u767E\u5EA6\u642D\u5B50\u91CD\u542F\u8FC7");
4762
+ this.#target = t;
4763
+ this.#d.log(`\u5DF2\u8FDE\u4E0A\u767E\u5EA6\u642D\u5B50\uFF08\u7AEF\u53E3 ${t.port}\uFF09\uFF0C\u72B6\u6001\u3001\u9009\u62E9\u9898\u4E0E\u6388\u6743\u53EF\u63A8\u5230\u8BBE\u5907`);
4764
+ }
4765
+ this.#ensureSubscribed();
4766
+ void this.reconcile();
4767
+ }
4768
+ stop() {
4769
+ this.#stopped = true;
4770
+ this.#teardown("");
4771
+ }
4772
+ #teardown(why) {
4773
+ for (const unsub of this.#subs.values()) unsub();
4774
+ this.#subs.clear();
4775
+ for (const c of this.#cards.values()) c.ac.abort();
4776
+ this.#cards.clear();
4777
+ for (const t of this.#busyTimers.values()) clearTimeout(t);
4778
+ this.#busyTimers.clear();
4779
+ this.#target = null;
4780
+ this.#globalUnsupported = false;
4781
+ this.#questionUnsupported = false;
4782
+ this.#permissionUnsupported = false;
4783
+ this.#sessionListAt = 0;
4784
+ this.#subEndedAt = 0;
4785
+ this.#dirs.clear();
4786
+ this.#busyDirs.clear();
4787
+ this.#sseVerified = false;
4788
+ this.#scanAt = 0;
4789
+ this.#sessionDir.clear();
4790
+ this.#children.clear();
4791
+ this.#userMsgs.clear();
4792
+ this.#promptedMsgs.clear();
4793
+ this.#calls.clear();
4794
+ this.#callReported.clear();
4795
+ this.#turnOpen.clear();
4796
+ this.#active.clear();
4797
+ this.#lastUserText.clear();
4798
+ if (why) this.#d.log(`\u5DF2\u65AD\u5F00\u767E\u5EA6\u642D\u5B50\uFF08${why}\uFF09`);
4799
+ }
4800
+ // ---------- 订阅 ----------
4801
+ #ensureSubscribed() {
4802
+ const t = this.#target;
4803
+ if (!t) return;
4804
+ if (this.#subEndedAt && this.#now() - this.#subEndedAt < RESUBSCRIBE_MS) return;
4805
+ const sub = this.#d.subscribe ?? defaultSubscribe;
4806
+ if (!this.#globalUnsupported) {
4807
+ if (this.#subs.has("")) return;
4808
+ this.#subs.set(
4809
+ "",
4810
+ sub(t, "/global/event", void 0, (ev) => this.#onEnvelope(ev, void 0), (status) => this.#onSubEnd("", t, status))
4811
+ );
4812
+ return;
4813
+ }
4814
+ const active = this.#activeDirs();
4815
+ const keys = new Set(active.map(dirKey));
4816
+ for (const [k, unsub] of [...this.#subs]) {
4817
+ if (k === "" || keys.has(k)) continue;
4818
+ unsub();
4819
+ this.#subs.delete(k);
4820
+ }
4821
+ for (const dir of active) {
4822
+ const k = dirKey(dir);
4823
+ if (this.#subs.has(k)) continue;
4824
+ this.#subs.set(
4825
+ k,
4826
+ sub(t, "/event", dir, (ev) => this.#onEnvelope(ev, dir), (status) => this.#onSubEnd(k, t, status))
4827
+ );
4828
+ }
3474
4829
  }
3475
- /** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
3476
- get activeSessions() {
3477
- return this.#tails.size;
4830
+ #onSubEnd(key, t, status) {
4831
+ if (this.#target !== t) return;
4832
+ this.#subs.delete(key);
4833
+ this.#subEndedAt = this.#now();
4834
+ if (key === "" && status === 404) {
4835
+ this.#globalUnsupported = true;
4836
+ this.#d.log("\u767E\u5EA6\u642D\u5B50\u6CA1\u6709 /global/event\uFF0C\u6539\u4E3A\u6309\u4F1A\u8BDD\u76EE\u5F55\u8BA2\u9605");
4837
+ }
3478
4838
  }
3479
- /** 等所有会话都排空。只给测试和优雅退出用 */
3480
- async drain() {
3481
- while (this.#tails.size > 0) {
3482
- await Promise.all([...this.#tails.values()]);
4839
+ /** /global/event 的信封 `{directory, payload}`;按目录的 /event 直接是事件 */
4840
+ #onEnvelope(raw, dir) {
4841
+ const inner = raw.payload && typeof raw.payload === "object" ? raw.payload : raw;
4842
+ const d = typeof raw.directory === "string" && raw.directory ? raw.directory : dir;
4843
+ if (dir === void 0 && d && inner.type !== "server.connected" && inner.type !== "server.heartbeat") this.#sseVerified = true;
4844
+ try {
4845
+ this.onEvent(inner, d);
4846
+ } catch (e) {
4847
+ this.#d.log(`\u5904\u7406\u767E\u5EA6\u642D\u5B50\u4E8B\u4EF6\u51FA\u9519\uFF1A${e?.message ?? e}`);
3483
4848
  }
3484
4849
  }
3485
- };
3486
-
3487
- // src/daemon/resolve.ts
3488
- function resolveTarget(cmd, deps) {
3489
- if (cmd.sessionId) {
3490
- const sessionId = deps.alias ? deps.alias(cmd.sessionId) : cmd.sessionId;
3491
- const state = deps.readState(sessionId);
3492
- if (!state) return { ok: false, reason: "session_gone" };
3493
- if (!state.cwd) return { ok: false, reason: "no_cwd" };
3494
- if (deps.windowOpen?.(state)) return { ok: false, reason: "window_open" };
3495
- return { ok: true, sessionId, cwd: state.cwd };
4850
+ #touchDir(dir, at = this.#now()) {
4851
+ if (!dir || !dir.startsWith("/")) return;
4852
+ const k = dirKey(dir);
4853
+ const cur = this.#dirs.get(k);
4854
+ if (!cur || cur.at < at) this.#dirs.set(k, { dir: cur?.dir ?? dir, at });
3496
4855
  }
3497
- let dirCwd = "";
3498
- if (cmd.dirRef) {
3499
- dirCwd = deps.dirOf ? deps.dirOf(cmd.dirRef) : "";
3500
- if (!dirCwd) return { ok: false, reason: "dir_gone" };
4856
+ #markBusy(dir) {
4857
+ if (dir) this.#busyDirs.set(dirKey(dir), this.#now());
3501
4858
  }
3502
- const cwd = dirCwd || deps.remoteCwd();
3503
- if (!cwd) return { ok: false, reason: "no_remote_cwd" };
3504
- return { ok: true, sessionId: "", cwd };
3505
- }
3506
- function failureMessage(reason) {
3507
- switch (reason) {
3508
- case "session_gone":
3509
- return "\u8FD9\u4E2A\u4F1A\u8BDD\u5DF2\u7ECF\u4E0D\u5728\u4E86";
3510
- case "no_cwd":
3511
- return "\u8FD9\u4E2A\u4F1A\u8BDD\u9700\u8981\u66F4\u65B0 DeskBuddy \u540E\u624D\u80FD\u8FDC\u7A0B\u64CD\u4F5C";
3512
- case "no_remote_cwd":
3513
- return "\u8FD8\u6CA1\u8BBE\u7F6E\u8FDC\u7A0B\u4EFB\u52A1\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u8DD1\u4E00\u6B21 deskbuddy setup";
3514
- case "window_open":
3515
- return "\u8FD9\u4E2A\u4EFB\u52A1\u6B63\u5728\u7535\u8111\u4E0A\u6253\u5F00\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u7EE7\u7EED\uFF0C\u6216\u65B0\u5EFA\u4EFB\u52A1";
3516
- case "dir_gone":
3517
- return "\u7535\u8111\u4E0A\u627E\u4E0D\u5230\u8FD9\u4E2A\u76EE\u5F55\u4E86\uFF0C\u8BF7\u6362\u4E00\u4E2A\u76EE\u5F55";
4859
+ #activeDirs() {
4860
+ const now = this.#now();
4861
+ for (const [k, v] of this.#dirs) if (now - v.at > ACTIVE_DIR_MS) this.#dirs.delete(k);
4862
+ return [...this.#dirs.values()].sort((a, b) => b.at - a.at).map((v) => v.dir).slice(0, MAX_POLL_DIRS);
3518
4863
  }
3519
- }
3520
-
3521
- // src/daemon/seen.ts
3522
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync11 } from "fs";
3523
- import { join as join17 } from "path";
3524
- var SEEN_CAPACITY = 200;
3525
- function seenPath() {
3526
- return join17(deskbuddyHome(), "seen.json");
3527
- }
3528
- var SeenIds = class _SeenIds {
3529
- #ids = [];
3530
- #set = /* @__PURE__ */ new Set();
3531
- constructor(initial = []) {
3532
- for (const id of initial) this.#push(id);
4864
+ /**
4865
+ * 这一轮要轮询的目录(评审 d):事件流还没证实收得到会话事件时全部活跃目录;
4866
+ * 证实之后只轮询有卡在等的、最近忙过的(提问 / 授权只会出现在忙的会话里,事件丢了靠这个兜)
4867
+ */
4868
+ #pollDirs() {
4869
+ const all = this.#activeDirs();
4870
+ if (!this.#sseVerified || !this.#subs.has("")) return all;
4871
+ const now = this.#now();
4872
+ const cardKeys = new Set([...this.#cards.values()].filter((c) => c.dir).map((c) => dirKey(c.dir)));
4873
+ return all.filter((d) => {
4874
+ const k = dirKey(d);
4875
+ return cardKeys.has(k) || now - (this.#busyDirs.get(k) ?? 0) <= BUSY_DIR_MS;
4876
+ });
3533
4877
  }
3534
- #push(id) {
3535
- if (this.#set.has(id)) return;
3536
- this.#ids.push(id);
3537
- this.#set.add(id);
3538
- while (this.#ids.length > SEEN_CAPACITY) {
3539
- const dropped = this.#ids.shift();
3540
- if (dropped !== void 0) this.#set.delete(dropped);
4878
+ // ---------- 事件 → 状态 ----------
4879
+ /** 一条 opencode 事件(`{type, properties}`)。dir = 事件所属会话目录 */
4880
+ onEvent(ev, dir) {
4881
+ const type = typeof ev.type === "string" ? ev.type : "";
4882
+ const p = ev.properties && typeof ev.properties === "object" ? ev.properties : {};
4883
+ this.#touchDir(dir);
4884
+ switch (type) {
4885
+ case "session.created":
4886
+ case "session.updated": {
4887
+ const info = p.info ?? {};
4888
+ this.#noteSession(info);
4889
+ return;
4890
+ }
4891
+ case "session.deleted": {
4892
+ const info = p.info ?? {};
4893
+ const sid = typeof info.id === "string" ? info.id : typeof p.sessionID === "string" ? p.sessionID : "";
4894
+ if (sid && !this.#children.has(sid)) this.#report("SessionEnd", sid, dir);
4895
+ return;
4896
+ }
4897
+ case "session.status": {
4898
+ const sid = typeof p.sessionID === "string" ? p.sessionID : "";
4899
+ const st = p.status?.type;
4900
+ if (!sid) return;
4901
+ if (st === "busy") {
4902
+ this.#markBusy(this.#dirOf(sid, dir));
4903
+ this.#onBusy(sid, dir);
4904
+ } else if (st === "idle") this.#onIdle(sid, dir);
4905
+ return;
4906
+ }
4907
+ case "session.idle":
4908
+ if (typeof p.sessionID === "string") this.#onIdle(p.sessionID, dir);
4909
+ return;
4910
+ case "message.updated": {
4911
+ const info = p.info ?? {};
4912
+ if (info.role === "user" && typeof info.id === "string" && typeof info.sessionID === "string") {
4913
+ this.#userMsgs.set(info.id, info.sessionID);
4914
+ while (this.#userMsgs.size > 500) this.#userMsgs.delete(this.#userMsgs.keys().next().value);
4915
+ }
4916
+ return;
4917
+ }
4918
+ case "message.part.updated":
4919
+ this.#onPart(p.part ?? {}, dir);
4920
+ return;
4921
+ case "question.asked":
4922
+ this.#onQuestion(p, dir);
4923
+ return;
4924
+ case "question.replied":
4925
+ case "question.rejected":
4926
+ this.#settle(typeof p.requestID === "string" ? p.requestID : "", "\u7535\u8111\u4E0A\u5DF2\u7B54");
4927
+ return;
4928
+ case "permission.asked":
4929
+ case "permission.updated":
4930
+ this.#onPermission(p, dir);
4931
+ return;
4932
+ case "permission.replied":
4933
+ this.#settle(typeof p.requestID === "string" ? p.requestID : typeof p.permissionID === "string" ? p.permissionID : "", "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
4934
+ return;
4935
+ default:
3541
4936
  }
3542
4937
  }
3543
- has(id) {
3544
- return this.#set.has(id);
4938
+ #noteSession(info) {
4939
+ const id = typeof info.id === "string" ? info.id : "";
4940
+ if (!id) return;
4941
+ if (typeof info.directory === "string" && info.directory) {
4942
+ this.#sessionDir.set(id, info.directory);
4943
+ }
4944
+ if (info.parentID || info.is_subagent === true) this.#children.add(id);
4945
+ rememberTitle(this.#titles, info);
3545
4946
  }
3546
- /** 记下来。返回 true 表示这是**新的**(之前没见过) */
3547
- remember(id) {
3548
- if (this.#set.has(id)) return false;
3549
- this.#push(id);
3550
- return true;
4947
+ #dirOf(sid, dir) {
4948
+ return this.#sessionDir.get(sid) ?? dir ?? "";
3551
4949
  }
3552
- snapshot() {
3553
- return [...this.#ids];
4950
+ #onBusy(sid, dir) {
4951
+ if (this.#turnOpen.has(sid) || this.#busyTimers.has(sid)) return;
4952
+ const timer = setTimeout(() => {
4953
+ this.#busyTimers.delete(sid);
4954
+ this.#openTurn(sid, this.#lastUserText.get(sid), dir);
4955
+ }, BUSY_WAIT_MS);
4956
+ timer.unref?.();
4957
+ this.#busyTimers.set(sid, timer);
4958
+ }
4959
+ #openTurn(sid, prompt, dir) {
4960
+ if (this.#turnOpen.has(sid)) return;
4961
+ const t = this.#busyTimers.get(sid);
4962
+ if (t) {
4963
+ clearTimeout(t);
4964
+ this.#busyTimers.delete(sid);
4965
+ }
4966
+ this.#turnOpen.add(sid);
4967
+ this.#report("UserPromptSubmit", sid, dir, prompt ? { prompt } : {});
4968
+ }
4969
+ #onIdle(sid, dir) {
4970
+ const t = this.#busyTimers.get(sid);
4971
+ if (t) {
4972
+ this.#openTurn(sid, this.#lastUserText.get(sid), dir);
4973
+ }
4974
+ const was = this.#turnOpen.delete(sid) || this.#active.has(sid);
4975
+ this.#active.delete(sid);
4976
+ this.#lastUserText.delete(sid);
4977
+ if (was) this.#report("Stop", sid, dir);
4978
+ }
4979
+ #onPart(part, dir) {
4980
+ const sid = typeof part.sessionID === "string" ? part.sessionID : "";
4981
+ if (!sid) return;
4982
+ if (part.type === "text") {
4983
+ const mid = typeof part.messageID === "string" ? part.messageID : "";
4984
+ if (!mid || this.#userMsgs.get(mid) !== sid || part.synthetic === true) return;
4985
+ const text = typeof part.text === "string" ? part.text : "";
4986
+ if (!text.trim() || this.#promptedMsgs.has(mid)) return;
4987
+ this.#promptedMsgs.add(mid);
4988
+ while (this.#promptedMsgs.size > 500) this.#promptedMsgs.delete(this.#promptedMsgs.values().next().value);
4989
+ this.#lastUserText.set(sid, text);
4990
+ if (!this.#turnOpen.has(sid)) this.#openTurn(sid, text, dir);
4991
+ return;
4992
+ }
4993
+ if (part.type !== "tool") return;
4994
+ this.#markBusy(this.#dirOf(sid, dir));
4995
+ const callID = typeof part.callID === "string" ? part.callID : "";
4996
+ const tool = typeof part.tool === "string" ? part.tool : "";
4997
+ if (!callID || !tool) return;
4998
+ const state = part.state ?? {};
4999
+ const cc = toCcTool(tool, state.input);
5000
+ this.#calls.set(callID, { tool, cc });
5001
+ while (this.#calls.size > 500) this.#calls.delete(this.#calls.keys().next().value);
5002
+ const status = state.status;
5003
+ const done = this.#callReported.get(callID);
5004
+ if (status === "running" && !done) {
5005
+ this.#callReported.set(callID, "pre");
5006
+ this.#active.add(sid);
5007
+ this.#report("PreToolUse", sid, dir, cc);
5008
+ } else if ((status === "completed" || status === "error") && done !== "post") {
5009
+ this.#callReported.set(callID, "post");
5010
+ this.#active.add(sid);
5011
+ this.#report("PostToolUse", sid, dir, cc);
5012
+ }
5013
+ while (this.#callReported.size > 500) this.#callReported.delete(this.#callReported.keys().next().value);
5014
+ }
5015
+ #report(event, sid, dir, extra = {}) {
5016
+ if (this.#children.has(sid)) return;
5017
+ const t = this.#target;
5018
+ if (!t) return;
5019
+ const input = { hook_event_name: event, session_id: sid, cwd: this.#dirOf(sid, dir), ...extra };
5020
+ const title = this.#titles.get(sid);
5021
+ if (title) input.oc_session_title = title;
5022
+ const run2 = this.#d.report ?? ((i, pid) => runHook(this.#d.nodePath ?? process.execPath, i, pid));
5023
+ this.#reportChain = this.#reportChain.then(() => run2(input, t.pid)).catch(() => {
5024
+ });
3554
5025
  }
3555
- static load() {
3556
- try {
3557
- const raw = JSON.parse(readFileSync12(seenPath(), "utf8"));
3558
- return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
3559
- } catch {
3560
- return new _SeenIds();
5026
+ // ---------- 选择题 / 授权 ----------
5027
+ #prune() {
5028
+ const now = this.#now();
5029
+ for (const [id, at] of this.#handled) if (now - at > HANDLED_TTL_MS) this.#handled.delete(id);
5030
+ }
5031
+ #onQuestion(p, dir) {
5032
+ const id = typeof p.id === "string" ? p.id : "";
5033
+ const sid = typeof p.sessionID === "string" ? p.sessionID : "";
5034
+ if (!id || !sid || this.#cards.has(id) || this.#handled.has(id) || !this.#target) return;
5035
+ this.#prune();
5036
+ this.#handled.set(id, this.#now());
5037
+ const callID = (p.tool ?? {}).callID;
5038
+ const callTool = typeof callID === "string" ? this.#calls.get(callID)?.tool ?? "question" : "question";
5039
+ if (!isPlainQuestion(p, callTool)) return;
5040
+ const cc = questionToCc(p);
5041
+ if (!cc) return;
5042
+ const d = this.#dirOf(sid, dir);
5043
+ const deadline = questionDeadline(p.deadlineAt, p.timeoutMs, this.#now());
5044
+ this.#push({ id, kind: "question", sessionID: sid, dir: d, deadlineAt: deadline }, cc);
5045
+ }
5046
+ #onPermission(p, dir) {
5047
+ const id = typeof p.id === "string" ? p.id : "";
5048
+ const sid = typeof p.sessionID === "string" ? p.sessionID : "";
5049
+ if (!id || !sid || this.#cards.has(id) || this.#handled.has(id) || !this.#target) return;
5050
+ this.#prune();
5051
+ this.#handled.set(id, this.#now());
5052
+ const callID = (p.tool ?? {}).callID;
5053
+ const call2 = typeof callID === "string" ? this.#calls.get(callID) : void 0;
5054
+ const cc = call2 ? call2.cc : askToCc(p);
5055
+ this.#report("PermissionRequest", sid, dir, cc);
5056
+ this.#push({ id, kind: "permission", sessionID: sid, dir: this.#dirOf(sid, dir), deadlineAt: 0 }, cc);
5057
+ }
5058
+ #push(c, cc) {
5059
+ const t = this.#target;
5060
+ if (!t) return;
5061
+ const card = { ...c, ac: new AbortController(), createdAt: this.#now() };
5062
+ this.#cards.set(card.id, card);
5063
+ const req2 = {
5064
+ window: true,
5065
+ tool: "dumate",
5066
+ hook: { hook_event_name: "PermissionRequest", session_id: card.sessionID, cwd: card.dir, tool_name: cc.tool_name, tool_input: cc.tool_input }
5067
+ };
5068
+ void this.#d.gate(req2, card.ac.signal).then(async (reply) => {
5069
+ if (this.#cards.get(card.id) !== card || card.ac.signal.aborted || this.#target !== t) return;
5070
+ this.#cards.delete(card.id);
5071
+ await this.#answer(t, card, reply);
5072
+ }).catch(() => {
5073
+ if (this.#cards.get(card.id) === card) this.#cards.delete(card.id);
5074
+ });
5075
+ }
5076
+ async #answer(t, card, reply) {
5077
+ const http = this.#d.http ?? defaultHttp;
5078
+ const short = card.id.slice(0, 12);
5079
+ let r;
5080
+ if (card.kind === "question") {
5081
+ if (reply.decision === "allow" && Array.isArray(reply.answerLists)) {
5082
+ r = await http(t, { method: "POST", path: `/question/${encodeURIComponent(card.id)}/reply`, directory: card.dir, body: { answers: reply.answerLists } });
5083
+ } else if (reply.decision === "deny") {
5084
+ r = await http(t, { method: "POST", path: `/question/${encodeURIComponent(card.id)}/reject`, directory: card.dir });
5085
+ } else return;
5086
+ } else {
5087
+ if (reply.decision !== "allow" && reply.decision !== "deny") return;
5088
+ const answer = reply.decision === "allow" ? "once" : "reject";
5089
+ r = await http(t, { method: "POST", path: `/permission/${encodeURIComponent(card.id)}/reply`, directory: card.dir, body: { reply: answer } });
5090
+ if (r.status === 404) {
5091
+ r = await http(t, {
5092
+ method: "POST",
5093
+ path: `/session/${encodeURIComponent(card.sessionID)}/permissions/${encodeURIComponent(card.id)}`,
5094
+ directory: card.dir,
5095
+ body: { response: answer }
5096
+ });
5097
+ }
5098
+ }
5099
+ if (r.status >= 200 && r.status < 300) {
5100
+ this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A${card.kind === "question" ? "\u9009\u62E9\u9898" : "\u6388\u6743"} ${short} \u5DF2\u6309\u8BBE\u5907\u7B54\u590D\u56DE\u586B`);
5101
+ } else if (r.status === 404) {
5102
+ this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A${short} \u5DF2\u4E0D\u5728\u7B49\uFF08\u7535\u8111\u4E0A\u5DF2\u5904\u7406\u6216\u5DF2\u8D85\u65F6\uFF09\uFF0C\u8BBE\u5907\u7B54\u590D\u4E0D\u518D\u56DE\u586B`);
5103
+ } else {
5104
+ this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A\u56DE\u586B ${short} \u5931\u8D25\uFF08HTTP ${r.status || "\u8FDE\u4E0D\u4E0A"}\uFF09\uFF0C\u754C\u9762\u7559\u7ED9\u4EBA\u7B54`);
3561
5105
  }
3562
5106
  }
3563
- save() {
5107
+ #settle(id, why) {
5108
+ if (!id) return;
5109
+ this.#handled.set(id, this.#now());
5110
+ const c = this.#cards.get(id);
5111
+ if (!c) return;
5112
+ this.#cards.delete(id);
5113
+ this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A${c.kind === "question" ? "\u9009\u62E9\u9898" : "\u6388\u6743"} ${id.slice(0, 12)} ${why}\uFF0C\u64A4\u5361`);
5114
+ c.ac.abort();
5115
+ }
5116
+ /**
5117
+ * 每个 tick:补会话目录、按目录轮询 /question 与 /permission(事件没订上或丢了时兜底推卡),
5118
+ * 轮询里不见了的卡、过了截止时间的选择题撤掉
5119
+ */
5120
+ async reconcile() {
5121
+ const t = this.#target;
5122
+ if (!t || this.#reconciling) return;
5123
+ this.#reconciling = true;
3564
5124
  try {
3565
- mkdirSync13(deskbuddyHome(), { recursive: true });
3566
- const tmp = seenPath() + ".tmp";
3567
- writeFileSync11(tmp, JSON.stringify({ ids: this.#ids }), "utf8");
3568
- renameSync9(tmp, seenPath());
3569
- } catch {
5125
+ const http = this.#d.http ?? defaultHttp;
5126
+ const now = this.#now();
5127
+ if (now - this.#scanAt >= SCAN_DIRS_MS) {
5128
+ this.#scanAt = now;
5129
+ for (const d of (this.#d.sessionDirs ?? (() => scanSessionDirs(join19(qianfanHome(), "workspace", "sessions"), now)))()) {
5130
+ if (!this.#dirs.has(dirKey(d))) this.#touchDir(d, now);
5131
+ }
5132
+ }
5133
+ if (now - this.#sessionListAt >= SESSION_LIST_MS) {
5134
+ this.#sessionListAt = now;
5135
+ const r = await http(t, { method: "GET", path: "/session" });
5136
+ if (this.#target !== t) return;
5137
+ if (r.status === 200 && Array.isArray(r.body)) {
5138
+ for (const s of r.body) {
5139
+ if (!s || typeof s !== "object") continue;
5140
+ this.#noteSession(s);
5141
+ const updated = Number((s.time ?? {}).updated) || 0;
5142
+ if (typeof s.directory === "string" && s.directory && now - updated <= ACTIVE_DIR_MS) this.#touchDir(s.directory, updated);
5143
+ }
5144
+ }
5145
+ }
5146
+ const seen = /* @__PURE__ */ new Set();
5147
+ const polled = /* @__PURE__ */ new Set();
5148
+ for (const dir of this.#pollDirs()) {
5149
+ const k = dirKey(dir);
5150
+ if (!this.#questionUnsupported) {
5151
+ const q = await http(t, { method: "GET", path: "/question", directory: dir });
5152
+ if (this.#target !== t) return;
5153
+ if (q.status === 404) {
5154
+ this.#questionUnsupported = true;
5155
+ this.#d.log("\u767E\u5EA6\u642D\u5B50\u6CA1\u6709 /question\uFF0C\u9009\u62E9\u9898\u4E0D\u63A8\u8BBE\u5907\uFF0C\u53EA\u4E0A\u62A5\u72B6\u6001");
5156
+ } else if (q.status === 200 && Array.isArray(q.body)) {
5157
+ polled.add(`question:${k}`);
5158
+ for (const item of q.body) {
5159
+ if (!item || typeof item.id !== "string") continue;
5160
+ seen.add(item.id);
5161
+ this.#onQuestion(item, dir);
5162
+ }
5163
+ }
5164
+ }
5165
+ if (!this.#permissionUnsupported) {
5166
+ const pr = await http(t, { method: "GET", path: "/permission", directory: dir });
5167
+ if (this.#target !== t) return;
5168
+ if (pr.status === 404) {
5169
+ this.#permissionUnsupported = true;
5170
+ this.#d.log("\u767E\u5EA6\u642D\u5B50\u6CA1\u6709 /permission\uFF0C\u6388\u6743\u4E0D\u63A8\u8BBE\u5907\uFF0C\u53EA\u4E0A\u62A5\u72B6\u6001");
5171
+ } else if (pr.status === 200 && Array.isArray(pr.body)) {
5172
+ polled.add(`permission:${k}`);
5173
+ for (const item of pr.body) {
5174
+ if (!item || typeof item.id !== "string") continue;
5175
+ seen.add(item.id);
5176
+ this.#onPermission(item, dir);
5177
+ }
5178
+ }
5179
+ }
5180
+ }
5181
+ const later = this.#now();
5182
+ for (const c of [...this.#cards.values()]) {
5183
+ if (c.kind === "question" && c.deadlineAt > 0 && later > c.deadlineAt) {
5184
+ this.#settle(c.id, "\u5DF2\u8D85\u65F6");
5185
+ continue;
5186
+ }
5187
+ if (c.dir && polled.has(`${c.kind}:${dirKey(c.dir)}`) && !seen.has(c.id) && later - c.createdAt >= MISSING_GRACE_MS) {
5188
+ this.#settle(c.id, "\u5DF2\u4E0D\u5728\u7B49\uFF08\u7535\u8111\u4E0A\u5DF2\u5904\u7406\uFF09");
5189
+ }
5190
+ }
5191
+ } finally {
5192
+ this.#reconciling = false;
3570
5193
  }
3571
5194
  }
3572
5195
  };
5196
+ function runHook(node, input, agentPid) {
5197
+ return new Promise((resolve7) => {
5198
+ let child;
5199
+ try {
5200
+ child = spawn3(node, [installedHookPath(), "dumate"], {
5201
+ stdio: ["pipe", "ignore", "ignore"],
5202
+ env: { ...process.env, DESKBUDDY_AGENT_PID: String(agentPid) }
5203
+ });
5204
+ } catch {
5205
+ resolve7();
5206
+ return;
5207
+ }
5208
+ const timer = setTimeout(() => child.kill("SIGKILL"), HOOK_KILL_MS);
5209
+ timer.unref?.();
5210
+ child.on("error", () => {
5211
+ clearTimeout(timer);
5212
+ resolve7();
5213
+ });
5214
+ child.on("close", () => {
5215
+ clearTimeout(timer);
5216
+ resolve7();
5217
+ });
5218
+ child.stdin.on("error", () => {
5219
+ });
5220
+ child.stdin.end(JSON.stringify(input));
5221
+ });
5222
+ }
3573
5223
 
3574
5224
  // src/daemon/codexWatch.ts
3575
- import { execFileSync as execFileSync9 } from "child_process";
5225
+ import { execFileSync as execFileSync10 } from "child_process";
3576
5226
  import WebSocket from "ws";
3577
5227
  var TURN_LIMIT_MS = 10 * 60 * 1e3;
3578
5228
  var QUEUE_MAX_MS = 10 * 60 * 1e3;
3579
5229
  var IDLE_GRACE_MS = 5 * 1e3;
3580
5230
  var RESUME_RETRY_MS = 1e3;
3581
5231
  var RESUME_MAX_TRIES = 120;
3582
- var REQUEST_TIMEOUT_MS = 30 * 1e3;
5232
+ var REQUEST_TIMEOUT_MS3 = 30 * 1e3;
3583
5233
  function finalReply(items) {
3584
5234
  let last = "";
3585
5235
  let final = "";
@@ -3617,7 +5267,7 @@ function discoverAppServers(configured) {
3617
5267
  return parseAppServerUrls((winProcs() ?? []).map((p) => p.cmdline).join("\n"));
3618
5268
  }
3619
5269
  try {
3620
- const out = execFileSync9("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
5270
+ const out = execFileSync10("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
3621
5271
  return parseAppServerUrls(out);
3622
5272
  } catch {
3623
5273
  return [];
@@ -3647,6 +5297,7 @@ var CodexWatch = class {
3647
5297
  /** fileChange 条目 → 改动的文件(授权请求本身不带文件,从 item/started 记) */
3648
5298
  #changes = /* @__PURE__ */ new Map();
3649
5299
  /** 服务端请求 id → 放掉等待的开关、所属会话 */
5300
+ /** 等设备答复的服务端请求。qids:选择题各题的 Codex 题目 id(按顺序),只有选择题有 —— 跟着这条记录走,不单独按请求 id 存(重连后 id 会从头编号) */
3650
5301
  #pending = /* @__PURE__ */ new Map();
3651
5302
  #stopped = false;
3652
5303
  /** 已空闲的会话(有卡片在等时暂缓退订,卡片了结后补退) */
@@ -3760,13 +5411,13 @@ var CodexWatch = class {
3760
5411
  }
3761
5412
  #request(method, params) {
3762
5413
  const id = this.#nextId++;
3763
- return new Promise((resolve6) => {
5414
+ return new Promise((resolve7) => {
3764
5415
  const timer = setTimeout(() => {
3765
5416
  this.#calls.delete(id);
3766
- resolve6({ error: { message: "timeout" }, local: true });
3767
- }, REQUEST_TIMEOUT_MS);
5417
+ resolve7({ error: { message: "timeout" }, local: true });
5418
+ }, REQUEST_TIMEOUT_MS3);
3768
5419
  timer.unref?.();
3769
- this.#calls.set(id, { resolve: resolve6, timer });
5420
+ this.#calls.set(id, { resolve: resolve7, timer });
3770
5421
  this.#send({ id, method, params });
3771
5422
  });
3772
5423
  }
@@ -3989,7 +5640,7 @@ var CodexWatch = class {
3989
5640
  return { ...out, ...merged ? { merged } : {} };
3990
5641
  }
3991
5642
  #trackTurn(threadId, turnId) {
3992
- return new Promise((resolve6) => {
5643
+ return new Promise((resolve7) => {
3993
5644
  const limit = this.#d.turnLimitMs ?? TURN_LIMIT_MS;
3994
5645
  const step = this.#d.turnTickMs ?? 1e3;
3995
5646
  const t = {
@@ -3999,7 +5650,7 @@ var CodexWatch = class {
3999
5650
  used: 0,
4000
5651
  timer: null,
4001
5652
  grace: null,
4002
- done: (end, text, error) => resolve6({ sent: true, end, turnId, text, ...error ? { error } : {} })
5653
+ done: (end, text, error) => resolve7({ sent: true, end, turnId, text, ...error ? { error } : {} })
4003
5654
  };
4004
5655
  this.#turns.set(turnId, t);
4005
5656
  const early = this.#early.get(turnId);
@@ -4046,7 +5697,7 @@ var CodexWatch = class {
4046
5697
  t.done(end, text || (end === "completed" ? "" : finalReply(t.items)), error);
4047
5698
  }
4048
5699
  #waitThread(threadId, ms) {
4049
- return new Promise((resolve6) => {
5700
+ return new Promise((resolve7) => {
4050
5701
  let set = this.#wake.get(threadId);
4051
5702
  if (!set) this.#wake.set(threadId, set = /* @__PURE__ */ new Set());
4052
5703
  const s = set;
@@ -4054,7 +5705,7 @@ var CodexWatch = class {
4054
5705
  clearTimeout(timer);
4055
5706
  s.delete(fire);
4056
5707
  if (s.size === 0 && this.#wake.get(threadId) === s) this.#wake.delete(threadId);
4057
- resolve6();
5708
+ resolve7();
4058
5709
  };
4059
5710
  const timer = setTimeout(fire, ms);
4060
5711
  timer.unref?.();
@@ -4188,6 +5839,7 @@ var CodexWatch = class {
4188
5839
  const threadId = typeof p.threadId === "string" ? p.threadId : "";
4189
5840
  if (!threadId) return;
4190
5841
  let hook;
5842
+ let qids;
4191
5843
  if (method === "item/commandExecution/requestApproval") {
4192
5844
  const command = typeof p.command === "string" ? unwrapShell(p.command) : "";
4193
5845
  if (!command) return;
@@ -4198,12 +5850,17 @@ var CodexWatch = class {
4198
5850
  const shown = paths.join(", ");
4199
5851
  if (!shown) return;
4200
5852
  hook = { tool_name: "Edit", tool_input: { file_path: shown }, cwd: this.#cwd.get(threadId) ?? "" };
5853
+ } else if (method === "item/tool/requestUserInput") {
5854
+ const q = codexQuestionInput(p);
5855
+ if (!q) return;
5856
+ hook = { tool_name: "AskUserQuestion", tool_input: q.toolInput, cwd: this.#cwd.get(threadId) ?? "" };
5857
+ qids = q.ids;
4201
5858
  } else {
4202
5859
  return;
4203
5860
  }
4204
5861
  const key = String(id);
4205
5862
  const ac = new AbortController();
4206
- const entry = { ac, threadId };
5863
+ const entry = { ac, threadId, ...qids ? { qids } : {} };
4207
5864
  this.#pending.set(key, entry);
4208
5865
  const req2 = {
4209
5866
  window: true,
@@ -4216,7 +5873,14 @@ var CodexWatch = class {
4216
5873
  void this.#d.gate(req2, ac.signal).then((reply) => {
4217
5874
  if (this.#pending.get(key) !== entry) return;
4218
5875
  this.#pending.delete(key);
4219
- if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
5876
+ if (qids) {
5877
+ const lists = reply.decision === "allow" ? reply.answerLists : void 0;
5878
+ if (Array.isArray(lists) && lists.length === qids.length) {
5879
+ const answers = {};
5880
+ qids.forEach((qid, i) => answers[qid] = { answers: lists[i] });
5881
+ this.#send({ id, result: { answers } });
5882
+ }
5883
+ } else if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
4220
5884
  else if (reply.decision === "deny") this.#send({ id, result: { decision: "decline" } });
4221
5885
  settled();
4222
5886
  }).catch(() => {
@@ -4225,12 +5889,34 @@ var CodexWatch = class {
4225
5889
  });
4226
5890
  }
4227
5891
  };
5892
+ function codexQuestionInput(p) {
5893
+ const qs = p.questions;
5894
+ if (!Array.isArray(qs) || qs.length === 0) return null;
5895
+ const ids = [];
5896
+ const questions = [];
5897
+ for (const q of qs) {
5898
+ if (!q || typeof q !== "object" || typeof q.id !== "string" || typeof q.question !== "string") return null;
5899
+ if (ids.includes(q.id)) return null;
5900
+ if (q.isSecret === true) return null;
5901
+ if (!Array.isArray(q.options) || q.options.length < 2) return null;
5902
+ ids.push(q.id);
5903
+ questions.push({
5904
+ question: q.question,
5905
+ header: typeof q.header === "string" ? q.header : "",
5906
+ multiSelect: false,
5907
+ options: q.options.map((o) => ({ label: o?.label, description: typeof o?.description === "string" ? o.description : "" })),
5908
+ // isOther=true 才允许自己填写(Codex 的 TUI 这时多一个「Other」)
5909
+ ...q.isOther === true ? {} : { custom: false }
5910
+ });
5911
+ }
5912
+ return { toolInput: { questions }, ids };
5913
+ }
4228
5914
 
4229
5915
  // src/core/codexRemote.ts
4230
5916
  import { mkdirSync as mkdirSync14, renameSync as renameSync10, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
4231
- import { join as join18 } from "path";
5917
+ import { join as join20 } from "path";
4232
5918
  function codexRemotePath() {
4233
- return join18(deskbuddyHome(), "codex-remote.json");
5919
+ return join20(deskbuddyHome(), "codex-remote.json");
4234
5920
  }
4235
5921
  function writeCodexRemote(threads, pid = process.pid, now = Date.now()) {
4236
5922
  try {
@@ -4254,16 +5940,16 @@ import { chmodSync as chmodSync4, rmSync as rmSync9 } from "fs";
4254
5940
  import { connect, createServer as createServer2 } from "net";
4255
5941
 
4256
5942
  // src/core/wake.ts
4257
- import { execFileSync as execFileSync10 } from "child_process";
5943
+ import { execFileSync as execFileSync11 } from "child_process";
4258
5944
  import { createHash as createHash3, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
4259
- import { chmodSync as chmodSync3, closeSync as closeSync3, mkdirSync as mkdirSync15, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync13, readSync as readSync2, renameSync as renameSync11, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync13 } from "fs";
5945
+ import { chmodSync as chmodSync3, closeSync as closeSync4, mkdirSync as mkdirSync15, openSync as openSync4, readdirSync as readdirSync5, readFileSync as readFileSync14, readSync as readSync3, renameSync as renameSync11, rmSync as rmSync8, statSync as statSync7, writeFileSync as writeFileSync13 } from "fs";
4260
5946
  import { tmpdir as tmpdir2 } from "os";
4261
- import { join as join19 } from "path";
5947
+ import { join as join21 } from "path";
4262
5948
  function wakeDir(home = deskbuddyHome()) {
4263
- return join19(home, "wake");
5949
+ return join21(home, "wake");
4264
5950
  }
4265
5951
  function marksDir(home = deskbuddyHome()) {
4266
- return join19(wakeDir(home), "marks");
5952
+ return join21(wakeDir(home), "marks");
4267
5953
  }
4268
5954
  function safeSid(sessionId) {
4269
5955
  return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
@@ -4273,10 +5959,10 @@ function homeTag(home) {
4273
5959
  }
4274
5960
  function wakeChannelPath(platform = process.platform, home = deskbuddyHome()) {
4275
5961
  if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-wake-${homeTag(home)}`;
4276
- const primary = join19(wakeDir(home), "wake.sock");
5962
+ const primary = join21(wakeDir(home), "wake.sock");
4277
5963
  if (Buffer.byteLength(primary) <= 103) return primary;
4278
5964
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
4279
- return join19(tmpdir2(), `deskbuddy-${uid}`, `wake-${homeTag(home)}.sock`);
5965
+ return join21(tmpdir2(), `deskbuddy-${uid}`, `wake-${homeTag(home)}.sock`);
4280
5966
  }
4281
5967
  function pidExists(pid) {
4282
5968
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -4292,7 +5978,7 @@ function writeWakeMark(sessionId, agentPid, now = Date.now()) {
4292
5978
  if (!safe) return;
4293
5979
  try {
4294
5980
  mkdirSync15(marksDir(), { recursive: true, mode: 448 });
4295
- const file = join19(marksDir(), `${safe}.json`);
5981
+ const file = join21(marksDir(), `${safe}.json`);
4296
5982
  const tmp = `${file}.${process.pid}.tmp`;
4297
5983
  writeFileSync13(tmp, JSON.stringify({ agentPid: agentPid ?? 0, daemonPid: process.pid, at: now }), { encoding: "utf8", mode: 384 });
4298
5984
  renameSync11(tmp, file);
@@ -4303,20 +5989,20 @@ function removeWakeMark(sessionId) {
4303
5989
  const safe = safeSid(sessionId);
4304
5990
  if (!safe) return;
4305
5991
  try {
4306
- rmSync8(join19(marksDir(), `${safe}.json`), { force: true });
5992
+ rmSync8(join21(marksDir(), `${safe}.json`), { force: true });
4307
5993
  } catch {
4308
5994
  }
4309
5995
  }
4310
5996
  function clearWakeMarks() {
4311
5997
  let names = [];
4312
5998
  try {
4313
- names = readdirSync4(marksDir());
5999
+ names = readdirSync5(marksDir());
4314
6000
  } catch {
4315
6001
  return;
4316
6002
  }
4317
6003
  for (const n of names) {
4318
6004
  try {
4319
- rmSync8(join19(marksDir(), n), { force: true });
6005
+ rmSync8(join21(marksDir(), n), { force: true });
4320
6006
  } catch {
4321
6007
  }
4322
6008
  }
@@ -4325,7 +6011,7 @@ function isWakeMarked(sessionId, alive = pidExists) {
4325
6011
  const safe = safeSid(sessionId);
4326
6012
  if (!safe) return false;
4327
6013
  try {
4328
- const m = JSON.parse(readFileSync13(join19(marksDir(), `${safe}.json`), "utf8"));
6014
+ const m = JSON.parse(readFileSync14(join21(marksDir(), `${safe}.json`), "utf8"));
4329
6015
  const daemonPid = Number(m?.daemonPid) || 0;
4330
6016
  if (!daemonPid || !alive(daemonPid)) return false;
4331
6017
  const pid = Number(m?.agentPid) || 0;
@@ -4338,7 +6024,7 @@ function withWake(states, alive) {
4338
6024
  return states.map((s) => s.remoteWindow === true || !isWakeMarked(s.sessionId, alive) ? s : { ...s, remoteWindow: true });
4339
6025
  }
4340
6026
  function wakeSecretPath(home = deskbuddyHome()) {
4341
- return join19(wakeDir(home), "secret");
6027
+ return join21(wakeDir(home), "secret");
4342
6028
  }
4343
6029
  function writeWakeSecret(hex, platform = process.platform) {
4344
6030
  const file = wakeSecretPath();
@@ -4347,7 +6033,7 @@ function writeWakeSecret(hex, platform = process.platform) {
4347
6033
  writeFileSync13(tmp, hex, { encoding: "utf8", mode: 384 });
4348
6034
  if (platform === "win32") {
4349
6035
  try {
4350
- execFileSync10("icacls", [tmp, "/inheritance:r", "/grant:r", `${currentUserSid()}:F`], { stdio: "ignore", windowsHide: true });
6036
+ execFileSync11("icacls", [tmp, "/inheritance:r", "/grant:r", `${currentUserSid()}:F`], { stdio: "ignore", windowsHide: true });
4351
6037
  } catch {
4352
6038
  renameSync11(tmp, file);
4353
6039
  return "\u7A97\u53E3\u5524\u9192\u5BC6\u94A5\u6587\u4EF6\u7684\u6743\u9650\u6CA1\u80FD\u6536\u7D27\uFF08icacls \u5931\u8D25\uFF09";
@@ -4361,7 +6047,7 @@ function writeWakeSecret(hex, platform = process.platform) {
4361
6047
  function restrictToUser(file, platform = process.platform) {
4362
6048
  if (platform !== "win32") return true;
4363
6049
  try {
4364
- execFileSync10("icacls", [file, "/inheritance:r", "/grant:r", `${currentUserSid()}:F`], { stdio: "ignore", windowsHide: true });
6050
+ execFileSync11("icacls", [file, "/inheritance:r", "/grant:r", `${currentUserSid()}:F`], { stdio: "ignore", windowsHide: true });
4365
6051
  return true;
4366
6052
  } catch {
4367
6053
  return false;
@@ -4369,7 +6055,7 @@ function restrictToUser(file, platform = process.platform) {
4369
6055
  }
4370
6056
  function currentUserSid() {
4371
6057
  try {
4372
- const out = execFileSync10("whoami", ["/user", "/fo", "csv", "/nh"], { encoding: "utf8", windowsHide: true });
6058
+ const out = execFileSync11("whoami", ["/user", "/fo", "csv", "/nh"], { encoding: "utf8", windowsHide: true });
4373
6059
  const m = /"(S-1-[0-9-]+)"/.exec(out);
4374
6060
  if (m) return `*${m[1]}`;
4375
6061
  } catch {
@@ -4404,11 +6090,11 @@ function analyzeTranscript(file) {
4404
6090
  if (!file) return null;
4405
6091
  let fd;
4406
6092
  try {
4407
- const size = statSync5(file).size;
6093
+ const size = statSync7(file).size;
4408
6094
  const len = Math.min(size, TRANSCRIPT_TAIL2);
4409
6095
  const buf = Buffer.alloc(len);
4410
- fd = openSync3(file, "r");
4411
- readSync2(fd, buf, 0, len, size - len);
6096
+ fd = openSync4(file, "r");
6097
+ readSync3(fd, buf, 0, len, size - len);
4412
6098
  const lines2 = buf.toString("utf8").split("\n");
4413
6099
  let seg = false;
4414
6100
  let reply = null;
@@ -4449,7 +6135,7 @@ function analyzeTranscript(file) {
4449
6135
  } finally {
4450
6136
  if (fd !== void 0) {
4451
6137
  try {
4452
- closeSync3(fd);
6138
+ closeSync4(fd);
4453
6139
  } catch {
4454
6140
  }
4455
6141
  }
@@ -4500,7 +6186,7 @@ function replyMac(secret, nonceC, r) {
4500
6186
  return wakeMac(secret, "r", nonceC, r.instructionId, r.status, r.reason ?? "");
4501
6187
  }
4502
6188
  function startChannelServer(path, secret, onRequest, platform = process.platform) {
4503
- return new Promise((resolve6) => {
6189
+ return new Promise((resolve7) => {
4504
6190
  const server = createServer2((sock) => {
4505
6191
  let open = true;
4506
6192
  let phase = "hello";
@@ -4561,7 +6247,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
4561
6247
  };
4562
6248
  server.once("error", () => {
4563
6249
  restore();
4564
- resolve6(null);
6250
+ resolve7(null);
4565
6251
  });
4566
6252
  server.listen(path, () => {
4567
6253
  restore();
@@ -4573,7 +6259,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
4573
6259
  }
4574
6260
  server.on("error", () => {
4575
6261
  });
4576
- resolve6(server);
6262
+ resolve7(server);
4577
6263
  });
4578
6264
  });
4579
6265
  }
@@ -4591,9 +6277,9 @@ function closeChannelServer(server, path, platform = process.platform) {
4591
6277
  }
4592
6278
  var FINAL = /* @__PURE__ */ new Set(["started", "rejected", "failed", "not_here"]);
4593
6279
  function sendToWindow(path, secret, req2, onReply, opts = {}) {
4594
- return new Promise((resolve6) => {
6280
+ return new Promise((resolve7) => {
4595
6281
  if (!secret) {
4596
- resolve6("fallback");
6282
+ resolve7("fallback");
4597
6283
  return;
4598
6284
  }
4599
6285
  let settled = false;
@@ -4606,13 +6292,13 @@ function sendToWindow(path, secret, req2, onReply, opts = {}) {
4606
6292
  settled = true;
4607
6293
  handled = v === "handled";
4608
6294
  clearTimeout(timer);
4609
- resolve6(v);
6295
+ resolve7(v);
4610
6296
  };
4611
6297
  let sock;
4612
6298
  try {
4613
6299
  sock = connect(path);
4614
6300
  } catch {
4615
- resolve6("fallback");
6301
+ resolve7("fallback");
4616
6302
  return;
4617
6303
  }
4618
6304
  const timer = setTimeout(() => {
@@ -4734,15 +6420,15 @@ function boxMatches(view, body) {
4734
6420
  }
4735
6421
 
4736
6422
  // src/core/wrapWindows.ts
4737
- import { chmodSync as chmodSync5, mkdirSync as mkdirSync16, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync12, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync14 } from "fs";
6423
+ import { chmodSync as chmodSync5, mkdirSync as mkdirSync16, readdirSync as readdirSync6, readFileSync as readFileSync15, renameSync as renameSync12, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync14 } from "fs";
4738
6424
  import { tmpdir as tmpdir3 } from "os";
4739
- import { join as join20 } from "path";
6425
+ import { join as join22 } from "path";
4740
6426
  var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
4741
6427
  function windowsDir() {
4742
- return join20(deskbuddyHome(), "windows");
6428
+ return join22(deskbuddyHome(), "windows");
4743
6429
  }
4744
6430
  function bySessionDir() {
4745
- return join20(windowsDir(), "by-session");
6431
+ return join22(windowsDir(), "by-session");
4746
6432
  }
4747
6433
  function safeSid2(sessionId) {
4748
6434
  return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
@@ -4756,7 +6442,7 @@ function atomicWrite(file, text) {
4756
6442
  renameSync12(tmp, file);
4757
6443
  }
4758
6444
  function windowFile(windowId) {
4759
- return join20(windowsDir(), `${windowId}.json`);
6445
+ return join22(windowsDir(), `${windowId}.json`);
4760
6446
  }
4761
6447
  function writeWindow(windowId, rec) {
4762
6448
  if (!WINDOW_ID_RE.test(windowId)) return;
@@ -4771,7 +6457,7 @@ function windowSecret(windowId) {
4771
6457
  function readWindow(windowId) {
4772
6458
  if (!WINDOW_ID_RE.test(windowId)) return null;
4773
6459
  try {
4774
- const r = JSON.parse(readFileSync14(windowFile(windowId), "utf8"));
6460
+ const r = JSON.parse(readFileSync15(windowFile(windowId), "utf8"));
4775
6461
  return r && typeof r.pid === "number" ? r : null;
4776
6462
  } catch {
4777
6463
  return null;
@@ -4787,14 +6473,14 @@ function removeWindow(windowId) {
4787
6473
  }
4788
6474
  let names = [];
4789
6475
  try {
4790
- names = readdirSync5(bySessionDir());
6476
+ names = readdirSync6(bySessionDir());
4791
6477
  } catch {
4792
6478
  return;
4793
6479
  }
4794
6480
  for (const n of names) {
4795
- const f = join20(bySessionDir(), n);
6481
+ const f = join22(bySessionDir(), n);
4796
6482
  try {
4797
- if (readFileSync14(f, "utf8").trim() === windowId) rmSync10(f, { force: true });
6483
+ if (readFileSync15(f, "utf8").trim() === windowId) rmSync10(f, { force: true });
4798
6484
  } catch {
4799
6485
  }
4800
6486
  }
@@ -4803,19 +6489,19 @@ function readBySession(sessionId) {
4803
6489
  const safe = safeSid2(sessionId);
4804
6490
  if (!safe) return null;
4805
6491
  try {
4806
- const id = readFileSync14(join20(bySessionDir(), safe), "utf8").trim();
6492
+ const id = readFileSync15(join22(bySessionDir(), safe), "utf8").trim();
4807
6493
  return WINDOW_ID_RE.test(id) ? id : null;
4808
6494
  } catch {
4809
6495
  return null;
4810
6496
  }
4811
6497
  }
4812
6498
  function lastEventFile(windowId) {
4813
- return join20(windowsDir(), `${windowId}.last`);
6499
+ return join22(windowsDir(), `${windowId}.last`);
4814
6500
  }
4815
6501
  function readLastEvent(windowId) {
4816
6502
  if (!WINDOW_ID_RE.test(windowId)) return null;
4817
6503
  try {
4818
- const e = JSON.parse(readFileSync14(lastEventFile(windowId), "utf8"));
6504
+ const e = JSON.parse(readFileSync15(lastEventFile(windowId), "utf8"));
4819
6505
  return e && typeof e.ev === "string" && typeof e.at === "number" ? { ev: e.ev, sid: String(e.sid ?? ""), at: e.at } : null;
4820
6506
  } catch {
4821
6507
  return null;
@@ -4824,16 +6510,16 @@ function readLastEvent(windowId) {
4824
6510
  function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
4825
6511
  if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
4826
6512
  const name = `${windowId.slice(0, 16)}.sock`;
4827
- const primary = join20(home, "win", name);
6513
+ const primary = join22(home, "win", name);
4828
6514
  if (Buffer.byteLength(primary) <= 103) return primary;
4829
6515
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
4830
- return join20(tmpdir3(), `deskbuddy-${uid}`, name);
6516
+ return join22(tmpdir3(), `deskbuddy-${uid}`, name);
4831
6517
  }
4832
6518
  function ensureChannelDir(sockPath) {
4833
- const dir = join20(sockPath, "..");
6519
+ const dir = join22(sockPath, "..");
4834
6520
  try {
4835
6521
  mkdirSync16(dir, { recursive: true, mode: 448 });
4836
- const st = statSync6(dir);
6522
+ const st = statSync8(dir);
4837
6523
  if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
4838
6524
  chmodSync5(dir, 448);
4839
6525
  return true;
@@ -4852,8 +6538,8 @@ function windowForSession(sessionId) {
4852
6538
  }
4853
6539
 
4854
6540
  // src/core/launch.ts
4855
- import { mkdirSync as mkdirSync17, readFileSync as readFileSync15, renameSync as renameSync13, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "fs";
4856
- import { join as join21 } from "path";
6541
+ import { mkdirSync as mkdirSync17, readFileSync as readFileSync16, renameSync as renameSync13, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "fs";
6542
+ import { join as join23 } from "path";
4857
6543
  var RUN_ID_RE = /^[a-f0-9]{32}$/;
4858
6544
  var SESSION_UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
4859
6545
  var LAUNCH_TOOLS = ["claude_code", "codex", "workbuddy"];
@@ -4863,10 +6549,10 @@ function validPlan(p) {
4863
6549
  return true;
4864
6550
  }
4865
6551
  function launchDir(home = deskbuddyHome()) {
4866
- return join21(home, "launch");
6552
+ return join23(home, "launch");
4867
6553
  }
4868
6554
  function planFile(runId) {
4869
- return join21(launchDir(), `${runId}.json`);
6555
+ return join23(launchDir(), `${runId}.json`);
4870
6556
  }
4871
6557
  function writeLaunch(runId, plan) {
4872
6558
  if (!RUN_ID_RE.test(runId)) throw new Error("bad runId");
@@ -4879,14 +6565,14 @@ function writeLaunch(runId, plan) {
4879
6565
  }
4880
6566
  function claimLaunch(runId) {
4881
6567
  if (!RUN_ID_RE.test(runId)) return null;
4882
- const claimed = join21(launchDir(), `${runId}.${process.pid}.claimed`);
6568
+ const claimed = join23(launchDir(), `${runId}.${process.pid}.claimed`);
4883
6569
  try {
4884
6570
  renameSync13(planFile(runId), claimed);
4885
6571
  } catch {
4886
6572
  return null;
4887
6573
  }
4888
6574
  try {
4889
- const p = JSON.parse(readFileSync15(claimed, "utf8"));
6575
+ const p = JSON.parse(readFileSync16(claimed, "utf8"));
4890
6576
  if (!p || typeof p.cwd !== "string" || typeof p.resume !== "string") return null;
4891
6577
  if (p.tool === void 0) p.tool = "claude_code";
4892
6578
  return validPlan(p) ? p : null;
@@ -4901,7 +6587,7 @@ function claimLaunch(runId) {
4901
6587
  }
4902
6588
  function launchPending(runId) {
4903
6589
  try {
4904
- readFileSync15(planFile(runId));
6590
+ readFileSync16(planFile(runId));
4905
6591
  return true;
4906
6592
  } catch {
4907
6593
  return false;
@@ -4909,10 +6595,10 @@ function launchPending(runId) {
4909
6595
  }
4910
6596
 
4911
6597
  // src/daemon/openWindow.ts
4912
- import { execFile as execFile4, execFileSync as execFileSync11, spawn as spawn3 } from "child_process";
6598
+ import { execFile as execFile4, execFileSync as execFileSync12, spawn as spawn4 } from "child_process";
4913
6599
  import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "fs";
4914
6600
  import { userInfo } from "os";
4915
- import { join as join22 } from "path";
6601
+ import { join as join24 } from "path";
4916
6602
  function safePath(p) {
4917
6603
  return /^(?:[A-Za-z]:[\\/]|\/)/.test(p) && !/[\r\n\0"'`$%;]/.test(p);
4918
6604
  }
@@ -4960,14 +6646,14 @@ var defaultGuiDeps = () => ({
4960
6646
  env: process.env,
4961
6647
  macManager: () => {
4962
6648
  try {
4963
- return execFileSync11("launchctl", ["managername"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
6649
+ return execFileSync12("launchctl", ["managername"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
4964
6650
  } catch {
4965
6651
  return "";
4966
6652
  }
4967
6653
  },
4968
6654
  macConsoleUser: () => {
4969
6655
  try {
4970
- return execFileSync11("stat", ["-f", "%Su", "/dev/console"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
6656
+ return execFileSync12("stat", ["-f", "%Su", "/dev/console"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
4971
6657
  } catch {
4972
6658
  return "";
4973
6659
  }
@@ -4981,7 +6667,7 @@ var defaultGuiDeps = () => ({
4981
6667
  },
4982
6668
  winSessionId: () => {
4983
6669
  try {
4984
- const out = execFileSync11("tasklist", ["/FI", `PID eq ${process.pid}`, "/FO", "CSV", "/NH"], {
6670
+ const out = execFileSync12("tasklist", ["/FI", `PID eq ${process.pid}`, "/FO", "CSV", "/NH"], {
4985
6671
  encoding: "utf8",
4986
6672
  stdio: ["ignore", "pipe", "ignore"],
4987
6673
  windowsHide: true,
@@ -4998,11 +6684,11 @@ var defaultGuiDeps = () => ({
4998
6684
  var defaultOpenerDeps = (findWt2) => ({
4999
6685
  platform: process.platform,
5000
6686
  findWt: findWt2,
5001
- run: (file, args) => new Promise((resolve6, reject) => {
5002
- execFile4(file, args, { windowsHide: true, timeout: 15e3 }, (err) => err ? reject(err) : resolve6());
6687
+ run: (file, args) => new Promise((resolve7, reject) => {
6688
+ execFile4(file, args, { windowsHide: true, timeout: 15e3 }, (err) => err ? reject(err) : resolve7());
5003
6689
  }),
5004
6690
  spawnDetached: (file, args) => {
5005
- const c = spawn3(file, args, { detached: true, stdio: "ignore", shell: false, windowsHide: false });
6691
+ const c = spawn4(file, args, { detached: true, stdio: "ignore", shell: false, windowsHide: false });
5006
6692
  c.on("error", () => {
5007
6693
  });
5008
6694
  c.unref();
@@ -5012,7 +6698,7 @@ async function openTerminal(t, d) {
5012
6698
  checkTarget(t);
5013
6699
  if (d.platform === "darwin") {
5014
6700
  mkdirSync18(launchDir(t.home), { recursive: true, mode: 448 });
5015
- const file = join22(launchDir(t.home), `${t.runId}.command`);
6701
+ const file = join24(launchDir(t.home), `${t.runId}.command`);
5016
6702
  writeFileSync16(file, macCommandScript(t), { encoding: "utf8", mode: 448 });
5017
6703
  chmodSync6(file, 448);
5018
6704
  await d.run("open", ["-a", "Terminal", file]);
@@ -5032,7 +6718,7 @@ async function openTerminal(t, d) {
5032
6718
  function removeCommandFile(home, runId) {
5033
6719
  if (!RUN_ID_RE.test(runId)) return;
5034
6720
  try {
5035
- rmSync12(join22(launchDir(home), `${runId}.command`), { force: true });
6721
+ rmSync12(join24(launchDir(home), `${runId}.command`), { force: true });
5036
6722
  } catch {
5037
6723
  }
5038
6724
  }
@@ -5338,7 +7024,7 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
5338
7024
  } catch {
5339
7025
  }
5340
7026
  }
5341
- return new Promise((resolve6) => {
7027
+ return new Promise((resolve7) => {
5342
7028
  const server = createServer3((sock) => {
5343
7029
  sock.on("error", () => {
5344
7030
  });
@@ -5424,36 +7110,36 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
5424
7110
  };
5425
7111
  server.once("error", () => {
5426
7112
  restore();
5427
- resolve6(null);
7113
+ resolve7(null);
5428
7114
  });
5429
7115
  server.listen(path, () => {
5430
7116
  restore();
5431
7117
  server.on("error", () => {
5432
7118
  });
5433
- resolve6(server);
7119
+ resolve7(server);
5434
7120
  });
5435
7121
  });
5436
7122
  }
5437
7123
 
5438
7124
  // src/core/appended.ts
5439
- import { chmodSync as chmodSync7, mkdirSync as mkdirSync19, readFileSync as readFileSync16, renameSync as renameSync14, rmSync as rmSync14, writeFileSync as writeFileSync17 } from "fs";
5440
- import { join as join23 } from "path";
7125
+ import { chmodSync as chmodSync7, mkdirSync as mkdirSync19, readFileSync as readFileSync17, renameSync as renameSync14, rmSync as rmSync14, writeFileSync as writeFileSync17 } from "fs";
7126
+ import { join as join25 } from "path";
5441
7127
  var APPENDED_MAX_ITEMS = 5;
5442
7128
  var APPENDED_TEXT_MAX = 500;
5443
7129
  var APPENDED_RESULT_MAX = 800;
5444
7130
  var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5445
7131
  function appendedDir() {
5446
- return join23(deskbuddyHome(), "appended");
7132
+ return join25(deskbuddyHome(), "appended");
5447
7133
  }
5448
7134
  function appendedPath(sessionId) {
5449
7135
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
5450
- return safe ? join23(appendedDir(), `${safe}.json`) : null;
7136
+ return safe ? join25(appendedDir(), `${safe}.json`) : null;
5451
7137
  }
5452
7138
  function readAppended(sessionId) {
5453
7139
  const file = appendedPath(sessionId);
5454
7140
  if (!file) return [];
5455
7141
  try {
5456
- const d = JSON.parse(readFileSync16(file, "utf8"));
7142
+ const d = JSON.parse(readFileSync17(file, "utf8"));
5457
7143
  return Array.isArray(d.items) ? d.items : [];
5458
7144
  } catch {
5459
7145
  return [];
@@ -5477,13 +7163,13 @@ function writeAppended(sessionId, item) {
5477
7163
  var RUNNING_TTL_MS = 60 * 60 * 1e3;
5478
7164
  function runningPath(sessionId) {
5479
7165
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
5480
- return safe ? join23(deskbuddyHome(), "running", `${safe}.json`) : null;
7166
+ return safe ? join25(deskbuddyHome(), "running", `${safe}.json`) : null;
5481
7167
  }
5482
7168
  function writeRunning(sessionId, now = Date.now()) {
5483
7169
  const file = runningPath(sessionId);
5484
7170
  if (!file) return;
5485
7171
  try {
5486
- mkdirSync19(join23(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
7172
+ mkdirSync19(join25(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
5487
7173
  writeFileSync17(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
5488
7174
  renameSync14(`${file}.tmp`, file);
5489
7175
  } catch {
@@ -5544,15 +7230,27 @@ var IDLE_POLL_MS2 = 1e3;
5544
7230
  var IDLE_QUEUE_MAX_MS = 30 * 60 * 1e3;
5545
7231
  var BUSY_STATES2 = /* @__PURE__ */ new Set(["watch", "ask_approval"]);
5546
7232
  var CODEX_WATCH_MS = 5 * 1e3;
7233
+ var DUMATE_WATCH_MS = 2500;
5547
7234
  function defaultCodexAppServers() {
5548
7235
  const cfg = loadConfig();
5549
7236
  return cfg.enabledTools.includes("codex") ? discoverAppServers(cfg.codexAppServer) : [];
5550
7237
  }
5551
7238
  function opencodeReady() {
5552
- return isOcPluginInstalled(opencodePluginPath()) && existsSync13(ocPluginModule(installedHookPath()));
7239
+ const mod = ocPluginModule(installedHookPath());
7240
+ return isOcPluginInstalled(opencodePluginPath()) && existsSync13(mod) && existsSync13(join26(dirname10(mod), "ocTranslate.mjs"));
5553
7241
  }
7242
+ var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
5554
7243
  function orphanResumeText(summary2) {
5555
- return `\uFF08\u4F60\u5728\u8BBE\u5907\u4E0A\u5DF2\u6279\u51C6\uFF1A${summary2}\uFF09\u8BF7\u7EE7\u7EED\u521A\u624D\u88AB\u4E2D\u65AD\u7684\u64CD\u4F5C\u3002`;
7244
+ return `${INTERNAL_PROMPT_MARK}\uFF08\u4F60\u5728\u8BBE\u5907\u4E0A\u5DF2\u6279\u51C6\uFF1A${summary2}\uFF09\u8BF7\u7EE7\u7EED\u521A\u624D\u88AB\u4E2D\u65AD\u7684\u64CD\u4F5C\u3002`;
7245
+ }
7246
+ function staleAfterRun(read, tool, alive) {
7247
+ const st = read();
7248
+ if (!st || st.tool !== tool || !BUSY_STATES2.has(st.state)) return null;
7249
+ if (st.interactive === true || st.embedded === true) return null;
7250
+ if (st.pid && alive(st.pid, st.pidName)) return null;
7251
+ const again = read();
7252
+ if (!again || again.state !== st.state || again.updatedAt !== st.updatedAt) return null;
7253
+ return st;
5556
7254
  }
5557
7255
  var ALIVE_REPORT_MS = 60 * 1e3;
5558
7256
  var Daemon = class {
@@ -5564,6 +7262,8 @@ var Daemon = class {
5564
7262
  #gate;
5565
7263
  /** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
5566
7264
  #runsDone = /* @__PURE__ */ new Map();
7265
+ /** runId → 子进程 pid(按 hook 状态里的进程号认出新建任务建出的会话) */
7266
+ #runPids = /* @__PURE__ */ new Map();
5567
7267
  #attempt = 0;
5568
7268
  #stopped = false;
5569
7269
  #ws = null;
@@ -5582,6 +7282,11 @@ var Daemon = class {
5582
7282
  #wakeReport = null;
5583
7283
  /** url → 连着它的 CodexWatch。插入顺序 = 发现顺序(DeskBuddy 管的在前) */
5584
7284
  #codex = /* @__PURE__ */ new Map();
7285
+ /** 千问办公的选择题 / 授权推设备(spec 2026-10-03 §3.1):hook 发 notify 进来,回填走 App 内置 MCP */
7286
+ #qwenwork;
7287
+ /** 百度搭子(spec 2026-10-03 §3.2):直连 dumate-opencode 的 HTTP,状态上报 + 选择题 / 授权推设备 */
7288
+ #dumate;
7289
+ #dumateTimer = null;
5585
7290
  #codexStarted = false;
5586
7291
  #codexReportTimer = null;
5587
7292
  /** daemon 交出去、还没有结局的指令流程数(开窗 / 写进窗口 / 经 app-server 发一轮)。空闲判定用 */
@@ -5607,6 +7312,7 @@ var Daemon = class {
5607
7312
  sendToWindow: deps.sendToWindow ?? ((id, req2, onReply) => sendToWindow(channelPath(id), windowSecret(id), req2, onReply)),
5608
7313
  gui: deps.gui ?? (() => guiAvailable(defaultGuiDeps())),
5609
7314
  openWindow: deps.openWindow ?? ((runId) => this.#openTerminal(runId)),
7315
+ ancestors: deps.ancestors ?? ((pid) => selfAndAncestors(pid)),
5610
7316
  launchable: deps.launchable ?? ((tool) => {
5611
7317
  try {
5612
7318
  return locate(tool) != null;
@@ -5652,7 +7358,21 @@ var Daemon = class {
5652
7358
  log,
5653
7359
  now: this.#deps.now
5654
7360
  });
5655
- this.#gate = new GateServer((req2, signal) => this.#coord.onGate(req2, signal));
7361
+ this.#qwenwork = new QwenWorkBridge({
7362
+ gate: (req2, signal) => this.#coord.onGate(req2, signal),
7363
+ log,
7364
+ enabled: () => loadConfig().enabledTools.includes("qwenwork")
7365
+ });
7366
+ this.#dumate = new DumateWatch({
7367
+ gate: (req2, signal) => this.#coord.onGate(req2, signal),
7368
+ log,
7369
+ enabled: () => loadConfig().enabledTools.includes("dumate"),
7370
+ nodePath: this.#deps.nodePath
7371
+ });
7372
+ this.#gate = new GateServer(
7373
+ (req2, signal) => this.#coord.onGate(req2, signal),
7374
+ (req2) => this.#qwenwork.onNotify(req2)
7375
+ );
5656
7376
  }
5657
7377
  /** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
5658
7378
  async startGate() {
@@ -5666,6 +7386,8 @@ var Daemon = class {
5666
7386
  this.#codexTick();
5667
7387
  this.#codexTimer = setInterval(() => this.#codexTick(), CODEX_WATCH_MS);
5668
7388
  this.#codexTimer.unref();
7389
+ this.#dumateTimer = setInterval(() => this.#dumate.tick(), DUMATE_WATCH_MS);
7390
+ this.#dumateTimer.unref();
5669
7391
  await this.startWake();
5670
7392
  return port;
5671
7393
  }
@@ -5707,7 +7429,7 @@ var Daemon = class {
5707
7429
  const cfg = loadConfig();
5708
7430
  if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes(tool)) return;
5709
7431
  try {
5710
- await postReport(buildSnapshot(withWake(readAliveStates(tool)), tool, cfg, this.#deps.now()));
7432
+ await postReport(buildSnapshot(withWake(this.#aliveStates(tool)), tool, cfg, this.#deps.now()));
5711
7433
  } catch (e) {
5712
7434
  log(`\u4E0A\u62A5 ${tool} \u4F1A\u8BDD\u5931\u8D25\uFF1A${e.message}`);
5713
7435
  }
@@ -5780,7 +7502,7 @@ var Daemon = class {
5780
7502
  const cfg = loadConfig();
5781
7503
  if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes("codex")) return;
5782
7504
  try {
5783
- await postReport(buildSnapshot(readAliveStates("codex"), "codex", cfg, this.#deps.now(), this.#codexLoaded()));
7505
+ await postReport(buildSnapshot(this.#aliveStates("codex"), "codex", cfg, this.#deps.now(), this.#codexLoaded()));
5784
7506
  } catch (e) {
5785
7507
  log(`\u4E0A\u62A5 codex \u5FEB\u7167\u5931\u8D25\uFF1A${e.message}`);
5786
7508
  }
@@ -5804,13 +7526,23 @@ var Daemon = class {
5804
7526
  log(`\u4E0A\u62A5\u6700\u8FD1\u76EE\u5F55\u5931\u8D25\uFF1A${e.message}`);
5805
7527
  }
5806
7528
  }
7529
+ /** 存活会话(顺手剔除死进程、退役没记 pid 又 2 小时无事件的) */
7530
+ #aliveStates(tool) {
7531
+ return readAliveStates(tool, { now: this.#deps.now(), onStale: (s) => log(`\u4F1A\u8BDD ${s.sessionId.slice(0, 8)} ${STALE_NO_PID_LOG}`) });
7532
+ }
5807
7533
  /** 按已启用的工具各报一次存活会话快照(readAliveStates 顺手剔除死进程) */
5808
7534
  async #reportAlive() {
5809
7535
  const cfg = loadConfig();
5810
7536
  if (!cfg.clientId || !cfg.boundAt) return;
5811
7537
  for (const tool of cfg.enabledTools) {
5812
7538
  await postReport(
5813
- buildSnapshot(withWake(readAliveStates(tool)), tool, cfg, this.#deps.now(), tool === "codex" ? this.#codexLoaded() : null)
7539
+ buildSnapshot(
7540
+ withWake(this.#aliveStates(tool)),
7541
+ tool,
7542
+ cfg,
7543
+ this.#deps.now(),
7544
+ tool === "codex" ? this.#codexLoaded() : null
7545
+ )
5814
7546
  );
5815
7547
  }
5816
7548
  if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
@@ -5870,6 +7602,9 @@ var Daemon = class {
5870
7602
  if (this.#codexReportTimer) clearTimeout(this.#codexReportTimer);
5871
7603
  for (const w of this.#codex.values()) w.stop();
5872
7604
  this.#codex.clear();
7605
+ this.#qwenwork.stop();
7606
+ if (this.#dumateTimer) clearInterval(this.#dumateTimer);
7607
+ this.#dumate.stop();
5873
7608
  if (this.#codexStarted) removeCodexRemote();
5874
7609
  this.#ws?.close();
5875
7610
  void this.#gate.stop();
@@ -5974,7 +7709,7 @@ var Daemon = class {
5974
7709
  if (!this.#seen.remember(ap.instructionId)) return;
5975
7710
  this.#seen.save();
5976
7711
  log(`\u6536\u5230\u6388\u6743\u51B3\u5B9A ${ap.approvalId} ${ap.decision}`);
5977
- this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName);
7712
+ this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName, ap.answers);
5978
7713
  return;
5979
7714
  }
5980
7715
  const cmd = parseCommand(frame);
@@ -6171,9 +7906,9 @@ var Daemon = class {
6171
7906
  }
6172
7907
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5F00\u7A97\u547D\u4EE4\u62A5\u9519\uFF08${e.message}\uFF09\uFF0C\u4F46\u7A97\u53E3\u5DF2\u8BA4\u9886\uFF0C\u6309\u7A97\u53E3\u7EE7\u7EED`);
6173
7908
  }
6174
- const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
7909
+ const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
6175
7910
  const claimDeadline = this.#deps.now() + this.#deps.claimWaitMs;
6176
- while (launchPending(runId) && this.#deps.now() < claimDeadline) await sleep2(300);
7911
+ while (launchPending(runId) && this.#deps.now() < claimDeadline) await sleep3(300);
6177
7912
  removeCommandFile(deskbuddyHome(), runId);
6178
7913
  if (launchPending(runId) && claimLaunch(runId)) return headless(`${Math.round(this.#deps.claimWaitMs / 1e3)} \u79D2\u5185\u7A97\u53E3\u6CA1\u8D77\u6765`);
6179
7914
  log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u5DF2\u8D77\u6765\uFF08${runId.slice(0, 8)}\uFF09\uFF0C\u7B49\u5B83\u5C31\u7EEA\u540E\u5199\u5165\u9996\u53E5`);
@@ -6211,14 +7946,14 @@ var Daemon = class {
6211
7946
  found = null;
6212
7947
  }
6213
7948
  if (found) break;
6214
- await sleep2(200);
7949
+ await sleep3(200);
6215
7950
  }
6216
7951
  }
6217
- await sleep2(50);
7952
+ await sleep3(50);
6218
7953
  if (fb.p) await fb.p;
6219
7954
  return;
6220
7955
  }
6221
- await sleep2(1e3);
7956
+ await sleep3(1e3);
6222
7957
  }
6223
7958
  log(`\u6307\u4EE4 ${cmd.instructionId} \u65B0\u7A97\u53E3\u7684\u901A\u9053\u4E00\u76F4\u6CA1\u5C31\u7EEA`);
6224
7959
  this.#report(cmd, sid, "failed", "window", "window_failed");
@@ -6378,15 +8113,28 @@ var Daemon = class {
6378
8113
  this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
6379
8114
  return;
6380
8115
  }
8116
+ const before = target.sessionId ? this.#runsOfSession(target.sessionId) : [];
8117
+ for (const r of before) this.#coord.supersedeRun(r.runId, target.sessionId);
6381
8118
  if (target.sessionId) this.#coord.cancelSession(target.sessionId);
6382
8119
  const key = target.sessionId || `new:${cmd.instructionId}`;
6383
- if (this.#queue.busy(key)) {
8120
+ if (this.#queue.busy(key) || before.length > 0) {
6384
8121
  log(`\u6307\u4EE4 ${cmd.instructionId} \u4F1A\u8BDD ${key.slice(0, 8)} \u524D\u9762\u8FD8\u6709\u4EFB\u52A1\uFF0C\u6392\u961F`);
6385
8122
  this.#report(cmd, target.sessionId, "queued", "headless");
6386
8123
  }
6387
8124
  this.#queue.enqueue(
6388
8125
  key,
6389
- () => this.#execute(cmd, target.sessionId, target.cwd),
8126
+ async () => {
8127
+ for (const r of before) await r.done.catch(() => {
8128
+ });
8129
+ for (let more = this.#runsOfSession(target.sessionId); target.sessionId && more.length > 0; more = this.#runsOfSession(target.sessionId)) {
8130
+ for (const r of more) {
8131
+ this.#coord.supersedeRun(r.runId, target.sessionId);
8132
+ await r.done.catch(() => {
8133
+ });
8134
+ }
8135
+ }
8136
+ await this.#execute(cmd, target.sessionId, target.cwd);
8137
+ },
6390
8138
  (e) => {
6391
8139
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u65F6\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
6392
8140
  this.#report(cmd, target.sessionId, "failed", "", "exec_failed");
@@ -6466,7 +8214,7 @@ var Daemon = class {
6466
8214
  this.#report(cmd, sessionId, "rejected", "", "tool_unsupported");
6467
8215
  return;
6468
8216
  }
6469
- log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename7(cwd)} len=${cmd.text.length}`);
8217
+ log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename8(cwd)} len=${cmd.text.length}`);
6470
8218
  const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd, () => this.#report(cmd, sessionId, "started", "headless"));
6471
8219
  if ("fail" in result) {
6472
8220
  log(
@@ -6478,7 +8226,8 @@ var Daemon = class {
6478
8226
  if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
6479
8227
  onResult?.(result);
6480
8228
  if (!result.ok) {
6481
- log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error}`);
8229
+ const tail = failureTail(result, cmd.text);
8230
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error ?? "\u672A\u77E5"}${tail ? `\uFF1B\u8F93\u51FA\u672B\u5C3E\uFF1A${tail}` : ""}`);
6482
8231
  this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
6483
8232
  }
6484
8233
  if (!sessionId) {
@@ -6489,6 +8238,53 @@ var Daemon = class {
6489
8238
  #gateTarget(runId) {
6490
8239
  return { runId, nodePath: this.#deps.nodePath, gateScript: this.#deps.gateScript };
6491
8240
  }
8241
+ /**
8242
+ * 正在跑、且已知在跑 sessionId 这个会话的后台进程(新建任务起跑时不知道会话号:
8243
+ * 授权关卡请求里带上来(coord.sessionOf),或 hook 写的状态里记的进程号就是它)
8244
+ */
8245
+ #runsOfSession(sessionId) {
8246
+ if (!sessionId || this.#runsDone.size === 0) return [];
8247
+ let chain = null;
8248
+ const chainOf = () => {
8249
+ if (chain) return chain;
8250
+ let pid;
8251
+ try {
8252
+ pid = readState(sessionId)?.pid;
8253
+ } catch {
8254
+ pid = void 0;
8255
+ }
8256
+ try {
8257
+ chain = typeof pid === "number" && pid > 0 ? this.#deps.ancestors(pid) : [];
8258
+ } catch {
8259
+ chain = typeof pid === "number" && pid > 0 ? [pid] : [];
8260
+ }
8261
+ return chain;
8262
+ };
8263
+ const out = [];
8264
+ for (const [runId, done] of this.#runsDone) {
8265
+ const rp = this.#runPids.get(runId);
8266
+ if (this.#coord.sessionOf(runId) === sessionId || rp !== void 0 && chainOf().includes(rp)) out.push({ runId, done });
8267
+ }
8268
+ return out;
8269
+ }
8270
+ /**
8271
+ * 后台进程跑完了,会话状态却还停在「进行中」(Buglist 161:Codex 续跑后一直 watch)。Codex 的 hook 是 async 的,
8272
+ * exec 退出时 Stop 那次可能没跑完 / 没跑;hook 也可能没认出进程号(pid 空 → 永远不会按进程退出清掉)。
8273
+ * 状态里的进程不在了 / 没记进程号时,按这次的结果落 done / failed。🔴 进程号是个还活着的进程(用户窗口)就不动:
8274
+ * 那是窗口自己的状态,由它的 hook 管;停在等设备授权上(defer)的也不动
8275
+ */
8276
+ #settleStateAfterRun(sessionId, result, tool) {
8277
+ if (!sessionId || isDeferred(result.stdout)) return;
8278
+ try {
8279
+ const st = staleAfterRun(() => readState(sessionId), tool, (pid, name) => isAlive(pid, name));
8280
+ if (!st) return;
8281
+ const now = this.#deps.now();
8282
+ writeState({ ...st, state: result.ok ? "done" : "failed", updatedAt: Math.max(now, st.updatedAt + 1) });
8283
+ log(`\u4F1A\u8BDD ${sessionId.slice(0, 8)} \u540E\u53F0\u8FDB\u7A0B\u5DF2\u7ED3\u675F\u4F46\u72B6\u6001\u8FD8\u662F ${st.state}\uFF0C\u6309\u7ED3\u679C\u6539\u6210 ${result.ok ? "done" : "failed"}`);
8284
+ void this.#reportAlive();
8285
+ } catch {
8286
+ }
8287
+ }
6492
8288
  /** 设备上有了决定、需要续会话。排进该会话的串行队列,和普通指令一样不会并发打同一个会话 */
6493
8289
  #scheduleResume(r, kind) {
6494
8290
  if (!r.sessionId) {
@@ -6528,8 +8324,10 @@ var Daemon = class {
6528
8324
  }
6529
8325
  if ("fail" in result) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u653E\u5F03\uFF1A${result.fail === "gate_unavailable" ? "\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528" : `\u672C\u673A\u627E\u4E0D\u5230 ${r.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`}`);
6530
8326
  else if (isDeferred(result.stdout)) log(`\u4F1A\u8BDD ${r.sessionId} \u53C8\u505C\u5728\u4E86\u4E0B\u4E00\u4E2A\u7B49\u6388\u6743\u7684\u64CD\u4F5C\u4E0A`);
6531
- else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
6532
- else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
8327
+ else if (!result.ok) {
8328
+ const tail = failureTail(result, "");
8329
+ log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error ?? "\u672A\u77E5"}${tail ? `\uFF1B\u8F93\u51FA\u672B\u5C3E\uFF1A${tail}` : ""}`);
8330
+ } else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
6533
8331
  }
6534
8332
  /**
6535
8333
  * 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
@@ -6561,13 +8359,19 @@ var Daemon = class {
6561
8359
  onWaitEnd: () => rc.resumeTimeout()
6562
8360
  });
6563
8361
  this.#runsDone.set(runId, rc.done);
8362
+ const runPid = rc.pid;
8363
+ if (typeof runPid === "number") this.#runPids.set(runId, runPid);
8364
+ let result = null;
6564
8365
  try {
6565
- return await rc.done;
8366
+ result = await rc.done;
8367
+ return result;
6566
8368
  } finally {
6567
8369
  this.#runsDone.delete(runId);
6568
- const sid = sessionId || this.#coord.sessionOf(runId);
8370
+ this.#runPids.delete(runId);
8371
+ const sid = sessionId || this.#coord.sessionOf(runId) || result?.sessionId || "";
6569
8372
  this.#coord.endRun(runId);
6570
8373
  this.#coord.settleSession(sid, startedAt);
8374
+ if (result) this.#settleStateAfterRun(sid, result, tool);
6571
8375
  }
6572
8376
  }
6573
8377
  };
@@ -6653,8 +8457,8 @@ async function runDaemonCommand(opts = {}) {
6653
8457
  }
6654
8458
 
6655
8459
  // src/core/registry.ts
6656
- import { mkdirSync as mkdirSync20, readFileSync as readFileSync17, writeFileSync as writeFileSync19 } from "fs";
6657
- import { join as join24 } from "path";
8460
+ import { mkdirSync as mkdirSync20, readFileSync as readFileSync18, writeFileSync as writeFileSync19 } from "fs";
8461
+ import { join as join27 } from "path";
6658
8462
  var FALLBACK = {
6659
8463
  version: "builtin",
6660
8464
  tools: {
@@ -6739,6 +8543,45 @@ var FALLBACK = {
6739
8543
  },
6740
8544
  lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
6741
8545
  },
8546
+ // 千问办公(QwenWorkCN.app,内核是 App 内置的 Qoder CN CLI,hooks 读 ~/.QwenWorkCN/settings.json)。
8547
+ // PermissionRequest 不触发:授权框只来一条 Notification(permission_prompt)(spec 2026-10-03 §2.1)
8548
+ qwenwork: {
8549
+ format: "cc-hook",
8550
+ processNames: ["QwenWorkCN"],
8551
+ // 工作目录是 ~/.qwenworkcn/workspace/<chatId>,目录名不能当标题
8552
+ titleSources: ["first-prompt", "project"],
8553
+ turnEvents: ["UserPromptSubmit"],
8554
+ events: {
8555
+ UserPromptSubmit: "watch",
8556
+ PreToolUse: "watch",
8557
+ PostToolUse: "watch",
8558
+ Stop: "done"
8559
+ },
8560
+ conditionalEvents: {
8561
+ Notification: [
8562
+ // 实测 message 是 "Tool Bash requires confirmation"(选择题是 "Tool AskUserQuestion requires confirmation"),与服务端注册表一致
8563
+ { match: "requires confirmation|permission|approve|\u9700\u8981\u786E\u8BA4|\u6388\u6743", state: "ask_approval" },
8564
+ { match: "waiting for your input|idle|\u95F2\u7F6E", state: "done" }
8565
+ ]
8566
+ },
8567
+ lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
8568
+ },
8569
+ // 百度搭子(DuMate.app,内核是改名的 opencode:dumate-opencode serve)。不往 DuMate 里装任何东西:
8570
+ // daemon 的 DumateWatch 直连它的 HTTP 接口,把事件翻成 cc 同构输入喂 hook.cjs(spec 2026-10-03 §3.2)
8571
+ dumate: {
8572
+ format: "dumate-http",
8573
+ processNames: ["dumate-opencode"],
8574
+ titleSources: ["oc-session-title", "first-prompt"],
8575
+ turnEvents: ["UserPromptSubmit"],
8576
+ events: {
8577
+ UserPromptSubmit: "watch",
8578
+ PreToolUse: "watch",
8579
+ PostToolUse: "watch",
8580
+ Stop: "done",
8581
+ PermissionRequest: "ask_approval"
8582
+ },
8583
+ lifecycle: { SessionEnd: "remove" }
8584
+ },
6742
8585
  opencode: {
6743
8586
  format: "oc-plugin",
6744
8587
  processNames: ["opencode"],
@@ -6757,7 +8600,7 @@ var FALLBACK = {
6757
8600
  }
6758
8601
  };
6759
8602
  function registryCachePath() {
6760
- return join24(deskbuddyHome(), "tool-registry.json");
8603
+ return join27(deskbuddyHome(), "tool-registry.json");
6761
8604
  }
6762
8605
  function withFallbackTools(registry) {
6763
8606
  if (!registry || typeof registry !== "object" || !registry.tools || typeof registry.tools !== "object") return FALLBACK;
@@ -6765,7 +8608,7 @@ function withFallbackTools(registry) {
6765
8608
  }
6766
8609
  function loadRegistry() {
6767
8610
  try {
6768
- return withFallbackTools(JSON.parse(readFileSync17(registryCachePath(), "utf8")));
8611
+ return withFallbackTools(JSON.parse(readFileSync18(registryCachePath(), "utf8")));
6769
8612
  } catch {
6770
8613
  return FALLBACK;
6771
8614
  }
@@ -6780,8 +8623,8 @@ async function refreshRegistry() {
6780
8623
  }
6781
8624
 
6782
8625
  // src/adapters/ccHook.ts
6783
- import { chmodSync as chmodSync8, existsSync as existsSync14, mkdirSync as mkdirSync21, readFileSync as readFileSync18, renameSync as renameSync15, rmSync as rmSync16, statSync as statSync7, writeFileSync as writeFileSync20 } from "fs";
6784
- import { dirname as dirname9, join as join25 } from "path";
8626
+ import { chmodSync as chmodSync8, existsSync as existsSync14, mkdirSync as mkdirSync21, readFileSync as readFileSync19, renameSync as renameSync15, rmSync as rmSync16, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
8627
+ import { dirname as dirname11, join as join28 } from "path";
6785
8628
  var CC_EVENTS = [
6786
8629
  "UserPromptSubmit",
6787
8630
  "PreToolUse",
@@ -6792,7 +8635,7 @@ var CC_EVENTS = [
6792
8635
  ];
6793
8636
  function readSettings(path) {
6794
8637
  if (!existsSync14(path)) return {};
6795
- const raw = readFileSync18(path, "utf8");
8638
+ const raw = readFileSync19(path, "utf8");
6796
8639
  if (raw.trim() === "") return {};
6797
8640
  try {
6798
8641
  return JSON.parse(raw);
@@ -6801,10 +8644,10 @@ function readSettings(path) {
6801
8644
  }
6802
8645
  }
6803
8646
  function writeSettings(path, settings) {
6804
- mkdirSync21(dirname9(path), { recursive: true });
8647
+ mkdirSync21(dirname11(path), { recursive: true });
6805
8648
  let mode;
6806
8649
  try {
6807
- mode = statSync7(path).mode & 511;
8650
+ mode = statSync9(path).mode & 511;
6808
8651
  } catch {
6809
8652
  mode = void 0;
6810
8653
  }
@@ -6933,8 +8776,8 @@ function retagCcHook(settingsPath, scripts, from, to) {
6933
8776
  function uninstallCcHook(settingsPath, hookPath) {
6934
8777
  const settings = readSettings(settingsPath);
6935
8778
  if (!settings.hooks) return;
6936
- const gatePath = join25(dirname9(hookPath), "gate.cjs");
6937
- const wakePath = join25(dirname9(hookPath), "wake.cjs");
8779
+ const gatePath = join28(dirname11(hookPath), "gate.cjs");
8780
+ const wakePath = join28(dirname11(hookPath), "wake.cjs");
6938
8781
  for (const event of Object.keys(settings.hooks)) {
6939
8782
  const groups = settings.hooks[event] ?? [];
6940
8783
  const kept = [];
@@ -6948,14 +8791,14 @@ function uninstallCcHook(settingsPath, hookPath) {
6948
8791
  }
6949
8792
  writeSettings(settingsPath, settings);
6950
8793
  try {
6951
- rmSync16(join25(dirname9(hookPath), "wake", "tokens"), { recursive: true, force: true });
8794
+ rmSync16(join28(dirname11(hookPath), "wake", "tokens"), { recursive: true, force: true });
6952
8795
  } catch {
6953
8796
  }
6954
8797
  }
6955
8798
 
6956
8799
  // src/adapters/codexHook.ts
6957
- import { chmodSync as chmodSync9, existsSync as existsSync15, mkdirSync as mkdirSync22, readFileSync as readFileSync19, renameSync as renameSync16, statSync as statSync8, writeFileSync as writeFileSync21 } from "fs";
6958
- import { dirname as dirname10, join as join26 } from "path";
8800
+ import { chmodSync as chmodSync9, existsSync as existsSync15, mkdirSync as mkdirSync22, readFileSync as readFileSync20, renameSync as renameSync16, statSync as statSync10, writeFileSync as writeFileSync21 } from "fs";
8801
+ import { dirname as dirname12, join as join29 } from "path";
6959
8802
  var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
6960
8803
  var CODEX_MARKER_END = "# <<< deskbuddy <<<";
6961
8804
  var CODEX_EVENTS = [
@@ -6968,7 +8811,7 @@ var CODEX_EVENTS = [
6968
8811
  ];
6969
8812
  var CODEX_TRUST_NOTICE = "\u26A0\uFE0F \u4E0B\u6B21\u542F\u52A8 Codex \u65F6\u4F1A\u8981\u6C42\u4F60\u786E\u8BA4\u4FE1\u4EFB\u8FD9\u4E2A hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F\u2014\u2014\u4E0D\u786E\u8BA4\u7684\u8BDD Codex \u4E0D\u4F1A\u6267\u884C\u5B83\u3002\n DeskBuddy \u5347\u7EA7\uFF08\u542B\u81EA\u52A8\u5347\u7EA7\uFF09\u65B0\u589E hook \u6761\u76EE\u65F6\uFF0CCodex \u53EA\u4F1A\u5C31\u65B0\u6761\u76EE\u518D\u95EE\u4E00\u6B21\uFF0C\u540C\u6837\u9009\u540C\u610F\uFF1B\u539F\u6709\u6761\u76EE\u4E0D\u53D7\u5F71\u54CD\u3001\u72B6\u6001\u7167\u5E38\u4E0A\u62A5\u3002\n Codex \u7ECF `--remote` \u8FDE\u5E38\u9A7B app-server \u4F7F\u7528\u65F6\uFF0C\u7A97\u53E3\u91CC\u7684\u6388\u6743\u6846\u4E5F\u80FD\u5728\u8BBE\u5907\u4E0A\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09";
6970
8813
  function codexConfigPath() {
6971
- return join26(codexHome(), "config.toml");
8814
+ return join29(codexHome(), "config.toml");
6972
8815
  }
6973
8816
  var CODEX_APPENDED_TIMEOUT_S = 3;
6974
8817
  function renderBlock(hookPath, events, tool, platform) {
@@ -6987,10 +8830,10 @@ function renderBlock(hookPath, events, tool, platform) {
6987
8830
  return lines2.join("\n");
6988
8831
  }
6989
8832
  function writeAtomic(path, content) {
6990
- mkdirSync22(dirname10(path), { recursive: true });
8833
+ mkdirSync22(dirname12(path), { recursive: true });
6991
8834
  let mode;
6992
8835
  try {
6993
- mode = statSync8(path).mode & 511;
8836
+ mode = statSync10(path).mode & 511;
6994
8837
  } catch {
6995
8838
  mode = void 0;
6996
8839
  }
@@ -7007,7 +8850,7 @@ function assertSafeToAppend(raw, path) {
7007
8850
  }
7008
8851
  }
7009
8852
  function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex", platform = process.platform) {
7010
- const raw = existsSync15(configPath2) ? readFileSync19(configPath2, "utf8") : "";
8853
+ const raw = existsSync15(configPath2) ? readFileSync20(configPath2, "utf8") : "";
7011
8854
  const block = renderBlock(hookPath, events, tool, platform);
7012
8855
  if (raw.includes(CODEX_MARKER_BEGIN)) {
7013
8856
  const lines2 = raw.split("\n");
@@ -7025,7 +8868,7 @@ function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "
7025
8868
  }
7026
8869
  function uninstallCodexHook(configPath2) {
7027
8870
  if (!existsSync15(configPath2)) return;
7028
- const raw = readFileSync19(configPath2, "utf8");
8871
+ const raw = readFileSync20(configPath2, "utf8");
7029
8872
  const lines2 = raw.split("\n");
7030
8873
  const begin = lines2.findIndex((l) => l.trim() === CODEX_MARKER_BEGIN);
7031
8874
  if (begin === -1) return;
@@ -7039,11 +8882,11 @@ function uninstallCodexHook(configPath2) {
7039
8882
  }
7040
8883
 
7041
8884
  // src/commands/tools.ts
7042
- import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
8885
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
7043
8886
  function fileContains(path, needle) {
7044
8887
  try {
7045
8888
  if (!existsSync16(path)) return false;
7046
- return readFileSync20(path, "utf8").includes(needle);
8889
+ return readFileSync21(path, "utf8").includes(needle);
7047
8890
  } catch {
7048
8891
  return false;
7049
8892
  }
@@ -7052,7 +8895,9 @@ var CC_CONFIG_PATHS = {
7052
8895
  claude_code: claudeSettingsPath,
7053
8896
  workbuddy: workbuddySettingsPath,
7054
8897
  qoder_cn: qoderCnSettingsPath,
7055
- qoder: qoderSettingsPath
8898
+ qoder: qoderSettingsPath,
8899
+ // 千问办公只装状态上报:不装 gate.cjs、不装 rewake(选择题 / 授权由 daemon 的 qwenworkBridge 经 App 内置 MCP 回填)
8900
+ qwenwork: qwenworkSettingsPath
7056
8901
  };
7057
8902
  var WINDOW_GATE_TOOLS = /* @__PURE__ */ new Set(["claude_code", "qoder_cn", "qoder"]);
7058
8903
  var ADAPTERS = {
@@ -7100,6 +8945,26 @@ var ADAPTERS = {
7100
8945
  installWindowGate: () => true
7101
8946
  }
7102
8947
  };
8948
+ var DUMATE_NOTICE = " \u767E\u5EA6\u642D\u5B50\u4E0D\u9700\u8981\u6539\u5B83\u7684\u4EFB\u4F55\u914D\u7F6E\uFF1A\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u76F4\u8FDE\u672C\u673A\u7684\u767E\u5EA6\u642D\u5B50\uFF0C\u72B6\u6001\u3001\u9009\u62E9\u9898\u548C\u6388\u6743\u90FD\u4F1A\u63A8\u5230\u8BBE\u5907";
8949
+ ADAPTERS["dumate-http"] = {
8950
+ configPath: (tool) => tool === "dumate" ? configPath() : null,
8951
+ install: () => {
8952
+ },
8953
+ uninstall: () => {
8954
+ },
8955
+ isInstalled: (p) => {
8956
+ try {
8957
+ const enabled = JSON.parse(readFileSync21(p, "utf8")).enabledTools;
8958
+ return Array.isArray(enabled) && enabled.includes("dumate");
8959
+ } catch {
8960
+ return false;
8961
+ }
8962
+ },
8963
+ defaultEvents: [],
8964
+ notice: DUMATE_NOTICE,
8965
+ // 选择题 / 授权由 DumateWatch 推,不另装
8966
+ installWindowGate: () => true
8967
+ };
7103
8968
  function adapterFor(registry, tool) {
7104
8969
  const format = registry?.tools?.[tool]?.format;
7105
8970
  return format && ADAPTERS[format] || null;
@@ -7120,13 +8985,13 @@ function subscribedEvents(registry, tool, fallback) {
7120
8985
  }
7121
8986
 
7122
8987
  // src/commands/enable.ts
7123
- import { dirname as dirname13, join as join29 } from "path";
8988
+ import { dirname as dirname15, join as join32 } from "path";
7124
8989
 
7125
8990
  // src/wrap/shim.ts
7126
- import { execFileSync as execFileSync12 } from "child_process";
7127
- import { chmodSync as chmodSync10, existsSync as existsSync17, mkdirSync as mkdirSync23, readdirSync as readdirSync6, readFileSync as readFileSync21, rmdirSync, rmSync as rmSync17, writeFileSync as writeFileSync22 } from "fs";
7128
- import { homedir as homedir7 } from "os";
7129
- import { delimiter as delimiter2, join as join27, resolve as resolve3 } from "path";
8991
+ import { execFileSync as execFileSync13 } from "child_process";
8992
+ import { chmodSync as chmodSync10, existsSync as existsSync17, mkdirSync as mkdirSync23, readdirSync as readdirSync7, readFileSync as readFileSync22, rmdirSync, rmSync as rmSync17, writeFileSync as writeFileSync22 } from "fs";
8993
+ import { homedir as homedir8 } from "os";
8994
+ import { delimiter as delimiter2, join as join30, resolve as resolve4 } from "path";
7130
8995
  var SHIM_FALLBACK_EXIT = 213;
7131
8996
  var WIN_SHIM_ENABLED = false;
7132
8997
  var SHIM_COMMANDS = { claude_code: "claude", codex: "codex" };
@@ -7136,10 +9001,10 @@ var RC_END = "# <<< deskbuddy <<<";
7136
9001
  var RC_FILES = [".zshrc", ".bashrc"];
7137
9002
  var WIN_PENDING = "Windows \u7A97\u53E3\u63A5\u7BA1\u5F85\u9A8C\u8BC1\uFF0C\u6682\u672A\u5F00\u542F";
7138
9003
  function shimDir() {
7139
- return join27(deskbuddyHome(), "bin");
9004
+ return join30(deskbuddyHome(), "bin");
7140
9005
  }
7141
9006
  function shimFile(binDir, command, platform = process.platform) {
7142
- return join27(binDir, platform === "win32" ? `${command}.cmd` : command);
9007
+ return join30(binDir, platform === "win32" ? `${command}.cmd` : command);
7143
9008
  }
7144
9009
  function shQuote3(s) {
7145
9010
  return `'${s.replace(/'/g, `'\\''`)}'`;
@@ -7227,7 +9092,7 @@ function cmdScript(tool, command, node, cli, bin) {
7227
9092
  ].join("\r\n");
7228
9093
  }
7229
9094
  function rcBlock(binDir, rcHome) {
7230
- const std = resolve3(rcHome, ".deskbuddy", "bin") === resolve3(binDir);
9095
+ const std = resolve4(rcHome, ".deskbuddy", "bin") === resolve4(binDir);
7231
9096
  const dir = std ? "$HOME/.deskbuddy/bin" : binDir.replace(/(["\\$`])/g, "\\$1");
7232
9097
  return [
7233
9098
  RC_BEGIN,
@@ -7268,7 +9133,7 @@ function removeRcBlock(content) {
7268
9133
  return content.slice(0, start) + content.slice(end);
7269
9134
  }
7270
9135
  function readRc(p) {
7271
- return readFileSync21(p).toString("latin1");
9136
+ return readFileSync22(p).toString("latin1");
7272
9137
  }
7273
9138
  function writeRc(p, latin1) {
7274
9139
  writeFileSync22(p, Buffer.from(latin1, "latin1"));
@@ -7293,7 +9158,7 @@ function removePathEntry(value, dir) {
7293
9158
  var PS_READ = "$k=[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment');$v='';if($k){$v=$k.GetValue('Path','',[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)};[Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$v)))";
7294
9159
  var PS_WRITE = "$k=[Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment');$kind=[Microsoft.Win32.RegistryValueKind]::ExpandString;try{$kind=$k.GetValueKind('Path')}catch{};if([string]::IsNullOrEmpty($env:DESKBUDDY_NEW_PATH)){$k.DeleteValue('Path',$false)}else{$k.SetValue('Path',$env:DESKBUDDY_NEW_PATH,$kind)};[Environment]::SetEnvironmentVariable('DESKBUDDY_PATH_REFRESH','1','User');[Environment]::SetEnvironmentVariable('DESKBUDDY_PATH_REFRESH',$null,'User')";
7295
9160
  function psWinPath() {
7296
- const ps = (script, env) => execFileSync12("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
9161
+ const ps = (script, env) => execFileSync13("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
7297
9162
  encoding: "utf8",
7298
9163
  stdio: ["ignore", "pipe", "ignore"],
7299
9164
  windowsHide: true,
@@ -7316,7 +9181,7 @@ function psWinPath() {
7316
9181
  function defaultShimEnv(nodePath, cliPath) {
7317
9182
  const env = process.env;
7318
9183
  const isolated = !!env.VITEST || !!env.DESKBUDDY_HOME;
7319
- const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir7());
9184
+ const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir8());
7320
9185
  return {
7321
9186
  platform: process.platform,
7322
9187
  binDir: shimDir(),
@@ -7359,7 +9224,7 @@ function installShim(tool, e) {
7359
9224
  } else if (e.rcHome) {
7360
9225
  const block = toLatin1(rcBlock(e.binDir, e.rcHome));
7361
9226
  for (const name of RC_FILES) {
7362
- const rc = join27(e.rcHome, name);
9227
+ const rc = join30(e.rcHome, name);
7363
9228
  if (!existsSync17(rc)) continue;
7364
9229
  const next = addRcBlock(readRc(rc), block);
7365
9230
  if (next !== null) writeRc(rc, next);
@@ -7380,13 +9245,13 @@ function installShim(tool, e) {
7380
9245
  function ourShims(binDir) {
7381
9246
  let names;
7382
9247
  try {
7383
- names = readdirSync6(binDir);
9248
+ names = readdirSync7(binDir);
7384
9249
  } catch {
7385
9250
  return [];
7386
9251
  }
7387
9252
  return names.filter((n) => {
7388
9253
  try {
7389
- return readFileSync21(join27(binDir, n), "utf8").includes(SHIM_MARKER);
9254
+ return readFileSync22(join30(binDir, n), "utf8").includes(SHIM_MARKER);
7390
9255
  } catch {
7391
9256
  return false;
7392
9257
  }
@@ -7398,7 +9263,7 @@ function uninstallShim(tool, e) {
7398
9263
  let removed = false;
7399
9264
  const file = shimFile(e.binDir, command, e.platform);
7400
9265
  try {
7401
- if (existsSync17(file) && readFileSync21(file, "utf8").includes(SHIM_MARKER)) {
9266
+ if (existsSync17(file) && readFileSync22(file, "utf8").includes(SHIM_MARKER)) {
7402
9267
  rmSync17(file, { force: true });
7403
9268
  removed = true;
7404
9269
  }
@@ -7417,7 +9282,7 @@ function uninstallShim(tool, e) {
7417
9282
  }
7418
9283
  } else if (e.rcHome) {
7419
9284
  for (const name of RC_FILES) {
7420
- const rc = join27(e.rcHome, name);
9285
+ const rc = join30(e.rcHome, name);
7421
9286
  if (!existsSync17(rc)) continue;
7422
9287
  const next = removeRcBlock(readRc(rc));
7423
9288
  if (next !== null) {
@@ -7427,7 +9292,7 @@ function uninstallShim(tool, e) {
7427
9292
  }
7428
9293
  }
7429
9294
  try {
7430
- if (existsSync17(e.binDir) && readdirSync6(e.binDir).length === 0) rmdirSync(e.binDir);
9295
+ if (existsSync17(e.binDir) && readdirSync7(e.binDir).length === 0) rmdirSync(e.binDir);
7431
9296
  } catch {
7432
9297
  }
7433
9298
  if (!removed && touched.length === 0) return { ok: true, message: "" };
@@ -7439,7 +9304,7 @@ function shimState(tool, binDir, pathEnv, platform = process.platform) {
7439
9304
  const file = shimFile(binDir, command, platform);
7440
9305
  let installed = false;
7441
9306
  try {
7442
- installed = readFileSync21(file, "utf8").includes(SHIM_MARKER);
9307
+ installed = readFileSync22(file, "utf8").includes(SHIM_MARKER);
7443
9308
  } catch {
7444
9309
  installed = false;
7445
9310
  }
@@ -7450,12 +9315,12 @@ function shimState(tool, binDir, pathEnv, platform = process.platform) {
7450
9315
  }
7451
9316
 
7452
9317
  // src/commands/migrateQoder.ts
7453
- import { dirname as dirname12, join as join28 } from "path";
9318
+ import { dirname as dirname14, join as join31 } from "path";
7454
9319
 
7455
9320
  // src/core/detect.ts
7456
9321
  import { existsSync as existsSync18 } from "fs";
7457
- import { dirname as dirname11 } from "path";
7458
- var ALL_TOOLS = ["claude_code", "codex", "workbuddy", "qoder_cn", "qoder", "opencode"];
9322
+ import { dirname as dirname13 } from "path";
9323
+ var ALL_TOOLS = ["claude_code", "codex", "workbuddy", "qoder_cn", "qoder", "opencode", "qwenwork", "dumate"];
7459
9324
  function toolConfigDir(tool) {
7460
9325
  switch (tool) {
7461
9326
  case "claude_code":
@@ -7463,17 +9328,23 @@ function toolConfigDir(tool) {
7463
9328
  case "codex":
7464
9329
  return codexHome();
7465
9330
  case "workbuddy":
7466
- return dirname11(workbuddySettingsPath());
9331
+ return dirname13(workbuddySettingsPath());
7467
9332
  case "qoder_cn":
7468
- return dirname11(qoderCnSettingsPath());
9333
+ return dirname13(qoderCnSettingsPath());
7469
9334
  case "qoder":
7470
- return dirname11(qoderSettingsPath());
9335
+ return dirname13(qoderSettingsPath());
7471
9336
  case "opencode":
7472
- return dirname11(dirname11(opencodePluginPath()));
9337
+ return dirname13(dirname13(opencodePluginPath()));
9338
+ case "qwenwork":
9339
+ return qwenworkHome();
7473
9340
  default:
7474
9341
  return null;
7475
9342
  }
7476
9343
  }
9344
+ function toolDetectDir(tool) {
9345
+ if (tool === "dumate") return qianfanHome();
9346
+ return toolConfigDir(tool);
9347
+ }
7477
9348
  var defaultDetectDeps = () => ({
7478
9349
  located: (tool) => locate(tool) != null,
7479
9350
  exists: existsSync18,
@@ -7488,7 +9359,7 @@ function detectTools(d = defaultDetectDeps()) {
7488
9359
  return ALL_TOOLS.filter((tool) => {
7489
9360
  if (tool === caller) return true;
7490
9361
  if (d.located(tool)) return true;
7491
- const dir = toolConfigDir(tool);
9362
+ const dir = toolDetectDir(tool);
7492
9363
  return dir != null && d.exists(dir);
7493
9364
  });
7494
9365
  }
@@ -7498,7 +9369,7 @@ function migrateLegacyQoder(out, detect = () => detectTools()) {
7498
9369
  try {
7499
9370
  const cfg = loadConfig();
7500
9371
  const hookPath = installedHookPath();
7501
- const scripts = [hookPath, join28(dirname12(hookPath), "gate.cjs")];
9372
+ const scripts = [hookPath, join31(dirname14(hookPath), "gate.cjs")];
7502
9373
  if (!retagCcHook(qoderCnSettingsPath(), scripts, "qoder", "qoder_cn")) return false;
7503
9374
  const hadQoder = cfg.enabledTools.includes("qoder");
7504
9375
  let intl = false;
@@ -7549,7 +9420,8 @@ function installShims(tools, out, shim) {
7549
9420
  const r = installShim(tool, shim);
7550
9421
  if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
7551
9422
  if (r.ok && tool === "claude_code" && (!loadPty() || !loadHeadless())) {
7552
- out("\u26A0\uFE0F node-pty \u6CA1\u88C5\u4E0A\uFF1A\u7A97\u53E3\u63A5\u7BA1\u5148\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C\uFF08claude \u7167\u5E38\u80FD\u7528\uFF0C\u53EA\u662F\u8BBE\u5907\u56DE\u590D\u5199\u4E0D\u8FDB\u7A97\u53E3\uFF09");
9423
+ const why = loadPty() ? "@xterm/headless \u6CA1\u88C5\u4E0A" : ptyMissingReason();
9424
+ out(`\u26A0\uFE0F ${why}\uFF1A\u7A97\u53E3\u63A5\u7BA1\u5148\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C\uFF08claude \u7167\u5E38\u80FD\u7528\uFF0C\u53EA\u662F\u8BBE\u5907\u56DE\u590D\u5199\u4E0D\u8FDB\u7A97\u53E3\uFF09`);
7553
9425
  }
7554
9426
  } catch (e) {
7555
9427
  out(`\u26A0\uFE0F \u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\u4E0A\uFF1A${e.message}\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09`);
@@ -7590,14 +9462,14 @@ function installTools(tools, out, shim = defaultShimEnvForInstall()) {
7590
9462
  installed.push(tool);
7591
9463
  out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
7592
9464
  try {
7593
- if (adapter.installWindowGate?.(configPath2, join29(dirname13(hookPath), "gate.cjs"), tool)) {
9465
+ if (adapter.installWindowGate?.(configPath2, join32(dirname15(hookPath), "gate.cjs"), tool)) {
7594
9466
  out(` \u7A97\u53E3\u91CC\u5F39\u6388\u6743\u6846\u65F6\uFF0C\u8BBE\u5907\u4E0A\u4E5F\u80FD\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09`);
7595
9467
  }
7596
9468
  } catch (e) {
7597
9469
  out(`\u26A0\uFE0F ${tool}\uFF1A\u8BBE\u5907\u786E\u8BA4\u6388\u6743\u6CA1\u88C5\u4E0A\uFF1A${e.message}`);
7598
9470
  }
7599
9471
  try {
7600
- if (adapter.installRewake?.(configPath2, join29(dirname13(hookPath), "wake.cjs"), tool)) {
9472
+ if (adapter.installRewake?.(configPath2, join32(dirname15(hookPath), "wake.cjs"), tool)) {
7601
9473
  out(` \u7A97\u53E3\u5F00\u7740\u65F6\uFF0C\u8BBE\u5907\u6307\u4EE4\u76F4\u63A5\u5728\u8FD9\u4E2A\u7A97\u53E3\u91CC\u6267\u884C\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09`);
7602
9474
  }
7603
9475
  } catch (e) {
@@ -7699,27 +9571,27 @@ async function runDisable(tools, opts = {}) {
7699
9571
 
7700
9572
  // src/commands/install.ts
7701
9573
  import { mkdirSync as mkdirSync24, rmSync as rmSync18, writeFileSync as writeFileSync23 } from "fs";
7702
- import { join as join30 } from "path";
9574
+ import { join as join33 } from "path";
7703
9575
 
7704
9576
  // src/commands/setup.ts
7705
9577
  import qrcode from "qrcode-terminal";
7706
9578
 
7707
9579
  // src/commands/remoteCwd.ts
7708
- import { existsSync as existsSync19, statSync as statSync9 } from "fs";
9580
+ import { existsSync as existsSync19, statSync as statSync11 } from "fs";
7709
9581
  import { createInterface } from "readline";
7710
- import { homedir as homedir8 } from "os";
7711
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
7712
- function normalizePath(raw, home = homedir8()) {
9582
+ import { homedir as homedir9 } from "os";
9583
+ import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
9584
+ function normalizePath(raw, home = homedir9()) {
7713
9585
  const s = (raw || "").trim();
7714
9586
  if (!s) return null;
7715
- const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ? resolve4(home, s.slice(2)) : s;
7716
- const abs = isAbsolute2(expanded) ? expanded : resolve4(process.cwd(), expanded);
9587
+ const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ? resolve5(home, s.slice(2)) : s;
9588
+ const abs = isAbsolute2(expanded) ? expanded : resolve5(process.cwd(), expanded);
7717
9589
  return abs;
7718
9590
  }
7719
9591
  function checkPath(abs) {
7720
9592
  if (!existsSync19(abs)) return "missing";
7721
9593
  try {
7722
- return statSync9(abs).isDirectory() ? "ok" : "not_a_dir";
9594
+ return statSync11(abs).isDirectory() ? "ok" : "not_a_dir";
7723
9595
  } catch {
7724
9596
  return "missing";
7725
9597
  }
@@ -7828,7 +9700,7 @@ function parseApiBase(argv) {
7828
9700
  }
7829
9701
  return value;
7830
9702
  }
7831
- function sleep(ms) {
9703
+ function sleep2(ms) {
7832
9704
  return new Promise((r) => setTimeout(r, ms));
7833
9705
  }
7834
9706
  function stdout3(line) {
@@ -7851,7 +9723,7 @@ async function runSetup(opts = {}) {
7851
9723
  await pollForBinding(
7852
9724
  authCode,
7853
9725
  { intervalSec: interval },
7854
- { authToken: (code) => api.authToken(code), sleep, now: () => Date.now() }
9726
+ { authToken: (code) => api.authToken(code), sleep: sleep2, now: () => Date.now() }
7855
9727
  );
7856
9728
  markBound();
7857
9729
  await refreshRegistry();
@@ -7889,7 +9761,9 @@ var TOOL_LABELS = {
7889
9761
  workbuddy: "WorkBuddy",
7890
9762
  qoder_cn: "Qoder CN",
7891
9763
  qoder: "Qoder",
7892
- opencode: "opencode"
9764
+ opencode: "opencode",
9765
+ qwenwork: "\u5343\u95EE\u529E\u516C",
9766
+ dumate: "\u767E\u5EA6\u642D\u5B50"
7893
9767
  };
7894
9768
  function checkApiBase(raw, env = process.env) {
7895
9769
  const norm3 = raw.replace(/\/+$/, "");
@@ -7924,7 +9798,7 @@ function inSandbox(env) {
7924
9798
  }
7925
9799
  function precheckWritable(dirs) {
7926
9800
  for (const dir of dirs) {
7927
- const probe = join30(dir, `.deskbuddy-probe-${process.pid}`);
9801
+ const probe = join33(dir, `.deskbuddy-probe-${process.pid}`);
7928
9802
  try {
7929
9803
  mkdirSync24(dir, { recursive: true });
7930
9804
  writeFileSync23(probe, "");
@@ -8062,11 +9936,11 @@ function stdout4(line) {
8062
9936
  process.stdout.write(line + "\n");
8063
9937
  }
8064
9938
  function askYesNo(question) {
8065
- return new Promise((resolve6) => {
9939
+ return new Promise((resolve7) => {
8066
9940
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
8067
9941
  rl.question(question, (answer) => {
8068
9942
  rl.close();
8069
- resolve6(/^(y|yes)$/i.test(answer.trim()));
9943
+ resolve7(/^(y|yes)$/i.test(answer.trim()));
8070
9944
  });
8071
9945
  });
8072
9946
  }
@@ -8151,7 +10025,7 @@ function formatStatus(input) {
8151
10025
  lines2.push(` ${pad("hook \u7248\u672C", 10)}${versionNote}`);
8152
10026
  if (input.windowWrap) {
8153
10027
  const w = input.windowWrap;
8154
- const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable claude_code" : !w.inPath ? `\u672A\u751F\u6548\uFF08\u5F53\u524D shell \u7684 PATH \u91CC\u6CA1\u6709 ${w.binDir}\uFF0C\u65B0\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u518D\u770B\uFF09` : !w.ptyOk ? "\u5DF2\u5F00\u542F\uFF0C\u4F46 node-pty \u6CA1\u88C5\u4E0A\uFF0C\u6682\u65F6\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C" : "\u5DF2\u5F00\u542F\uFF08\u7EC8\u7AEF\u91CC\u6572 claude \u4F1A\u7ECF DeskBuddy \u6253\u5F00\uFF0C\u8BBE\u5907\u56DE\u590D\u80FD\u5199\u8FDB\u7A97\u53E3\uFF09";
10028
+ const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable claude_code" : !w.inPath ? `\u672A\u751F\u6548\uFF08\u5F53\u524D shell \u7684 PATH \u91CC\u6CA1\u6709 ${w.binDir}\uFF0C\u65B0\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u518D\u770B\uFF09` : !w.ptyOk ? `\u5DF2\u5F00\u542F\uFF0C\u4F46 ${w.ptyWhy ?? "node-pty \u6CA1\u88C5\u4E0A"}\uFF0C\u6682\u65F6\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C` : "\u5DF2\u5F00\u542F\uFF08\u7EC8\u7AEF\u91CC\u6572 claude \u4F1A\u7ECF DeskBuddy \u6253\u5F00\uFF0C\u8BBE\u5907\u56DE\u590D\u80FD\u5199\u8FDB\u7A97\u53E3\uFF09";
8155
10029
  lines2.push(` ${pad("\u7A97\u53E3\u63A5\u7BA1", 10)}${text}`);
8156
10030
  }
8157
10031
  if (input.codexWrap) {
@@ -8196,7 +10070,7 @@ function collectStatus(now = Date.now()) {
8196
10070
  installed,
8197
10071
  notice: installed ? adapter.notice : void 0
8198
10072
  });
8199
- sessions.push(...readAliveStates(tool));
10073
+ sessions.push(...readAliveStates(tool, { now }));
8200
10074
  }
8201
10075
  const wrapTool = config.enabledTools.find((t) => t === "claude_code" && SHIM_COMMANDS[t]);
8202
10076
  let windowWrap;
@@ -8208,7 +10082,15 @@ function collectStatus(now = Date.now()) {
8208
10082
  if (wrapTool) {
8209
10083
  const binDir = shimDir();
8210
10084
  const st = shimState(wrapTool, binDir, process.env.PATH ?? "");
8211
- windowWrap = { ...st, binDir, ptyOk: !!loadPty() && !!loadHeadless(), winPending: process.platform === "win32" && !WIN_SHIM_ENABLED };
10085
+ const pty = !!loadPty();
10086
+ const headless = !!loadHeadless();
10087
+ windowWrap = {
10088
+ ...st,
10089
+ binDir,
10090
+ ptyOk: pty && headless,
10091
+ ptyWhy: !pty ? ptyMissingReason() : !headless ? "@xterm/headless \u6CA1\u88C5\u4E0A" : void 0,
10092
+ winPending: process.platform === "win32" && !WIN_SHIM_ENABLED
10093
+ };
8212
10094
  }
8213
10095
  return {
8214
10096
  windowWrap,
@@ -8277,8 +10159,8 @@ async function runAutoSwitch(opts) {
8277
10159
 
8278
10160
  // src/wrap/run.ts
8279
10161
  import { randomBytes as randomBytes9 } from "crypto";
8280
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync26, statSync as statSync11, truncateSync as truncateSync3 } from "fs";
8281
- import { join as join33 } from "path";
10162
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync26, statSync as statSync13, truncateSync as truncateSync3 } from "fs";
10163
+ import { join as join36 } from "path";
8282
10164
  import { StringDecoder } from "string_decoder";
8283
10165
 
8284
10166
  // src/wrap/injector.ts
@@ -8526,10 +10408,10 @@ var Injector = class {
8526
10408
  };
8527
10409
 
8528
10410
  // src/wrap/passthrough.ts
8529
- import { spawn as spawn4 } from "child_process";
8530
- import { realpathSync as realpathSync3 } from "fs";
10411
+ import { spawn as spawn5 } from "child_process";
10412
+ import { realpathSync as realpathSync4 } from "fs";
8531
10413
  import { constants as constants2 } from "os";
8532
- import { dirname as dirname14, resolve as resolve5 } from "path";
10414
+ import { dirname as dirname16, resolve as resolve6 } from "path";
8533
10415
  var CLAUDE_SUBCOMMANDS = /* @__PURE__ */ new Set([
8534
10416
  "agents",
8535
10417
  "attach",
@@ -8580,12 +10462,12 @@ function passthroughReason(i) {
8580
10462
  return null;
8581
10463
  }
8582
10464
  function norm2(p, win) {
8583
- const r = resolve5(p).replace(/[\\/]+$/, "");
10465
+ const r = resolve6(p).replace(/[\\/]+$/, "");
8584
10466
  return win ? r.replace(/\//g, "\\").toLowerCase() : r;
8585
10467
  }
8586
10468
  function real(p) {
8587
10469
  try {
8588
- return realpathSync3(p);
10470
+ return realpathSync4(p);
8589
10471
  } catch {
8590
10472
  return p;
8591
10473
  }
@@ -8593,7 +10475,7 @@ function real(p) {
8593
10475
  function findReal(tool, d = defaultLocateDeps(), skip = shimDir()) {
8594
10476
  const win = (d.platform ?? process.platform) === "win32";
8595
10477
  const skipKeys = /* @__PURE__ */ new Set([norm2(skip, win), norm2(real(skip), win)]);
8596
- const inSkip = (file) => skipKeys.has(norm2(dirname14(file), win)) || skipKeys.has(norm2(dirname14(real(file)), win));
10478
+ const inSkip = (file) => skipKeys.has(norm2(dirname16(file), win)) || skipKeys.has(norm2(dirname16(real(file)), win));
8597
10479
  const dd = { ...d, skipDirs: [...d.skipDirs ?? [], skip] };
8598
10480
  const name = SHIM_COMMANDS[tool];
8599
10481
  if (!name) {
@@ -8622,7 +10504,7 @@ function runDirect(real2, args, env = process.env) {
8622
10504
  return new Promise((done) => {
8623
10505
  let child;
8624
10506
  try {
8625
- child = spawn4(real2.file, [...real2.prefixArgs, ...args], {
10507
+ child = spawn5(real2.file, [...real2.prefixArgs, ...args], {
8626
10508
  stdio: "inherit",
8627
10509
  // 🔴 再经过垫片也直接放行,不会绕回来
8628
10510
  env: withoutShimFlag({ ...env, DESKBUDDY_WRAPPED: "1" }),
@@ -8672,21 +10554,21 @@ function runDirect(real2, args, env = process.env) {
8672
10554
  }
8673
10555
 
8674
10556
  // src/wrap/codexLaunch.ts
8675
- import { spawn as spawn5 } from "child_process";
8676
- import { closeSync as closeSync4, mkdirSync as mkdirSync25, openSync as openSync4, readdirSync as readdirSync7, readFileSync as readFileSync22, rmSync as rmSync20, statSync as statSync10, truncateSync as truncateSync2, writeFileSync as writeFileSync24, renameSync as renameSync17 } from "fs";
10557
+ import { spawn as spawn6 } from "child_process";
10558
+ import { closeSync as closeSync5, mkdirSync as mkdirSync25, openSync as openSync5, readdirSync as readdirSync8, readFileSync as readFileSync23, rmSync as rmSync20, statSync as statSync12, truncateSync as truncateSync2, writeFileSync as writeFileSync24, renameSync as renameSync17 } from "fs";
8677
10559
  import { request } from "http";
8678
10560
  import { createServer as createServer4 } from "net";
8679
- import { join as join31 } from "path";
10561
+ import { join as join34 } from "path";
8680
10562
 
8681
10563
  // src/core/codexServer.ts
8682
- import { execFileSync as execFileSync13 } from "child_process";
10564
+ import { execFileSync as execFileSync14 } from "child_process";
8683
10565
  function isAppServerArgs(args, url) {
8684
10566
  return /\bapp-server\b/.test(args) && new RegExp(`--listen[= ]${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$)`).test(args);
8685
10567
  }
8686
10568
  function processArgs(pid) {
8687
10569
  if (process.platform === "win32") return null;
8688
10570
  try {
8689
- const out = execFileSync13("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
10571
+ const out = execFileSync14("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
8690
10572
  encoding: "utf8",
8691
10573
  stdio: ["ignore", "pipe", "ignore"]
8692
10574
  });
@@ -8844,10 +10726,10 @@ async function runCodexWindow(real2, args, d) {
8844
10726
  }
8845
10727
  var MAX_LOG_BYTES2 = 1024 * 1024;
8846
10728
  function appServerLogPath() {
8847
- return join31(deskbuddyHome(), "codex-app-server.log");
10729
+ return join34(deskbuddyHome(), "codex-app-server.log");
8848
10730
  }
8849
10731
  function windowsDir2() {
8850
- return join31(deskbuddyHome(), "codex-windows");
10732
+ return join34(deskbuddyHome(), "codex-windows");
8851
10733
  }
8852
10734
  function pidAlive2(pid) {
8853
10735
  try {
@@ -8860,7 +10742,7 @@ function pidAlive2(pid) {
8860
10742
  function daemonAlive(pidFile = pidFilePath(), args = processArgs) {
8861
10743
  let pid = 0;
8862
10744
  try {
8863
- pid = Number(readFileSync22(pidFile, "utf8").trim());
10745
+ pid = Number(readFileSync23(pidFile, "utf8").trim());
8864
10746
  } catch {
8865
10747
  return false;
8866
10748
  }
@@ -8870,25 +10752,25 @@ function daemonAlive(pidFile = pidFilePath(), args = processArgs) {
8870
10752
  }
8871
10753
  function probeReady(url) {
8872
10754
  const port = Number(url.slice(url.lastIndexOf(":") + 1));
8873
- return new Promise((resolve6) => {
10755
+ return new Promise((resolve7) => {
8874
10756
  const req2 = request({ host: "127.0.0.1", port, path: "/readyz", method: "GET", timeout: 800 }, (res) => {
8875
10757
  res.resume();
8876
- resolve6((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
10758
+ resolve7((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
8877
10759
  });
8878
10760
  req2.on("timeout", () => req2.destroy());
8879
- req2.on("error", () => resolve6(false));
10761
+ req2.on("error", () => resolve7(false));
8880
10762
  req2.end();
8881
10763
  });
8882
10764
  }
8883
10765
  function freePort() {
8884
- return new Promise((resolve6, reject) => {
10766
+ return new Promise((resolve7, reject) => {
8885
10767
  const srv = createServer4();
8886
10768
  srv.unref();
8887
10769
  srv.on("error", reject);
8888
10770
  srv.listen(0, "127.0.0.1", () => {
8889
10771
  const addr = srv.address();
8890
10772
  const port = typeof addr === "object" && addr ? addr.port : 0;
8891
- srv.close(() => port ? resolve6(port) : reject(new Error("no port")));
10773
+ srv.close(() => port ? resolve7(port) : reject(new Error("no port")));
8892
10774
  });
8893
10775
  });
8894
10776
  }
@@ -8896,14 +10778,14 @@ function startServer(real2, url, env, cwd) {
8896
10778
  const logPath2 = appServerLogPath();
8897
10779
  mkdirSync25(deskbuddyHome(), { recursive: true });
8898
10780
  try {
8899
- if (statSync10(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
10781
+ if (statSync12(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
8900
10782
  } catch {
8901
10783
  }
8902
- const fd = openSync4(logPath2, "a");
10784
+ const fd = openSync5(logPath2, "a");
8903
10785
  try {
8904
10786
  const childEnv2 = { ...env, DESKBUDDY_WRAPPED: "1" };
8905
10787
  delete childEnv2.DESKBUDDY_SHIM;
8906
- const child = spawn5(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
10788
+ const child = spawn6(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
8907
10789
  detached: true,
8908
10790
  stdio: ["ignore", fd, fd],
8909
10791
  cwd,
@@ -8914,7 +10796,7 @@ function startServer(real2, url, env, cwd) {
8914
10796
  child.unref();
8915
10797
  return child.pid ?? null;
8916
10798
  } finally {
8917
- closeSync4(fd);
10799
+ closeSync5(fd);
8918
10800
  }
8919
10801
  }
8920
10802
  function killServer(pid) {
@@ -8928,7 +10810,7 @@ function killOwnServer(pid, url) {
8928
10810
  if (a && isAppServerArgs(a, url)) killServer(pid);
8929
10811
  }
8930
10812
  function recordPath(rec) {
8931
- return join31(windowsDir2(), `${rec.server}.json`);
10813
+ return join34(windowsDir2(), `${rec.server}.json`);
8932
10814
  }
8933
10815
  function writeRecord(rec) {
8934
10816
  try {
@@ -8948,15 +10830,15 @@ function removeRecord(rec) {
8948
10830
  function reapWindows(dir = windowsDir2(), alive = pidAlive2, args = processArgs, kill = killServer) {
8949
10831
  let names = [];
8950
10832
  try {
8951
- names = readdirSync7(dir).filter((n) => n.endsWith(".json"));
10833
+ names = readdirSync8(dir).filter((n) => n.endsWith(".json"));
8952
10834
  } catch {
8953
10835
  return;
8954
10836
  }
8955
10837
  for (const n of names) {
8956
- const p = join31(dir, n);
10838
+ const p = join34(dir, n);
8957
10839
  let rec;
8958
10840
  try {
8959
- rec = JSON.parse(readFileSync22(p, "utf8"));
10841
+ rec = JSON.parse(readFileSync23(p, "utf8"));
8960
10842
  } catch {
8961
10843
  rmSync20(p, { force: true });
8962
10844
  continue;
@@ -8976,7 +10858,7 @@ function reapWindows(dir = windowsDir2(), alive = pidAlive2, args = processArgs,
8976
10858
  function launchLog(line) {
8977
10859
  try {
8978
10860
  mkdirSync25(deskbuddyHome(), { recursive: true });
8979
- writeFileSync24(join31(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
10861
+ writeFileSync24(join34(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
8980
10862
  `, { flag: "a" });
8981
10863
  } catch {
8982
10864
  }
@@ -9000,15 +10882,15 @@ function defaultCodexWindowDeps(base) {
9000
10882
 
9001
10883
  // src/wrap/promptLaunch.ts
9002
10884
  import { randomBytes as randomBytes8 } from "crypto";
9003
- import { homedir as homedir9 } from "os";
9004
- import { join as join32 } from "path";
10885
+ import { homedir as homedir10 } from "os";
10886
+ import { join as join35 } from "path";
9005
10887
  var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
9006
10888
  function promptArgs(text) {
9007
10889
  return text ? ["--", prepare(text)] : [];
9008
10890
  }
9009
10891
  function promptEnv(tool, env) {
9010
10892
  const out = { ...env };
9011
- if (tool === "workbuddy" && !out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join32(homedir9(), ".workbuddy");
10893
+ if (tool === "workbuddy" && !out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join35(homedir10(), ".workbuddy");
9012
10894
  return out;
9013
10895
  }
9014
10896
  async function runPromptLaunch(tool, windowId, cwd, d) {
@@ -9065,7 +10947,7 @@ function receiveFirstLine(windowId, tool, cwd, opts = {}) {
9065
10947
  cleanup(null);
9066
10948
  return Promise.resolve(null);
9067
10949
  }
9068
- return new Promise((resolve6) => {
10950
+ return new Promise((resolve7) => {
9069
10951
  let server = null;
9070
10952
  let done = false;
9071
10953
  let firstId = "";
@@ -9076,7 +10958,7 @@ function receiveFirstLine(windowId, tool, cwd, opts = {}) {
9076
10958
  done = true;
9077
10959
  clearTimeout(timer);
9078
10960
  if (!v) cleanup(server);
9079
- resolve6(v);
10961
+ resolve7(v);
9080
10962
  };
9081
10963
  const timer = setTimeout(() => finish(null), opts.waitMs ?? FIRST_LINE_WAIT_MS2);
9082
10964
  void startChannelServer(
@@ -9139,10 +11021,10 @@ function childEnv(src, windowId) {
9139
11021
  }
9140
11022
  function wrapLog(line) {
9141
11023
  try {
9142
- const p = join33(deskbuddyHome(), "wrap.log");
11024
+ const p = join36(deskbuddyHome(), "wrap.log");
9143
11025
  mkdirSync26(deskbuddyHome(), { recursive: true });
9144
11026
  try {
9145
- if (statSync11(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
11027
+ if (statSync13(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
9146
11028
  } catch {
9147
11029
  }
9148
11030
  appendFileSync2(p, `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${line}
@@ -9459,7 +11341,7 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
9459
11341
  injector.tick().catch(() => {
9460
11342
  });
9461
11343
  }, TICK_MS);
9462
- return new Promise((resolve6) => {
11344
+ return new Promise((resolve7) => {
9463
11345
  let degraded = false;
9464
11346
  const onFatal = (e) => {
9465
11347
  deps.log(`\u5305\u88C5\u5C42\u5F02\u5E38\uFF1A${e?.stack ?? String(e)}`);
@@ -9510,8 +11392,8 @@ ${WRAP_HINT}\uFF08\u8BBE\u5907\u56DE\u590D\u6682\u65F6\u5199\u4E0D\u8FDB\u8FD9\u
9510
11392
  restoreTerminal();
9511
11393
  process.removeListener("uncaughtException", onFatal);
9512
11394
  for (const [s, h] of sigHandlers) process.removeListener(s, h);
9513
- if (pending > 0) setTimeout(() => resolve6(code), 150);
9514
- else resolve6(code);
11395
+ if (pending > 0) setTimeout(() => resolve7(code), 150);
11396
+ else resolve7(code);
9515
11397
  }
9516
11398
  process.on("uncaughtException", onFatal);
9517
11399
  for (const [s, h] of sigHandlers) {