deskbuddy 0.2.0-beta.11 → 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 +2511 -430
  2. package/dist/gate.cjs +21 -13
  3. package/dist/hook.cjs +376 -18
  4. package/dist/ocTranslate.mjs +106 -0
  5. package/dist/opencode.mjs +104 -59
  6. package/hook/gate.cjs +21 -13
  7. package/hook/hook-debug.log +5 -0
  8. package/hook/hook.cjs +376 -18
  9. package/hook/ocTranslate.d.mts +7 -0
  10. package/hook/ocTranslate.mjs +106 -0
  11. package/hook/opencode.d.mts +5 -1
  12. package/hook/opencode.mjs +104 -59
  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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/daemon.ts
4
- import { writeFileSync as writeFileSync19, rmSync as rmSync15 } from "fs";
4
+ import { writeFileSync as writeFileSync18, rmSync as rmSync15 } from "fs";
5
5
 
6
6
  // src/core/api.ts
7
7
  import { execFileSync } from "child_process";
@@ -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 {
@@ -1439,6 +1521,8 @@ function writeEnded(s, now = Date.now()) {
1439
1521
  const dir = endedDir();
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 };
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;
1442
1526
  const file = join8(dir, `${s.sessionId}.json`);
1443
1527
  const tmp = `${file}.${process.pid}.tmp`;
1444
1528
  writeFileSync6(tmp, JSON.stringify(rec), { encoding: "utf8", mode: 384 });
@@ -1561,6 +1645,12 @@ function readState(sessionId) {
1561
1645
  return null;
1562
1646
  }
1563
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
+ }
1564
1654
  function retireState(s, now = Date.now()) {
1565
1655
  writeEnded(s, now);
1566
1656
  removeState(s.sessionId);
@@ -1571,8 +1661,26 @@ function removeState(sessionId) {
1571
1661
  } catch {
1572
1662
  }
1573
1663
  }
1574
- 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 = {}) {
1575
1680
  if (!existsSync8(stateDir())) return [];
1681
+ const now = opts.now ?? Date.now();
1682
+ let pend = null;
1683
+ const pending = () => pend ??= pendingApprovalSessions();
1576
1684
  const alive = [];
1577
1685
  for (const file of readdirSync3(stateDir())) {
1578
1686
  if (!file.endsWith(".json")) continue;
@@ -1588,6 +1696,14 @@ function readAliveStates(tool) {
1588
1696
  retireState(s);
1589
1697
  continue;
1590
1698
  }
1699
+ if (isStaleNoPid(s, now, pending)) {
1700
+ try {
1701
+ opts.onStale?.(s);
1702
+ } catch {
1703
+ }
1704
+ retireState(s);
1705
+ continue;
1706
+ }
1591
1707
  alive.push(s);
1592
1708
  }
1593
1709
  return alive;
@@ -1636,19 +1752,27 @@ async function postReport(payload) {
1636
1752
  import { basename as basename3 } from "path";
1637
1753
  var KEEPALIVE_MS = 2 * 60 * 1e3;
1638
1754
  var MAX_SESSIONS = 20;
1639
- var CAPS = ["cmd_result", "window_inject", "resume_ended", "reply_idle_window"];
1755
+ var CAPS = ["cmd_result", "window_inject", "resume_ended", "reply_idle_window", "turns"];
1756
+ function turnsOf(s) {
1757
+ return Number.isInteger(s.turns) && s.turns >= 0 ? s.turns : void 0;
1758
+ }
1640
1759
  function buildSnapshot(states, tool, cfg, nowMs, remote) {
1641
- const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => ({
1642
- sessionId: s.sessionId,
1643
- title: s.title,
1644
- // 隐私:只带目录名,不带全路径——全路径会暴露公司目录结构与用户名
1645
- project: basename3(s.project || ""),
1646
- state: s.state,
1647
- startedAt: s.startedAt,
1648
- updatedAt: s.updatedAt,
1649
- interactive: s.interactive === true,
1650
- remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
1651
- }));
1760
+ const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => {
1761
+ const o = {
1762
+ sessionId: s.sessionId,
1763
+ title: s.title,
1764
+ // 隐私:只带目录名,不带全路径——全路径会暴露公司目录结构与用户名
1765
+ project: basename3(s.project || ""),
1766
+ state: s.state,
1767
+ startedAt: s.startedAt,
1768
+ updatedAt: s.updatedAt,
1769
+ interactive: s.interactive === true,
1770
+ remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
1771
+ };
1772
+ const turns = turnsOf(s);
1773
+ if (turns !== void 0) o.turns = turns;
1774
+ return o;
1775
+ });
1652
1776
  return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions, caps: [...CAPS] };
1653
1777
  }
1654
1778
 
@@ -1803,7 +1927,13 @@ function parseApprovalFrame(frame) {
1803
1927
  if (!isValidId(approvalId)) return null;
1804
1928
  const decision = str(d.decision);
1805
1929
  if (decision !== "allow" && decision !== "deny" && decision !== "allow_always") return null;
1806
- 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
+ };
1807
1937
  }
1808
1938
  function cleanDeviceName(v) {
1809
1939
  return typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 32) : "";
@@ -1819,7 +1949,7 @@ import { join as join12 } from "path";
1819
1949
 
1820
1950
  // src/daemon/gate/settings.ts
1821
1951
  import { existsSync as existsSync10 } from "fs";
1822
- import { fileURLToPath as fileURLToPath3 } from "url";
1952
+ import { fileURLToPath as fileURLToPath4 } from "url";
1823
1953
  var GATE_HOOK_TIMEOUT_S = 7 * 24 * 60 * 60;
1824
1954
  function waitModeFor(tool) {
1825
1955
  return tool === "claude_code" ? "defer" : "hold";
@@ -1842,8 +1972,8 @@ function winPsPath(p) {
1842
1972
  }
1843
1973
  function defaultGateScript() {
1844
1974
  const candidates = [
1845
- fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
1846
- 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))
1847
1977
  ];
1848
1978
  return candidates.find((p) => existsSync10(p)) ?? candidates[0];
1849
1979
  }
@@ -1877,6 +2007,10 @@ import { execFileSync as execFileSync6 } from "child_process";
1877
2007
  import { accessSync, constants, existsSync as existsSync11, lstatSync, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
1878
2008
  import { homedir as homedir4 } from "os";
1879
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
+ }
1880
2014
  var defaultLocateDeps = () => ({
1881
2015
  home: homedir4(),
1882
2016
  path: process.env.PATH ?? "",
@@ -2075,14 +2209,14 @@ function locateWin(tool, d) {
2075
2209
  case "qoder_cn": {
2076
2210
  for (const dir of winAppDirs(d, "Qoder CN")) {
2077
2211
  const p = join11(dir, QODER_CN_WORKER_IN_WIN_APP);
2078
- if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
2212
+ if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder CN.exe"));
2079
2213
  }
2080
2214
  return null;
2081
2215
  }
2082
2216
  case "qoder": {
2083
2217
  for (const dir of winAppDirs(d, "Qoder")) {
2084
2218
  const p = join11(dir, QODER_WORKER_IN_WIN_APP);
2085
- if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
2219
+ if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder.exe"));
2086
2220
  }
2087
2221
  return plain("qodercli");
2088
2222
  }
@@ -2119,14 +2253,14 @@ function locate(tool, d = defaultLocateDeps()) {
2119
2253
  case "qoder_cn": {
2120
2254
  for (const dir of appDirs(d.home)) {
2121
2255
  const p = join11(dir, QODER_CN_WORKER_IN_APP);
2122
- 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"));
2123
2257
  }
2124
2258
  return null;
2125
2259
  }
2126
2260
  case "qoder": {
2127
2261
  for (const dir of appDirs(d.home)) {
2128
2262
  const p = join11(dir, QODER_WORKER_IN_APP);
2129
- 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"));
2130
2264
  }
2131
2265
  return plain("qodercli");
2132
2266
  }
@@ -2253,7 +2387,7 @@ function materialize(plan, tool, d = defaultLocateDeps()) {
2253
2387
  file: loc.file,
2254
2388
  args: [...loc.prefixArgs, ...plan.args],
2255
2389
  cwd: plan.cwd,
2256
- env: { ...plan.env, PATH: childPath(d) }
2390
+ env: { ...plan.env, ...loc.env, PATH: childPath(d) }
2257
2391
  };
2258
2392
  }
2259
2393
  function isDeferred(stdout7) {
@@ -2317,6 +2451,17 @@ function parseSessionId(tool, stdout7) {
2317
2451
  if (tool === "opencode") return parseOpencodeSessionId(stdout7);
2318
2452
  return parseClaudeSessionId(stdout7);
2319
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
+ }
2320
2465
  function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2321
2466
  let remaining = limitMs;
2322
2467
  let startedAt = Date.now();
@@ -2332,7 +2477,7 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2332
2477
  }, Math.max(0, remaining));
2333
2478
  };
2334
2479
  let timedOut = false;
2335
- const done = new Promise((resolve6) => {
2480
+ const done = new Promise((resolve7) => {
2336
2481
  child = execFile3(
2337
2482
  plan.file,
2338
2483
  plan.args,
@@ -2344,16 +2489,18 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2344
2489
  // Windows:别给子进程弹黑框
2345
2490
  windowsHide: true
2346
2491
  },
2347
- (err, stdout7) => {
2492
+ (err, stdout7, stderr) => {
2348
2493
  finished = true;
2349
2494
  if (timer) clearTimeout(timer);
2350
2495
  const out = String(stdout7 ?? "");
2496
+ const errOut = String(stderr ?? "");
2351
2497
  if (err || timedOut) {
2352
- const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : err.message;
2353
- 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 });
2354
2501
  return;
2355
2502
  }
2356
- resolve6({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
2503
+ resolve7({ ok: true, sessionId: parseSessionId(tool, out), stdout: out, stderr: errOut });
2357
2504
  }
2358
2505
  );
2359
2506
  child?.stdin?.end();
@@ -2361,6 +2508,9 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
2361
2508
  });
2362
2509
  return {
2363
2510
  done,
2511
+ get pid() {
2512
+ return child?.pid;
2513
+ },
2364
2514
  pauseTimeout() {
2365
2515
  if (finished) return;
2366
2516
  if (paused++ === 0 && timer) {
@@ -2428,6 +2578,7 @@ var ApprovalStore = class _ApprovalStore {
2428
2578
  }
2429
2579
  }
2430
2580
  save() {
2581
+ for (const r of this.#records) if (r.status !== "pending" && r.toolInput !== void 0) delete r.toolInput;
2431
2582
  try {
2432
2583
  mkdirSync10(deskbuddyHome(), { recursive: true });
2433
2584
  const tmp = storePath() + ".tmp";
@@ -2483,7 +2634,9 @@ function serverBackend(client = api) {
2483
2634
  summary: r.summary,
2484
2635
  cwdName: basename5(r.cwd),
2485
2636
  source: r.mode === "window" ? "window" : "remote",
2486
- ...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 } : {} } : {}
2487
2640
  }),
2488
2641
  cancel: (approvalId, reason) => client.approvalCancel(approvalId, reason),
2489
2642
  sync: async (ids) => {
@@ -2491,17 +2644,17 @@ function serverBackend(client = api) {
2491
2644
  const out = [];
2492
2645
  for (const a of res?.approvals ?? []) {
2493
2646
  if (typeof a?.approvalId !== "string") continue;
2494
- const d = mapStatus(a.status);
2495
- if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName) });
2647
+ const d = mapStatus(a.status, a.decision);
2648
+ if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName), ...Array.isArray(a.answers) ? { answers: a.answers } : {} });
2496
2649
  }
2497
2650
  return out;
2498
2651
  }
2499
2652
  };
2500
2653
  }
2501
- function mapStatus(s) {
2654
+ function mapStatus(s, decision) {
2502
2655
  switch (s) {
2503
2656
  case "APPROVED":
2504
- return "allow";
2657
+ return decision === "allow_always" ? "allow_always" : "allow";
2505
2658
  case "DENIED":
2506
2659
  return "deny";
2507
2660
  case "CANCELLED":
@@ -2514,7 +2667,8 @@ function mapStatus(s) {
2514
2667
  // src/daemon/gate/coordinator.ts
2515
2668
  import { randomUUID as randomUUID3 } from "crypto";
2516
2669
  import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
2517
- import { basename as basename6, dirname as dirname8, join as join14 } from "path";
2670
+ import { homedir as homedir6 } from "os";
2671
+ import { basename as basename6, dirname as dirname8, join as join14, posix as posix3, win32 as win324 } from "path";
2518
2672
 
2519
2673
  // src/daemon/gate/classify.ts
2520
2674
  import { isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
@@ -2661,6 +2815,152 @@ function describe(toolName, toolInput, cwd) {
2661
2815
  return { kind: SHELL_TOOLS.has(toolName) ? "command" : "other", summary: truncate(detail ? `${toolName} ${detail}` : toolName) };
2662
2816
  }
2663
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
+
2664
2964
  // src/daemon/gate/procs.ts
2665
2965
  import { execFileSync as execFileSync8 } from "child_process";
2666
2966
  function listDescendants(rootPid) {
@@ -2712,7 +3012,19 @@ var CMD_MATCH_MIN = 6;
2712
3012
  var CMD_MATCH_LEN = 80;
2713
3013
 
2714
3014
  // src/daemon/gate/coordinator.ts
2715
- function pickRememberSuggestion(suggestions) {
3015
+ var REMEMBER_TOOLS = /* @__PURE__ */ new Set([
3016
+ "claude_code",
3017
+ // Qoder CN / Qoder 的 worker 与 Claude Code 同一套 PermissionUpdate(addRules / setMode / addDirectories),
3018
+ // 输入原样带建议、输出的 updatedPermissions 会被应用(2026-10-02 读 Qoder CN 0.4.1 worker 源码核对)⚠️ 未实机点过
3019
+ "qoder_cn",
3020
+ "qoder",
3021
+ // opencode:插件把授权请求的 always 模式翻成一条 session 级 addRules 建议,设备选「记住」时插件回 always(hook/opencode.mjs)
3022
+ "opencode"
3023
+ ]);
3024
+ function cut100(text) {
3025
+ return text.length > 100 ? text.slice(0, 99) + "\u2026" : text;
3026
+ }
3027
+ function pickRememberSuggestion(suggestions, opts = {}) {
2716
3028
  if (!Array.isArray(suggestions)) return null;
2717
3029
  for (const s of suggestions) {
2718
3030
  if (!s || s.type !== "addRules" || s.behavior !== "allow" || !Array.isArray(s.rules) || s.rules.length === 0) continue;
@@ -2721,10 +3033,41 @@ function pickRememberSuggestion(suggestions) {
2721
3033
  if (raw.some((r) => typeof r?.ruleContent !== "string" || !r.ruleContent)) continue;
2722
3034
  const rules = raw.map((r) => r.ruleContent);
2723
3035
  const scope = s.destination === "session" ? "\u8FD9\u6B21\u4F1A\u8BDD\u91CC" : "\u8FD9\u4E2A\u9879\u76EE\u91CC";
2724
- const text = `\u4EE5\u540E\u5728${scope}\u90FD\u5141\u8BB8\uFF1A${rules.join("\u3001")}`;
2725
- return { suggestion: s, text: text.length > 100 ? text.slice(0, 99) + "\u2026" : text };
2726
- }
2727
- return null;
3036
+ return { updates: [s], text: cut100(`\u4EE5\u540E\u5728${scope}\u90FD\u5141\u8BB8\uFF1A${rules.join("\u3001")}`) };
3037
+ }
3038
+ if (!opts.edit) return null;
3039
+ const all = suggestions;
3040
+ const mode = all.find((s) => s && s.type === "setMode" && s.mode === "acceptEdits" && s.destination === "session");
3041
+ if (!mode) return null;
3042
+ const dirs = all.filter((s) => s && s.type === "addDirectories");
3043
+ if (dirs.length > 1) return null;
3044
+ let text = "\u672C\u4F1A\u8BDD\u81EA\u52A8\u63A5\u53D7\u6587\u4EF6\u7F16\u8F91\uFF08\u542B\u65B0\u5EFA/\u79FB\u52A8\u6587\u4EF6\u7B49\uFF09";
3045
+ if (dirs.length === 1) {
3046
+ const d = dirs[0];
3047
+ const list = Array.isArray(d.directories) ? d.directories : [];
3048
+ if (d.destination !== "session" || list.length !== 1 || !grantableDir(list[0], opts.home ?? homedir6())) return null;
3049
+ text += `\uFF0C\u5E76\u5141\u8BB8\u8BBF\u95EE\u76EE\u5F55 ${list[0]}`;
3050
+ }
3051
+ if (text.length > 100) return null;
3052
+ return { updates: dirs.length === 1 ? [mode, dirs[0]] : [mode], text };
3053
+ }
3054
+ var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
3055
+ function normDir(p, win) {
3056
+ if (win) return win324.normalize(p).replace(/[\\/]+$/, "").toLowerCase();
3057
+ return posix3.normalize(p).replace(/\/+$/, "");
3058
+ }
3059
+ function grantableDir(dir, home) {
3060
+ if (typeof dir !== "string" || dir === "") return false;
3061
+ const win = /^[A-Za-z]:[\\/]/.test(dir) || dir.startsWith("\\\\");
3062
+ if (!win && !dir.startsWith("/")) return false;
3063
+ const n = normDir(dir, win);
3064
+ if (n === "" || /^[a-z]:$/.test(n) || win && /^\\\\[^\\]+\\[^\\]+$/.test(n)) return false;
3065
+ if (home && n === normDir(home, win)) return false;
3066
+ return true;
3067
+ }
3068
+ function rememberUpdatesOf(rec) {
3069
+ if (Array.isArray(rec.rememberUpdates) && rec.rememberUpdates.length > 0) return rec.rememberUpdates;
3070
+ return rec.rememberSuggestion !== void 0 ? [rec.rememberSuggestion] : null;
2728
3071
  }
2729
3072
  var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
2730
3073
  function windowNotice(decision, summary2, deviceName = "") {
@@ -2732,7 +3075,17 @@ function windowNotice(decision, summary2, deviceName = "") {
2732
3075
  const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
2733
3076
  return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
2734
3077
  }
2735
- 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
+ }
2736
3089
  var WINDOW_SETTLED_EVENTS = /* @__PURE__ */ new Set(["UserPromptSubmit", "Stop"]);
2737
3090
  var WINDOW_TOOL_DONE_EVENTS = /* @__PURE__ */ new Set(["PostToolUse", "PostToolUseFailure"]);
2738
3091
  var WINDOW_PROC_WATCH_MS = 10 * 60 * 1e3;
@@ -2770,7 +3123,10 @@ var ApprovalCoordinator = class {
2770
3123
  const toolName = typeof req2.hook.tool_name === "string" ? req2.hook.tool_name : "";
2771
3124
  const sessionId = typeof req2.hook.session_id === "string" && req2.hook.session_id || run2.sessionId || "";
2772
3125
  if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
2773
- 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 });
2774
3130
  if (v.action === "auto") return { decision: "allow" };
2775
3131
  const fp = fingerprint(toolName, req2.hook.tool_input);
2776
3132
  const store = this.#d.store;
@@ -2795,13 +3151,17 @@ var ApprovalCoordinator = class {
2795
3151
  summary: v.summary,
2796
3152
  mode: run2.mode,
2797
3153
  status: "pending",
2798
- 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
+ })() : {}
2799
3159
  };
2800
3160
  store.add(rec);
2801
3161
  store.save();
2802
3162
  let early = null;
2803
3163
  let settle;
2804
- const decided = new Promise((resolve6) => settle = resolve6);
3164
+ const decided = new Promise((resolve7) => settle = resolve7);
2805
3165
  this.#waiters.set(rec.approvalId, (r) => {
2806
3166
  early = r;
2807
3167
  settle(r);
@@ -2825,8 +3185,8 @@ var ApprovalCoordinator = class {
2825
3185
  }
2826
3186
  run2.onWaitStart?.();
2827
3187
  try {
2828
- return await new Promise((resolve6) => {
2829
- void decided.then(resolve6);
3188
+ return await new Promise((resolve7) => {
3189
+ void decided.then(resolve7);
2830
3190
  const onAbort = () => {
2831
3191
  if (!this.#waiters.delete(rec.approvalId)) return;
2832
3192
  if (rec.status === "pending") {
@@ -2835,7 +3195,7 @@ var ApprovalCoordinator = class {
2835
3195
  store.save();
2836
3196
  void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
2837
3197
  }
2838
- resolve6({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
3198
+ resolve7({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
2839
3199
  };
2840
3200
  if (signal.aborted) onAbort();
2841
3201
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -2856,10 +3216,14 @@ var ApprovalCoordinator = class {
2856
3216
  const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
2857
3217
  const cwd = typeof hook.cwd === "string" ? hook.cwd : "";
2858
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;
2859
3223
  for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
2860
3224
  const store = this.#d.store;
2861
- const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
2862
- const remember = tool === "claude_code" ? pickRememberSuggestion(hook.permission_suggestions) : 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;
2863
3227
  const rec = {
2864
3228
  approvalId: randomUUID3(),
2865
3229
  runId: "",
@@ -2872,7 +3236,11 @@ var ApprovalCoordinator = class {
2872
3236
  mode: "window",
2873
3237
  status: "pending",
2874
3238
  createdAt: this.#now(),
2875
- ...remember ? { remember: remember.text, rememberSuggestion: remember.suggestion } : {}
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
+ })() : {}
2876
3244
  };
2877
3245
  store.add(rec);
2878
3246
  store.save();
@@ -2887,7 +3255,7 @@ var ApprovalCoordinator = class {
2887
3255
  ...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
2888
3256
  });
2889
3257
  let settle;
2890
- const decided = new Promise((resolve6) => settle = resolve6);
3258
+ const decided = new Promise((resolve7) => settle = resolve7);
2891
3259
  this.#waiters.set(rec.approvalId, settle);
2892
3260
  try {
2893
3261
  await this.#d.backend.create(rec);
@@ -2903,11 +3271,11 @@ var ApprovalCoordinator = class {
2903
3271
  }
2904
3272
  this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
2905
3273
  if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
2906
- return new Promise((resolve6) => {
2907
- void decided.then(resolve6);
3274
+ return new Promise((resolve7) => {
3275
+ void decided.then(resolve7);
2908
3276
  const onAbort = () => {
2909
3277
  if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
2910
- resolve6(none);
3278
+ resolve7(none);
2911
3279
  };
2912
3280
  if (signal.aborted) onAbort();
2913
3281
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -2936,8 +3304,10 @@ var ApprovalCoordinator = class {
2936
3304
  }
2937
3305
  const fp = rec.fingerprint.slice(0, 16);
2938
3306
  const settled = (st.recent ?? []).some(
2939
- (e) => e.at > rec.createdAt && (WINDOW_SETTLED_EVENTS.has(e.e) || // 跑完的必须正是这一步:并行的同名工具不算(评审 #2)。老版 hook 没有 fp 时退回只比工具名
2940
- 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"))
2941
3311
  );
2942
3312
  if (settled) {
2943
3313
  this.#settleWindow(rec, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
@@ -2973,6 +3343,56 @@ var ApprovalCoordinator = class {
2973
3343
  }
2974
3344
  void this.#cancelRemote(rec.approvalId, reason);
2975
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
+ }
2976
3396
  #readState(sessionId) {
2977
3397
  try {
2978
3398
  return (this.#d.readState ?? readState)(sessionId);
@@ -2980,15 +3400,20 @@ var ApprovalCoordinator = class {
2980
3400
  return null;
2981
3401
  }
2982
3402
  }
2983
- /** 设备上按了允许/拒绝(经服务端下发,或重连后 sync 补到) */
2984
- onDecision(approvalId, decision, deviceName = "") {
3403
+ /** 设备上按了允许/拒绝(经服务端下发,或重连后 sync 补到)。选择题的答案在 answers 里(下标) */
3404
+ onDecision(approvalId, decision, deviceName = "", answers) {
2985
3405
  const rec = this.#d.store.get(approvalId);
2986
3406
  if (!rec) {
2987
3407
  this.#d.log(`\u6536\u5230\u672A\u77E5\u6388\u6743 ${approvalId} \u7684\u51B3\u5B9A\uFF0C\u5FFD\u7565`);
2988
3408
  return;
2989
3409
  }
2990
3410
  if (rec.status !== "pending") return;
2991
- const always = decision === "allow_always" && rec.mode === "window" && rec.rememberSuggestion !== void 0;
3411
+ if (rec.kind === "question") {
3412
+ this.#onQuestionDecision(rec, decision, deviceName, answers);
3413
+ return;
3414
+ }
3415
+ const updates = rememberUpdatesOf(rec);
3416
+ const always = decision === "allow_always" && rec.mode === "window" && updates !== null;
2992
3417
  if (decision === "allow_always") decision = "allow";
2993
3418
  rec.status = decision === "allow" ? "approved" : "denied";
2994
3419
  rec.decidedAt = this.#now();
@@ -3000,13 +3425,15 @@ var ApprovalCoordinator = class {
3000
3425
  this.#d.store.save();
3001
3426
  if (rec.mode === "window") {
3002
3427
  const notice = windowNotice(decision, rec.summary, deviceName) + (always ? "\uFF08\u5DF2\u8BB0\u4F4F\uFF09" : "");
3003
- try {
3004
- ;
3005
- (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
3006
- } catch {
3428
+ if (!NO_NOTICE_TOOLS.has(rec.tool)) {
3429
+ try {
3430
+ ;
3431
+ (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
3432
+ } catch {
3433
+ }
3007
3434
  }
3008
3435
  waiter(
3009
- decision === "allow" ? { decision: "allow", notice, ...always ? { updatedPermissions: [rec.rememberSuggestion] } : {} } : (
3436
+ decision === "allow" ? { decision: "allow", notice, ...always && updates ? { updatedPermissions: updates } : {} } : (
3010
3437
  // 🔴 交给模型的原因里不带设备名:名字是家庭成员在 App 里起的,别让它进提示词。设备名只进给人看的回显
3011
3438
  { decision: "deny", reason: "\u7528\u6237\u5728 DeskBuddy \u8BBE\u5907\u4E0A\u62D2\u7EDD\u4E86\u8FD9\u4E00\u6B65\uFF0C\u4E0D\u8981\u91CD\u8BD5\u8FD9\u4E2A\u64CD\u4F5C", notice }
3012
3439
  )
@@ -3031,6 +3458,16 @@ var ApprovalCoordinator = class {
3031
3458
  this.#d.store.save();
3032
3459
  if (decision === "allow") this.#d.resume(rec, "orphan");
3033
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
+ }
3034
3471
  /**
3035
3472
  * 同一会话来了新指令:等待中的那一步按"没批准"处理(spec E9,和窗口里弹框时直接打字一样)。
3036
3473
  * 🔴 必须在新指令【入队之前】调:hold 模式下旧进程还占着这个会话的串行队列,
@@ -3113,7 +3550,7 @@ var ApprovalCoordinator = class {
3113
3550
  const got = await this.#d.backend.sync(ids.slice(i, i + 50));
3114
3551
  for (const g of got) {
3115
3552
  if (g.decision === "cancelled") this.#markCancelled(g.approvalId);
3116
- else this.onDecision(g.approvalId, g.decision, g.deviceName);
3553
+ else this.onDecision(g.approvalId, g.decision, g.deviceName, g.answers);
3117
3554
  }
3118
3555
  } catch (e) {
3119
3556
  this.#d.log(`\u540C\u6B65\u6388\u6743\u72B6\u6001\u5931\u8D25\uFF1A${e.message}`);
@@ -3204,13 +3641,15 @@ function tokenEquals(a, b) {
3204
3641
  }
3205
3642
  var GateServer = class {
3206
3643
  #handler;
3644
+ #notify;
3207
3645
  #token = randomBytes3(32).toString("hex");
3208
3646
  #server = null;
3209
3647
  #inflight = 0;
3210
3648
  /** 自动升级排空中:所有请求直接 503(远程 gate → exit 2 拦下;窗口 gate → 退回窗口里本地确认) */
3211
3649
  refusing = false;
3212
- constructor(handler) {
3650
+ constructor(handler, notify = null) {
3213
3651
  this.#handler = handler;
3652
+ this.#notify = notify;
3214
3653
  }
3215
3654
  get token() {
3216
3655
  return this.#token;
@@ -3222,9 +3661,9 @@ var GateServer = class {
3222
3661
  /** 起监听并写 gate.json。返回实际端口 */
3223
3662
  async start() {
3224
3663
  const server = createServer((req2, res) => void this.#serve(req2, res));
3225
- await new Promise((resolve6, reject) => {
3664
+ await new Promise((resolve7, reject) => {
3226
3665
  server.once("error", reject);
3227
- server.listen(0, "127.0.0.1", () => resolve6());
3666
+ server.listen(0, "127.0.0.1", () => resolve7());
3228
3667
  });
3229
3668
  this.#server = server;
3230
3669
  const addr = server.address();
@@ -3245,7 +3684,7 @@ var GateServer = class {
3245
3684
  }
3246
3685
  const s = this.#server;
3247
3686
  this.#server = null;
3248
- if (s) await new Promise((resolve6) => s.close(() => resolve6()));
3687
+ if (s) await new Promise((resolve7) => s.close(() => resolve7()));
3249
3688
  }
3250
3689
  async #serve(req2, res) {
3251
3690
  const reply = (status, body2) => {
@@ -3275,6 +3714,14 @@ var GateServer = class {
3275
3714
  return reply(400);
3276
3715
  }
3277
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
+ }
3278
3725
  const window = parsed.mode === "window";
3279
3726
  if (!window && typeof parsed.runId !== "string") return reply(400);
3280
3727
  const ac = new AbortController();
@@ -3357,7 +3804,7 @@ function buildHandshakeUrl(cfg, opts = {}) {
3357
3804
  }
3358
3805
 
3359
3806
  // src/daemon/log.ts
3360
- import { appendFileSync, mkdirSync as mkdirSync12, statSync as statSync4, truncateSync } from "fs";
3807
+ import { appendFileSync, mkdirSync as mkdirSync12, statSync as statSync5, truncateSync } from "fs";
3361
3808
  import { join as join16 } from "path";
3362
3809
  var MAX_LOG_BYTES = 2 * 1024 * 1024;
3363
3810
  function logPath() {
@@ -3365,7 +3812,7 @@ function logPath() {
3365
3812
  }
3366
3813
  function rotateIfNeeded(path) {
3367
3814
  try {
3368
- if (statSync4(path).size > MAX_LOG_BYTES) truncateSync(path, 0);
3815
+ if (statSync5(path).size > MAX_LOG_BYTES) truncateSync(path, 0);
3369
3816
  } catch {
3370
3817
  }
3371
3818
  }
@@ -3430,42 +3877,6 @@ var SessionQueue = class {
3430
3877
  }
3431
3878
  };
3432
3879
 
3433
- // src/daemon/remote.ts
3434
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync11 } from "fs";
3435
- import { join as join17 } from "path";
3436
- function handleDir() {
3437
- return join17(deskbuddyHome(), "remote");
3438
- }
3439
- function handlePath(tool) {
3440
- const safe = (tool || "").replace(/[^0-9a-zA-Z_-]/g, "");
3441
- return join17(handleDir(), `${safe || "unknown"}.json`);
3442
- }
3443
- function readHandle(tool) {
3444
- try {
3445
- const h = JSON.parse(readFileSync12(handlePath(tool), "utf8"));
3446
- if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
3447
- if (typeof h?.cwd !== "string" || !h.cwd) return null;
3448
- return h;
3449
- } catch {
3450
- return null;
3451
- }
3452
- }
3453
- function writeHandle(tool, handle) {
3454
- try {
3455
- mkdirSync13(handleDir(), { recursive: true });
3456
- const tmp = handlePath(tool) + ".tmp";
3457
- writeFileSync11(tmp, JSON.stringify(handle), "utf8");
3458
- renameSync9(tmp, handlePath(tool));
3459
- } catch {
3460
- }
3461
- }
3462
- function handleIsUsable(handle) {
3463
- const s = readState(handle.sessionId);
3464
- if (!s) return false;
3465
- if (s.pid && !isAlive(s.pid, s.pidName)) return false;
3466
- return true;
3467
- }
3468
-
3469
3880
  // src/daemon/resolve.ts
3470
3881
  function resolveTarget(cmd, deps) {
3471
3882
  if (cmd.sessionId) {
@@ -3474,23 +3885,16 @@ function resolveTarget(cmd, deps) {
3474
3885
  if (!state) return { ok: false, reason: "session_gone" };
3475
3886
  if (!state.cwd) return { ok: false, reason: "no_cwd" };
3476
3887
  if (deps.windowOpen?.(state)) return { ok: false, reason: "window_open" };
3477
- return { ok: true, sessionId, cwd: state.cwd, handleKey: cmd.tool };
3888
+ return { ok: true, sessionId, cwd: state.cwd };
3478
3889
  }
3479
3890
  let dirCwd = "";
3480
3891
  if (cmd.dirRef) {
3481
3892
  dirCwd = deps.dirOf ? deps.dirOf(cmd.dirRef) : "";
3482
3893
  if (!dirCwd) return { ok: false, reason: "dir_gone" };
3483
3894
  }
3484
- const handleKey = cmd.dirRef ? `${cmd.tool}_${cmd.dirRef}` : cmd.tool;
3485
- const handle = deps.readHandle(handleKey);
3486
- const handleState = handle ? deps.readState(handle.sessionId) : null;
3487
- const handleInWindow = !!(handleState && deps.windowOpen?.(handleState));
3488
- if (handle && !handleInWindow && deps.handleIsUsable(handle)) {
3489
- return { ok: true, sessionId: handle.sessionId, cwd: handle.cwd, handleKey };
3490
- }
3491
3895
  const cwd = dirCwd || deps.remoteCwd();
3492
3896
  if (!cwd) return { ok: false, reason: "no_remote_cwd" };
3493
- return { ok: true, sessionId: "", cwd, handleKey };
3897
+ return { ok: true, sessionId: "", cwd };
3494
3898
  }
3495
3899
  function failureMessage(reason) {
3496
3900
  switch (reason) {
@@ -3508,11 +3912,11 @@ function failureMessage(reason) {
3508
3912
  }
3509
3913
 
3510
3914
  // src/daemon/seen.ts
3511
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync13, renameSync as renameSync10, writeFileSync as writeFileSync12 } from "fs";
3512
- import { join as join18 } from "path";
3915
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync11 } from "fs";
3916
+ import { join as join17 } from "path";
3513
3917
  var SEEN_CAPACITY = 200;
3514
3918
  function seenPath() {
3515
- return join18(deskbuddyHome(), "seen.json");
3919
+ return join17(deskbuddyHome(), "seen.json");
3516
3920
  }
3517
3921
  var SeenIds = class _SeenIds {
3518
3922
  #ids = [];
@@ -3520,55 +3924,1312 @@ var SeenIds = class _SeenIds {
3520
3924
  constructor(initial = []) {
3521
3925
  for (const id of initial) this.#push(id);
3522
3926
  }
3523
- #push(id) {
3524
- if (this.#set.has(id)) return;
3525
- this.#ids.push(id);
3526
- this.#set.add(id);
3527
- while (this.#ids.length > SEEN_CAPACITY) {
3528
- const dropped = this.#ids.shift();
3529
- if (dropped !== void 0) this.#set.delete(dropped);
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
+ }
4829
+ }
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
+ }
4838
+ }
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}`);
4848
+ }
4849
+ }
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 });
4855
+ }
4856
+ #markBusy(dir) {
4857
+ if (dir) this.#busyDirs.set(dirKey(dir), this.#now());
4858
+ }
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);
4863
+ }
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
+ });
4877
+ }
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:
4936
+ }
4937
+ }
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);
3530
4943
  }
4944
+ if (info.parentID || info.is_subagent === true) this.#children.add(id);
4945
+ rememberTitle(this.#titles, info);
3531
4946
  }
3532
- has(id) {
3533
- return this.#set.has(id);
4947
+ #dirOf(sid, dir) {
4948
+ return this.#sessionDir.get(sid) ?? dir ?? "";
3534
4949
  }
3535
- /** 记下来。返回 true 表示这是**新的**(之前没见过) */
3536
- remember(id) {
3537
- if (this.#set.has(id)) return false;
3538
- this.#push(id);
3539
- return true;
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
+ });
3540
5025
  }
3541
- snapshot() {
3542
- return [...this.#ids];
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
+ });
3543
5075
  }
3544
- static load() {
3545
- try {
3546
- const raw = JSON.parse(readFileSync13(seenPath(), "utf8"));
3547
- return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
3548
- } catch {
3549
- return new _SeenIds();
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`);
3550
5105
  }
3551
5106
  }
3552
- 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;
3553
5124
  try {
3554
- mkdirSync14(deskbuddyHome(), { recursive: true });
3555
- const tmp = seenPath() + ".tmp";
3556
- writeFileSync12(tmp, JSON.stringify({ ids: this.#ids }), "utf8");
3557
- renameSync10(tmp, seenPath());
3558
- } 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;
3559
5193
  }
3560
5194
  }
3561
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
+ }
3562
5223
 
3563
5224
  // src/daemon/codexWatch.ts
3564
- import { execFileSync as execFileSync9 } from "child_process";
5225
+ import { execFileSync as execFileSync10 } from "child_process";
3565
5226
  import WebSocket from "ws";
3566
5227
  var TURN_LIMIT_MS = 10 * 60 * 1e3;
3567
5228
  var QUEUE_MAX_MS = 10 * 60 * 1e3;
3568
5229
  var IDLE_GRACE_MS = 5 * 1e3;
3569
5230
  var RESUME_RETRY_MS = 1e3;
3570
5231
  var RESUME_MAX_TRIES = 120;
3571
- var REQUEST_TIMEOUT_MS = 30 * 1e3;
5232
+ var REQUEST_TIMEOUT_MS3 = 30 * 1e3;
3572
5233
  function finalReply(items) {
3573
5234
  let last = "";
3574
5235
  let final = "";
@@ -3606,7 +5267,7 @@ function discoverAppServers(configured) {
3606
5267
  return parseAppServerUrls((winProcs() ?? []).map((p) => p.cmdline).join("\n"));
3607
5268
  }
3608
5269
  try {
3609
- 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"] });
3610
5271
  return parseAppServerUrls(out);
3611
5272
  } catch {
3612
5273
  return [];
@@ -3636,6 +5297,7 @@ var CodexWatch = class {
3636
5297
  /** fileChange 条目 → 改动的文件(授权请求本身不带文件,从 item/started 记) */
3637
5298
  #changes = /* @__PURE__ */ new Map();
3638
5299
  /** 服务端请求 id → 放掉等待的开关、所属会话 */
5300
+ /** 等设备答复的服务端请求。qids:选择题各题的 Codex 题目 id(按顺序),只有选择题有 —— 跟着这条记录走,不单独按请求 id 存(重连后 id 会从头编号) */
3639
5301
  #pending = /* @__PURE__ */ new Map();
3640
5302
  #stopped = false;
3641
5303
  /** 已空闲的会话(有卡片在等时暂缓退订,卡片了结后补退) */
@@ -3749,13 +5411,13 @@ var CodexWatch = class {
3749
5411
  }
3750
5412
  #request(method, params) {
3751
5413
  const id = this.#nextId++;
3752
- return new Promise((resolve6) => {
5414
+ return new Promise((resolve7) => {
3753
5415
  const timer = setTimeout(() => {
3754
5416
  this.#calls.delete(id);
3755
- resolve6({ error: { message: "timeout" }, local: true });
3756
- }, REQUEST_TIMEOUT_MS);
5417
+ resolve7({ error: { message: "timeout" }, local: true });
5418
+ }, REQUEST_TIMEOUT_MS3);
3757
5419
  timer.unref?.();
3758
- this.#calls.set(id, { resolve: resolve6, timer });
5420
+ this.#calls.set(id, { resolve: resolve7, timer });
3759
5421
  this.#send({ id, method, params });
3760
5422
  });
3761
5423
  }
@@ -3978,7 +5640,7 @@ var CodexWatch = class {
3978
5640
  return { ...out, ...merged ? { merged } : {} };
3979
5641
  }
3980
5642
  #trackTurn(threadId, turnId) {
3981
- return new Promise((resolve6) => {
5643
+ return new Promise((resolve7) => {
3982
5644
  const limit = this.#d.turnLimitMs ?? TURN_LIMIT_MS;
3983
5645
  const step = this.#d.turnTickMs ?? 1e3;
3984
5646
  const t = {
@@ -3988,7 +5650,7 @@ var CodexWatch = class {
3988
5650
  used: 0,
3989
5651
  timer: null,
3990
5652
  grace: null,
3991
- 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 } : {} })
3992
5654
  };
3993
5655
  this.#turns.set(turnId, t);
3994
5656
  const early = this.#early.get(turnId);
@@ -4035,7 +5697,7 @@ var CodexWatch = class {
4035
5697
  t.done(end, text || (end === "completed" ? "" : finalReply(t.items)), error);
4036
5698
  }
4037
5699
  #waitThread(threadId, ms) {
4038
- return new Promise((resolve6) => {
5700
+ return new Promise((resolve7) => {
4039
5701
  let set = this.#wake.get(threadId);
4040
5702
  if (!set) this.#wake.set(threadId, set = /* @__PURE__ */ new Set());
4041
5703
  const s = set;
@@ -4043,7 +5705,7 @@ var CodexWatch = class {
4043
5705
  clearTimeout(timer);
4044
5706
  s.delete(fire);
4045
5707
  if (s.size === 0 && this.#wake.get(threadId) === s) this.#wake.delete(threadId);
4046
- resolve6();
5708
+ resolve7();
4047
5709
  };
4048
5710
  const timer = setTimeout(fire, ms);
4049
5711
  timer.unref?.();
@@ -4177,6 +5839,7 @@ var CodexWatch = class {
4177
5839
  const threadId = typeof p.threadId === "string" ? p.threadId : "";
4178
5840
  if (!threadId) return;
4179
5841
  let hook;
5842
+ let qids;
4180
5843
  if (method === "item/commandExecution/requestApproval") {
4181
5844
  const command = typeof p.command === "string" ? unwrapShell(p.command) : "";
4182
5845
  if (!command) return;
@@ -4187,12 +5850,17 @@ var CodexWatch = class {
4187
5850
  const shown = paths.join(", ");
4188
5851
  if (!shown) return;
4189
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;
4190
5858
  } else {
4191
5859
  return;
4192
5860
  }
4193
5861
  const key = String(id);
4194
5862
  const ac = new AbortController();
4195
- const entry = { ac, threadId };
5863
+ const entry = { ac, threadId, ...qids ? { qids } : {} };
4196
5864
  this.#pending.set(key, entry);
4197
5865
  const req2 = {
4198
5866
  window: true,
@@ -4205,7 +5873,14 @@ var CodexWatch = class {
4205
5873
  void this.#d.gate(req2, ac.signal).then((reply) => {
4206
5874
  if (this.#pending.get(key) !== entry) return;
4207
5875
  this.#pending.delete(key);
4208
- 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" } });
4209
5884
  else if (reply.decision === "deny") this.#send({ id, result: { decision: "decline" } });
4210
5885
  settled();
4211
5886
  }).catch(() => {
@@ -4214,19 +5889,41 @@ var CodexWatch = class {
4214
5889
  });
4215
5890
  }
4216
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
+ }
4217
5914
 
4218
5915
  // src/core/codexRemote.ts
4219
- import { mkdirSync as mkdirSync15, renameSync as renameSync11, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
4220
- import { join as join19 } from "path";
5916
+ import { mkdirSync as mkdirSync14, renameSync as renameSync10, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
5917
+ import { join as join20 } from "path";
4221
5918
  function codexRemotePath() {
4222
- return join19(deskbuddyHome(), "codex-remote.json");
5919
+ return join20(deskbuddyHome(), "codex-remote.json");
4223
5920
  }
4224
5921
  function writeCodexRemote(threads, pid = process.pid, now = Date.now()) {
4225
5922
  try {
4226
- mkdirSync15(deskbuddyHome(), { recursive: true });
5923
+ mkdirSync14(deskbuddyHome(), { recursive: true });
4227
5924
  const p = codexRemotePath();
4228
- writeFileSync13(p + ".tmp", JSON.stringify({ pid, threads: [...threads], at: now }), "utf8");
4229
- renameSync11(p + ".tmp", p);
5925
+ writeFileSync12(p + ".tmp", JSON.stringify({ pid, threads: [...threads], at: now }), "utf8");
5926
+ renameSync10(p + ".tmp", p);
4230
5927
  } catch {
4231
5928
  }
4232
5929
  }
@@ -4243,16 +5940,16 @@ import { chmodSync as chmodSync4, rmSync as rmSync9 } from "fs";
4243
5940
  import { connect, createServer as createServer2 } from "net";
4244
5941
 
4245
5942
  // src/core/wake.ts
4246
- import { execFileSync as execFileSync10 } from "child_process";
5943
+ import { execFileSync as execFileSync11 } from "child_process";
4247
5944
  import { createHash as createHash3, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
4248
- import { chmodSync as chmodSync3, closeSync as closeSync3, mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync14, readSync as readSync2, renameSync as renameSync12, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync14 } 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";
4249
5946
  import { tmpdir as tmpdir2 } from "os";
4250
- import { join as join20 } from "path";
5947
+ import { join as join21 } from "path";
4251
5948
  function wakeDir(home = deskbuddyHome()) {
4252
- return join20(home, "wake");
5949
+ return join21(home, "wake");
4253
5950
  }
4254
5951
  function marksDir(home = deskbuddyHome()) {
4255
- return join20(wakeDir(home), "marks");
5952
+ return join21(wakeDir(home), "marks");
4256
5953
  }
4257
5954
  function safeSid(sessionId) {
4258
5955
  return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
@@ -4262,10 +5959,10 @@ function homeTag(home) {
4262
5959
  }
4263
5960
  function wakeChannelPath(platform = process.platform, home = deskbuddyHome()) {
4264
5961
  if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-wake-${homeTag(home)}`;
4265
- const primary = join20(wakeDir(home), "wake.sock");
5962
+ const primary = join21(wakeDir(home), "wake.sock");
4266
5963
  if (Buffer.byteLength(primary) <= 103) return primary;
4267
5964
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
4268
- return join20(tmpdir2(), `deskbuddy-${uid}`, `wake-${homeTag(home)}.sock`);
5965
+ return join21(tmpdir2(), `deskbuddy-${uid}`, `wake-${homeTag(home)}.sock`);
4269
5966
  }
4270
5967
  function pidExists(pid) {
4271
5968
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -4280,11 +5977,11 @@ function writeWakeMark(sessionId, agentPid, now = Date.now()) {
4280
5977
  const safe = safeSid(sessionId);
4281
5978
  if (!safe) return;
4282
5979
  try {
4283
- mkdirSync16(marksDir(), { recursive: true, mode: 448 });
4284
- const file = join20(marksDir(), `${safe}.json`);
5980
+ mkdirSync15(marksDir(), { recursive: true, mode: 448 });
5981
+ const file = join21(marksDir(), `${safe}.json`);
4285
5982
  const tmp = `${file}.${process.pid}.tmp`;
4286
- writeFileSync14(tmp, JSON.stringify({ agentPid: agentPid ?? 0, daemonPid: process.pid, at: now }), { encoding: "utf8", mode: 384 });
4287
- renameSync12(tmp, file);
5983
+ writeFileSync13(tmp, JSON.stringify({ agentPid: agentPid ?? 0, daemonPid: process.pid, at: now }), { encoding: "utf8", mode: 384 });
5984
+ renameSync11(tmp, file);
4288
5985
  } catch {
4289
5986
  }
4290
5987
  }
@@ -4292,20 +5989,20 @@ function removeWakeMark(sessionId) {
4292
5989
  const safe = safeSid(sessionId);
4293
5990
  if (!safe) return;
4294
5991
  try {
4295
- rmSync8(join20(marksDir(), `${safe}.json`), { force: true });
5992
+ rmSync8(join21(marksDir(), `${safe}.json`), { force: true });
4296
5993
  } catch {
4297
5994
  }
4298
5995
  }
4299
5996
  function clearWakeMarks() {
4300
5997
  let names = [];
4301
5998
  try {
4302
- names = readdirSync4(marksDir());
5999
+ names = readdirSync5(marksDir());
4303
6000
  } catch {
4304
6001
  return;
4305
6002
  }
4306
6003
  for (const n of names) {
4307
6004
  try {
4308
- rmSync8(join20(marksDir(), n), { force: true });
6005
+ rmSync8(join21(marksDir(), n), { force: true });
4309
6006
  } catch {
4310
6007
  }
4311
6008
  }
@@ -4314,7 +6011,7 @@ function isWakeMarked(sessionId, alive = pidExists) {
4314
6011
  const safe = safeSid(sessionId);
4315
6012
  if (!safe) return false;
4316
6013
  try {
4317
- const m = JSON.parse(readFileSync14(join20(marksDir(), `${safe}.json`), "utf8"));
6014
+ const m = JSON.parse(readFileSync14(join21(marksDir(), `${safe}.json`), "utf8"));
4318
6015
  const daemonPid = Number(m?.daemonPid) || 0;
4319
6016
  if (!daemonPid || !alive(daemonPid)) return false;
4320
6017
  const pid = Number(m?.agentPid) || 0;
@@ -4327,30 +6024,30 @@ function withWake(states, alive) {
4327
6024
  return states.map((s) => s.remoteWindow === true || !isWakeMarked(s.sessionId, alive) ? s : { ...s, remoteWindow: true });
4328
6025
  }
4329
6026
  function wakeSecretPath(home = deskbuddyHome()) {
4330
- return join20(wakeDir(home), "secret");
6027
+ return join21(wakeDir(home), "secret");
4331
6028
  }
4332
6029
  function writeWakeSecret(hex, platform = process.platform) {
4333
6030
  const file = wakeSecretPath();
4334
- mkdirSync16(wakeDir(), { recursive: true, mode: 448 });
6031
+ mkdirSync15(wakeDir(), { recursive: true, mode: 448 });
4335
6032
  const tmp = `${file}.${process.pid}.tmp`;
4336
- writeFileSync14(tmp, hex, { encoding: "utf8", mode: 384 });
6033
+ writeFileSync13(tmp, hex, { encoding: "utf8", mode: 384 });
4337
6034
  if (platform === "win32") {
4338
6035
  try {
4339
- 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 });
4340
6037
  } catch {
4341
- renameSync12(tmp, file);
6038
+ renameSync11(tmp, file);
4342
6039
  return "\u7A97\u53E3\u5524\u9192\u5BC6\u94A5\u6587\u4EF6\u7684\u6743\u9650\u6CA1\u80FD\u6536\u7D27\uFF08icacls \u5931\u8D25\uFF09";
4343
6040
  }
4344
6041
  } else {
4345
6042
  chmodSync3(tmp, 384);
4346
6043
  }
4347
- renameSync12(tmp, file);
6044
+ renameSync11(tmp, file);
4348
6045
  return "";
4349
6046
  }
4350
6047
  function restrictToUser(file, platform = process.platform) {
4351
6048
  if (platform !== "win32") return true;
4352
6049
  try {
4353
- 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 });
4354
6051
  return true;
4355
6052
  } catch {
4356
6053
  return false;
@@ -4358,7 +6055,7 @@ function restrictToUser(file, platform = process.platform) {
4358
6055
  }
4359
6056
  function currentUserSid() {
4360
6057
  try {
4361
- 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 });
4362
6059
  const m = /"(S-1-[0-9-]+)"/.exec(out);
4363
6060
  if (m) return `*${m[1]}`;
4364
6061
  } catch {
@@ -4393,11 +6090,11 @@ function analyzeTranscript(file) {
4393
6090
  if (!file) return null;
4394
6091
  let fd;
4395
6092
  try {
4396
- const size = statSync5(file).size;
6093
+ const size = statSync7(file).size;
4397
6094
  const len = Math.min(size, TRANSCRIPT_TAIL2);
4398
6095
  const buf = Buffer.alloc(len);
4399
- fd = openSync3(file, "r");
4400
- readSync2(fd, buf, 0, len, size - len);
6096
+ fd = openSync4(file, "r");
6097
+ readSync3(fd, buf, 0, len, size - len);
4401
6098
  const lines2 = buf.toString("utf8").split("\n");
4402
6099
  let seg = false;
4403
6100
  let reply = null;
@@ -4438,7 +6135,7 @@ function analyzeTranscript(file) {
4438
6135
  } finally {
4439
6136
  if (fd !== void 0) {
4440
6137
  try {
4441
- closeSync3(fd);
6138
+ closeSync4(fd);
4442
6139
  } catch {
4443
6140
  }
4444
6141
  }
@@ -4489,7 +6186,7 @@ function replyMac(secret, nonceC, r) {
4489
6186
  return wakeMac(secret, "r", nonceC, r.instructionId, r.status, r.reason ?? "");
4490
6187
  }
4491
6188
  function startChannelServer(path, secret, onRequest, platform = process.platform) {
4492
- return new Promise((resolve6) => {
6189
+ return new Promise((resolve7) => {
4493
6190
  const server = createServer2((sock) => {
4494
6191
  let open = true;
4495
6192
  let phase = "hello";
@@ -4550,7 +6247,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
4550
6247
  };
4551
6248
  server.once("error", () => {
4552
6249
  restore();
4553
- resolve6(null);
6250
+ resolve7(null);
4554
6251
  });
4555
6252
  server.listen(path, () => {
4556
6253
  restore();
@@ -4562,7 +6259,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
4562
6259
  }
4563
6260
  server.on("error", () => {
4564
6261
  });
4565
- resolve6(server);
6262
+ resolve7(server);
4566
6263
  });
4567
6264
  });
4568
6265
  }
@@ -4580,9 +6277,9 @@ function closeChannelServer(server, path, platform = process.platform) {
4580
6277
  }
4581
6278
  var FINAL = /* @__PURE__ */ new Set(["started", "rejected", "failed", "not_here"]);
4582
6279
  function sendToWindow(path, secret, req2, onReply, opts = {}) {
4583
- return new Promise((resolve6) => {
6280
+ return new Promise((resolve7) => {
4584
6281
  if (!secret) {
4585
- resolve6("fallback");
6282
+ resolve7("fallback");
4586
6283
  return;
4587
6284
  }
4588
6285
  let settled = false;
@@ -4595,13 +6292,13 @@ function sendToWindow(path, secret, req2, onReply, opts = {}) {
4595
6292
  settled = true;
4596
6293
  handled = v === "handled";
4597
6294
  clearTimeout(timer);
4598
- resolve6(v);
6295
+ resolve7(v);
4599
6296
  };
4600
6297
  let sock;
4601
6298
  try {
4602
6299
  sock = connect(path);
4603
6300
  } catch {
4604
- resolve6("fallback");
6301
+ resolve7("fallback");
4605
6302
  return;
4606
6303
  }
4607
6304
  const timer = setTimeout(() => {
@@ -4723,29 +6420,29 @@ function boxMatches(view, body) {
4723
6420
  }
4724
6421
 
4725
6422
  // src/core/wrapWindows.ts
4726
- import { chmodSync as chmodSync5, mkdirSync as mkdirSync17, readdirSync as readdirSync5, readFileSync as readFileSync15, renameSync as renameSync13, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync15 } 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";
4727
6424
  import { tmpdir as tmpdir3 } from "os";
4728
- import { join as join21 } from "path";
6425
+ import { join as join22 } from "path";
4729
6426
  var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
4730
6427
  function windowsDir() {
4731
- return join21(deskbuddyHome(), "windows");
6428
+ return join22(deskbuddyHome(), "windows");
4732
6429
  }
4733
6430
  function bySessionDir() {
4734
- return join21(windowsDir(), "by-session");
6431
+ return join22(windowsDir(), "by-session");
4735
6432
  }
4736
6433
  function safeSid2(sessionId) {
4737
6434
  return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
4738
6435
  }
4739
6436
  function ensureDir(dir) {
4740
- mkdirSync17(dir, { recursive: true, mode: 448 });
6437
+ mkdirSync16(dir, { recursive: true, mode: 448 });
4741
6438
  }
4742
6439
  function atomicWrite(file, text) {
4743
6440
  const tmp = `${file}.${process.pid}.tmp`;
4744
- writeFileSync15(tmp, text, { encoding: "utf8", mode: 384 });
4745
- renameSync13(tmp, file);
6441
+ writeFileSync14(tmp, text, { encoding: "utf8", mode: 384 });
6442
+ renameSync12(tmp, file);
4746
6443
  }
4747
6444
  function windowFile(windowId) {
4748
- return join21(windowsDir(), `${windowId}.json`);
6445
+ return join22(windowsDir(), `${windowId}.json`);
4749
6446
  }
4750
6447
  function writeWindow(windowId, rec) {
4751
6448
  if (!WINDOW_ID_RE.test(windowId)) return;
@@ -4776,12 +6473,12 @@ function removeWindow(windowId) {
4776
6473
  }
4777
6474
  let names = [];
4778
6475
  try {
4779
- names = readdirSync5(bySessionDir());
6476
+ names = readdirSync6(bySessionDir());
4780
6477
  } catch {
4781
6478
  return;
4782
6479
  }
4783
6480
  for (const n of names) {
4784
- const f = join21(bySessionDir(), n);
6481
+ const f = join22(bySessionDir(), n);
4785
6482
  try {
4786
6483
  if (readFileSync15(f, "utf8").trim() === windowId) rmSync10(f, { force: true });
4787
6484
  } catch {
@@ -4792,14 +6489,14 @@ function readBySession(sessionId) {
4792
6489
  const safe = safeSid2(sessionId);
4793
6490
  if (!safe) return null;
4794
6491
  try {
4795
- const id = readFileSync15(join21(bySessionDir(), safe), "utf8").trim();
6492
+ const id = readFileSync15(join22(bySessionDir(), safe), "utf8").trim();
4796
6493
  return WINDOW_ID_RE.test(id) ? id : null;
4797
6494
  } catch {
4798
6495
  return null;
4799
6496
  }
4800
6497
  }
4801
6498
  function lastEventFile(windowId) {
4802
- return join21(windowsDir(), `${windowId}.last`);
6499
+ return join22(windowsDir(), `${windowId}.last`);
4803
6500
  }
4804
6501
  function readLastEvent(windowId) {
4805
6502
  if (!WINDOW_ID_RE.test(windowId)) return null;
@@ -4813,16 +6510,16 @@ function readLastEvent(windowId) {
4813
6510
  function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
4814
6511
  if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
4815
6512
  const name = `${windowId.slice(0, 16)}.sock`;
4816
- const primary = join21(home, "win", name);
6513
+ const primary = join22(home, "win", name);
4817
6514
  if (Buffer.byteLength(primary) <= 103) return primary;
4818
6515
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
4819
- return join21(tmpdir3(), `deskbuddy-${uid}`, name);
6516
+ return join22(tmpdir3(), `deskbuddy-${uid}`, name);
4820
6517
  }
4821
6518
  function ensureChannelDir(sockPath) {
4822
- const dir = join21(sockPath, "..");
6519
+ const dir = join22(sockPath, "..");
4823
6520
  try {
4824
- mkdirSync17(dir, { recursive: true, mode: 448 });
4825
- const st = statSync6(dir);
6521
+ mkdirSync16(dir, { recursive: true, mode: 448 });
6522
+ const st = statSync8(dir);
4826
6523
  if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
4827
6524
  chmodSync5(dir, 448);
4828
6525
  return true;
@@ -4841,38 +6538,44 @@ function windowForSession(sessionId) {
4841
6538
  }
4842
6539
 
4843
6540
  // src/core/launch.ts
4844
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync16, renameSync as renameSync14, rmSync as rmSync11, writeFileSync as writeFileSync16 } from "fs";
4845
- import { join as join22 } 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";
4846
6543
  var RUN_ID_RE = /^[a-f0-9]{32}$/;
4847
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}$/;
6545
+ var LAUNCH_TOOLS = ["claude_code", "codex", "workbuddy"];
6546
+ function validPlan(p) {
6547
+ if (!LAUNCH_TOOLS.includes(p.tool)) return false;
6548
+ if (p.resume && (p.tool !== "claude_code" || !SESSION_UUID_RE.test(p.resume))) return false;
6549
+ return true;
6550
+ }
4848
6551
  function launchDir(home = deskbuddyHome()) {
4849
- return join22(home, "launch");
6552
+ return join23(home, "launch");
4850
6553
  }
4851
6554
  function planFile(runId) {
4852
- return join22(launchDir(), `${runId}.json`);
6555
+ return join23(launchDir(), `${runId}.json`);
4853
6556
  }
4854
6557
  function writeLaunch(runId, plan) {
4855
6558
  if (!RUN_ID_RE.test(runId)) throw new Error("bad runId");
4856
- if (plan.resume && !SESSION_UUID_RE.test(plan.resume)) throw new Error("bad session id");
4857
- mkdirSync18(launchDir(), { recursive: true, mode: 448 });
6559
+ if (!validPlan(plan)) throw new Error("bad launch plan");
6560
+ mkdirSync17(launchDir(), { recursive: true, mode: 448 });
4858
6561
  const tmp = `${planFile(runId)}.${process.pid}.tmp`;
4859
- writeFileSync16(tmp, JSON.stringify(plan), { encoding: "utf8", mode: 384 });
6562
+ writeFileSync15(tmp, JSON.stringify(plan), { encoding: "utf8", mode: 384 });
4860
6563
  restrictToUser(tmp);
4861
- renameSync14(tmp, planFile(runId));
6564
+ renameSync13(tmp, planFile(runId));
4862
6565
  }
4863
6566
  function claimLaunch(runId) {
4864
6567
  if (!RUN_ID_RE.test(runId)) return null;
4865
- const claimed = join22(launchDir(), `${runId}.${process.pid}.claimed`);
6568
+ const claimed = join23(launchDir(), `${runId}.${process.pid}.claimed`);
4866
6569
  try {
4867
- renameSync14(planFile(runId), claimed);
6570
+ renameSync13(planFile(runId), claimed);
4868
6571
  } catch {
4869
6572
  return null;
4870
6573
  }
4871
6574
  try {
4872
6575
  const p = JSON.parse(readFileSync16(claimed, "utf8"));
4873
6576
  if (!p || typeof p.cwd !== "string" || typeof p.resume !== "string") return null;
4874
- if (p.resume && !SESSION_UUID_RE.test(p.resume)) return null;
4875
- return p;
6577
+ if (p.tool === void 0) p.tool = "claude_code";
6578
+ return validPlan(p) ? p : null;
4876
6579
  } catch {
4877
6580
  return null;
4878
6581
  } finally {
@@ -4892,10 +6595,10 @@ function launchPending(runId) {
4892
6595
  }
4893
6596
 
4894
6597
  // src/daemon/openWindow.ts
4895
- import { execFile as execFile4, execFileSync as execFileSync11, spawn as spawn3 } from "child_process";
4896
- import { chmodSync as chmodSync6, mkdirSync as mkdirSync19, rmSync as rmSync12, writeFileSync as writeFileSync17 } from "fs";
6598
+ import { execFile as execFile4, execFileSync as execFileSync12, spawn as spawn4 } from "child_process";
6599
+ import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "fs";
4897
6600
  import { userInfo } from "os";
4898
- import { join as join23 } from "path";
6601
+ import { join as join24 } from "path";
4899
6602
  function safePath(p) {
4900
6603
  return /^(?:[A-Za-z]:[\\/]|\/)/.test(p) && !/[\r\n\0"'`$%;]/.test(p);
4901
6604
  }
@@ -4943,14 +6646,14 @@ var defaultGuiDeps = () => ({
4943
6646
  env: process.env,
4944
6647
  macManager: () => {
4945
6648
  try {
4946
- 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 });
4947
6650
  } catch {
4948
6651
  return "";
4949
6652
  }
4950
6653
  },
4951
6654
  macConsoleUser: () => {
4952
6655
  try {
4953
- 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 });
4954
6657
  } catch {
4955
6658
  return "";
4956
6659
  }
@@ -4964,7 +6667,7 @@ var defaultGuiDeps = () => ({
4964
6667
  },
4965
6668
  winSessionId: () => {
4966
6669
  try {
4967
- 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"], {
4968
6671
  encoding: "utf8",
4969
6672
  stdio: ["ignore", "pipe", "ignore"],
4970
6673
  windowsHide: true,
@@ -4981,11 +6684,11 @@ var defaultGuiDeps = () => ({
4981
6684
  var defaultOpenerDeps = (findWt2) => ({
4982
6685
  platform: process.platform,
4983
6686
  findWt: findWt2,
4984
- run: (file, args) => new Promise((resolve6, reject) => {
4985
- 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());
4986
6689
  }),
4987
6690
  spawnDetached: (file, args) => {
4988
- 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 });
4989
6692
  c.on("error", () => {
4990
6693
  });
4991
6694
  c.unref();
@@ -4994,9 +6697,9 @@ var defaultOpenerDeps = (findWt2) => ({
4994
6697
  async function openTerminal(t, d) {
4995
6698
  checkTarget(t);
4996
6699
  if (d.platform === "darwin") {
4997
- mkdirSync19(launchDir(t.home), { recursive: true, mode: 448 });
4998
- const file = join23(launchDir(t.home), `${t.runId}.command`);
4999
- writeFileSync17(file, macCommandScript(t), { encoding: "utf8", mode: 448 });
6700
+ mkdirSync18(launchDir(t.home), { recursive: true, mode: 448 });
6701
+ const file = join24(launchDir(t.home), `${t.runId}.command`);
6702
+ writeFileSync16(file, macCommandScript(t), { encoding: "utf8", mode: 448 });
5000
6703
  chmodSync6(file, 448);
5001
6704
  await d.run("open", ["-a", "Terminal", file]);
5002
6705
  return "terminal";
@@ -5015,7 +6718,7 @@ async function openTerminal(t, d) {
5015
6718
  function removeCommandFile(home, runId) {
5016
6719
  if (!RUN_ID_RE.test(runId)) return;
5017
6720
  try {
5018
- rmSync12(join23(launchDir(home), `${runId}.command`), { force: true });
6721
+ rmSync12(join24(launchDir(home), `${runId}.command`), { force: true });
5019
6722
  } catch {
5020
6723
  }
5021
6724
  }
@@ -5321,7 +7024,7 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
5321
7024
  } catch {
5322
7025
  }
5323
7026
  }
5324
- return new Promise((resolve6) => {
7027
+ return new Promise((resolve7) => {
5325
7028
  const server = createServer3((sock) => {
5326
7029
  sock.on("error", () => {
5327
7030
  });
@@ -5407,30 +7110,30 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
5407
7110
  };
5408
7111
  server.once("error", () => {
5409
7112
  restore();
5410
- resolve6(null);
7113
+ resolve7(null);
5411
7114
  });
5412
7115
  server.listen(path, () => {
5413
7116
  restore();
5414
7117
  server.on("error", () => {
5415
7118
  });
5416
- resolve6(server);
7119
+ resolve7(server);
5417
7120
  });
5418
7121
  });
5419
7122
  }
5420
7123
 
5421
7124
  // src/core/appended.ts
5422
- import { chmodSync as chmodSync7, mkdirSync as mkdirSync20, readFileSync as readFileSync17, renameSync as renameSync15, rmSync as rmSync14, writeFileSync as writeFileSync18 } from "fs";
5423
- import { join as join24 } 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";
5424
7127
  var APPENDED_MAX_ITEMS = 5;
5425
7128
  var APPENDED_TEXT_MAX = 500;
5426
7129
  var APPENDED_RESULT_MAX = 800;
5427
7130
  var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5428
7131
  function appendedDir() {
5429
- return join24(deskbuddyHome(), "appended");
7132
+ return join25(deskbuddyHome(), "appended");
5430
7133
  }
5431
7134
  function appendedPath(sessionId) {
5432
7135
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
5433
- return safe ? join24(appendedDir(), `${safe}.json`) : null;
7136
+ return safe ? join25(appendedDir(), `${safe}.json`) : null;
5434
7137
  }
5435
7138
  function readAppended(sessionId) {
5436
7139
  const file = appendedPath(sessionId);
@@ -5449,26 +7152,26 @@ function writeAppended(sessionId, item) {
5449
7152
  const items = readAppended(sessionId).concat([
5450
7153
  { text: item.text.slice(0, APPENDED_TEXT_MAX), result: item.result.slice(0, APPENDED_RESULT_MAX), ok: item.ok, at: item.at }
5451
7154
  ]);
5452
- mkdirSync20(appendedDir(), { recursive: true, mode: 448 });
7155
+ mkdirSync19(appendedDir(), { recursive: true, mode: 448 });
5453
7156
  const tmp = `${file}.${process.pid}.tmp`;
5454
- writeFileSync18(tmp, JSON.stringify({ items: items.slice(-APPENDED_MAX_ITEMS) }), { encoding: "utf8", mode: 384 });
7157
+ writeFileSync17(tmp, JSON.stringify({ items: items.slice(-APPENDED_MAX_ITEMS) }), { encoding: "utf8", mode: 384 });
5455
7158
  chmodSync7(tmp, 384);
5456
- renameSync15(tmp, file);
7159
+ renameSync14(tmp, file);
5457
7160
  } catch {
5458
7161
  }
5459
7162
  }
5460
7163
  var RUNNING_TTL_MS = 60 * 60 * 1e3;
5461
7164
  function runningPath(sessionId) {
5462
7165
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
5463
- return safe ? join24(deskbuddyHome(), "running", `${safe}.json`) : null;
7166
+ return safe ? join25(deskbuddyHome(), "running", `${safe}.json`) : null;
5464
7167
  }
5465
7168
  function writeRunning(sessionId, now = Date.now()) {
5466
7169
  const file = runningPath(sessionId);
5467
7170
  if (!file) return;
5468
7171
  try {
5469
- mkdirSync20(join24(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
5470
- writeFileSync18(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
5471
- renameSync15(`${file}.tmp`, file);
7172
+ mkdirSync19(join25(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
7173
+ writeFileSync17(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
7174
+ renameSync14(`${file}.tmp`, file);
5472
7175
  } catch {
5473
7176
  }
5474
7177
  }
@@ -5527,15 +7230,27 @@ var IDLE_POLL_MS2 = 1e3;
5527
7230
  var IDLE_QUEUE_MAX_MS = 30 * 60 * 1e3;
5528
7231
  var BUSY_STATES2 = /* @__PURE__ */ new Set(["watch", "ask_approval"]);
5529
7232
  var CODEX_WATCH_MS = 5 * 1e3;
7233
+ var DUMATE_WATCH_MS = 2500;
5530
7234
  function defaultCodexAppServers() {
5531
7235
  const cfg = loadConfig();
5532
7236
  return cfg.enabledTools.includes("codex") ? discoverAppServers(cfg.codexAppServer) : [];
5533
7237
  }
5534
7238
  function opencodeReady() {
5535
- return isOcPluginInstalled(opencodePluginPath()) && existsSync13(ocPluginModule(installedHookPath()));
7239
+ const mod = ocPluginModule(installedHookPath());
7240
+ return isOcPluginInstalled(opencodePluginPath()) && existsSync13(mod) && existsSync13(join26(dirname10(mod), "ocTranslate.mjs"));
5536
7241
  }
7242
+ var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
5537
7243
  function orphanResumeText(summary2) {
5538
- 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;
5539
7254
  }
5540
7255
  var ALIVE_REPORT_MS = 60 * 1e3;
5541
7256
  var Daemon = class {
@@ -5547,6 +7262,8 @@ var Daemon = class {
5547
7262
  #gate;
5548
7263
  /** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
5549
7264
  #runsDone = /* @__PURE__ */ new Map();
7265
+ /** runId → 子进程 pid(按 hook 状态里的进程号认出新建任务建出的会话) */
7266
+ #runPids = /* @__PURE__ */ new Map();
5550
7267
  #attempt = 0;
5551
7268
  #stopped = false;
5552
7269
  #ws = null;
@@ -5565,6 +7282,11 @@ var Daemon = class {
5565
7282
  #wakeReport = null;
5566
7283
  /** url → 连着它的 CodexWatch。插入顺序 = 发现顺序(DeskBuddy 管的在前) */
5567
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;
5568
7290
  #codexStarted = false;
5569
7291
  #codexReportTimer = null;
5570
7292
  /** daemon 交出去、还没有结局的指令流程数(开窗 / 写进窗口 / 经 app-server 发一轮)。空闲判定用 */
@@ -5590,6 +7312,14 @@ var Daemon = class {
5590
7312
  sendToWindow: deps.sendToWindow ?? ((id, req2, onReply) => sendToWindow(channelPath(id), windowSecret(id), req2, onReply)),
5591
7313
  gui: deps.gui ?? (() => guiAvailable(defaultGuiDeps())),
5592
7314
  openWindow: deps.openWindow ?? ((runId) => this.#openTerminal(runId)),
7315
+ ancestors: deps.ancestors ?? ((pid) => selfAndAncestors(pid)),
7316
+ launchable: deps.launchable ?? ((tool) => {
7317
+ try {
7318
+ return locate(tool) != null;
7319
+ } catch {
7320
+ return false;
7321
+ }
7322
+ }),
5593
7323
  endedCwd: deps.endedCwd ?? ((sid, tool, stateCwd) => resolveEndedCwd(sid, tool, {
5594
7324
  readEnded: (s) => readEnded(s) ?? (stateCwd ? { sessionId: s, tool, cwd: stateCwd, interactive: false, endedAt: 0 } : null),
5595
7325
  transcriptCwd: (s) => claudeTranscriptCwd(s),
@@ -5628,7 +7358,21 @@ var Daemon = class {
5628
7358
  log,
5629
7359
  now: this.#deps.now
5630
7360
  });
5631
- 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
+ );
5632
7376
  }
5633
7377
  /** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
5634
7378
  async startGate() {
@@ -5642,6 +7386,8 @@ var Daemon = class {
5642
7386
  this.#codexTick();
5643
7387
  this.#codexTimer = setInterval(() => this.#codexTick(), CODEX_WATCH_MS);
5644
7388
  this.#codexTimer.unref();
7389
+ this.#dumateTimer = setInterval(() => this.#dumate.tick(), DUMATE_WATCH_MS);
7390
+ this.#dumateTimer.unref();
5645
7391
  await this.startWake();
5646
7392
  return port;
5647
7393
  }
@@ -5683,7 +7429,7 @@ var Daemon = class {
5683
7429
  const cfg = loadConfig();
5684
7430
  if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes(tool)) return;
5685
7431
  try {
5686
- await postReport(buildSnapshot(withWake(readAliveStates(tool)), tool, cfg, this.#deps.now()));
7432
+ await postReport(buildSnapshot(withWake(this.#aliveStates(tool)), tool, cfg, this.#deps.now()));
5687
7433
  } catch (e) {
5688
7434
  log(`\u4E0A\u62A5 ${tool} \u4F1A\u8BDD\u5931\u8D25\uFF1A${e.message}`);
5689
7435
  }
@@ -5756,7 +7502,7 @@ var Daemon = class {
5756
7502
  const cfg = loadConfig();
5757
7503
  if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes("codex")) return;
5758
7504
  try {
5759
- 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()));
5760
7506
  } catch (e) {
5761
7507
  log(`\u4E0A\u62A5 codex \u5FEB\u7167\u5931\u8D25\uFF1A${e.message}`);
5762
7508
  }
@@ -5780,13 +7526,23 @@ var Daemon = class {
5780
7526
  log(`\u4E0A\u62A5\u6700\u8FD1\u76EE\u5F55\u5931\u8D25\uFF1A${e.message}`);
5781
7527
  }
5782
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
+ }
5783
7533
  /** 按已启用的工具各报一次存活会话快照(readAliveStates 顺手剔除死进程) */
5784
7534
  async #reportAlive() {
5785
7535
  const cfg = loadConfig();
5786
7536
  if (!cfg.clientId || !cfg.boundAt) return;
5787
7537
  for (const tool of cfg.enabledTools) {
5788
7538
  await postReport(
5789
- 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
+ )
5790
7546
  );
5791
7547
  }
5792
7548
  if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
@@ -5846,6 +7602,9 @@ var Daemon = class {
5846
7602
  if (this.#codexReportTimer) clearTimeout(this.#codexReportTimer);
5847
7603
  for (const w of this.#codex.values()) w.stop();
5848
7604
  this.#codex.clear();
7605
+ this.#qwenwork.stop();
7606
+ if (this.#dumateTimer) clearInterval(this.#dumateTimer);
7607
+ this.#dumate.stop();
5849
7608
  if (this.#codexStarted) removeCodexRemote();
5850
7609
  this.#ws?.close();
5851
7610
  void this.#gate.stop();
@@ -5950,7 +7709,7 @@ var Daemon = class {
5950
7709
  if (!this.#seen.remember(ap.instructionId)) return;
5951
7710
  this.#seen.save();
5952
7711
  log(`\u6536\u5230\u6388\u6743\u51B3\u5B9A ${ap.approvalId} ${ap.decision}`);
5953
- this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName);
7712
+ this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName, ap.answers);
5954
7713
  return;
5955
7714
  }
5956
7715
  const cmd = parseCommand(frame);
@@ -5966,6 +7725,7 @@ var Daemon = class {
5966
7725
  }
5967
7726
  this.#seen.save();
5968
7727
  cmd.receivedAt = this.#deps.now();
7728
+ log(`\u6536\u5230\u6307\u4EE4 ${cmd.instructionId} tool=${cmd.tool} ${cmd.sessionId ? `\u56DE\u590D ${cmd.sessionId.slice(0, 8)}` : "\u65B0\u5EFA"}`);
5969
7729
  this.#route(cmd);
5970
7730
  }
5971
7731
  /**
@@ -6023,7 +7783,8 @@ var Daemon = class {
6023
7783
  const sid = resolveAlias(cmd.sessionId);
6024
7784
  if (this.#wake.submit(cmd, sid)) return;
6025
7785
  }
6026
- if (cmd.tool === "claude_code" && !isExpired(cmd, this.#deps.now())) {
7786
+ const opensWindow = (cmd.tool === "claude_code" || !cmd.sessionId && LAUNCH_TOOLS.includes(cmd.tool)) && this.#launchable(cmd.tool);
7787
+ if (opensWindow && !isExpired(cmd, this.#deps.now())) {
6027
7788
  void this.#track(this.#openOrDispatch(cmd)).catch((e) => {
6028
7789
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5F00\u7A97\u6D41\u7A0B\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
6029
7790
  this.#report(cmd, cmd.sessionId, "failed", "", "window_failed");
@@ -6046,11 +7807,22 @@ var Daemon = class {
6046
7807
  }
6047
7808
  this.#dispatch(cmd);
6048
7809
  }
7810
+ /** 开窗前预检:找不到可执行文件就走原后台路径(它按原方式快速回报找不到),不开一个立刻 127 的窗口 */
7811
+ #launchable(tool) {
7812
+ let ok = false;
7813
+ try {
7814
+ ok = this.#deps.launchable(tool);
7815
+ } catch {
7816
+ ok = false;
7817
+ }
7818
+ if (!ok) log(`\u672C\u673A\u627E\u4E0D\u5230 ${tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u4E0D\u5F00\u7A97\uFF0C\u6309\u540E\u53F0\u8DEF\u5F84\u5904\u7406`);
7819
+ return ok;
7820
+ }
6049
7821
  /**
6050
7822
  * 新建任务、会话已结束 → 开窗;会话还活着(后台 -p 在跑、或在用户自己开的窗口里)→ 原来的路径(排队 / window_open)
6051
7823
  */
6052
7824
  async #openOrDispatch(cmd) {
6053
- if (tooLongToInject(cmd.text)) {
7825
+ if (cmd.tool === "claude_code" && this.#guiAvailable() && tooLongToInject(cmd.text)) {
6054
7826
  log(`\u6307\u4EE4 ${cmd.instructionId} \u592A\u957F\uFF08${cmd.text.length} \u5B57\uFF09\uFF0C\u4E0D\u6267\u884C`);
6055
7827
  this.#report(cmd, cmd.sessionId ? resolveAlias(cmd.sessionId) : "", "rejected", "", TEXT_TOO_LONG);
6056
7828
  return;
@@ -6101,6 +7873,13 @@ var Daemon = class {
6101
7873
  release2();
6102
7874
  }
6103
7875
  }
7876
+ #guiAvailable() {
7877
+ try {
7878
+ return this.#deps.gui();
7879
+ } catch {
7880
+ return false;
7881
+ }
7882
+ }
6104
7883
  /**
6105
7884
  * 开窗 → 等认领 → 首句经通道交给窗口;任何一步起不来 → 后台 -p(回报 started/headless)。
6106
7885
  * 返回时:窗口已收下首句 / 已按失败回报 / 降级的后台那次已跑完
@@ -6110,16 +7889,10 @@ var Daemon = class {
6110
7889
  log(`\u6307\u4EE4 ${cmd.instructionId} ${why}\uFF0C\u6539\u540E\u53F0\u6267\u884C`);
6111
7890
  return this.#headless(c, sid, cwd);
6112
7891
  };
6113
- let gui = false;
6114
- try {
6115
- gui = this.#deps.gui();
6116
- } catch {
6117
- gui = false;
6118
- }
6119
- if (!gui) return headless("\u6CA1\u6709\u56FE\u5F62\u4F1A\u8BDD\uFF08SSH / \u9501\u5B9A\u7684\u670D\u52A1\u4F1A\u8BDD / Linux\uFF09");
7892
+ if (!this.#guiAvailable()) return headless("\u6CA1\u6709\u56FE\u5F62\u4F1A\u8BDD\uFF08SSH / \u9501\u5B9A\u7684\u670D\u52A1\u4F1A\u8BDD / Linux\uFF09");
6120
7893
  const runId = randomBytes7(16).toString("hex");
6121
7894
  try {
6122
- writeLaunch(runId, { tool: "claude_code", cwd, resume: sid, at: this.#deps.now() });
7895
+ writeLaunch(runId, { tool: cmd.tool, cwd, resume: sid, at: this.#deps.now() });
6123
7896
  } catch (e) {
6124
7897
  return headless(`\u5199\u4EA4\u63A5\u5355\u5931\u8D25\uFF1A${e.message}`);
6125
7898
  }
@@ -6133,9 +7906,9 @@ var Daemon = class {
6133
7906
  }
6134
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`);
6135
7908
  }
6136
- const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
7909
+ const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
6137
7910
  const claimDeadline = this.#deps.now() + this.#deps.claimWaitMs;
6138
- while (launchPending(runId) && this.#deps.now() < claimDeadline) await sleep2(300);
7911
+ while (launchPending(runId) && this.#deps.now() < claimDeadline) await sleep3(300);
6139
7912
  removeCommandFile(deskbuddyHome(), runId);
6140
7913
  if (launchPending(runId) && claimLaunch(runId)) return headless(`${Math.round(this.#deps.claimWaitMs / 1e3)} \u79D2\u5185\u7A97\u53E3\u6CA1\u8D77\u6765`);
6141
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`);
@@ -6173,14 +7946,14 @@ var Daemon = class {
6173
7946
  found = null;
6174
7947
  }
6175
7948
  if (found) break;
6176
- await sleep2(200);
7949
+ await sleep3(200);
6177
7950
  }
6178
7951
  }
6179
- await sleep2(50);
7952
+ await sleep3(50);
6180
7953
  if (fb.p) await fb.p;
6181
7954
  return;
6182
7955
  }
6183
- await sleep2(1e3);
7956
+ await sleep3(1e3);
6184
7957
  }
6185
7958
  log(`\u6307\u4EE4 ${cmd.instructionId} \u65B0\u7A97\u53E3\u7684\u901A\u9053\u4E00\u76F4\u6CA1\u5C31\u7EEA`);
6186
7959
  this.#report(cmd, sid, "failed", "window", "window_failed");
@@ -6201,7 +7974,7 @@ var Daemon = class {
6201
7974
  }
6202
7975
  return this.#queue.enqueue(
6203
7976
  sid,
6204
- () => this.#execute(cmd, sid, cwd, cmd.tool, true),
7977
+ () => this.#execute(cmd, sid, cwd, true),
6205
7978
  (e) => {
6206
7979
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u65F6\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
6207
7980
  this.#report(cmd, sid, "failed", "", "exec_failed");
@@ -6330,8 +8103,6 @@ var Daemon = class {
6330
8103
  }
6331
8104
  const target = resolveTarget(cmd, {
6332
8105
  readState,
6333
- readHandle,
6334
- handleIsUsable,
6335
8106
  remoteCwd: () => loadConfig().remoteCwd || "",
6336
8107
  alias: resolveAlias,
6337
8108
  dirOf: (ref) => dirOf(ref),
@@ -6342,10 +8113,28 @@ var Daemon = class {
6342
8113
  this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
6343
8114
  return;
6344
8115
  }
8116
+ const before = target.sessionId ? this.#runsOfSession(target.sessionId) : [];
8117
+ for (const r of before) this.#coord.supersedeRun(r.runId, target.sessionId);
6345
8118
  if (target.sessionId) this.#coord.cancelSession(target.sessionId);
8119
+ const key = target.sessionId || `new:${cmd.instructionId}`;
8120
+ if (this.#queue.busy(key) || before.length > 0) {
8121
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u4F1A\u8BDD ${key.slice(0, 8)} \u524D\u9762\u8FD8\u6709\u4EFB\u52A1\uFF0C\u6392\u961F`);
8122
+ this.#report(cmd, target.sessionId, "queued", "headless");
8123
+ }
6346
8124
  this.#queue.enqueue(
6347
- target.sessionId,
6348
- () => this.#execute(cmd, target.sessionId, target.cwd, target.handleKey),
8125
+ key,
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
+ },
6349
8138
  (e) => {
6350
8139
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u65F6\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
6351
8140
  this.#report(cmd, target.sessionId, "failed", "", "exec_failed");
@@ -6390,7 +8179,7 @@ var Daemon = class {
6390
8179
  log(`\u6307\u4EE4 ${cmd.instructionId} \u4F1A\u8BDD\u5728\u7535\u8111\u7A97\u53E3\u91CC\u7A7A\u95F2\uFF0C\u540E\u53F0\u7EED\u8DD1`);
6391
8180
  writeRunning(sid, this.#deps.now());
6392
8181
  try {
6393
- await this.#execute(cmd, sid, runCwd, cmd.tool, true, (r) => {
8182
+ await this.#execute(cmd, sid, runCwd, true, (r) => {
6394
8183
  const now = readState(sid);
6395
8184
  if (!now || now.interactive !== true && now.embedded !== true || !now.pid || !isAlive(now.pid, now.pidName)) return;
6396
8185
  writeAppended(sid, { text: cmd.text, result: resultSummary(cmd.tool, r.stdout), ok: r.ok, at: this.#deps.now() });
@@ -6404,7 +8193,12 @@ var Daemon = class {
6404
8193
  this.#report(cmd, sid, "failed", "", "exec_failed");
6405
8194
  });
6406
8195
  }
6407
- async #execute(cmd, sessionId, cwd, handleKey = cmd.tool, ended = false, onResult) {
8196
+ async #execute(cmd, sessionId, cwd, ended = false, onResult) {
8197
+ if (isExpired(cmd, this.#deps.now())) {
8198
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u6392\u961F\u5230\u6267\u884C\u65F6\u5DF2\u8FC7\u671F\uFF0C\u4E0D\u6267\u884C`);
8199
+ this.#report(cmd, sessionId, "rejected", "", "expired");
8200
+ return;
8201
+ }
6408
8202
  if (sessionId && !ended) {
6409
8203
  const fresh = readState(sessionId);
6410
8204
  if (!fresh) {
@@ -6420,7 +8214,7 @@ var Daemon = class {
6420
8214
  this.#report(cmd, sessionId, "rejected", "", "tool_unsupported");
6421
8215
  return;
6422
8216
  }
6423
- 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}`);
6424
8218
  const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd, () => this.#report(cmd, sessionId, "started", "headless"));
6425
8219
  if ("fail" in result) {
6426
8220
  log(
@@ -6432,22 +8226,65 @@ var Daemon = class {
6432
8226
  if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
6433
8227
  onResult?.(result);
6434
8228
  if (!result.ok) {
6435
- 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}` : ""}`);
6436
8231
  this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
6437
8232
  }
6438
8233
  if (!sessionId) {
6439
8234
  const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
6440
- if (newId) {
6441
- writeHandle(handleKey, { sessionId: newId, cwd, createdAt: this.#deps.now() });
6442
- log(`\u6307\u4EE4 ${cmd.instructionId} \u5EFA\u51FA\u4F1A\u8BDD ${newId}\uFF0C\u5DF2\u8BB0\u4E3A ${cmd.tool} \u7684\u8FDC\u7A0B\u4F1A\u8BDD`);
6443
- } else {
6444
- log(`\u6307\u4EE4 ${cmd.instructionId} \u672A\u80FD\u89E3\u6790\u51FA\u4F1A\u8BDD id\uFF0C\u4E0B\u6B21\u4ECD\u4F1A\u65B0\u5EFA`);
6445
- }
8235
+ log(`\u6307\u4EE4 ${cmd.instructionId} ${newId ? `\u5EFA\u51FA\u4F1A\u8BDD ${newId}` : "\u672A\u80FD\u89E3\u6790\u51FA\u4F1A\u8BDD id"}`);
6446
8236
  }
6447
8237
  }
6448
8238
  #gateTarget(runId) {
6449
8239
  return { runId, nodePath: this.#deps.nodePath, gateScript: this.#deps.gateScript };
6450
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
+ }
6451
8288
  /** 设备上有了决定、需要续会话。排进该会话的串行队列,和普通指令一样不会并发打同一个会话 */
6452
8289
  #scheduleResume(r, kind) {
6453
8290
  if (!r.sessionId) {
@@ -6487,8 +8324,10 @@ var Daemon = class {
6487
8324
  }
6488
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`}`);
6489
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`);
6490
- else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
6491
- 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`);
6492
8331
  }
6493
8332
  /**
6494
8333
  * 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
@@ -6520,13 +8359,19 @@ var Daemon = class {
6520
8359
  onWaitEnd: () => rc.resumeTimeout()
6521
8360
  });
6522
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;
6523
8365
  try {
6524
- return await rc.done;
8366
+ result = await rc.done;
8367
+ return result;
6525
8368
  } finally {
6526
8369
  this.#runsDone.delete(runId);
6527
- const sid = sessionId || this.#coord.sessionOf(runId);
8370
+ this.#runPids.delete(runId);
8371
+ const sid = sessionId || this.#coord.sessionOf(runId) || result?.sessionId || "";
6528
8372
  this.#coord.endRun(runId);
6529
8373
  this.#coord.settleSession(sid, startedAt);
8374
+ if (result) this.#settleStateAfterRun(sid, result, tool);
6530
8375
  }
6531
8376
  }
6532
8377
  };
@@ -6565,7 +8410,7 @@ async function runDaemonCommand(opts = {}) {
6565
8410
  }
6566
8411
  if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
6567
8412
  try {
6568
- writeFileSync19(pidFilePath(), String(process.pid), "utf8");
8413
+ writeFileSync18(pidFilePath(), String(process.pid), "utf8");
6569
8414
  } catch {
6570
8415
  }
6571
8416
  let daemon;
@@ -6612,15 +8457,16 @@ async function runDaemonCommand(opts = {}) {
6612
8457
  }
6613
8458
 
6614
8459
  // src/core/registry.ts
6615
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync18, writeFileSync as writeFileSync20 } from "fs";
6616
- import { join as join25 } from "path";
8460
+ import { mkdirSync as mkdirSync20, readFileSync as readFileSync18, writeFileSync as writeFileSync19 } from "fs";
8461
+ import { join as join27 } from "path";
6617
8462
  var FALLBACK = {
6618
8463
  version: "builtin",
6619
8464
  tools: {
6620
8465
  claude_code: {
6621
8466
  format: "cc-hook",
6622
8467
  processNames: ["claude"],
6623
- titleSources: ["cc-ai-title", "git-branch", "project"],
8468
+ titleSources: ["cc-ai-title", "first-prompt", "git-branch", "project"],
8469
+ turnEvents: ["UserPromptSubmit"],
6624
8470
  events: {
6625
8471
  UserPromptSubmit: "watch",
6626
8472
  PreToolUse: "watch",
@@ -6639,6 +8485,7 @@ var FALLBACK = {
6639
8485
  // codex-thread-name 是机会主义的一级:session_index.jsonl 里的 thread_name
6640
8486
  // 只有 /rename 或 VS Code 扩展起过名的才有,多数是空的,取不到用首条指令
6641
8487
  titleSources: ["codex-thread-name", "first-prompt", "git-branch", "project"],
8488
+ turnEvents: ["UserPromptSubmit"],
6642
8489
  events: {
6643
8490
  UserPromptSubmit: "watch",
6644
8491
  PreToolUse: "watch",
@@ -6654,6 +8501,7 @@ var FALLBACK = {
6654
8501
  // WorkBuddy 的 transcript 与 Claude Code 同格式,也写 ai-title(2026-10-01 WorkBuddy 5.6.2 实测);
6655
8502
  // 工作目录是按时间自动建的,目录名不能当标题
6656
8503
  titleSources: ["cc-ai-title", "first-prompt", "git-branch", "project"],
8504
+ turnEvents: ["UserPromptSubmit"],
6657
8505
  events: {
6658
8506
  UserPromptSubmit: "watch",
6659
8507
  PreToolUse: "watch",
@@ -6670,6 +8518,7 @@ var FALLBACK = {
6670
8518
  // 桌面端会话库里的标题(先是首条指令,后被换成 AI 摘要);命令行跑的会话不在库里,落到首条指令
6671
8519
  // (CLI 1.1.61 实测 transcript 里没有 ai-title,UserPromptSubmit 带 prompt)
6672
8520
  titleSources: ["qoder-session-title", "first-prompt", "git-branch", "project"],
8521
+ turnEvents: ["UserPromptSubmit"],
6673
8522
  events: {
6674
8523
  UserPromptSubmit: "watch",
6675
8524
  PreToolUse: "watch",
@@ -6684,6 +8533,7 @@ var FALLBACK = {
6684
8533
  format: "cc-hook",
6685
8534
  processNames: ["Qoder"],
6686
8535
  titleSources: ["qoder-session-title", "first-prompt", "git-branch", "project"],
8536
+ turnEvents: ["UserPromptSubmit"],
6687
8537
  events: {
6688
8538
  UserPromptSubmit: "watch",
6689
8539
  PreToolUse: "watch",
@@ -6693,11 +8543,51 @@ var FALLBACK = {
6693
8543
  },
6694
8544
  lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
6695
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
+ },
6696
8585
  opencode: {
6697
8586
  format: "oc-plugin",
6698
8587
  processNames: ["opencode"],
6699
8588
  // opencode 自己生成的会话标题(插件从 session.updated 取,随事件传 oc_session_title),没有就用首条指令
6700
8589
  titleSources: ["oc-session-title", "first-prompt", "git-branch", "project"],
8590
+ turnEvents: ["UserPromptSubmit"],
6701
8591
  events: {
6702
8592
  UserPromptSubmit: "watch",
6703
8593
  PreToolUse: "watch",
@@ -6710,7 +8600,7 @@ var FALLBACK = {
6710
8600
  }
6711
8601
  };
6712
8602
  function registryCachePath() {
6713
- return join25(deskbuddyHome(), "tool-registry.json");
8603
+ return join27(deskbuddyHome(), "tool-registry.json");
6714
8604
  }
6715
8605
  function withFallbackTools(registry) {
6716
8606
  if (!registry || typeof registry !== "object" || !registry.tools || typeof registry.tools !== "object") return FALLBACK;
@@ -6726,15 +8616,15 @@ function loadRegistry() {
6726
8616
  async function refreshRegistry() {
6727
8617
  try {
6728
8618
  const registry = await api.toolRegistry();
6729
- mkdirSync21(deskbuddyHome(), { recursive: true });
6730
- writeFileSync20(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
8619
+ mkdirSync20(deskbuddyHome(), { recursive: true });
8620
+ writeFileSync19(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
6731
8621
  } catch {
6732
8622
  }
6733
8623
  }
6734
8624
 
6735
8625
  // src/adapters/ccHook.ts
6736
- import { chmodSync as chmodSync8, existsSync as existsSync14, mkdirSync as mkdirSync22, readFileSync as readFileSync19, renameSync as renameSync16, rmSync as rmSync16, statSync as statSync7, writeFileSync as writeFileSync21 } from "fs";
6737
- import { dirname as dirname9, join as join26 } 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";
6738
8628
  var CC_EVENTS = [
6739
8629
  "UserPromptSubmit",
6740
8630
  "PreToolUse",
@@ -6754,17 +8644,17 @@ function readSettings(path) {
6754
8644
  }
6755
8645
  }
6756
8646
  function writeSettings(path, settings) {
6757
- mkdirSync22(dirname9(path), { recursive: true });
8647
+ mkdirSync21(dirname11(path), { recursive: true });
6758
8648
  let mode;
6759
8649
  try {
6760
- mode = statSync7(path).mode & 511;
8650
+ mode = statSync9(path).mode & 511;
6761
8651
  } catch {
6762
8652
  mode = void 0;
6763
8653
  }
6764
8654
  const tmp = path + ".deskbuddy.tmp";
6765
- writeFileSync21(tmp, JSON.stringify(settings, null, 2), "utf8");
8655
+ writeFileSync20(tmp, JSON.stringify(settings, null, 2), "utf8");
6766
8656
  if (mode !== void 0) chmodSync8(tmp, mode);
6767
- renameSync16(tmp, path);
8657
+ renameSync15(tmp, path);
6768
8658
  }
6769
8659
  function mentions(command, script) {
6770
8660
  return !!command && (command.includes(script) || command.includes(fwdSlash(script)));
@@ -6886,8 +8776,8 @@ function retagCcHook(settingsPath, scripts, from, to) {
6886
8776
  function uninstallCcHook(settingsPath, hookPath) {
6887
8777
  const settings = readSettings(settingsPath);
6888
8778
  if (!settings.hooks) return;
6889
- const gatePath = join26(dirname9(hookPath), "gate.cjs");
6890
- const wakePath = join26(dirname9(hookPath), "wake.cjs");
8779
+ const gatePath = join28(dirname11(hookPath), "gate.cjs");
8780
+ const wakePath = join28(dirname11(hookPath), "wake.cjs");
6891
8781
  for (const event of Object.keys(settings.hooks)) {
6892
8782
  const groups = settings.hooks[event] ?? [];
6893
8783
  const kept = [];
@@ -6901,14 +8791,14 @@ function uninstallCcHook(settingsPath, hookPath) {
6901
8791
  }
6902
8792
  writeSettings(settingsPath, settings);
6903
8793
  try {
6904
- rmSync16(join26(dirname9(hookPath), "wake", "tokens"), { recursive: true, force: true });
8794
+ rmSync16(join28(dirname11(hookPath), "wake", "tokens"), { recursive: true, force: true });
6905
8795
  } catch {
6906
8796
  }
6907
8797
  }
6908
8798
 
6909
8799
  // src/adapters/codexHook.ts
6910
- import { chmodSync as chmodSync9, existsSync as existsSync15, mkdirSync as mkdirSync23, readFileSync as readFileSync20, renameSync as renameSync17, statSync as statSync8, writeFileSync as writeFileSync22 } from "fs";
6911
- import { dirname as dirname10, join as join27 } 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";
6912
8802
  var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
6913
8803
  var CODEX_MARKER_END = "# <<< deskbuddy <<<";
6914
8804
  var CODEX_EVENTS = [
@@ -6921,7 +8811,7 @@ var CODEX_EVENTS = [
6921
8811
  ];
6922
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";
6923
8813
  function codexConfigPath() {
6924
- return join27(codexHome(), "config.toml");
8814
+ return join29(codexHome(), "config.toml");
6925
8815
  }
6926
8816
  var CODEX_APPENDED_TIMEOUT_S = 3;
6927
8817
  function renderBlock(hookPath, events, tool, platform) {
@@ -6940,17 +8830,17 @@ function renderBlock(hookPath, events, tool, platform) {
6940
8830
  return lines2.join("\n");
6941
8831
  }
6942
8832
  function writeAtomic(path, content) {
6943
- mkdirSync23(dirname10(path), { recursive: true });
8833
+ mkdirSync22(dirname12(path), { recursive: true });
6944
8834
  let mode;
6945
8835
  try {
6946
- mode = statSync8(path).mode & 511;
8836
+ mode = statSync10(path).mode & 511;
6947
8837
  } catch {
6948
8838
  mode = void 0;
6949
8839
  }
6950
8840
  const tmp = path + ".deskbuddy.tmp";
6951
- writeFileSync22(tmp, content, "utf8");
8841
+ writeFileSync21(tmp, content, "utf8");
6952
8842
  if (mode !== void 0) chmodSync9(tmp, mode);
6953
- renameSync17(tmp, path);
8843
+ renameSync16(tmp, path);
6954
8844
  }
6955
8845
  function assertSafeToAppend(raw, path) {
6956
8846
  for (const quote of ['"""', "'''"]) {
@@ -7005,7 +8895,9 @@ var CC_CONFIG_PATHS = {
7005
8895
  claude_code: claudeSettingsPath,
7006
8896
  workbuddy: workbuddySettingsPath,
7007
8897
  qoder_cn: qoderCnSettingsPath,
7008
- qoder: qoderSettingsPath
8898
+ qoder: qoderSettingsPath,
8899
+ // 千问办公只装状态上报:不装 gate.cjs、不装 rewake(选择题 / 授权由 daemon 的 qwenworkBridge 经 App 内置 MCP 回填)
8900
+ qwenwork: qwenworkSettingsPath
7009
8901
  };
7010
8902
  var WINDOW_GATE_TOOLS = /* @__PURE__ */ new Set(["claude_code", "qoder_cn", "qoder"]);
7011
8903
  var ADAPTERS = {
@@ -7053,6 +8945,26 @@ var ADAPTERS = {
7053
8945
  installWindowGate: () => true
7054
8946
  }
7055
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
+ };
7056
8968
  function adapterFor(registry, tool) {
7057
8969
  const format = registry?.tools?.[tool]?.format;
7058
8970
  return format && ADAPTERS[format] || null;
@@ -7073,13 +8985,13 @@ function subscribedEvents(registry, tool, fallback) {
7073
8985
  }
7074
8986
 
7075
8987
  // src/commands/enable.ts
7076
- import { dirname as dirname13, join as join30 } from "path";
8988
+ import { dirname as dirname15, join as join32 } from "path";
7077
8989
 
7078
8990
  // src/wrap/shim.ts
7079
- import { execFileSync as execFileSync12 } from "child_process";
7080
- import { chmodSync as chmodSync10, existsSync as existsSync17, mkdirSync as mkdirSync24, readdirSync as readdirSync6, readFileSync as readFileSync22, rmdirSync, rmSync as rmSync17, writeFileSync as writeFileSync23 } from "fs";
7081
- import { homedir as homedir6 } from "os";
7082
- import { delimiter as delimiter2, join as join28, 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";
7083
8995
  var SHIM_FALLBACK_EXIT = 213;
7084
8996
  var WIN_SHIM_ENABLED = false;
7085
8997
  var SHIM_COMMANDS = { claude_code: "claude", codex: "codex" };
@@ -7089,10 +9001,10 @@ var RC_END = "# <<< deskbuddy <<<";
7089
9001
  var RC_FILES = [".zshrc", ".bashrc"];
7090
9002
  var WIN_PENDING = "Windows \u7A97\u53E3\u63A5\u7BA1\u5F85\u9A8C\u8BC1\uFF0C\u6682\u672A\u5F00\u542F";
7091
9003
  function shimDir() {
7092
- return join28(deskbuddyHome(), "bin");
9004
+ return join30(deskbuddyHome(), "bin");
7093
9005
  }
7094
9006
  function shimFile(binDir, command, platform = process.platform) {
7095
- return join28(binDir, platform === "win32" ? `${command}.cmd` : command);
9007
+ return join30(binDir, platform === "win32" ? `${command}.cmd` : command);
7096
9008
  }
7097
9009
  function shQuote3(s) {
7098
9010
  return `'${s.replace(/'/g, `'\\''`)}'`;
@@ -7180,7 +9092,7 @@ function cmdScript(tool, command, node, cli, bin) {
7180
9092
  ].join("\r\n");
7181
9093
  }
7182
9094
  function rcBlock(binDir, rcHome) {
7183
- const std = resolve3(rcHome, ".deskbuddy", "bin") === resolve3(binDir);
9095
+ const std = resolve4(rcHome, ".deskbuddy", "bin") === resolve4(binDir);
7184
9096
  const dir = std ? "$HOME/.deskbuddy/bin" : binDir.replace(/(["\\$`])/g, "\\$1");
7185
9097
  return [
7186
9098
  RC_BEGIN,
@@ -7224,7 +9136,7 @@ function readRc(p) {
7224
9136
  return readFileSync22(p).toString("latin1");
7225
9137
  }
7226
9138
  function writeRc(p, latin1) {
7227
- writeFileSync23(p, Buffer.from(latin1, "latin1"));
9139
+ writeFileSync22(p, Buffer.from(latin1, "latin1"));
7228
9140
  }
7229
9141
  function toLatin1(utf8) {
7230
9142
  return Buffer.from(utf8, "utf8").toString("latin1");
@@ -7246,7 +9158,7 @@ function removePathEntry(value, dir) {
7246
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)))";
7247
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')";
7248
9160
  function psWinPath() {
7249
- 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], {
7250
9162
  encoding: "utf8",
7251
9163
  stdio: ["ignore", "pipe", "ignore"],
7252
9164
  windowsHide: true,
@@ -7269,7 +9181,7 @@ function psWinPath() {
7269
9181
  function defaultShimEnv(nodePath, cliPath) {
7270
9182
  const env = process.env;
7271
9183
  const isolated = !!env.VITEST || !!env.DESKBUDDY_HOME;
7272
- const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir6());
9184
+ const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir8());
7273
9185
  return {
7274
9186
  platform: process.platform,
7275
9187
  binDir: shimDir(),
@@ -7293,11 +9205,11 @@ function installShim(tool, e) {
7293
9205
  if (!node || !cli || !bin) {
7294
9206
  return { ok: false, message: "\u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\uFF1A\u5B89\u88C5\u8DEF\u5F84\u91CC\u6709\u975E ASCII \u5B57\u7B26\uFF0CWindows \u7684 .cmd \u8BFB\u4E0D\u4E86\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09" };
7295
9207
  }
7296
- mkdirSync24(e.binDir, { recursive: true });
7297
- writeFileSync23(file, cmdScript(tool, command, node, cli, bin), "utf8");
9208
+ mkdirSync23(e.binDir, { recursive: true });
9209
+ writeFileSync22(file, cmdScript(tool, command, node, cli, bin), "utf8");
7298
9210
  } else {
7299
- mkdirSync24(e.binDir, { recursive: true, mode: 448 });
7300
- writeFileSync23(file, shScript(tool, command, e.nodePath, e.cliPath, e.binDir), { encoding: "utf8", mode: 493 });
9211
+ mkdirSync23(e.binDir, { recursive: true, mode: 448 });
9212
+ writeFileSync22(file, shScript(tool, command, e.nodePath, e.cliPath, e.binDir), { encoding: "utf8", mode: 493 });
7301
9213
  chmodSync10(file, 493);
7302
9214
  }
7303
9215
  const touched = [];
@@ -7312,7 +9224,7 @@ function installShim(tool, e) {
7312
9224
  } else if (e.rcHome) {
7313
9225
  const block = toLatin1(rcBlock(e.binDir, e.rcHome));
7314
9226
  for (const name of RC_FILES) {
7315
- const rc = join28(e.rcHome, name);
9227
+ const rc = join30(e.rcHome, name);
7316
9228
  if (!existsSync17(rc)) continue;
7317
9229
  const next = addRcBlock(readRc(rc), block);
7318
9230
  if (next !== null) writeRc(rc, next);
@@ -7333,13 +9245,13 @@ function installShim(tool, e) {
7333
9245
  function ourShims(binDir) {
7334
9246
  let names;
7335
9247
  try {
7336
- names = readdirSync6(binDir);
9248
+ names = readdirSync7(binDir);
7337
9249
  } catch {
7338
9250
  return [];
7339
9251
  }
7340
9252
  return names.filter((n) => {
7341
9253
  try {
7342
- return readFileSync22(join28(binDir, n), "utf8").includes(SHIM_MARKER);
9254
+ return readFileSync22(join30(binDir, n), "utf8").includes(SHIM_MARKER);
7343
9255
  } catch {
7344
9256
  return false;
7345
9257
  }
@@ -7370,7 +9282,7 @@ function uninstallShim(tool, e) {
7370
9282
  }
7371
9283
  } else if (e.rcHome) {
7372
9284
  for (const name of RC_FILES) {
7373
- const rc = join28(e.rcHome, name);
9285
+ const rc = join30(e.rcHome, name);
7374
9286
  if (!existsSync17(rc)) continue;
7375
9287
  const next = removeRcBlock(readRc(rc));
7376
9288
  if (next !== null) {
@@ -7380,7 +9292,7 @@ function uninstallShim(tool, e) {
7380
9292
  }
7381
9293
  }
7382
9294
  try {
7383
- 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);
7384
9296
  } catch {
7385
9297
  }
7386
9298
  if (!removed && touched.length === 0) return { ok: true, message: "" };
@@ -7403,12 +9315,12 @@ function shimState(tool, binDir, pathEnv, platform = process.platform) {
7403
9315
  }
7404
9316
 
7405
9317
  // src/commands/migrateQoder.ts
7406
- import { dirname as dirname12, join as join29 } from "path";
9318
+ import { dirname as dirname14, join as join31 } from "path";
7407
9319
 
7408
9320
  // src/core/detect.ts
7409
9321
  import { existsSync as existsSync18 } from "fs";
7410
- import { dirname as dirname11 } from "path";
7411
- 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"];
7412
9324
  function toolConfigDir(tool) {
7413
9325
  switch (tool) {
7414
9326
  case "claude_code":
@@ -7416,17 +9328,23 @@ function toolConfigDir(tool) {
7416
9328
  case "codex":
7417
9329
  return codexHome();
7418
9330
  case "workbuddy":
7419
- return dirname11(workbuddySettingsPath());
9331
+ return dirname13(workbuddySettingsPath());
7420
9332
  case "qoder_cn":
7421
- return dirname11(qoderCnSettingsPath());
9333
+ return dirname13(qoderCnSettingsPath());
7422
9334
  case "qoder":
7423
- return dirname11(qoderSettingsPath());
9335
+ return dirname13(qoderSettingsPath());
7424
9336
  case "opencode":
7425
- return dirname11(dirname11(opencodePluginPath()));
9337
+ return dirname13(dirname13(opencodePluginPath()));
9338
+ case "qwenwork":
9339
+ return qwenworkHome();
7426
9340
  default:
7427
9341
  return null;
7428
9342
  }
7429
9343
  }
9344
+ function toolDetectDir(tool) {
9345
+ if (tool === "dumate") return qianfanHome();
9346
+ return toolConfigDir(tool);
9347
+ }
7430
9348
  var defaultDetectDeps = () => ({
7431
9349
  located: (tool) => locate(tool) != null,
7432
9350
  exists: existsSync18,
@@ -7441,7 +9359,7 @@ function detectTools(d = defaultDetectDeps()) {
7441
9359
  return ALL_TOOLS.filter((tool) => {
7442
9360
  if (tool === caller) return true;
7443
9361
  if (d.located(tool)) return true;
7444
- const dir = toolConfigDir(tool);
9362
+ const dir = toolDetectDir(tool);
7445
9363
  return dir != null && d.exists(dir);
7446
9364
  });
7447
9365
  }
@@ -7451,7 +9369,7 @@ function migrateLegacyQoder(out, detect = () => detectTools()) {
7451
9369
  try {
7452
9370
  const cfg = loadConfig();
7453
9371
  const hookPath = installedHookPath();
7454
- const scripts = [hookPath, join29(dirname12(hookPath), "gate.cjs")];
9372
+ const scripts = [hookPath, join31(dirname14(hookPath), "gate.cjs")];
7455
9373
  if (!retagCcHook(qoderCnSettingsPath(), scripts, "qoder", "qoder_cn")) return false;
7456
9374
  const hadQoder = cfg.enabledTools.includes("qoder");
7457
9375
  let intl = false;
@@ -7502,7 +9420,8 @@ function installShims(tools, out, shim) {
7502
9420
  const r = installShim(tool, shim);
7503
9421
  if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
7504
9422
  if (r.ok && tool === "claude_code" && (!loadPty() || !loadHeadless())) {
7505
- 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`);
7506
9425
  }
7507
9426
  } catch (e) {
7508
9427
  out(`\u26A0\uFE0F \u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\u4E0A\uFF1A${e.message}\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09`);
@@ -7543,14 +9462,14 @@ function installTools(tools, out, shim = defaultShimEnvForInstall()) {
7543
9462
  installed.push(tool);
7544
9463
  out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
7545
9464
  try {
7546
- if (adapter.installWindowGate?.(configPath2, join30(dirname13(hookPath), "gate.cjs"), tool)) {
9465
+ if (adapter.installWindowGate?.(configPath2, join32(dirname15(hookPath), "gate.cjs"), tool)) {
7547
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`);
7548
9467
  }
7549
9468
  } catch (e) {
7550
9469
  out(`\u26A0\uFE0F ${tool}\uFF1A\u8BBE\u5907\u786E\u8BA4\u6388\u6743\u6CA1\u88C5\u4E0A\uFF1A${e.message}`);
7551
9470
  }
7552
9471
  try {
7553
- if (adapter.installRewake?.(configPath2, join30(dirname13(hookPath), "wake.cjs"), tool)) {
9472
+ if (adapter.installRewake?.(configPath2, join32(dirname15(hookPath), "wake.cjs"), tool)) {
7554
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`);
7555
9474
  }
7556
9475
  } catch (e) {
@@ -7651,28 +9570,28 @@ async function runDisable(tools, opts = {}) {
7651
9570
  }
7652
9571
 
7653
9572
  // src/commands/install.ts
7654
- import { mkdirSync as mkdirSync25, rmSync as rmSync18, writeFileSync as writeFileSync24 } from "fs";
7655
- import { join as join31 } from "path";
9573
+ import { mkdirSync as mkdirSync24, rmSync as rmSync18, writeFileSync as writeFileSync23 } from "fs";
9574
+ import { join as join33 } from "path";
7656
9575
 
7657
9576
  // src/commands/setup.ts
7658
9577
  import qrcode from "qrcode-terminal";
7659
9578
 
7660
9579
  // src/commands/remoteCwd.ts
7661
- import { existsSync as existsSync19, statSync as statSync9 } from "fs";
9580
+ import { existsSync as existsSync19, statSync as statSync11 } from "fs";
7662
9581
  import { createInterface } from "readline";
7663
- import { homedir as homedir7 } from "os";
7664
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
7665
- function normalizePath(raw, home = homedir7()) {
9582
+ import { homedir as homedir9 } from "os";
9583
+ import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
9584
+ function normalizePath(raw, home = homedir9()) {
7666
9585
  const s = (raw || "").trim();
7667
9586
  if (!s) return null;
7668
- const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ? resolve4(home, s.slice(2)) : s;
7669
- 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);
7670
9589
  return abs;
7671
9590
  }
7672
9591
  function checkPath(abs) {
7673
9592
  if (!existsSync19(abs)) return "missing";
7674
9593
  try {
7675
- return statSync9(abs).isDirectory() ? "ok" : "not_a_dir";
9594
+ return statSync11(abs).isDirectory() ? "ok" : "not_a_dir";
7676
9595
  } catch {
7677
9596
  return "missing";
7678
9597
  }
@@ -7781,7 +9700,7 @@ function parseApiBase(argv) {
7781
9700
  }
7782
9701
  return value;
7783
9702
  }
7784
- function sleep(ms) {
9703
+ function sleep2(ms) {
7785
9704
  return new Promise((r) => setTimeout(r, ms));
7786
9705
  }
7787
9706
  function stdout3(line) {
@@ -7804,7 +9723,7 @@ async function runSetup(opts = {}) {
7804
9723
  await pollForBinding(
7805
9724
  authCode,
7806
9725
  { intervalSec: interval },
7807
- { authToken: (code) => api.authToken(code), sleep, now: () => Date.now() }
9726
+ { authToken: (code) => api.authToken(code), sleep: sleep2, now: () => Date.now() }
7808
9727
  );
7809
9728
  markBound();
7810
9729
  await refreshRegistry();
@@ -7842,7 +9761,9 @@ var TOOL_LABELS = {
7842
9761
  workbuddy: "WorkBuddy",
7843
9762
  qoder_cn: "Qoder CN",
7844
9763
  qoder: "Qoder",
7845
- opencode: "opencode"
9764
+ opencode: "opencode",
9765
+ qwenwork: "\u5343\u95EE\u529E\u516C",
9766
+ dumate: "\u767E\u5EA6\u642D\u5B50"
7846
9767
  };
7847
9768
  function checkApiBase(raw, env = process.env) {
7848
9769
  const norm3 = raw.replace(/\/+$/, "");
@@ -7877,10 +9798,10 @@ function inSandbox(env) {
7877
9798
  }
7878
9799
  function precheckWritable(dirs) {
7879
9800
  for (const dir of dirs) {
7880
- const probe = join31(dir, `.deskbuddy-probe-${process.pid}`);
9801
+ const probe = join33(dir, `.deskbuddy-probe-${process.pid}`);
7881
9802
  try {
7882
- mkdirSync25(dir, { recursive: true });
7883
- writeFileSync24(probe, "");
9803
+ mkdirSync24(dir, { recursive: true });
9804
+ writeFileSync23(probe, "");
7884
9805
  rmSync18(probe, { force: true });
7885
9806
  } catch (e) {
7886
9807
  return { ok: false, dir, error: e.message };
@@ -8015,11 +9936,11 @@ function stdout4(line) {
8015
9936
  process.stdout.write(line + "\n");
8016
9937
  }
8017
9938
  function askYesNo(question) {
8018
- return new Promise((resolve6) => {
9939
+ return new Promise((resolve7) => {
8019
9940
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
8020
9941
  rl.question(question, (answer) => {
8021
9942
  rl.close();
8022
- resolve6(/^(y|yes)$/i.test(answer.trim()));
9943
+ resolve7(/^(y|yes)$/i.test(answer.trim()));
8023
9944
  });
8024
9945
  });
8025
9946
  }
@@ -8104,7 +10025,7 @@ function formatStatus(input) {
8104
10025
  lines2.push(` ${pad("hook \u7248\u672C", 10)}${versionNote}`);
8105
10026
  if (input.windowWrap) {
8106
10027
  const w = input.windowWrap;
8107
- 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";
8108
10029
  lines2.push(` ${pad("\u7A97\u53E3\u63A5\u7BA1", 10)}${text}`);
8109
10030
  }
8110
10031
  if (input.codexWrap) {
@@ -8149,7 +10070,7 @@ function collectStatus(now = Date.now()) {
8149
10070
  installed,
8150
10071
  notice: installed ? adapter.notice : void 0
8151
10072
  });
8152
- sessions.push(...readAliveStates(tool));
10073
+ sessions.push(...readAliveStates(tool, { now }));
8153
10074
  }
8154
10075
  const wrapTool = config.enabledTools.find((t) => t === "claude_code" && SHIM_COMMANDS[t]);
8155
10076
  let windowWrap;
@@ -8161,7 +10082,15 @@ function collectStatus(now = Date.now()) {
8161
10082
  if (wrapTool) {
8162
10083
  const binDir = shimDir();
8163
10084
  const st = shimState(wrapTool, binDir, process.env.PATH ?? "");
8164
- 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
+ };
8165
10094
  }
8166
10095
  return {
8167
10096
  windowWrap,
@@ -8229,9 +10158,9 @@ async function runAutoSwitch(opts) {
8229
10158
  }
8230
10159
 
8231
10160
  // src/wrap/run.ts
8232
- import { randomBytes as randomBytes8 } from "crypto";
8233
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync27, statSync as statSync11, truncateSync as truncateSync3 } from "fs";
8234
- import { join as join33 } from "path";
10161
+ import { randomBytes as randomBytes9 } from "crypto";
10162
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync26, statSync as statSync13, truncateSync as truncateSync3 } from "fs";
10163
+ import { join as join36 } from "path";
8235
10164
  import { StringDecoder } from "string_decoder";
8236
10165
 
8237
10166
  // src/wrap/injector.ts
@@ -8479,10 +10408,10 @@ var Injector = class {
8479
10408
  };
8480
10409
 
8481
10410
  // src/wrap/passthrough.ts
8482
- import { spawn as spawn4 } from "child_process";
8483
- import { realpathSync as realpathSync3 } from "fs";
10411
+ import { spawn as spawn5 } from "child_process";
10412
+ import { realpathSync as realpathSync4 } from "fs";
8484
10413
  import { constants as constants2 } from "os";
8485
- import { dirname as dirname14, resolve as resolve5 } from "path";
10414
+ import { dirname as dirname16, resolve as resolve6 } from "path";
8486
10415
  var CLAUDE_SUBCOMMANDS = /* @__PURE__ */ new Set([
8487
10416
  "agents",
8488
10417
  "attach",
@@ -8533,12 +10462,12 @@ function passthroughReason(i) {
8533
10462
  return null;
8534
10463
  }
8535
10464
  function norm2(p, win) {
8536
- const r = resolve5(p).replace(/[\\/]+$/, "");
10465
+ const r = resolve6(p).replace(/[\\/]+$/, "");
8537
10466
  return win ? r.replace(/\//g, "\\").toLowerCase() : r;
8538
10467
  }
8539
10468
  function real(p) {
8540
10469
  try {
8541
- return realpathSync3(p);
10470
+ return realpathSync4(p);
8542
10471
  } catch {
8543
10472
  return p;
8544
10473
  }
@@ -8546,7 +10475,7 @@ function real(p) {
8546
10475
  function findReal(tool, d = defaultLocateDeps(), skip = shimDir()) {
8547
10476
  const win = (d.platform ?? process.platform) === "win32";
8548
10477
  const skipKeys = /* @__PURE__ */ new Set([norm2(skip, win), norm2(real(skip), win)]);
8549
- 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));
8550
10479
  const dd = { ...d, skipDirs: [...d.skipDirs ?? [], skip] };
8551
10480
  const name = SHIM_COMMANDS[tool];
8552
10481
  if (!name) {
@@ -8575,7 +10504,7 @@ function runDirect(real2, args, env = process.env) {
8575
10504
  return new Promise((done) => {
8576
10505
  let child;
8577
10506
  try {
8578
- child = spawn4(real2.file, [...real2.prefixArgs, ...args], {
10507
+ child = spawn5(real2.file, [...real2.prefixArgs, ...args], {
8579
10508
  stdio: "inherit",
8580
10509
  // 🔴 再经过垫片也直接放行,不会绕回来
8581
10510
  env: withoutShimFlag({ ...env, DESKBUDDY_WRAPPED: "1" }),
@@ -8625,21 +10554,21 @@ function runDirect(real2, args, env = process.env) {
8625
10554
  }
8626
10555
 
8627
10556
  // src/wrap/codexLaunch.ts
8628
- import { spawn as spawn5 } from "child_process";
8629
- import { closeSync as closeSync4, mkdirSync as mkdirSync26, openSync as openSync4, readdirSync as readdirSync7, readFileSync as readFileSync23, rmSync as rmSync20, statSync as statSync10, truncateSync as truncateSync2, writeFileSync as writeFileSync25, renameSync as renameSync18 } 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";
8630
10559
  import { request } from "http";
8631
10560
  import { createServer as createServer4 } from "net";
8632
- import { join as join32 } from "path";
10561
+ import { join as join34 } from "path";
8633
10562
 
8634
10563
  // src/core/codexServer.ts
8635
- import { execFileSync as execFileSync13 } from "child_process";
10564
+ import { execFileSync as execFileSync14 } from "child_process";
8636
10565
  function isAppServerArgs(args, url) {
8637
10566
  return /\bapp-server\b/.test(args) && new RegExp(`--listen[= ]${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$)`).test(args);
8638
10567
  }
8639
10568
  function processArgs(pid) {
8640
10569
  if (process.platform === "win32") return null;
8641
10570
  try {
8642
- const out = execFileSync13("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
10571
+ const out = execFileSync14("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
8643
10572
  encoding: "utf8",
8644
10573
  stdio: ["ignore", "pipe", "ignore"]
8645
10574
  });
@@ -8797,10 +10726,10 @@ async function runCodexWindow(real2, args, d) {
8797
10726
  }
8798
10727
  var MAX_LOG_BYTES2 = 1024 * 1024;
8799
10728
  function appServerLogPath() {
8800
- return join32(deskbuddyHome(), "codex-app-server.log");
10729
+ return join34(deskbuddyHome(), "codex-app-server.log");
8801
10730
  }
8802
10731
  function windowsDir2() {
8803
- return join32(deskbuddyHome(), "codex-windows");
10732
+ return join34(deskbuddyHome(), "codex-windows");
8804
10733
  }
8805
10734
  function pidAlive2(pid) {
8806
10735
  try {
@@ -8823,40 +10752,40 @@ function daemonAlive(pidFile = pidFilePath(), args = processArgs) {
8823
10752
  }
8824
10753
  function probeReady(url) {
8825
10754
  const port = Number(url.slice(url.lastIndexOf(":") + 1));
8826
- return new Promise((resolve6) => {
10755
+ return new Promise((resolve7) => {
8827
10756
  const req2 = request({ host: "127.0.0.1", port, path: "/readyz", method: "GET", timeout: 800 }, (res) => {
8828
10757
  res.resume();
8829
- resolve6((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
10758
+ resolve7((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
8830
10759
  });
8831
10760
  req2.on("timeout", () => req2.destroy());
8832
- req2.on("error", () => resolve6(false));
10761
+ req2.on("error", () => resolve7(false));
8833
10762
  req2.end();
8834
10763
  });
8835
10764
  }
8836
10765
  function freePort() {
8837
- return new Promise((resolve6, reject) => {
10766
+ return new Promise((resolve7, reject) => {
8838
10767
  const srv = createServer4();
8839
10768
  srv.unref();
8840
10769
  srv.on("error", reject);
8841
10770
  srv.listen(0, "127.0.0.1", () => {
8842
10771
  const addr = srv.address();
8843
10772
  const port = typeof addr === "object" && addr ? addr.port : 0;
8844
- srv.close(() => port ? resolve6(port) : reject(new Error("no port")));
10773
+ srv.close(() => port ? resolve7(port) : reject(new Error("no port")));
8845
10774
  });
8846
10775
  });
8847
10776
  }
8848
10777
  function startServer(real2, url, env, cwd) {
8849
10778
  const logPath2 = appServerLogPath();
8850
- mkdirSync26(deskbuddyHome(), { recursive: true });
10779
+ mkdirSync25(deskbuddyHome(), { recursive: true });
8851
10780
  try {
8852
- if (statSync10(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
10781
+ if (statSync12(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
8853
10782
  } catch {
8854
10783
  }
8855
- const fd = openSync4(logPath2, "a");
10784
+ const fd = openSync5(logPath2, "a");
8856
10785
  try {
8857
10786
  const childEnv2 = { ...env, DESKBUDDY_WRAPPED: "1" };
8858
10787
  delete childEnv2.DESKBUDDY_SHIM;
8859
- const child = spawn5(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
10788
+ const child = spawn6(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
8860
10789
  detached: true,
8861
10790
  stdio: ["ignore", fd, fd],
8862
10791
  cwd,
@@ -8867,7 +10796,7 @@ function startServer(real2, url, env, cwd) {
8867
10796
  child.unref();
8868
10797
  return child.pid ?? null;
8869
10798
  } finally {
8870
- closeSync4(fd);
10799
+ closeSync5(fd);
8871
10800
  }
8872
10801
  }
8873
10802
  function killServer(pid) {
@@ -8881,14 +10810,14 @@ function killOwnServer(pid, url) {
8881
10810
  if (a && isAppServerArgs(a, url)) killServer(pid);
8882
10811
  }
8883
10812
  function recordPath(rec) {
8884
- return join32(windowsDir2(), `${rec.server}.json`);
10813
+ return join34(windowsDir2(), `${rec.server}.json`);
8885
10814
  }
8886
10815
  function writeRecord(rec) {
8887
10816
  try {
8888
- mkdirSync26(windowsDir2(), { recursive: true, mode: 448 });
10817
+ mkdirSync25(windowsDir2(), { recursive: true, mode: 448 });
8889
10818
  const p = recordPath(rec);
8890
- writeFileSync25(p + ".tmp", JSON.stringify(rec), { encoding: "utf8", mode: 384 });
8891
- renameSync18(p + ".tmp", p);
10819
+ writeFileSync24(p + ".tmp", JSON.stringify(rec), { encoding: "utf8", mode: 384 });
10820
+ renameSync17(p + ".tmp", p);
8892
10821
  } catch {
8893
10822
  }
8894
10823
  }
@@ -8901,12 +10830,12 @@ function removeRecord(rec) {
8901
10830
  function reapWindows(dir = windowsDir2(), alive = pidAlive2, args = processArgs, kill = killServer) {
8902
10831
  let names = [];
8903
10832
  try {
8904
- names = readdirSync7(dir).filter((n) => n.endsWith(".json"));
10833
+ names = readdirSync8(dir).filter((n) => n.endsWith(".json"));
8905
10834
  } catch {
8906
10835
  return;
8907
10836
  }
8908
10837
  for (const n of names) {
8909
- const p = join32(dir, n);
10838
+ const p = join34(dir, n);
8910
10839
  let rec;
8911
10840
  try {
8912
10841
  rec = JSON.parse(readFileSync23(p, "utf8"));
@@ -8928,8 +10857,8 @@ function reapWindows(dir = windowsDir2(), alive = pidAlive2, args = processArgs,
8928
10857
  }
8929
10858
  function launchLog(line) {
8930
10859
  try {
8931
- mkdirSync26(deskbuddyHome(), { recursive: true });
8932
- writeFileSync25(join32(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
10860
+ mkdirSync25(deskbuddyHome(), { recursive: true });
10861
+ writeFileSync24(join34(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
8933
10862
  `, { flag: "a" });
8934
10863
  } catch {
8935
10864
  }
@@ -8951,6 +10880,129 @@ function defaultCodexWindowDeps(base) {
8951
10880
  };
8952
10881
  }
8953
10882
 
10883
+ // src/wrap/promptLaunch.ts
10884
+ import { randomBytes as randomBytes8 } from "crypto";
10885
+ import { homedir as homedir10 } from "os";
10886
+ import { join as join35 } from "path";
10887
+ var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
10888
+ function promptArgs(text) {
10889
+ return text ? ["--", prepare(text)] : [];
10890
+ }
10891
+ function promptEnv(tool, env) {
10892
+ const out = { ...env };
10893
+ if (tool === "workbuddy" && !out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join35(homedir10(), ".workbuddy");
10894
+ return out;
10895
+ }
10896
+ async function runPromptLaunch(tool, windowId, cwd, d) {
10897
+ let real2 = null;
10898
+ try {
10899
+ real2 = d.findReal(tool);
10900
+ } catch {
10901
+ real2 = null;
10902
+ }
10903
+ if (!real2) {
10904
+ d.stderr.write(`deskbuddy: \u627E\u4E0D\u5230 ${tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6
10905
+ `);
10906
+ return 127;
10907
+ }
10908
+ let first = null;
10909
+ try {
10910
+ first = await d.receive(windowId, tool, cwd);
10911
+ } catch (e) {
10912
+ d.log(`\u7B49\u9996\u53E5\u51FA\u9519\uFF1A${e.message}`);
10913
+ first = null;
10914
+ }
10915
+ let text = null;
10916
+ if (first) {
10917
+ const { req: req2 } = first;
10918
+ if (req2.expireAt > 0 && d.now() > req2.expireAt) {
10919
+ first.reply({ instructionId: req2.instructionId, status: "rejected", reason: "expired" });
10920
+ } else {
10921
+ text = req2.text;
10922
+ first.reply({ instructionId: req2.instructionId, status: "started", mode: "window" });
10923
+ }
10924
+ } else {
10925
+ d.log(`[${windowId.slice(0, 8)}] \u6CA1\u7B49\u5230\u9996\u53E5\uFF0C\u8D77\u4E00\u4E2A\u7A7A\u7684 ${tool}`);
10926
+ }
10927
+ return d.run(tool, real2, promptArgs(text), promptEnv(tool, d.env));
10928
+ }
10929
+ function receiveFirstLine(windowId, tool, cwd, opts = {}) {
10930
+ const platform = opts.platform ?? process.platform;
10931
+ const sock = channelPath(windowId, platform);
10932
+ const secret = randomBytes8(32);
10933
+ const cleanup = (server) => {
10934
+ closeChannelServer(server, sock, platform);
10935
+ try {
10936
+ removeWindow(windowId);
10937
+ } catch {
10938
+ }
10939
+ };
10940
+ try {
10941
+ writeWindow(windowId, { pid: process.pid, sock, tool, cwd, at: Date.now(), secret: secret.toString("hex"), launched: true });
10942
+ } catch (e) {
10943
+ opts.log?.(`\u767B\u8BB0\u7A97\u53E3\u5931\u8D25\uFF1A${e.message}`);
10944
+ return Promise.resolve(null);
10945
+ }
10946
+ if (platform !== "win32" && !ensureChannelDir(sock)) {
10947
+ cleanup(null);
10948
+ return Promise.resolve(null);
10949
+ }
10950
+ return new Promise((resolve7) => {
10951
+ let server = null;
10952
+ let done = false;
10953
+ let firstId = "";
10954
+ let final = null;
10955
+ const replies = [];
10956
+ const finish = (v) => {
10957
+ if (done) return;
10958
+ done = true;
10959
+ clearTimeout(timer);
10960
+ if (!v) cleanup(server);
10961
+ resolve7(v);
10962
+ };
10963
+ const timer = setTimeout(() => finish(null), opts.waitMs ?? FIRST_LINE_WAIT_MS2);
10964
+ void startChannelServer(
10965
+ sock,
10966
+ secret,
10967
+ (req2, reply) => {
10968
+ if (firstId && req2.instructionId === firstId) {
10969
+ reply({ instructionId: req2.instructionId, status: "ack" });
10970
+ if (final) reply(final);
10971
+ else replies.push(reply);
10972
+ return;
10973
+ }
10974
+ if (done) {
10975
+ reply({ instructionId: req2.instructionId, status: "not_here" });
10976
+ return;
10977
+ }
10978
+ firstId = req2.instructionId;
10979
+ replies.push(reply);
10980
+ reply({ instructionId: req2.instructionId, status: "ack" });
10981
+ finish({
10982
+ req: req2,
10983
+ reply: (r) => {
10984
+ if (final) return;
10985
+ final = r;
10986
+ for (const f of replies.splice(0)) {
10987
+ try {
10988
+ f(r);
10989
+ } catch {
10990
+ }
10991
+ }
10992
+ setTimeout(() => cleanup(server), opts.lingerMs ?? FINAL_LINGER_MS);
10993
+ }
10994
+ });
10995
+ },
10996
+ platform
10997
+ ).then((s) => {
10998
+ server = s;
10999
+ if (!s) finish(null);
11000
+ else if (done && !firstId) cleanup(s);
11001
+ });
11002
+ });
11003
+ }
11004
+ var FINAL_LINGER_MS = 5e3;
11005
+
8954
11006
  // src/wrap/run.ts
8955
11007
  var DEFAULT_TTL_MS = 10 * 60 * 1e3;
8956
11008
  var TICK_MS = 300;
@@ -8969,10 +11021,10 @@ function childEnv(src, windowId) {
8969
11021
  }
8970
11022
  function wrapLog(line) {
8971
11023
  try {
8972
- const p = join33(deskbuddyHome(), "wrap.log");
8973
- mkdirSync27(deskbuddyHome(), { recursive: true });
11024
+ const p = join36(deskbuddyHome(), "wrap.log");
11025
+ mkdirSync26(deskbuddyHome(), { recursive: true });
8974
11026
  try {
8975
- if (statSync11(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
11027
+ if (statSync13(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
8976
11028
  } catch {
8977
11029
  }
8978
11030
  appendFileSync2(p, `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${line}
@@ -9057,6 +11109,34 @@ var defaultLaunchDeps = () => ({
9057
11109
  isDir: isDirSync,
9058
11110
  chdir: (p) => process.chdir(p)
9059
11111
  });
11112
+ function defaultPromptDeps(deps, cwd) {
11113
+ return {
11114
+ platform: deps.platform,
11115
+ now: () => Date.now(),
11116
+ stderr: deps.stderr,
11117
+ findReal: deps.findReal,
11118
+ receive: (id, tool, c) => receiveFirstLine(id, tool, c, { platform: deps.platform, log: deps.log }),
11119
+ run: (tool, real2, args, env) => {
11120
+ if (tool === "codex") {
11121
+ return runCodexWindow(
11122
+ real2,
11123
+ args,
11124
+ defaultCodexWindowDeps({
11125
+ env,
11126
+ platform: deps.platform,
11127
+ stdinTTY: !!deps.stdin.isTTY,
11128
+ stdoutTTY: !!deps.stdout.isTTY,
11129
+ cwd: () => cwd,
11130
+ runDirect: (r, a) => runDirect(r, a, env)
11131
+ })
11132
+ );
11133
+ }
11134
+ return runDirect(real2, args, env);
11135
+ },
11136
+ env: deps.env,
11137
+ log: deps.log
11138
+ };
11139
+ }
9060
11140
  async function runLaunch(argv, deps = defaultWrapDeps(), ld = defaultLaunchDeps()) {
9061
11141
  const runId = argv[0] ?? "";
9062
11142
  const hi = argv.indexOf("--home");
@@ -9083,6 +11163,7 @@ async function runLaunch(argv, deps = defaultWrapDeps(), ld = defaultLaunchDeps(
9083
11163
  return 1;
9084
11164
  }
9085
11165
  ld.chdir(plan.cwd);
11166
+ if (plan.tool !== "claude_code") return runPromptLaunch(plan.tool, runId, plan.cwd, ld.prompt ?? defaultPromptDeps(deps, plan.cwd));
9086
11167
  const args = plan.resume ? ["--resume", plan.resume] : [];
9087
11168
  let real2 = null;
9088
11169
  try {
@@ -9112,7 +11193,7 @@ async function wrapInPty(tool, real2, args, deps, opts = {}) {
9112
11193
  return null;
9113
11194
  }
9114
11195
  const { stdout: stdout7 } = deps;
9115
- const windowId = opts.windowId ?? randomBytes8(16).toString("hex");
11196
+ const windowId = opts.windowId ?? randomBytes9(16).toString("hex");
9116
11197
  const cols = stdout7.columns || 80;
9117
11198
  const rows = stdout7.rows || 24;
9118
11199
  const term = new Headless({ cols, rows, allowProposedApi: true });
@@ -9219,7 +11300,7 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
9219
11300
  } catch (e) {
9220
11301
  deps.log(`\u63A5\u7BA1\u7EC8\u7AEF\u51FA\u9519\uFF1A${e.message}`);
9221
11302
  }
9222
- const secret = randomBytes8(32);
11303
+ const secret = randomBytes9(32);
9223
11304
  try {
9224
11305
  writeWindow(windowId, {
9225
11306
  pid: process.pid,
@@ -9260,7 +11341,7 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
9260
11341
  injector.tick().catch(() => {
9261
11342
  });
9262
11343
  }, TICK_MS);
9263
- return new Promise((resolve6) => {
11344
+ return new Promise((resolve7) => {
9264
11345
  let degraded = false;
9265
11346
  const onFatal = (e) => {
9266
11347
  deps.log(`\u5305\u88C5\u5C42\u5F02\u5E38\uFF1A${e?.stack ?? String(e)}`);
@@ -9311,8 +11392,8 @@ ${WRAP_HINT}\uFF08\u8BBE\u5907\u56DE\u590D\u6682\u65F6\u5199\u4E0D\u8FDB\u8FD9\u
9311
11392
  restoreTerminal();
9312
11393
  process.removeListener("uncaughtException", onFatal);
9313
11394
  for (const [s, h] of sigHandlers) process.removeListener(s, h);
9314
- if (pending > 0) setTimeout(() => resolve6(code), 150);
9315
- else resolve6(code);
11395
+ if (pending > 0) setTimeout(() => resolve7(code), 150);
11396
+ else resolve7(code);
9316
11397
  }
9317
11398
  process.on("uncaughtException", onFatal);
9318
11399
  for (const [s, h] of sigHandlers) {