deskbuddy 0.2.0-beta.1 → 0.2.0-beta.2

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
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/daemon.ts
4
- import { writeFileSync as writeFileSync11, rmSync as rmSync4 } from "fs";
4
+ import { writeFileSync as writeFileSync13, rmSync as rmSync7 } from "fs";
5
5
 
6
6
  // src/core/config.ts
7
7
  import { randomUUID } from "crypto";
8
8
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
9
9
  import { homedir, hostname } from "os";
10
10
  import { join } from "path";
11
- function defaultRemoteCwd(home = homedir()) {
12
- return join(home, "Desktop");
11
+ function defaultRemoteCwd(home = homedir(), platform = process.platform, exists = existsSync) {
12
+ const desktop = join(home, "Desktop");
13
+ if (platform === "win32" && !exists(desktop)) {
14
+ const od = join(home, "OneDrive", "Desktop");
15
+ if (exists(od)) return od;
16
+ }
17
+ return desktop;
13
18
  }
14
19
  var PROD_API_BASE = "https://api.shifengintelli.com";
15
20
  var TEST_API_BASE = "https://test-api.shifengintelli.com";
@@ -83,7 +88,8 @@ function loadConfig() {
83
88
  boundApiBase: parsed.boundApiBase,
84
89
  remoteCwd: parsed.remoteCwd,
85
90
  // 不认识的值一律回落默认,不能让一个手滑写错的配置变成"全放行"或"全拦"
86
- approvalPolicy: parsed.approvalPolicy === "all" ? "all" : parsed.approvalPolicy === "commands" ? "commands" : void 0
91
+ approvalPolicy: parsed.approvalPolicy === "all" ? "all" : parsed.approvalPolicy === "commands" ? "commands" : void 0,
92
+ codexAppServer: typeof parsed.codexAppServer === "string" ? parsed.codexAppServer : void 0
87
93
  };
88
94
  } catch {
89
95
  return fallback;
@@ -105,12 +111,124 @@ function ensureClientId() {
105
111
  }
106
112
 
107
113
  // src/daemon/autostart.ts
108
- import { execFile } from "child_process";
109
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
114
+ import { execFile, spawn } from "child_process";
115
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
110
116
  import { homedir as homedir2 } from "os";
111
117
  import { fileURLToPath } from "url";
112
118
  import { basename, dirname, join as join2 } from "path";
113
119
  import { promisify } from "util";
120
+
121
+ // src/core/winproc.ts
122
+ import { execFileSync } from "child_process";
123
+ function winSplitArgs(cmdline) {
124
+ const out = [];
125
+ let cur = "";
126
+ let inQ = false;
127
+ let has = false;
128
+ const s = cmdline || "";
129
+ for (let i = 0; i < s.length; i++) {
130
+ const c = s[i];
131
+ if (c === "\\") {
132
+ let n = 0;
133
+ while (s[i + n] === "\\") n++;
134
+ if (s[i + n] === '"') {
135
+ cur += "\\".repeat(Math.floor(n / 2));
136
+ if (n % 2 === 1) cur += '"';
137
+ else inQ = !inQ;
138
+ i += n;
139
+ has = true;
140
+ } else {
141
+ cur += "\\".repeat(n);
142
+ i += n - 1;
143
+ has = true;
144
+ }
145
+ } else if (c === '"') {
146
+ inQ = !inQ;
147
+ has = true;
148
+ } else if (!inQ && (c === " " || c === " ")) {
149
+ if (has) out.push(cur);
150
+ cur = "";
151
+ has = false;
152
+ } else {
153
+ cur += c;
154
+ has = true;
155
+ }
156
+ }
157
+ if (has) out.push(cur);
158
+ return out;
159
+ }
160
+ var SCRIPT_HOSTS = /* @__PURE__ */ new Set(["node", "bun", "deno"]);
161
+ function stripExe(name) {
162
+ return name.replace(/\.exe$/i, "");
163
+ }
164
+ function lastTwo(p) {
165
+ return p.split(/[\\/]+/).filter(Boolean).slice(-2).join("/");
166
+ }
167
+ function hostedScript(argv) {
168
+ for (const a of argv.slice(1)) if (!a.startsWith("-")) return a;
169
+ return "";
170
+ }
171
+ function winEffectiveName(name, cmdline) {
172
+ if (!SCRIPT_HOSTS.has(stripExe(name).toLowerCase())) return name;
173
+ const script = hostedScript(winSplitArgs(cmdline));
174
+ return script ? lastTwo(script) : name;
175
+ }
176
+ function winNormalizedArgs(name, cmdline) {
177
+ let argv = winSplitArgs(cmdline);
178
+ if (argv.length === 0) return "";
179
+ if (SCRIPT_HOSTS.has(stripExe(name).toLowerCase())) {
180
+ const script = hostedScript(argv);
181
+ if (script) argv = argv.slice(argv.indexOf(script));
182
+ }
183
+ return argv.map((a) => a.replace(/\s+/g, "_").replace(/\\/g, "/")).join(" ");
184
+ }
185
+ var WIN_PS_SCRIPT = "[Console]::OutputEncoding=[Text.Encoding]::UTF8;Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
186
+ function parseWinProcJson(json) {
187
+ let d;
188
+ try {
189
+ d = JSON.parse(json.replace(/^\uFEFF/, ""));
190
+ } catch {
191
+ return [];
192
+ }
193
+ const list = Array.isArray(d) ? d : d ? [d] : [];
194
+ const out = [];
195
+ for (const r of list) {
196
+ const pid = Number(r?.ProcessId);
197
+ if (!Number.isInteger(pid) || pid <= 0) continue;
198
+ const rawName = typeof r.Name === "string" ? r.Name : "";
199
+ const cmdline = typeof r.CommandLine === "string" ? r.CommandLine.replace(/[\r\n]+/g, " ") : "";
200
+ out.push({
201
+ pid,
202
+ ppid: Number(r.ParentProcessId) || 0,
203
+ name: winEffectiveName(rawName, cmdline),
204
+ args: winNormalizedArgs(rawName, cmdline),
205
+ cmdline
206
+ });
207
+ }
208
+ return out;
209
+ }
210
+ var CACHE_MS = 1500;
211
+ var FAIL_CACHE_MS = 1e4;
212
+ var cache = null;
213
+ function winProcs(now = Date.now()) {
214
+ if (cache && now - cache.at < (cache.procs ? CACHE_MS : FAIL_CACHE_MS)) return cache.procs;
215
+ try {
216
+ const out = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WIN_PS_SCRIPT], {
217
+ encoding: "utf8",
218
+ stdio: ["ignore", "pipe", "ignore"],
219
+ maxBuffer: 32 * 1024 * 1024,
220
+ windowsHide: true,
221
+ timeout: 15e3
222
+ });
223
+ const procs = parseWinProcJson(out);
224
+ cache = { at: now, procs: procs.length > 0 ? procs : null };
225
+ } catch {
226
+ cache = { at: now, procs: null };
227
+ }
228
+ return cache.procs;
229
+ }
230
+
231
+ // src/daemon/autostart.ts
114
232
  var run = promisify(execFile);
115
233
  var LAUNCH_LABEL = "com.deskbuddy.daemon";
116
234
  var SYSTEMD_UNIT = "deskbuddy.service";
@@ -157,6 +275,49 @@ RestartSec=10
157
275
  WantedBy=default.target
158
276
  `;
159
277
  }
278
+ function winStartupVbsPath(appData = process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming")) {
279
+ return join2(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "DeskBuddyDaemon.vbs");
280
+ }
281
+ function vbsQuote(s) {
282
+ return '"' + s.replace(/"/g, '""') + '"';
283
+ }
284
+ function winStartupVbs(t) {
285
+ const cmd = `"${t.execPath}" "${t.cliPath}" daemon`;
286
+ return [
287
+ "' DeskBuddy \u5B88\u62A4\u8FDB\u7A0B\u5F00\u673A\u81EA\u542F\u3002deskbuddy disable / logout \u4F1A\u5220\u6389\u5B83",
288
+ `CreateObject("WScript.Shell").Run ${vbsQuote(cmd)}, 0, False`,
289
+ ""
290
+ ].join("\r\n");
291
+ }
292
+ function isOurDaemonCmdline(cmdline) {
293
+ const argv = winSplitArgs(cmdline);
294
+ const i = argv.findIndex((a) => /(?:^|[\\/])deskbuddy[\\/]dist[\\/]cli\.js$/i.test(a));
295
+ return i >= 1 && argv[i + 1] === "daemon";
296
+ }
297
+ async function stopPidFileDaemon() {
298
+ let pid = 0;
299
+ try {
300
+ pid = Number(readFileSync2(pidFilePath(), "utf8").trim());
301
+ } catch {
302
+ return;
303
+ }
304
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return;
305
+ const p = winProcs()?.find((x) => x.pid === pid);
306
+ if (!p || !isOurDaemonCmdline(p.cmdline)) return;
307
+ try {
308
+ process.kill(pid);
309
+ } catch {
310
+ return;
311
+ }
312
+ for (let i = 0; i < 30; i++) {
313
+ try {
314
+ process.kill(pid, 0);
315
+ } catch {
316
+ return;
317
+ }
318
+ await new Promise((r) => setTimeout(r, 100));
319
+ }
320
+ }
160
321
  function launchAgentPath(home = homedir2()) {
161
322
  return join2(home, "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
162
323
  }
@@ -195,19 +356,14 @@ async function installAutostart(t, platform = process.platform) {
195
356
  return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08systemd --user\uFF09" };
196
357
  }
197
358
  if (platform === "win32") {
198
- await run("schtasks", [
199
- "/create",
200
- "/f",
201
- "/sc",
202
- "onlogon",
203
- "/rl",
204
- "LIMITED",
205
- "/tn",
206
- SCHTASK_NAME,
207
- "/tr",
208
- `"${t.execPath}" "${t.cliPath}" daemon`
209
- ]);
210
- return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08\u8BA1\u5212\u4EFB\u52A1\uFF09" };
359
+ const p = winStartupVbsPath();
360
+ mkdirSync2(dirname(p), { recursive: true });
361
+ writeFileSync2(p, "\uFEFF" + winStartupVbs(t), "utf16le");
362
+ await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME], { windowsHide: true }).catch(() => {
363
+ });
364
+ await stopPidFileDaemon();
365
+ spawn("wscript.exe", [p], { detached: true, stdio: "ignore", windowsHide: true }).unref();
366
+ return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08\u300C\u542F\u52A8\u300D\u6587\u4EF6\u5939\uFF09" };
211
367
  }
212
368
  return { ok: false, message: `\u6682\u4E0D\u652F\u6301\u5728 ${platform} \u4E0A\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF0C\u8BF7\u81EA\u884C\u5E38\u9A7B\u8FD0\u884C deskbuddy daemon` };
213
369
  } catch (e) {
@@ -233,8 +389,11 @@ async function uninstallAutostart(platform = process.platform) {
233
389
  return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
234
390
  }
235
391
  if (platform === "win32") {
236
- await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME]).catch(() => {
392
+ await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME], { windowsHide: true }).catch(() => {
237
393
  });
394
+ const p = winStartupVbsPath();
395
+ if (existsSync2(p)) rmSync(p, { force: true });
396
+ await stopPidFileDaemon();
238
397
  return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
239
398
  }
240
399
  return { ok: true, message: "" };
@@ -253,14 +412,16 @@ import { basename as basename6 } from "path";
253
412
  import WebSocket2 from "ws";
254
413
 
255
414
  // src/adapters/ocPlugin.ts
256
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
415
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
257
416
  import { dirname as dirname2, join as join3 } from "path";
417
+ import { pathToFileURL } from "url";
258
418
  var OC_MARKER = "// deskbuddy:opencode-plugin";
259
- function ocPluginSource(pluginModule, nodePath) {
419
+ function ocPluginSource(pluginModule, nodePath, platform = process.platform) {
420
+ const spec = platform === "win32" ? pathToFileURL(pluginModule).href : pluginModule;
260
421
  return [
261
422
  OC_MARKER,
262
423
  "// \u7531 deskbuddy enable opencode \u751F\u6210\uFF0Cdeskbuddy disable opencode \u4F1A\u5220\u6389\u5B83\u3002",
263
- `import { make } from ${JSON.stringify(pluginModule)}`,
424
+ `import { make } from ${JSON.stringify(spec)}`,
264
425
  `export const DeskBuddy = make({ node: ${JSON.stringify(nodePath)} })`,
265
426
  ""
266
427
  ].join("\n");
@@ -269,7 +430,7 @@ function ocPluginModule(hookPath) {
269
430
  return join3(dirname2(hookPath), "opencode.mjs");
270
431
  }
271
432
  function installOcPlugin(pluginPath, hookPath, nodePath) {
272
- if (existsSync3(pluginPath) && !readFileSync2(pluginPath, "utf8").includes(OC_MARKER)) {
433
+ if (existsSync3(pluginPath) && !readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) {
273
434
  throw new Error(`${pluginPath} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F DeskBuddy \u751F\u6210\u7684\uFF0C\u8BF7\u5148\u632A\u8D70\u518D\u91CD\u8BD5`);
274
435
  }
275
436
  mkdirSync3(dirname2(pluginPath), { recursive: true });
@@ -279,13 +440,13 @@ function installOcPlugin(pluginPath, hookPath, nodePath) {
279
440
  }
280
441
  function uninstallOcPlugin(pluginPath) {
281
442
  try {
282
- if (readFileSync2(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
443
+ if (readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
283
444
  } catch {
284
445
  }
285
446
  }
286
447
  function isOcPluginInstalled(pluginPath) {
287
448
  try {
288
- return readFileSync2(pluginPath, "utf8").includes(OC_MARKER);
449
+ return readFileSync3(pluginPath, "utf8").includes(OC_MARKER);
289
450
  } catch {
290
451
  return false;
291
452
  }
@@ -293,14 +454,16 @@ function isOcPluginInstalled(pluginPath) {
293
454
  var OC_NOTICE = " \u5DF2\u5F00\u7740\u7684 opencode \u7A97\u53E3\u91CD\u5F00\u540E\u751F\u6548";
294
455
 
295
456
  // src/core/state.ts
296
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
457
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
297
458
  import { join as join4 } from "path";
298
459
 
299
460
  // src/core/process.ts
300
- import { execFileSync } from "child_process";
461
+ import { execFileSync as execFileSync2 } from "child_process";
462
+ var WIN = process.platform === "win32";
301
463
  function processName(pid) {
464
+ if (WIN) return winProcs()?.find((p) => p.pid === pid)?.name ?? null;
302
465
  try {
303
- const out = execFileSync("ps", ["-p", String(pid), "-o", "comm="], {
466
+ const out = execFileSync2("ps", ["-p", String(pid), "-o", "comm="], {
304
467
  encoding: "utf8",
305
468
  stdio: ["ignore", "pipe", "ignore"]
306
469
  });
@@ -318,6 +481,12 @@ function isAlive(pid, expectName) {
318
481
  return false;
319
482
  }
320
483
  if (!expectName) return true;
484
+ if (WIN) {
485
+ const procs = winProcs();
486
+ if (!procs) return true;
487
+ const p = procs.find((x) => x.pid === pid);
488
+ return !!p && p.name.toLowerCase().includes(expectName.toLowerCase());
489
+ }
321
490
  const name = processName(pid);
322
491
  return name != null && name.toLowerCase().includes(expectName.toLowerCase());
323
492
  }
@@ -332,7 +501,7 @@ function statePath(sessionId) {
332
501
  }
333
502
  function readState(sessionId) {
334
503
  try {
335
- return JSON.parse(readFileSync3(statePath(sessionId), "utf8"));
504
+ return JSON.parse(readFileSync4(statePath(sessionId), "utf8"));
336
505
  } catch {
337
506
  return null;
338
507
  }
@@ -350,7 +519,7 @@ function readAliveStates(tool) {
350
519
  if (!file.endsWith(".json")) continue;
351
520
  let s;
352
521
  try {
353
- s = JSON.parse(readFileSync3(join4(stateDir(), file), "utf8"));
522
+ s = JSON.parse(readFileSync4(join4(stateDir(), file), "utf8"));
354
523
  } catch {
355
524
  rmSync2(join4(stateDir(), file), { force: true });
356
525
  continue;
@@ -383,7 +552,7 @@ var ALIAS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
383
552
  function resolveAlias(sessionId, now = Date.now()) {
384
553
  let map = {};
385
554
  try {
386
- map = JSON.parse(readFileSync3(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
555
+ map = JSON.parse(readFileSync4(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
387
556
  } catch {
388
557
  return sessionId;
389
558
  }
@@ -397,13 +566,13 @@ function resolveAlias(sessionId, now = Date.now()) {
397
566
  }
398
567
 
399
568
  // src/core/api.ts
400
- import { execFileSync as execFileSync2 } from "child_process";
569
+ import { execFileSync as execFileSync3 } from "child_process";
401
570
  import { createHash, randomBytes, randomUUID as randomUUID2 } from "crypto";
402
- import { readFileSync as readFileSync5 } from "fs";
571
+ import { readFileSync as readFileSync6 } from "fs";
403
572
  import { release } from "os";
404
573
 
405
574
  // src/core/hookFile.ts
406
- import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
575
+ import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
407
576
  import { createRequire } from "module";
408
577
  import { dirname as dirname3, join as join5 } from "path";
409
578
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -438,7 +607,7 @@ function hookVersionPath(target = installedHookPath()) {
438
607
  }
439
608
  function installedHookVersion(target = installedHookPath()) {
440
609
  try {
441
- return readFileSync4(hookVersionPath(target), "utf8").trim() || null;
610
+ return readFileSync5(hookVersionPath(target), "utf8").trim() || null;
442
611
  } catch {
443
612
  return null;
444
613
  }
@@ -482,6 +651,8 @@ var SERVICES = {
482
651
  confirmAuth: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyconfirmauthapi"
483
652
  };
484
653
  var DIRS_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyclientdirsreportapi";
654
+ var INSTALL_REDEEM_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyinstallredeemapi";
655
+ var CMD_RESULT_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddycommandresultapi";
485
656
  var APPROVAL_SERVICES = {
486
657
  create: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcreateapi",
487
658
  cancel: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcancelapi",
@@ -548,12 +719,12 @@ function humanOs(platform = process.platform) {
548
719
  return known[platform] ?? platform;
549
720
  }
550
721
  var defaultOsVersionDeps = {
551
- runSwVers: () => execFileSync2("sw_vers", ["-productVersion"], {
722
+ runSwVers: () => execFileSync3("sw_vers", ["-productVersion"], {
552
723
  encoding: "utf8",
553
724
  stdio: ["ignore", "pipe", "ignore"],
554
725
  timeout: 2e3
555
726
  }),
556
- readOsRelease: () => readFileSync5("/etc/os-release", "utf8"),
727
+ readOsRelease: () => readFileSync6("/etc/os-release", "utf8"),
557
728
  kernelRelease: () => release()
558
729
  };
559
730
  function productOsVersion(platform = process.platform, deps = defaultOsVersionDeps) {
@@ -619,7 +790,10 @@ var api = {
619
790
  approvalCreate: (payload) => call(APPROVAL_SERVICES.create, payload),
620
791
  approvalCancel: (approvalId, reason) => call(APPROVAL_SERVICES.cancel, { approvalId, reason }),
621
792
  approvalSync: (approvalIds) => call(APPROVAL_SERVICES.sync, { approvalIds }),
622
- dirsReport: (payload) => call(DIRS_SERVICE, payload)
793
+ dirsReport: (payload) => call(DIRS_SERVICE, payload),
794
+ installRedeem: (installCode, clientId, hostName) => call(INSTALL_REDEEM_SERVICE, { installCode, clientId, hostName, ...machineInfo() }),
795
+ // 2 秒超时、失败不重试(spec §4.1):回报丢了只是设备少一句提示,任务状态仍由会话快照负责
796
+ commandResult: (payload) => call(CMD_RESULT_SERVICE, payload, 2e3)
623
797
  };
624
798
 
625
799
  // src/core/report.ts
@@ -634,7 +808,8 @@ async function postReport(payload) {
634
808
  import { basename as basename2 } from "path";
635
809
  var KEEPALIVE_MS = 2 * 60 * 1e3;
636
810
  var MAX_SESSIONS = 20;
637
- function buildSnapshot(states, tool, cfg, nowMs) {
811
+ var CAPS = ["cmd_result", "window_inject"];
812
+ function buildSnapshot(states, tool, cfg, nowMs, remote) {
638
813
  const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => ({
639
814
  sessionId: s.sessionId,
640
815
  title: s.title,
@@ -643,14 +818,15 @@ function buildSnapshot(states, tool, cfg, nowMs) {
643
818
  state: s.state,
644
819
  startedAt: s.startedAt,
645
820
  updatedAt: s.updatedAt,
646
- interactive: s.interactive === true
821
+ interactive: s.interactive === true,
822
+ remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
647
823
  }));
648
- return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions };
824
+ return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions, caps: [...CAPS] };
649
825
  }
650
826
 
651
827
  // src/core/dirs.ts
652
828
  import { createHmac, randomBytes as randomBytes2 } from "crypto";
653
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
829
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
654
830
  import { basename as basename3, join as join6 } from "path";
655
831
  var DIRS_REPORT_MAX = 10;
656
832
  function dirsPath() {
@@ -659,7 +835,7 @@ function dirsPath() {
659
835
  function dirSecret() {
660
836
  const file = join6(deskbuddyHome(), "dir-secret");
661
837
  try {
662
- const s2 = readFileSync6(file, "utf8").trim();
838
+ const s2 = readFileSync7(file, "utf8").trim();
663
839
  if (s2) return s2;
664
840
  } catch {
665
841
  }
@@ -677,7 +853,7 @@ function dirRef(path) {
677
853
  }
678
854
  function readMap() {
679
855
  try {
680
- const m = JSON.parse(readFileSync6(dirsPath(), "utf8"));
856
+ const m = JSON.parse(readFileSync7(dirsPath(), "utf8"));
681
857
  const out = {};
682
858
  for (const [k, v] of Object.entries(m ?? {})) if (typeof v === "number") out[k] = v;
683
859
  return out;
@@ -773,7 +949,7 @@ function isExpired(cmd, now = Date.now()) {
773
949
  }
774
950
 
775
951
  // src/daemon/executor.ts
776
- import { execFile as execFile2 } from "child_process";
952
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "child_process";
777
953
  import { homedir as homedir4 } from "os";
778
954
  import { join as join8 } from "path";
779
955
 
@@ -787,6 +963,19 @@ function waitModeFor(tool) {
787
963
  function shQuote(s) {
788
964
  return `'${s.replace(/'/g, `'\\''`)}'`;
789
965
  }
966
+ function fwdSlash(p) {
967
+ return p.replace(/\\/g, "/");
968
+ }
969
+ function winBare(p) {
970
+ const f = fwdSlash(p);
971
+ return /^[A-Za-z0-9_.:/~+@\-\u0080-\uFFFF]+$/.test(f) ? f : null;
972
+ }
973
+ function winCmdPath(p) {
974
+ return winBare(p) ?? `"${fwdSlash(p)}"`;
975
+ }
976
+ function winPsPath(p) {
977
+ return winBare(p) ?? `'${fwdSlash(p).replace(/'/g, "''")}'`;
978
+ }
790
979
  function defaultGateScript() {
791
980
  const candidates = [
792
981
  fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
@@ -794,17 +983,17 @@ function defaultGateScript() {
794
983
  ];
795
984
  return candidates.find((p) => existsSync7(p)) ?? candidates[0];
796
985
  }
797
- function gateCommand(t) {
986
+ function gateCommand(t, platform = process.platform) {
987
+ if (platform === "win32") return winGateCommand(t);
798
988
  return `${shQuote(t.nodePath)} ${shQuote(t.gateScript)} --run ${t.runId} || exit 2`;
799
989
  }
800
- function ccSettingsJson(t) {
801
- return JSON.stringify({
802
- hooks: {
803
- PreToolUse: [
804
- { matcher: "*", hooks: [{ type: "command", command: gateCommand(t), timeout: GATE_HOOK_TIMEOUT_S }] }
805
- ]
806
- }
807
- });
990
+ function winGateCommand(t) {
991
+ return `node ${winPsPath(t.gateScript)} --run ${t.runId}; if (-not $? -or $LASTEXITCODE -ne 0) { exit 2 }`;
992
+ }
993
+ function ccSettingsJson(t, platform = process.platform) {
994
+ const entry = { type: "command", command: gateCommand(t, platform), timeout: GATE_HOOK_TIMEOUT_S };
995
+ if (platform === "win32") entry.shell = "powershell";
996
+ return JSON.stringify({ hooks: { PreToolUse: [{ matcher: "*", hooks: [entry] }] } });
808
997
  }
809
998
  function codexGateArgs(t) {
810
999
  const hook = `hooks.PreToolUse=[{matcher="*",hooks=[{type="command",command=${JSON.stringify(gateCommand(t))},timeout=${GATE_HOOK_TIMEOUT_S}}]}]`;
@@ -820,7 +1009,7 @@ function codexGateArgs(t) {
820
1009
  }
821
1010
 
822
1011
  // src/daemon/locate.ts
823
- import { accessSync, constants, existsSync as existsSync8, statSync } from "fs";
1012
+ import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync8, statSync } from "fs";
824
1013
  import { homedir as homedir3 } from "os";
825
1014
  import { delimiter, dirname as dirname4, join as join7 } from "path";
826
1015
  var defaultLocateDeps = () => ({
@@ -836,9 +1025,33 @@ var defaultLocateDeps = () => ({
836
1025
  return false;
837
1026
  }
838
1027
  },
839
- exists: (p) => existsSync8(p)
1028
+ exists: (p) => existsSync8(p),
1029
+ platform: process.platform,
1030
+ appData: process.env.APPDATA ?? "",
1031
+ localAppData: process.env.LOCALAPPDATA ?? "",
1032
+ readText: (p) => {
1033
+ try {
1034
+ return readFileSync8(p, "utf8");
1035
+ } catch {
1036
+ return null;
1037
+ }
1038
+ },
1039
+ // 🔴 daemon 自己也不能找到垫片:远程任务要的是真程序(窗口接管 spec 2026-09-30 §3.1)
1040
+ skipDirs: [join7(deskbuddyHome(), "bin")]
840
1041
  });
841
- function extraBinDirs(home, nodePath = "") {
1042
+ var isWin = (d) => (d.platform ?? process.platform) === "win32";
1043
+ var WIN_EXTS = [".exe", ".cmd", ".bat"];
1044
+ function extraBinDirs(home, nodePath = "", win) {
1045
+ if (win) {
1046
+ return [
1047
+ join7(home, ".local", "bin"),
1048
+ ...win.appData ? [join7(win.appData, "npm")] : [],
1049
+ join7(home, ".bun", "bin"),
1050
+ ...win.localAppData ? [join7(win.localAppData, "Volta", "bin")] : [],
1051
+ join7(home, "scoop", "shims"),
1052
+ ...nodePath ? [dirname4(nodePath)] : []
1053
+ ];
1054
+ }
842
1055
  return [
843
1056
  join7(home, ".local", "bin"),
844
1057
  join7(home, ".claude", "local"),
@@ -851,19 +1064,110 @@ function extraBinDirs(home, nodePath = "") {
851
1064
  ];
852
1065
  }
853
1066
  function whichIn(name, d) {
854
- const dirs = [...d.path.split(delimiter).filter(Boolean), ...extraBinDirs(d.home, d.nodePath)];
855
- for (const dir of dirs) {
856
- const p = join7(dir, name);
857
- if (d.isExecutable(p)) return p;
1067
+ return whichAllIn(name, d)[0] ?? null;
1068
+ }
1069
+ function whichAllIn(name, d) {
1070
+ const exts = isWin(d) ? WIN_EXTS : [""];
1071
+ const out = [];
1072
+ for (const dir of binDirs(d)) {
1073
+ for (const ext of exts) {
1074
+ const p = join7(dir, name + ext);
1075
+ if (d.isExecutable(p)) out.push(p);
1076
+ }
858
1077
  }
1078
+ return out;
1079
+ }
1080
+ function binDirs(d) {
1081
+ const win = isWin(d);
1082
+ const sep2 = win ? ";" : delimiter;
1083
+ const all = [
1084
+ ...d.path.split(sep2).filter(Boolean),
1085
+ ...extraBinDirs(d.home, d.nodePath, win ? { appData: d.appData, localAppData: d.localAppData } : void 0)
1086
+ ];
1087
+ if (!d.skipDirs || d.skipDirs.length === 0) return all;
1088
+ const key = (p) => {
1089
+ const t = p.replace(/[\\/]+$/, "");
1090
+ return win ? t.replace(/\//g, "\\").toLowerCase() : t;
1091
+ };
1092
+ const skip = new Set(d.skipDirs.map(key));
1093
+ return all.filter((p) => !skip.has(key(p)));
1094
+ }
1095
+ function parseNpmCmdShim(shimPath, text) {
1096
+ const m = text.match(/"%~?dp0%?\\?([^"%]+)"\s+%\*/i);
1097
+ if (!m) return null;
1098
+ return join7(dirname4(shimPath), m[1].replace(/^\\/, ""));
1099
+ }
1100
+ function resolveWinLaunch(file, d) {
1101
+ const lower = file.toLowerCase();
1102
+ if (lower.endsWith(".exe") || lower.endsWith(".com")) return { file, prefixArgs: [] };
1103
+ if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
1104
+ const text = d.readText?.(file);
1105
+ const target = text ? parseNpmCmdShim(file, text) : null;
1106
+ if (!target || !d.exists(target)) return null;
1107
+ if (/\.(?:cmd|bat)$/i.test(target)) return null;
1108
+ return resolveWinLaunch(target, d);
1109
+ }
1110
+ if (/\.(?:c|m)?js$/.test(lower)) return { file: d.nodePath, prefixArgs: [file] };
1111
+ const head = d.readText?.(file)?.slice(0, 200) ?? "";
1112
+ if (/^#!.*\bnode\b/.test(head)) return { file: d.nodePath, prefixArgs: [file] };
859
1113
  return null;
860
1114
  }
861
1115
  function appDirs(home) {
862
1116
  return ["/Applications", join7(home, "Applications")];
863
1117
  }
1118
+ function winAppDirs(d, product) {
1119
+ return [
1120
+ ...d.localAppData ? [join7(d.localAppData, "Programs", product)] : [],
1121
+ join7("C:\\Program Files", product)
1122
+ ];
1123
+ }
1124
+ var WORKBUDDY_CLI_IN_WIN_APP = "resources/app.asar.unpacked/cli/bin/codebuddy";
1125
+ var QODER_CN_WORKER_IN_WIN_APP = "resources/app.asar.unpacked/node_modules/@qoder-ai/qoder-cn-agent-sdk/dist/_worker/qoder-worker-runtime.obf.mjs";
1126
+ function locateWin(tool, d) {
1127
+ const plain = (name) => {
1128
+ for (const f of whichAllIn(name, d)) {
1129
+ const r = resolveWinLaunch(f, d);
1130
+ if (r) return r;
1131
+ }
1132
+ return null;
1133
+ };
1134
+ switch (tool) {
1135
+ case "claude_code":
1136
+ return plain("claude");
1137
+ case "codex":
1138
+ return plain("codex");
1139
+ case "workbuddy": {
1140
+ for (const dir of winAppDirs(d, "WorkBuddy")) {
1141
+ for (const ext of [".exe", ".cmd", ""]) {
1142
+ const p = join7(dir, WORKBUDDY_CLI_IN_WIN_APP + ext);
1143
+ if (d.exists(p)) {
1144
+ const r = resolveWinLaunch(p, d);
1145
+ if (r) return r;
1146
+ }
1147
+ }
1148
+ }
1149
+ return plain("codebuddy");
1150
+ }
1151
+ case "qoder": {
1152
+ for (const dir of winAppDirs(d, "Qoder CN")) {
1153
+ const p = join7(dir, QODER_CN_WORKER_IN_WIN_APP);
1154
+ if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
1155
+ }
1156
+ return plain("qodercli");
1157
+ }
1158
+ case "opencode": {
1159
+ const p = join7(d.home, ".opencode", "bin", "opencode.exe");
1160
+ if (d.exists(p)) return { file: p, prefixArgs: [] };
1161
+ return plain("opencode");
1162
+ }
1163
+ default:
1164
+ return null;
1165
+ }
1166
+ }
864
1167
  var WORKBUDDY_CLI_IN_APP = "WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy";
865
1168
  var QODER_CN_WORKER_IN_APP = "Qoder CN.app/Contents/Resources/app.asar.unpacked/node_modules/@qoder-ai/qoder-cn-agent-sdk/dist/_worker/qoder-worker-runtime.obf.mjs";
866
1169
  function locate(tool, d = defaultLocateDeps()) {
1170
+ if (isWin(d)) return locateWin(tool, d);
867
1171
  const plain = (name) => {
868
1172
  const f = whichIn(name, d);
869
1173
  return f ? { file: f, prefixArgs: [] } : null;
@@ -898,7 +1202,8 @@ function locate(tool, d = defaultLocateDeps()) {
898
1202
  }
899
1203
  function childPath(d = defaultLocateDeps()) {
900
1204
  const seen = /* @__PURE__ */ new Set();
901
- return [...d.path.split(delimiter), ...extraBinDirs(d.home, d.nodePath)].filter((p) => p && !seen.has(p) && (seen.add(p), true)).join(delimiter);
1205
+ const key = (p) => isWin(d) ? p.toLowerCase() : p;
1206
+ return binDirs(d).filter((p) => p && !seen.has(key(p)) && (seen.add(key(p)), true)).join(isWin(d) ? ";" : delimiter);
902
1207
  }
903
1208
 
904
1209
  // src/daemon/executor.ts
@@ -1075,11 +1380,11 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1075
1380
  startedAt = Date.now();
1076
1381
  timer = setTimeout(() => {
1077
1382
  timedOut = true;
1078
- child?.kill("SIGTERM");
1383
+ killTree(child);
1079
1384
  }, Math.max(0, remaining));
1080
1385
  };
1081
1386
  let timedOut = false;
1082
- const done = new Promise((resolve3) => {
1387
+ const done = new Promise((resolve5) => {
1083
1388
  child = execFile2(
1084
1389
  plan.file,
1085
1390
  plan.args,
@@ -1087,7 +1392,9 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1087
1392
  cwd: plan.cwd,
1088
1393
  maxBuffer: EXEC_MAX_BUFFER,
1089
1394
  // agent 自己要读 PATH 里的东西,环境原样继承
1090
- env: { ...process.env, ...plan.env }
1395
+ env: mergeEnv(process.env, plan.env),
1396
+ // Windows:别给子进程弹黑框
1397
+ windowsHide: true
1091
1398
  },
1092
1399
  (err, stdout7) => {
1093
1400
  finished = true;
@@ -1095,10 +1402,10 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1095
1402
  const out = String(stdout7 ?? "");
1096
1403
  if (err || timedOut) {
1097
1404
  const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : err.message;
1098
- resolve3({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
1405
+ resolve5({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
1099
1406
  return;
1100
1407
  }
1101
- resolve3({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
1408
+ resolve5({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
1102
1409
  }
1103
1410
  );
1104
1411
  child?.stdin?.end();
@@ -1120,10 +1427,31 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1120
1427
  }
1121
1428
  };
1122
1429
  }
1430
+ function mergeEnv(base, extra, platform = process.platform) {
1431
+ const env = { ...base };
1432
+ for (const [k, v] of Object.entries(extra ?? {})) {
1433
+ if (platform === "win32") {
1434
+ for (const old of Object.keys(env)) if (old !== k && old.toUpperCase() === k.toUpperCase()) delete env[old];
1435
+ }
1436
+ env[k] = v;
1437
+ }
1438
+ return env;
1439
+ }
1440
+ function killTree(child) {
1441
+ if (!child) return;
1442
+ if (process.platform === "win32" && child.pid) {
1443
+ try {
1444
+ execFileSync4("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
1445
+ return;
1446
+ } catch {
1447
+ }
1448
+ }
1449
+ child.kill("SIGTERM");
1450
+ }
1123
1451
 
1124
1452
  // src/daemon/gate/approvals.ts
1125
1453
  import { createHash as createHash2 } from "crypto";
1126
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
1454
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
1127
1455
  import { join as join9 } from "path";
1128
1456
  var RETAIN_MS = 7 * 24 * 60 * 60 * 1e3;
1129
1457
  function fingerprint(toolName, toolInput) {
@@ -1145,7 +1473,7 @@ var ApprovalStore = class _ApprovalStore {
1145
1473
  }
1146
1474
  static load() {
1147
1475
  try {
1148
- const raw = JSON.parse(readFileSync7(storePath(), "utf8"));
1476
+ const raw = JSON.parse(readFileSync9(storePath(), "utf8"));
1149
1477
  return new _ApprovalStore(Array.isArray(raw) ? raw : []);
1150
1478
  } catch {
1151
1479
  return new _ApprovalStore();
@@ -1341,12 +1669,12 @@ function isReadOnlyCommand(command) {
1341
1669
  }
1342
1670
  function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
1343
1671
  const roots = (Array.isArray(cwd) ? cwd : [cwd]).filter(Boolean);
1344
- const real = opts.real;
1672
+ const real2 = opts.real;
1345
1673
  const inside = (p) => {
1346
1674
  if (!roots.some((r) => isInside(r, p))) return false;
1347
- if (!real) return true;
1348
- const rp = real(resolve(roots[0] ?? "/", p));
1349
- return roots.some((r) => isInside(real(r), rp));
1675
+ if (!real2) return true;
1676
+ const rp = real2(resolve(roots[0] ?? "/", p));
1677
+ return roots.some((r) => isInside(real2(r), rp));
1350
1678
  };
1351
1679
  const rel = (p) => {
1352
1680
  const r = roots.find((x) => isInside(x, p)) ?? roots[0] ?? "";
@@ -1363,7 +1691,7 @@ function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
1363
1691
  if (paths) {
1364
1692
  const allInside = paths.length > 0 && paths.every(inside);
1365
1693
  const touchesProtected = paths.some(
1366
- (p) => isProtectedPath(roots[0] ?? "/", p) || (real ? isProtectedPath("/", real(resolve(roots[0] ?? "/", p))) : false)
1694
+ (p) => isProtectedPath(roots[0] ?? "/", p) || (real2 ? isProtectedPath("/", real2(resolve(roots[0] ?? "/", p))) : false)
1367
1695
  );
1368
1696
  if (policy === "commands" && allInside && !touchesProtected) return { action: "auto" };
1369
1697
  const shown = paths.map((p) => inside(p) ? rel(p) : p);
@@ -1386,11 +1714,14 @@ function describe(toolName, toolInput, cwd) {
1386
1714
  }
1387
1715
 
1388
1716
  // src/daemon/gate/procs.ts
1389
- import { execFileSync as execFileSync3 } from "child_process";
1717
+ import { execFileSync as execFileSync5 } from "child_process";
1390
1718
  function listDescendants(rootPid) {
1719
+ if (process.platform === "win32") {
1720
+ return descendantsOf(rootPid, (winProcs() ?? []).map((p) => ({ pid: p.pid, ppid: p.ppid, args: p.cmdline })));
1721
+ }
1391
1722
  let out = "";
1392
1723
  try {
1393
- out = execFileSync3("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
1724
+ out = execFileSync5("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
1394
1725
  encoding: "utf8",
1395
1726
  stdio: ["ignore", "pipe", "ignore"],
1396
1727
  maxBuffer: 16 * 1024 * 1024
@@ -1398,19 +1729,28 @@ function listDescendants(rootPid) {
1398
1729
  } catch {
1399
1730
  return [];
1400
1731
  }
1401
- const kids = /* @__PURE__ */ new Map();
1732
+ const rows = [];
1402
1733
  for (const line of out.split("\n")) {
1403
1734
  const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
1404
- if (!m) continue;
1405
- const pp = Number(m[2]);
1406
- const list = kids.get(pp) ?? [];
1407
- list.push({ pid: Number(m[1]), args: m[3] });
1408
- kids.set(pp, list);
1735
+ if (m) rows.push({ pid: Number(m[1]), ppid: Number(m[2]), args: m[3] });
1736
+ }
1737
+ return descendantsOf(rootPid, rows);
1738
+ }
1739
+ function descendantsOf(rootPid, rows) {
1740
+ const kids = /* @__PURE__ */ new Map();
1741
+ for (const r of rows) {
1742
+ if (r.pid === r.ppid) continue;
1743
+ const list = kids.get(r.ppid) ?? [];
1744
+ list.push({ pid: r.pid, args: r.args });
1745
+ kids.set(r.ppid, list);
1409
1746
  }
1747
+ const seen = /* @__PURE__ */ new Set([rootPid]);
1410
1748
  const res = [];
1411
1749
  const stack = [rootPid];
1412
1750
  while (stack.length > 0) {
1413
1751
  for (const c of kids.get(stack.pop()) ?? []) {
1752
+ if (seen.has(c.pid)) continue;
1753
+ seen.add(c.pid);
1414
1754
  res.push(c);
1415
1755
  stack.push(c.pid);
1416
1756
  }
@@ -1439,9 +1779,9 @@ function pickRememberSuggestion(suggestions) {
1439
1779
  return null;
1440
1780
  }
1441
1781
  var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
1442
- function windowNotice(decision, summary, deviceName = "") {
1782
+ function windowNotice(decision, summary2, deviceName = "") {
1443
1783
  const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
1444
- const what = summary.length > 60 ? summary.slice(0, 59) + "\u2026" : summary;
1784
+ const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
1445
1785
  return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
1446
1786
  }
1447
1787
  var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
@@ -1474,17 +1814,17 @@ var ApprovalCoordinator = class {
1474
1814
  sessionOf(runId) {
1475
1815
  return this.#runs.get(runId)?.sessionId ?? "";
1476
1816
  }
1477
- async onGate(req, signal) {
1478
- if (req.window) return this.#onWindow(req.hook, signal, req.tool || "claude_code");
1479
- const run2 = this.#runs.get(req.runId ?? "");
1817
+ async onGate(req2, signal) {
1818
+ if (req2.window) return this.#onWindow(req2.hook, signal, req2.tool || "claude_code");
1819
+ const run2 = this.#runs.get(req2.runId ?? "");
1480
1820
  if (!run2) return { decision: "deny", reason: REASON_UNKNOWN_RUN };
1481
1821
  if (run2.superseded) return { decision: "deny", reason: REASON_SUPERSEDED };
1482
- const toolName = typeof req.hook.tool_name === "string" ? req.hook.tool_name : "";
1483
- const sessionId = typeof req.hook.session_id === "string" && req.hook.session_id || run2.sessionId || "";
1822
+ const toolName = typeof req2.hook.tool_name === "string" ? req2.hook.tool_name : "";
1823
+ const sessionId = typeof req2.hook.session_id === "string" && req2.hook.session_id || run2.sessionId || "";
1484
1824
  if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
1485
- const v = classify(toolName, req.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
1825
+ const v = classify(toolName, req2.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
1486
1826
  if (v.action === "auto") return { decision: "allow" };
1487
- const fp = fingerprint(toolName, req.hook.tool_input);
1827
+ const fp = fingerprint(toolName, req2.hook.tool_input);
1488
1828
  const store = this.#d.store;
1489
1829
  if (run2.mode === "defer" && sessionId) {
1490
1830
  const prev = store.latestFor(sessionId, fp);
@@ -1513,7 +1853,7 @@ var ApprovalCoordinator = class {
1513
1853
  store.save();
1514
1854
  let early = null;
1515
1855
  let settle;
1516
- const decided = new Promise((resolve3) => settle = resolve3);
1856
+ const decided = new Promise((resolve5) => settle = resolve5);
1517
1857
  this.#waiters.set(rec.approvalId, (r) => {
1518
1858
  early = r;
1519
1859
  settle(r);
@@ -1537,8 +1877,8 @@ var ApprovalCoordinator = class {
1537
1877
  }
1538
1878
  run2.onWaitStart?.();
1539
1879
  try {
1540
- return await new Promise((resolve3) => {
1541
- void decided.then(resolve3);
1880
+ return await new Promise((resolve5) => {
1881
+ void decided.then(resolve5);
1542
1882
  const onAbort = () => {
1543
1883
  if (!this.#waiters.delete(rec.approvalId)) return;
1544
1884
  if (rec.status === "pending") {
@@ -1547,7 +1887,7 @@ var ApprovalCoordinator = class {
1547
1887
  store.save();
1548
1888
  void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
1549
1889
  }
1550
- resolve3({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
1890
+ resolve5({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
1551
1891
  };
1552
1892
  if (signal.aborted) onAbort();
1553
1893
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -1570,7 +1910,7 @@ var ApprovalCoordinator = class {
1570
1910
  if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
1571
1911
  for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
1572
1912
  const store = this.#d.store;
1573
- const { kind, summary } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
1913
+ const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
1574
1914
  const remember = tool === "claude_code" ? pickRememberSuggestion(hook.permission_suggestions) : null;
1575
1915
  const rec = {
1576
1916
  approvalId: randomUUID3(),
@@ -1580,7 +1920,7 @@ var ApprovalCoordinator = class {
1580
1920
  cwd,
1581
1921
  fingerprint: fingerprint(toolName, hook.tool_input),
1582
1922
  kind,
1583
- summary,
1923
+ summary: summary2,
1584
1924
  mode: "window",
1585
1925
  status: "pending",
1586
1926
  createdAt: this.#now(),
@@ -1599,7 +1939,7 @@ var ApprovalCoordinator = class {
1599
1939
  ...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
1600
1940
  });
1601
1941
  let settle;
1602
- const decided = new Promise((resolve3) => settle = resolve3);
1942
+ const decided = new Promise((resolve5) => settle = resolve5);
1603
1943
  this.#waiters.set(rec.approvalId, settle);
1604
1944
  try {
1605
1945
  await this.#d.backend.create(rec);
@@ -1615,11 +1955,11 @@ var ApprovalCoordinator = class {
1615
1955
  }
1616
1956
  this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
1617
1957
  if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
1618
- return new Promise((resolve3) => {
1619
- void decided.then(resolve3);
1958
+ return new Promise((resolve5) => {
1959
+ void decided.then(resolve5);
1620
1960
  const onAbort = () => {
1621
1961
  if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
1622
- resolve3(none);
1962
+ resolve5(none);
1623
1963
  };
1624
1964
  if (signal.aborted) onAbort();
1625
1965
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -1893,8 +2233,8 @@ function realpathNearest(p) {
1893
2233
  }
1894
2234
  function cwdForms(cwd) {
1895
2235
  try {
1896
- const real = realpathSync(cwd);
1897
- return real === cwd ? [cwd] : [cwd, real];
2236
+ const real2 = realpathSync(cwd);
2237
+ return real2 === cwd ? [cwd] : [cwd, real2];
1898
2238
  } catch {
1899
2239
  return [cwd];
1900
2240
  }
@@ -1926,10 +2266,10 @@ var GateServer = class {
1926
2266
  }
1927
2267
  /** 起监听并写 gate.json。返回实际端口 */
1928
2268
  async start() {
1929
- const server = createServer((req, res) => void this.#serve(req, res));
1930
- await new Promise((resolve3, reject) => {
2269
+ const server = createServer((req2, res) => void this.#serve(req2, res));
2270
+ await new Promise((resolve5, reject) => {
1931
2271
  server.once("error", reject);
1932
- server.listen(0, "127.0.0.1", () => resolve3());
2272
+ server.listen(0, "127.0.0.1", () => resolve5());
1933
2273
  });
1934
2274
  this.#server = server;
1935
2275
  const addr = server.address();
@@ -1950,21 +2290,21 @@ var GateServer = class {
1950
2290
  }
1951
2291
  const s = this.#server;
1952
2292
  this.#server = null;
1953
- if (s) await new Promise((resolve3) => s.close(() => resolve3()));
2293
+ if (s) await new Promise((resolve5) => s.close(() => resolve5()));
1954
2294
  }
1955
- async #serve(req, res) {
2295
+ async #serve(req2, res) {
1956
2296
  const reply = (status, body2) => {
1957
2297
  if (res.writableEnded || res.destroyed) return;
1958
2298
  res.writeHead(status, { "content-type": "application/json" });
1959
2299
  res.end(body2 ? JSON.stringify(body2) : "");
1960
2300
  };
1961
- if (req.method !== "POST" || req.url !== "/gate") return reply(404);
1962
- const token = req.headers["x-gate-token"];
2301
+ if (req2.method !== "POST" || req2.url !== "/gate") return reply(404);
2302
+ const token = req2.headers["x-gate-token"];
1963
2303
  if (typeof token !== "string" || !tokenEquals(token, this.#token)) return reply(403);
1964
2304
  let body = "";
1965
2305
  let size = 0;
1966
2306
  try {
1967
- for await (const chunk of req) {
2307
+ for await (const chunk of req2) {
1968
2308
  size += chunk.length;
1969
2309
  if (size > MAX_BODY_BYTES) return reply(413);
1970
2310
  body += String(chunk);
@@ -1986,12 +2326,12 @@ var GateServer = class {
1986
2326
  if (!res.writableEnded) ac.abort();
1987
2327
  });
1988
2328
  try {
1989
- const req2 = window ? {
2329
+ const req3 = window ? {
1990
2330
  window: true,
1991
2331
  tool: typeof parsed.tool === "string" && /^[a-z_]{1,32}$/.test(parsed.tool) ? parsed.tool : "claude_code",
1992
2332
  hook: parsed.hook
1993
2333
  } : { runId: parsed.runId, hook: parsed.hook };
1994
- const out = await this.#handler(req2, ac.signal);
2334
+ const out = await this.#handler(req3, ac.signal);
1995
2335
  reply(200, out);
1996
2336
  } catch {
1997
2337
  reply(500);
@@ -2108,6 +2448,10 @@ var SessionQueue = class {
2108
2448
  });
2109
2449
  return next;
2110
2450
  }
2451
+ /** 这个会话有没有在跑 / 在排队的任务 */
2452
+ busy(sessionId) {
2453
+ return this.#tails.has(sessionId || "");
2454
+ }
2111
2455
  /** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
2112
2456
  get activeSessions() {
2113
2457
  return this.#tails.size;
@@ -2121,7 +2465,7 @@ var SessionQueue = class {
2121
2465
  };
2122
2466
 
2123
2467
  // src/daemon/remote.ts
2124
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync8, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
2468
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
2125
2469
  import { join as join13 } from "path";
2126
2470
  function handleDir() {
2127
2471
  return join13(deskbuddyHome(), "remote");
@@ -2132,7 +2476,7 @@ function handlePath(tool) {
2132
2476
  }
2133
2477
  function readHandle(tool) {
2134
2478
  try {
2135
- const h = JSON.parse(readFileSync8(handlePath(tool), "utf8"));
2479
+ const h = JSON.parse(readFileSync10(handlePath(tool), "utf8"));
2136
2480
  if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
2137
2481
  if (typeof h?.cwd !== "string" || !h.cwd) return null;
2138
2482
  return h;
@@ -2198,7 +2542,7 @@ function failureMessage(reason) {
2198
2542
  }
2199
2543
 
2200
2544
  // src/daemon/seen.ts
2201
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync9, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "fs";
2545
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync11, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "fs";
2202
2546
  import { join as join14 } from "path";
2203
2547
  var SEEN_CAPACITY = 200;
2204
2548
  function seenPath() {
@@ -2233,7 +2577,7 @@ var SeenIds = class _SeenIds {
2233
2577
  }
2234
2578
  static load() {
2235
2579
  try {
2236
- const raw = JSON.parse(readFileSync9(seenPath(), "utf8"));
2580
+ const raw = JSON.parse(readFileSync11(seenPath(), "utf8"));
2237
2581
  return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
2238
2582
  } catch {
2239
2583
  return new _SeenIds();
@@ -2251,11 +2595,25 @@ var SeenIds = class _SeenIds {
2251
2595
  };
2252
2596
 
2253
2597
  // src/daemon/codexWatch.ts
2254
- import { execFileSync as execFileSync4 } from "child_process";
2598
+ import { execFileSync as execFileSync6 } from "child_process";
2255
2599
  import WebSocket from "ws";
2600
+ var TURN_LIMIT_MS = 10 * 60 * 1e3;
2601
+ var QUEUE_MAX_MS = 10 * 60 * 1e3;
2602
+ var IDLE_GRACE_MS = 5 * 1e3;
2256
2603
  var RESUME_RETRY_MS = 1e3;
2257
2604
  var RESUME_MAX_TRIES = 120;
2258
2605
  var REQUEST_TIMEOUT_MS = 30 * 1e3;
2606
+ function finalReply(items) {
2607
+ let last = "";
2608
+ let final = "";
2609
+ for (const it of items) {
2610
+ const i = it ?? {};
2611
+ if (i.type !== "agentMessage" || typeof i.text !== "string") continue;
2612
+ last = i.text;
2613
+ if (i.phase === "final_answer") final = i.text;
2614
+ }
2615
+ return final || last;
2616
+ }
2259
2617
  function isLoopbackWs(url) {
2260
2618
  try {
2261
2619
  const u = new URL(url);
@@ -2274,17 +2632,29 @@ function parseAppServerUrls(psOutput) {
2274
2632
  return urls;
2275
2633
  }
2276
2634
  function discoverAppServer(configured) {
2277
- if (configured) return isLoopbackWs(configured) ? configured : null;
2635
+ return discoverAppServers(configured)[0] ?? null;
2636
+ }
2637
+ function discoverAppServers(configured) {
2638
+ if (configured) return isLoopbackWs(configured) ? [configured] : [];
2639
+ if (process.platform === "win32") {
2640
+ return parseAppServerUrls((winProcs() ?? []).map((p) => p.cmdline).join("\n"));
2641
+ }
2278
2642
  try {
2279
- const out = execFileSync4("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
2280
- return parseAppServerUrls(out)[0] ?? null;
2643
+ const out = execFileSync6("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
2644
+ return parseAppServerUrls(out);
2281
2645
  } catch {
2282
- return null;
2646
+ return [];
2283
2647
  }
2284
2648
  }
2285
2649
  function unwrapShell(command) {
2286
2650
  const m = command.match(/^\/bin\/(?:ba|z)?sh -lc '([\s\S]*)'$/);
2287
- return m ? m[1].replace(/'\\''/g, "'") : command;
2651
+ if (m) return m[1].replace(/'\\''/g, "'");
2652
+ const w = command.match(/^"?(?:[^"]*[\\/])?(?:powershell|pwsh)(?:\.exe)?"?(?: -NoLogo| -NoProfile| -NonInteractive)* -Command ([\s\S]+)$/i);
2653
+ if (!w) return command;
2654
+ const body = w[1].trim();
2655
+ if (body.length >= 2 && body.startsWith('"') && body.endsWith('"')) return body.slice(1, -1).replace(/\\"/g, '"');
2656
+ if (body.length >= 2 && body.startsWith("'") && body.endsWith("'")) return body.slice(1, -1).replace(/''/g, "'");
2657
+ return body;
2288
2658
  }
2289
2659
  var CodexWatch = class {
2290
2660
  #d;
@@ -2306,17 +2676,48 @@ var CodexWatch = class {
2306
2676
  #idle = /* @__PURE__ */ new Set();
2307
2677
  /** 连接代数:断线重连后,上一条连接遗留的订阅重试不再跑 */
2308
2678
  #gen = 0;
2679
+ /** initialize 已成功 */
2680
+ #ready = false;
2681
+ /** app-server 里已加载的会话(thread/loaded/list + 通知维护)。窗口开着 = 能往里发,据此上报 remoteWindow */
2682
+ #loaded = /* @__PURE__ */ new Set();
2683
+ /** 会话状态(thread/status/changed 是广播,没订阅也收得到) */
2684
+ #status = /* @__PURE__ */ new Map();
2685
+ /** 会话里正在跑的那一轮(turn/started 起、turn/completed 止;只有订阅了才收得到) */
2686
+ #activeTurn = /* @__PURE__ */ new Map();
2687
+ /** 会话 → 我们自己在排队 / 在跑的活数。🔴 有活时不退订(否则收不到 turn/completed) */
2688
+ #own = /* @__PURE__ */ new Map();
2689
+ /** 会话 → 我们发的轮次串行链:同一会话的设备指令一条跑完再发下一条 */
2690
+ #chain = /* @__PURE__ */ new Map();
2691
+ /** turnId → 我们发出、正在跟踪的那一轮 */
2692
+ #turns = /* @__PURE__ */ new Map();
2693
+ /** 会话 → 在途的 thread/resume(授权推送那条路径发的) */
2694
+ #inflight = /* @__PURE__ */ new Map();
2695
+ /** 回话还没到就先到了的 turn/completed(按 turnId 缓冲,最多几条) */
2696
+ #early = /* @__PURE__ */ new Map();
2697
+ /** 会话 → 等状态变化的回调(排队等空闲用) */
2698
+ #wake = /* @__PURE__ */ new Map();
2309
2699
  constructor(deps) {
2310
2700
  this.#d = { connect: (url) => new WebSocket(url), ...deps };
2311
2701
  }
2312
2702
  get connected() {
2313
2703
  return this.#ws !== null;
2314
2704
  }
2705
+ get url() {
2706
+ return this.#url;
2707
+ }
2708
+ /** 这个会话此刻是否加载在 app-server 里(能直接往里发一轮) */
2709
+ hasLoaded(threadId) {
2710
+ return this.#ready && this.#loaded.has(threadId);
2711
+ }
2712
+ loadedThreads() {
2713
+ return this.#ready ? [...this.#loaded] : [];
2714
+ }
2315
2715
  /** 定时调:没连上就找一次 app-server 连上去;连着时 codex 被 disable 了就断开 */
2316
2716
  tick() {
2317
2717
  if (this.#stopped) return;
2318
2718
  if (this.#ws) {
2319
2719
  if (this.#d.enabled && !this.#d.enabled()) this.#teardown();
2720
+ else if (this.#ready) void this.#syncLoaded();
2320
2721
  return;
2321
2722
  }
2322
2723
  const url = (this.#d.discover ?? (() => discoverAppServer()))();
@@ -2339,10 +2740,10 @@ var CodexWatch = class {
2339
2740
  this.#gen++;
2340
2741
  ws.on("open", () => void this.#onOpen());
2341
2742
  ws.on("message", (buf) => this.#onMessage(String(buf)));
2342
- ws.on("close", () => this.#teardown());
2343
- ws.on("error", () => this.#teardown());
2743
+ ws.on("close", (code) => this.#teardown(`\u5173\u95ED ${code}`));
2744
+ ws.on("error", (e) => this.#teardown(`\u51FA\u9519 ${e?.message ?? ""}`));
2344
2745
  }
2345
- #teardown() {
2746
+ #teardown(why = "") {
2346
2747
  const ws = this.#ws;
2347
2748
  if (!ws) return;
2348
2749
  this.#ws = null;
@@ -2352,7 +2753,7 @@ var CodexWatch = class {
2352
2753
  }
2353
2754
  for (const c of this.#calls.values()) {
2354
2755
  clearTimeout(c.timer);
2355
- c.resolve({ error: { message: "disconnected" } });
2756
+ c.resolve({ error: { message: "disconnected" }, local: true });
2356
2757
  }
2357
2758
  this.#calls.clear();
2358
2759
  for (const { ac } of this.#pending.values()) ac.abort();
@@ -2362,7 +2763,17 @@ var CodexWatch = class {
2362
2763
  this.#cwd.clear();
2363
2764
  this.#changes.clear();
2364
2765
  this.#idle.clear();
2365
- this.#d.log(`\u5DF2\u65AD\u5F00 Codex app-server ${this.#url}`);
2766
+ this.#ready = false;
2767
+ this.#status.clear();
2768
+ this.#activeTurn.clear();
2769
+ this.#inflight.clear();
2770
+ this.#early.clear();
2771
+ for (const t of [...this.#turns.values()]) this.#finishTurn(t, "disconnected");
2772
+ this.#wakeAll();
2773
+ const hadLoaded = this.#loaded.size > 0;
2774
+ this.#loaded.clear();
2775
+ if (hadLoaded) this.#notifyLoaded();
2776
+ this.#d.log(`\u5DF2\u65AD\u5F00 Codex app-server ${this.#url}${why ? `\uFF08${why}\uFF09` : ""}`);
2366
2777
  }
2367
2778
  #send(obj) {
2368
2779
  try {
@@ -2372,13 +2783,13 @@ var CodexWatch = class {
2372
2783
  }
2373
2784
  #request(method, params) {
2374
2785
  const id = this.#nextId++;
2375
- return new Promise((resolve3) => {
2786
+ return new Promise((resolve5) => {
2376
2787
  const timer = setTimeout(() => {
2377
2788
  this.#calls.delete(id);
2378
- resolve3({ error: { message: "timeout" } });
2789
+ resolve5({ error: { message: "timeout" }, local: true });
2379
2790
  }, REQUEST_TIMEOUT_MS);
2380
2791
  timer.unref?.();
2381
- this.#calls.set(id, { resolve: resolve3, timer });
2792
+ this.#calls.set(id, { resolve: resolve5, timer });
2382
2793
  this.#send({ id, method, params });
2383
2794
  });
2384
2795
  }
@@ -2390,7 +2801,64 @@ var CodexWatch = class {
2390
2801
  return;
2391
2802
  }
2392
2803
  this.#send({ method: "initialized" });
2804
+ this.#ready = true;
2393
2805
  this.#d.log(`\u5DF2\u8FDE\u4E0A Codex app-server ${this.#url}\uFF0C\u7A97\u53E3\u6388\u6743\u53EF\u63A8\u5230\u8BBE\u5907`);
2806
+ await this.#syncLoaded();
2807
+ }
2808
+ #notifyLoaded() {
2809
+ try {
2810
+ this.#d.onLoadedChange?.();
2811
+ } catch {
2812
+ }
2813
+ }
2814
+ #setLoaded(threadId, on) {
2815
+ if (on === this.#loaded.has(threadId)) return;
2816
+ if (on) this.#loaded.add(threadId);
2817
+ else this.#loaded.delete(threadId);
2818
+ this.#notifyLoaded();
2819
+ }
2820
+ /** 拉一遍已加载的会话(分页)。拉不到返回 null,不动现有集合 */
2821
+ async #fetchLoaded() {
2822
+ const gen = this.#gen;
2823
+ const all = /* @__PURE__ */ new Set();
2824
+ let cursor = null;
2825
+ for (let page = 0; page < 20; page++) {
2826
+ const r = await this.#request("thread/loaded/list", cursor ? { cursor } : {});
2827
+ if (r.error || gen !== this.#gen) return null;
2828
+ const res = r.result ?? {};
2829
+ if (Array.isArray(res.data)) {
2830
+ for (const id of res.data) if (typeof id === "string") all.add(id);
2831
+ }
2832
+ cursor = typeof res.nextCursor === "string" && res.nextCursor ? res.nextCursor : null;
2833
+ if (!cursor) break;
2834
+ }
2835
+ return all;
2836
+ }
2837
+ async #syncLoaded() {
2838
+ const gen = this.#gen;
2839
+ const fresh = await this.#fetchLoaded();
2840
+ if (!fresh || gen !== this.#gen) return null;
2841
+ let changed = fresh.size !== this.#loaded.size;
2842
+ if (!changed) {
2843
+ for (const id of fresh) if (!this.#loaded.has(id)) changed = true;
2844
+ }
2845
+ if (changed) {
2846
+ this.#loaded.clear();
2847
+ for (const id of fresh) this.#loaded.add(id);
2848
+ this.#notifyLoaded();
2849
+ }
2850
+ return fresh;
2851
+ }
2852
+ /** resume 的回话:记下工作目录与会话状态 */
2853
+ #applyThread(threadId, r) {
2854
+ const th = (r.result ?? {}).thread;
2855
+ if (typeof th?.cwd === "string") this.#cwd.set(threadId, th.cwd);
2856
+ const st = th?.status?.type;
2857
+ if (typeof st === "string") {
2858
+ this.#status.set(threadId, st);
2859
+ if (st === "active") this.#idle.delete(threadId);
2860
+ else this.#idle.add(threadId);
2861
+ }
2394
2862
  }
2395
2863
  #subscribe(threadId) {
2396
2864
  if (this.#subscribed.has(threadId) || this.#retrying.has(threadId)) return;
@@ -2398,16 +2866,26 @@ var CodexWatch = class {
2398
2866
  const gen = this.#gen;
2399
2867
  const attempt = async () => {
2400
2868
  if (!this.#ws || gen !== this.#gen || !this.#retrying.has(threadId)) return;
2401
- const r = await this.#request("thread/resume", { threadId });
2402
- if (gen !== this.#gen || !this.#retrying.has(threadId)) {
2403
- if (!r.error && gen === this.#gen) void this.#request("thread/unsubscribe", { threadId });
2869
+ const req2 = this.#request("thread/resume", { threadId, excludeTurns: true });
2870
+ this.#inflight.set(threadId, req2);
2871
+ const r = await req2;
2872
+ if (this.#inflight.get(threadId) === req2) this.#inflight.delete(threadId);
2873
+ if (gen !== this.#gen) return;
2874
+ if (!this.#retrying.has(threadId)) {
2875
+ if (!r.error) {
2876
+ if ((this.#own.get(threadId) ?? 0) > 0 || this.#subscribed.has(threadId)) {
2877
+ this.#subscribed.add(threadId);
2878
+ this.#applyThread(threadId, r);
2879
+ } else {
2880
+ void this.#request("thread/unsubscribe", { threadId });
2881
+ }
2882
+ }
2404
2883
  return;
2405
2884
  }
2406
2885
  if (!r.error) {
2407
2886
  this.#retrying.delete(threadId);
2408
2887
  this.#subscribed.add(threadId);
2409
- const cwd = r.result?.thread?.cwd;
2410
- if (typeof cwd === "string") this.#cwd.set(threadId, cwd);
2888
+ this.#applyThread(threadId, r);
2411
2889
  return;
2412
2890
  }
2413
2891
  const n = (this.#retrying.get(threadId) ?? 0) + 1;
@@ -2425,9 +2903,194 @@ var CodexWatch = class {
2425
2903
  this.#retrying.delete(threadId);
2426
2904
  if (!this.#subscribed.has(threadId)) return;
2427
2905
  for (const p of this.#pending.values()) if (p.threadId === threadId) return;
2906
+ if ((this.#own.get(threadId) ?? 0) > 0) return;
2428
2907
  this.#subscribed.delete(threadId);
2429
2908
  void this.#request("thread/unsubscribe", { threadId });
2430
2909
  }
2910
+ /**
2911
+ * 往一个已加载的会话里发一轮(切片 B)。同一会话的多条串行;会话正忙时排队到空闲再发,
2912
+ * 🔴 绝不在人正在跑的那一轮里插话(turn/start 打到 active 的会话上会并进当前那一轮,0.157.1 实测)。
2913
+ *
2914
+ * 返回 sent=false = 确定没发出去,调用方可以退回后台 exec;sent=true 之后无论结局如何都不能再重跑
2915
+ */
2916
+ async runTurn(req2, hooks = {}) {
2917
+ const tid = req2.threadId;
2918
+ let queued = false;
2919
+ const h = {
2920
+ onQueued: () => {
2921
+ if (queued) return;
2922
+ queued = true;
2923
+ hooks.onQueued?.();
2924
+ },
2925
+ onStarted: () => hooks.onStarted?.()
2926
+ };
2927
+ this.#own.set(tid, (this.#own.get(tid) ?? 0) + 1);
2928
+ const prev = this.#chain.get(tid);
2929
+ let release2;
2930
+ const mine = new Promise((r) => release2 = r);
2931
+ const tail = (prev ?? Promise.resolve()).then(() => mine);
2932
+ this.#chain.set(tid, tail);
2933
+ try {
2934
+ if (prev) {
2935
+ h.onQueued?.();
2936
+ await prev;
2937
+ }
2938
+ return await this.#runTurnNow(req2, h);
2939
+ } catch (e) {
2940
+ this.#d.log(`Codex \u53D1\u4E00\u8F6E\u51FA\u9519\uFF1A${e.message}`);
2941
+ return { sent: false, reason: "start_failed" };
2942
+ } finally {
2943
+ release2();
2944
+ if (this.#chain.get(tid) === tail) this.#chain.delete(tid);
2945
+ const n = (this.#own.get(tid) ?? 1) - 1;
2946
+ if (n > 0) this.#own.set(tid, n);
2947
+ else {
2948
+ this.#own.delete(tid);
2949
+ if (this.#idle.has(tid)) this.#unsubscribe(tid);
2950
+ }
2951
+ }
2952
+ }
2953
+ #expired(req2, since) {
2954
+ const now = Date.now();
2955
+ return req2.expireAt > 0 ? now > req2.expireAt : now - since > QUEUE_MAX_MS;
2956
+ }
2957
+ async #runTurnNow(req2, h) {
2958
+ const tid = req2.threadId;
2959
+ const since = Date.now();
2960
+ const gen = this.#gen;
2961
+ const lost = () => !this.#ws || gen !== this.#gen;
2962
+ if (!this.#ws || !this.#ready) return { sent: false, reason: "not_connected" };
2963
+ const loaded = await this.#syncLoaded();
2964
+ if (lost()) return { sent: false, reason: "disconnected" };
2965
+ if (!loaded || !loaded.has(tid)) return { sent: false, reason: "not_loaded" };
2966
+ const inflight = this.#inflight.get(tid);
2967
+ if (inflight) {
2968
+ this.#retrying.delete(tid);
2969
+ await inflight;
2970
+ if (lost()) return { sent: false, reason: "disconnected" };
2971
+ }
2972
+ if (!this.#subscribed.has(tid)) {
2973
+ this.#retrying.delete(tid);
2974
+ const r2 = await this.#request("thread/resume", { threadId: tid, excludeTurns: true });
2975
+ if (lost()) return { sent: false, reason: "disconnected" };
2976
+ if (r2.error) {
2977
+ this.#d.log(`\u8BA2\u9605 Codex \u4F1A\u8BDD\u5931\u8D25\uFF1A${JSON.stringify(r2.error).slice(0, 120)}`);
2978
+ return { sent: false, reason: "resume_failed" };
2979
+ }
2980
+ this.#subscribed.add(tid);
2981
+ this.#applyThread(tid, r2);
2982
+ }
2983
+ for (; ; ) {
2984
+ if (lost()) return { sent: false, reason: "disconnected" };
2985
+ const st = this.#status.get(tid);
2986
+ if (st === "notLoaded" || !this.#loaded.has(tid)) return { sent: false, reason: "closed" };
2987
+ if (this.#expired(req2, since)) return { sent: false, reason: "expired" };
2988
+ if (st !== "active") break;
2989
+ h.onQueued?.();
2990
+ const left = req2.expireAt > 0 ? req2.expireAt - Date.now() : QUEUE_MAX_MS - (Date.now() - since);
2991
+ await this.#waitThread(tid, Math.max(1, Math.min(left + 5, 60 * 1e3)));
2992
+ }
2993
+ const humanTurn = this.#activeTurn.get(tid);
2994
+ const r = await this.#request("turn/start", {
2995
+ threadId: tid,
2996
+ input: [{ type: "text", text: req2.text, text_elements: [] }]
2997
+ });
2998
+ if (r.error && !r.local) {
2999
+ this.#d.log(`Codex turn/start \u88AB\u62D2\uFF1A${JSON.stringify(r.error).slice(0, 120)}`);
3000
+ return { sent: false, reason: "start_failed" };
3001
+ }
3002
+ if (r.error) {
3003
+ return { sent: true, end: "disconnected", turnId: "", text: "", error: String(r.error.message ?? "") };
3004
+ }
3005
+ const turnId = String((r.result ?? {}).turn?.id ?? "");
3006
+ const merged = !!turnId && (turnId === humanTurn || turnId === this.#activeTurn.get(tid));
3007
+ if (merged) this.#d.log("\u8BBE\u5907\u6307\u4EE4\u5E76\u8FDB\u4E86\u7A97\u53E3\u91CC\u6B63\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E");
3008
+ h.onStarted?.();
3009
+ if (!turnId) return { sent: true, end: "failed", turnId: "", text: "", error: "no turn id" };
3010
+ if (lost()) return { sent: true, end: "disconnected", turnId, text: "" };
3011
+ const out = await this.#trackTurn(tid, turnId);
3012
+ return { ...out, ...merged ? { merged } : {} };
3013
+ }
3014
+ #trackTurn(threadId, turnId) {
3015
+ return new Promise((resolve5) => {
3016
+ const limit = this.#d.turnLimitMs ?? TURN_LIMIT_MS;
3017
+ const step = this.#d.turnTickMs ?? 1e3;
3018
+ const t = {
3019
+ threadId,
3020
+ turnId,
3021
+ items: [],
3022
+ used: 0,
3023
+ timer: null,
3024
+ grace: null,
3025
+ done: (end, text, error) => resolve5({ sent: true, end, turnId, text, ...error ? { error } : {} })
3026
+ };
3027
+ this.#turns.set(turnId, t);
3028
+ const early = this.#early.get(turnId);
3029
+ if (early) {
3030
+ this.#early.delete(turnId);
3031
+ const items = Array.isArray(early.items) ? early.items : [];
3032
+ const st = early.status;
3033
+ const err = early.error?.message;
3034
+ this.#finishTurn(t, st === "interrupted" ? "interrupted" : st === "failed" ? "failed" : "completed", finalReply(items), typeof err === "string" ? err : void 0);
3035
+ return;
3036
+ }
3037
+ t.timer = setInterval(() => {
3038
+ if (this.#waitingApproval(threadId)) return;
3039
+ t.used += step;
3040
+ if (t.used < limit) return;
3041
+ this.#d.log(`Codex \u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u8D85\u8FC7 ${Math.round(limit / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u6253\u65AD`);
3042
+ void this.#request("turn/interrupt", { threadId, turnId });
3043
+ this.#finishTurn(t, "timeout");
3044
+ }, step);
3045
+ t.timer.unref?.();
3046
+ if (this.#status.get(threadId) !== "active") this.#armIdleGrace(t);
3047
+ });
3048
+ }
3049
+ #waitingApproval(threadId) {
3050
+ for (const p of this.#pending.values()) if (p.threadId === threadId) return true;
3051
+ return false;
3052
+ }
3053
+ /** 会话已空闲却等不到 turn/completed(丢了一条通知):过一会儿按已完成收尾 */
3054
+ #armIdleGrace(t) {
3055
+ if (t.grace) return;
3056
+ t.grace = setTimeout(() => {
3057
+ t.grace = null;
3058
+ if (!this.#turns.has(t.turnId) || this.#status.get(t.threadId) === "active") return;
3059
+ this.#d.log("Codex \u4F1A\u8BDD\u5DF2\u7A7A\u95F2\u4F46\u6CA1\u6536\u5230 turn/completed\uFF0C\u6309\u5DF2\u5B8C\u6210\u6536\u5C3E");
3060
+ this.#finishTurn(t, "completed", finalReply(t.items));
3061
+ }, IDLE_GRACE_MS);
3062
+ t.grace.unref?.();
3063
+ }
3064
+ #finishTurn(t, end, text = "", error) {
3065
+ if (this.#turns.get(t.turnId) !== t) return;
3066
+ this.#turns.delete(t.turnId);
3067
+ if (t.timer) clearInterval(t.timer);
3068
+ if (t.grace) clearTimeout(t.grace);
3069
+ t.done(end, text || (end === "completed" ? "" : finalReply(t.items)), error);
3070
+ }
3071
+ #waitThread(threadId, ms) {
3072
+ return new Promise((resolve5) => {
3073
+ let set = this.#wake.get(threadId);
3074
+ if (!set) this.#wake.set(threadId, set = /* @__PURE__ */ new Set());
3075
+ const s = set;
3076
+ const fire = () => {
3077
+ clearTimeout(timer);
3078
+ s.delete(fire);
3079
+ if (s.size === 0 && this.#wake.get(threadId) === s) this.#wake.delete(threadId);
3080
+ resolve5();
3081
+ };
3082
+ const timer = setTimeout(fire, ms);
3083
+ timer.unref?.();
3084
+ s.add(fire);
3085
+ });
3086
+ }
3087
+ #wakeThread(threadId) {
3088
+ const s = this.#wake.get(threadId);
3089
+ if (s) for (const f of [...s]) f();
3090
+ }
3091
+ #wakeAll() {
3092
+ for (const id of [...this.#wake.keys()]) this.#wakeThread(id);
3093
+ }
2431
3094
  #onMessage(raw) {
2432
3095
  let m;
2433
3096
  try {
@@ -2458,29 +3121,64 @@ var CodexWatch = class {
2458
3121
  case "thread/started": {
2459
3122
  const t = p.thread;
2460
3123
  if (t && typeof t.id === "string" && typeof t.cwd === "string") this.#cwd.set(t.id, t.cwd);
3124
+ if (t && typeof t.id === "string") this.#setLoaded(t.id, true);
2461
3125
  return;
2462
3126
  }
2463
3127
  case "thread/status/changed": {
2464
3128
  const id = typeof p.threadId === "string" ? p.threadId : "";
2465
3129
  const type = p.status?.type;
2466
3130
  if (!id) return;
3131
+ if (typeof type === "string") this.#status.set(id, type);
3132
+ if (type === "notLoaded") this.#setLoaded(id, false);
3133
+ else if (typeof type === "string") this.#setLoaded(id, true);
2467
3134
  if (type === "active") {
2468
3135
  this.#idle.delete(id);
2469
3136
  this.#subscribe(id);
2470
3137
  } else {
2471
3138
  this.#idle.add(id);
2472
3139
  this.#unsubscribe(id);
3140
+ for (const t of this.#turns.values()) if (t.threadId === id) this.#armIdleGrace(t);
2473
3141
  }
3142
+ this.#wakeThread(id);
2474
3143
  return;
2475
3144
  }
2476
3145
  case "thread/closed":
2477
3146
  if (typeof p.threadId === "string") {
2478
- this.#idle.delete(p.threadId);
2479
- this.#subscribed.delete(p.threadId);
2480
- this.#retrying.delete(p.threadId);
2481
- this.#cwd.delete(p.threadId);
3147
+ const id = p.threadId;
3148
+ this.#idle.delete(id);
3149
+ this.#subscribed.delete(id);
3150
+ this.#retrying.delete(id);
3151
+ this.#cwd.delete(id);
3152
+ this.#status.set(id, "notLoaded");
3153
+ this.#activeTurn.delete(id);
3154
+ this.#setLoaded(id, false);
3155
+ for (const t of [...this.#turns.values()]) if (t.threadId === id) this.#finishTurn(t, "closed");
3156
+ this.#wakeThread(id);
2482
3157
  }
2483
3158
  return;
3159
+ case "turn/started": {
3160
+ const turn = p.turn;
3161
+ if (typeof p.threadId === "string" && turn && typeof turn.id === "string") this.#activeTurn.set(p.threadId, turn.id);
3162
+ return;
3163
+ }
3164
+ case "turn/completed": {
3165
+ const turn = p.turn ?? {};
3166
+ const tid = typeof p.threadId === "string" ? p.threadId : "";
3167
+ if (tid && this.#activeTurn.get(tid) === turn.id) this.#activeTurn.delete(tid);
3168
+ const own = typeof turn.id === "string" ? this.#turns.get(turn.id) : void 0;
3169
+ if (own) {
3170
+ const items = Array.isArray(turn.items) ? turn.items : [];
3171
+ const text = finalReply(items) || finalReply(own.items);
3172
+ const st = turn.status;
3173
+ const end = st === "interrupted" ? "interrupted" : st === "failed" ? "failed" : "completed";
3174
+ const err = turn.error?.message;
3175
+ this.#finishTurn(own, end, text, typeof err === "string" ? err : void 0);
3176
+ } else if (typeof turn.id === "string") {
3177
+ this.#early.set(turn.id, turn);
3178
+ while (this.#early.size > 16) this.#early.delete(this.#early.keys().next().value);
3179
+ }
3180
+ return;
3181
+ }
2484
3182
  case "item/started": {
2485
3183
  const item = p.item;
2486
3184
  if (item && item.type === "fileChange" && typeof item.id === "string" && Array.isArray(item.changes)) {
@@ -2492,6 +3190,8 @@ var CodexWatch = class {
2492
3190
  case "item/completed": {
2493
3191
  const item = p.item;
2494
3192
  if (item && typeof item.id === "string") this.#changes.delete(item.id);
3193
+ const own = typeof p.turnId === "string" ? this.#turns.get(p.turnId) : void 0;
3194
+ if (own && item && item.type === "agentMessage") own.items.push(item);
2495
3195
  return;
2496
3196
  }
2497
3197
  case "serverRequest/resolved": {
@@ -2528,7 +3228,7 @@ var CodexWatch = class {
2528
3228
  const ac = new AbortController();
2529
3229
  const entry = { ac, threadId };
2530
3230
  this.#pending.set(key, entry);
2531
- const req = {
3231
+ const req2 = {
2532
3232
  window: true,
2533
3233
  tool: "codex",
2534
3234
  hook: { hook_event_name: "PermissionRequest", session_id: threadId, ...hook }
@@ -2536,7 +3236,7 @@ var CodexWatch = class {
2536
3236
  const settled = () => {
2537
3237
  if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
2538
3238
  };
2539
- void this.#d.gate(req, ac.signal).then((reply) => {
3239
+ void this.#d.gate(req2, ac.signal).then((reply) => {
2540
3240
  if (this.#pending.get(key) !== entry) return;
2541
3241
  this.#pending.delete(key);
2542
3242
  if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
@@ -2549,84 +3249,461 @@ var CodexWatch = class {
2549
3249
  }
2550
3250
  };
2551
3251
 
2552
- // src/daemon/index.ts
2553
- var IDLE_TIMEOUT_MS = 150 * 1e3;
2554
- var POLICY_VIOLATION = 1008;
2555
- var PING_FRAME_SERVICE = "ping";
2556
- var PONG_FRAME = JSON.stringify({ service: "pong", version: "1.0" });
2557
- var CODEX_WATCH_MS = 5 * 1e3;
2558
- function defaultCodexAppServer() {
2559
- const cfg = loadConfig();
2560
- return cfg.enabledTools.includes("codex") ? discoverAppServer(cfg.codexAppServer) : null;
2561
- }
2562
- function opencodeReady() {
2563
- return isOcPluginInstalled(opencodePluginPath()) && existsSync10(ocPluginModule(installedHookPath()));
3252
+ // src/core/codexRemote.ts
3253
+ import { mkdirSync as mkdirSync12, renameSync as renameSync9, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
3254
+ import { join as join15 } from "path";
3255
+ function codexRemotePath() {
3256
+ return join15(deskbuddyHome(), "codex-remote.json");
2564
3257
  }
2565
- function orphanResumeText(summary) {
2566
- return `\uFF08\u4F60\u5728\u8BBE\u5907\u4E0A\u5DF2\u6279\u51C6\uFF1A${summary}\uFF09\u8BF7\u7EE7\u7EED\u521A\u624D\u88AB\u4E2D\u65AD\u7684\u64CD\u4F5C\u3002`;
3258
+ function writeCodexRemote(threads, pid = process.pid, now = Date.now()) {
3259
+ try {
3260
+ mkdirSync12(deskbuddyHome(), { recursive: true });
3261
+ const p = codexRemotePath();
3262
+ writeFileSync11(p + ".tmp", JSON.stringify({ pid, threads: [...threads], at: now }), "utf8");
3263
+ renameSync9(p + ".tmp", p);
3264
+ } catch {
3265
+ }
2567
3266
  }
2568
- var ALIVE_REPORT_MS = 60 * 1e3;
2569
- var Daemon = class {
2570
- #queue = new SessionQueue();
2571
- #seen = SeenIds.load();
2572
- #deps;
2573
- #store = ApprovalStore.load();
2574
- #coord;
2575
- #gate;
2576
- /** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
2577
- #runsDone = /* @__PURE__ */ new Map();
2578
- #attempt = 0;
2579
- #stopped = false;
2580
- #ws = null;
2581
- #idleTimer = null;
2582
- #windowScan = null;
2583
- #aliveReport = null;
2584
- #codexTimer = null;
2585
- #codex = null;
2586
- constructor(deps = {}) {
2587
- this.#deps = {
2588
- connect: deps.connect ?? ((url) => new WebSocket2(url)),
2589
- now: deps.now ?? (() => Date.now()),
2590
- backend: deps.backend ?? serverBackend(),
2591
- gateScript: deps.gateScript ?? defaultGateScript(),
2592
- // 🔴 优先用 PATH 上那个稳定的 node(如 /opt/homebrew/bin/node 这种链接),不用 process.execPath:
2593
- // 后者是 Cellar 里的真实路径,`brew upgrade node` 之后就被删了,而 daemon 还活着、
2594
- // hook 命令里写的还是旧路径 —— hook 起不来,Qoder 在 bypass 下就是全放行(评审 R3)
2595
- nodePath: deps.nodePath ?? whichIn("node", defaultLocateDeps()) ?? process.execPath,
2596
- materialize: deps.materialize ?? ((plan, tool) => materialize(plan, tool)),
2597
- opencodeReady: deps.opencodeReady ?? opencodeReady,
2598
- codexAppServer: deps.codexAppServer ?? defaultCodexAppServer
2599
- };
2600
- this.#store.prune(this.#deps.now());
2601
- this.#coord = new ApprovalCoordinator({
2602
- store: this.#store,
2603
- backend: this.#deps.backend,
2604
- policy: () => loadConfig().approvalPolicy ?? "commands",
2605
- resume: (r, kind) => this.#scheduleResume(r, kind),
2606
- log,
2607
- now: this.#deps.now
2608
- });
2609
- this.#gate = new GateServer((req, signal) => this.#coord.onGate(req, signal));
3267
+ function removeCodexRemote() {
3268
+ try {
3269
+ rmSync4(codexRemotePath(), { force: true });
3270
+ } catch {
2610
3271
  }
2611
- /** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
2612
- async startGate() {
2613
- const port = await this.#gate.start();
3272
+ }
3273
+
3274
+ // src/wrap/channel.ts
3275
+ import { chmodSync as chmodSync2, rmSync as rmSync5 } from "fs";
3276
+ import { connect, createServer as createServer2 } from "net";
3277
+ var MAX_LINE_BYTES = 64 * 1024;
3278
+ function lines(sock, onLine) {
3279
+ let buf = "";
3280
+ sock.setEncoding("utf8");
3281
+ sock.on("data", (d) => {
3282
+ buf += d;
3283
+ if (buf.length > MAX_LINE_BYTES) {
3284
+ sock.destroy();
3285
+ return;
3286
+ }
3287
+ let i;
3288
+ while ((i = buf.indexOf("\n")) >= 0) {
3289
+ const line = buf.slice(0, i);
3290
+ buf = buf.slice(i + 1);
3291
+ if (line.trim()) onLine(line);
3292
+ }
3293
+ });
3294
+ }
3295
+ function parseRequest(line) {
3296
+ let m;
3297
+ try {
3298
+ m = JSON.parse(line);
3299
+ } catch {
3300
+ return null;
3301
+ }
3302
+ if (!m || typeof m !== "object") return null;
3303
+ const instructionId = typeof m.instructionId === "string" ? m.instructionId : "";
3304
+ const sessionId = typeof m.sessionId === "string" ? m.sessionId : "";
3305
+ if (!isValidId(instructionId) || !isValidId(sessionId)) return null;
3306
+ const text = sanitizeText(typeof m.text === "string" ? m.text : "");
3307
+ if (!text || text.length > MAX_TEXT_LENGTH) return null;
3308
+ const expireAt = typeof m.expireAt === "number" && Number.isFinite(m.expireAt) ? m.expireAt : 0;
3309
+ return { instructionId, sessionId, text, expireAt };
3310
+ }
3311
+ function startChannelServer(path, onRequest, platform = process.platform) {
3312
+ return new Promise((resolve5) => {
3313
+ const server = createServer2((sock) => {
3314
+ let open = true;
3315
+ sock.on("error", () => {
3316
+ });
3317
+ sock.on("close", () => open = false);
3318
+ const reply = (r) => {
3319
+ if (sock.destroyed || !sock.writable) return;
3320
+ try {
3321
+ sock.write(JSON.stringify(r) + "\n");
3322
+ } catch {
3323
+ }
3324
+ };
3325
+ lines(sock, (line) => {
3326
+ const req2 = parseRequest(line);
3327
+ if (!req2) {
3328
+ reply({ instructionId: "", status: "rejected", reason: "bad_request" });
3329
+ return;
3330
+ }
3331
+ onRequest(req2, reply, () => open);
3332
+ });
3333
+ });
3334
+ if (platform !== "win32") {
3335
+ try {
3336
+ rmSync5(path, { force: true });
3337
+ } catch {
3338
+ }
3339
+ }
3340
+ const oldMask = platform === "win32" ? null : process.umask(127);
3341
+ const restore = () => {
3342
+ if (oldMask !== null) process.umask(oldMask);
3343
+ };
3344
+ server.once("error", () => {
3345
+ restore();
3346
+ resolve5(null);
3347
+ });
3348
+ server.listen(path, () => {
3349
+ restore();
3350
+ if (platform !== "win32") {
3351
+ try {
3352
+ chmodSync2(path, 384);
3353
+ } catch {
3354
+ }
3355
+ }
3356
+ server.on("error", () => {
3357
+ });
3358
+ resolve5(server);
3359
+ });
3360
+ });
3361
+ }
3362
+ function closeChannelServer(server, path, platform = process.platform) {
3363
+ try {
3364
+ server?.close();
3365
+ } catch {
3366
+ }
3367
+ if (platform !== "win32") {
3368
+ try {
3369
+ rmSync5(path, { force: true });
3370
+ } catch {
3371
+ }
3372
+ }
3373
+ }
3374
+ var FINAL = /* @__PURE__ */ new Set(["started", "rejected", "failed", "not_here"]);
3375
+ function sendToWindow(path, req2, onReply, opts = {}) {
3376
+ return new Promise((resolve5) => {
3377
+ let settled = false;
3378
+ let handled = false;
3379
+ let done = false;
3380
+ const settle = (v) => {
3381
+ if (settled) return;
3382
+ settled = true;
3383
+ handled = v === "handled";
3384
+ clearTimeout(timer);
3385
+ resolve5(v);
3386
+ };
3387
+ let sock;
3388
+ try {
3389
+ sock = connect(path);
3390
+ } catch {
3391
+ resolve5("fallback");
3392
+ return;
3393
+ }
3394
+ const timer = setTimeout(() => {
3395
+ settle("fallback");
3396
+ sock.destroy();
3397
+ }, opts.ackTimeoutMs ?? 3e3);
3398
+ sock.on("error", () => {
3399
+ if (!settled) settle("fallback");
3400
+ });
3401
+ sock.on("close", () => {
3402
+ if (!settled) {
3403
+ settle("fallback");
3404
+ return;
3405
+ }
3406
+ if (handled && !done) {
3407
+ done = true;
3408
+ onReply({ instructionId: req2.instructionId, status: "failed", mode: "window", reason: "window_failed" });
3409
+ }
3410
+ });
3411
+ sock.on("connect", () => {
3412
+ sock.write(JSON.stringify(req2) + "\n");
3413
+ });
3414
+ lines(sock, (line) => {
3415
+ let r;
3416
+ try {
3417
+ r = JSON.parse(line);
3418
+ } catch {
3419
+ return;
3420
+ }
3421
+ if (r.instructionId !== req2.instructionId) return;
3422
+ if (!settled) {
3423
+ if (r.status === "ack") {
3424
+ settle("handled");
3425
+ return;
3426
+ }
3427
+ settle("fallback");
3428
+ sock.destroy();
3429
+ return;
3430
+ }
3431
+ if (done) return;
3432
+ if (FINAL.has(r.status)) {
3433
+ done = true;
3434
+ onReply(r);
3435
+ sock.end();
3436
+ return;
3437
+ }
3438
+ onReply(r);
3439
+ });
3440
+ });
3441
+ }
3442
+
3443
+ // src/core/wrapWindows.ts
3444
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync2, readFileSync as readFileSync12, renameSync as renameSync10, rmSync as rmSync6, statSync as statSync3, writeFileSync as writeFileSync12 } from "fs";
3445
+ import { tmpdir } from "os";
3446
+ import { join as join16 } from "path";
3447
+ var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
3448
+ function windowsDir() {
3449
+ return join16(deskbuddyHome(), "windows");
3450
+ }
3451
+ function bySessionDir() {
3452
+ return join16(windowsDir(), "by-session");
3453
+ }
3454
+ function safeSid(sessionId) {
3455
+ return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
3456
+ }
3457
+ function ensureDir(dir) {
3458
+ mkdirSync13(dir, { recursive: true, mode: 448 });
3459
+ }
3460
+ function atomicWrite(file, text) {
3461
+ const tmp = `${file}.${process.pid}.tmp`;
3462
+ writeFileSync12(tmp, text, { encoding: "utf8", mode: 384 });
3463
+ renameSync10(tmp, file);
3464
+ }
3465
+ function windowFile(windowId) {
3466
+ return join16(windowsDir(), `${windowId}.json`);
3467
+ }
3468
+ function writeWindow(windowId, rec) {
3469
+ if (!WINDOW_ID_RE.test(windowId)) return;
3470
+ ensureDir(windowsDir());
3471
+ atomicWrite(windowFile(windowId), JSON.stringify(rec));
3472
+ }
3473
+ function readWindow(windowId) {
3474
+ if (!WINDOW_ID_RE.test(windowId)) return null;
3475
+ try {
3476
+ const r = JSON.parse(readFileSync12(windowFile(windowId), "utf8"));
3477
+ return r && typeof r.pid === "number" ? r : null;
3478
+ } catch {
3479
+ return null;
3480
+ }
3481
+ }
3482
+ function removeWindow(windowId) {
3483
+ if (!WINDOW_ID_RE.test(windowId)) return;
3484
+ for (const f of [windowFile(windowId), lastEventFile(windowId)]) {
3485
+ try {
3486
+ rmSync6(f, { force: true });
3487
+ } catch {
3488
+ }
3489
+ }
3490
+ let names = [];
3491
+ try {
3492
+ names = readdirSync2(bySessionDir());
3493
+ } catch {
3494
+ return;
3495
+ }
3496
+ for (const n of names) {
3497
+ const f = join16(bySessionDir(), n);
3498
+ try {
3499
+ if (readFileSync12(f, "utf8").trim() === windowId) rmSync6(f, { force: true });
3500
+ } catch {
3501
+ }
3502
+ }
3503
+ }
3504
+ function readBySession(sessionId) {
3505
+ const safe = safeSid(sessionId);
3506
+ if (!safe) return null;
3507
+ try {
3508
+ const id = readFileSync12(join16(bySessionDir(), safe), "utf8").trim();
3509
+ return WINDOW_ID_RE.test(id) ? id : null;
3510
+ } catch {
3511
+ return null;
3512
+ }
3513
+ }
3514
+ function lastEventFile(windowId) {
3515
+ return join16(windowsDir(), `${windowId}.last`);
3516
+ }
3517
+ function readLastEvent(windowId) {
3518
+ if (!WINDOW_ID_RE.test(windowId)) return null;
3519
+ try {
3520
+ const e = JSON.parse(readFileSync12(lastEventFile(windowId), "utf8"));
3521
+ return e && typeof e.ev === "string" && typeof e.at === "number" ? { ev: e.ev, sid: String(e.sid ?? ""), at: e.at } : null;
3522
+ } catch {
3523
+ return null;
3524
+ }
3525
+ }
3526
+ function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
3527
+ if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
3528
+ const name = `${windowId.slice(0, 16)}.sock`;
3529
+ const primary = join16(home, "win", name);
3530
+ if (Buffer.byteLength(primary) <= 103) return primary;
3531
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
3532
+ return join16(tmpdir(), `deskbuddy-${uid}`, name);
3533
+ }
3534
+ function ensureChannelDir(sockPath) {
3535
+ const dir = join16(sockPath, "..");
3536
+ try {
3537
+ mkdirSync13(dir, { recursive: true, mode: 448 });
3538
+ const st = statSync3(dir);
3539
+ if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
3540
+ chmodSync3(dir, 448);
3541
+ return true;
3542
+ } catch {
3543
+ return false;
3544
+ }
3545
+ }
3546
+
3547
+ // src/daemon/window.ts
3548
+ function windowForSession(sessionId) {
3549
+ const id = readBySession(sessionId);
3550
+ if (!id) return null;
3551
+ const rec = readWindow(id);
3552
+ if (!rec || !isAlive(rec.pid)) return null;
3553
+ return id;
3554
+ }
3555
+
3556
+ // src/daemon/index.ts
3557
+ var IDLE_TIMEOUT_MS = 150 * 1e3;
3558
+ var POLICY_VIOLATION = 1008;
3559
+ var PING_FRAME_SERVICE = "ping";
3560
+ var PONG_FRAME = JSON.stringify({ service: "pong", version: "1.0" });
3561
+ var CODEX_REMOTE_REFRESH_MS = 30 * 1e3;
3562
+ var CODEX_WATCH_MS = 5 * 1e3;
3563
+ function defaultCodexAppServers() {
3564
+ const cfg = loadConfig();
3565
+ return cfg.enabledTools.includes("codex") ? discoverAppServers(cfg.codexAppServer) : [];
3566
+ }
3567
+ function opencodeReady() {
3568
+ return isOcPluginInstalled(opencodePluginPath()) && existsSync10(ocPluginModule(installedHookPath()));
3569
+ }
3570
+ function orphanResumeText(summary2) {
3571
+ 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`;
3572
+ }
3573
+ var ALIVE_REPORT_MS = 60 * 1e3;
3574
+ var Daemon = class {
3575
+ #queue = new SessionQueue();
3576
+ #seen = SeenIds.load();
3577
+ #deps;
3578
+ #store = ApprovalStore.load();
3579
+ #coord;
3580
+ #gate;
3581
+ /** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
3582
+ #runsDone = /* @__PURE__ */ new Map();
3583
+ #attempt = 0;
3584
+ #stopped = false;
3585
+ #ws = null;
3586
+ #idleTimer = null;
3587
+ #windowScan = null;
3588
+ #aliveReport = null;
3589
+ #codexTimer = null;
3590
+ /** url → 连着它的 CodexWatch。插入顺序 = 发现顺序(DeskBuddy 管的在前) */
3591
+ #codex = /* @__PURE__ */ new Map();
3592
+ #codexStarted = false;
3593
+ #codexReportTimer = null;
3594
+ constructor(deps = {}) {
3595
+ this.#deps = {
3596
+ connect: deps.connect ?? ((url) => new WebSocket2(url)),
3597
+ now: deps.now ?? (() => Date.now()),
3598
+ backend: deps.backend ?? serverBackend(),
3599
+ gateScript: deps.gateScript ?? defaultGateScript(),
3600
+ // 🔴 优先用 PATH 上那个稳定的 node(如 /opt/homebrew/bin/node 这种链接),不用 process.execPath:
3601
+ // 后者是 Cellar 里的真实路径,`brew upgrade node` 之后就被删了,而 daemon 还活着、
3602
+ // hook 命令里写的还是旧路径 —— hook 起不来,Qoder 在 bypass 下就是全放行(评审 R3)
3603
+ nodePath: deps.nodePath ?? whichIn("node", defaultLocateDeps()) ?? process.execPath,
3604
+ materialize: deps.materialize ?? ((plan, tool) => materialize(plan, tool)),
3605
+ opencodeReady: deps.opencodeReady ?? opencodeReady,
3606
+ codexAppServers: deps.codexAppServers ?? defaultCodexAppServers,
3607
+ codexRoute: deps.codexRoute ?? ((tid) => this.#codexRouteFor(tid)),
3608
+ report: deps.report ?? ((p) => api.commandResult(p)),
3609
+ windowFor: deps.windowFor ?? windowForSession,
3610
+ sendToWindow: deps.sendToWindow ?? ((id, req2, onReply) => sendToWindow(channelPath(id), req2, onReply))
3611
+ };
3612
+ this.#store.prune(this.#deps.now());
3613
+ this.#coord = new ApprovalCoordinator({
3614
+ store: this.#store,
3615
+ backend: this.#deps.backend,
3616
+ policy: () => loadConfig().approvalPolicy ?? "commands",
3617
+ resume: (r, kind) => this.#scheduleResume(r, kind),
3618
+ log,
3619
+ now: this.#deps.now
3620
+ });
3621
+ this.#gate = new GateServer((req2, signal) => this.#coord.onGate(req2, signal));
3622
+ }
3623
+ /** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
3624
+ async startGate() {
3625
+ const port = await this.#gate.start();
2614
3626
  log(`\u6388\u6743\u7AEF\u70B9\u5DF2\u5C31\u7EEA 127.0.0.1:${port}`);
2615
3627
  this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
2616
3628
  this.#windowScan.unref();
2617
3629
  this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
2618
3630
  this.#aliveReport.unref();
2619
- this.#codex = new CodexWatch({
2620
- gate: (req, signal) => this.#coord.onGate(req, signal),
2621
- log,
2622
- discover: () => this.#deps.codexAppServer(),
2623
- enabled: () => loadConfig().enabledTools.includes("codex")
2624
- });
2625
- this.#codex.tick();
2626
- this.#codexTimer = setInterval(() => this.#codex?.tick(), CODEX_WATCH_MS);
3631
+ this.#codexStarted = true;
3632
+ this.#codexTick();
3633
+ this.#codexTimer = setInterval(() => this.#codexTick(), CODEX_WATCH_MS);
2627
3634
  this.#codexTimer.unref();
2628
3635
  return port;
2629
3636
  }
3637
+ /** 找 app-server:新出现的连上,不在了且已断开的丢掉;连着的各自 tick */
3638
+ #codexTick() {
3639
+ let urls = [];
3640
+ try {
3641
+ urls = this.#deps.codexAppServers();
3642
+ } catch {
3643
+ urls = [];
3644
+ }
3645
+ for (const url of urls) {
3646
+ if (this.#codex.has(url)) continue;
3647
+ this.#codex.set(
3648
+ url,
3649
+ new CodexWatch({
3650
+ gate: (req2, signal) => this.#coord.onGate(req2, signal),
3651
+ log,
3652
+ discover: () => url,
3653
+ enabled: () => loadConfig().enabledTools.includes("codex"),
3654
+ onLoadedChange: () => this.#onCodexLoaded()
3655
+ })
3656
+ );
3657
+ }
3658
+ for (const [url, w] of [...this.#codex]) {
3659
+ if (!urls.includes(url) && !w.connected) {
3660
+ w.stop();
3661
+ this.#codex.delete(url);
3662
+ continue;
3663
+ }
3664
+ w.tick();
3665
+ }
3666
+ if (this.#deps.now() - this.#codexRemoteAt >= CODEX_REMOTE_REFRESH_MS) this.#writeCodexRemote();
3667
+ }
3668
+ #codexRemoteAt = 0;
3669
+ #writeCodexRemote() {
3670
+ this.#codexRemoteAt = this.#deps.now();
3671
+ writeCodexRemote(this.#codexLoaded(), process.pid, this.#codexRemoteAt);
3672
+ }
3673
+ /** 哪个 app-server 里加载着这个会话;不止一个 → 'ambiguous'(评审 #6) */
3674
+ #codexRouteFor(threadId) {
3675
+ const hits = [...this.#codex.values()].filter((w) => w.hasLoaded(threadId));
3676
+ if (hits.length > 1) return "ambiguous";
3677
+ return hits[0] ?? null;
3678
+ }
3679
+ #codexLoaded() {
3680
+ const all = /* @__PURE__ */ new Set();
3681
+ for (const w of this.#codex.values()) for (const id of w.loadedThreads()) all.add(id);
3682
+ return all;
3683
+ }
3684
+ /**
3685
+ * 已加载的会话变了(切片 B):落清单给 hook 用,并立刻补报一次 codex 快照(remoteWindow 进 / 出),
3686
+ * 否则设备要等下一次 hook 事件或 60 秒保活才知道
3687
+ */
3688
+ #onCodexLoaded() {
3689
+ if (!this.#codexStarted) return;
3690
+ this.#writeCodexRemote();
3691
+ if (this.#codexReportTimer) return;
3692
+ this.#codexReportTimer = setTimeout(() => {
3693
+ this.#codexReportTimer = null;
3694
+ void this.#reportCodex();
3695
+ }, 300);
3696
+ this.#codexReportTimer.unref?.();
3697
+ }
3698
+ async #reportCodex() {
3699
+ const cfg = loadConfig();
3700
+ if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes("codex")) return;
3701
+ try {
3702
+ await postReport(buildSnapshot(readAliveStates("codex"), "codex", cfg, this.#deps.now(), this.#codexLoaded()));
3703
+ } catch (e) {
3704
+ log(`\u4E0A\u62A5 codex \u5FEB\u7167\u5931\u8D25\uFF1A${e.message}`);
3705
+ }
3706
+ }
2630
3707
  #lastDirs = "";
2631
3708
  #dirsTick = 0;
2632
3709
  /**
@@ -2651,7 +3728,7 @@ var Daemon = class {
2651
3728
  const cfg = loadConfig();
2652
3729
  if (!cfg.clientId || !cfg.boundAt) return;
2653
3730
  for (const tool of cfg.enabledTools) {
2654
- await postReport(buildSnapshot(readAliveStates(tool), tool, cfg, this.#deps.now()));
3731
+ await postReport(buildSnapshot(readAliveStates(tool), tool, cfg, this.#deps.now(), tool === "codex" ? this.#codexLoaded() : null));
2655
3732
  }
2656
3733
  if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
2657
3734
  }
@@ -2664,7 +3741,10 @@ var Daemon = class {
2664
3741
  if (this.#windowScan) clearInterval(this.#windowScan);
2665
3742
  if (this.#aliveReport) clearInterval(this.#aliveReport);
2666
3743
  if (this.#codexTimer) clearInterval(this.#codexTimer);
2667
- this.#codex?.stop();
3744
+ if (this.#codexReportTimer) clearTimeout(this.#codexReportTimer);
3745
+ for (const w of this.#codex.values()) w.stop();
3746
+ this.#codex.clear();
3747
+ if (this.#codexStarted) removeCodexRemote();
2668
3748
  this.#ws?.close();
2669
3749
  void this.#gate.stop();
2670
3750
  }
@@ -2768,8 +3848,139 @@ var Daemon = class {
2768
3848
  return;
2769
3849
  }
2770
3850
  this.#seen.save();
3851
+ if (cmd.sessionId && !isExpired(cmd, this.#deps.now())) {
3852
+ const sid = resolveAlias(cmd.sessionId);
3853
+ let win = null;
3854
+ try {
3855
+ win = this.#deps.windowFor(sid);
3856
+ } catch {
3857
+ win = null;
3858
+ }
3859
+ if (win) {
3860
+ void this.#toWindow(cmd, sid, win);
3861
+ return;
3862
+ }
3863
+ if (cmd.tool === "codex" && !this.#queue.busy(sid)) {
3864
+ let route = null;
3865
+ try {
3866
+ route = this.#deps.codexRoute(sid);
3867
+ } catch {
3868
+ route = null;
3869
+ }
3870
+ if (route === "ambiguous") {
3871
+ log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u4F1A\u8BDD\u540C\u65F6\u5F00\u5728\u591A\u4E2A\u7A97\u53E3\u91CC\uFF0C\u4E0D\u77E5\u9053\u53D1\u7ED9\u54EA\u4E2A`);
3872
+ this.#report(cmd, sid, "failed", "window", "window_failed");
3873
+ return;
3874
+ }
3875
+ if (route) {
3876
+ void this.#toCodexWindow(cmd, sid, route);
3877
+ return;
3878
+ }
3879
+ }
3880
+ }
3881
+ this.#dispatch(cmd);
3882
+ }
3883
+ /**
3884
+ * 经 app-server 往 Codex 会话里发一轮(切片 B)。回报语义与 Claude 窗口一致(mode=window):
3885
+ * 排队 queued → 发出 started;排队中过期 rejected/expired。
3886
+ * 🔴 只有确定窗口里没有这个会话(未连上 / 未加载 / 会话已关)才退回后台 exec;
3887
+ * 订阅失败 / turn/start 被拒 / 发之前断线 → failed/window_failed(会话还开在窗口里,后台跑会两个进程抢一个会话);
3888
+ * 发出去之后任何失败 → failed/exec_failed,绝不再后台重跑(会执行两遍)
3889
+ */
3890
+ async #toCodexWindow(cmd, sessionId, route) {
3891
+ let out;
3892
+ try {
3893
+ out = await route.runTurn(
3894
+ { threadId: sessionId, text: cmd.text, expireAt: cmd.expireAt },
3895
+ {
3896
+ onQueued: () => {
3897
+ log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u4F1A\u8BDD\u6B63\u5FD9\uFF0C\u6392\u961F`);
3898
+ this.#report(cmd, sessionId, "queued", "window");
3899
+ },
3900
+ onStarted: () => {
3901
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u5728 Codex \u7A97\u53E3\u4F1A\u8BDD\u91CC\u53D1\u51FA len=${cmd.text.length}`);
3902
+ this.#report(cmd, sessionId, "started", "window");
3903
+ }
3904
+ }
3905
+ );
3906
+ } catch (e) {
3907
+ out = { sent: false, reason: "start_failed" };
3908
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u7ECF app-server \u53D1\u51FA\u51FA\u9519\uFF1A${e.message}`);
3909
+ }
3910
+ if (!out.sent) {
3911
+ if (out.reason === "expired") {
3912
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u6392\u961F\u7B49 Codex \u4F1A\u8BDD\u7A7A\u95F2\u65F6\u8FC7\u671F`);
3913
+ this.#report(cmd, sessionId, "rejected", "window", "expired");
3914
+ return;
3915
+ }
3916
+ if (out.reason === "not_connected" || out.reason === "not_loaded" || out.reason === "closed") {
3917
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u91CC\u5DF2\u6CA1\u6709\u8FD9\u4E2A Codex \u4F1A\u8BDD\uFF08${out.reason}\uFF09\uFF0C\u6539\u8D70\u540E\u53F0`);
3918
+ this.#dispatch(cmd);
3919
+ return;
3920
+ }
3921
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u6CA1\u80FD\u7ECF app-server \u53D1\u51FA\uFF08${out.reason}\uFF09`);
3922
+ this.#report(cmd, sessionId, "failed", "window", "window_failed");
3923
+ return;
3924
+ }
3925
+ if (out.end === "completed") {
3926
+ log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u7A97\u53E3\u8FD9\u4E00\u8F6E\u5B8C\u6210\uFF0C\u56DE\u590D ${out.text.length} \u5B57`);
3927
+ return;
3928
+ }
3929
+ log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u7A97\u53E3\u8FD9\u4E00\u8F6E\u6CA1\u5B8C\u6210\uFF1A${out.end}${out.error ? " " + out.error.slice(0, 120) : ""}`);
3930
+ this.#report(cmd, sessionId, "failed", "window", "exec_failed");
3931
+ }
3932
+ /** 回报一条指令的处理结果。2 秒超时、失败不重试,只记日志(seen 去重命中的不会走到这里) */
3933
+ #report(cmd, sessionId, status, mode, reason = "") {
3934
+ const payload = {
3935
+ instructionId: cmd.instructionId,
3936
+ sessionId,
3937
+ tool: cmd.tool,
3938
+ status,
3939
+ mode,
3940
+ reason: status === "started" || status === "queued" ? "" : reason,
3941
+ at: String(this.#deps.now())
3942
+ };
3943
+ let p;
3944
+ try {
3945
+ p = this.#deps.report(payload);
3946
+ } catch (e) {
3947
+ log(`\u56DE\u62A5\u6307\u4EE4 ${cmd.instructionId} \u7ED3\u679C\u5931\u8D25\uFF1A${e.message}`);
3948
+ return;
3949
+ }
3950
+ p.catch((e) => log(`\u56DE\u62A5\u6307\u4EE4 ${cmd.instructionId} \u7ED3\u679C\u5931\u8D25\uFF1A${e.message}`));
3951
+ }
3952
+ async #toWindow(cmd, sessionId, windowId) {
3953
+ let fellBack = false;
3954
+ const fallback = () => {
3955
+ if (fellBack) return;
3956
+ fellBack = true;
3957
+ this.#dispatch(cmd);
3958
+ };
3959
+ const req2 = { instructionId: cmd.instructionId, sessionId, text: cmd.text, expireAt: cmd.expireAt };
3960
+ let r;
3961
+ try {
3962
+ r = await this.#deps.sendToWindow(windowId, req2, (reply) => {
3963
+ if (reply.status === "not_here") {
3964
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u91CC\u5DF2\u4E0D\u662F\u8FD9\u4E2A\u4F1A\u8BDD\uFF0C\u6539\u8D70\u540E\u53F0`);
3965
+ fallback();
3966
+ return;
3967
+ }
3968
+ if (reply.status === "ack") return;
3969
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3 ${reply.status}${reply.reason ? " " + reply.reason : ""}`);
3970
+ this.#report(cmd, sessionId, reply.status, "window", reply.reason ?? "");
3971
+ });
3972
+ } catch (e) {
3973
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u4EA4\u7ED9\u7A97\u53E3\u51FA\u9519\uFF1A${e.message}`);
3974
+ r = "fallback";
3975
+ }
3976
+ if (r === "fallback") fallback();
3977
+ else log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u4EA4\u7ED9\u7A97\u53E3 len=${cmd.text.length}`);
3978
+ }
3979
+ /** 原来的后台路径:校验 → 定位会话与目录 → 排队执行 */
3980
+ #dispatch(cmd) {
2771
3981
  if (isExpired(cmd, this.#deps.now())) {
2772
3982
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u8FC7\u671F\uFF0C\u4E22\u5F03`);
3983
+ this.#report(cmd, cmd.sessionId, "rejected", "", "expired");
2773
3984
  return;
2774
3985
  }
2775
3986
  const target = resolveTarget(cmd, {
@@ -2783,6 +3994,7 @@ var Daemon = class {
2783
3994
  });
2784
3995
  if (!target.ok) {
2785
3996
  log(`\u6307\u4EE4 ${cmd.instructionId} \u65E0\u6CD5\u6267\u884C\uFF1A${failureMessage(target.reason)}`);
3997
+ this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
2786
3998
  return;
2787
3999
  }
2788
4000
  if (target.sessionId) this.#coord.cancelSession(target.sessionId);
@@ -2793,6 +4005,7 @@ var Daemon = class {
2793
4005
  const fresh = readState(sessionId);
2794
4006
  if (!fresh) {
2795
4007
  log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u4F1A\u8BDD ${sessionId} \u5728\u6267\u884C\u524D\u5DF2\u6D88\u5931`);
4008
+ this.#report(cmd, sessionId, "rejected", "", "session_gone");
2796
4009
  return;
2797
4010
  }
2798
4011
  }
@@ -2800,17 +4013,22 @@ var Daemon = class {
2800
4013
  const plan = buildSpawnPlan({ ...cmd, sessionId }, cwd, this.#gateTarget(runId));
2801
4014
  if (!plan) {
2802
4015
  log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u672C\u673A\u4E0D\u652F\u6301\u8FDC\u7A0B\u6267\u884C ${cmd.tool}`);
4016
+ this.#report(cmd, sessionId, "rejected", "", "tool_unsupported");
2803
4017
  return;
2804
4018
  }
2805
4019
  log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename6(cwd)} len=${cmd.text.length}`);
2806
- const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd);
2807
- if (!result) {
2808
- log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u672C\u673A\u627E\u4E0D\u5230 ${cmd.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`);
4020
+ const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd, () => this.#report(cmd, sessionId, "started", "headless"));
4021
+ if ("fail" in result) {
4022
+ log(
4023
+ result.fail === "gate_unavailable" ? `\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528` : `\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u672C\u673A\u627E\u4E0D\u5230 ${cmd.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`
4024
+ );
4025
+ this.#report(cmd, sessionId, "failed", "", result.fail);
2809
4026
  return;
2810
4027
  }
2811
4028
  if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
2812
4029
  if (!result.ok) {
2813
4030
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error}`);
4031
+ this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
2814
4032
  }
2815
4033
  if (!sessionId) {
2816
4034
  const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
@@ -2856,28 +4074,29 @@ var Daemon = class {
2856
4074
  r.consumed = true;
2857
4075
  this.#store.save();
2858
4076
  }
2859
- if (!result) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u653E\u5F03\uFF1A\u672C\u673A\u627E\u4E0D\u5230 ${r.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`);
4077
+ 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`}`);
2860
4078
  else if (isDeferred(result.stdout)) log(`\u4F1A\u8BDD ${r.sessionId} \u53C8\u505C\u5728\u4E86\u4E0B\u4E00\u4E2A\u7B49\u6388\u6743\u7684\u64CD\u4F5C\u4E0A`);
2861
4079
  else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
2862
4080
  else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
2863
4081
  }
2864
4082
  /**
2865
4083
  * 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
2866
- * 找不到可执行文件返回 null。
4084
+ * 起不了(关卡不可用 / 找不到可执行文件)返回 {fail};进程起来那一刻调 onStarted
2867
4085
  */
2868
- async #run(plan, tool, runId, sessionId, cwd) {
4086
+ async #run(plan, tool, runId, sessionId, cwd, onStarted) {
2869
4087
  if (!existsSync10(this.#deps.nodePath) || !existsSync10(this.#deps.gateScript)) {
2870
4088
  log(`\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528\uFF08node \u6216 gate.cjs \u627E\u4E0D\u5230\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C ${tool}`);
2871
- return null;
4089
+ return { fail: "gate_unavailable" };
2872
4090
  }
2873
4091
  if (tool === "opencode" && !this.#deps.opencodeReady()) {
2874
4092
  log("opencode \u63D2\u4EF6\u4E0D\u5728\uFF08deskbuddy enable opencode \u6CA1\u88C5\u6216\u5DF2\u5378\u8F7D\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C opencode");
2875
- return null;
4093
+ return { fail: "gate_unavailable" };
2876
4094
  }
2877
- const real = this.#deps.materialize(plan, tool);
2878
- if (!real) return null;
4095
+ const real2 = this.#deps.materialize(plan, tool);
4096
+ if (!real2) return { fail: "binary_not_found" };
2879
4097
  const startedAt = this.#deps.now();
2880
- const rc = startCommand(real, tool);
4098
+ const rc = startCommand(real2, tool);
4099
+ onStarted?.();
2881
4100
  this.#coord.registerRun({
2882
4101
  runId,
2883
4102
  tool,
@@ -2918,7 +4137,7 @@ async function runDaemonCommand(opts = {}) {
2918
4137
  }
2919
4138
  if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
2920
4139
  try {
2921
- writeFileSync11(pidFilePath(), String(process.pid), "utf8");
4140
+ writeFileSync13(pidFilePath(), String(process.pid), "utf8");
2922
4141
  } catch {
2923
4142
  }
2924
4143
  let daemon;
@@ -2934,7 +4153,7 @@ async function runDaemonCommand(opts = {}) {
2934
4153
  log(`\u6536\u5230 ${sig}\uFF0C\u9000\u51FA`);
2935
4154
  daemon.stop();
2936
4155
  try {
2937
- rmSync4(pidFilePath(), { force: true });
4156
+ rmSync7(pidFilePath(), { force: true });
2938
4157
  } catch {
2939
4158
  }
2940
4159
  process.exit(0);
@@ -2951,8 +4170,8 @@ async function runDaemonCommand(opts = {}) {
2951
4170
  }
2952
4171
 
2953
4172
  // src/core/registry.ts
2954
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync12 } from "fs";
2955
- import { join as join15 } from "path";
4173
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync14 } from "fs";
4174
+ import { join as join17 } from "path";
2956
4175
  var FALLBACK = {
2957
4176
  version: "builtin",
2958
4177
  tools: {
@@ -3030,11 +4249,11 @@ var FALLBACK = {
3030
4249
  }
3031
4250
  };
3032
4251
  function registryCachePath() {
3033
- return join15(deskbuddyHome(), "tool-registry.json");
4252
+ return join17(deskbuddyHome(), "tool-registry.json");
3034
4253
  }
3035
4254
  function loadRegistry() {
3036
4255
  try {
3037
- return JSON.parse(readFileSync10(registryCachePath(), "utf8"));
4256
+ return JSON.parse(readFileSync13(registryCachePath(), "utf8"));
3038
4257
  } catch {
3039
4258
  return FALLBACK;
3040
4259
  }
@@ -3042,15 +4261,15 @@ function loadRegistry() {
3042
4261
  async function refreshRegistry() {
3043
4262
  try {
3044
4263
  const registry = await api.toolRegistry();
3045
- mkdirSync12(deskbuddyHome(), { recursive: true });
3046
- writeFileSync12(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
4264
+ mkdirSync14(deskbuddyHome(), { recursive: true });
4265
+ writeFileSync14(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
3047
4266
  } catch {
3048
4267
  }
3049
4268
  }
3050
4269
 
3051
4270
  // src/adapters/ccHook.ts
3052
- import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync13, readFileSync as readFileSync11, renameSync as renameSync9, statSync as statSync3, writeFileSync as writeFileSync13 } from "fs";
3053
- import { dirname as dirname6, join as join16 } from "path";
4271
+ import { chmodSync as chmodSync4, existsSync as existsSync11, mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync11, statSync as statSync4, writeFileSync as writeFileSync15 } from "fs";
4272
+ import { dirname as dirname6, join as join18 } from "path";
3054
4273
  var CC_EVENTS = [
3055
4274
  "UserPromptSubmit",
3056
4275
  "PreToolUse",
@@ -3061,7 +4280,7 @@ var CC_EVENTS = [
3061
4280
  ];
3062
4281
  function readSettings(path) {
3063
4282
  if (!existsSync11(path)) return {};
3064
- const raw = readFileSync11(path, "utf8");
4283
+ const raw = readFileSync14(path, "utf8");
3065
4284
  if (raw.trim() === "") return {};
3066
4285
  try {
3067
4286
  return JSON.parse(raw);
@@ -3070,26 +4289,32 @@ function readSettings(path) {
3070
4289
  }
3071
4290
  }
3072
4291
  function writeSettings(path, settings) {
3073
- mkdirSync13(dirname6(path), { recursive: true });
4292
+ mkdirSync15(dirname6(path), { recursive: true });
3074
4293
  let mode;
3075
4294
  try {
3076
- mode = statSync3(path).mode & 511;
4295
+ mode = statSync4(path).mode & 511;
3077
4296
  } catch {
3078
4297
  mode = void 0;
3079
4298
  }
3080
4299
  const tmp = path + ".deskbuddy.tmp";
3081
- writeFileSync13(tmp, JSON.stringify(settings, null, 2), "utf8");
3082
- if (mode !== void 0) chmodSync2(tmp, mode);
3083
- renameSync9(tmp, path);
4300
+ writeFileSync15(tmp, JSON.stringify(settings, null, 2), "utf8");
4301
+ if (mode !== void 0) chmodSync4(tmp, mode);
4302
+ renameSync11(tmp, path);
4303
+ }
4304
+ function mentions(command, script) {
4305
+ return !!command && (command.includes(script) || command.includes(fwdSlash(script)));
3084
4306
  }
3085
- function installCcHook(settingsPath, hookPath, events = CC_EVENTS, tool = "claude_code") {
4307
+ function scriptArg(script, platform, quote = (s) => s) {
4308
+ return platform === "win32" ? winCmdPath(script) : quote(script);
4309
+ }
4310
+ function installCcHook(settingsPath, hookPath, events = CC_EVENTS, tool = "claude_code", platform = process.platform) {
3086
4311
  const settings = readSettings(settingsPath);
3087
4312
  settings.hooks ??= {};
3088
4313
  for (const event of events) {
3089
4314
  const groups = settings.hooks[event] ??= [];
3090
- const already = groups.some((g) => g.hooks?.some((h) => h.command?.includes(hookPath)));
4315
+ const already = groups.some((g) => g.hooks?.some((h) => mentions(h.command, hookPath)));
3091
4316
  if (already) continue;
3092
- groups.push({ hooks: [{ type: "command", command: `node ${hookPath} ${tool}` }] });
4317
+ groups.push({ hooks: [{ type: "command", command: `node ${scriptArg(hookPath, platform)} ${tool}` }] });
3093
4318
  }
3094
4319
  writeSettings(settingsPath, settings);
3095
4320
  }
@@ -3097,26 +4322,26 @@ var WINDOW_GATE_TIMEOUT_S = 7 * 24 * 60 * 60;
3097
4322
  function shQuote2(s) {
3098
4323
  return `'${s.replace(/'/g, `'\\''`)}'`;
3099
4324
  }
3100
- function installCcWindowGate(settingsPath, gatePath, tool = "claude_code") {
4325
+ function installCcWindowGate(settingsPath, gatePath, tool = "claude_code", platform = process.platform) {
3101
4326
  const settings = readSettings(settingsPath);
3102
4327
  settings.hooks ??= {};
3103
4328
  const groups = settings.hooks.PermissionRequest ??= [];
3104
- if (groups.some((g) => g.hooks?.some((h) => h.command?.includes(gatePath)))) return;
4329
+ if (groups.some((g) => g.hooks?.some((h) => mentions(h.command, gatePath)))) return;
3105
4330
  const toolArg = tool === "claude_code" ? "" : ` --tool ${tool}`;
3106
4331
  groups.push({
3107
- hooks: [{ type: "command", command: `node ${shQuote2(gatePath)} --window${toolArg}`, timeout: WINDOW_GATE_TIMEOUT_S }]
4332
+ hooks: [{ type: "command", command: `node ${scriptArg(gatePath, platform, shQuote2)} --window${toolArg}`, timeout: WINDOW_GATE_TIMEOUT_S }]
3108
4333
  });
3109
4334
  writeSettings(settingsPath, settings);
3110
4335
  }
3111
4336
  function uninstallCcHook(settingsPath, hookPath) {
3112
4337
  const settings = readSettings(settingsPath);
3113
4338
  if (!settings.hooks) return;
3114
- const gatePath = join16(dirname6(hookPath), "gate.cjs");
4339
+ const gatePath = join18(dirname6(hookPath), "gate.cjs");
3115
4340
  for (const event of Object.keys(settings.hooks)) {
3116
4341
  const groups = settings.hooks[event] ?? [];
3117
4342
  const kept = [];
3118
4343
  for (const g of groups) {
3119
- const hooks = (g.hooks ?? []).filter((h) => !h.command?.includes(hookPath) && !h.command?.includes(gatePath));
4344
+ const hooks = (g.hooks ?? []).filter((h) => !mentions(h.command, hookPath) && !mentions(h.command, gatePath));
3120
4345
  if (hooks.length > 0) kept.push({ ...g, hooks });
3121
4346
  }
3122
4347
  settings.hooks[event] = kept;
@@ -3125,8 +4350,8 @@ function uninstallCcHook(settingsPath, hookPath) {
3125
4350
  }
3126
4351
 
3127
4352
  // src/adapters/codexHook.ts
3128
- import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, renameSync as renameSync10, statSync as statSync4, writeFileSync as writeFileSync14 } from "fs";
3129
- import { dirname as dirname7, join as join17 } from "path";
4353
+ import { chmodSync as chmodSync5, existsSync as existsSync12, mkdirSync as mkdirSync16, readFileSync as readFileSync15, renameSync as renameSync12, statSync as statSync5, writeFileSync as writeFileSync16 } from "fs";
4354
+ import { dirname as dirname7, join as join19 } from "path";
3130
4355
  var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
3131
4356
  var CODEX_MARKER_END = "# <<< deskbuddy <<<";
3132
4357
  var CODEX_EVENTS = [
@@ -3139,13 +4364,13 @@ var CODEX_EVENTS = [
3139
4364
  ];
3140
4365
  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 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";
3141
4366
  function codexConfigPath() {
3142
- return join17(codexHome(), "config.toml");
4367
+ return join19(codexHome(), "config.toml");
3143
4368
  }
3144
- function renderBlock(hookPath, events, tool) {
3145
- const command = JSON.stringify(`node ${hookPath} ${tool}`);
3146
- const lines = [CODEX_MARKER_BEGIN];
4369
+ function renderBlock(hookPath, events, tool, platform) {
4370
+ const command = JSON.stringify(`node ${platform === "win32" ? winPsPath(hookPath) : hookPath} ${tool}`);
4371
+ const lines2 = [CODEX_MARKER_BEGIN];
3147
4372
  for (const event of events) {
3148
- lines.push(
4373
+ lines2.push(
3149
4374
  `[[hooks.${event}]]`,
3150
4375
  'matcher = ""',
3151
4376
  "hooks = [",
@@ -3154,21 +4379,21 @@ function renderBlock(hookPath, events, tool) {
3154
4379
  ""
3155
4380
  );
3156
4381
  }
3157
- lines.push(CODEX_MARKER_END);
3158
- return lines.join("\n");
4382
+ lines2.push(CODEX_MARKER_END);
4383
+ return lines2.join("\n");
3159
4384
  }
3160
4385
  function writeAtomic(path, content) {
3161
- mkdirSync14(dirname7(path), { recursive: true });
4386
+ mkdirSync16(dirname7(path), { recursive: true });
3162
4387
  let mode;
3163
4388
  try {
3164
- mode = statSync4(path).mode & 511;
4389
+ mode = statSync5(path).mode & 511;
3165
4390
  } catch {
3166
4391
  mode = void 0;
3167
4392
  }
3168
4393
  const tmp = path + ".deskbuddy.tmp";
3169
- writeFileSync14(tmp, content, "utf8");
3170
- if (mode !== void 0) chmodSync3(tmp, mode);
3171
- renameSync10(tmp, path);
4394
+ writeFileSync16(tmp, content, "utf8");
4395
+ if (mode !== void 0) chmodSync5(tmp, mode);
4396
+ renameSync12(tmp, path);
3172
4397
  }
3173
4398
  function assertSafeToAppend(raw, path) {
3174
4399
  for (const quote of ['"""', "'''"]) {
@@ -3177,35 +4402,35 @@ function assertSafeToAppend(raw, path) {
3177
4402
  }
3178
4403
  }
3179
4404
  }
3180
- function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex") {
3181
- const raw = existsSync12(configPath2) ? readFileSync12(configPath2, "utf8") : "";
4405
+ function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex", platform = process.platform) {
4406
+ const raw = existsSync12(configPath2) ? readFileSync15(configPath2, "utf8") : "";
3182
4407
  if (raw.includes(CODEX_MARKER_BEGIN)) return;
3183
4408
  assertSafeToAppend(raw, configPath2);
3184
- const block = renderBlock(hookPath, events, tool);
4409
+ const block = renderBlock(hookPath, events, tool, platform);
3185
4410
  const head = raw === "" ? "" : (raw.endsWith("\n") ? raw : raw + "\n") + "\n";
3186
4411
  writeAtomic(configPath2, head + block + "\n");
3187
4412
  }
3188
4413
  function uninstallCodexHook(configPath2) {
3189
4414
  if (!existsSync12(configPath2)) return;
3190
- const raw = readFileSync12(configPath2, "utf8");
3191
- const lines = raw.split("\n");
3192
- const begin = lines.findIndex((l) => l.trim() === CODEX_MARKER_BEGIN);
4415
+ const raw = readFileSync15(configPath2, "utf8");
4416
+ const lines2 = raw.split("\n");
4417
+ const begin = lines2.findIndex((l) => l.trim() === CODEX_MARKER_BEGIN);
3193
4418
  if (begin === -1) return;
3194
- const end = lines.findIndex((l, i) => i > begin && l.trim() === CODEX_MARKER_END);
4419
+ const end = lines2.findIndex((l, i) => i > begin && l.trim() === CODEX_MARKER_END);
3195
4420
  if (end === -1) {
3196
4421
  throw new Error(`${configPath2} \u91CC\u53EA\u627E\u5230\u5F00\u59CB\u6807\u8BB0\u3001\u6CA1\u6709\u7ED3\u675F\u6807\u8BB0\uFF0C\u4E3A\u514D\u8BEF\u5220\u5DF2\u4E2D\u6B62\uFF0C\u8BF7\u624B\u5DE5\u68C0\u67E5`);
3197
4422
  }
3198
- const start = begin > 0 && lines[begin - 1].trim() === "" ? begin - 1 : begin;
3199
- lines.splice(start, end - start + 1);
3200
- writeAtomic(configPath2, lines.join("\n"));
4423
+ const start = begin > 0 && lines2[begin - 1].trim() === "" ? begin - 1 : begin;
4424
+ lines2.splice(start, end - start + 1);
4425
+ writeAtomic(configPath2, lines2.join("\n"));
3201
4426
  }
3202
4427
 
3203
4428
  // src/commands/tools.ts
3204
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
4429
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3205
4430
  function fileContains(path, needle) {
3206
4431
  try {
3207
4432
  if (!existsSync13(path)) return false;
3208
- return readFileSync13(path, "utf8").includes(needle);
4433
+ return readFileSync16(path, "utf8").includes(needle);
3209
4434
  } catch {
3210
4435
  return false;
3211
4436
  }
@@ -3220,7 +4445,8 @@ var ADAPTERS = {
3220
4445
  configPath: (tool) => CC_CONFIG_PATHS[tool]?.() ?? null,
3221
4446
  install: (p, hook, events, tool) => installCcHook(p, hook, events, tool),
3222
4447
  uninstall: (p, hook) => uninstallCcHook(p, hook),
3223
- isInstalled: (p, hook) => fileContains(p, hook),
4448
+ // Windows 上装出去的是正斜杠形式;JSON 里的反斜杠还会被转义成两个,原样搜不到
4449
+ isInstalled: (p, hook) => fileContains(p, hook) || fileContains(p, fwdSlash(hook)),
3224
4450
  defaultEvents: CC_EVENTS,
3225
4451
  // 实测 PermissionRequest 与窗口授权框并行的:Claude Code(S10 §2)、Qoder CN 桌面端(二期 V8)。
3226
4452
  // WorkBuddy 桌面端弹授权框不触发 PermissionRequest;Codex 的 hook 会阻塞授权框——都不装
@@ -3271,91 +4497,427 @@ function subscribedEvents(registry, tool, fallback) {
3271
4497
  return picked.length > 0 ? picked : fallback;
3272
4498
  }
3273
4499
 
3274
- // src/commands/disable.ts
3275
- function stdout(line) {
3276
- process.stdout.write(line + "\n");
4500
+ // src/commands/enable.ts
4501
+ import { dirname as dirname9, join as join22 } from "path";
4502
+
4503
+ // src/wrap/shim.ts
4504
+ import { execFileSync as execFileSync7 } from "child_process";
4505
+ import { chmodSync as chmodSync6, existsSync as existsSync14, mkdirSync as mkdirSync17, readdirSync as readdirSync3, readFileSync as readFileSync17, rmdirSync, rmSync as rmSync8, writeFileSync as writeFileSync17 } from "fs";
4506
+ import { homedir as homedir5 } from "os";
4507
+ import { delimiter as delimiter2, join as join20, resolve as resolve2 } from "path";
4508
+ var SHIM_FALLBACK_EXIT = 213;
4509
+ var WIN_SHIM_ENABLED = false;
4510
+ var SHIM_COMMANDS = { claude_code: "claude", codex: "codex" };
4511
+ var SHIM_MARKER = "DeskBuddy window-wrap shim";
4512
+ var RC_BEGIN = "# >>> deskbuddy >>>";
4513
+ var RC_END = "# <<< deskbuddy <<<";
4514
+ var RC_FILES = [".zshrc", ".bashrc"];
4515
+ var WIN_PENDING = "Windows \u7A97\u53E3\u63A5\u7BA1\u5F85\u9A8C\u8BC1\uFF0C\u6682\u672A\u5F00\u542F";
4516
+ function shimDir() {
4517
+ return join20(deskbuddyHome(), "bin");
4518
+ }
4519
+ function shimFile(binDir, command, platform = process.platform) {
4520
+ return join20(binDir, platform === "win32" ? `${command}.cmd` : command);
4521
+ }
4522
+ function shQuote3(s) {
4523
+ return `'${s.replace(/'/g, `'\\''`)}'`;
3277
4524
  }
3278
- function toolsToDisable(explicit) {
3279
- if (explicit.length > 0) return explicit;
3280
- const known = Object.keys(loadRegistry()?.tools ?? {});
3281
- return [.../* @__PURE__ */ new Set([...loadConfig().enabledTools, ...known])];
4525
+ function shStripPath(command) {
4526
+ return [
4527
+ // 🔴 防递归:exec 不换 pid,同一个 pid 第二次走到这里 = 垫片 exec 到了自己(PATH 里还有别的写法指向垫片)
4528
+ 'if [ "${DESKBUDDY_SHIM_HOPS:-}" = "$$" ]; then',
4529
+ ` echo "deskbuddy: \u627E\u4E0D\u5230\u771F\u6B63\u7684 ${command}\uFF08\u57AB\u7247\u5728\u8C03\u7528\u81EA\u5DF1\uFF09\u3002\u53EF\u8BBE DESKBUDDY_WRAP_OFF=1 \u6216\u68C0\u67E5 PATH" >&2`,
4530
+ " exit 127",
4531
+ "fi",
4532
+ "DESKBUDDY_SHIM_HOPS=$$",
4533
+ "export DESKBUDDY_SHIM_HOPS",
4534
+ "set -f",
4535
+ "DB_P=''",
4536
+ "DB_IFS=$IFS",
4537
+ "IFS=:",
4538
+ "for d in $PATH; do",
4539
+ ' case "$d" in "$DB_BIN"|"$DB_BIN/") ;; *) DB_P="${DB_P:+$DB_P:}$d" ;; esac',
4540
+ "done",
4541
+ "IFS=$DB_IFS",
4542
+ "set +f",
4543
+ "PATH=$DB_P",
4544
+ "export PATH"
4545
+ ];
3282
4546
  }
3283
- async function runDisable(tools, opts = {}) {
3284
- const out = opts.out ?? stdout;
3285
- const registry = loadRegistry();
3286
- const hookPath = installedHookPath();
3287
- const targets = toolsToDisable(tools);
3288
- const cfg = loadConfig();
3289
- const enabled = new Set(cfg.enabledTools);
3290
- let failed = 0;
3291
- for (const tool of targets) {
3292
- const adapter = adapterFor(registry, tool);
3293
- if (!adapter) {
3294
- continue;
4547
+ function shScript(tool, command, nodePath, cliPath, binDir) {
4548
+ return [
4549
+ "#!/bin/sh",
4550
+ `# ${SHIM_MARKER} (${tool}). Generated by deskbuddy; \`deskbuddy disable ${tool}\` removes it.`,
4551
+ "# DeskBuddy \u7A97\u53E3\u63A5\u7BA1\u57AB\u7247\uFF1A\u51FA\u4EFB\u4F55\u95EE\u9898\u53EF\u7528 DESKBUDDY_WRAP_OFF=1 \u4E34\u65F6\u7ED5\u5F00",
4552
+ `DB_NODE=${shQuote3(nodePath)}`,
4553
+ `DB_CLI=${shQuote3(cliPath)}`,
4554
+ `DB_BIN=${shQuote3(binDir)}`,
4555
+ 'if [ -z "${DESKBUDDY_WRAPPED:-}" ] && [ "${DESKBUDDY_WRAP_OFF:-}" != "1" ] && [ -x "$DB_NODE" ] && [ -f "$DB_CLI" ]; then',
4556
+ // 不 exec:wrap 找不到真程序时以 213 退出,垫片接着走下面的放行分支(约定见 run.ts SHIM_FALLBACK_EXIT)
4557
+ ` DESKBUDDY_SHIM=1 "$DB_NODE" "$DB_CLI" wrap ${tool} "$@"`,
4558
+ " DB_RC=$?",
4559
+ ` [ "$DB_RC" -eq ${SHIM_FALLBACK_EXIT} ] || exit "$DB_RC"`,
4560
+ "fi",
4561
+ "# \u653E\u884C\uFF1A\u628A\u672C\u76EE\u5F55\u4ECE PATH \u91CC\u62FF\u6389\uFF0C\u76F4\u63A5\u8DD1\u771F\u7684\u7A0B\u5E8F",
4562
+ ...shStripPath(command),
4563
+ `exec ${command} "$@"`,
4564
+ ""
4565
+ ].join("\n");
4566
+ }
4567
+ function cmdPath(p, env) {
4568
+ let out = p.replace(/%/g, "%%");
4569
+ for (const name of ["LOCALAPPDATA", "APPDATA", "USERPROFILE"]) {
4570
+ const v = env[name];
4571
+ if (v && p.toLowerCase().startsWith(v.toLowerCase() + "\\")) {
4572
+ out = `%${name}%` + p.slice(v.length).replace(/%/g, "%%");
4573
+ break;
3295
4574
  }
3296
- const configPath2 = adapter.configPath(tool);
3297
- if (!configPath2) continue;
4575
+ }
4576
+ return /^[\x20-\x7e]*$/.test(out) && !out.includes('"') ? out : null;
4577
+ }
4578
+ function cmdScript(tool, command, node, cli, bin) {
4579
+ return [
4580
+ "@echo off",
4581
+ `rem ${SHIM_MARKER} (${tool}). Generated by deskbuddy; "deskbuddy disable ${tool}" removes it.`,
4582
+ "rem Set DESKBUDDY_WRAP_OFF=1 to bypass.",
4583
+ "setlocal",
4584
+ "if defined DESKBUDDY_WRAPPED goto direct",
4585
+ 'if "%DESKBUDDY_WRAP_OFF%"=="1" goto direct',
4586
+ `if not exist "${node}" goto direct`,
4587
+ `if not exist "${cli}" goto direct`,
4588
+ 'set "DESKBUDDY_SHIM=1"',
4589
+ `"${node}" "${cli}" wrap ${tool} %*`,
4590
+ `if not "%ERRORLEVEL%"=="${SHIM_FALLBACK_EXIT}" exit /b %ERRORLEVEL%`,
4591
+ 'set "DESKBUDDY_SHIM="',
4592
+ ":direct",
4593
+ // 从 PATH 里去掉本目录(cmd 的替换不分大小写);相邻重复项一次替换删不净,循环到不再变化。
4594
+ // 延迟展开只开在这一小段:它会吃掉 %* 里的 !
4595
+ "setlocal EnableDelayedExpansion",
4596
+ `set "DB_BIN=${bin}"`,
4597
+ 'set "DB_P=;!PATH!;"',
4598
+ ":dbstrip",
4599
+ 'set "DB_Q=!DB_P:;%DB_BIN%;=;!"',
4600
+ 'if not "!DB_Q!"=="!DB_P!" (set "DB_P=!DB_Q!" & goto dbstrip)',
4601
+ 'endlocal & set "PATH=%DB_P:~1,-1%"',
4602
+ `${command} %*`,
4603
+ "exit /b %ERRORLEVEL%",
4604
+ ""
4605
+ ].join("\r\n");
4606
+ }
4607
+ function rcBlock(binDir, rcHome) {
4608
+ const std = resolve2(rcHome, ".deskbuddy", "bin") === resolve2(binDir);
4609
+ const dir = std ? "$HOME/.deskbuddy/bin" : binDir.replace(/(["\\$`])/g, "\\$1");
4610
+ return [
4611
+ RC_BEGIN,
4612
+ "# DeskBuddy \u7A97\u53E3\u63A5\u7BA1\uFF1A\u7EC8\u7AEF\u91CC\u7684 claude \u7ECF DeskBuddy \u6253\u5F00\uFF0C\u8BBE\u5907\u4E0A\u7684\u56DE\u590D\u80FD\u5199\u8FDB\u7A97\u53E3\u3002",
4613
+ "# `deskbuddy disable claude_code` \u4F1A\u5220\u6389\u8FD9\u4E00\u6BB5\u3002",
4614
+ `export PATH="${dir}:$PATH"`,
4615
+ RC_END
4616
+ ].join("\n");
4617
+ }
4618
+ var RC_NL_FLAG = " (+nl)";
4619
+ function locateBlock(content) {
4620
+ const i = content.indexOf(RC_BEGIN);
4621
+ if (i < 0) return null;
4622
+ if (i > 0 && content[i - 1] !== "\n") return null;
4623
+ const k = content.indexOf(RC_END, i);
4624
+ if (k < 0) return null;
4625
+ const lineEnd = content.indexOf("\n", i);
4626
+ const beginLine = content.slice(i, lineEnd < 0 ? void 0 : lineEnd);
4627
+ return { i, j: k + RC_END.length, addedNl: beginLine.endsWith(RC_NL_FLAG) };
4628
+ }
4629
+ function flagged(block) {
4630
+ return block.replace(RC_BEGIN, RC_BEGIN + RC_NL_FLAG);
4631
+ }
4632
+ function addRcBlock(content, block) {
4633
+ const loc = locateBlock(content);
4634
+ if (loc) {
4635
+ const next = content.slice(0, loc.i) + (loc.addedNl ? flagged(block) : block) + content.slice(loc.j);
4636
+ return next === content ? null : next;
4637
+ }
4638
+ if (content === "" || content.endsWith("\n")) return content + block + "\n";
4639
+ return content + "\n" + flagged(block) + "\n";
4640
+ }
4641
+ function removeRcBlock(content) {
4642
+ const loc = locateBlock(content);
4643
+ if (!loc) return null;
4644
+ const start = loc.addedNl && loc.i > 0 && content[loc.i - 1] === "\n" ? loc.i - 1 : loc.i;
4645
+ const end = content[loc.j] === "\n" ? loc.j + 1 : loc.j;
4646
+ return content.slice(0, start) + content.slice(end);
4647
+ }
4648
+ function readRc(p) {
4649
+ return readFileSync17(p).toString("latin1");
4650
+ }
4651
+ function writeRc(p, latin1) {
4652
+ writeFileSync17(p, Buffer.from(latin1, "latin1"));
4653
+ }
4654
+ function toLatin1(utf8) {
4655
+ return Buffer.from(utf8, "utf8").toString("latin1");
4656
+ }
4657
+ function winKey(p) {
4658
+ return p.replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
4659
+ }
4660
+ function addPathEntry(value, dir) {
4661
+ const parts = value.split(";");
4662
+ if (parts.some((p) => winKey(p) === winKey(dir))) return null;
4663
+ return value === "" ? dir : `${dir};${value}`;
4664
+ }
4665
+ function removePathEntry(value, dir) {
4666
+ const parts = value.split(";");
4667
+ const kept = parts.filter((p) => winKey(p) !== winKey(dir));
4668
+ if (kept.length === parts.length) return null;
4669
+ return kept.join(";");
4670
+ }
4671
+ 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)))";
4672
+ 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')";
4673
+ function psWinPath() {
4674
+ const ps = (script, env) => execFileSync7("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
4675
+ encoding: "utf8",
4676
+ stdio: ["ignore", "pipe", "ignore"],
4677
+ windowsHide: true,
4678
+ timeout: 15e3,
4679
+ env: env ?? process.env
4680
+ });
4681
+ return {
4682
+ read: () => {
4683
+ try {
4684
+ return Buffer.from(ps(PS_READ).trim(), "base64").toString("utf8");
4685
+ } catch {
4686
+ return null;
4687
+ }
4688
+ },
4689
+ write: (v) => {
4690
+ ps(PS_WRITE, { ...process.env, DESKBUDDY_NEW_PATH: v });
4691
+ }
4692
+ };
4693
+ }
4694
+ function defaultShimEnv(nodePath, cliPath) {
4695
+ const env = process.env;
4696
+ const isolated = !!env.VITEST || !!env.DESKBUDDY_HOME;
4697
+ const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir5());
4698
+ return {
4699
+ platform: process.platform,
4700
+ binDir: shimDir(),
4701
+ nodePath,
4702
+ cliPath,
4703
+ rcHome,
4704
+ winPath: process.platform === "win32" && !isolated ? psWinPath() : null,
4705
+ env
4706
+ };
4707
+ }
4708
+ function installShim(tool, e) {
4709
+ const command = SHIM_COMMANDS[tool];
4710
+ if (!command) return { ok: false, message: "" };
4711
+ if (e.platform === "win32" && !WIN_SHIM_ENABLED) return { ok: false, message: WIN_PENDING };
4712
+ if (e.platform === "win32" && tool === "codex") return { ok: false, message: WIN_PENDING };
4713
+ const file = shimFile(e.binDir, command, e.platform);
4714
+ if (e.platform === "win32") {
4715
+ const node = cmdPath(e.nodePath, e.env);
4716
+ const cli = cmdPath(e.cliPath, e.env);
4717
+ const bin = cmdPath(e.binDir, e.env);
4718
+ if (!node || !cli || !bin) {
4719
+ 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" };
4720
+ }
4721
+ mkdirSync17(e.binDir, { recursive: true });
4722
+ writeFileSync17(file, cmdScript(tool, command, node, cli, bin), "utf8");
4723
+ } else {
4724
+ mkdirSync17(e.binDir, { recursive: true, mode: 448 });
4725
+ writeFileSync17(file, shScript(tool, command, e.nodePath, e.cliPath, e.binDir), { encoding: "utf8", mode: 493 });
4726
+ chmodSync6(file, 493);
4727
+ }
4728
+ const touched = [];
4729
+ if (e.platform === "win32") {
4730
+ if (e.winPath) {
4731
+ const cur = e.winPath.read();
4732
+ if (cur === null) return { ok: false, message: "\u7A97\u53E3\u63A5\u7BA1\uFF1A\u8BFB\u4E0D\u5230\u7528\u6237 PATH\uFF0C\u8BF7\u624B\u52A8\u628A " + e.binDir + " \u52A0\u5230\u7528\u6237 PATH \u6700\u524D\u9762" };
4733
+ const next = addPathEntry(cur, e.binDir);
4734
+ if (next !== null) e.winPath.write(next);
4735
+ touched.push("\u7528\u6237 PATH");
4736
+ }
4737
+ } else if (e.rcHome) {
4738
+ const block = toLatin1(rcBlock(e.binDir, e.rcHome));
4739
+ for (const name of RC_FILES) {
4740
+ const rc = join20(e.rcHome, name);
4741
+ if (!existsSync14(rc)) continue;
4742
+ const next = addRcBlock(readRc(rc), block);
4743
+ if (next !== null) writeRc(rc, next);
4744
+ touched.push(`~/${name}`);
4745
+ }
4746
+ }
4747
+ if (touched.length === 0) {
4748
+ return {
4749
+ ok: true,
4750
+ message: `\u7A97\u53E3\u63A5\u7BA1\uFF1A\u57AB\u7247\u5DF2\u88C5\u5728 ${e.binDir}\uFF0C\u4F46\u6CA1\u6539 shell \u914D\u7F6E\u2014\u2014\u8981\u751F\u6548\u8BF7\u628A\u5B83\u52A0\u5230 PATH \u6700\u524D\u9762`
4751
+ };
4752
+ }
4753
+ return {
4754
+ ok: true,
4755
+ message: `\u7A97\u53E3\u63A5\u7BA1\u5DF2\u5F00\u542F\uFF08\u5DF2\u6539 ${touched.join("\u3001")}\uFF09\uFF1A\u65B0\u5F00\u7684\u7EC8\u7AEF\u91CC\u6572 ${command} \u5C31\u7ECF DeskBuddy \u6253\u5F00\uFF0C\u5DF2\u5F00\u7740\u7684\u7EC8\u7AEF\u8981\u91CD\u5F00\u624D\u751F\u6548`
4756
+ };
4757
+ }
4758
+ function ourShims(binDir) {
4759
+ let names;
4760
+ try {
4761
+ names = readdirSync3(binDir);
4762
+ } catch {
4763
+ return [];
4764
+ }
4765
+ return names.filter((n) => {
3298
4766
  try {
3299
- adapter.uninstall(configPath2, hookPath);
3300
- out(`\u2705 ${tool}\uFF1A\u5DF2\u4ECE ${configPath2} \u79FB\u9664`);
3301
- } catch (e) {
3302
- out(`\u2717 ${tool}\uFF1A${e.message}`);
3303
- failed += 1;
3304
- continue;
4767
+ return readFileSync17(join20(binDir, n), "utf8").includes(SHIM_MARKER);
4768
+ } catch {
4769
+ return false;
3305
4770
  }
3306
- enabled.delete(tool);
4771
+ });
4772
+ }
4773
+ function uninstallShim(tool, e) {
4774
+ const command = SHIM_COMMANDS[tool];
4775
+ if (!command) return { ok: true, message: "" };
4776
+ let removed = false;
4777
+ const file = shimFile(e.binDir, command, e.platform);
4778
+ try {
4779
+ if (existsSync14(file) && readFileSync17(file, "utf8").includes(SHIM_MARKER)) {
4780
+ rmSync8(file, { force: true });
4781
+ removed = true;
4782
+ }
4783
+ } catch {
3307
4784
  }
3308
- cfg.enabledTools = [...enabled];
3309
- saveConfig(cfg);
3310
- if (enabled.size === 0 && !opts.keepAutostart) {
3311
- const r = await uninstallAutostart();
3312
- if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
4785
+ if (ourShims(e.binDir).length > 0) return { ok: true, message: removed ? `\u5DF2\u5220\u9664\u7A97\u53E3\u63A5\u7BA1\u57AB\u7247 ${file}` : "" };
4786
+ const touched = [];
4787
+ if (e.platform === "win32") {
4788
+ if (e.winPath) {
4789
+ const cur = e.winPath.read();
4790
+ const next = cur === null ? null : removePathEntry(cur, e.binDir);
4791
+ if (next !== null) {
4792
+ e.winPath.write(next);
4793
+ touched.push("\u7528\u6237 PATH");
4794
+ }
4795
+ }
4796
+ } else if (e.rcHome) {
4797
+ for (const name of RC_FILES) {
4798
+ const rc = join20(e.rcHome, name);
4799
+ if (!existsSync14(rc)) continue;
4800
+ const next = removeRcBlock(readRc(rc));
4801
+ if (next !== null) {
4802
+ writeRc(rc, next);
4803
+ touched.push(`~/${name}`);
4804
+ }
4805
+ }
3313
4806
  }
3314
- if (!opts.brief) {
3315
- out("");
3316
- out("\u5DF2\u505C\u6B62\u91C7\u96C6\u3002\u6388\u6743\u4E0E clientId \u4ECD\u4FDD\u7559\uFF0C\u91CD\u65B0 deskbuddy enable \u5373\u53EF\u6062\u590D\u3002");
3317
- out("\u8981\u5F7B\u5E95\u6E05\u9664\u672C\u673A\u6570\u636E\u8BF7\u8FD0\u884C\uFF1Adeskbuddy logout");
4807
+ try {
4808
+ if (existsSync14(e.binDir) && readdirSync3(e.binDir).length === 0) rmdirSync(e.binDir);
4809
+ } catch {
3318
4810
  }
3319
- return failed > 0 ? 1 : 0;
4811
+ if (!removed && touched.length === 0) return { ok: true, message: "" };
4812
+ return { ok: true, message: `\u7A97\u53E3\u63A5\u7BA1\u5DF2\u5173\u95ED${touched.length ? `\uFF08\u5DF2\u4ECE ${touched.join("\u3001")} \u79FB\u9664\uFF09` : ""}\uFF0C\u65B0\u5F00\u7684\u7EC8\u7AEF\u751F\u6548` };
4813
+ }
4814
+ function shimState(tool, binDir, pathEnv, platform = process.platform) {
4815
+ const command = SHIM_COMMANDS[tool];
4816
+ if (!command) return { installed: false, inPath: false };
4817
+ const file = shimFile(binDir, command, platform);
4818
+ let installed = false;
4819
+ try {
4820
+ installed = readFileSync17(file, "utf8").includes(SHIM_MARKER);
4821
+ } catch {
4822
+ installed = false;
4823
+ }
4824
+ const win = platform === "win32";
4825
+ const key = (p) => win ? winKey(p) : p.replace(/\/+$/, "");
4826
+ const inPath = pathEnv.split(win ? ";" : delimiter2).some((p) => p && key(p) === key(binDir));
4827
+ return { installed, inPath };
4828
+ }
4829
+
4830
+ // src/wrap/pty.ts
4831
+ import { chmodSync as chmodSync7, existsSync as existsSync15, statSync as statSync6 } from "fs";
4832
+ import { createRequire as createRequire2 } from "module";
4833
+ import { dirname as dirname8, join as join21 } from "path";
4834
+ var req = createRequire2(import.meta.url);
4835
+ function fixSpawnHelper() {
4836
+ if (process.platform !== "darwin") return;
4837
+ try {
4838
+ const dir = join21(dirname8(req.resolve("node-pty/package.json")), "prebuilds", `darwin-${process.arch}`);
4839
+ const helper = join21(dir, "spawn-helper");
4840
+ if (existsSync15(helper) && (statSync6(helper).mode & 73) === 0) chmodSync7(helper, 493);
4841
+ } catch {
4842
+ }
4843
+ }
4844
+ function loadPty() {
4845
+ try {
4846
+ fixSpawnHelper();
4847
+ const m = req("node-pty");
4848
+ return typeof m?.spawn === "function" ? m : null;
4849
+ } catch {
4850
+ return null;
4851
+ }
4852
+ }
4853
+ function loadHeadless() {
4854
+ try {
4855
+ const m = req("@xterm/headless");
4856
+ return typeof m?.Terminal === "function" ? m.Terminal : null;
4857
+ } catch {
4858
+ return null;
4859
+ }
4860
+ }
4861
+ function screenLines(term) {
4862
+ const b = term.buffer.active;
4863
+ const out = [];
4864
+ for (let i = b.viewportY; i < b.viewportY + term.rows; i++) {
4865
+ const l = b.getLine(i);
4866
+ out.push(l ? l.translateToString(true) : "");
4867
+ }
4868
+ return out;
3320
4869
  }
3321
4870
 
3322
4871
  // src/commands/enable.ts
3323
- import { dirname as dirname8, join as join18 } from "path";
3324
- function stdout2(line) {
4872
+ function stdout(line) {
3325
4873
  process.stdout.write(line + "\n");
3326
4874
  }
3327
4875
  function parseTools(arg) {
3328
4876
  return (arg ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
3329
4877
  }
3330
- async function runEnable(tools, opts = {}) {
3331
- const out = opts.out ?? stdout2;
3332
- if (tools.length === 0) {
3333
- out("\u7528\u6CD5\uFF1Adeskbuddy enable <tool[,tool]>\uFF0C\u4F8B\u5982 deskbuddy enable claude_code,codex");
3334
- return 1;
4878
+ function defaultShimEnvForInstall() {
4879
+ let node = null;
4880
+ try {
4881
+ node = whichIn("node", defaultLocateDeps());
4882
+ } catch {
4883
+ node = null;
3335
4884
  }
3336
- if (!loadConfig().boundAt) {
3337
- out("\u8FD8\u6CA1\u6709\u6388\u6743\u3002\u8BF7\u5148\u8FD0\u884C\uFF1Adeskbuddy setup");
3338
- return 1;
4885
+ return defaultShimEnv(node ?? process.execPath, defaultCliPath());
4886
+ }
4887
+ function installShims(tools, out, shim) {
4888
+ if (!shim) return;
4889
+ for (const tool of tools) {
4890
+ if (!SHIM_COMMANDS[tool]) continue;
4891
+ try {
4892
+ const r = installShim(tool, shim);
4893
+ if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
4894
+ if (r.ok && tool === "claude_code" && (!loadPty() || !loadHeadless())) {
4895
+ 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");
4896
+ }
4897
+ } catch (e) {
4898
+ out(`\u26A0\uFE0F \u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\u4E0A\uFF1A${e.message}\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09`);
4899
+ }
3339
4900
  }
3340
- ensureClientId();
3341
- if (!opts.skipRefresh) await refreshRegistry();
4901
+ }
4902
+ function installTools(tools, out, shim = defaultShimEnvForInstall()) {
3342
4903
  const registry = loadRegistry();
3343
4904
  const hookPath = installedHookPath();
3344
4905
  syncHookFile();
3345
4906
  const cfg = loadConfig();
3346
4907
  const enabled = new Set(cfg.enabledTools);
3347
- let failed = 0;
4908
+ const installed = [];
4909
+ const failed = [];
3348
4910
  for (const tool of tools) {
3349
4911
  const adapter = adapterFor(registry, tool);
3350
4912
  if (!adapter) {
3351
4913
  out(`\u2717 ${tool}\uFF1A\u6620\u5C04\u8868\u91CC\u6CA1\u6709\u8FD9\u4E2A\u5DE5\u5177\uFF08\u6216\u5B83\u7684 format \u672C\u5BA2\u6237\u7AEF\u8FD8\u4E0D\u652F\u6301\uFF09\uFF0C\u5DF2\u8DF3\u8FC7`);
3352
- failed += 1;
4914
+ failed.push({ tool, reason: "\u672C\u5BA2\u6237\u7AEF\u8FD8\u4E0D\u652F\u6301" });
3353
4915
  continue;
3354
4916
  }
3355
4917
  const configPath2 = adapter.configPath(tool);
3356
4918
  if (!configPath2) {
3357
4919
  out(`\u2717 ${tool}\uFF1A\u672C\u5BA2\u6237\u7AEF\u4E0D\u77E5\u9053\u5B83\u7684\u914D\u7F6E\u6587\u4EF6\u5728\u54EA\uFF0C\u5DF2\u8DF3\u8FC7`);
3358
- failed += 1;
4920
+ failed.push({ tool, reason: "\u627E\u4E0D\u5230\u914D\u7F6E\u6587\u4EF6" });
3359
4921
  continue;
3360
4922
  }
3361
4923
  const events = subscribedEvents(registry, tool, adapter.defaultEvents);
@@ -3363,13 +4925,14 @@ async function runEnable(tools, opts = {}) {
3363
4925
  adapter.install(configPath2, hookPath, events, tool);
3364
4926
  } catch (e) {
3365
4927
  out(`\u2717 ${tool}\uFF1A${e.message}`);
3366
- failed += 1;
4928
+ failed.push({ tool, reason: e.message });
3367
4929
  continue;
3368
4930
  }
3369
4931
  enabled.add(tool);
4932
+ installed.push(tool);
3370
4933
  out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
3371
4934
  try {
3372
- if (adapter.installWindowGate?.(configPath2, join18(dirname8(hookPath), "gate.cjs"), tool)) {
4935
+ if (adapter.installWindowGate?.(configPath2, join22(dirname9(hookPath), "gate.cjs"), tool)) {
3373
4936
  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`);
3374
4937
  }
3375
4938
  } catch (e) {
@@ -3379,6 +4942,22 @@ async function runEnable(tools, opts = {}) {
3379
4942
  }
3380
4943
  cfg.enabledTools = [...enabled];
3381
4944
  saveConfig(cfg);
4945
+ installShims(installed, out, shim);
4946
+ return { installed, failed };
4947
+ }
4948
+ async function runEnable(tools, opts = {}) {
4949
+ const out = opts.out ?? stdout;
4950
+ if (tools.length === 0) {
4951
+ out("\u7528\u6CD5\uFF1Adeskbuddy enable <tool[,tool]>\uFF0C\u4F8B\u5982 deskbuddy enable claude_code,codex");
4952
+ return 1;
4953
+ }
4954
+ if (!loadConfig().boundAt) {
4955
+ out("\u8FD8\u6CA1\u6709\u6388\u6743\u3002\u8BF7\u5148\u8FD0\u884C\uFF1Adeskbuddy setup");
4956
+ return 1;
4957
+ }
4958
+ ensureClientId();
4959
+ if (!opts.skipRefresh) await refreshRegistry();
4960
+ const { failed } = installTools(tools, out, opts.shim === void 0 ? defaultShimEnvForInstall() : opts.shim);
3382
4961
  if (!opts.skipAutostart) {
3383
4962
  const r = await installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() });
3384
4963
  out("");
@@ -3388,69 +4967,132 @@ async function runEnable(tools, opts = {}) {
3388
4967
  out("\u88C5\u5B8C\u4E86\u3002\u72B6\u6001\u91C7\u96C6\u9760 hook\uFF0C\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF1B");
3389
4968
  out("\u5B88\u62A4\u8FDB\u7A0B\u53EA\u5728\u300C\u786C\u4EF6\u4E0A\u7ED9\u8FD9\u53F0\u7535\u8111\u4E0B\u6307\u4EE4\u300D\u65F6\u624D\u7528\u5F97\u7740\uFF0C\u5DF2\u968F\u4E0A\u9762\u4E00\u6B65\u8BBE\u597D\u5F00\u673A\u81EA\u542F\u3002");
3390
4969
  out("\u968F\u65F6\u53EF\u4EE5 deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001deskbuddy disable \u5E72\u51C0\u79FB\u9664\u3002");
3391
- return failed > 0 ? 1 : 0;
4970
+ return failed.length > 0 ? 1 : 0;
3392
4971
  }
3393
4972
 
3394
- // src/commands/logout.ts
3395
- import { createInterface } from "readline";
3396
- import { existsSync as existsSync14, rmSync as rmSync5 } from "fs";
3397
- function stdout3(line) {
4973
+ // src/commands/disable.ts
4974
+ function stdout2(line) {
3398
4975
  process.stdout.write(line + "\n");
3399
4976
  }
3400
- function askYesNo(question) {
3401
- return new Promise((resolve3) => {
3402
- const rl = createInterface({ input: process.stdin, output: process.stdout });
3403
- rl.question(question, (answer) => {
3404
- rl.close();
3405
- resolve3(/^(y|yes)$/i.test(answer.trim()));
3406
- });
3407
- });
4977
+ function toolsToDisable(explicit) {
4978
+ if (explicit.length > 0) return explicit;
4979
+ const known = Object.keys(loadRegistry()?.tools ?? {});
4980
+ return [.../* @__PURE__ */ new Set([...loadConfig().enabledTools, ...known])];
3408
4981
  }
3409
- async function runLogout(opts = {}) {
3410
- const out = opts.out ?? stdout3;
3411
- const home = deskbuddyHome();
3412
- out("deskbuddy logout \u4F1A\u505A\u8FD9\u4E24\u4EF6\u4E8B\uFF1A");
3413
- out(" 1. \u4ECE\u5404\u667A\u80FD\u4F53\u7684\u914D\u7F6E\u91CC\u79FB\u9664 DeskBuddy \u7684 hook\uFF08\u4F60\u81EA\u5DF1\u7684 hook \u4E0D\u52A8\uFF09");
3414
- out(` 2. \u5220\u9664\u6574\u4E2A ${home}\uFF08clientId\u3001\u6388\u6743\u72B6\u6001\u3001\u4F1A\u8BDD\u72B6\u6001\u3001\u6620\u5C04\u8868\u7F13\u5B58\uFF09`);
3415
- out("");
3416
- out("\u5220\u6389\u4E4B\u540E\u518D\u7528\u5C31\u8981\u91CD\u65B0 deskbuddy setup \u626B\u7801\u3002");
3417
- out("\u6CE8\u610F\uFF1A\u8FD9\u53EA\u6E05\u9664\u672C\u673A\u6570\u636E\uFF0C\u670D\u52A1\u7AEF\u7684\u7ED1\u5B9A\u5173\u7CFB\u9700\u8981\u5728 App \u91CC\u64A4\u9500\uFF08\u6682\u672A\u652F\u6301\uFF09\uFF1B");
3418
- out("\u4F46\u91CD\u65B0 setup \u4F1A\u6362\u4E00\u4E2A\u65B0\u7684 clientId\uFF0C\u65E7\u7684\u4E0D\u4F1A\u518D\u4E0A\u62A5\u3002");
3419
- if (!opts.yes) {
3420
- const confirm = opts.confirm ?? (() => askYesNo("\u786E\u8BA4\u6267\u884C\uFF1F(y/N) "));
3421
- if (!await confirm()) {
3422
- out("\u5DF2\u53D6\u6D88\uFF0C\u4EC0\u4E48\u90FD\u6CA1\u52A8\u3002");
3423
- return 1;
4982
+ async function runDisable(tools, opts = {}) {
4983
+ const out = opts.out ?? stdout2;
4984
+ const registry = loadRegistry();
4985
+ const hookPath = installedHookPath();
4986
+ const targets = toolsToDisable(tools);
4987
+ const cfg = loadConfig();
4988
+ const enabled = new Set(cfg.enabledTools);
4989
+ let failed = 0;
4990
+ for (const tool of targets) {
4991
+ const adapter = adapterFor(registry, tool);
4992
+ if (!adapter) {
4993
+ continue;
4994
+ }
4995
+ const configPath2 = adapter.configPath(tool);
4996
+ if (!configPath2) continue;
4997
+ try {
4998
+ adapter.uninstall(configPath2, hookPath);
4999
+ out(`\u2705 ${tool}\uFF1A\u5DF2\u4ECE ${configPath2} \u79FB\u9664`);
5000
+ } catch (e) {
5001
+ out(`\u2717 ${tool}\uFF1A${e.message}`);
5002
+ failed += 1;
5003
+ continue;
3424
5004
  }
5005
+ enabled.delete(tool);
3425
5006
  }
3426
- await runDisable([], { out, brief: true, keepAutostart: opts.keepAutostart });
3427
- if (existsSync14(home)) {
3428
- rmSync5(home, { recursive: true, force: true });
5007
+ cfg.enabledTools = [...enabled];
5008
+ saveConfig(cfg);
5009
+ const shim = opts.shim === void 0 ? defaultShimEnvForInstall() : opts.shim;
5010
+ if (shim) {
5011
+ for (const tool of targets) {
5012
+ if (!SHIM_COMMANDS[tool]) continue;
5013
+ try {
5014
+ const r = uninstallShim(tool, shim);
5015
+ if (r.message) out(`\u2705 ${r.message}`);
5016
+ } catch (e) {
5017
+ out(`\u2717 \u7A97\u53E3\u63A5\u7BA1\u57AB\u7247\u6CA1\u5220\u5E72\u51C0\uFF1A${e.message}`);
5018
+ failed += 1;
5019
+ }
5020
+ }
3429
5021
  }
3430
- out("");
3431
- out("\u2705 \u5DF2\u6E05\u9664\u672C\u673A\u7684 DeskBuddy \u6570\u636E\u3002");
3432
- return 0;
5022
+ if (enabled.size === 0 && !opts.keepAutostart) {
5023
+ const r = await uninstallAutostart();
5024
+ if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
5025
+ }
5026
+ if (!opts.brief) {
5027
+ out("");
5028
+ out("\u5DF2\u505C\u6B62\u91C7\u96C6\u3002\u6388\u6743\u4E0E clientId \u4ECD\u4FDD\u7559\uFF0C\u91CD\u65B0 deskbuddy enable \u5373\u53EF\u6062\u590D\u3002");
5029
+ out("\u8981\u5F7B\u5E95\u6E05\u9664\u672C\u673A\u6570\u636E\u8BF7\u8FD0\u884C\uFF1Adeskbuddy logout");
5030
+ }
5031
+ return failed > 0 ? 1 : 0;
5032
+ }
5033
+
5034
+ // src/commands/install.ts
5035
+ import { mkdirSync as mkdirSync18, rmSync as rmSync9, writeFileSync as writeFileSync18 } from "fs";
5036
+ import { join as join23 } from "path";
5037
+
5038
+ // src/core/detect.ts
5039
+ import { existsSync as existsSync16 } from "fs";
5040
+ import { dirname as dirname10 } from "path";
5041
+ var ALL_TOOLS = ["claude_code", "codex", "workbuddy", "qoder", "opencode"];
5042
+ function toolConfigDir(tool) {
5043
+ switch (tool) {
5044
+ case "claude_code":
5045
+ return claudeHome();
5046
+ case "codex":
5047
+ return codexHome();
5048
+ case "workbuddy":
5049
+ return dirname10(workbuddySettingsPath());
5050
+ case "qoder":
5051
+ return dirname10(qoderSettingsPath());
5052
+ case "opencode":
5053
+ return dirname10(dirname10(opencodePluginPath()));
5054
+ default:
5055
+ return null;
5056
+ }
5057
+ }
5058
+ var defaultDetectDeps = () => ({
5059
+ located: (tool) => locate(tool) != null,
5060
+ exists: existsSync16,
5061
+ env: process.env
5062
+ });
5063
+ function callerTool(env) {
5064
+ if (env.CLAUDECODE === "1") return "claude_code";
5065
+ return null;
5066
+ }
5067
+ function detectTools(d = defaultDetectDeps()) {
5068
+ const caller = callerTool(d.env);
5069
+ return ALL_TOOLS.filter((tool) => {
5070
+ if (tool === caller) return true;
5071
+ if (d.located(tool)) return true;
5072
+ const dir = toolConfigDir(tool);
5073
+ return dir != null && d.exists(dir);
5074
+ });
3433
5075
  }
3434
5076
 
3435
5077
  // src/commands/setup.ts
3436
5078
  import qrcode from "qrcode-terminal";
3437
5079
 
3438
5080
  // src/commands/remoteCwd.ts
3439
- import { existsSync as existsSync15, statSync as statSync5 } from "fs";
3440
- import { createInterface as createInterface2 } from "readline";
3441
- import { homedir as homedir5 } from "os";
3442
- import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
3443
- function normalizePath(raw, home = homedir5()) {
5081
+ import { existsSync as existsSync17, statSync as statSync7 } from "fs";
5082
+ import { createInterface } from "readline";
5083
+ import { homedir as homedir6 } from "os";
5084
+ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
5085
+ function normalizePath(raw, home = homedir6()) {
3444
5086
  const s = (raw || "").trim();
3445
5087
  if (!s) return null;
3446
- const expanded = s === "~" ? home : s.startsWith("~/") ? resolve2(home, s.slice(2)) : s;
3447
- const abs = isAbsolute2(expanded) ? expanded : resolve2(process.cwd(), expanded);
5088
+ const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ? resolve3(home, s.slice(2)) : s;
5089
+ const abs = isAbsolute2(expanded) ? expanded : resolve3(process.cwd(), expanded);
3448
5090
  return abs;
3449
5091
  }
3450
5092
  function checkPath(abs) {
3451
- if (!existsSync15(abs)) return "missing";
5093
+ if (!existsSync17(abs)) return "missing";
3452
5094
  try {
3453
- return statSync5(abs).isDirectory() ? "ok" : "not_a_dir";
5095
+ return statSync7(abs).isDirectory() ? "ok" : "not_a_dir";
3454
5096
  } catch {
3455
5097
  return "missing";
3456
5098
  }
@@ -3481,7 +5123,7 @@ async function askRemoteCwd(deps) {
3481
5123
  }
3482
5124
  function terminalAsk(prompt) {
3483
5125
  if (!process.stdin.isTTY) return Promise.resolve(null);
3484
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
5126
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3485
5127
  return new Promise((res) => {
3486
5128
  rl.question(prompt, (answer) => {
3487
5129
  rl.close();
@@ -3562,11 +5204,11 @@ function parseApiBase(argv) {
3562
5204
  function sleep(ms) {
3563
5205
  return new Promise((r) => setTimeout(r, ms));
3564
5206
  }
3565
- function stdout4(line) {
5207
+ function stdout3(line) {
3566
5208
  process.stdout.write(line + "\n");
3567
5209
  }
3568
5210
  async function runSetup(opts = {}) {
3569
- const out = opts.out ?? stdout4;
5211
+ const out = opts.out ?? stdout3;
3570
5212
  const clientId = ensureClientId();
3571
5213
  if (opts.apiBase) applyApiBase(opts.apiBase);
3572
5214
  const cfg = loadConfig();
@@ -3599,22 +5241,247 @@ async function runSetup(opts = {}) {
3599
5241
  return 0;
3600
5242
  }
3601
5243
 
3602
- // src/commands/status.ts
3603
- function fmtTime(ms) {
3604
- const d = new Date(ms);
3605
- const p = (n) => String(n).padStart(2, "0");
3606
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
3607
- }
3608
- function stdout5(line) {
3609
- process.stdout.write(line + "\n");
3610
- }
3611
- var STATE_LABELS = {
3612
- watch: "\u8FDB\u884C\u4E2D",
3613
- ask_approval: "\u5F85\u786E\u8BA4",
3614
- done: "\u5DF2\u5B8C\u6210",
3615
- failed: "\u5931\u8D25"
5244
+ // src/commands/install.ts
5245
+ var EXIT = {
5246
+ ok: 0,
5247
+ error: 1,
5248
+ codeInvalid: 2,
5249
+ boundOther: 3,
5250
+ network: 4,
5251
+ permission: 5,
5252
+ partial: 6
3616
5253
  };
3617
- function relativeTime(from, now) {
5254
+ var CODE_INSTALL_INVALID = "8257";
5255
+ var CODE_BOUND_OTHER = "8254";
5256
+ var CODE_RATE_LIMITED = "5020";
5257
+ var TOOL_LABELS = {
5258
+ claude_code: "Claude Code",
5259
+ codex: "Codex",
5260
+ workbuddy: "WorkBuddy",
5261
+ qoder: "Qoder CN",
5262
+ opencode: "opencode"
5263
+ };
5264
+ function checkApiBase(raw, env = process.env) {
5265
+ const norm2 = raw.replace(/\/+$/, "");
5266
+ return norm2 === PROD_API_BASE || norm2 === TEST_API_BASE || env.DESKBUDDY_ALLOW_CUSTOM_API === "1";
5267
+ }
5268
+ function parseCode(argv) {
5269
+ const eq = argv.find((a) => a.startsWith("--code="));
5270
+ if (eq) return eq.slice("--code=".length).trim();
5271
+ const i = argv.indexOf("--code");
5272
+ const v = i >= 0 ? argv[i + 1] : void 0;
5273
+ return v && !v.startsWith("-") ? v.trim() : "";
5274
+ }
5275
+ function npxPrefix(version = packageVersion()) {
5276
+ return `npx -y deskbuddy@${/-beta/.test(version) ? "beta" : "latest"}`;
5277
+ }
5278
+ var PERM_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS"]);
5279
+ var NET_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "EHOSTUNREACH"]);
5280
+ function errCode(e) {
5281
+ const x = e;
5282
+ return String(x?.code ?? x?.cause?.code ?? "");
5283
+ }
5284
+ function isPermissionError(e) {
5285
+ return PERM_CODES.has(errCode(e));
5286
+ }
5287
+ function isNetworkError(e) {
5288
+ if (e instanceof ApiError) return false;
5289
+ const name = e?.name;
5290
+ return name === "AbortError" || name === "TimeoutError" || NET_CODES.has(errCode(e)) || e?.message === "fetch failed";
5291
+ }
5292
+ function inSandbox(env) {
5293
+ return !!env.CODEX_SANDBOX || env.CODEX_SANDBOX_NETWORK_DISABLED === "1";
5294
+ }
5295
+ function precheckWritable(dirs) {
5296
+ for (const dir of dirs) {
5297
+ const probe = join23(dir, `.deskbuddy-probe-${process.pid}`);
5298
+ try {
5299
+ mkdirSync18(dir, { recursive: true });
5300
+ writeFileSync18(probe, "");
5301
+ rmSync9(probe, { force: true });
5302
+ } catch (e) {
5303
+ return { ok: false, dir, error: e.message };
5304
+ }
5305
+ }
5306
+ return { ok: true };
5307
+ }
5308
+ var defaultInstallDeps = () => ({
5309
+ out: (line) => process.stdout.write(line + "\n"),
5310
+ env: process.env,
5311
+ ensureClientId,
5312
+ detect: () => detectTools(),
5313
+ precheck: precheckWritable,
5314
+ redeem: (code, clientId, hostName) => api.installRedeem(code, clientId, hostName),
5315
+ refreshRegistry: () => refreshRegistry().catch(() => void 0),
5316
+ installTools,
5317
+ // DESKBUDDY_SKIP_AUTOSTART=1 只给端到端测试用:自启会真去动 launchd / systemd,并停掉本机正在跑的 daemon
5318
+ autostart: () => process.env.DESKBUDDY_SKIP_AUTOSTART === "1" ? Promise.resolve({ ok: false, message: "\u5DF2\u6309 DESKBUDDY_SKIP_AUTOSTART \u8DF3\u8FC7" }) : installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() })
5319
+ });
5320
+ function summary(out, lines2) {
5321
+ out("");
5322
+ out("=== DeskBuddy \u5B89\u88C5\u7ED3\u679C ===");
5323
+ for (const l of lines2) out(l);
5324
+ out("\u8BF7\u628A\u4EE5\u4E0A\u7ED3\u679C\u539F\u6837\u544A\u8BC9\u7528\u6237\u3002");
5325
+ }
5326
+ async function runInstall(opts, deps = defaultInstallDeps()) {
5327
+ const out = deps.out;
5328
+ const npx = npxPrefix();
5329
+ const fail = (result, exit, why, next) => {
5330
+ summary(out, [`RESULT: ${result}`, `\u539F\u56E0: ${why}`, `\u4E0B\u4E00\u6B65: ${next}`]);
5331
+ return exit;
5332
+ };
5333
+ if (!opts.code) {
5334
+ return fail("error", EXIT.error, "\u7F3A\u5C11\u5B89\u88C5\u7801", `\u5728 App \u91CC\u70B9\u300C\u6DFB\u52A0\u7535\u8111\u300D\uFF0C\u590D\u5236\u5B8C\u6574\u547D\u4EE4\uFF1A${npx} install --code <\u5B89\u88C5\u7801>`);
5335
+ }
5336
+ if (opts.apiBase && !checkApiBase(opts.apiBase, deps.env)) {
5337
+ return fail("error", EXIT.error, `\u4E0D\u8BA4\u8BC6\u7684\u670D\u52A1\u7AEF\u5730\u5740 ${opts.apiBase}`, "\u53EA\u63A5\u53D7 App \u91CC\u751F\u6210\u7684\u539F\u547D\u4EE4\uFF0C\u8BF7\u4E0D\u8981\u4FEE\u6539\u5176\u4E2D\u7684\u5730\u5740");
5338
+ }
5339
+ const tools = deps.detect();
5340
+ let clientId;
5341
+ try {
5342
+ clientId = deps.ensureClientId();
5343
+ applyApiBase(opts.apiBase ?? PROD_API_BASE);
5344
+ } catch (e) {
5345
+ if (isPermissionError(e)) return permissionFail(`\u65E0\u6CD5\u5199\u5165 ~/.deskbuddy\uFF08${e.message}\uFF09`);
5346
+ throw e;
5347
+ }
5348
+ const dirs = [deskbuddyHome(), ...tools.map(toolConfigDir).filter((d) => !!d)];
5349
+ const pre = deps.precheck(dirs);
5350
+ if (!pre.ok) return permissionFail(`\u65E0\u6CD5\u5199\u5165 ${pre.dir}\uFF08${pre.error}\uFF09`);
5351
+ function permissionFail(why) {
5352
+ return fail(
5353
+ "permission",
5354
+ EXIT.permission,
5355
+ why,
5356
+ "\u5F53\u524D\u53EF\u80FD\u8FD0\u884C\u5728\u6C99\u7BB1\u91CC\uFF0C\u6CA1\u6709\u5199\u914D\u7F6E\u76EE\u5F55\u6216\u8054\u7F51\u7684\u6743\u9650\u3002\u8BF7\u7533\u8BF7\u63D0\u5347\u6743\u9650\uFF08\u5728\u6C99\u7BB1\u5916\u6267\u884C\uFF09\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4\uFF0C\u5B89\u88C5\u7801\u4ECD\u7136\u6709\u6548"
5357
+ );
5358
+ }
5359
+ const cfg = loadConfig();
5360
+ out(`\u670D\u52A1\u7AEF\uFF1A${cfg.apiBase}`);
5361
+ const envBase = deps.env.DESKBUDDY_API_BASE;
5362
+ if (envBase && !checkApiBase(envBase, {})) out(`\u26A0\uFE0F \u6CE8\u610F\uFF1A\u73AF\u5883\u53D8\u91CF DESKBUDDY_API_BASE \u628A\u670D\u52A1\u7AEF\u6307\u5411\u4E86 ${envBase}\uFF0C\u8FD9\u4E0D\u662F\u5B98\u65B9\u5730\u5740`);
5363
+ let redeemed;
5364
+ try {
5365
+ redeemed = await deps.redeem(opts.code, clientId, cfg.hostName);
5366
+ } catch (e) {
5367
+ if (e instanceof ApiError) {
5368
+ if (e.code === CODE_INSTALL_INVALID) {
5369
+ return fail("code_invalid", EXIT.codeInvalid, "\u5B89\u88C5\u7801\u65E0\u6548\u3001\u5DF2\u8FC7\u671F\u6216\u5DF2\u88AB\u4F7F\u7528", "\u8BF7\u5728 App \u91CC\u91CD\u65B0\u70B9\u300C\u6DFB\u52A0\u7535\u8111\u300D\u751F\u6210\u65B0\u7801\u3002\u4E0D\u8981\u91CD\u8BD5\u540C\u4E00\u4E2A\u7801");
5370
+ }
5371
+ if (e.code === CODE_BOUND_OTHER) {
5372
+ return fail(
5373
+ "bound_other",
5374
+ EXIT.boundOther,
5375
+ "\u8FD9\u53F0\u7535\u8111\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u8D26\u53F7",
5376
+ `\u5148\u95EE\u7528\u6237\u662F\u5426\u8981\u6362\u7ED1\uFF0C**\u4E0D\u8981\u81EA\u52A8\u6267\u884C**\u3002\u7528\u6237\u786E\u8BA4\u540E\u8FD0\u884C ${npx} logout --yes\uFF0C\u518D\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u5B89\u88C5\u547D\u4EE4\uFF08\u5B89\u88C5\u7801 10 \u5206\u949F\u5185\u6709\u6548\uFF09`
5377
+ );
5378
+ }
5379
+ if (e.code === CODE_RATE_LIMITED) {
5380
+ return fail("error", EXIT.error, "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41", "\u7B49\u4E00\u5206\u949F\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4");
5381
+ }
5382
+ return fail("error", EXIT.error, `\u670D\u52A1\u7AEF\u8FD4\u56DE ${e.message}`, "\u7A0D\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4\uFF1B\u53CD\u590D\u5931\u8D25\u8BF7\u5728 App \u91CC\u91CD\u65B0\u751F\u6210\u5B89\u88C5\u7801");
5383
+ }
5384
+ if (isNetworkError(e) && inSandbox(deps.env)) return permissionFail(`\u8FDE\u4E0D\u4E0A\u670D\u52A1\u7AEF\uFF08${e.message}\uFF09\uFF0C\u6C99\u7BB1\u53EF\u80FD\u7981\u6B62\u4E86\u8054\u7F51`);
5385
+ if (isNetworkError(e)) {
5386
+ return fail("network", EXIT.network, `\u8FDE\u4E0D\u4E0A\u670D\u52A1\u7AEF\uFF08${e.message}\uFF09`, "\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4");
5387
+ }
5388
+ return fail("error", EXIT.error, `\u670D\u52A1\u7AEF\u5F02\u5E38\uFF08${e.message}\uFF09`, "\u7A0D\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4");
5389
+ }
5390
+ try {
5391
+ markBound();
5392
+ } catch (e) {
5393
+ if (isPermissionError(e)) return permissionFail(`\u65E0\u6CD5\u5199\u5165 ~/.deskbuddy\uFF08${e.message}\uFF09`);
5394
+ throw e;
5395
+ }
5396
+ await deps.refreshRegistry();
5397
+ out("");
5398
+ const r = tools.length > 0 ? deps.installTools(tools, out) : { installed: [], failed: [] };
5399
+ const bound = loadConfig();
5400
+ if (!bound.remoteCwd) {
5401
+ bound.remoteCwd = defaultRemoteCwd();
5402
+ saveConfig(bound);
5403
+ }
5404
+ const auto = await deps.autostart();
5405
+ const label = (t) => TOOL_LABELS[t] ?? t;
5406
+ const ok = r.installed.length > 0;
5407
+ const lines2 = [
5408
+ `RESULT: ${ok ? "ok" : "partial"}`,
5409
+ `\u8D26\u53F7: ${redeemed.accountHint || "\uFF08\u672A\u77E5\uFF09"}${redeemed.reused ? "\uFF08\u8FD9\u53F0\u7535\u8111\u4E4B\u524D\u5DF2\u7ED1\u5B9A\u8BE5\u8D26\u53F7\uFF09" : ""}\u2014\u2014\u5982\u679C\u8FD9\u4E0D\u662F\u4F60\u7684\u8D26\u53F7\uFF0C\u8BF7\u7ACB\u5373\u8FD0\u884C ${npx} logout --yes`,
5410
+ `\u5DF2\u63A5\u5165: ${r.installed.map(label).join("\u3001") || "\u65E0"}` + (r.failed.length > 0 ? ` \u672A\u63A5\u5165: ${r.failed.map((f) => `${label(f.tool)}\uFF08${f.reason}\uFF09`).join("\u3001")}` : ""),
5411
+ `\u5F00\u673A\u81EA\u542F/\u5B88\u62A4\u8FDB\u7A0B: ${auto.ok ? "\u5DF2\u542F\u52A8" : `\u672A\u8BBE\u7F6E\uFF08${auto.message}\uFF09`}`,
5412
+ `\u8FDC\u7A0B\u65B0\u5EFA\u4EFB\u52A1\u76EE\u5F55: ${loadConfig().remoteCwd}`
5413
+ ];
5414
+ if (ok) {
5415
+ const restart = r.installed.map(label).join(" / ");
5416
+ lines2.push(
5417
+ `\u4E0B\u4E00\u6B65: \u91CD\u542F ${restart} \u540E\u5F00\u59CB\u540C\u6B65\u72B6\u6001\uFF08\u6B63\u5728\u8FD0\u884C\u7684\u4F1A\u8BDD\u4E0D\u4F1A\u91CD\u65B0\u52A0\u8F7D hook\uFF09` + (r.installed.includes("codex") ? "\uFF1BCodex \u4E0B\u6B21\u542F\u52A8\u4F1A\u8981\u6C42\u4FE1\u4EFB DeskBuddy \u7684 hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F" : "")
5418
+ );
5419
+ } else {
5420
+ lines2.push(
5421
+ tools.length === 0 ? `\u4E0B\u4E00\u6B65: \u8FD9\u53F0\u7535\u8111\u4E0A\u6CA1\u68C0\u6D4B\u5230\u652F\u6301\u7684\u667A\u80FD\u4F53\uFF08Claude Code / Codex / WorkBuddy / Qoder CN / opencode\uFF09\u3002\u88C5\u597D\u540E\u8FD0\u884C ${npx} enable <\u5DE5\u5177\u540D>\uFF0C\u5982 claude_code` : `\u4E0B\u4E00\u6B65: \u624B\u52A8\u8FD0\u884C ${npx} enable ${tools.join(",")} \u91CD\u8BD5\uFF0C\u5E76\u628A\u62A5\u9519\u544A\u8BC9\u7528\u6237`
5422
+ );
5423
+ }
5424
+ summary(out, lines2);
5425
+ return ok ? EXIT.ok : EXIT.partial;
5426
+ }
5427
+
5428
+ // src/commands/logout.ts
5429
+ import { createInterface as createInterface2 } from "readline";
5430
+ import { existsSync as existsSync18, rmSync as rmSync10 } from "fs";
5431
+ function stdout4(line) {
5432
+ process.stdout.write(line + "\n");
5433
+ }
5434
+ function askYesNo(question) {
5435
+ return new Promise((resolve5) => {
5436
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
5437
+ rl.question(question, (answer) => {
5438
+ rl.close();
5439
+ resolve5(/^(y|yes)$/i.test(answer.trim()));
5440
+ });
5441
+ });
5442
+ }
5443
+ async function runLogout(opts = {}) {
5444
+ const out = opts.out ?? stdout4;
5445
+ const home = deskbuddyHome();
5446
+ out("deskbuddy logout \u4F1A\u505A\u8FD9\u4E24\u4EF6\u4E8B\uFF1A");
5447
+ out(" 1. \u4ECE\u5404\u667A\u80FD\u4F53\u7684\u914D\u7F6E\u91CC\u79FB\u9664 DeskBuddy \u7684 hook\uFF08\u4F60\u81EA\u5DF1\u7684 hook \u4E0D\u52A8\uFF09");
5448
+ out(` 2. \u5220\u9664\u6574\u4E2A ${home}\uFF08clientId\u3001\u6388\u6743\u72B6\u6001\u3001\u4F1A\u8BDD\u72B6\u6001\u3001\u6620\u5C04\u8868\u7F13\u5B58\uFF09`);
5449
+ out("");
5450
+ out("\u5220\u6389\u4E4B\u540E\u518D\u7528\u5C31\u8981\u91CD\u65B0 deskbuddy setup \u626B\u7801\u3002");
5451
+ out("\u6CE8\u610F\uFF1A\u8FD9\u53EA\u6E05\u9664\u672C\u673A\u6570\u636E\uFF0C\u670D\u52A1\u7AEF\u7684\u7ED1\u5B9A\u5173\u7CFB\u9700\u8981\u5728 App \u91CC\u64A4\u9500\uFF08\u6682\u672A\u652F\u6301\uFF09\uFF1B");
5452
+ out("\u4F46\u91CD\u65B0 setup \u4F1A\u6362\u4E00\u4E2A\u65B0\u7684 clientId\uFF0C\u65E7\u7684\u4E0D\u4F1A\u518D\u4E0A\u62A5\u3002");
5453
+ if (!opts.yes) {
5454
+ const confirm = opts.confirm ?? (() => askYesNo("\u786E\u8BA4\u6267\u884C\uFF1F(y/N) "));
5455
+ if (!await confirm()) {
5456
+ out("\u5DF2\u53D6\u6D88\uFF0C\u4EC0\u4E48\u90FD\u6CA1\u52A8\u3002");
5457
+ return 1;
5458
+ }
5459
+ }
5460
+ await runDisable([], { out, brief: true, keepAutostart: opts.keepAutostart });
5461
+ if (existsSync18(home)) {
5462
+ rmSync10(home, { recursive: true, force: true });
5463
+ }
5464
+ out("");
5465
+ out("\u2705 \u5DF2\u6E05\u9664\u672C\u673A\u7684 DeskBuddy \u6570\u636E\u3002");
5466
+ return 0;
5467
+ }
5468
+
5469
+ // src/commands/status.ts
5470
+ function fmtTime(ms) {
5471
+ const d = new Date(ms);
5472
+ const p = (n) => String(n).padStart(2, "0");
5473
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
5474
+ }
5475
+ function stdout5(line) {
5476
+ process.stdout.write(line + "\n");
5477
+ }
5478
+ var STATE_LABELS = {
5479
+ watch: "\u8FDB\u884C\u4E2D",
5480
+ ask_approval: "\u5F85\u786E\u8BA4",
5481
+ done: "\u5DF2\u5B8C\u6210",
5482
+ failed: "\u5931\u8D25"
5483
+ };
5484
+ function relativeTime(from, now) {
3618
5485
  const sec = Math.max(0, Math.round((now - from) / 1e3));
3619
5486
  if (sec < 60) return `${sec} \u79D2\u524D`;
3620
5487
  const min = Math.round(sec / 60);
@@ -3628,48 +5495,58 @@ function pad(s, width) {
3628
5495
  return s + " ".repeat(Math.max(1, width - w));
3629
5496
  }
3630
5497
  function formatStatus(input) {
3631
- const lines = [];
5498
+ const lines2 = [];
3632
5499
  const cfg = input.config;
3633
- lines.push("DeskBuddy \u72B6\u6001");
5500
+ lines2.push("DeskBuddy \u72B6\u6001");
3634
5501
  const account = cfg.boundAt ? `\u5DF2\u6388\u6743\uFF08clientId ${cfg.clientId.slice(0, 4)}\u2026\uFF0C\u7ED1\u5B9A\u4E8E ${fmtTime(cfg.boundAt)}\uFF09` : "\u672A\u6388\u6743 \u2014\u2014 \u8FD0\u884C deskbuddy setup \u626B\u7801\u6388\u6743";
3635
- lines.push(` ${pad("\u8D26\u53F7", 10)}${account}`);
3636
- lines.push(` ${pad("\u670D\u52A1\u7AEF", 10)}${cfg.apiBase}\uFF08${environmentLabel(cfg.apiBase)}\uFF09`);
5502
+ lines2.push(` ${pad("\u8D26\u53F7", 10)}${account}`);
5503
+ lines2.push(` ${pad("\u670D\u52A1\u7AEF", 10)}${cfg.apiBase}\uFF08${environmentLabel(cfg.apiBase)}\uFF09`);
3637
5504
  if (cfg.boundAt && cfg.boundApiBase && cfg.boundApiBase !== cfg.apiBase) {
3638
- lines.push(
5505
+ lines2.push(
3639
5506
  ` \u26A0\uFE0F \u6388\u6743\u65F6\u8FDE\u7684\u662F ${cfg.boundApiBase}\uFF08${environmentLabel(cfg.boundApiBase)}\uFF09\uFF0C\u4E0E\u5F53\u524D\u4E0D\u4E00\u81F4 \u2014\u2014 \u5F53\u524D\u73AF\u5883\u5E76\u4E0D\u8BA4\u8BC6\u8FD9\u53F0\u673A\u5668\uFF0C\u9700\u8981\u91CD\u65B0 deskbuddy setup`
3640
5507
  );
3641
5508
  }
3642
5509
  if (input.envOverride?.active) {
3643
- lines.push(
5510
+ lines2.push(
3644
5511
  ` \u26A0\uFE0F \u73AF\u5883\u53D8\u91CF DESKBUDDY_API_BASE=${input.envOverride.envValue} \u6B63\u5728\u8986\u76D6 config.json \u91CC\u7684 ${input.envOverride.fileValue}\uFF08\u591A\u534A\u662F\u5199\u8FDB\u4E86 .zshrc \u4E4B\u7C7B\uFF09`
3645
5512
  );
3646
5513
  }
3647
5514
  if (input.tools.length === 0) {
3648
- lines.push(` ${pad("\u5DF2\u542F\u7528", 10)}\u65E0 \u2014\u2014 \u8FD0\u884C deskbuddy enable claude_code,codex`);
5515
+ lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}\u65E0 \u2014\u2014 \u8FD0\u884C deskbuddy enable claude_code,codex`);
3649
5516
  } else {
3650
5517
  const desc = input.tools.map((t) => `${t.tool}\uFF08${t.installed ? "hook \u5DF2\u88C5" : "\u26A0\uFE0F hook \u672A\u88C5\uFF0C\u91CD\u8DD1 deskbuddy enable"}\uFF09`).join("\u3001");
3651
- lines.push(` ${pad("\u5DF2\u542F\u7528", 10)}${desc}`);
5518
+ lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}${desc}`);
3652
5519
  }
3653
5520
  const versionNote = input.hookVersion === null ? "\u672A\u5B89\u88C5" : input.hookVersion === input.currentVersion ? `${input.hookVersion}\uFF08\u4E0E\u5F53\u524D\u5305\u4E00\u81F4\uFF09` : `${input.hookVersion} \u26A0\uFE0F \u5F53\u524D\u5305\u662F ${input.currentVersion}\uFF0C\u4E0B\u6B21\u8FD0\u884C\u4EFB\u610F deskbuddy \u547D\u4EE4\u4F1A\u81EA\u52A8\u6821\u51C6`;
3654
- lines.push(` ${pad("hook \u7248\u672C", 10)}${versionNote}`);
5521
+ lines2.push(` ${pad("hook \u7248\u672C", 10)}${versionNote}`);
5522
+ if (input.windowWrap) {
5523
+ const w = input.windowWrap;
5524
+ 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";
5525
+ lines2.push(` ${pad("\u7A97\u53E3\u63A5\u7BA1", 10)}${text}`);
5526
+ }
5527
+ if (input.codexWrap) {
5528
+ const w = input.codexWrap;
5529
+ const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable codex" : !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` : "\u5DF2\u5F00\u542F\uFF08\u7EC8\u7AEF\u91CC\u6572 codex \u4F1A\u7ED9\u8FD9\u4E2A\u7A97\u53E3\u8D77\u4E00\u4E2A app-server \u518D\u8FDE\u4E0A\u53BB\uFF1Bdaemon \u5728\u8DD1\u65F6\u8BBE\u5907\u6307\u4EE4\u80FD\u53D1\u8FDB\u7A97\u53E3\uFF09";
5530
+ lines2.push(` ${pad("Codex\u7A97\u53E3", 10)}${text}`);
5531
+ }
3655
5532
  for (const t of input.tools) {
3656
- if (t.notice) lines.push(` ${t.notice}`);
5533
+ if (t.notice) lines2.push(` ${t.notice}`);
3657
5534
  }
3658
- lines.push("");
5535
+ lines2.push("");
3659
5536
  const sorted = [...input.sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3660
- lines.push(`\u5F53\u524D\u672C\u5730\u4F1A\u8BDD\uFF08${sorted.length}\uFF09`);
5537
+ lines2.push(`\u5F53\u524D\u672C\u5730\u4F1A\u8BDD\uFF08${sorted.length}\uFF09`);
3661
5538
  if (sorted.length === 0) {
3662
- lines.push(" \u6682\u65E0 \u2014\u2014 \u667A\u80FD\u4F53\u8DD1\u8D77\u6765\u4E4B\u540E\u8FD9\u91CC\u5C31\u4F1A\u6709\u5185\u5BB9");
5539
+ lines2.push(" \u6682\u65E0 \u2014\u2014 \u667A\u80FD\u4F53\u8DD1\u8D77\u6765\u4E4B\u540E\u8FD9\u91CC\u5C31\u4F1A\u6709\u5185\u5BB9");
3663
5540
  }
3664
5541
  for (const s of sorted) {
3665
- lines.push(
5542
+ lines2.push(
3666
5543
  ` ${pad(s.tool, 14)}${pad(s.title || s.project || "-", 24)}${pad(STATE_LABELS[s.state] ?? s.state, 8)}${relativeTime(s.updatedAt, input.now)}`
3667
5544
  );
3668
5545
  }
3669
- lines.push("");
3670
- lines.push("\u91C7\u96C6\u8303\u56F4\uFF1A\u53EA\u4E0A\u62A5 \u5DE5\u5177\u540D/\u72B6\u6001/\u4F1A\u8BDD\u6807\u9898/\u9879\u76EE\u76EE\u5F55\u540D/\u65F6\u95F4\u6233");
3671
- lines.push("\u4E0D\u4E0A\u62A5\uFF1Aprompt \u539F\u6587\u3001\u547D\u4EE4\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u4EE3\u7801\u5185\u5BB9");
3672
- return lines.join("\n");
5546
+ lines2.push("");
5547
+ lines2.push("\u91C7\u96C6\u8303\u56F4\uFF1A\u53EA\u4E0A\u62A5 \u5DE5\u5177\u540D/\u72B6\u6001/\u4F1A\u8BDD\u6807\u9898/\u9879\u76EE\u76EE\u5F55\u540D/\u65F6\u95F4\u6233");
5548
+ lines2.push("\u4E0D\u4E0A\u62A5\uFF1Aprompt \u539F\u6587\u3001\u547D\u4EE4\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u4EE3\u7801\u5185\u5BB9");
5549
+ return lines2.join("\n");
3673
5550
  }
3674
5551
  function collectStatus(now = Date.now()) {
3675
5552
  const config = loadConfig();
@@ -3691,7 +5568,21 @@ function collectStatus(now = Date.now()) {
3691
5568
  });
3692
5569
  sessions.push(...readAliveStates(tool));
3693
5570
  }
5571
+ const wrapTool = config.enabledTools.find((t) => t === "claude_code" && SHIM_COMMANDS[t]);
5572
+ let windowWrap;
5573
+ let codexWrap;
5574
+ if (config.enabledTools.includes("codex")) {
5575
+ const binDir = shimDir();
5576
+ codexWrap = { ...shimState("codex", binDir, process.env.PATH ?? ""), binDir, winPending: process.platform === "win32" };
5577
+ }
5578
+ if (wrapTool) {
5579
+ const binDir = shimDir();
5580
+ const st = shimState(wrapTool, binDir, process.env.PATH ?? "");
5581
+ windowWrap = { ...st, binDir, ptyOk: !!loadPty() && !!loadHeadless(), winPending: process.platform === "win32" && !WIN_SHIM_ENABLED };
5582
+ }
3694
5583
  return {
5584
+ windowWrap,
5585
+ codexWrap,
3695
5586
  config,
3696
5587
  hookVersion: installedHookVersion(),
3697
5588
  currentVersion: packageVersion(),
@@ -3727,10 +5618,1008 @@ async function runUpdate(opts = {}) {
3727
5618
  return runEnable([...cfg.enabledTools], opts);
3728
5619
  }
3729
5620
 
5621
+ // src/wrap/run.ts
5622
+ import { randomBytes as randomBytes6 } from "crypto";
5623
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync20, statSync as statSync9, truncateSync as truncateSync3 } from "fs";
5624
+ import { join as join25 } from "path";
5625
+ import { StringDecoder } from "string_decoder";
5626
+
5627
+ // src/wrap/screen.ts
5628
+ var ESC = "\x1B";
5629
+ var PASTE_BEGIN = ESC + "[200~";
5630
+ var PASTE_END = ESC + "[201~";
5631
+ function sanitize(text) {
5632
+ return String(text).replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
5633
+ }
5634
+ function prepare(text) {
5635
+ return " " + sanitize(text).trim();
5636
+ }
5637
+ var PERMISSION_RE = /Do you want to (proceed|make|create|run|allow)|[❯>]\s*1\. Yes/;
5638
+ function inspectLines(lines2) {
5639
+ const text = lines2.join("\n");
5640
+ const permission = PERMISSION_RE.test(text);
5641
+ const rules = [];
5642
+ lines2.forEach((l, i) => {
5643
+ if (/^─{20,}/.test(l.trim())) rules.push(i);
5644
+ });
5645
+ let input = null;
5646
+ if (rules.length >= 2) {
5647
+ const a = rules[rules.length - 2];
5648
+ const b = rules[rules.length - 1];
5649
+ input = lines2.slice(a + 1, b).join("\n").replace(/^\s*[❯>]\s?/, "").trimEnd();
5650
+ }
5651
+ return { permission, input };
5652
+ }
5653
+ function boxMatches(view, body) {
5654
+ if (view.input === null || view.permission) return false;
5655
+ const flat = (x) => x.replace(/\s+/g, "");
5656
+ return flat(view.input) === flat(body) || /\[Pasted text #\d+/.test(view.input);
5657
+ }
5658
+
5659
+ // src/wrap/injector.ts
5660
+ var QUIET_MS = 1500;
5661
+ var KEY_QUIET_MS = 3e3;
5662
+ var PASTE_SETTLE_MS = 400;
5663
+ var SUBMIT_WAIT_MS = 1e4;
5664
+ var IDLE_EVENTS = /* @__PURE__ */ new Set(["Stop", "SessionStart"]);
5665
+ var Injector = class {
5666
+ #d;
5667
+ #queue = [];
5668
+ #lastOut;
5669
+ #lastKey = 0;
5670
+ #busy = false;
5671
+ #closed = false;
5672
+ constructor(deps) {
5673
+ this.#d = deps;
5674
+ this.#lastOut = deps.now();
5675
+ }
5676
+ noteOutput() {
5677
+ this.#lastOut = this.#d.now();
5678
+ }
5679
+ noteKey() {
5680
+ this.#lastKey = this.#d.now();
5681
+ }
5682
+ get pending() {
5683
+ return this.#queue.length;
5684
+ }
5685
+ /** 回终态(只回一次),并移出队列 */
5686
+ #final(job, msg) {
5687
+ const i = this.#queue.indexOf(job);
5688
+ if (i >= 0) this.#queue.splice(i, 1);
5689
+ if (job.done) return;
5690
+ job.done = true;
5691
+ job.reply({ instructionId: job.instructionId, ...msg });
5692
+ }
5693
+ /**
5694
+ * 这个会话还是不是本窗口当前的会话。
5695
+ * 🔴 不是(/clear 换了会话号、会话 SessionEnd)时回 rejected/session_gone,**不回 not_here**:
5696
+ * claude 还在这个窗口里活着,交回 daemon 走后台也只会被 window_open / session_gone 拒掉,
5697
+ * 而且旧会话的上下文已经不在窗口里了,写进新会话等于答非所问。只有窗口真正关闭(drain)才 not_here
5698
+ */
5699
+ #owns(sessionId) {
5700
+ const last = this.#d.lastEvent();
5701
+ if (!last) return true;
5702
+ return last.sid === sessionId && last.ev !== "SessionEnd";
5703
+ }
5704
+ /** 通道收到一条指令 */
5705
+ submit(job) {
5706
+ if (this.#closed) {
5707
+ job.reply({ instructionId: job.instructionId, status: "not_here" });
5708
+ return;
5709
+ }
5710
+ if (!this.#owns(job.sessionId)) {
5711
+ job.done = true;
5712
+ job.reply({ instructionId: job.instructionId, status: "rejected", mode: "window", reason: "session_gone" });
5713
+ return;
5714
+ }
5715
+ job.phase = "queued";
5716
+ job.reply({ instructionId: job.instructionId, status: "ack" });
5717
+ this.#queue.push(job);
5718
+ if (this.#queue.length > 1 || this.#busy) this.#markQueued(job);
5719
+ void this.tick();
5720
+ }
5721
+ #markQueued(job) {
5722
+ if (job.queuedSent) return;
5723
+ job.queuedSent = true;
5724
+ job.reply({ instructionId: job.instructionId, status: "queued", mode: "window", reason: "" });
5725
+ }
5726
+ /** 空闲判断。返回不满足的原因,空数组 = 可以写 */
5727
+ precheck() {
5728
+ const why = [];
5729
+ const v = this.#d.view();
5730
+ if (!v) return ["no-screen"];
5731
+ if (v.permission) why.push("permission");
5732
+ if (v.input === null) why.push("no-input-box");
5733
+ else if (v.input !== "") why.push("user-typing");
5734
+ const last = this.#d.lastEvent();
5735
+ if (!last) why.push("hook:none");
5736
+ else if (!IDLE_EVENTS.has(last.ev)) why.push("hook:" + last.ev);
5737
+ const now = this.#d.now();
5738
+ if (now - this.#lastOut < QUIET_MS) why.push("screen-busy");
5739
+ if (now - this.#lastKey < KEY_QUIET_MS) why.push("recent-key");
5740
+ return why;
5741
+ }
5742
+ /** 定时调(包装层每 300ms 一次)。一次最多处理队首一条 */
5743
+ async tick() {
5744
+ if (this.#busy || this.#closed || this.#queue.length === 0) return;
5745
+ const job = this.#queue[0];
5746
+ if (job.alive && !job.alive()) {
5747
+ this.#queue.shift();
5748
+ job.done = true;
5749
+ this.#d.log?.("daemon \u8FDE\u63A5\u5DF2\u65AD\uFF0C\u4E22\u5F03\u672A\u5199\u5165\u7684\u4E00\u6761");
5750
+ return;
5751
+ }
5752
+ if (job.expireAt > 0 && this.#d.now() > job.expireAt) {
5753
+ this.#final(job, { status: "rejected", mode: "window", reason: "expired" });
5754
+ return;
5755
+ }
5756
+ if (!this.#owns(job.sessionId)) {
5757
+ this.#final(job, { status: "rejected", mode: "window", reason: "session_gone" });
5758
+ return;
5759
+ }
5760
+ let why;
5761
+ try {
5762
+ why = this.precheck();
5763
+ } catch (e) {
5764
+ why = ["precheck-error"];
5765
+ this.#d.log?.(`\u7A7A\u95F2\u5224\u65AD\u51FA\u9519\uFF1A${e.message}`);
5766
+ }
5767
+ if (why.length > 0) {
5768
+ this.#markQueued(job);
5769
+ return;
5770
+ }
5771
+ this.#busy = true;
5772
+ try {
5773
+ await this.#inject(job);
5774
+ } catch (e) {
5775
+ this.#d.log?.(`\u5199\u5165\u51FA\u9519\uFF1A${e.message}`);
5776
+ this.#final(job, job.phase === "entered" ? { status: "started", mode: "window", reason: "" } : { status: "failed", mode: "window", reason: "window_failed" });
5777
+ } finally {
5778
+ this.#busy = false;
5779
+ }
5780
+ }
5781
+ async #inject(job) {
5782
+ const body = prepare(job.text);
5783
+ let enterAt = 0;
5784
+ this.#d.holdKeys(true);
5785
+ try {
5786
+ job.phase = "pasted";
5787
+ this.#d.write(PASTE_BEGIN + body + PASTE_END);
5788
+ await this.#d.sleep(PASTE_SETTLE_MS);
5789
+ if (job.done) return;
5790
+ const v = this.#d.view();
5791
+ if (!v || !boxMatches(v, body)) {
5792
+ this.#d.log?.("\u7C98\u8D34\u540E\u8F93\u5165\u6846\u5BF9\u4E0D\u4E0A\uFF0C\u653E\u5F03\u8FD9\u6761");
5793
+ this.#final(job, { status: "failed", mode: "window", reason: "window_failed" });
5794
+ return;
5795
+ }
5796
+ enterAt = this.#d.now();
5797
+ job.phase = "entered";
5798
+ this.#d.write("\r");
5799
+ } finally {
5800
+ this.#d.holdKeys(false);
5801
+ }
5802
+ let ok = false;
5803
+ const deadline = this.#d.now() + SUBMIT_WAIT_MS;
5804
+ while (this.#d.now() < deadline && !job.done) {
5805
+ const e = this.#d.lastEvent();
5806
+ if (e && e.at >= enterAt && e.ev !== "SessionEnd") {
5807
+ ok = true;
5808
+ break;
5809
+ }
5810
+ await this.#d.sleep(200);
5811
+ }
5812
+ if (!ok && !job.done) {
5813
+ const v = this.#d.view();
5814
+ ok = !!v && v.input === "";
5815
+ }
5816
+ this.#final(job, ok ? { status: "started", mode: "window", reason: "" } : { status: "failed", mode: "window", reason: "window_failed" });
5817
+ }
5818
+ /**
5819
+ * 窗口要关了(或包装层降级、关掉写入能力):
5820
+ * - 还没碰窗口的 → not_here,交回 daemon 走后台(窗口一关,会话就能 --resume 了)
5821
+ * - 已回车的 → started(大概率已经提交);已粘贴未回车的 → failed/window_failed
5822
+ * 🔴 后两种绝不能 not_here:daemon 会在后台再执行一遍
5823
+ */
5824
+ drain() {
5825
+ this.#closed = true;
5826
+ for (const job of [...this.#queue]) {
5827
+ if (job.phase === "entered") this.#final(job, { status: "started", mode: "window", reason: "" });
5828
+ else if (job.phase === "pasted") this.#final(job, { status: "failed", mode: "window", reason: "window_failed" });
5829
+ else this.#final(job, { status: "not_here" });
5830
+ }
5831
+ }
5832
+ };
5833
+
5834
+ // src/wrap/passthrough.ts
5835
+ import { spawn as spawn2 } from "child_process";
5836
+ import { realpathSync as realpathSync2 } from "fs";
5837
+ import { constants as constants2 } from "os";
5838
+ import { dirname as dirname11, resolve as resolve4 } from "path";
5839
+ var CLAUDE_SUBCOMMANDS = /* @__PURE__ */ new Set([
5840
+ "agents",
5841
+ "attach",
5842
+ "auth",
5843
+ "auto-mode",
5844
+ "config",
5845
+ "doctor",
5846
+ "gateway",
5847
+ "import",
5848
+ "install",
5849
+ "kill",
5850
+ "logs",
5851
+ "mcp",
5852
+ "migrate-installer",
5853
+ "plugin",
5854
+ "plugins",
5855
+ "project",
5856
+ "respawn",
5857
+ "rm",
5858
+ "setup-token",
5859
+ "stop",
5860
+ "ultrareview",
5861
+ "update",
5862
+ "upgrade"
5863
+ ]);
5864
+ var WRAPPABLE_TOOLS = /* @__PURE__ */ new Set(["claude_code"]);
5865
+ var LONG_PASS = /* @__PURE__ */ new Set(["--print", "--output-format", "--version", "--help", "--input-format"]);
5866
+ function passthroughReason(i) {
5867
+ if (!WRAPPABLE_TOOLS.has(i.tool)) return "tool";
5868
+ if (i.env.DESKBUDDY_WRAP_OFF === "1") return "wrap_off";
5869
+ if (i.env.DESKBUDDY_WRAPPED) return "wrapped";
5870
+ if (i.env.DESKBUDDY_RUN_ID) return "remote_run";
5871
+ if (!i.stdinTTY || !i.stdoutTTY) return "not_tty";
5872
+ let firstPositional = null;
5873
+ for (const a of i.args) {
5874
+ if (a === "--") break;
5875
+ if (a.startsWith("--")) {
5876
+ if (LONG_PASS.has(a.split("=")[0])) return "flag";
5877
+ continue;
5878
+ }
5879
+ if (/^-[a-zA-Z]+$/.test(a)) {
5880
+ if (/[pvh]/.test(a.slice(1))) return "flag";
5881
+ continue;
5882
+ }
5883
+ if (firstPositional === null) firstPositional = a;
5884
+ }
5885
+ if (firstPositional !== null && CLAUDE_SUBCOMMANDS.has(firstPositional)) return "subcommand";
5886
+ return null;
5887
+ }
5888
+ function norm(p, win) {
5889
+ const r = resolve4(p).replace(/[\\/]+$/, "");
5890
+ return win ? r.replace(/\//g, "\\").toLowerCase() : r;
5891
+ }
5892
+ function real(p) {
5893
+ try {
5894
+ return realpathSync2(p);
5895
+ } catch {
5896
+ return p;
5897
+ }
5898
+ }
5899
+ function findReal(tool, d = defaultLocateDeps(), skip = shimDir()) {
5900
+ const win = (d.platform ?? process.platform) === "win32";
5901
+ const skipKeys = /* @__PURE__ */ new Set([norm(skip, win), norm(real(skip), win)]);
5902
+ const inSkip = (file) => skipKeys.has(norm(dirname11(file), win)) || skipKeys.has(norm(dirname11(real(file)), win));
5903
+ const dd = { ...d, skipDirs: [...d.skipDirs ?? [], skip] };
5904
+ const name = SHIM_COMMANDS[tool];
5905
+ if (!name) {
5906
+ const loc = locate(tool, dd);
5907
+ return loc && !inSkip(loc.prefixArgs[0] && /\.(?:c|m)?js$/i.test(loc.prefixArgs[0]) ? loc.prefixArgs[0] : loc.file) ? loc : null;
5908
+ }
5909
+ for (const f of whichAllIn(name, dd)) {
5910
+ if (inSkip(f)) continue;
5911
+ const loc = win ? resolveWinLaunch(f, dd) : { file: f, prefixArgs: [] };
5912
+ if (!loc) continue;
5913
+ const target = loc.prefixArgs[0] && /\.(?:c|m)?js$/i.test(loc.prefixArgs[0]) ? loc.prefixArgs[0] : loc.file;
5914
+ if (inSkip(target)) continue;
5915
+ return loc;
5916
+ }
5917
+ return null;
5918
+ }
5919
+ function withoutShimFlag(env) {
5920
+ const out = { ...env };
5921
+ delete out.DESKBUDDY_SHIM;
5922
+ return out;
5923
+ }
5924
+ function runDirect(real2, args, env = process.env) {
5925
+ return new Promise((done) => {
5926
+ let child;
5927
+ try {
5928
+ child = spawn2(real2.file, [...real2.prefixArgs, ...args], {
5929
+ stdio: "inherit",
5930
+ // 🔴 再经过垫片也直接放行,不会绕回来
5931
+ env: withoutShimFlag({ ...env, DESKBUDDY_WRAPPED: "1" }),
5932
+ windowsHide: false
5933
+ });
5934
+ } catch (e) {
5935
+ process.stderr.write(`deskbuddy: \u542F\u52A8 ${real2.file} \u5931\u8D25\uFF1A${e.message}
5936
+ `);
5937
+ done(127);
5938
+ return;
5939
+ }
5940
+ const ignore = () => {
5941
+ };
5942
+ const forward = (sig) => () => {
5943
+ try {
5944
+ child.kill(sig);
5945
+ } catch {
5946
+ }
5947
+ };
5948
+ const handlers = [
5949
+ ["SIGINT", ignore],
5950
+ ["SIGQUIT", ignore],
5951
+ ["SIGTERM", forward("SIGTERM")],
5952
+ ["SIGHUP", forward("SIGHUP")]
5953
+ ];
5954
+ for (const [s, h] of handlers) {
5955
+ try {
5956
+ process.on(s, h);
5957
+ } catch {
5958
+ }
5959
+ }
5960
+ const cleanup = () => {
5961
+ for (const [s, h] of handlers) process.removeListener(s, h);
5962
+ };
5963
+ child.on("error", (e) => {
5964
+ cleanup();
5965
+ process.stderr.write(`deskbuddy: \u542F\u52A8 ${real2.file} \u5931\u8D25\uFF1A${e.message}
5966
+ `);
5967
+ done(127);
5968
+ });
5969
+ child.on("exit", (code, signal) => {
5970
+ cleanup();
5971
+ if (code !== null) done(code);
5972
+ else done(128 + (signal && constants2.signals[signal] || 1));
5973
+ });
5974
+ });
5975
+ }
5976
+
5977
+ // src/wrap/codexLaunch.ts
5978
+ import { spawn as spawn3 } from "child_process";
5979
+ import { closeSync, mkdirSync as mkdirSync19, openSync, readdirSync as readdirSync4, readFileSync as readFileSync18, rmSync as rmSync11, statSync as statSync8, truncateSync as truncateSync2, writeFileSync as writeFileSync19, renameSync as renameSync13 } from "fs";
5980
+ import { request } from "http";
5981
+ import { createServer as createServer3 } from "net";
5982
+ import { join as join24 } from "path";
5983
+
5984
+ // src/core/codexServer.ts
5985
+ import { execFileSync as execFileSync8 } from "child_process";
5986
+ function isAppServerArgs(args, url) {
5987
+ return /\bapp-server\b/.test(args) && new RegExp(`--listen[= ]${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$)`).test(args);
5988
+ }
5989
+ function processArgs(pid) {
5990
+ if (process.platform === "win32") return null;
5991
+ try {
5992
+ const out = execFileSync8("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
5993
+ encoding: "utf8",
5994
+ stdio: ["ignore", "pipe", "ignore"]
5995
+ });
5996
+ return out.trim() || null;
5997
+ } catch {
5998
+ return null;
5999
+ }
6000
+ }
6001
+
6002
+ // src/wrap/codexLaunch.ts
6003
+ var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
6004
+ var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
6005
+ var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
6006
+ var RESUME_FLAGS = /* @__PURE__ */ new Set(["--last", "--all", "--include-non-interactive"]);
6007
+ var CODEX_TUI_SUBCOMMANDS = /* @__PURE__ */ new Set(["resume", "fork"]);
6008
+ var CODEX_SUBCOMMANDS = /* @__PURE__ */ new Set([
6009
+ "agents",
6010
+ "exec",
6011
+ "e",
6012
+ "review",
6013
+ "login",
6014
+ "logout",
6015
+ "mcp",
6016
+ "plugin",
6017
+ "app-server",
6018
+ "remote-control",
6019
+ "app",
6020
+ "completion",
6021
+ "update",
6022
+ "doctor",
6023
+ "sandbox",
6024
+ "debug",
6025
+ "apply",
6026
+ "a",
6027
+ "resume",
6028
+ "queue",
6029
+ "archive",
6030
+ "delete",
6031
+ "migrate-rollouts",
6032
+ "unarchive",
6033
+ "fork",
6034
+ "cloud",
6035
+ "exec-server",
6036
+ "features",
6037
+ "help",
6038
+ "mcp-server"
6039
+ ]);
6040
+ function codexTakeoverBlock(args) {
6041
+ let sub = null;
6042
+ let prompt = false;
6043
+ for (let i = 0; i < args.length; i++) {
6044
+ const a = args[i];
6045
+ if (a === "--") return null;
6046
+ if (a.startsWith("-") && a !== "-") {
6047
+ let name = a;
6048
+ let inline = false;
6049
+ if (a.startsWith("--")) {
6050
+ name = a.split("=")[0];
6051
+ inline = a.includes("=");
6052
+ } else if (a.length > 2 && SHORT_VALUE_OPTS.has(a.slice(0, 2))) {
6053
+ name = a.slice(0, 2);
6054
+ inline = true;
6055
+ }
6056
+ if (name === "--remote") return "remote";
6057
+ if (TAKEOVER_VALUE_OPTS.has(name)) {
6058
+ if (!inline) i++;
6059
+ continue;
6060
+ }
6061
+ if (TAKEOVER_FLAGS.has(name)) continue;
6062
+ if (sub && RESUME_FLAGS.has(name)) continue;
6063
+ return `option:${name}`;
6064
+ }
6065
+ if (sub || prompt) continue;
6066
+ if (CODEX_TUI_SUBCOMMANDS.has(a)) {
6067
+ sub = a;
6068
+ continue;
6069
+ }
6070
+ if (CODEX_SUBCOMMANDS.has(a)) return `subcommand:${a}`;
6071
+ prompt = true;
6072
+ }
6073
+ return null;
6074
+ }
6075
+ function hasCdFlag(args) {
6076
+ const end = args.indexOf("--");
6077
+ return (end < 0 ? args : args.slice(0, end)).some((a) => a === "-C" || a === "--cd" || a.startsWith("--cd=") || a.startsWith("-C") && a.length > 2);
6078
+ }
6079
+ var READY_WAIT_MS = 10 * 1e3;
6080
+ var READY_POLL_MS = 50;
6081
+ function codexPassthroughReason(args, d) {
6082
+ if (d.platform === "win32") return "windows";
6083
+ if (d.env.DESKBUDDY_WRAP_OFF === "1") return "wrap_off";
6084
+ if (d.env.DESKBUDDY_WRAPPED) return "wrapped";
6085
+ if (d.env.DESKBUDDY_RUN_ID) return "remote_run";
6086
+ if (!d.stdinTTY || !d.stdoutTTY) return "not_tty";
6087
+ return codexTakeoverBlock(args);
6088
+ }
6089
+ async function runCodexWindow(real2, args, d) {
6090
+ let rec = null;
6091
+ let tuiArgs = null;
6092
+ try {
6093
+ const why = codexPassthroughReason(args, d) ?? (d.daemonAlive() ? null : "no_daemon");
6094
+ if (why) {
6095
+ d.log(`codex \u76F4\u63A5\u8FD0\u884C\uFF08${why}\uFF09`);
6096
+ } else {
6097
+ try {
6098
+ d.reap();
6099
+ } catch {
6100
+ }
6101
+ const t0 = d.now();
6102
+ const url = `ws://127.0.0.1:${await d.freePort()}`;
6103
+ const pid = d.startServer(real2, url);
6104
+ if (!pid) {
6105
+ d.log("app-server \u8D77\u4E0D\u6765\uFF0Ccodex \u76F4\u63A5\u8FD0\u884C");
6106
+ } else {
6107
+ rec = { supervisor: process.pid, server: pid, url };
6108
+ d.record(rec);
6109
+ let ok = false;
6110
+ for (; ; ) {
6111
+ if (await d.ready(url)) {
6112
+ ok = true;
6113
+ break;
6114
+ }
6115
+ if (d.now() - t0 >= READY_WAIT_MS) break;
6116
+ await d.sleep(READY_POLL_MS);
6117
+ }
6118
+ if (ok) {
6119
+ d.log(`app-server ${url} \u5C31\u7EEA ${d.now() - t0}ms`);
6120
+ tuiArgs = ["--remote", url, ...hasCdFlag(args) ? [] : ["-C", d.cwd()], ...args];
6121
+ } else {
6122
+ d.log("app-server \u6CA1\u80FD\u6309\u65F6\u5C31\u7EEA\uFF0Ccodex \u76F4\u63A5\u8FD0\u884C");
6123
+ d.killServer(pid, url);
6124
+ d.unrecord(rec);
6125
+ rec = null;
6126
+ }
6127
+ }
6128
+ }
6129
+ } catch (e) {
6130
+ d.log(`codex \u7A97\u53E3\u51C6\u5907\u51FA\u9519\uFF0C\u76F4\u63A5\u8FD0\u884C\uFF1A${e.message}`);
6131
+ if (rec) {
6132
+ d.killServer(rec.server, rec.url);
6133
+ d.unrecord(rec);
6134
+ rec = null;
6135
+ }
6136
+ tuiArgs = null;
6137
+ }
6138
+ if (!tuiArgs) return d.runDirect(real2, args);
6139
+ try {
6140
+ return await d.runDirect(real2, tuiArgs);
6141
+ } finally {
6142
+ if (rec) {
6143
+ d.killServer(rec.server, rec.url);
6144
+ d.unrecord(rec);
6145
+ }
6146
+ }
6147
+ }
6148
+ var MAX_LOG_BYTES2 = 1024 * 1024;
6149
+ function appServerLogPath() {
6150
+ return join24(deskbuddyHome(), "codex-app-server.log");
6151
+ }
6152
+ function windowsDir2() {
6153
+ return join24(deskbuddyHome(), "codex-windows");
6154
+ }
6155
+ function pidAlive(pid) {
6156
+ try {
6157
+ process.kill(pid, 0);
6158
+ return true;
6159
+ } catch {
6160
+ return false;
6161
+ }
6162
+ }
6163
+ function daemonAlive(pidFile = pidFilePath(), args = processArgs) {
6164
+ let pid = 0;
6165
+ try {
6166
+ pid = Number(readFileSync18(pidFile, "utf8").trim());
6167
+ } catch {
6168
+ return false;
6169
+ }
6170
+ if (!Number.isInteger(pid) || pid <= 0 || !pidAlive(pid)) return false;
6171
+ const a = args(pid);
6172
+ return !!a && /cli\.js daemon(?:\s|$)/.test(a);
6173
+ }
6174
+ function probeReady(url) {
6175
+ const port = Number(url.slice(url.lastIndexOf(":") + 1));
6176
+ return new Promise((resolve5) => {
6177
+ const req2 = request({ host: "127.0.0.1", port, path: "/readyz", method: "GET", timeout: 800 }, (res) => {
6178
+ res.resume();
6179
+ resolve5((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
6180
+ });
6181
+ req2.on("timeout", () => req2.destroy());
6182
+ req2.on("error", () => resolve5(false));
6183
+ req2.end();
6184
+ });
6185
+ }
6186
+ function freePort() {
6187
+ return new Promise((resolve5, reject) => {
6188
+ const srv = createServer3();
6189
+ srv.unref();
6190
+ srv.on("error", reject);
6191
+ srv.listen(0, "127.0.0.1", () => {
6192
+ const addr = srv.address();
6193
+ const port = typeof addr === "object" && addr ? addr.port : 0;
6194
+ srv.close(() => port ? resolve5(port) : reject(new Error("no port")));
6195
+ });
6196
+ });
6197
+ }
6198
+ function startServer(real2, url, env, cwd) {
6199
+ const logPath2 = appServerLogPath();
6200
+ mkdirSync19(deskbuddyHome(), { recursive: true });
6201
+ try {
6202
+ if (statSync8(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
6203
+ } catch {
6204
+ }
6205
+ const fd = openSync(logPath2, "a");
6206
+ try {
6207
+ const childEnv2 = { ...env, DESKBUDDY_WRAPPED: "1" };
6208
+ delete childEnv2.DESKBUDDY_SHIM;
6209
+ const child = spawn3(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
6210
+ detached: true,
6211
+ stdio: ["ignore", fd, fd],
6212
+ cwd,
6213
+ env: childEnv2
6214
+ });
6215
+ child.on("error", () => {
6216
+ });
6217
+ child.unref();
6218
+ return child.pid ?? null;
6219
+ } finally {
6220
+ closeSync(fd);
6221
+ }
6222
+ }
6223
+ function killServer(pid) {
6224
+ try {
6225
+ process.kill(-pid, "SIGTERM");
6226
+ } catch {
6227
+ }
6228
+ }
6229
+ function killOwnServer(pid, url) {
6230
+ const a = processArgs(pid);
6231
+ if (a && isAppServerArgs(a, url)) killServer(pid);
6232
+ }
6233
+ function recordPath(rec) {
6234
+ return join24(windowsDir2(), `${rec.server}.json`);
6235
+ }
6236
+ function writeRecord(rec) {
6237
+ try {
6238
+ mkdirSync19(windowsDir2(), { recursive: true, mode: 448 });
6239
+ const p = recordPath(rec);
6240
+ writeFileSync19(p + ".tmp", JSON.stringify(rec), { encoding: "utf8", mode: 384 });
6241
+ renameSync13(p + ".tmp", p);
6242
+ } catch {
6243
+ }
6244
+ }
6245
+ function removeRecord(rec) {
6246
+ try {
6247
+ rmSync11(recordPath(rec), { force: true });
6248
+ } catch {
6249
+ }
6250
+ }
6251
+ function reapWindows(dir = windowsDir2(), alive = pidAlive, args = processArgs, kill = killServer) {
6252
+ let names = [];
6253
+ try {
6254
+ names = readdirSync4(dir).filter((n) => n.endsWith(".json"));
6255
+ } catch {
6256
+ return;
6257
+ }
6258
+ for (const n of names) {
6259
+ const p = join24(dir, n);
6260
+ let rec;
6261
+ try {
6262
+ rec = JSON.parse(readFileSync18(p, "utf8"));
6263
+ } catch {
6264
+ rmSync11(p, { force: true });
6265
+ continue;
6266
+ }
6267
+ if (typeof rec.supervisor !== "number" || typeof rec.server !== "number" || typeof rec.url !== "string") {
6268
+ rmSync11(p, { force: true });
6269
+ continue;
6270
+ }
6271
+ if (alive(rec.supervisor)) continue;
6272
+ if (alive(rec.server)) {
6273
+ const a = args(rec.server);
6274
+ if (a && isAppServerArgs(a, rec.url)) kill(rec.server);
6275
+ }
6276
+ rmSync11(p, { force: true });
6277
+ }
6278
+ }
6279
+ function launchLog(line) {
6280
+ try {
6281
+ mkdirSync19(deskbuddyHome(), { recursive: true });
6282
+ writeFileSync19(join24(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
6283
+ `, { flag: "a" });
6284
+ } catch {
6285
+ }
6286
+ }
6287
+ function defaultCodexWindowDeps(base) {
6288
+ return {
6289
+ ...base,
6290
+ now: () => Date.now(),
6291
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
6292
+ daemonAlive: () => daemonAlive(),
6293
+ freePort,
6294
+ startServer: (real2, url) => startServer(real2, url, base.env, base.cwd()),
6295
+ ready: probeReady,
6296
+ killServer: killOwnServer,
6297
+ record: writeRecord,
6298
+ unrecord: removeRecord,
6299
+ reap: () => reapWindows(),
6300
+ log: launchLog
6301
+ };
6302
+ }
6303
+
6304
+ // src/wrap/run.ts
6305
+ var DEFAULT_TTL_MS = 10 * 60 * 1e3;
6306
+ var TICK_MS = 300;
6307
+ var MAX_LOG_BYTES3 = 512 * 1024;
6308
+ var WRAP_HINT = "DeskBuddy \u5305\u88C5\u51FA\u9519\uFF0C\u8BF7\u8BBE DESKBUDDY_WRAP_OFF=1 \u4E34\u65F6\u7ED5\u5F00\uFF08\u4F8B\u5982 DESKBUDDY_WRAP_OFF=1 claude\uFF09";
6309
+ var SESSION_ENV = ["DESKBUDDY_SHIM", "CLAUDECODE", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_ID", "CLAUDE_PID", "CLAUDE_CODE_ENTRYPOINT"];
6310
+ function childEnv(src, windowId) {
6311
+ const env = {};
6312
+ for (const [k, v] of Object.entries(src)) {
6313
+ if (SESSION_ENV.includes(k)) continue;
6314
+ env[k] = v;
6315
+ }
6316
+ env.DESKBUDDY_WINDOW = windowId;
6317
+ env.DESKBUDDY_WRAPPED = "1";
6318
+ return env;
6319
+ }
6320
+ function wrapLog(line) {
6321
+ try {
6322
+ const p = join25(deskbuddyHome(), "wrap.log");
6323
+ mkdirSync20(deskbuddyHome(), { recursive: true });
6324
+ try {
6325
+ if (statSync9(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
6326
+ } catch {
6327
+ }
6328
+ appendFileSync2(p, `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${line}
6329
+ `, "utf8");
6330
+ } catch {
6331
+ }
6332
+ }
6333
+ var defaultWrapDeps = () => ({
6334
+ env: process.env,
6335
+ stdin: process.stdin,
6336
+ stdout: process.stdout,
6337
+ stderr: process.stderr,
6338
+ platform: process.platform,
6339
+ cwd: () => process.cwd(),
6340
+ findReal: (tool) => findReal(tool),
6341
+ runDirect: (real2, args) => runDirect(real2, args),
6342
+ loadPty,
6343
+ loadHeadless,
6344
+ log: wrapLog
6345
+ });
6346
+ async function runWrap(argv, deps = defaultWrapDeps()) {
6347
+ const viaShim = deps.env.DESKBUDDY_SHIM === "1";
6348
+ const r = await runWrapInner(argv, deps);
6349
+ if (r === "not_found") {
6350
+ if (viaShim) return SHIM_FALLBACK_EXIT;
6351
+ deps.stderr.write(`deskbuddy: \u627E\u4E0D\u5230 ${SHIM_COMMANDS[argv[0] ?? ""] ?? argv[0]}\uFF08\u5DF2\u8DF3\u8FC7 DeskBuddy \u81EA\u5DF1\u7684\u57AB\u7247\u76EE\u5F55\uFF09
6352
+ `);
6353
+ return 127;
6354
+ }
6355
+ return viaShim && r === SHIM_FALLBACK_EXIT ? 1 : r;
6356
+ }
6357
+ async function runWrapInner(argv, deps) {
6358
+ const tool = argv[0] ?? "";
6359
+ const args = argv.slice(1);
6360
+ let real2 = null;
6361
+ try {
6362
+ real2 = deps.findReal(tool);
6363
+ } catch {
6364
+ real2 = null;
6365
+ }
6366
+ if (!real2) return "not_found";
6367
+ if (tool === "codex") {
6368
+ return runCodexWindow(
6369
+ real2,
6370
+ args,
6371
+ defaultCodexWindowDeps({
6372
+ env: deps.env,
6373
+ platform: deps.platform,
6374
+ stdinTTY: !!deps.stdin.isTTY,
6375
+ stdoutTTY: !!deps.stdout.isTTY,
6376
+ cwd: deps.cwd,
6377
+ runDirect: deps.runDirect
6378
+ })
6379
+ );
6380
+ }
6381
+ let reason;
6382
+ try {
6383
+ reason = passthroughReason({
6384
+ tool,
6385
+ args,
6386
+ env: deps.env,
6387
+ stdinTTY: !!deps.stdin.isTTY,
6388
+ stdoutTTY: !!deps.stdout.isTTY
6389
+ });
6390
+ } catch {
6391
+ reason = "error";
6392
+ }
6393
+ if (reason) return deps.runDirect(real2, args);
6394
+ let r = null;
6395
+ try {
6396
+ r = await wrapInPty(tool, real2, args, deps);
6397
+ } catch (e) {
6398
+ deps.log(`\u5305\u88C5\u5931\u8D25\uFF0C\u653E\u884C\u76F4\u8DD1\uFF1A${e.message}`);
6399
+ r = null;
6400
+ }
6401
+ if (r !== null) return r;
6402
+ return deps.runDirect(real2, args);
6403
+ }
6404
+ async function wrapInPty(tool, real2, args, deps) {
6405
+ const pty = deps.loadPty();
6406
+ const Headless = deps.loadHeadless();
6407
+ if (!pty || !Headless) {
6408
+ deps.log("node-pty / @xterm/headless \u52A0\u8F7D\u4E0D\u4E86\uFF0C\u653E\u884C\u76F4\u8DD1");
6409
+ return null;
6410
+ }
6411
+ const { stdout: stdout7 } = deps;
6412
+ const windowId = randomBytes6(16).toString("hex");
6413
+ const cols = stdout7.columns || 80;
6414
+ const rows = stdout7.rows || 24;
6415
+ const term = new Headless({ cols, rows, allowProposedApi: true });
6416
+ let p;
6417
+ try {
6418
+ p = pty.spawn(real2.file, [...real2.prefixArgs, ...args], {
6419
+ name: deps.env.TERM || "xterm-256color",
6420
+ cols,
6421
+ rows,
6422
+ cwd: deps.cwd(),
6423
+ env: childEnv(deps.env, windowId)
6424
+ });
6425
+ } catch (e) {
6426
+ deps.log(`pty \u8D77\u4E0D\u6765\uFF0C\u653E\u884C\u76F4\u8DD1\uFF1A${e.message}`);
6427
+ return null;
6428
+ }
6429
+ return runAttached(tool, p, term, windowId, deps);
6430
+ }
6431
+ function runAttached(tool, p, term0, windowId, deps) {
6432
+ const { stdin, stdout: stdout7 } = deps;
6433
+ let term = term0;
6434
+ let finished = false;
6435
+ let held = null;
6436
+ let server = null;
6437
+ let timer = null;
6438
+ const sock = channelPath(windowId, deps.platform);
6439
+ const decoder = new StringDecoder("utf8");
6440
+ const toPty = (s) => {
6441
+ try {
6442
+ p.write(s);
6443
+ } catch {
6444
+ }
6445
+ };
6446
+ const injector = new Injector({
6447
+ now: () => Date.now(),
6448
+ view: () => term ? inspectLines(screenLines(term)) : null,
6449
+ lastEvent: () => readLastEvent(windowId),
6450
+ write: toPty,
6451
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
6452
+ holdKeys: (on) => {
6453
+ if (on) {
6454
+ held = held ?? [];
6455
+ return;
6456
+ }
6457
+ const h = held ?? [];
6458
+ held = null;
6459
+ for (const s of h) toPty(s);
6460
+ },
6461
+ log: (m) => deps.log(`[${windowId.slice(0, 8)}] ${m}`)
6462
+ });
6463
+ p.onData((d) => {
6464
+ try {
6465
+ stdout7.write(d);
6466
+ } catch {
6467
+ }
6468
+ if (term) {
6469
+ try {
6470
+ term.write(d);
6471
+ } catch {
6472
+ term = null;
6473
+ }
6474
+ }
6475
+ injector.noteOutput();
6476
+ });
6477
+ const onStdin = (b) => {
6478
+ injector.noteKey();
6479
+ const s = typeof b === "string" ? b : decoder.write(b);
6480
+ if (!s) return;
6481
+ if (held) held.push(s);
6482
+ else toPty(s);
6483
+ };
6484
+ const onResize = () => {
6485
+ const c = stdout7.columns;
6486
+ const r = stdout7.rows;
6487
+ if (!c || !r) return;
6488
+ try {
6489
+ p.resize(c, r);
6490
+ } catch {
6491
+ }
6492
+ try {
6493
+ term?.resize(c, r);
6494
+ } catch {
6495
+ term = null;
6496
+ }
6497
+ };
6498
+ const restoreTerminal = () => {
6499
+ try {
6500
+ stdin.off("data", onStdin);
6501
+ if (stdin.isTTY) stdin.setRawMode(false);
6502
+ stdin.pause();
6503
+ } catch {
6504
+ }
6505
+ try {
6506
+ stdout7.off("resize", onResize);
6507
+ } catch {
6508
+ }
6509
+ };
6510
+ try {
6511
+ if (stdin.isTTY) stdin.setRawMode(true);
6512
+ stdin.on("data", onStdin);
6513
+ stdin.resume();
6514
+ stdout7.on("resize", onResize);
6515
+ } catch (e) {
6516
+ deps.log(`\u63A5\u7BA1\u7EC8\u7AEF\u51FA\u9519\uFF1A${e.message}`);
6517
+ }
6518
+ try {
6519
+ writeWindow(windowId, { pid: process.pid, childPid: p.pid, sock, tool, cwd: deps.cwd(), at: Date.now() });
6520
+ } catch (e) {
6521
+ deps.log(`\u767B\u8BB0\u7A97\u53E3\u5931\u8D25\uFF1A${e.message}`);
6522
+ }
6523
+ const channelOk = deps.platform === "win32" || ensureChannelDir(sock);
6524
+ if (channelOk) {
6525
+ void startChannelServer(
6526
+ sock,
6527
+ (req2, reply, alive) => {
6528
+ try {
6529
+ injector.submit({ ...req2, expireAt: req2.expireAt || Date.now() + DEFAULT_TTL_MS, reply, alive });
6530
+ } catch (e) {
6531
+ deps.log(`\u6536\u6307\u4EE4\u51FA\u9519\uFF1A${e.message}`);
6532
+ }
6533
+ },
6534
+ deps.platform
6535
+ ).then((s) => {
6536
+ if (finished) {
6537
+ closeChannelServer(s, sock, deps.platform);
6538
+ return;
6539
+ }
6540
+ server = s;
6541
+ if (!s) deps.log("\u901A\u9053\u8D77\u4E0D\u6765\uFF0C\u8FD9\u4E2A\u7A97\u53E3\u6536\u4E0D\u5230\u8BBE\u5907\u6307\u4EE4");
6542
+ });
6543
+ }
6544
+ timer = setInterval(() => {
6545
+ injector.tick().catch(() => {
6546
+ });
6547
+ }, TICK_MS);
6548
+ return new Promise((resolve5) => {
6549
+ let degraded = false;
6550
+ const onFatal = (e) => {
6551
+ deps.log(`\u5305\u88C5\u5C42\u5F02\u5E38\uFF1A${e?.stack ?? String(e)}`);
6552
+ if (degraded) return;
6553
+ degraded = true;
6554
+ if (timer) clearInterval(timer);
6555
+ term = null;
6556
+ try {
6557
+ injector.drain();
6558
+ } catch {
6559
+ }
6560
+ closeChannelServer(server, sock, deps.platform);
6561
+ try {
6562
+ removeWindow(windowId);
6563
+ } catch {
6564
+ }
6565
+ try {
6566
+ deps.stderr.write(`\r
6567
+ ${WRAP_HINT}\uFF08\u8BBE\u5907\u56DE\u590D\u6682\u65F6\u5199\u4E0D\u8FDB\u8FD9\u4E2A\u7A97\u53E3\uFF09\r
6568
+ `);
6569
+ } catch {
6570
+ }
6571
+ };
6572
+ const sigs = ["SIGTERM", "SIGHUP", "SIGINT"];
6573
+ const onSignal = (sig) => {
6574
+ try {
6575
+ if (deps.platform === "win32") p.kill();
6576
+ else p.kill(sig);
6577
+ } catch {
6578
+ }
6579
+ setTimeout(() => finish(128 + (sig === "SIGHUP" ? 1 : sig === "SIGINT" ? 2 : 15)), 3e3).unref();
6580
+ };
6581
+ const sigHandlers = sigs.map((s) => [s, () => onSignal(s)]);
6582
+ function finish(code) {
6583
+ if (finished) return;
6584
+ finished = true;
6585
+ if (timer) clearInterval(timer);
6586
+ const pending = injector.pending;
6587
+ try {
6588
+ injector.drain();
6589
+ } catch {
6590
+ }
6591
+ closeChannelServer(server, sock, deps.platform);
6592
+ try {
6593
+ removeWindow(windowId);
6594
+ } catch {
6595
+ }
6596
+ restoreTerminal();
6597
+ process.removeListener("uncaughtException", onFatal);
6598
+ for (const [s, h] of sigHandlers) process.removeListener(s, h);
6599
+ if (pending > 0) setTimeout(() => resolve5(code), 150);
6600
+ else resolve5(code);
6601
+ }
6602
+ process.on("uncaughtException", onFatal);
6603
+ for (const [s, h] of sigHandlers) {
6604
+ try {
6605
+ process.on(s, h);
6606
+ } catch {
6607
+ }
6608
+ }
6609
+ p.onExit(({ exitCode, signal }) => {
6610
+ const flushed = decoder.end();
6611
+ if (flushed) toPty(flushed);
6612
+ finish(signal ? 128 + signal : exitCode);
6613
+ });
6614
+ });
6615
+ }
6616
+
3730
6617
  // src/cli.ts
3731
6618
  var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6BCF\u4E2A\u4F1A\u8BDD\u7A97\u53E3\u7684\u72B6\u6001\u540C\u6B65\u5230\u684C\u9762\u786C\u4EF6\u5C4F\u5E55
3732
6619
 
3733
6620
  \u7528\u6CD5:
6621
+ deskbuddy install --code <\u5B89\u88C5\u7801> \u4E00\u952E\u63A5\u5165\uFF1A\u7528 App\u300C\u6DFB\u52A0\u7535\u8111\u300D\u7ED9\u7684\u5B89\u88C5\u7801\u7ED1\u5B9A\u8D26\u53F7\uFF0C
6622
+ \u5E76\u63A5\u5165\u672C\u673A\u6240\u6709\u667A\u80FD\u4F53\u3001\u8BBE\u597D\u5F00\u673A\u81EA\u542F\uFF08\u4E0D\u9700\u8981\u4EFB\u4F55\u4EA4\u4E92\uFF09
3734
6623
  deskbuddy setup [--api-base <url>] \u626B\u7801\u6388\u6743\uFF08--api-base \u6307\u5B9A\u7F51\u5173\u5E76\u8BB0\u4F4F\uFF0C\u9ED8\u8BA4\u751F\u4EA7\uFF09
3735
6624
  deskbuddy enable <tool[,tool]> \u5B89\u88C5\u91C7\u96C6 hook\uFF08\u5982 claude_code,codex\uFF09
3736
6625
  deskbuddy disable [tool[,tool]] \u79FB\u9664\u91C7\u96C6 hook\uFF08\u4E0D\u5E26\u53C2\u6570=\u5168\u90E8\uFF09
@@ -3739,6 +6628,8 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
3739
6628
  deskbuddy daemon [--foreground] \u5E38\u9A7B\u6536\u53D6\u8BBE\u5907\u4E0B\u53D1\u7684\u6307\u4EE4\uFF08\u5F00\u673A\u81EA\u542F\u4F1A\u81EA\u52A8\u62C9\u8D77\uFF09
3740
6629
  deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001\u4E0A\u62A5\u4E86\u54EA\u4E9B\u5B57\u6BB5
3741
6630
  deskbuddy logout [--yes] \u79FB\u9664 hook \u5E76\u6E05\u9664\u672C\u673A\u6570\u636E
6631
+ deskbuddy wrap <tool> [\u53C2\u6570\u2026] \uFF08\u57AB\u7247\u5185\u90E8\u7528\uFF09\u7ECF DeskBuddy \u6253\u5F00\u667A\u80FD\u4F53\u7A97\u53E3\uFF0C
6632
+ \u8BBE\u5907\u4E0A\u7684\u56DE\u590D\u80FD\u5199\u8FDB\u8FD9\u4E2A\u7A97\u53E3\uFF1BDESKBUDDY_WRAP_OFF=1 \u53EF\u7ED5\u5F00
3742
6633
 
3743
6634
  \u72B6\u6001\u91C7\u96C6\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF08\u53EA\u662F\u5F80\u5404\u667A\u80FD\u4F53\u7684 hook \u914D\u7F6E\u91CC\u52A0\u4E86\u4E00\u6761\u8BB0\u5F55\uFF09\uFF1B
3744
6635
  \u8981\u5728\u786C\u4EF6\u4E0A\u7ED9\u7535\u8111\u4E0B\u6307\u4EE4\uFF0C\u624D\u9700\u8981 daemon \u5E38\u9A7B\u2014\u2014setup \u65F6\u4F1A\u5E2E\u4F60\u8BBE\u597D\u5F00\u673A\u81EA\u542F\u3002
@@ -3747,7 +6638,7 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
3747
6638
  async function main(argv) {
3748
6639
  const cmd = argv[2];
3749
6640
  const arg = argv[3];
3750
- if (cmd && !["-h", "--help", "help", "-v", "--version"].includes(cmd)) {
6641
+ if (cmd && !["-h", "--help", "help", "-v", "--version", "wrap"].includes(cmd)) {
3751
6642
  try {
3752
6643
  syncHookFile();
3753
6644
  } catch {
@@ -3764,6 +6655,8 @@ async function main(argv) {
3764
6655
  case "--version":
3765
6656
  process.stdout.write(packageVersion() + "\n");
3766
6657
  return 0;
6658
+ case "install":
6659
+ return runInstall({ code: parseCode(argv), apiBase: parseApiBase(argv) });
3767
6660
  case "setup":
3768
6661
  return runSetup({ apiBase: parseApiBase(argv) });
3769
6662
  case "enable":
@@ -3778,6 +6671,8 @@ async function main(argv) {
3778
6671
  return runStatus();
3779
6672
  case "logout":
3780
6673
  return runLogout({ yes: argv.includes("--yes") || argv.includes("-y") });
6674
+ case "wrap":
6675
+ return runWrap(argv.slice(3));
3781
6676
  default:
3782
6677
  process.stderr.write(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}
3783
6678