deskbuddy 0.2.0-beta.39 → 0.2.0-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,10 +5,12 @@ import {
5
5
  APP_IN_USE_REASON,
6
6
  APP_NOT_RUNNING,
7
7
  APP_PORT_HUNG_REASON,
8
+ APP_PORT_MISSING,
8
9
  APP_SECRET,
9
10
  APP_WAIT_MARGIN_MS,
10
11
  ApiError,
11
12
  CODEBUDDY_TEXT,
13
+ CODEX_PERMISSIONS_TOOL,
12
14
  CODE_NOT_SUPPORT,
13
15
  COZE_BRAND,
14
16
  COZE_DEFAULT_CDP_PORT,
@@ -35,8 +37,12 @@ import {
35
37
  QODER_APP_POLL_MS,
36
38
  QODER_BRANDS,
37
39
  QODER_SHORTCUT_EVERY_MS,
40
+ QW_DEFAULT_CDP_PORT,
41
+ QW_SHORTCUT_EVERY_MS,
38
42
  QoderAppTasks,
39
43
  QoderCdpKeeper,
44
+ QwShortcutRelaunchFixer,
45
+ QwenworkKeeper,
40
46
  RUN_ID_RE,
41
47
  SESSION_UUID_RE,
42
48
  SHIM_COMMANDS,
@@ -85,6 +91,7 @@ import {
85
91
  codexFileOptions,
86
92
  codexHome,
87
93
  codexMcpOptions,
94
+ codexPermissionsCard,
88
95
  compareVersions,
89
96
  connectCdp,
90
97
  daemonCheckDeps,
@@ -109,6 +116,7 @@ import {
109
116
  ensureClientId,
110
117
  ensureDumateShortcuts,
111
118
  ensureQoderShortcuts,
119
+ ensureQwenworkShortcuts,
112
120
  envOverride,
113
121
  environmentLabel,
114
122
  failureHead,
@@ -194,6 +202,7 @@ import {
194
202
  qwenworkDataHome,
195
203
  qwenworkHome,
196
204
  qwenworkOptions,
205
+ qwenworkShortcutsWanted,
197
206
  readAliveStates,
198
207
  readEnded,
199
208
  readFailed,
@@ -262,7 +271,7 @@ import {
262
271
  writeState,
263
272
  writeWakeMark,
264
273
  writeWakeSecret
265
- } from "./chunk-37YGRQ4H.js";
274
+ } from "./chunk-YZGPNQYG.js";
266
275
 
267
276
  // src/commands/config.ts
268
277
  function runConfig(args, out = (s2) => process.stdout.write(s2 + "\n")) {
@@ -795,7 +804,7 @@ var AutoEnabler = class {
795
804
 
796
805
  // src/daemon/index.ts
797
806
  import { randomBytes as randomBytes7 } from "crypto";
798
- import { existsSync as existsSync7, readdirSync as readdirSync11, readFileSync as readFileSync14, statSync as statSync13 } from "fs";
807
+ import { existsSync as existsSync7, readdirSync as readdirSync12, readFileSync as readFileSync14, statSync as statSync13 } from "fs";
799
808
  import { homedir as homedir8 } from "os";
800
809
  import { basename as basename9, dirname as dirname9, join as join28 } from "path";
801
810
  import WebSocket2 from "ws";
@@ -2425,7 +2434,8 @@ function describe(toolName, toolInput, cwd) {
2425
2434
  if (v.action === "ask") return { kind: v.kind, summary: v.summary };
2426
2435
  const input = toolInput && typeof toolInput === "object" ? toolInput : {};
2427
2436
  const detail = str(input.command) || str(input.file_path) || str(input.path) || str(input.pattern) || str(input.url);
2428
- return { kind: SHELL_TOOLS.has(toolName) ? "command" : "other", summary: truncate(detail ? `${toolName} ${detail}` : toolName) };
2437
+ if (SHELL_TOOLS.has(toolName)) return { kind: "command", summary: truncate(detail || toolName) };
2438
+ return { kind: "other", summary: truncate(detail ? `${toolName} ${detail}` : toolName) };
2429
2439
  }
2430
2440
  function editInProject(toolName, toolInput, cwd, opts) {
2431
2441
  if (!opts.real || !EDIT_TOOLS.has(toolName) && !PATCH_TOOLS.has(toolName)) return false;
@@ -3707,7 +3717,8 @@ var ApprovalCoordinator = class {
3707
3717
  const trust = toolName === TRUST_TOOL_NAME;
3708
3718
  if (trust !== sessionId.startsWith(TRUST_SESSION_PREFIX)) return none;
3709
3719
  const { kind, summary: summary2 } = qv ? { kind: "question", summary: qv.summary } : trust ? { kind: "other", summary: trustCardSummary(cwd, hook.tool_input) } : describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
3710
- const ownSummary = !viaGate && (tool === "workbuddy" || tool === "coze" || isQwAppCard(tool, hook, viaGate) || isQoderAppCard(tool, hook, viaGate)) && !qv && typeof hook.deskbuddy_summary === "string" ? truncate(hook.deskbuddy_summary) : "";
3720
+ const codexPerm = !viaGate && tool === "codex" && toolName === CODEX_PERMISSIONS_TOOL;
3721
+ const ownSummary = !viaGate && (tool === "workbuddy" || tool === "coze" || codexPerm || isQwAppCard(tool, hook, viaGate) || isQoderAppCard(tool, hook, viaGate)) && !qv && typeof hook.deskbuddy_summary === "string" ? truncate(hook.deskbuddy_summary) : "";
3711
3722
  const traeSummary = tool === "trae" ? truncate(traeNoticeSummary(toolName, hook.tool_input, summary2, hook.trae_notice)) : "";
3712
3723
  const cardSummary = ownSummary || traeSummary || summary2;
3713
3724
  const inProject = kind === "edit" && !qv && !trust && !!cwd && editInProject(toolName, hook.tool_input, cwdForms(cwd), { real: realpathNearest });
@@ -5711,7 +5722,7 @@ function sleep(ms) {
5711
5722
 
5712
5723
  // src/daemon/qwenworkApp/watch.ts
5713
5724
  import { homedir as homedir4 } from "os";
5714
- import { join as join15 } from "path";
5725
+ import { join as join14 } from "path";
5715
5726
 
5716
5727
  // src/daemon/appReasons.ts
5717
5728
  var REASON_APP_EXITED = "\u7535\u8111\u4E0A\u7684 App \u5DF2\u9000\u51FA\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u5904\u7406";
@@ -6032,79 +6043,6 @@ function answerOf(set, native, decision) {
6032
6043
  return null;
6033
6044
  }
6034
6045
 
6035
- // src/daemon/qwenworkApp/keeper.ts
6036
- import { join as join14 } from "path";
6037
- var QW_DEFAULT_CDP_PORT = 39237;
6038
- var QWENWORK_BRAND = {
6039
- tool: "qwenwork",
6040
- label: "\u5343\u95EE\u529E\u516C",
6041
- macApp: "/Applications/QwenWorkCN.app",
6042
- macExe: "/Applications/QwenWorkCN.app/Contents/MacOS/QwenWorkCN",
6043
- appName: "QwenWorkCN",
6044
- bundleId: "cn.qwenwork.desktop.mac",
6045
- // Electron 默认 UA 带 `<应用名>/<版本>`;包里 package.json 的 name 是 qwen-work-cn、bundle 名是 QwenWorkCN(待真机核对是哪一个)
6046
- ua: /\b(QwenWorkCN|qwen-work-cn)\/\d/i,
6047
- comm: /\/QwenWorkCN\.app\//,
6048
- defaultPort: QW_DEFAULT_CDP_PORT
6049
- };
6050
- var QW_DOWN_ROUNDS = 2;
6051
- function qwKeeperThrottleFile() {
6052
- return join14(deskbuddyHome(), "qwapp", "keeper.json");
6053
- }
6054
- var QwenworkKeeper = class {
6055
- #d;
6056
- #inner;
6057
- #down = 0;
6058
- #lastResult = "";
6059
- constructor(deps2) {
6060
- this.#d = deps2;
6061
- this.#inner = deps2.inner ?? new QoderCdpKeeper({
6062
- ...deps2,
6063
- brand: QWENWORK_BRAND,
6064
- throttleFile: deps2.throttleFile ?? qwKeeperThrottleFile,
6065
- skipSystem: deps2.skipSystem ?? (() => process.env.DESKBUDDY_SKIP_QW_CDP === "1"),
6066
- busy: () => (deps2.busy ? deps2.busy() : toolBusy("qwenwork")) || deps2.hasCards?.() === true
6067
- });
6068
- }
6069
- #platform() {
6070
- return this.#d.platform ?? process.platform;
6071
- }
6072
- async tick() {
6073
- if (this.#platform() !== "darwin") return "untrusted";
6074
- if (!this.#d.inner && (this.#d.skipSystem ? this.#d.skipSystem() : process.env.DESKBUDDY_SKIP_QW_CDP === "1")) return "down";
6075
- const st = await this.#inner.tick();
6076
- if (st !== "down") {
6077
- this.#down = 0;
6078
- return st;
6079
- }
6080
- this.#down++;
6081
- if (this.#down >= QW_DOWN_ROUNDS) await this.#maybeRestart().catch((e) => this.#d.log(`\u5343\u95EE\u529E\u516C\u8C03\u8BD5\u7AEF\u53E3\uFF1A\u91CD\u542F\u51FA\u9519 ${e?.message ?? e}`));
6082
- return "down";
6083
- }
6084
- async #maybeRestart() {
6085
- if (this.#d.hasCards?.() === true) return;
6086
- if (!await this.#inner.appRunning()) {
6087
- this.#lastResult = "";
6088
- return;
6089
- }
6090
- const r = await this.#inner.wakeForTask();
6091
- if (r === "restarted") this.#down = 0;
6092
- if (r !== this.#lastResult) {
6093
- this.#lastResult = r;
6094
- const why = {
6095
- hung: "\u7AEF\u53E3\u5728\u76D1\u542C\u4F46\u65E0\u54CD\u5E94\uFF08App \u53EF\u80FD\u5361\u4F4F\u4E86\uFF09\uFF0C\u6309\u5361\u6B7B\u7B56\u7565\u5904\u7406\uFF1A\u8FDE\u7EED\u6EE1 20 \u5206\u949F\u3001\u7528\u6237\u4E0D\u5728\u573A\u4E14\u7A7A\u95F2\u624D\u7ED3\u675F\u5E76\u5E26\u7AEF\u53E3\u91CD\u542F\uFF0C\u5426\u5219\u8BF7\u5728\u7535\u8111\u4E0A\u91CD\u5F00\u5343\u95EE\u529E\u516C",
6096
- restarted: "\u5DF2\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u542F",
6097
- busy: "\u6B63\u5728\u8DD1\u4EFB\u52A1\uFF0C\u7B49\u7A7A\u95F2\u518D\u91CD\u542F",
6098
- present: "\u7528\u6237\u5728\u7535\u8111\u524D\uFF0C\u4E0D\u6253\u65AD",
6099
- failed: "\u91CD\u542F\u5931\u8D25\u6216\u4ECA\u5929\u7684\u91CD\u542F\u6B21\u6570\u5DF2\u7528\u5B8C",
6100
- launched: "\u521A\u62C9\u8D77\u8FC7\uFF0C\u7B49\u5B83\u8D77\u6765",
6101
- not_installed: "\u6CA1\u627E\u5230 App \u672C\u4F53"
6102
- };
6103
- if (why[r]) this.#d.log(`\u5343\u95EE\u529E\u516C\u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u4E0D\u5728\uFF1A${why[r]}`);
6104
- }
6105
- }
6106
- };
6107
-
6108
6046
  // src/daemon/qwenworkApp/page.ts
6109
6047
  var PAGE_VER = 1;
6110
6048
  var STATE_KEY = "__deskbuddyQwApp";
@@ -6291,6 +6229,18 @@ var QwenworkAppWatch = class {
6291
6229
  return false;
6292
6230
  }
6293
6231
  }
6232
+ /**
6233
+ * 千问办公开着、但调试端口没连上(没带端口 / 监听者不对,Bug 表 274):设备新建失败时据此回 app_port_missing 而不是 app_not_running。
6234
+ * 端口状态还没巡检过 / 这一路没启用 → 判不准,按否
6235
+ */
6236
+ async portMissing() {
6237
+ try {
6238
+ if (this.#stopped || !this.#d.enabled() || this.#keeperAt === 0 || this.#portStatus === "up") return false;
6239
+ return await this.#keeper.appRunning?.() === true;
6240
+ } catch {
6241
+ return false;
6242
+ }
6243
+ }
6294
6244
  /** hook 那一路的 preferApp:App 画的卡(页面发布等)与 App 自己的工具,连着时让给这里 */
6295
6245
  prefers(toolName) {
6296
6246
  return this.available() && (APP_CONFIRM_TOOL_RE.test(toolName) || APP_ONLY_TOOLS.has(toolName));
@@ -6518,7 +6468,7 @@ var QwenworkAppWatch = class {
6518
6468
  if (cached) return cached;
6519
6469
  let sid = null;
6520
6470
  try {
6521
- const root = this.#d.workspaceRoot ?? join15(homedir4(), ".qwenworkcn", "workspace");
6471
+ const root = this.#d.workspaceRoot ?? join14(homedir4(), ".qwenworkcn", "workspace");
6522
6472
  sid = (this.#d.findSession ?? ((id) => findQwenworkSession(id, root)))(p.chatId);
6523
6473
  } catch {
6524
6474
  sid = null;
@@ -6531,14 +6481,14 @@ var QwenworkAppWatch = class {
6531
6481
  const card = { id: p.id, set, notice: c.type === "notice", ...c.type === "plain" ? { plain: { allow: c.allow, deny: c.deny } } : {}, ac: new AbortController() };
6532
6482
  const { toolName, toolInput, summary: summary2 } = c;
6533
6483
  this.#cards.set(p.id, card);
6534
- const root = this.#d.workspaceRoot ?? join15(homedir4(), ".qwenworkcn", "workspace");
6484
+ const root = this.#d.workspaceRoot ?? join14(homedir4(), ".qwenworkcn", "workspace");
6535
6485
  const req = {
6536
6486
  window: true,
6537
6487
  tool: "qwenwork",
6538
6488
  hook: {
6539
6489
  hook_event_name: "PermissionRequest",
6540
6490
  session_id: this.#sessionOf(p),
6541
- cwd: p.chatId ? join15(root, p.chatId) : "",
6491
+ cwd: p.chatId ? join14(root, p.chatId) : "",
6542
6492
  tool_name: toolName,
6543
6493
  tool_input: toolInput,
6544
6494
  ...set ? { deskbuddy_options: set } : {},
@@ -6586,7 +6536,7 @@ var QwenworkAppWatch = class {
6586
6536
 
6587
6537
  // src/daemon/cozeApp/watch.ts
6588
6538
  import { homedir as homedir6 } from "os";
6589
- import { join as join17 } from "path";
6539
+ import { join as join16 } from "path";
6590
6540
 
6591
6541
  // src/daemon/cozeApp/cards.ts
6592
6542
  var NOTICE_BUTTON = "\u53BB\u7535\u8111\u4E0A\u5904\u7406";
@@ -6712,13 +6662,13 @@ function questionBody(map, decision, answerLists) {
6712
6662
  // src/daemon/cozeApp/keeper.ts
6713
6663
  import { lstatSync as lstatSync2, readdirSync as readdirSync6 } from "fs";
6714
6664
  import { homedir as homedir5 } from "os";
6715
- import { join as join16 } from "path";
6665
+ import { join as join15 } from "path";
6716
6666
  var COZE_DOWN_ROUNDS = 2;
6717
6667
  var COZE_ACTIVE_MS = 10 * 60 * 1e3;
6718
6668
  var WALK_MAX_ENTRIES = 3e3;
6719
6669
  var WALK_MAX_DEPTH = 3;
6720
6670
  function cozeUserDataDir(home = homedir5()) {
6721
- return join16(home, "Library", "Application Support", "Coze");
6671
+ return join15(home, "Library", "Application Support", "Coze");
6722
6672
  }
6723
6673
  function cozeActivity(dir2 = cozeUserDataDir(), now = Date.now()) {
6724
6674
  let seen = 0;
@@ -6733,7 +6683,7 @@ function cozeActivity(dir2 = cozeUserDataDir(), now = Date.now()) {
6733
6683
  if (depth === 0) readable = true;
6734
6684
  for (const n of names) {
6735
6685
  if (++seen > WALK_MAX_ENTRIES) return false;
6736
- const p = join16(d, n);
6686
+ const p = join15(d, n);
6737
6687
  let st;
6738
6688
  try {
6739
6689
  st = lstatSync2(p);
@@ -6759,7 +6709,7 @@ function cozeKeeperBusy(d) {
6759
6709
  }
6760
6710
  }
6761
6711
  function cozeKeeperThrottleFile() {
6762
- return join16(deskbuddyHome(), "cozeapp", "keeper.json");
6712
+ return join15(deskbuddyHome(), "cozeapp", "keeper.json");
6763
6713
  }
6764
6714
  var CozeKeeper = class {
6765
6715
  #d;
@@ -7435,7 +7385,7 @@ async function defaultMainPid(exec, exe = COZE_BRAND.macExe) {
7435
7385
  if (process.platform !== "darwin" || process.env.DESKBUDDY_SKIP_COZE_CDP === "1") return 0;
7436
7386
  const r = await exec("ps", ["-axo", "pid=,args="]);
7437
7387
  if (r.code !== 0) return 0;
7438
- const home = join17(homedir6(), "Applications", "\u6263\u5B50.app", "Contents", "MacOS", "Coze");
7388
+ const home = join16(homedir6(), "Applications", "\u6263\u5B50.app", "Contents", "MacOS", "Coze");
7439
7389
  for (const line of r.stdout.split(/\r?\n/)) {
7440
7390
  const m = /^\s*(\d+)\s+(.*)$/.exec(line);
7441
7391
  if (m && (isMainArgs(m[2], exe) || isMainArgs(m[2], home))) return Number(m[1]);
@@ -8839,7 +8789,7 @@ var QoderAppConn = class {
8839
8789
 
8840
8790
  // src/daemon/qoderApp/hookQuestions.ts
8841
8791
  import { closeSync as closeSync5, existsSync as existsSync3, openSync as openSync5, readdirSync as readdirSync7, readFileSync as readFileSync8, readSync as readSync4, statSync as statSync6 } from "fs";
8842
- import { join as join18 } from "path";
8792
+ import { join as join17 } from "path";
8843
8793
  var HOOK_QUESTION_MAX_AGE_MS = 6 * 60 * 60 * 1e3;
8844
8794
  var TAIL_BYTES2 = 256 * 1024;
8845
8795
  var BUSY = /* @__PURE__ */ new Set(["watch", "ask_approval"]);
@@ -8911,7 +8861,7 @@ function scanHookQuestions(tool, now) {
8911
8861
  if (!f.endsWith(".json")) continue;
8912
8862
  let s2;
8913
8863
  try {
8914
- s2 = JSON.parse(readFileSync8(join18(dir2, f), "utf8"));
8864
+ s2 = JSON.parse(readFileSync8(join17(dir2, f), "utf8"));
8915
8865
  } catch {
8916
8866
  continue;
8917
8867
  }
@@ -10307,7 +10257,7 @@ import { dirname as dirname4, sep as sep3 } from "path";
10307
10257
  // src/core/wrapWindows.ts
10308
10258
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readdirSync as readdirSync8, readFileSync as readFileSync9, renameSync as renameSync6, rmSync as rmSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "fs";
10309
10259
  import { tmpdir } from "os";
10310
- import { join as join19 } from "path";
10260
+ import { join as join18 } from "path";
10311
10261
  var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
10312
10262
  var WINDOW_EVENTS = [
10313
10263
  "SessionStart",
@@ -10320,10 +10270,10 @@ var WINDOW_EVENTS = [
10320
10270
  "SessionEnd"
10321
10271
  ];
10322
10272
  function windowsDir() {
10323
- return join19(deskbuddyHome(), "windows");
10273
+ return join18(deskbuddyHome(), "windows");
10324
10274
  }
10325
10275
  function bySessionDir() {
10326
- return join19(windowsDir(), "by-session");
10276
+ return join18(windowsDir(), "by-session");
10327
10277
  }
10328
10278
  function safeSid(sessionId) {
10329
10279
  return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
@@ -10337,7 +10287,7 @@ function atomicWrite(file, text) {
10337
10287
  renameSync6(tmp, file);
10338
10288
  }
10339
10289
  function windowFile(windowId) {
10340
- return join19(windowsDir(), `${windowId}.json`);
10290
+ return join18(windowsDir(), `${windowId}.json`);
10341
10291
  }
10342
10292
  function writeWindow(windowId, rec) {
10343
10293
  if (!WINDOW_ID_RE.test(windowId)) return;
@@ -10373,7 +10323,7 @@ function removeWindow(windowId) {
10373
10323
  return;
10374
10324
  }
10375
10325
  for (const n of names) {
10376
- const f = join19(bySessionDir(), n);
10326
+ const f = join18(bySessionDir(), n);
10377
10327
  try {
10378
10328
  if (readFileSync9(f, "utf8").trim() === windowId) rmSync5(f, { force: true });
10379
10329
  } catch {
@@ -10384,14 +10334,14 @@ function readBySession(sessionId) {
10384
10334
  const safe2 = safeSid(sessionId);
10385
10335
  if (!safe2) return null;
10386
10336
  try {
10387
- const id = readFileSync9(join19(bySessionDir(), safe2), "utf8").trim();
10337
+ const id = readFileSync9(join18(bySessionDir(), safe2), "utf8").trim();
10388
10338
  return WINDOW_ID_RE.test(id) ? id : null;
10389
10339
  } catch {
10390
10340
  return null;
10391
10341
  }
10392
10342
  }
10393
10343
  function lastEventFile(windowId) {
10394
- return join19(windowsDir(), `${windowId}.last`);
10344
+ return join18(windowsDir(), `${windowId}.last`);
10395
10345
  }
10396
10346
  function readLastEvent(windowId) {
10397
10347
  if (!WINDOW_ID_RE.test(windowId)) return null;
@@ -10410,13 +10360,13 @@ function writeLastEvent(windowId, ev, sid, at) {
10410
10360
  function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
10411
10361
  if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
10412
10362
  const name = `${windowId.slice(0, 16)}.sock`;
10413
- const primary = join19(home, "win", name);
10363
+ const primary = join18(home, "win", name);
10414
10364
  if (Buffer.byteLength(primary) <= 103) return primary;
10415
10365
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
10416
- return join19(tmpdir(), `deskbuddy-${uid}`, name);
10366
+ return join18(tmpdir(), `deskbuddy-${uid}`, name);
10417
10367
  }
10418
10368
  function ensureChannelDir(sockPath) {
10419
- const dir2 = join19(sockPath, "..");
10369
+ const dir2 = join18(sockPath, "..");
10420
10370
  try {
10421
10371
  mkdirSync9(dir2, { recursive: true, mode: 448 });
10422
10372
  const st = statSync7(dir2);
@@ -11517,7 +11467,7 @@ var CodebuddyTerminal = class {
11517
11467
  // src/wrap/trustPrompt.ts
11518
11468
  import { existsSync as existsSync4, realpathSync as realpathSync4, statSync as statSync10 } from "fs";
11519
11469
  import { homedir as homedir7 } from "os";
11520
- import { dirname as dirname5, join as join20, resolve as resolve5 } from "path";
11470
+ import { dirname as dirname5, join as join19, resolve as resolve5 } from "path";
11521
11471
  var TRUST_TOOLS = /* @__PURE__ */ new Set(["claude_code", "workbuddy", "codex"]);
11522
11472
  var CURSOR_CHARS = "\u276F>\u203A";
11523
11473
  var BOX = /[─-╿|]/g;
@@ -11725,7 +11675,7 @@ function scanRepo(start, stat2, home) {
11725
11675
  let d = start.replace(/[\\/]+$/, "") || start;
11726
11676
  let aboveHome = false;
11727
11677
  for (let i = 0; i < 64; i++) {
11728
- const g = stat2(join20(d, ".git"));
11678
+ const g = stat2(join19(d, ".git"));
11729
11679
  if (g) {
11730
11680
  if (aboveHome) return { kind: "unknown" };
11731
11681
  if (g === "file") return { kind: "worktree", at: d };
@@ -12770,6 +12720,44 @@ var DumateCdp = class {
12770
12720
  // src/daemon/codexWatch.ts
12771
12721
  import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
12772
12722
  import WebSocket from "ws";
12723
+ var CodexClaims = class {
12724
+ #m = /* @__PURE__ */ new Map();
12725
+ #wait = /* @__PURE__ */ new Map();
12726
+ claim(key, owner) {
12727
+ const cur = this.#m.get(key);
12728
+ if (cur && cur !== owner) return false;
12729
+ this.#m.set(key, owner);
12730
+ return true;
12731
+ }
12732
+ wait(key, owner, fire) {
12733
+ const q = this.#wait.get(key) ?? [];
12734
+ if (!q.some((w) => w.owner === owner)) q.push({ owner, fire });
12735
+ this.#wait.set(key, q);
12736
+ }
12737
+ unwait(key, owner) {
12738
+ const q = (this.#wait.get(key) ?? []).filter((w) => w.owner !== owner);
12739
+ if (q.length) this.#wait.set(key, q);
12740
+ else this.#wait.delete(key);
12741
+ }
12742
+ /** withdrawn = 占位方是撤卡(不是答复 / 已了结):交给下一个备用方补推 */
12743
+ release(key, owner, withdrawn = false) {
12744
+ if (this.#m.get(key) !== owner) return;
12745
+ this.#m.delete(key);
12746
+ if (!withdrawn) return;
12747
+ const q = this.#wait.get(key);
12748
+ const next = q?.shift();
12749
+ if (q && !q.length) this.#wait.delete(key);
12750
+ next?.fire();
12751
+ }
12752
+ get size() {
12753
+ return this.#m.size;
12754
+ }
12755
+ get waiting() {
12756
+ let n = 0;
12757
+ for (const q of this.#wait.values()) n += q.length;
12758
+ return n;
12759
+ }
12760
+ };
12773
12761
  var TURN_LIMIT_MS = 10 * 60 * 1e3;
12774
12762
  var QUEUE_MAX_MS2 = 10 * 60 * 1e3;
12775
12763
  var IDLE_GRACE_MS = 5 * 1e3;
@@ -12917,8 +12905,171 @@ var CodexWatch = class {
12917
12905
  #early = /* @__PURE__ */ new Map();
12918
12906
  /** 会话 → 等状态变化的回调(排队等空闲用) */
12919
12907
  #wake = /* @__PURE__ */ new Map();
12908
+ /** ws 主连接的请求处理上下文 */
12909
+ #main;
12910
+ /** 额外通道(名字 → 上下文) */
12911
+ #channels = /* @__PURE__ */ new Map();
12912
+ #claims;
12913
+ /** 主连接上去重抢不到的备用请求 */
12914
+ #standby = /* @__PURE__ */ new Map();
12920
12915
  constructor(deps2) {
12921
12916
  this.#d = { connect: (url) => new WebSocket(url), ...deps2 };
12917
+ this.#claims = deps2.claims ?? new CodexClaims();
12918
+ this.#main = {
12919
+ name: "",
12920
+ label: "Codex \u7A97\u53E3",
12921
+ cwd: this.#cwd,
12922
+ changes: this.#changes,
12923
+ mcpCalls: this.#mcpCalls,
12924
+ status: this.#status,
12925
+ pending: this.#pending,
12926
+ standby: this.#standby,
12927
+ keyOf: (_threadId, id) => String(id),
12928
+ reply: (_threadId, id, result) => this.#send({ id, result }),
12929
+ settled: (threadId) => {
12930
+ if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
12931
+ },
12932
+ feedback: (threadId, text) => this.#sendFeedback(threadId, text),
12933
+ inflight: (threadId) => this.#inflight.get(threadId)
12934
+ };
12935
+ }
12936
+ /**
12937
+ * 挂一条额外通道(同名的先关掉旧的)。请求 key = `<name>:<threadId>:<id>`,答复回到这条通道;
12938
+ * 会话状态 / 订阅 / 发一轮都不管(只管待确认),不报会话状态
12939
+ */
12940
+ attachChannel(name, opts) {
12941
+ if (this.#channels.has(name)) this.#closeChannel(name);
12942
+ const ctx = {
12943
+ name,
12944
+ label: opts.label,
12945
+ cwd: /* @__PURE__ */ new Map(),
12946
+ changes: /* @__PURE__ */ new Map(),
12947
+ mcpCalls: /* @__PURE__ */ new Map(),
12948
+ status: /* @__PURE__ */ new Map(),
12949
+ pending: /* @__PURE__ */ new Map(),
12950
+ standby: /* @__PURE__ */ new Map(),
12951
+ keyOf: (threadId, id) => `${name}:${threadId}:${String(id)}`,
12952
+ reply: (threadId, id, result) => opts.respond(threadId, id, result),
12953
+ settled: () => void 0,
12954
+ // App 里没法替用户发下一轮:附带的说明不写进会话(只按拒绝回),日志不记内容
12955
+ feedback: (threadId) => this.#d.log(`${opts.label} \u4F1A\u8BDD ${threadId.slice(0, 8)}\uFF1A\u8BBE\u5907\u62D2\u7EDD\u65F6\u9644\u4E86\u8BF4\u660E\uFF0C\u8FD9\u6761\u901A\u9053\u5199\u4E0D\u8FDB\u4F1A\u8BDD\uFF0C\u53EA\u6309\u62D2\u7EDD\u56DE`),
12956
+ inflight: () => void 0
12957
+ };
12958
+ this.#channels.set(name, ctx);
12959
+ return {
12960
+ onMessage: (msg) => {
12961
+ if (this.#channels.get(name) !== ctx) return;
12962
+ this.#onChannelMessage(ctx, msg);
12963
+ },
12964
+ pending: () => this.#channels.get(name) === ctx ? ctx.pending.size : 0,
12965
+ close: () => {
12966
+ if (this.#channels.get(name) === ctx) this.#closeChannel(name);
12967
+ }
12968
+ };
12969
+ }
12970
+ #closeChannel(name) {
12971
+ const ctx = this.#channels.get(name);
12972
+ if (!ctx) return;
12973
+ this.#channels.delete(name);
12974
+ this.#dropStandby(ctx);
12975
+ const gone = [...ctx.pending.values()];
12976
+ ctx.pending.clear();
12977
+ for (const e of gone) {
12978
+ e.ac.abort();
12979
+ this.#release(e, true);
12980
+ }
12981
+ ctx.cwd.clear();
12982
+ ctx.changes.clear();
12983
+ ctx.mcpCalls.clear();
12984
+ ctx.status.clear();
12985
+ }
12986
+ #onChannelMessage(ctx, m) {
12987
+ const method = typeof m.method === "string" ? m.method : "";
12988
+ if (!method) return;
12989
+ const params = m.params && typeof m.params === "object" ? m.params : {};
12990
+ const id = m.id;
12991
+ if (id !== void 0 && id !== null && (typeof id === "number" || typeof id === "string")) {
12992
+ this.#onServerRequest(id, method, params, ctx);
12993
+ return;
12994
+ }
12995
+ const tid = typeof params.threadId === "string" ? params.threadId : "";
12996
+ switch (method) {
12997
+ case "thread/started": {
12998
+ const t = params.thread;
12999
+ if (t && typeof t.id === "string" && typeof t.cwd === "string") ctx.cwd.set(t.id, t.cwd);
13000
+ return;
13001
+ }
13002
+ case "thread/settings/updated": {
13003
+ const cwd = (params.threadSettings ?? {}).cwd;
13004
+ if (tid && typeof cwd === "string") ctx.cwd.set(tid, cwd);
13005
+ return;
13006
+ }
13007
+ case "thread/status/changed": {
13008
+ const type2 = (params.status ?? {}).type;
13009
+ if (tid && typeof type2 === "string") ctx.status.set(tid, type2);
13010
+ return;
13011
+ }
13012
+ case "thread/closed":
13013
+ if (tid) {
13014
+ ctx.cwd.delete(tid);
13015
+ ctx.mcpCalls.delete(tid);
13016
+ ctx.status.set(tid, "notLoaded");
13017
+ }
13018
+ return;
13019
+ case "item/started":
13020
+ if (tid) this.#noteItem(ctx, tid, params.item);
13021
+ return;
13022
+ case "item/completed":
13023
+ this.#itemDone(ctx, params);
13024
+ return;
13025
+ case "serverRequest/resolved":
13026
+ this.#resolved(ctx, params);
13027
+ return;
13028
+ default:
13029
+ }
13030
+ }
13031
+ /** 日志前缀:主连接「Codex 窗口」,额外通道「<label> 」 */
13032
+ #tag(ctx) {
13033
+ return ctx.name ? `${ctx.label} ` : ctx.label;
13034
+ }
13035
+ /** 释放去重占位 */
13036
+ #release(e, withdrawn = false) {
13037
+ if (e.claim) this.#claims.release(e.claim, e, withdrawn);
13038
+ }
13039
+ #dropStandby(ctx) {
13040
+ for (const e of ctx.standby.values()) if (e.claim) this.#claims.unwait(e.claim, e);
13041
+ ctx.standby.clear();
13042
+ }
13043
+ /** 条目结束:清改动文件 / MCP 调用 */
13044
+ #itemDone(ctx, p) {
13045
+ const item = p.item;
13046
+ if (item && typeof item.id === "string") ctx.changes.delete(item.id);
13047
+ if (item && item.type === "mcpToolCall" && typeof p.threadId === "string") ctx.mcpCalls.delete(p.threadId);
13048
+ }
13049
+ /** 请求已了结(人在窗口 / App 里先答了,或别的客户端答了):撤设备卡片,🔴 之后绝不再回话 */
13050
+ #resolved(ctx, p) {
13051
+ const rid = String(p.requestId);
13052
+ const tid = typeof p.threadId === "string" ? p.threadId : "";
13053
+ let keys;
13054
+ if (!ctx.name) keys = [rid];
13055
+ else if (tid) keys = [ctx.keyOf(tid, rid)];
13056
+ else keys = [...ctx.pending.keys()].filter((k) => k.endsWith(`:${rid}`));
13057
+ if (ctx.name && !tid) keys = [.../* @__PURE__ */ new Set([...keys, ...[...ctx.standby.keys()].filter((k) => k.endsWith(`:${rid}`))])];
13058
+ for (const key of keys) {
13059
+ const sb = ctx.standby.get(key);
13060
+ if (sb) {
13061
+ ctx.standby.delete(key);
13062
+ if (sb.claim) this.#claims.unwait(sb.claim, sb);
13063
+ ctx.settled(sb.threadId);
13064
+ }
13065
+ const pend = ctx.pending.get(key);
13066
+ if (!pend) continue;
13067
+ ctx.pending.delete(key);
13068
+ this.#release(pend);
13069
+ pend.ac.abort();
13070
+ this.#reportResumed(pend, ctx);
13071
+ ctx.settled(pend.threadId);
13072
+ }
12922
13073
  }
12923
13074
  get connected() {
12924
13075
  return this.#ws !== null;
@@ -12951,6 +13102,7 @@ var CodexWatch = class {
12951
13102
  }
12952
13103
  stop() {
12953
13104
  this.#stopped = true;
13105
+ for (const name of [...this.#channels.keys()]) this.#closeChannel(name);
12954
13106
  this.#teardown();
12955
13107
  }
12956
13108
  #open(url) {
@@ -12982,8 +13134,13 @@ var CodexWatch = class {
12982
13134
  c.resolve({ error: { message: "disconnected" }, local: true });
12983
13135
  }
12984
13136
  this.#calls.clear();
12985
- for (const { ac } of this.#pending.values()) ac.abort();
13137
+ this.#dropStandby(this.#main);
13138
+ const gone = [...this.#pending.values()];
12986
13139
  this.#pending.clear();
13140
+ for (const e of gone) {
13141
+ e.ac.abort();
13142
+ this.#release(e, true);
13143
+ }
12987
13144
  this.#subscribed.clear();
12988
13145
  this.#retrying.clear();
12989
13146
  this.#cwd.clear();
@@ -13142,16 +13299,16 @@ var CodexWatch = class {
13142
13299
  }
13143
13300
  const turns = Array.isArray(th?.turns) ? th.turns : [];
13144
13301
  const last = turns[turns.length - 1];
13145
- if (last && last.status === "inProgress" && Array.isArray(last.items)) for (const it of last.items) this.#noteItem(threadId, it);
13302
+ if (last && last.status === "inProgress" && Array.isArray(last.items)) for (const it of last.items) this.#noteItem(this.#main, threadId, it);
13146
13303
  }
13147
13304
  /** 进行中的条目:改文件记文件列表,MCP 调用记工具名 */
13148
- #noteItem(threadId, item) {
13305
+ #noteItem(ctx, threadId, item) {
13149
13306
  if (!item || typeof item.id !== "string") return;
13150
13307
  if (item.type === "fileChange" && Array.isArray(item.changes)) {
13151
13308
  const paths = item.changes.map((c) => typeof c?.path === "string" ? c.path : "").filter(Boolean);
13152
- this.#changes.set(item.id, paths);
13309
+ ctx.changes.set(item.id, paths);
13153
13310
  } else if (item.type === "mcpToolCall" && typeof item.server === "string" && typeof item.tool === "string") {
13154
- this.#mcpCalls.set(threadId, { server: item.server, tool: item.tool, args: item.arguments });
13311
+ ctx.mcpCalls.set(threadId, { server: item.server, tool: item.tool, args: item.arguments });
13155
13312
  }
13156
13313
  }
13157
13314
  #subscribe(threadId) {
@@ -13197,6 +13354,7 @@ var CodexWatch = class {
13197
13354
  this.#retrying.delete(threadId);
13198
13355
  if (!this.#subscribed.has(threadId)) return;
13199
13356
  for (const p of this.#pending.values()) if (p.threadId === threadId) return;
13357
+ for (const p of this.#standby.values()) if (p.threadId === threadId) return;
13200
13358
  if ((this.#own.get(threadId) ?? 0) > 0) return;
13201
13359
  this.#subscribed.delete(threadId);
13202
13360
  void this.#request("thread/unsubscribe", { threadId });
@@ -13409,7 +13567,7 @@ var CodexWatch = class {
13409
13567
  }
13410
13568
  const params = m.params && typeof m.params === "object" ? m.params : {};
13411
13569
  if (id !== void 0) {
13412
- this.#onServerRequest(id, method, params);
13570
+ this.#onServerRequest(id, method, params, this.#main);
13413
13571
  return;
13414
13572
  }
13415
13573
  this.#onNotification(method, params);
@@ -13478,27 +13636,18 @@ var CodexWatch = class {
13478
13636
  return;
13479
13637
  }
13480
13638
  case "item/started":
13481
- if (typeof p.threadId === "string") this.#noteItem(p.threadId, p.item);
13639
+ if (typeof p.threadId === "string") this.#noteItem(this.#main, p.threadId, p.item);
13482
13640
  return;
13483
13641
  case "item/completed": {
13484
13642
  const item = p.item;
13485
- if (item && typeof item.id === "string") this.#changes.delete(item.id);
13486
- if (item && item.type === "mcpToolCall" && typeof p.threadId === "string") this.#mcpCalls.delete(p.threadId);
13643
+ this.#itemDone(this.#main, p);
13487
13644
  const own = typeof p.turnId === "string" ? this.#turns.get(p.turnId) : void 0;
13488
13645
  if (own && item && item.type === "agentMessage") own.items.push(item);
13489
13646
  return;
13490
13647
  }
13491
- case "serverRequest/resolved": {
13492
- const key = String(p.requestId);
13493
- const pend = this.#pending.get(key);
13494
- if (pend) {
13495
- this.#pending.delete(key);
13496
- pend.ac.abort();
13497
- this.#reportResumed(pend);
13498
- if (this.#idle.has(pend.threadId)) this.#unsubscribe(pend.threadId);
13499
- }
13648
+ case "serverRequest/resolved":
13649
+ this.#resolved(this.#main, p);
13500
13650
  return;
13501
- }
13502
13651
  default:
13503
13652
  }
13504
13653
  }
@@ -13506,19 +13655,19 @@ var CodexWatch = class {
13506
13655
  * 我们替 Codex 报过 ask_approval 的卡片了结了(设备答了 / 窗口里答了):再报一次 watch,
13507
13656
  * 别指望 Codex 对 MCP 工具发 PostToolUse(没有实测依据,评审)。会话已不在跑就不报,免得盖掉 Stop 的 done
13508
13657
  */
13509
- #reportResumed(entry) {
13658
+ #reportResumed(entry, ctx) {
13510
13659
  const hook = entry.reported;
13511
13660
  if (!hook) return;
13512
13661
  entry.reported = void 0;
13513
- if (this.#status.get(entry.threadId) !== "active") return;
13662
+ if (ctx.status.get(entry.threadId) !== "active") return;
13514
13663
  try {
13515
13664
  this.#d.report?.({ ...hook, hook_event_name: "PostToolUse" });
13516
13665
  } catch {
13517
13666
  }
13518
13667
  }
13519
13668
  /** 服务端请求 → 设备卡片;推不了返回原因(只进日志,不含内容) */
13520
- #card(method, p, threadId) {
13521
- const cwd = this.#cwd.get(threadId) ?? "";
13669
+ #card(method, p, threadId, ctx) {
13670
+ const cwd = ctx.cwd.get(threadId) ?? "";
13522
13671
  const approve = (reply) => nativeReply(reply, "decision") ?? (reply.decision === "allow" ? { decision: "accept" } : reply.decision === "deny" ? { decision: "decline" } : void 0);
13523
13672
  const withOptions = (opts) => opts ? { deskbuddy_options: opts } : {};
13524
13673
  if (method === "item/commandExecution/requestApproval") {
@@ -13531,7 +13680,7 @@ var CodexWatch = class {
13531
13680
  }
13532
13681
  if (method === "item/fileChange/requestApproval") {
13533
13682
  if (typeof p.grantRoot === "string" && p.grantRoot) return "\u5E26 grantRoot";
13534
- const paths = typeof p.itemId === "string" && this.#changes.get(p.itemId) || [];
13683
+ const paths = typeof p.itemId === "string" && ctx.changes.get(p.itemId) || [];
13535
13684
  const shown = paths.join(", ");
13536
13685
  if (!shown) return NO_FILE_LIST;
13537
13686
  return { hook: { tool_name: "Edit", tool_input: { file_path: shown, file_paths: [...paths] }, cwd, ...withOptions(codexFileOptions()) }, respond: approve };
@@ -13553,7 +13702,7 @@ var CodexWatch = class {
13553
13702
  };
13554
13703
  }
13555
13704
  if (method === "mcpServer/elicitation/request") {
13556
- const e = codexElicitation(p, this.#mcpCalls.get(threadId));
13705
+ const e = codexElicitation(p, ctx.mcpCalls.get(threadId));
13557
13706
  if (typeof e === "string") return e;
13558
13707
  if (e.kind === "approval") {
13559
13708
  const remember = e.remember;
@@ -13587,26 +13736,41 @@ var CodexWatch = class {
13587
13736
  report: true
13588
13737
  };
13589
13738
  }
13739
+ if (method === "item/permissions/requestApproval") {
13740
+ const c = codexPermissionsCard(p);
13741
+ if (!c) return "\u6743\u9650\u7533\u8BF7\u5185\u5BB9\u8BA4\u4E0D\u51FA";
13742
+ return {
13743
+ hook: { tool_name: CODEX_PERMISSIONS_TOOL, tool_input: c.toolInput, cwd: typeof p.cwd === "string" ? p.cwd : cwd, deskbuddy_summary: c.summary, deskbuddy_options: c.options },
13744
+ respond: (reply) => {
13745
+ const n = reply.native;
13746
+ if (n && typeof n === "object" && "permissions" in n && (reply.decision === "allow" || reply.decision === "deny")) return n;
13747
+ if (reply.decision === "allow") return c.options.actions.allow?.native;
13748
+ if (reply.decision === "deny") return { permissions: {}, scope: "turn" };
13749
+ return void 0;
13750
+ }
13751
+ };
13752
+ }
13590
13753
  return "\u4E0D\u5904\u7406\u7684\u8BF7\u6C42\u7C7B\u578B";
13591
13754
  }
13592
- #onServerRequest(id, method, p) {
13755
+ #onServerRequest(id, method, p, ctx) {
13593
13756
  const threadId = typeof p.threadId === "string" ? p.threadId : "";
13594
13757
  if (!threadId) return;
13595
- const key = String(id);
13596
- if (this.#pending.get(key)?.threadId === threadId) return;
13758
+ const key = ctx.keyOf(threadId, id);
13759
+ if (ctx.pending.get(key)?.threadId === threadId || ctx.standby.get(key)?.threadId === threadId) return;
13597
13760
  const entry = { ac: new AbortController(), threadId };
13598
- const skip = (why) => this.#d.log(`Codex \u7A97\u53E3\u8BF7\u6C42 ${method}\uFF08\u4F1A\u8BDD ${threadId.slice(0, 8)}\uFF09\u4E0D\u63A8\u8BBE\u5907\uFF1A${why}`);
13599
- const card = this.#card(method, p, threadId);
13600
- const inflight = this.#inflight.get(threadId);
13761
+ if (typeof p.itemId === "string" && p.itemId) entry.claim = `${threadId}:${p.itemId}`;
13762
+ const skip = (why) => this.#d.log(`${this.#tag(ctx)}\u8BF7\u6C42 ${method}\uFF08\u4F1A\u8BDD ${threadId.slice(0, 8)}\uFF09\u4E0D\u63A8\u8BBE\u5907\uFF1A${why}`);
13763
+ const card = this.#card(method, p, threadId, ctx);
13764
+ const inflight = ctx.inflight(threadId);
13601
13765
  if (card === NO_FILE_LIST && inflight) {
13602
- this.#pending.set(key, entry);
13766
+ ctx.pending.set(key, entry);
13603
13767
  const gen = this.#gen;
13604
13768
  void inflight.then(() => void 0).then(() => {
13605
- if (gen !== this.#gen || this.#pending.get(key) !== entry) return;
13606
- this.#pending.delete(key);
13607
- const again = this.#card(method, p, threadId);
13769
+ if (gen !== this.#gen || ctx.pending.get(key) !== entry) return;
13770
+ ctx.pending.delete(key);
13771
+ const again = this.#card(method, p, threadId, ctx);
13608
13772
  if (typeof again === "string") skip(again);
13609
- else this.#push(id, entry, again);
13773
+ else this.#push(id, entry, again, ctx);
13610
13774
  });
13611
13775
  return;
13612
13776
  }
@@ -13614,7 +13778,8 @@ var CodexWatch = class {
13614
13778
  skip(card);
13615
13779
  return;
13616
13780
  }
13617
- this.#push(id, entry, card);
13781
+ this.#push(id, entry, card, ctx);
13782
+ if (ctx.name && ctx.pending.get(key) === entry) this.#d.log(`${this.#tag(ctx)}\u8BF7\u6C42 ${method}\uFF08\u4F1A\u8BDD ${threadId.slice(0, 8)}\uFF09\u5DF2\u63A8\u8BBE\u5907`);
13618
13783
  }
13619
13784
  /** 设备上「拒绝并说明」的那段话:作为下一轮发进同一会话(会话正忙时排队到空闲) */
13620
13785
  #sendFeedback(threadId, text) {
@@ -13623,10 +13788,24 @@ var CodexWatch = class {
13623
13788
  () => this.#d.log(`\u8BBE\u5907\u53CD\u9988\u5199\u8FDB Codex \u4F1A\u8BDD ${threadId.slice(0, 8)} \u5931\u8D25`)
13624
13789
  );
13625
13790
  }
13626
- #push(id, entry, card) {
13627
- const key = String(id);
13791
+ #push(id, entry, card, ctx) {
13792
+ const key = ctx.keyOf(entry.threadId, id);
13628
13793
  const threadId = entry.threadId;
13629
- this.#pending.set(key, entry);
13794
+ if (entry.claim && !this.#claims.claim(entry.claim, entry)) {
13795
+ this.#d.log(`${this.#tag(ctx)}\u8BF7\u6C42\uFF08\u4F1A\u8BDD ${threadId.slice(0, 8)}\uFF09\u5DF2\u7ECF\u4ECE\u53E6\u4E00\u6761\u901A\u9053\u63A8\u8FC7\u8BBE\u5907\uFF0C\u5148\u4E0D\u63A8\u7B2C\u4E8C\u5F20\uFF08\u90A3\u8FB9\u64A4\u5361\u65F6\u8865\u63A8\uFF09`);
13796
+ const claim = entry.claim;
13797
+ const gen = this.#gen;
13798
+ ctx.standby.set(key, entry);
13799
+ this.#claims.wait(claim, entry, () => {
13800
+ if (ctx.standby.get(key) !== entry) return;
13801
+ ctx.standby.delete(key);
13802
+ if (ctx.name ? this.#channels.get(ctx.name) !== ctx : !this.#ws || gen !== this.#gen) return;
13803
+ this.#d.log(`${this.#tag(ctx)}\u8BF7\u6C42\uFF08\u4F1A\u8BDD ${threadId.slice(0, 8)}\uFF09\u53E6\u4E00\u6761\u901A\u9053\u64A4\u5361\u4E86\uFF0C\u8865\u63A8\u8BBE\u5907`);
13804
+ this.#push(id, { ac: new AbortController(), threadId, claim, ...entry.reported ? { reported: entry.reported } : {} }, card, ctx);
13805
+ });
13806
+ return;
13807
+ }
13808
+ ctx.pending.set(key, entry);
13630
13809
  const hook = { hook_event_name: "PermissionRequest", session_id: threadId, ...card.hook };
13631
13810
  if (card.report) {
13632
13811
  entry.reported = hook;
@@ -13636,21 +13815,23 @@ var CodexWatch = class {
13636
13815
  }
13637
13816
  }
13638
13817
  const req = { window: true, tool: "codex", hook };
13639
- const settled2 = () => {
13640
- if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
13641
- };
13818
+ const settled2 = () => ctx.settled(threadId);
13642
13819
  void this.#d.gate(req, entry.ac.signal).then((reply) => {
13643
- if (this.#pending.get(key) !== entry) return;
13644
- this.#pending.delete(key);
13820
+ if (ctx.pending.get(key) !== entry || entry.ac.signal.aborted) return;
13821
+ ctx.pending.delete(key);
13822
+ this.#release(entry);
13645
13823
  const result = card.respond(reply);
13646
13824
  if (result) {
13647
- this.#send({ id, result });
13648
- this.#reportResumed(entry);
13649
- if (reply.decision === "deny" && typeof reply.feedback === "string" && reply.feedback.trim()) this.#sendFeedback(threadId, reply.feedback);
13825
+ ctx.reply(threadId, id, result);
13826
+ this.#reportResumed(entry, ctx);
13827
+ if (reply.decision === "deny" && typeof reply.feedback === "string" && reply.feedback.trim()) ctx.feedback(threadId, reply.feedback);
13650
13828
  }
13651
13829
  settled2();
13652
13830
  }).catch(() => {
13653
- if (this.#pending.get(key) === entry) this.#pending.delete(key);
13831
+ if (ctx.pending.get(key) === entry) {
13832
+ ctx.pending.delete(key);
13833
+ this.#release(entry);
13834
+ }
13654
13835
  settled2();
13655
13836
  });
13656
13837
  }
@@ -13826,6 +14007,445 @@ function codexQuestionInput(p) {
13826
14007
  return { toolInput: { questions }, ids };
13827
14008
  }
13828
14009
 
14010
+ // src/daemon/chatgptApp/bridge.ts
14011
+ var INJECT_VERSION = 2;
14012
+ var QUEUE_MAX2 = 500;
14013
+ var RESOLVED_MAX = 200;
14014
+ var KEEPER_EVERY_MS5 = 15 * 1e3;
14015
+ var LAUNCH_CHECK_EVERY_MS2 = 2 * 1e3;
14016
+ var CONNECT_BACKOFF_MIN_MS5 = 15 * 1e3;
14017
+ var CONNECT_BACKOFF_MAX_MS5 = 5 * 60 * 1e3;
14018
+ var LABEL = "ChatGPT App";
14019
+ var CHANNEL = "app";
14020
+ var REQUEST_METHODS = [
14021
+ "item/commandExecution/requestApproval",
14022
+ "item/fileChange/requestApproval",
14023
+ "item/permissions/requestApproval",
14024
+ "mcpServer/elicitation/request",
14025
+ "item/tool/requestUserInput"
14026
+ ];
14027
+ var INJECT = `(() => {
14028
+ const V = ${INJECT_VERSION}
14029
+ const w = window
14030
+ const cur = w.__deskbuddyChatgpt
14031
+ if (cur && cur.v === V) return 'ok'
14032
+ if (cur) cur.off = true
14033
+ const M = new Set(${JSON.stringify(REQUEST_METHODS)})
14034
+ const s = { v: V, g: Date.now().toString(36) + Math.random().toString(36).slice(2), q: [], d: 0, r: [], off: false }
14035
+ const push = (item) => {
14036
+ s.q.push(item)
14037
+ if (s.q.length > ${QUEUE_MAX2}) { s.d += s.q.length - ${QUEUE_MAX2}; s.q.splice(0, s.q.length - ${QUEUE_MAX2}) }
14038
+ }
14039
+ w.addEventListener('message', (e) => {
14040
+ try {
14041
+ if (s.off) return
14042
+ const m = e && e.data
14043
+ if (!m || typeof m !== 'object' || m.hostId !== 'local') return
14044
+ if (m.type === 'mcp-request') {
14045
+ const r = m.request
14046
+ if (!r || typeof r !== 'object' || !M.has(r.method)) return
14047
+ // \u540C\u4F1A\u8BDD id \u91CD\u65B0\u4ECE 0 \u7F16\u53F7\uFF08App \u91CD\u542F app-server / \u4F1A\u8BDD\u91CD\u8F7D\uFF09\uFF1A\u8FD9\u4E2A id \u4E4B\u524D\u7684\u4E86\u7ED3\u8BB0\u5F55\u4F5C\u5E9F\uFF0C\u5426\u5219\u65B0\u8BF7\u6C42\u7684\u7B54\u590D\u4F1A\u88AB\u8BEF\u62E6
14048
+ const t = r.params && typeof r.params.threadId === 'string' ? r.params.threadId : '*'
14049
+ const id = String(r.id)
14050
+ s.r = s.r.filter((k) => k !== t + ':' + id && k !== '*:' + id)
14051
+ push({ type: 'mcp-request', request: { id: r.id, method: r.method, params: r.params } })
14052
+ } else if (m.type === 'mcp-notification') {
14053
+ if (typeof m.method !== 'string' || /delta$/i.test(m.method)) return
14054
+ if (m.method === 'serverRequest/resolved' && m.params && typeof m.params === 'object') {
14055
+ const t = typeof m.params.threadId === 'string' ? m.params.threadId : '*'
14056
+ s.r.push(t + ':' + String(m.params.requestId))
14057
+ if (s.r.length > ${RESOLVED_MAX}) s.r.splice(0, s.r.length - ${RESOLVED_MAX})
14058
+ }
14059
+ push({ type: 'mcp-notification', method: m.method, params: m.params })
14060
+ }
14061
+ } catch (_) {}
14062
+ })
14063
+ w.__deskbuddyChatgpt = s
14064
+ return 'injected'
14065
+ })()`;
14066
+ var DRAIN = `(() => {
14067
+ const s = window.__deskbuddyChatgpt
14068
+ if (!s || s.v !== ${INJECT_VERSION} || s.off) return { missing: true }
14069
+ const items = s.q
14070
+ const dropped = s.d
14071
+ s.q = []
14072
+ s.d = 0
14073
+ return { g: s.g, items, dropped }
14074
+ })()`;
14075
+ function answerExpr4(gen, threadId, id, result) {
14076
+ return `(async () => {
14077
+ const s = window.__deskbuddyChatgpt
14078
+ if (!s || s.g !== ${JSON.stringify(gen)}) return { ok: false, why: 'reloaded' }
14079
+ const T = ${JSON.stringify(threadId)}
14080
+ const ID = ${JSON.stringify(id)}
14081
+ if (s.r.indexOf(T + ':' + String(ID)) >= 0 || s.r.indexOf('*:' + String(ID)) >= 0) return { ok: false, why: 'resolved' }
14082
+ const b = window.electronBridge
14083
+ if (!b || typeof b.sendMessageFromView !== 'function') return { ok: false, why: 'no_bridge' }
14084
+ try {
14085
+ await b.sendMessageFromView({ type: 'mcp-response', hostId: 'local', threadId: T, response: { id: ID, result: ${JSON.stringify(result)} } })
14086
+ } catch (e) {
14087
+ return { ok: false, why: 'send_failed', err: String((e && e.message) || e).slice(0, 120) }
14088
+ }
14089
+ return { ok: true }
14090
+ })()`;
14091
+ }
14092
+ function pickChatgptTarget(list) {
14093
+ if (!Array.isArray(list)) return null;
14094
+ for (const t of list) {
14095
+ if (!t || t.type !== "page" || typeof t.webSocketDebuggerUrl !== "string") continue;
14096
+ if (String(t.url ?? "").split(/[?#]/)[0] === "app://-/index.html") return t;
14097
+ }
14098
+ return null;
14099
+ }
14100
+ var ChatgptAppBridge = class {
14101
+ #d;
14102
+ #client = null;
14103
+ #channel = null;
14104
+ /** 当前页面的注入代号('' = 还没取到过) */
14105
+ #gen = "";
14106
+ #portStatus = "down";
14107
+ #portChecked = false;
14108
+ #keeperAt = 0;
14109
+ #launchAt = 0;
14110
+ #launchRunning = false;
14111
+ #connectRetryAt = 0;
14112
+ #connectBackoffMs = CONNECT_BACKOFF_MIN_MS5;
14113
+ #connectFailLogged = false;
14114
+ #running = false;
14115
+ #stopped = false;
14116
+ /** 会话 → 最近一次状态(thread/status/changed);有 active 的 = App 忙 */
14117
+ #status = /* @__PURE__ */ new Map();
14118
+ constructor(deps2) {
14119
+ this.#d = deps2;
14120
+ }
14121
+ #now() {
14122
+ return this.#d.now ? this.#d.now() : Date.now();
14123
+ }
14124
+ get connected() {
14125
+ return !!this.#client && !this.#client.closed;
14126
+ }
14127
+ /** 页面里有进行中的会话(keeper 判忙用;断开时清空) */
14128
+ busy() {
14129
+ for (const t of this.#status.values()) if (t === "active") return true;
14130
+ return false;
14131
+ }
14132
+ /** 等设备答复的卡有几张 */
14133
+ pending() {
14134
+ return this.#channel?.pending() ?? 0;
14135
+ }
14136
+ stop() {
14137
+ this.#stopped = true;
14138
+ this.#disconnect();
14139
+ }
14140
+ /** daemon 每秒调一次;重入直接返回。不抛 */
14141
+ async tick() {
14142
+ if (this.#running || this.#stopped) return;
14143
+ this.#running = true;
14144
+ try {
14145
+ await this.#tick();
14146
+ } catch (e) {
14147
+ this.#d.log(`${LABEL} \u6388\u6743\u6865\u51FA\u9519\uFF1A${e?.message ?? e}`);
14148
+ } finally {
14149
+ this.#running = false;
14150
+ }
14151
+ }
14152
+ async #tick() {
14153
+ if (!this.#d.enabled()) {
14154
+ this.#disconnect();
14155
+ return;
14156
+ }
14157
+ const now = this.#now();
14158
+ if (this.#portStatus !== "up") this.#checkLaunch(now);
14159
+ if (this.#keeperAt === 0 || now - this.#keeperAt >= KEEPER_EVERY_MS5) {
14160
+ this.#keeperAt = now;
14161
+ const prev = this.#portChecked ? this.#portStatus : null;
14162
+ this.#portStatus = await this.#d.keeper.tick();
14163
+ this.#portChecked = true;
14164
+ if (this.#stopped) return this.#disconnect();
14165
+ if (this.#portStatus !== prev) {
14166
+ const p = this.#d.port();
14167
+ if (this.#portStatus === "down") this.#d.log(`${LABEL} \u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF08${p} \u4E0D\u901A\uFF0C\u6216 App \u6CA1\u5F00\uFF09\uFF0CApp \u91CC Codex \u7684\u5F85\u786E\u8BA4\u63A8\u4E0D\u5230\u8BBE\u5907`);
14168
+ else if (this.#portStatus === "untrusted") this.#d.log(`${LABEL} \u8C03\u8BD5\u7AEF\u53E3 ${p} \u7684\u76D1\u542C\u8005\u4E0D\u662F\u672C\u673A\u56DE\u73AF\u4E0A\u7684 ChatGPT App\uFF0C\u4E0D\u8FDE`);
14169
+ else if (this.#portStatus === "hung") this.#d.log(`${LABEL} \u8C03\u8BD5\u7AEF\u53E3 ${p} \u5728\u76D1\u542C\u4F46\u65E0\u54CD\u5E94`);
14170
+ }
14171
+ if (this.#portStatus !== "up" && this.#client) {
14172
+ this.#d.log(`${LABEL} \u6388\u6743\u6865\u65AD\u5F00\uFF08\u8C03\u8BD5\u7AEF\u53E3${this.#portStatus === "hung" ? "\u65E0\u54CD\u5E94" : this.#portStatus === "untrusted" ? "\u76D1\u542C\u8005\u4E0D\u5BF9" : "\u4E0D\u901A"}\uFF09`);
14173
+ this.#disconnect();
14174
+ }
14175
+ }
14176
+ if (this.#portStatus !== "up") return;
14177
+ if (!this.#client || this.#client.closed) {
14178
+ if (this.#client) {
14179
+ this.#d.log(`${LABEL} \u6388\u6743\u6865\u8FDE\u63A5\u65AD\u4E86\uFF0C\u91CD\u8FDE`);
14180
+ this.#disconnect();
14181
+ }
14182
+ if (now < this.#connectRetryAt) return;
14183
+ if (!await this.#connect()) return;
14184
+ }
14185
+ await this.#poll();
14186
+ }
14187
+ /** 刚启动补端口:不等它(退出 + 拉起要十几秒),keeper 自己防并发;重开了催一下健康检查 */
14188
+ #checkLaunch(now) {
14189
+ if (this.#launchRunning || now - this.#launchAt < LAUNCH_CHECK_EVERY_MS2) return;
14190
+ this.#launchAt = now;
14191
+ this.#launchRunning = true;
14192
+ void this.#d.keeper.fixFreshLaunch().then((r) => {
14193
+ if (r === "reopened") {
14194
+ this.#keeperAt = 0;
14195
+ this.#connectRetryAt = 0;
14196
+ }
14197
+ }).catch((e) => this.#d.log(`${LABEL} \u542F\u52A8\u8865\u7AEF\u53E3\u51FA\u9519\uFF1A${e?.message ?? e}`)).finally(() => {
14198
+ this.#launchRunning = false;
14199
+ });
14200
+ }
14201
+ async #connect() {
14202
+ let client;
14203
+ try {
14204
+ client = await (this.#d.connect ?? ((p) => connectCdp(p, { pick: pickChatgptTarget })))(this.#d.port());
14205
+ } catch (e) {
14206
+ this.#connectRetryAt = this.#now() + this.#connectBackoffMs;
14207
+ this.#connectBackoffMs = Math.min(this.#connectBackoffMs * 2, CONNECT_BACKOFF_MAX_MS5);
14208
+ if (!this.#connectFailLogged) {
14209
+ this.#connectFailLogged = true;
14210
+ this.#d.log(`${LABEL} \u6388\u6743\u6865\u8FDE\u4E0D\u4E0A\uFF1A${e?.message ?? e}\uFF08\u5C06\u9000\u907F\u91CD\u8BD5\uFF09`);
14211
+ }
14212
+ return false;
14213
+ }
14214
+ if (this.#stopped) {
14215
+ client.close();
14216
+ return false;
14217
+ }
14218
+ this.#client = client;
14219
+ this.#gen = "";
14220
+ this.#connectBackoffMs = CONNECT_BACKOFF_MIN_MS5;
14221
+ this.#connectRetryAt = 0;
14222
+ this.#connectFailLogged = false;
14223
+ try {
14224
+ await client.call?.("Page.addScriptToEvaluateOnNewDocument", { source: INJECT });
14225
+ } catch {
14226
+ }
14227
+ await this.#eval(INJECT);
14228
+ if (!this.#client) return false;
14229
+ this.#attach();
14230
+ this.#d.log(`${LABEL} \u6388\u6743\u6865\u5DF2\u8FDE\u4E0A\uFF08\u7AEF\u53E3 ${this.#d.port()}\uFF09\uFF0CApp \u91CC Codex \u7684\u5F85\u786E\u8BA4\u53EF\u63A8\u5230\u8BBE\u5907`);
14231
+ return true;
14232
+ }
14233
+ #attach() {
14234
+ this.#channel?.close();
14235
+ this.#channel = this.#d.codex.attachChannel(CHANNEL, {
14236
+ label: LABEL,
14237
+ respond: (threadId, id, result) => void this.#answer(threadId, id, result)
14238
+ });
14239
+ }
14240
+ async #poll() {
14241
+ const r = await this.#eval(DRAIN);
14242
+ if (!this.#client || !r) return;
14243
+ if (r.missing || typeof r.g !== "string") {
14244
+ this.#d.log(`${LABEL} \u9875\u9762\u5237\u65B0\u8FC7\uFF0C\u91CD\u65B0\u6CE8\u5165\u76D1\u542C`);
14245
+ await this.#eval(INJECT);
14246
+ if (!this.#client) return;
14247
+ this.#gen = "";
14248
+ this.#attach();
14249
+ return;
14250
+ }
14251
+ if (this.#gen && r.g !== this.#gen) {
14252
+ this.#d.log(`${LABEL} \u9875\u9762\u5237\u65B0\u8FC7\uFF08\u76D1\u542C\u5DF2\u968F\u65B0\u9875\u9762\u88C5\u4E0A\uFF09\uFF0C\u64A4\u6389\u65E7\u9875\u9762\u4E0A\u7684\u5361`);
14253
+ this.#status.clear();
14254
+ this.#attach();
14255
+ }
14256
+ this.#gen = r.g;
14257
+ if (typeof r.dropped === "number" && r.dropped > 0) this.#d.log(`${LABEL} \u9875\u9762\u6D88\u606F\u961F\u5217\u6EE1\uFF0C\u4E22\u4E86\u6700\u65E7\u7684 ${r.dropped} \u6761`);
14258
+ if (!Array.isArray(r.items)) return;
14259
+ for (const it of r.items) this.#feed(it);
14260
+ }
14261
+ #feed(raw) {
14262
+ const ch = this.#channel;
14263
+ if (!ch || !raw || typeof raw !== "object") return;
14264
+ const m = raw;
14265
+ if (m.type === "mcp-request") {
14266
+ const req = m.request ?? {};
14267
+ const id = req.id;
14268
+ if (typeof req.method !== "string" || !REQUEST_METHODS.includes(req.method)) return;
14269
+ if (typeof id !== "number" && typeof id !== "string") return;
14270
+ ch.onMessage({ id, method: req.method, params: req.params });
14271
+ return;
14272
+ }
14273
+ if (m.type === "mcp-notification" && typeof m.method === "string") {
14274
+ const p = m.params && typeof m.params === "object" ? m.params : {};
14275
+ if (m.method === "thread/status/changed" && typeof p.threadId === "string") {
14276
+ const type2 = (p.status ?? {}).type;
14277
+ if (typeof type2 === "string") this.#status.set(p.threadId, type2);
14278
+ } else if (m.method === "thread/closed" && typeof p.threadId === "string") this.#status.delete(p.threadId);
14279
+ ch.onMessage({ method: m.method, params: p });
14280
+ }
14281
+ }
14282
+ async #answer(threadId, id, result) {
14283
+ const sid = threadId.slice(0, 8);
14284
+ const gen = this.#gen;
14285
+ if (!this.#client || !gen) {
14286
+ this.#d.log(`${LABEL} \u4F1A\u8BDD ${sid}\uFF1A\u6388\u6743\u6865\u5DF2\u65AD\u5F00\uFF0C\u8BBE\u5907\u7B54\u590D\u6CA1\u6709\u53D1\u51FA`);
14287
+ return;
14288
+ }
14289
+ const r = await this.#eval(answerExpr4(gen, threadId, id, result));
14290
+ if (r?.ok) this.#d.log(`${LABEL} \u4F1A\u8BDD ${sid}\uFF1A\u5DF2\u6309\u8BBE\u5907\u7B54\u590D`);
14291
+ else if (r?.why === "resolved") this.#d.log(`${LABEL} \u4F1A\u8BDD ${sid}\uFF1A\u7535\u8111\u4E0A\u5DF2\u5904\u7406\uFF0C\u8BBE\u5907\u7B54\u590D\u6CA1\u6709\u53D1\u51FA`);
14292
+ else if (r?.why === "reloaded") this.#d.log(`${LABEL} \u4F1A\u8BDD ${sid}\uFF1A\u9875\u9762\u5237\u65B0\u8FC7\uFF0C\u8BBE\u5907\u7B54\u590D\u6CA1\u6709\u53D1\u51FA`);
14293
+ else if (r?.why === "no_bridge") this.#d.log(`${LABEL} \u4F1A\u8BDD ${sid}\uFF1A\u9875\u9762\u91CC\u6CA1\u6709 electronBridge.sendMessageFromView\uFF0C\u8BBE\u5907\u7B54\u590D\u6CA1\u6709\u53D1\u51FA`);
14294
+ else this.#d.log(`${LABEL} \u4F1A\u8BDD ${sid}\uFF1A\u8BBE\u5907\u7B54\u590D\u53D1\u9001\u5931\u8D25${r?.err ? `\uFF08${r.err}\uFF09` : ""}`);
14295
+ }
14296
+ async #eval(expr) {
14297
+ const c = this.#client;
14298
+ if (!c) return null;
14299
+ try {
14300
+ return await c.evaluate(expr);
14301
+ } catch (e) {
14302
+ if (c.closed) this.#disconnect();
14303
+ else this.#d.log(`${LABEL} \u9875\u9762\u811A\u672C\u51FA\u9519\uFF1A${e?.message ?? e}`);
14304
+ return null;
14305
+ }
14306
+ }
14307
+ #disconnect() {
14308
+ this.#channel?.close();
14309
+ this.#channel = null;
14310
+ this.#status.clear();
14311
+ this.#gen = "";
14312
+ try {
14313
+ this.#client?.close();
14314
+ } catch {
14315
+ }
14316
+ this.#client = null;
14317
+ }
14318
+ };
14319
+
14320
+ // src/daemon/chatgptApp/keeper.ts
14321
+ import { lstatSync as lstatSync3, readdirSync as readdirSync9 } from "fs";
14322
+ import { join as join20 } from "path";
14323
+ var CHATGPT_DEFAULT_CDP_PORT = 39277;
14324
+ var CHATGPT_DOWN_ROUNDS = 2;
14325
+ var CHATGPT_BRAND = {
14326
+ tool: "chatgpt_app",
14327
+ label: "ChatGPT App",
14328
+ macApp: "/Applications/ChatGPT.app",
14329
+ // 主进程:可执行文件正好是它、只跟 `-` 开头的开关、没有 --type=(排除 helper 与 CodexCLI.app 里的 codex)
14330
+ macExe: "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT",
14331
+ appName: "ChatGPT",
14332
+ // 26.1007 的 Info.plist:CFBundleIdentifier = com.openai.codex
14333
+ bundleId: "com.openai.codex",
14334
+ // package.json productName = Codex(Electron 默认 UA 带 `<应用名>/<版本>`);也认 ChatGPT 以防改名
14335
+ ua: /\b(Codex|ChatGPT)\/\d/,
14336
+ comm: /\/ChatGPT\.app\//,
14337
+ defaultPort: CHATGPT_DEFAULT_CDP_PORT
14338
+ };
14339
+ var CODEX_ACTIVE_MS = 5 * 60 * 1e3;
14340
+ var ROLLOUT_MAX_ENTRIES = 2e4;
14341
+ function codexRolloutsActive(dir2 = join20(codexHome(), "sessions"), now = Date.now()) {
14342
+ let seen = 0;
14343
+ let overflow = false;
14344
+ const walk = (d, depth) => {
14345
+ let names;
14346
+ try {
14347
+ names = readdirSync9(d);
14348
+ } catch {
14349
+ return false;
14350
+ }
14351
+ names.sort().reverse();
14352
+ for (const n of names) {
14353
+ if (++seen > ROLLOUT_MAX_ENTRIES) {
14354
+ overflow = true;
14355
+ return false;
14356
+ }
14357
+ const p = join20(d, n);
14358
+ let st;
14359
+ try {
14360
+ st = lstatSync3(p);
14361
+ } catch {
14362
+ continue;
14363
+ }
14364
+ if (st.isDirectory()) {
14365
+ if (depth < 4 && walk(p, depth + 1)) return true;
14366
+ if (overflow) return false;
14367
+ } else if (n.endsWith(".jsonl") && now - st.mtimeMs < CODEX_ACTIVE_MS) return true;
14368
+ }
14369
+ return false;
14370
+ };
14371
+ return walk(dir2, 0) || overflow;
14372
+ }
14373
+ function chatgptKeeperThrottleFile() {
14374
+ return join20(deskbuddyHome(), "chatgptapp", "keeper.json");
14375
+ }
14376
+ var ChatgptKeeper = class {
14377
+ #d;
14378
+ #inner;
14379
+ #down = 0;
14380
+ #lastResult = "";
14381
+ constructor(deps2) {
14382
+ this.#d = deps2;
14383
+ this.#inner = deps2.inner ?? new QoderCdpKeeper({
14384
+ ...deps2,
14385
+ brand: CHATGPT_BRAND,
14386
+ throttleFile: deps2.throttleFile ?? chatgptKeeperThrottleFile,
14387
+ skipSystem: deps2.skipSystem ?? (() => process.env.DESKBUDDY_SKIP_CHATGPT_CDP === "1"),
14388
+ // 忙:App 页面里有进行中的会话(桥的最近状态)/ Codex hook 报着在跑或等授权 / Codex 会话记录 5 分钟内写过 / 有等设备的卡
14389
+ busy: () => (deps2.busy ? deps2.busy() : toolBusy("codex") || codexRolloutsActive()) || deps2.hasCards?.() === true
14390
+ });
14391
+ }
14392
+ #platform() {
14393
+ return this.#d.platform ?? process.platform;
14394
+ }
14395
+ #skip() {
14396
+ return !this.#d.inner && (this.#d.skipSystem ? this.#d.skipSystem() : process.env.DESKBUDDY_SKIP_CHATGPT_CDP === "1");
14397
+ }
14398
+ #manage() {
14399
+ try {
14400
+ return this.#d.manage ? this.#d.manage() !== false : true;
14401
+ } catch {
14402
+ return false;
14403
+ }
14404
+ }
14405
+ async tick() {
14406
+ if (this.#platform() !== "darwin") return "untrusted";
14407
+ if (this.#skip()) return "down";
14408
+ const st = await this.#inner.tick();
14409
+ if (st !== "down") {
14410
+ this.#down = 0;
14411
+ return st;
14412
+ }
14413
+ this.#down++;
14414
+ if (this.#down >= CHATGPT_DOWN_ROUNDS) await this.#keepOpen().catch((e) => this.#d.log(`ChatGPT App \u8C03\u8BD5\u7AEF\u53E3\uFF1A\u91CD\u5F00\u51FA\u9519 ${e?.message ?? e}`));
14415
+ return "down";
14416
+ }
14417
+ /** App 刚启动(不到 10 秒)没带端口:立即带端口重开一次(Qoder keeper 的同一套,单独限流) */
14418
+ async fixFreshLaunch() {
14419
+ if (this.#platform() !== "darwin" || this.#skip() || !this.#manage()) return "skipped";
14420
+ return this.#inner.fixFreshLaunch();
14421
+ }
14422
+ async #keepOpen() {
14423
+ if (!this.#manage()) return;
14424
+ if (this.#d.hasCards?.() === true) return;
14425
+ if (!await this.#inner.appRunning()) {
14426
+ if (this.#lastResult !== "not_running") {
14427
+ this.#lastResult = "not_running";
14428
+ this.#d.log(`ChatGPT App \u6CA1\u5728\u8FD0\u884C\uFF08\u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u4E0D\u5728\uFF09\uFF0CApp \u91CC Codex \u7684\u5F85\u786E\u8BA4\u63A8\u4E0D\u5230\u8BBE\u5907`);
14429
+ }
14430
+ return;
14431
+ }
14432
+ const r = await this.#inner.wakeForTask("keep_open");
14433
+ if (r === "restarted") this.#down = 0;
14434
+ if (r === this.#lastResult) return;
14435
+ this.#lastResult = r;
14436
+ const why = {
14437
+ hung: "\u7AEF\u53E3\u5728\u76D1\u542C\u4F46\u65E0\u54CD\u5E94\uFF08App \u53EF\u80FD\u5361\u4F4F\u4E86\uFF09\uFF0C\u6309\u5361\u6B7B\u7B56\u7565\u5904\u7406",
14438
+ restarted: "\u5DF2\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u5F00",
14439
+ busy: "\u6709\u4F1A\u8BDD\u5728\u8DD1\u6216\u6709\u7B49\u8BBE\u5907\u7684\u5361\uFF0C\u7B49\u7A7A\u95F2\u518D\u91CD\u5F00",
14440
+ present: "\u7528\u6237\u5728\u7535\u8111\u524D\uFF0C\u4E0D\u6253\u65AD\uFF08ChatGPT App \u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF0CApp \u91CC Codex \u7684\u5F85\u786E\u8BA4\u63A8\u4E0D\u5230\u8BBE\u5907\uFF09",
14441
+ failed: "\u91CD\u5F00\u5931\u8D25\u6216\u4ECA\u5929\u7684\u91CD\u5F00\u6B21\u6570\u5DF2\u7528\u5B8C",
14442
+ launched: "\u521A\u62C9\u8D77\u8FC7\uFF0C\u7B49\u5B83\u8D77\u6765",
14443
+ not_installed: "\u6CA1\u627E\u5230 App \u672C\u4F53"
14444
+ };
14445
+ if (why[r]) this.#d.log(`ChatGPT App \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u4E0D\u5728\uFF1A${why[r]}`);
14446
+ }
14447
+ };
14448
+
13829
14449
  // src/core/codexRemote.ts
13830
14450
  import { mkdirSync as mkdirSync10, renameSync as renameSync7, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
13831
14451
  import { join as join21 } from "path";
@@ -13849,7 +14469,7 @@ function removeCodexRemote() {
13849
14469
  }
13850
14470
 
13851
14471
  // src/core/interrupt.ts
13852
- import { closeSync as closeSync8, openSync as openSync8, readdirSync as readdirSync9, readSync as readSync7, statSync as statSync11 } from "fs";
14472
+ import { closeSync as closeSync8, openSync as openSync8, readdirSync as readdirSync10, readSync as readSync7, statSync as statSync11 } from "fs";
13853
14473
  import { dirname as dirname6, join as join22 } from "path";
13854
14474
  var INTERRUPT_TOOLS = ["claude_code", "qoder_cn", "qoder", "qwenwork"];
13855
14475
  var CODEX_INTERRUPT_TOOL = "codex";
@@ -13963,7 +14583,7 @@ function claudeTranscriptFile(sessionId, cwd, home = claudeHome()) {
13963
14583
  }
13964
14584
  let dirs = [];
13965
14585
  try {
13966
- dirs = readdirSync9(join22(home, "projects"));
14586
+ dirs = readdirSync10(join22(home, "projects"));
13967
14587
  } catch {
13968
14588
  return null;
13969
14589
  }
@@ -14960,7 +15580,7 @@ function resultSummary(tool, stdout6) {
14960
15580
  }
14961
15581
 
14962
15582
  // src/core/dshMailbox.ts
14963
- import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync13, renameSync as renameSync9, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
15583
+ import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync12, readdirSync as readdirSync11, readFileSync as readFileSync13, renameSync as renameSync9, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
14964
15584
  import { join as join27 } from "path";
14965
15585
  var DSH_HEARTBEAT_FRESH_MS = 5 * 1e3;
14966
15586
  var DSH_ACK_TIMEOUT_MS = 8 * 1e3;
@@ -15006,7 +15626,7 @@ function liveHeartbeats(sessionId, d0 = {}) {
15006
15626
  const dir2 = sub(d.home, "alive", d.channel);
15007
15627
  let names;
15008
15628
  try {
15009
- names = readdirSync10(dir2);
15629
+ names = readdirSync11(dir2);
15010
15630
  } catch {
15011
15631
  return [];
15012
15632
  }
@@ -15131,7 +15751,7 @@ function dshClearAcks(home = deskbuddyHome(), channel = "dsh") {
15131
15751
  function clearDir(inbox) {
15132
15752
  let names;
15133
15753
  try {
15134
- names = readdirSync10(inbox);
15754
+ names = readdirSync11(inbox);
15135
15755
  } catch {
15136
15756
  return 0;
15137
15757
  }
@@ -15305,6 +15925,12 @@ var Daemon = class {
15305
15925
  #wakeReport = null;
15306
15926
  /** url → 连着它的 CodexWatch。插入顺序 = 发现顺序(DeskBuddy 管的在前) */
15307
15927
  #codex = /* @__PURE__ */ new Map();
15928
+ /** Codex 待确认去重(threadId:itemId):ChatGPT App 页面桥与各 ws app-server 共用一份,同一条只推一张 */
15929
+ #codexClaims = new CodexClaims();
15930
+ /** ChatGPT 桌面 App 里 Codex 的待确认(spec 2026-10-12 chatgpt-app-bridge):不连 ws,只挂页面桥这条额外通道 */
15931
+ #chatgptCodex;
15932
+ #chatgptApp;
15933
+ #chatgptAppTimer = null;
15308
15934
  /** 千问办公的选择题 / 授权推设备(spec 2026-10-03 §3.1):hook 发 notify 进来,回填走 App 内置 MCP */
15309
15935
  #qwenwork;
15310
15936
  /** 千问办公 App 自弹的确认框(spec 2026-10-10 qwenwork-newtask-appcards §4);与 #qwenwork 共用认领表,一张确认只推一张 */
@@ -15342,6 +15968,9 @@ var Daemon = class {
15342
15968
  /** Qoder / Qoder CN 桌面 App 里的选择题推设备(spec 2026-10-11 qoder-app-question),每品牌一份 */
15343
15969
  #qoderQuestions = /* @__PURE__ */ new Map();
15344
15970
  #qoderLnkAt = 0;
15971
+ #qwLnkAt = 0;
15972
+ /** 千问办公新启动后补快捷方式(App 启动约 1 秒后会清空自己快捷方式的参数,Bug 表 274) */
15973
+ #qwRelaunch = null;
15345
15974
  #winLocateTimer = null;
15346
15975
  #interruptTimer = null;
15347
15976
  /** 会话 → transcript 路径(打断扫描用,找一次记住) */
@@ -15414,6 +16043,7 @@ var Daemon = class {
15414
16043
  ocLive: deps2.ocLive ?? ((sid) => ocLiveFor(sid) != null),
15415
16044
  appReply: deps2.appReply ?? ((tool, sid, cwd, text, exp) => tool === "dumate" ? this.#dumate.deliverReply(sid, text, exp) : this.#qwenwork.reply(cwd, text, sid)),
15416
16045
  appNewTask: deps2.appNewTask ?? ((_tool, id, text, dirs) => this.#qwenwork.newTask(id, text, dirs)),
16046
+ qwPortMissing: deps2.qwPortMissing ?? (() => this.#qwApp.portMissing()),
15417
16047
  dumateNewTask: deps2.dumateNewTask ?? ((cwd, text, iid, exp) => this.#dumate.startTask(cwd, text, iid, exp)),
15418
16048
  dumateNewTaskEnabled: deps2.dumateNewTaskEnabled ?? (() => process.platform === "win32" || this.#dmCdp.target() !== null),
15419
16049
  ocDeliver: deps2.ocDeliver ?? ((sid, msg) => ocDeliver(sid, msg)),
@@ -15542,6 +16172,30 @@ var Daemon = class {
15542
16172
  },
15543
16173
  port: () => loadConfig().workbuddyCdpPort ?? DEFAULT_CDP_PORT
15544
16174
  });
16175
+ this.#chatgptCodex = new CodexWatch({
16176
+ gate: (req, signal) => this.#coord.onGate(req, signal),
16177
+ log,
16178
+ discover: () => null,
16179
+ // MCP 授权 Codex 不触发 hook:同 ws 那一路替它报待确认
16180
+ report: (input) => runCodexHook(this.#deps.nodePath, input),
16181
+ claims: this.#codexClaims
16182
+ });
16183
+ const chatgptPort = () => loadConfig().chatgptCdpPort ?? CHATGPT_DEFAULT_CDP_PORT;
16184
+ this.#chatgptApp = new ChatgptAppBridge({
16185
+ log,
16186
+ // 一期只做 macOS;卡片走 codex,须启用 codex
16187
+ enabled: () => process.platform === "darwin" && loadConfig().enabledTools.includes("codex"),
16188
+ port: chatgptPort,
16189
+ codex: this.#chatgptCodex,
16190
+ keeper: new ChatgptKeeper({
16191
+ port: chatgptPort,
16192
+ log,
16193
+ manage: () => loadConfig().chatgptAppPort !== false,
16194
+ // 端口不通时页面状态看不到:再看 Codex hook 状态与会话记录的修改时间(App 里 hook 没被信任时只有后者)
16195
+ busy: () => this.#chatgptApp.busy() || toolBusy("codex") || codexRolloutsActive(),
16196
+ hasCards: () => this.#chatgptApp.pending() > 0
16197
+ })
16198
+ });
15545
16199
  this.#wbTasks = deps2.wbAppTasks ?? new WbAppTasks({
15546
16200
  eval: (expr) => this.#wbApp.appEval(expr),
15547
16201
  available: () => this.#wbApp.available(),
@@ -15654,7 +16308,24 @@ var Daemon = class {
15654
16308
  this.#dumateTimer.unref();
15655
16309
  this.#wbAppTimer = setInterval(() => void this.#wbApp.tick(), WB_APP_WATCH_MS);
15656
16310
  this.#wbAppTimer.unref();
15657
- this.#qwAppTimer = setInterval(() => void this.#qwApp.tick(), WB_APP_WATCH_MS);
16311
+ this.#chatgptAppTimer = setInterval(() => void this.#chatgptApp.tick(), WB_APP_WATCH_MS);
16312
+ this.#chatgptAppTimer.unref();
16313
+ const qwPidKeeper = new QwenworkKeeper({ port: () => loadConfig().qwenworkCdpPort ?? QW_DEFAULT_CDP_PORT, log });
16314
+ this.#qwRelaunch = new QwShortcutRelaunchFixer({
16315
+ now: () => this.#deps.now(),
16316
+ wanted: qwenworkShortcutsWanted,
16317
+ mainPid: () => qwPidKeeper.winMainPid(),
16318
+ ensure: () => ensureQwenworkShortcuts({ log })
16319
+ });
16320
+ this.#qwAppTimer = setInterval(() => {
16321
+ void this.#qwApp.tick();
16322
+ const now = this.#deps.now();
16323
+ if (now - this.#qwLnkAt >= QW_SHORTCUT_EVERY_MS) {
16324
+ this.#qwLnkAt = now;
16325
+ void ensureQwenworkShortcuts({ log }).catch(() => void 0);
16326
+ }
16327
+ void this.#qwRelaunch?.tick().catch(() => void 0);
16328
+ }, WB_APP_WATCH_MS);
15658
16329
  this.#qwAppTimer.unref();
15659
16330
  this.#cozeAppTimer = setInterval(() => void this.#cozeApp.tick(), WB_APP_WATCH_MS);
15660
16331
  this.#cozeAppTimer.unref();
@@ -15684,6 +16355,7 @@ var Daemon = class {
15684
16355
  if (ids.length === 0) return;
15685
16356
  void winAppLocator(log).refresh(ids).then(() => {
15686
16357
  for (const tool of ["qoder_cn", "qoder"]) if (ids.includes(tool)) void ensureQoderShortcuts(tool, { log }).catch(() => void 0);
16358
+ if (ids.includes("qwenwork")) void ensureQwenworkShortcuts({ log }).catch(() => void 0);
15687
16359
  }).catch(() => void 0);
15688
16360
  };
15689
16361
  locateApps();
@@ -15763,7 +16435,8 @@ var Daemon = class {
15763
16435
  onLoadedChange: () => this.#onCodexLoaded(),
15764
16436
  // MCP 授权 Codex 不触发 hook:替它报 PermissionRequest,屏上显示待确认(Bug 表 162 / 164)
15765
16437
  report: (input) => runCodexHook(this.#deps.nodePath, input),
15766
- onIdle: (tid) => this.#codexIdle(tid, w)
16438
+ onIdle: (tid) => this.#codexIdle(tid, w),
16439
+ claims: this.#codexClaims
15767
16440
  })
15768
16441
  );
15769
16442
  }
@@ -16053,6 +16726,9 @@ var Daemon = class {
16053
16726
  if (this.#dumateTimer) clearInterval(this.#dumateTimer);
16054
16727
  if (this.#dmCdpTimer) clearInterval(this.#dmCdpTimer);
16055
16728
  if (this.#wbAppTimer) clearInterval(this.#wbAppTimer);
16729
+ if (this.#chatgptAppTimer) clearInterval(this.#chatgptAppTimer);
16730
+ this.#chatgptApp.stop();
16731
+ this.#chatgptCodex.stop();
16056
16732
  if (this.#qwAppTimer) clearInterval(this.#qwAppTimer);
16057
16733
  this.#qwApp.stop();
16058
16734
  if (this.#cozeAppTimer) clearInterval(this.#cozeAppTimer);
@@ -16999,6 +17675,15 @@ var Daemon = class {
16999
17675
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5728\u5343\u95EE\u529E\u516C\u91CC\u5EFA\u4EFB\u52A1\u51FA\u9519\uFF1A${e.message}`);
17000
17676
  r = { status: "failed", reason: "window_failed", message: e.message };
17001
17677
  }
17678
+ if (r.status === "not_sent" && r.reason === APP_NOT_RUNNING) {
17679
+ let missing = false;
17680
+ try {
17681
+ missing = await this.#deps.qwPortMissing();
17682
+ } catch {
17683
+ missing = false;
17684
+ }
17685
+ if (missing) r = { status: "not_sent", reason: APP_PORT_MISSING, message: "\u5343\u95EE\u529E\u516C\u5F00\u7740\uFF0C\u4F46\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF08\u6216\u8C03\u8BD5\u7AEF\u53E3\u7684\u76D1\u542C\u8005\u4E0D\u5BF9\uFF09\uFF0C\u6CA1\u5EFA\u4EFB\u52A1" };
17686
+ }
17002
17687
  if (r.status !== "started") {
17003
17688
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6CA1\u80FD\u5728\u5343\u95EE\u529E\u516C\u91CC\u5EFA\u4EFB\u52A1\uFF1A${r.message}`);
17004
17689
  if (r.status === "not_sent") this.#report(cmd, "", "rejected", "", r.reason);
@@ -17072,7 +17757,7 @@ var Daemon = class {
17072
17757
  const enabled = new Set(loadConfig().enabledTools);
17073
17758
  let names = [];
17074
17759
  try {
17075
- names = readdirSync11(stateDir());
17760
+ names = readdirSync12(stateDir());
17076
17761
  } catch {
17077
17762
  return out;
17078
17763
  }
@@ -19190,7 +19875,7 @@ function runDirect(real2, args, env = process.env) {
19190
19875
 
19191
19876
  // src/wrap/codexLaunch.ts
19192
19877
  import { execFileSync as execFileSync4, spawn as spawn4 } from "child_process";
19193
- import { closeSync as closeSync9, mkdirSync as mkdirSync14, openSync as openSync9, readdirSync as readdirSync12, readFileSync as readFileSync16, rmSync as rmSync14, statSync as statSync15, truncateSync as truncateSync2, writeFileSync as writeFileSync14, renameSync as renameSync10 } from "fs";
19878
+ import { closeSync as closeSync9, mkdirSync as mkdirSync14, openSync as openSync9, readdirSync as readdirSync13, readFileSync as readFileSync16, rmSync as rmSync14, statSync as statSync15, truncateSync as truncateSync2, writeFileSync as writeFileSync14, renameSync as renameSync10 } from "fs";
19194
19879
  import { request as request2 } from "http";
19195
19880
  import { createServer as createServer5 } from "net";
19196
19881
  import { join as join30 } from "path";
@@ -19475,7 +20160,7 @@ function removeRecord(rec) {
19475
20160
  function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs, kill = (pid) => killServer(pid)) {
19476
20161
  let names = [];
19477
20162
  try {
19478
- names = readdirSync12(dir2).filter((n) => n.endsWith(".json"));
20163
+ names = readdirSync13(dir2).filter((n) => n.endsWith(".json"));
19479
20164
  } catch {
19480
20165
  return;
19481
20166
  }