deskbuddy 0.2.0-beta.0 → 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
  }
@@ -167,6 +328,8 @@ function write(path, content) {
167
328
  mkdirSync2(dirname(path), { recursive: true });
168
329
  writeFileSync2(path, content, "utf8");
169
330
  }
331
+ var BOOTSTRAP_RETRIES = 10;
332
+ var BOOTSTRAP_RETRY_MS = 500;
170
333
  async function installAutostart(t, platform = process.platform) {
171
334
  try {
172
335
  if (platform === "darwin") {
@@ -174,29 +337,33 @@ async function installAutostart(t, platform = process.platform) {
174
337
  write(p, launchAgentPlist(t));
175
338
  await run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCH_LABEL}`]).catch(() => {
176
339
  });
177
- await run("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, p]);
340
+ for (let i = 0; ; i++) {
341
+ try {
342
+ await run("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, p]);
343
+ break;
344
+ } catch (e) {
345
+ if (i >= BOOTSTRAP_RETRIES) throw e;
346
+ await new Promise((r) => setTimeout(r, BOOTSTRAP_RETRY_MS));
347
+ }
348
+ }
178
349
  return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08launchd\uFF09" };
179
350
  }
180
351
  if (platform === "linux") {
181
352
  write(systemdUnitPath(), systemdUnit(t));
182
353
  await run("systemctl", ["--user", "daemon-reload"]);
183
354
  await run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
355
+ await run("systemctl", ["--user", "restart", SYSTEMD_UNIT]);
184
356
  return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08systemd --user\uFF09" };
185
357
  }
186
358
  if (platform === "win32") {
187
- await run("schtasks", [
188
- "/create",
189
- "/f",
190
- "/sc",
191
- "onlogon",
192
- "/rl",
193
- "LIMITED",
194
- "/tn",
195
- SCHTASK_NAME,
196
- "/tr",
197
- `"${t.execPath}" "${t.cliPath}" daemon`
198
- ]);
199
- 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" };
200
367
  }
201
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` };
202
369
  } catch (e) {
@@ -222,8 +389,11 @@ async function uninstallAutostart(platform = process.platform) {
222
389
  return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
223
390
  }
224
391
  if (platform === "win32") {
225
- await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME]).catch(() => {
392
+ await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME], { windowsHide: true }).catch(() => {
226
393
  });
394
+ const p = winStartupVbsPath();
395
+ if (existsSync2(p)) rmSync(p, { force: true });
396
+ await stopPidFileDaemon();
227
397
  return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
228
398
  }
229
399
  return { ok: true, message: "" };
@@ -242,14 +412,16 @@ import { basename as basename6 } from "path";
242
412
  import WebSocket2 from "ws";
243
413
 
244
414
  // src/adapters/ocPlugin.ts
245
- 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";
246
416
  import { dirname as dirname2, join as join3 } from "path";
417
+ import { pathToFileURL } from "url";
247
418
  var OC_MARKER = "// deskbuddy:opencode-plugin";
248
- function ocPluginSource(pluginModule, nodePath) {
419
+ function ocPluginSource(pluginModule, nodePath, platform = process.platform) {
420
+ const spec = platform === "win32" ? pathToFileURL(pluginModule).href : pluginModule;
249
421
  return [
250
422
  OC_MARKER,
251
423
  "// \u7531 deskbuddy enable opencode \u751F\u6210\uFF0Cdeskbuddy disable opencode \u4F1A\u5220\u6389\u5B83\u3002",
252
- `import { make } from ${JSON.stringify(pluginModule)}`,
424
+ `import { make } from ${JSON.stringify(spec)}`,
253
425
  `export const DeskBuddy = make({ node: ${JSON.stringify(nodePath)} })`,
254
426
  ""
255
427
  ].join("\n");
@@ -258,7 +430,7 @@ function ocPluginModule(hookPath) {
258
430
  return join3(dirname2(hookPath), "opencode.mjs");
259
431
  }
260
432
  function installOcPlugin(pluginPath, hookPath, nodePath) {
261
- if (existsSync3(pluginPath) && !readFileSync2(pluginPath, "utf8").includes(OC_MARKER)) {
433
+ if (existsSync3(pluginPath) && !readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) {
262
434
  throw new Error(`${pluginPath} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F DeskBuddy \u751F\u6210\u7684\uFF0C\u8BF7\u5148\u632A\u8D70\u518D\u91CD\u8BD5`);
263
435
  }
264
436
  mkdirSync3(dirname2(pluginPath), { recursive: true });
@@ -268,13 +440,13 @@ function installOcPlugin(pluginPath, hookPath, nodePath) {
268
440
  }
269
441
  function uninstallOcPlugin(pluginPath) {
270
442
  try {
271
- if (readFileSync2(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
443
+ if (readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
272
444
  } catch {
273
445
  }
274
446
  }
275
447
  function isOcPluginInstalled(pluginPath) {
276
448
  try {
277
- return readFileSync2(pluginPath, "utf8").includes(OC_MARKER);
449
+ return readFileSync3(pluginPath, "utf8").includes(OC_MARKER);
278
450
  } catch {
279
451
  return false;
280
452
  }
@@ -282,14 +454,16 @@ function isOcPluginInstalled(pluginPath) {
282
454
  var OC_NOTICE = " \u5DF2\u5F00\u7740\u7684 opencode \u7A97\u53E3\u91CD\u5F00\u540E\u751F\u6548";
283
455
 
284
456
  // src/core/state.ts
285
- 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";
286
458
  import { join as join4 } from "path";
287
459
 
288
460
  // src/core/process.ts
289
- import { execFileSync } from "child_process";
461
+ import { execFileSync as execFileSync2 } from "child_process";
462
+ var WIN = process.platform === "win32";
290
463
  function processName(pid) {
464
+ if (WIN) return winProcs()?.find((p) => p.pid === pid)?.name ?? null;
291
465
  try {
292
- const out = execFileSync("ps", ["-p", String(pid), "-o", "comm="], {
466
+ const out = execFileSync2("ps", ["-p", String(pid), "-o", "comm="], {
293
467
  encoding: "utf8",
294
468
  stdio: ["ignore", "pipe", "ignore"]
295
469
  });
@@ -307,6 +481,12 @@ function isAlive(pid, expectName) {
307
481
  return false;
308
482
  }
309
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
+ }
310
490
  const name = processName(pid);
311
491
  return name != null && name.toLowerCase().includes(expectName.toLowerCase());
312
492
  }
@@ -321,7 +501,7 @@ function statePath(sessionId) {
321
501
  }
322
502
  function readState(sessionId) {
323
503
  try {
324
- return JSON.parse(readFileSync3(statePath(sessionId), "utf8"));
504
+ return JSON.parse(readFileSync4(statePath(sessionId), "utf8"));
325
505
  } catch {
326
506
  return null;
327
507
  }
@@ -339,7 +519,7 @@ function readAliveStates(tool) {
339
519
  if (!file.endsWith(".json")) continue;
340
520
  let s;
341
521
  try {
342
- s = JSON.parse(readFileSync3(join4(stateDir(), file), "utf8"));
522
+ s = JSON.parse(readFileSync4(join4(stateDir(), file), "utf8"));
343
523
  } catch {
344
524
  rmSync2(join4(stateDir(), file), { force: true });
345
525
  continue;
@@ -372,7 +552,7 @@ var ALIAS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
372
552
  function resolveAlias(sessionId, now = Date.now()) {
373
553
  let map = {};
374
554
  try {
375
- map = JSON.parse(readFileSync3(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
555
+ map = JSON.parse(readFileSync4(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
376
556
  } catch {
377
557
  return sessionId;
378
558
  }
@@ -386,13 +566,13 @@ function resolveAlias(sessionId, now = Date.now()) {
386
566
  }
387
567
 
388
568
  // src/core/api.ts
389
- import { execFileSync as execFileSync2 } from "child_process";
569
+ import { execFileSync as execFileSync3 } from "child_process";
390
570
  import { createHash, randomBytes, randomUUID as randomUUID2 } from "crypto";
391
- import { readFileSync as readFileSync5 } from "fs";
571
+ import { readFileSync as readFileSync6 } from "fs";
392
572
  import { release } from "os";
393
573
 
394
574
  // src/core/hookFile.ts
395
- 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";
396
576
  import { createRequire } from "module";
397
577
  import { dirname as dirname3, join as join5 } from "path";
398
578
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -427,7 +607,7 @@ function hookVersionPath(target = installedHookPath()) {
427
607
  }
428
608
  function installedHookVersion(target = installedHookPath()) {
429
609
  try {
430
- return readFileSync4(hookVersionPath(target), "utf8").trim() || null;
610
+ return readFileSync5(hookVersionPath(target), "utf8").trim() || null;
431
611
  } catch {
432
612
  return null;
433
613
  }
@@ -471,6 +651,8 @@ var SERVICES = {
471
651
  confirmAuth: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyconfirmauthapi"
472
652
  };
473
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";
474
656
  var APPROVAL_SERVICES = {
475
657
  create: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcreateapi",
476
658
  cancel: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcancelapi",
@@ -537,12 +719,12 @@ function humanOs(platform = process.platform) {
537
719
  return known[platform] ?? platform;
538
720
  }
539
721
  var defaultOsVersionDeps = {
540
- runSwVers: () => execFileSync2("sw_vers", ["-productVersion"], {
722
+ runSwVers: () => execFileSync3("sw_vers", ["-productVersion"], {
541
723
  encoding: "utf8",
542
724
  stdio: ["ignore", "pipe", "ignore"],
543
725
  timeout: 2e3
544
726
  }),
545
- readOsRelease: () => readFileSync5("/etc/os-release", "utf8"),
727
+ readOsRelease: () => readFileSync6("/etc/os-release", "utf8"),
546
728
  kernelRelease: () => release()
547
729
  };
548
730
  function productOsVersion(platform = process.platform, deps = defaultOsVersionDeps) {
@@ -608,7 +790,10 @@ var api = {
608
790
  approvalCreate: (payload) => call(APPROVAL_SERVICES.create, payload),
609
791
  approvalCancel: (approvalId, reason) => call(APPROVAL_SERVICES.cancel, { approvalId, reason }),
610
792
  approvalSync: (approvalIds) => call(APPROVAL_SERVICES.sync, { approvalIds }),
611
- 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)
612
797
  };
613
798
 
614
799
  // src/core/report.ts
@@ -623,7 +808,8 @@ async function postReport(payload) {
623
808
  import { basename as basename2 } from "path";
624
809
  var KEEPALIVE_MS = 2 * 60 * 1e3;
625
810
  var MAX_SESSIONS = 20;
626
- function buildSnapshot(states, tool, cfg, nowMs) {
811
+ var CAPS = ["cmd_result", "window_inject"];
812
+ function buildSnapshot(states, tool, cfg, nowMs, remote) {
627
813
  const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => ({
628
814
  sessionId: s.sessionId,
629
815
  title: s.title,
@@ -632,14 +818,15 @@ function buildSnapshot(states, tool, cfg, nowMs) {
632
818
  state: s.state,
633
819
  startedAt: s.startedAt,
634
820
  updatedAt: s.updatedAt,
635
- interactive: s.interactive === true
821
+ interactive: s.interactive === true,
822
+ remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
636
823
  }));
637
- 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] };
638
825
  }
639
826
 
640
827
  // src/core/dirs.ts
641
828
  import { createHmac, randomBytes as randomBytes2 } from "crypto";
642
- 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";
643
830
  import { basename as basename3, join as join6 } from "path";
644
831
  var DIRS_REPORT_MAX = 10;
645
832
  function dirsPath() {
@@ -648,7 +835,7 @@ function dirsPath() {
648
835
  function dirSecret() {
649
836
  const file = join6(deskbuddyHome(), "dir-secret");
650
837
  try {
651
- const s2 = readFileSync6(file, "utf8").trim();
838
+ const s2 = readFileSync7(file, "utf8").trim();
652
839
  if (s2) return s2;
653
840
  } catch {
654
841
  }
@@ -666,7 +853,7 @@ function dirRef(path) {
666
853
  }
667
854
  function readMap() {
668
855
  try {
669
- const m = JSON.parse(readFileSync6(dirsPath(), "utf8"));
856
+ const m = JSON.parse(readFileSync7(dirsPath(), "utf8"));
670
857
  const out = {};
671
858
  for (const [k, v] of Object.entries(m ?? {})) if (typeof v === "number") out[k] = v;
672
859
  return out;
@@ -762,7 +949,7 @@ function isExpired(cmd, now = Date.now()) {
762
949
  }
763
950
 
764
951
  // src/daemon/executor.ts
765
- import { execFile as execFile2 } from "child_process";
952
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "child_process";
766
953
  import { homedir as homedir4 } from "os";
767
954
  import { join as join8 } from "path";
768
955
 
@@ -776,6 +963,19 @@ function waitModeFor(tool) {
776
963
  function shQuote(s) {
777
964
  return `'${s.replace(/'/g, `'\\''`)}'`;
778
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
+ }
779
979
  function defaultGateScript() {
780
980
  const candidates = [
781
981
  fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
@@ -783,17 +983,17 @@ function defaultGateScript() {
783
983
  ];
784
984
  return candidates.find((p) => existsSync7(p)) ?? candidates[0];
785
985
  }
786
- function gateCommand(t) {
986
+ function gateCommand(t, platform = process.platform) {
987
+ if (platform === "win32") return winGateCommand(t);
787
988
  return `${shQuote(t.nodePath)} ${shQuote(t.gateScript)} --run ${t.runId} || exit 2`;
788
989
  }
789
- function ccSettingsJson(t) {
790
- return JSON.stringify({
791
- hooks: {
792
- PreToolUse: [
793
- { matcher: "*", hooks: [{ type: "command", command: gateCommand(t), timeout: GATE_HOOK_TIMEOUT_S }] }
794
- ]
795
- }
796
- });
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] }] } });
797
997
  }
798
998
  function codexGateArgs(t) {
799
999
  const hook = `hooks.PreToolUse=[{matcher="*",hooks=[{type="command",command=${JSON.stringify(gateCommand(t))},timeout=${GATE_HOOK_TIMEOUT_S}}]}]`;
@@ -809,7 +1009,7 @@ function codexGateArgs(t) {
809
1009
  }
810
1010
 
811
1011
  // src/daemon/locate.ts
812
- import { accessSync, constants, existsSync as existsSync8, statSync } from "fs";
1012
+ import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync8, statSync } from "fs";
813
1013
  import { homedir as homedir3 } from "os";
814
1014
  import { delimiter, dirname as dirname4, join as join7 } from "path";
815
1015
  var defaultLocateDeps = () => ({
@@ -825,9 +1025,33 @@ var defaultLocateDeps = () => ({
825
1025
  return false;
826
1026
  }
827
1027
  },
828
- 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")]
829
1041
  });
830
- 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
+ }
831
1055
  return [
832
1056
  join7(home, ".local", "bin"),
833
1057
  join7(home, ".claude", "local"),
@@ -840,19 +1064,110 @@ function extraBinDirs(home, nodePath = "") {
840
1064
  ];
841
1065
  }
842
1066
  function whichIn(name, d) {
843
- const dirs = [...d.path.split(delimiter).filter(Boolean), ...extraBinDirs(d.home, d.nodePath)];
844
- for (const dir of dirs) {
845
- const p = join7(dir, name);
846
- 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
+ }
847
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] };
848
1113
  return null;
849
1114
  }
850
1115
  function appDirs(home) {
851
1116
  return ["/Applications", join7(home, "Applications")];
852
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
+ }
853
1167
  var WORKBUDDY_CLI_IN_APP = "WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy";
854
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";
855
1169
  function locate(tool, d = defaultLocateDeps()) {
1170
+ if (isWin(d)) return locateWin(tool, d);
856
1171
  const plain = (name) => {
857
1172
  const f = whichIn(name, d);
858
1173
  return f ? { file: f, prefixArgs: [] } : null;
@@ -887,7 +1202,8 @@ function locate(tool, d = defaultLocateDeps()) {
887
1202
  }
888
1203
  function childPath(d = defaultLocateDeps()) {
889
1204
  const seen = /* @__PURE__ */ new Set();
890
- 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);
891
1207
  }
892
1208
 
893
1209
  // src/daemon/executor.ts
@@ -992,21 +1308,21 @@ function materialize(plan, tool, d = defaultLocateDeps()) {
992
1308
  env: { ...plan.env, PATH: childPath(d) }
993
1309
  };
994
1310
  }
995
- function isDeferred(stdout6) {
1311
+ function isDeferred(stdout7) {
996
1312
  try {
997
- return JSON.parse(stdout6).stop_reason === "tool_deferred";
1313
+ return JSON.parse(stdout7).stop_reason === "tool_deferred";
998
1314
  } catch {
999
1315
  return false;
1000
1316
  }
1001
1317
  }
1002
- function parseClaudeSessionId(stdout6) {
1318
+ function parseClaudeSessionId(stdout7) {
1003
1319
  const pick = (o) => {
1004
1320
  const d = o ?? {};
1005
1321
  const v = d.session_id ?? d.sessionId;
1006
1322
  return typeof v === "string" && v ? v : null;
1007
1323
  };
1008
1324
  try {
1009
- const d = JSON.parse(stdout6);
1325
+ const d = JSON.parse(stdout7);
1010
1326
  if (Array.isArray(d)) {
1011
1327
  for (const m of d) {
1012
1328
  const id = pick(m);
@@ -1019,8 +1335,8 @@ function parseClaudeSessionId(stdout6) {
1019
1335
  return null;
1020
1336
  }
1021
1337
  }
1022
- function parseCodexSessionId(stdout6) {
1023
- for (const line of stdout6.split("\n")) {
1338
+ function parseCodexSessionId(stdout7) {
1339
+ for (const line of stdout7.split("\n")) {
1024
1340
  const t = line.trim();
1025
1341
  if (!t) continue;
1026
1342
  try {
@@ -1034,8 +1350,8 @@ function parseCodexSessionId(stdout6) {
1034
1350
  }
1035
1351
  return null;
1036
1352
  }
1037
- function parseOpencodeSessionId(stdout6) {
1038
- for (const line of stdout6.split("\n")) {
1353
+ function parseOpencodeSessionId(stdout7) {
1354
+ for (const line of stdout7.split("\n")) {
1039
1355
  const t = line.trim();
1040
1356
  if (!t) continue;
1041
1357
  try {
@@ -1048,10 +1364,10 @@ function parseOpencodeSessionId(stdout6) {
1048
1364
  }
1049
1365
  return null;
1050
1366
  }
1051
- function parseSessionId(tool, stdout6) {
1052
- if (tool === "codex") return parseCodexSessionId(stdout6);
1053
- if (tool === "opencode") return parseOpencodeSessionId(stdout6);
1054
- return parseClaudeSessionId(stdout6);
1367
+ function parseSessionId(tool, stdout7) {
1368
+ if (tool === "codex") return parseCodexSessionId(stdout7);
1369
+ if (tool === "opencode") return parseOpencodeSessionId(stdout7);
1370
+ return parseClaudeSessionId(stdout7);
1055
1371
  }
1056
1372
  function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1057
1373
  let remaining = limitMs;
@@ -1064,11 +1380,11 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1064
1380
  startedAt = Date.now();
1065
1381
  timer = setTimeout(() => {
1066
1382
  timedOut = true;
1067
- child?.kill("SIGTERM");
1383
+ killTree(child);
1068
1384
  }, Math.max(0, remaining));
1069
1385
  };
1070
1386
  let timedOut = false;
1071
- const done = new Promise((resolve3) => {
1387
+ const done = new Promise((resolve5) => {
1072
1388
  child = execFile2(
1073
1389
  plan.file,
1074
1390
  plan.args,
@@ -1076,18 +1392,20 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1076
1392
  cwd: plan.cwd,
1077
1393
  maxBuffer: EXEC_MAX_BUFFER,
1078
1394
  // agent 自己要读 PATH 里的东西,环境原样继承
1079
- env: { ...process.env, ...plan.env }
1395
+ env: mergeEnv(process.env, plan.env),
1396
+ // Windows:别给子进程弹黑框
1397
+ windowsHide: true
1080
1398
  },
1081
- (err, stdout6) => {
1399
+ (err, stdout7) => {
1082
1400
  finished = true;
1083
1401
  if (timer) clearTimeout(timer);
1084
- const out = String(stdout6 ?? "");
1402
+ const out = String(stdout7 ?? "");
1085
1403
  if (err || timedOut) {
1086
1404
  const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : err.message;
1087
- resolve3({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
1405
+ resolve5({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
1088
1406
  return;
1089
1407
  }
1090
- resolve3({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
1408
+ resolve5({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
1091
1409
  }
1092
1410
  );
1093
1411
  child?.stdin?.end();
@@ -1109,10 +1427,31 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1109
1427
  }
1110
1428
  };
1111
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
+ }
1112
1451
 
1113
1452
  // src/daemon/gate/approvals.ts
1114
1453
  import { createHash as createHash2 } from "crypto";
1115
- 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";
1116
1455
  import { join as join9 } from "path";
1117
1456
  var RETAIN_MS = 7 * 24 * 60 * 60 * 1e3;
1118
1457
  function fingerprint(toolName, toolInput) {
@@ -1134,7 +1473,7 @@ var ApprovalStore = class _ApprovalStore {
1134
1473
  }
1135
1474
  static load() {
1136
1475
  try {
1137
- const raw = JSON.parse(readFileSync7(storePath(), "utf8"));
1476
+ const raw = JSON.parse(readFileSync9(storePath(), "utf8"));
1138
1477
  return new _ApprovalStore(Array.isArray(raw) ? raw : []);
1139
1478
  } catch {
1140
1479
  return new _ApprovalStore();
@@ -1330,12 +1669,12 @@ function isReadOnlyCommand(command) {
1330
1669
  }
1331
1670
  function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
1332
1671
  const roots = (Array.isArray(cwd) ? cwd : [cwd]).filter(Boolean);
1333
- const real = opts.real;
1672
+ const real2 = opts.real;
1334
1673
  const inside = (p) => {
1335
1674
  if (!roots.some((r) => isInside(r, p))) return false;
1336
- if (!real) return true;
1337
- const rp = real(resolve(roots[0] ?? "/", p));
1338
- 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));
1339
1678
  };
1340
1679
  const rel = (p) => {
1341
1680
  const r = roots.find((x) => isInside(x, p)) ?? roots[0] ?? "";
@@ -1352,7 +1691,7 @@ function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
1352
1691
  if (paths) {
1353
1692
  const allInside = paths.length > 0 && paths.every(inside);
1354
1693
  const touchesProtected = paths.some(
1355
- (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)
1356
1695
  );
1357
1696
  if (policy === "commands" && allInside && !touchesProtected) return { action: "auto" };
1358
1697
  const shown = paths.map((p) => inside(p) ? rel(p) : p);
@@ -1375,11 +1714,14 @@ function describe(toolName, toolInput, cwd) {
1375
1714
  }
1376
1715
 
1377
1716
  // src/daemon/gate/procs.ts
1378
- import { execFileSync as execFileSync3 } from "child_process";
1717
+ import { execFileSync as execFileSync5 } from "child_process";
1379
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
+ }
1380
1722
  let out = "";
1381
1723
  try {
1382
- out = execFileSync3("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
1724
+ out = execFileSync5("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
1383
1725
  encoding: "utf8",
1384
1726
  stdio: ["ignore", "pipe", "ignore"],
1385
1727
  maxBuffer: 16 * 1024 * 1024
@@ -1387,19 +1729,28 @@ function listDescendants(rootPid) {
1387
1729
  } catch {
1388
1730
  return [];
1389
1731
  }
1390
- const kids = /* @__PURE__ */ new Map();
1732
+ const rows = [];
1391
1733
  for (const line of out.split("\n")) {
1392
1734
  const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
1393
- if (!m) continue;
1394
- const pp = Number(m[2]);
1395
- const list = kids.get(pp) ?? [];
1396
- list.push({ pid: Number(m[1]), args: m[3] });
1397
- 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);
1398
1746
  }
1747
+ const seen = /* @__PURE__ */ new Set([rootPid]);
1399
1748
  const res = [];
1400
1749
  const stack = [rootPid];
1401
1750
  while (stack.length > 0) {
1402
1751
  for (const c of kids.get(stack.pop()) ?? []) {
1752
+ if (seen.has(c.pid)) continue;
1753
+ seen.add(c.pid);
1403
1754
  res.push(c);
1404
1755
  stack.push(c.pid);
1405
1756
  }
@@ -1428,9 +1779,9 @@ function pickRememberSuggestion(suggestions) {
1428
1779
  return null;
1429
1780
  }
1430
1781
  var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
1431
- function windowNotice(decision, summary, deviceName = "") {
1782
+ function windowNotice(decision, summary2, deviceName = "") {
1432
1783
  const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
1433
- const what = summary.length > 60 ? summary.slice(0, 59) + "\u2026" : summary;
1784
+ const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
1434
1785
  return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
1435
1786
  }
1436
1787
  var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
@@ -1463,17 +1814,17 @@ var ApprovalCoordinator = class {
1463
1814
  sessionOf(runId) {
1464
1815
  return this.#runs.get(runId)?.sessionId ?? "";
1465
1816
  }
1466
- async onGate(req, signal) {
1467
- if (req.window) return this.#onWindow(req.hook, signal, req.tool || "claude_code");
1468
- 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 ?? "");
1469
1820
  if (!run2) return { decision: "deny", reason: REASON_UNKNOWN_RUN };
1470
1821
  if (run2.superseded) return { decision: "deny", reason: REASON_SUPERSEDED };
1471
- const toolName = typeof req.hook.tool_name === "string" ? req.hook.tool_name : "";
1472
- 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 || "";
1473
1824
  if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
1474
- 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 });
1475
1826
  if (v.action === "auto") return { decision: "allow" };
1476
- const fp = fingerprint(toolName, req.hook.tool_input);
1827
+ const fp = fingerprint(toolName, req2.hook.tool_input);
1477
1828
  const store = this.#d.store;
1478
1829
  if (run2.mode === "defer" && sessionId) {
1479
1830
  const prev = store.latestFor(sessionId, fp);
@@ -1502,7 +1853,7 @@ var ApprovalCoordinator = class {
1502
1853
  store.save();
1503
1854
  let early = null;
1504
1855
  let settle;
1505
- const decided = new Promise((resolve3) => settle = resolve3);
1856
+ const decided = new Promise((resolve5) => settle = resolve5);
1506
1857
  this.#waiters.set(rec.approvalId, (r) => {
1507
1858
  early = r;
1508
1859
  settle(r);
@@ -1526,8 +1877,8 @@ var ApprovalCoordinator = class {
1526
1877
  }
1527
1878
  run2.onWaitStart?.();
1528
1879
  try {
1529
- return await new Promise((resolve3) => {
1530
- void decided.then(resolve3);
1880
+ return await new Promise((resolve5) => {
1881
+ void decided.then(resolve5);
1531
1882
  const onAbort = () => {
1532
1883
  if (!this.#waiters.delete(rec.approvalId)) return;
1533
1884
  if (rec.status === "pending") {
@@ -1536,7 +1887,7 @@ var ApprovalCoordinator = class {
1536
1887
  store.save();
1537
1888
  void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
1538
1889
  }
1539
- resolve3({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
1890
+ resolve5({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
1540
1891
  };
1541
1892
  if (signal.aborted) onAbort();
1542
1893
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -1559,7 +1910,7 @@ var ApprovalCoordinator = class {
1559
1910
  if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
1560
1911
  for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
1561
1912
  const store = this.#d.store;
1562
- const { kind, summary } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
1913
+ const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
1563
1914
  const remember = tool === "claude_code" ? pickRememberSuggestion(hook.permission_suggestions) : null;
1564
1915
  const rec = {
1565
1916
  approvalId: randomUUID3(),
@@ -1569,7 +1920,7 @@ var ApprovalCoordinator = class {
1569
1920
  cwd,
1570
1921
  fingerprint: fingerprint(toolName, hook.tool_input),
1571
1922
  kind,
1572
- summary,
1923
+ summary: summary2,
1573
1924
  mode: "window",
1574
1925
  status: "pending",
1575
1926
  createdAt: this.#now(),
@@ -1588,7 +1939,7 @@ var ApprovalCoordinator = class {
1588
1939
  ...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
1589
1940
  });
1590
1941
  let settle;
1591
- const decided = new Promise((resolve3) => settle = resolve3);
1942
+ const decided = new Promise((resolve5) => settle = resolve5);
1592
1943
  this.#waiters.set(rec.approvalId, settle);
1593
1944
  try {
1594
1945
  await this.#d.backend.create(rec);
@@ -1604,11 +1955,11 @@ var ApprovalCoordinator = class {
1604
1955
  }
1605
1956
  this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
1606
1957
  if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
1607
- return new Promise((resolve3) => {
1608
- void decided.then(resolve3);
1958
+ return new Promise((resolve5) => {
1959
+ void decided.then(resolve5);
1609
1960
  const onAbort = () => {
1610
1961
  if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
1611
- resolve3(none);
1962
+ resolve5(none);
1612
1963
  };
1613
1964
  if (signal.aborted) onAbort();
1614
1965
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -1882,8 +2233,8 @@ function realpathNearest(p) {
1882
2233
  }
1883
2234
  function cwdForms(cwd) {
1884
2235
  try {
1885
- const real = realpathSync(cwd);
1886
- return real === cwd ? [cwd] : [cwd, real];
2236
+ const real2 = realpathSync(cwd);
2237
+ return real2 === cwd ? [cwd] : [cwd, real2];
1887
2238
  } catch {
1888
2239
  return [cwd];
1889
2240
  }
@@ -1915,10 +2266,10 @@ var GateServer = class {
1915
2266
  }
1916
2267
  /** 起监听并写 gate.json。返回实际端口 */
1917
2268
  async start() {
1918
- const server = createServer((req, res) => void this.#serve(req, res));
1919
- await new Promise((resolve3, reject) => {
2269
+ const server = createServer((req2, res) => void this.#serve(req2, res));
2270
+ await new Promise((resolve5, reject) => {
1920
2271
  server.once("error", reject);
1921
- server.listen(0, "127.0.0.1", () => resolve3());
2272
+ server.listen(0, "127.0.0.1", () => resolve5());
1922
2273
  });
1923
2274
  this.#server = server;
1924
2275
  const addr = server.address();
@@ -1939,21 +2290,21 @@ var GateServer = class {
1939
2290
  }
1940
2291
  const s = this.#server;
1941
2292
  this.#server = null;
1942
- if (s) await new Promise((resolve3) => s.close(() => resolve3()));
2293
+ if (s) await new Promise((resolve5) => s.close(() => resolve5()));
1943
2294
  }
1944
- async #serve(req, res) {
2295
+ async #serve(req2, res) {
1945
2296
  const reply = (status, body2) => {
1946
2297
  if (res.writableEnded || res.destroyed) return;
1947
2298
  res.writeHead(status, { "content-type": "application/json" });
1948
2299
  res.end(body2 ? JSON.stringify(body2) : "");
1949
2300
  };
1950
- if (req.method !== "POST" || req.url !== "/gate") return reply(404);
1951
- 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"];
1952
2303
  if (typeof token !== "string" || !tokenEquals(token, this.#token)) return reply(403);
1953
2304
  let body = "";
1954
2305
  let size = 0;
1955
2306
  try {
1956
- for await (const chunk of req) {
2307
+ for await (const chunk of req2) {
1957
2308
  size += chunk.length;
1958
2309
  if (size > MAX_BODY_BYTES) return reply(413);
1959
2310
  body += String(chunk);
@@ -1975,12 +2326,12 @@ var GateServer = class {
1975
2326
  if (!res.writableEnded) ac.abort();
1976
2327
  });
1977
2328
  try {
1978
- const req2 = window ? {
2329
+ const req3 = window ? {
1979
2330
  window: true,
1980
2331
  tool: typeof parsed.tool === "string" && /^[a-z_]{1,32}$/.test(parsed.tool) ? parsed.tool : "claude_code",
1981
2332
  hook: parsed.hook
1982
2333
  } : { runId: parsed.runId, hook: parsed.hook };
1983
- const out = await this.#handler(req2, ac.signal);
2334
+ const out = await this.#handler(req3, ac.signal);
1984
2335
  reply(200, out);
1985
2336
  } catch {
1986
2337
  reply(500);
@@ -2097,6 +2448,10 @@ var SessionQueue = class {
2097
2448
  });
2098
2449
  return next;
2099
2450
  }
2451
+ /** 这个会话有没有在跑 / 在排队的任务 */
2452
+ busy(sessionId) {
2453
+ return this.#tails.has(sessionId || "");
2454
+ }
2100
2455
  /** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
2101
2456
  get activeSessions() {
2102
2457
  return this.#tails.size;
@@ -2110,7 +2465,7 @@ var SessionQueue = class {
2110
2465
  };
2111
2466
 
2112
2467
  // src/daemon/remote.ts
2113
- 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";
2114
2469
  import { join as join13 } from "path";
2115
2470
  function handleDir() {
2116
2471
  return join13(deskbuddyHome(), "remote");
@@ -2121,7 +2476,7 @@ function handlePath(tool) {
2121
2476
  }
2122
2477
  function readHandle(tool) {
2123
2478
  try {
2124
- const h = JSON.parse(readFileSync8(handlePath(tool), "utf8"));
2479
+ const h = JSON.parse(readFileSync10(handlePath(tool), "utf8"));
2125
2480
  if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
2126
2481
  if (typeof h?.cwd !== "string" || !h.cwd) return null;
2127
2482
  return h;
@@ -2187,7 +2542,7 @@ function failureMessage(reason) {
2187
2542
  }
2188
2543
 
2189
2544
  // src/daemon/seen.ts
2190
- 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";
2191
2546
  import { join as join14 } from "path";
2192
2547
  var SEEN_CAPACITY = 200;
2193
2548
  function seenPath() {
@@ -2222,7 +2577,7 @@ var SeenIds = class _SeenIds {
2222
2577
  }
2223
2578
  static load() {
2224
2579
  try {
2225
- const raw = JSON.parse(readFileSync9(seenPath(), "utf8"));
2580
+ const raw = JSON.parse(readFileSync11(seenPath(), "utf8"));
2226
2581
  return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
2227
2582
  } catch {
2228
2583
  return new _SeenIds();
@@ -2240,11 +2595,25 @@ var SeenIds = class _SeenIds {
2240
2595
  };
2241
2596
 
2242
2597
  // src/daemon/codexWatch.ts
2243
- import { execFileSync as execFileSync4 } from "child_process";
2598
+ import { execFileSync as execFileSync6 } from "child_process";
2244
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;
2245
2603
  var RESUME_RETRY_MS = 1e3;
2246
2604
  var RESUME_MAX_TRIES = 120;
2247
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
+ }
2248
2617
  function isLoopbackWs(url) {
2249
2618
  try {
2250
2619
  const u = new URL(url);
@@ -2263,17 +2632,29 @@ function parseAppServerUrls(psOutput) {
2263
2632
  return urls;
2264
2633
  }
2265
2634
  function discoverAppServer(configured) {
2266
- 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
+ }
2267
2642
  try {
2268
- const out = execFileSync4("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
2269
- return parseAppServerUrls(out)[0] ?? null;
2643
+ const out = execFileSync6("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
2644
+ return parseAppServerUrls(out);
2270
2645
  } catch {
2271
- return null;
2646
+ return [];
2272
2647
  }
2273
2648
  }
2274
2649
  function unwrapShell(command) {
2275
2650
  const m = command.match(/^\/bin\/(?:ba|z)?sh -lc '([\s\S]*)'$/);
2276
- 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;
2277
2658
  }
2278
2659
  var CodexWatch = class {
2279
2660
  #d;
@@ -2295,17 +2676,48 @@ var CodexWatch = class {
2295
2676
  #idle = /* @__PURE__ */ new Set();
2296
2677
  /** 连接代数:断线重连后,上一条连接遗留的订阅重试不再跑 */
2297
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();
2298
2699
  constructor(deps) {
2299
2700
  this.#d = { connect: (url) => new WebSocket(url), ...deps };
2300
2701
  }
2301
2702
  get connected() {
2302
2703
  return this.#ws !== null;
2303
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
+ }
2304
2715
  /** 定时调:没连上就找一次 app-server 连上去;连着时 codex 被 disable 了就断开 */
2305
2716
  tick() {
2306
2717
  if (this.#stopped) return;
2307
2718
  if (this.#ws) {
2308
2719
  if (this.#d.enabled && !this.#d.enabled()) this.#teardown();
2720
+ else if (this.#ready) void this.#syncLoaded();
2309
2721
  return;
2310
2722
  }
2311
2723
  const url = (this.#d.discover ?? (() => discoverAppServer()))();
@@ -2328,10 +2740,10 @@ var CodexWatch = class {
2328
2740
  this.#gen++;
2329
2741
  ws.on("open", () => void this.#onOpen());
2330
2742
  ws.on("message", (buf) => this.#onMessage(String(buf)));
2331
- ws.on("close", () => this.#teardown());
2332
- 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 ?? ""}`));
2333
2745
  }
2334
- #teardown() {
2746
+ #teardown(why = "") {
2335
2747
  const ws = this.#ws;
2336
2748
  if (!ws) return;
2337
2749
  this.#ws = null;
@@ -2341,7 +2753,7 @@ var CodexWatch = class {
2341
2753
  }
2342
2754
  for (const c of this.#calls.values()) {
2343
2755
  clearTimeout(c.timer);
2344
- c.resolve({ error: { message: "disconnected" } });
2756
+ c.resolve({ error: { message: "disconnected" }, local: true });
2345
2757
  }
2346
2758
  this.#calls.clear();
2347
2759
  for (const { ac } of this.#pending.values()) ac.abort();
@@ -2351,7 +2763,17 @@ var CodexWatch = class {
2351
2763
  this.#cwd.clear();
2352
2764
  this.#changes.clear();
2353
2765
  this.#idle.clear();
2354
- 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` : ""}`);
2355
2777
  }
2356
2778
  #send(obj) {
2357
2779
  try {
@@ -2361,13 +2783,13 @@ var CodexWatch = class {
2361
2783
  }
2362
2784
  #request(method, params) {
2363
2785
  const id = this.#nextId++;
2364
- return new Promise((resolve3) => {
2786
+ return new Promise((resolve5) => {
2365
2787
  const timer = setTimeout(() => {
2366
2788
  this.#calls.delete(id);
2367
- resolve3({ error: { message: "timeout" } });
2789
+ resolve5({ error: { message: "timeout" }, local: true });
2368
2790
  }, REQUEST_TIMEOUT_MS);
2369
2791
  timer.unref?.();
2370
- this.#calls.set(id, { resolve: resolve3, timer });
2792
+ this.#calls.set(id, { resolve: resolve5, timer });
2371
2793
  this.#send({ id, method, params });
2372
2794
  });
2373
2795
  }
@@ -2379,7 +2801,64 @@ var CodexWatch = class {
2379
2801
  return;
2380
2802
  }
2381
2803
  this.#send({ method: "initialized" });
2804
+ this.#ready = true;
2382
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
+ }
2383
2862
  }
2384
2863
  #subscribe(threadId) {
2385
2864
  if (this.#subscribed.has(threadId) || this.#retrying.has(threadId)) return;
@@ -2387,16 +2866,26 @@ var CodexWatch = class {
2387
2866
  const gen = this.#gen;
2388
2867
  const attempt = async () => {
2389
2868
  if (!this.#ws || gen !== this.#gen || !this.#retrying.has(threadId)) return;
2390
- const r = await this.#request("thread/resume", { threadId });
2391
- if (gen !== this.#gen || !this.#retrying.has(threadId)) {
2392
- 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
+ }
2393
2883
  return;
2394
2884
  }
2395
2885
  if (!r.error) {
2396
2886
  this.#retrying.delete(threadId);
2397
2887
  this.#subscribed.add(threadId);
2398
- const cwd = r.result?.thread?.cwd;
2399
- if (typeof cwd === "string") this.#cwd.set(threadId, cwd);
2888
+ this.#applyThread(threadId, r);
2400
2889
  return;
2401
2890
  }
2402
2891
  const n = (this.#retrying.get(threadId) ?? 0) + 1;
@@ -2414,9 +2903,194 @@ var CodexWatch = class {
2414
2903
  this.#retrying.delete(threadId);
2415
2904
  if (!this.#subscribed.has(threadId)) return;
2416
2905
  for (const p of this.#pending.values()) if (p.threadId === threadId) return;
2906
+ if ((this.#own.get(threadId) ?? 0) > 0) return;
2417
2907
  this.#subscribed.delete(threadId);
2418
2908
  void this.#request("thread/unsubscribe", { threadId });
2419
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
+ }
2420
3094
  #onMessage(raw) {
2421
3095
  let m;
2422
3096
  try {
@@ -2447,29 +3121,64 @@ var CodexWatch = class {
2447
3121
  case "thread/started": {
2448
3122
  const t = p.thread;
2449
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);
2450
3125
  return;
2451
3126
  }
2452
3127
  case "thread/status/changed": {
2453
3128
  const id = typeof p.threadId === "string" ? p.threadId : "";
2454
3129
  const type = p.status?.type;
2455
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);
2456
3134
  if (type === "active") {
2457
3135
  this.#idle.delete(id);
2458
3136
  this.#subscribe(id);
2459
3137
  } else {
2460
3138
  this.#idle.add(id);
2461
3139
  this.#unsubscribe(id);
3140
+ for (const t of this.#turns.values()) if (t.threadId === id) this.#armIdleGrace(t);
2462
3141
  }
3142
+ this.#wakeThread(id);
2463
3143
  return;
2464
3144
  }
2465
3145
  case "thread/closed":
2466
3146
  if (typeof p.threadId === "string") {
2467
- this.#idle.delete(p.threadId);
2468
- this.#subscribed.delete(p.threadId);
2469
- this.#retrying.delete(p.threadId);
2470
- 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);
3157
+ }
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);
2471
3179
  }
2472
3180
  return;
3181
+ }
2473
3182
  case "item/started": {
2474
3183
  const item = p.item;
2475
3184
  if (item && item.type === "fileChange" && typeof item.id === "string" && Array.isArray(item.changes)) {
@@ -2481,6 +3190,8 @@ var CodexWatch = class {
2481
3190
  case "item/completed": {
2482
3191
  const item = p.item;
2483
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);
2484
3195
  return;
2485
3196
  }
2486
3197
  case "serverRequest/resolved": {
@@ -2517,7 +3228,7 @@ var CodexWatch = class {
2517
3228
  const ac = new AbortController();
2518
3229
  const entry = { ac, threadId };
2519
3230
  this.#pending.set(key, entry);
2520
- const req = {
3231
+ const req2 = {
2521
3232
  window: true,
2522
3233
  tool: "codex",
2523
3234
  hook: { hook_event_name: "PermissionRequest", session_id: threadId, ...hook }
@@ -2525,7 +3236,7 @@ var CodexWatch = class {
2525
3236
  const settled = () => {
2526
3237
  if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
2527
3238
  };
2528
- void this.#d.gate(req, ac.signal).then((reply) => {
3239
+ void this.#d.gate(req2, ac.signal).then((reply) => {
2529
3240
  if (this.#pending.get(key) !== entry) return;
2530
3241
  this.#pending.delete(key);
2531
3242
  if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
@@ -2538,64 +3249,376 @@ var CodexWatch = class {
2538
3249
  }
2539
3250
  };
2540
3251
 
2541
- // src/daemon/index.ts
2542
- var IDLE_TIMEOUT_MS = 150 * 1e3;
2543
- var POLICY_VIOLATION = 1008;
2544
- var PING_FRAME_SERVICE = "ping";
2545
- var PONG_FRAME = JSON.stringify({ service: "pong", version: "1.0" });
2546
- var CODEX_WATCH_MS = 5 * 1e3;
2547
- function defaultCodexAppServer() {
2548
- const cfg = loadConfig();
2549
- return cfg.enabledTools.includes("codex") ? discoverAppServer(cfg.codexAppServer) : null;
2550
- }
2551
- function opencodeReady() {
2552
- 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");
2553
3257
  }
2554
- function orphanResumeText(summary) {
2555
- 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
+ }
2556
3266
  }
2557
- var ALIVE_REPORT_MS = 60 * 1e3;
2558
- var Daemon = class {
2559
- #queue = new SessionQueue();
2560
- #seen = SeenIds.load();
2561
- #deps;
2562
- #store = ApprovalStore.load();
2563
- #coord;
2564
- #gate;
2565
- /** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
2566
- #runsDone = /* @__PURE__ */ new Map();
2567
- #attempt = 0;
2568
- #stopped = false;
2569
- #ws = null;
2570
- #idleTimer = null;
2571
- #windowScan = null;
2572
- #aliveReport = null;
2573
- #codexTimer = null;
2574
- #codex = null;
2575
- constructor(deps = {}) {
2576
- this.#deps = {
2577
- connect: deps.connect ?? ((url) => new WebSocket2(url)),
2578
- now: deps.now ?? (() => Date.now()),
2579
- backend: deps.backend ?? serverBackend(),
2580
- gateScript: deps.gateScript ?? defaultGateScript(),
2581
- // 🔴 优先用 PATH 上那个稳定的 node(如 /opt/homebrew/bin/node 这种链接),不用 process.execPath:
2582
- // 后者是 Cellar 里的真实路径,`brew upgrade node` 之后就被删了,而 daemon 还活着、
2583
- // hook 命令里写的还是旧路径 —— hook 起不来,Qoder 在 bypass 下就是全放行(评审 R3)
2584
- nodePath: deps.nodePath ?? whichIn("node", defaultLocateDeps()) ?? process.execPath,
2585
- materialize: deps.materialize ?? ((plan, tool) => materialize(plan, tool)),
2586
- opencodeReady: deps.opencodeReady ?? opencodeReady,
2587
- codexAppServer: deps.codexAppServer ?? defaultCodexAppServer
2588
- };
2589
- this.#store.prune(this.#deps.now());
2590
- this.#coord = new ApprovalCoordinator({
2591
- store: this.#store,
2592
- backend: this.#deps.backend,
2593
- policy: () => loadConfig().approvalPolicy ?? "commands",
2594
- resume: (r, kind) => this.#scheduleResume(r, kind),
2595
- log,
2596
- now: this.#deps.now
2597
- });
2598
- this.#gate = new GateServer((req, signal) => this.#coord.onGate(req, signal));
3267
+ function removeCodexRemote() {
3268
+ try {
3269
+ rmSync4(codexRemotePath(), { force: true });
3270
+ } catch {
3271
+ }
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));
2599
3622
  }
2600
3623
  /** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
2601
3624
  async startGate() {
@@ -2605,17 +3628,82 @@ var Daemon = class {
2605
3628
  this.#windowScan.unref();
2606
3629
  this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
2607
3630
  this.#aliveReport.unref();
2608
- this.#codex = new CodexWatch({
2609
- gate: (req, signal) => this.#coord.onGate(req, signal),
2610
- log,
2611
- discover: () => this.#deps.codexAppServer(),
2612
- enabled: () => loadConfig().enabledTools.includes("codex")
2613
- });
2614
- this.#codex.tick();
2615
- 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);
2616
3634
  this.#codexTimer.unref();
2617
3635
  return port;
2618
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
+ }
2619
3707
  #lastDirs = "";
2620
3708
  #dirsTick = 0;
2621
3709
  /**
@@ -2640,7 +3728,7 @@ var Daemon = class {
2640
3728
  const cfg = loadConfig();
2641
3729
  if (!cfg.clientId || !cfg.boundAt) return;
2642
3730
  for (const tool of cfg.enabledTools) {
2643
- 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));
2644
3732
  }
2645
3733
  if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
2646
3734
  }
@@ -2653,7 +3741,10 @@ var Daemon = class {
2653
3741
  if (this.#windowScan) clearInterval(this.#windowScan);
2654
3742
  if (this.#aliveReport) clearInterval(this.#aliveReport);
2655
3743
  if (this.#codexTimer) clearInterval(this.#codexTimer);
2656
- 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();
2657
3748
  this.#ws?.close();
2658
3749
  void this.#gate.stop();
2659
3750
  }
@@ -2757,8 +3848,139 @@ var Daemon = class {
2757
3848
  return;
2758
3849
  }
2759
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) {
2760
3981
  if (isExpired(cmd, this.#deps.now())) {
2761
3982
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u8FC7\u671F\uFF0C\u4E22\u5F03`);
3983
+ this.#report(cmd, cmd.sessionId, "rejected", "", "expired");
2762
3984
  return;
2763
3985
  }
2764
3986
  const target = resolveTarget(cmd, {
@@ -2772,6 +3994,7 @@ var Daemon = class {
2772
3994
  });
2773
3995
  if (!target.ok) {
2774
3996
  log(`\u6307\u4EE4 ${cmd.instructionId} \u65E0\u6CD5\u6267\u884C\uFF1A${failureMessage(target.reason)}`);
3997
+ this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
2775
3998
  return;
2776
3999
  }
2777
4000
  if (target.sessionId) this.#coord.cancelSession(target.sessionId);
@@ -2782,6 +4005,7 @@ var Daemon = class {
2782
4005
  const fresh = readState(sessionId);
2783
4006
  if (!fresh) {
2784
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");
2785
4009
  return;
2786
4010
  }
2787
4011
  }
@@ -2789,17 +4013,22 @@ var Daemon = class {
2789
4013
  const plan = buildSpawnPlan({ ...cmd, sessionId }, cwd, this.#gateTarget(runId));
2790
4014
  if (!plan) {
2791
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");
2792
4017
  return;
2793
4018
  }
2794
4019
  log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename6(cwd)} len=${cmd.text.length}`);
2795
- const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd);
2796
- if (!result) {
2797
- 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);
2798
4026
  return;
2799
4027
  }
2800
4028
  if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
2801
4029
  if (!result.ok) {
2802
4030
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error}`);
4031
+ this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
2803
4032
  }
2804
4033
  if (!sessionId) {
2805
4034
  const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
@@ -2845,28 +4074,29 @@ var Daemon = class {
2845
4074
  r.consumed = true;
2846
4075
  this.#store.save();
2847
4076
  }
2848
- 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`}`);
2849
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`);
2850
4079
  else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
2851
4080
  else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
2852
4081
  }
2853
4082
  /**
2854
4083
  * 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
2855
- * 找不到可执行文件返回 null。
4084
+ * 起不了(关卡不可用 / 找不到可执行文件)返回 {fail};进程起来那一刻调 onStarted
2856
4085
  */
2857
- async #run(plan, tool, runId, sessionId, cwd) {
4086
+ async #run(plan, tool, runId, sessionId, cwd, onStarted) {
2858
4087
  if (!existsSync10(this.#deps.nodePath) || !existsSync10(this.#deps.gateScript)) {
2859
4088
  log(`\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528\uFF08node \u6216 gate.cjs \u627E\u4E0D\u5230\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C ${tool}`);
2860
- return null;
4089
+ return { fail: "gate_unavailable" };
2861
4090
  }
2862
4091
  if (tool === "opencode" && !this.#deps.opencodeReady()) {
2863
4092
  log("opencode \u63D2\u4EF6\u4E0D\u5728\uFF08deskbuddy enable opencode \u6CA1\u88C5\u6216\u5DF2\u5378\u8F7D\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C opencode");
2864
- return null;
4093
+ return { fail: "gate_unavailable" };
2865
4094
  }
2866
- const real = this.#deps.materialize(plan, tool);
2867
- if (!real) return null;
4095
+ const real2 = this.#deps.materialize(plan, tool);
4096
+ if (!real2) return { fail: "binary_not_found" };
2868
4097
  const startedAt = this.#deps.now();
2869
- const rc = startCommand(real, tool);
4098
+ const rc = startCommand(real2, tool);
4099
+ onStarted?.();
2870
4100
  this.#coord.registerRun({
2871
4101
  runId,
2872
4102
  tool,
@@ -2907,7 +4137,7 @@ async function runDaemonCommand(opts = {}) {
2907
4137
  }
2908
4138
  if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
2909
4139
  try {
2910
- writeFileSync11(pidFilePath(), String(process.pid), "utf8");
4140
+ writeFileSync13(pidFilePath(), String(process.pid), "utf8");
2911
4141
  } catch {
2912
4142
  }
2913
4143
  let daemon;
@@ -2923,7 +4153,7 @@ async function runDaemonCommand(opts = {}) {
2923
4153
  log(`\u6536\u5230 ${sig}\uFF0C\u9000\u51FA`);
2924
4154
  daemon.stop();
2925
4155
  try {
2926
- rmSync4(pidFilePath(), { force: true });
4156
+ rmSync7(pidFilePath(), { force: true });
2927
4157
  } catch {
2928
4158
  }
2929
4159
  process.exit(0);
@@ -2940,8 +4170,8 @@ async function runDaemonCommand(opts = {}) {
2940
4170
  }
2941
4171
 
2942
4172
  // src/core/registry.ts
2943
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync12 } from "fs";
2944
- 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";
2945
4175
  var FALLBACK = {
2946
4176
  version: "builtin",
2947
4177
  tools: {
@@ -3019,11 +4249,11 @@ var FALLBACK = {
3019
4249
  }
3020
4250
  };
3021
4251
  function registryCachePath() {
3022
- return join15(deskbuddyHome(), "tool-registry.json");
4252
+ return join17(deskbuddyHome(), "tool-registry.json");
3023
4253
  }
3024
4254
  function loadRegistry() {
3025
4255
  try {
3026
- return JSON.parse(readFileSync10(registryCachePath(), "utf8"));
4256
+ return JSON.parse(readFileSync13(registryCachePath(), "utf8"));
3027
4257
  } catch {
3028
4258
  return FALLBACK;
3029
4259
  }
@@ -3031,15 +4261,15 @@ function loadRegistry() {
3031
4261
  async function refreshRegistry() {
3032
4262
  try {
3033
4263
  const registry = await api.toolRegistry();
3034
- mkdirSync12(deskbuddyHome(), { recursive: true });
3035
- writeFileSync12(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
4264
+ mkdirSync14(deskbuddyHome(), { recursive: true });
4265
+ writeFileSync14(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
3036
4266
  } catch {
3037
4267
  }
3038
4268
  }
3039
4269
 
3040
4270
  // src/adapters/ccHook.ts
3041
- import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync13, readFileSync as readFileSync11, renameSync as renameSync9, statSync as statSync3, writeFileSync as writeFileSync13 } from "fs";
3042
- 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";
3043
4273
  var CC_EVENTS = [
3044
4274
  "UserPromptSubmit",
3045
4275
  "PreToolUse",
@@ -3050,7 +4280,7 @@ var CC_EVENTS = [
3050
4280
  ];
3051
4281
  function readSettings(path) {
3052
4282
  if (!existsSync11(path)) return {};
3053
- const raw = readFileSync11(path, "utf8");
4283
+ const raw = readFileSync14(path, "utf8");
3054
4284
  if (raw.trim() === "") return {};
3055
4285
  try {
3056
4286
  return JSON.parse(raw);
@@ -3059,26 +4289,32 @@ function readSettings(path) {
3059
4289
  }
3060
4290
  }
3061
4291
  function writeSettings(path, settings) {
3062
- mkdirSync13(dirname6(path), { recursive: true });
4292
+ mkdirSync15(dirname6(path), { recursive: true });
3063
4293
  let mode;
3064
4294
  try {
3065
- mode = statSync3(path).mode & 511;
4295
+ mode = statSync4(path).mode & 511;
3066
4296
  } catch {
3067
4297
  mode = void 0;
3068
4298
  }
3069
4299
  const tmp = path + ".deskbuddy.tmp";
3070
- writeFileSync13(tmp, JSON.stringify(settings, null, 2), "utf8");
3071
- if (mode !== void 0) chmodSync2(tmp, mode);
3072
- renameSync9(tmp, path);
4300
+ writeFileSync15(tmp, JSON.stringify(settings, null, 2), "utf8");
4301
+ if (mode !== void 0) chmodSync4(tmp, mode);
4302
+ renameSync11(tmp, path);
3073
4303
  }
3074
- function installCcHook(settingsPath, hookPath, events = CC_EVENTS, tool = "claude_code") {
4304
+ function mentions(command, script) {
4305
+ return !!command && (command.includes(script) || command.includes(fwdSlash(script)));
4306
+ }
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) {
3075
4311
  const settings = readSettings(settingsPath);
3076
4312
  settings.hooks ??= {};
3077
4313
  for (const event of events) {
3078
4314
  const groups = settings.hooks[event] ??= [];
3079
- 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)));
3080
4316
  if (already) continue;
3081
- groups.push({ hooks: [{ type: "command", command: `node ${hookPath} ${tool}` }] });
4317
+ groups.push({ hooks: [{ type: "command", command: `node ${scriptArg(hookPath, platform)} ${tool}` }] });
3082
4318
  }
3083
4319
  writeSettings(settingsPath, settings);
3084
4320
  }
@@ -3086,26 +4322,26 @@ var WINDOW_GATE_TIMEOUT_S = 7 * 24 * 60 * 60;
3086
4322
  function shQuote2(s) {
3087
4323
  return `'${s.replace(/'/g, `'\\''`)}'`;
3088
4324
  }
3089
- function installCcWindowGate(settingsPath, gatePath, tool = "claude_code") {
4325
+ function installCcWindowGate(settingsPath, gatePath, tool = "claude_code", platform = process.platform) {
3090
4326
  const settings = readSettings(settingsPath);
3091
4327
  settings.hooks ??= {};
3092
4328
  const groups = settings.hooks.PermissionRequest ??= [];
3093
- 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;
3094
4330
  const toolArg = tool === "claude_code" ? "" : ` --tool ${tool}`;
3095
4331
  groups.push({
3096
- 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 }]
3097
4333
  });
3098
4334
  writeSettings(settingsPath, settings);
3099
4335
  }
3100
4336
  function uninstallCcHook(settingsPath, hookPath) {
3101
4337
  const settings = readSettings(settingsPath);
3102
4338
  if (!settings.hooks) return;
3103
- const gatePath = join16(dirname6(hookPath), "gate.cjs");
4339
+ const gatePath = join18(dirname6(hookPath), "gate.cjs");
3104
4340
  for (const event of Object.keys(settings.hooks)) {
3105
4341
  const groups = settings.hooks[event] ?? [];
3106
4342
  const kept = [];
3107
4343
  for (const g of groups) {
3108
- 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));
3109
4345
  if (hooks.length > 0) kept.push({ ...g, hooks });
3110
4346
  }
3111
4347
  settings.hooks[event] = kept;
@@ -3114,8 +4350,8 @@ function uninstallCcHook(settingsPath, hookPath) {
3114
4350
  }
3115
4351
 
3116
4352
  // src/adapters/codexHook.ts
3117
- import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, renameSync as renameSync10, statSync as statSync4, writeFileSync as writeFileSync14 } from "fs";
3118
- 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";
3119
4355
  var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
3120
4356
  var CODEX_MARKER_END = "# <<< deskbuddy <<<";
3121
4357
  var CODEX_EVENTS = [
@@ -3128,13 +4364,13 @@ var CODEX_EVENTS = [
3128
4364
  ];
3129
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";
3130
4366
  function codexConfigPath() {
3131
- return join17(codexHome(), "config.toml");
4367
+ return join19(codexHome(), "config.toml");
3132
4368
  }
3133
- function renderBlock(hookPath, events, tool) {
3134
- const command = JSON.stringify(`node ${hookPath} ${tool}`);
3135
- 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];
3136
4372
  for (const event of events) {
3137
- lines.push(
4373
+ lines2.push(
3138
4374
  `[[hooks.${event}]]`,
3139
4375
  'matcher = ""',
3140
4376
  "hooks = [",
@@ -3143,21 +4379,21 @@ function renderBlock(hookPath, events, tool) {
3143
4379
  ""
3144
4380
  );
3145
4381
  }
3146
- lines.push(CODEX_MARKER_END);
3147
- return lines.join("\n");
4382
+ lines2.push(CODEX_MARKER_END);
4383
+ return lines2.join("\n");
3148
4384
  }
3149
4385
  function writeAtomic(path, content) {
3150
- mkdirSync14(dirname7(path), { recursive: true });
4386
+ mkdirSync16(dirname7(path), { recursive: true });
3151
4387
  let mode;
3152
4388
  try {
3153
- mode = statSync4(path).mode & 511;
4389
+ mode = statSync5(path).mode & 511;
3154
4390
  } catch {
3155
4391
  mode = void 0;
3156
4392
  }
3157
4393
  const tmp = path + ".deskbuddy.tmp";
3158
- writeFileSync14(tmp, content, "utf8");
3159
- if (mode !== void 0) chmodSync3(tmp, mode);
3160
- renameSync10(tmp, path);
4394
+ writeFileSync16(tmp, content, "utf8");
4395
+ if (mode !== void 0) chmodSync5(tmp, mode);
4396
+ renameSync12(tmp, path);
3161
4397
  }
3162
4398
  function assertSafeToAppend(raw, path) {
3163
4399
  for (const quote of ['"""', "'''"]) {
@@ -3166,35 +4402,35 @@ function assertSafeToAppend(raw, path) {
3166
4402
  }
3167
4403
  }
3168
4404
  }
3169
- function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex") {
3170
- 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") : "";
3171
4407
  if (raw.includes(CODEX_MARKER_BEGIN)) return;
3172
4408
  assertSafeToAppend(raw, configPath2);
3173
- const block = renderBlock(hookPath, events, tool);
4409
+ const block = renderBlock(hookPath, events, tool, platform);
3174
4410
  const head = raw === "" ? "" : (raw.endsWith("\n") ? raw : raw + "\n") + "\n";
3175
4411
  writeAtomic(configPath2, head + block + "\n");
3176
4412
  }
3177
4413
  function uninstallCodexHook(configPath2) {
3178
4414
  if (!existsSync12(configPath2)) return;
3179
- const raw = readFileSync12(configPath2, "utf8");
3180
- const lines = raw.split("\n");
3181
- 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);
3182
4418
  if (begin === -1) return;
3183
- 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);
3184
4420
  if (end === -1) {
3185
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`);
3186
4422
  }
3187
- const start = begin > 0 && lines[begin - 1].trim() === "" ? begin - 1 : begin;
3188
- lines.splice(start, end - start + 1);
3189
- 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"));
3190
4426
  }
3191
4427
 
3192
4428
  // src/commands/tools.ts
3193
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
4429
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3194
4430
  function fileContains(path, needle) {
3195
4431
  try {
3196
4432
  if (!existsSync13(path)) return false;
3197
- return readFileSync13(path, "utf8").includes(needle);
4433
+ return readFileSync16(path, "utf8").includes(needle);
3198
4434
  } catch {
3199
4435
  return false;
3200
4436
  }
@@ -3209,7 +4445,8 @@ var ADAPTERS = {
3209
4445
  configPath: (tool) => CC_CONFIG_PATHS[tool]?.() ?? null,
3210
4446
  install: (p, hook, events, tool) => installCcHook(p, hook, events, tool),
3211
4447
  uninstall: (p, hook) => uninstallCcHook(p, hook),
3212
- isInstalled: (p, hook) => fileContains(p, hook),
4448
+ // Windows 上装出去的是正斜杠形式;JSON 里的反斜杠还会被转义成两个,原样搜不到
4449
+ isInstalled: (p, hook) => fileContains(p, hook) || fileContains(p, fwdSlash(hook)),
3213
4450
  defaultEvents: CC_EVENTS,
3214
4451
  // 实测 PermissionRequest 与窗口授权框并行的:Claude Code(S10 §2)、Qoder CN 桌面端(二期 V8)。
3215
4452
  // WorkBuddy 桌面端弹授权框不触发 PermissionRequest;Codex 的 hook 会阻塞授权框——都不装
@@ -3260,91 +4497,427 @@ function subscribedEvents(registry, tool, fallback) {
3260
4497
  return picked.length > 0 ? picked : fallback;
3261
4498
  }
3262
4499
 
3263
- // src/commands/disable.ts
3264
- function stdout(line) {
3265
- 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, `'\\''`)}'`;
3266
4524
  }
3267
- function toolsToDisable(explicit) {
3268
- if (explicit.length > 0) return explicit;
3269
- const known = Object.keys(loadRegistry()?.tools ?? {});
3270
- 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
+ ];
3271
4546
  }
3272
- async function runDisable(tools, opts = {}) {
3273
- const out = opts.out ?? stdout;
3274
- const registry = loadRegistry();
3275
- const hookPath = installedHookPath();
3276
- const targets = toolsToDisable(tools);
3277
- const cfg = loadConfig();
3278
- const enabled = new Set(cfg.enabledTools);
3279
- let failed = 0;
3280
- for (const tool of targets) {
3281
- const adapter = adapterFor(registry, tool);
3282
- if (!adapter) {
3283
- 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;
3284
4574
  }
3285
- const configPath2 = adapter.configPath(tool);
3286
- 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) => {
3287
4766
  try {
3288
- adapter.uninstall(configPath2, hookPath);
3289
- out(`\u2705 ${tool}\uFF1A\u5DF2\u4ECE ${configPath2} \u79FB\u9664`);
3290
- } catch (e) {
3291
- out(`\u2717 ${tool}\uFF1A${e.message}`);
3292
- failed += 1;
3293
- continue;
4767
+ return readFileSync17(join20(binDir, n), "utf8").includes(SHIM_MARKER);
4768
+ } catch {
4769
+ return false;
3294
4770
  }
3295
- 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 {
3296
4784
  }
3297
- cfg.enabledTools = [...enabled];
3298
- saveConfig(cfg);
3299
- if (enabled.size === 0 && !opts.keepAutostart) {
3300
- const r = await uninstallAutostart();
3301
- 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
+ }
3302
4806
  }
3303
- if (!opts.brief) {
3304
- out("");
3305
- out("\u5DF2\u505C\u6B62\u91C7\u96C6\u3002\u6388\u6743\u4E0E clientId \u4ECD\u4FDD\u7559\uFF0C\u91CD\u65B0 deskbuddy enable \u5373\u53EF\u6062\u590D\u3002");
3306
- 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 {
3307
4810
  }
3308
- 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;
3309
4869
  }
3310
4870
 
3311
4871
  // src/commands/enable.ts
3312
- import { dirname as dirname8, join as join18 } from "path";
3313
- function stdout2(line) {
4872
+ function stdout(line) {
3314
4873
  process.stdout.write(line + "\n");
3315
4874
  }
3316
4875
  function parseTools(arg) {
3317
4876
  return (arg ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
3318
4877
  }
3319
- async function runEnable(tools, opts = {}) {
3320
- const out = opts.out ?? stdout2;
3321
- if (tools.length === 0) {
3322
- out("\u7528\u6CD5\uFF1Adeskbuddy enable <tool[,tool]>\uFF0C\u4F8B\u5982 deskbuddy enable claude_code,codex");
3323
- return 1;
4878
+ function defaultShimEnvForInstall() {
4879
+ let node = null;
4880
+ try {
4881
+ node = whichIn("node", defaultLocateDeps());
4882
+ } catch {
4883
+ node = null;
3324
4884
  }
3325
- if (!loadConfig().boundAt) {
3326
- out("\u8FD8\u6CA1\u6709\u6388\u6743\u3002\u8BF7\u5148\u8FD0\u884C\uFF1Adeskbuddy setup");
3327
- 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
+ }
3328
4900
  }
3329
- ensureClientId();
3330
- if (!opts.skipRefresh) await refreshRegistry();
4901
+ }
4902
+ function installTools(tools, out, shim = defaultShimEnvForInstall()) {
3331
4903
  const registry = loadRegistry();
3332
4904
  const hookPath = installedHookPath();
3333
4905
  syncHookFile();
3334
4906
  const cfg = loadConfig();
3335
4907
  const enabled = new Set(cfg.enabledTools);
3336
- let failed = 0;
4908
+ const installed = [];
4909
+ const failed = [];
3337
4910
  for (const tool of tools) {
3338
4911
  const adapter = adapterFor(registry, tool);
3339
4912
  if (!adapter) {
3340
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`);
3341
- failed += 1;
4914
+ failed.push({ tool, reason: "\u672C\u5BA2\u6237\u7AEF\u8FD8\u4E0D\u652F\u6301" });
3342
4915
  continue;
3343
4916
  }
3344
4917
  const configPath2 = adapter.configPath(tool);
3345
4918
  if (!configPath2) {
3346
4919
  out(`\u2717 ${tool}\uFF1A\u672C\u5BA2\u6237\u7AEF\u4E0D\u77E5\u9053\u5B83\u7684\u914D\u7F6E\u6587\u4EF6\u5728\u54EA\uFF0C\u5DF2\u8DF3\u8FC7`);
3347
- failed += 1;
4920
+ failed.push({ tool, reason: "\u627E\u4E0D\u5230\u914D\u7F6E\u6587\u4EF6" });
3348
4921
  continue;
3349
4922
  }
3350
4923
  const events = subscribedEvents(registry, tool, adapter.defaultEvents);
@@ -3352,13 +4925,14 @@ async function runEnable(tools, opts = {}) {
3352
4925
  adapter.install(configPath2, hookPath, events, tool);
3353
4926
  } catch (e) {
3354
4927
  out(`\u2717 ${tool}\uFF1A${e.message}`);
3355
- failed += 1;
4928
+ failed.push({ tool, reason: e.message });
3356
4929
  continue;
3357
4930
  }
3358
4931
  enabled.add(tool);
4932
+ installed.push(tool);
3359
4933
  out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
3360
4934
  try {
3361
- if (adapter.installWindowGate?.(configPath2, join18(dirname8(hookPath), "gate.cjs"), tool)) {
4935
+ if (adapter.installWindowGate?.(configPath2, join22(dirname9(hookPath), "gate.cjs"), tool)) {
3362
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`);
3363
4937
  }
3364
4938
  } catch (e) {
@@ -3368,6 +4942,22 @@ async function runEnable(tools, opts = {}) {
3368
4942
  }
3369
4943
  cfg.enabledTools = [...enabled];
3370
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);
3371
4961
  if (!opts.skipAutostart) {
3372
4962
  const r = await installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() });
3373
4963
  out("");
@@ -3377,69 +4967,132 @@ async function runEnable(tools, opts = {}) {
3377
4967
  out("\u88C5\u5B8C\u4E86\u3002\u72B6\u6001\u91C7\u96C6\u9760 hook\uFF0C\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF1B");
3378
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");
3379
4969
  out("\u968F\u65F6\u53EF\u4EE5 deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001deskbuddy disable \u5E72\u51C0\u79FB\u9664\u3002");
3380
- return failed > 0 ? 1 : 0;
4970
+ return failed.length > 0 ? 1 : 0;
3381
4971
  }
3382
4972
 
3383
- // src/commands/logout.ts
3384
- import { createInterface } from "readline";
3385
- import { existsSync as existsSync14, rmSync as rmSync5 } from "fs";
3386
- function stdout3(line) {
4973
+ // src/commands/disable.ts
4974
+ function stdout2(line) {
3387
4975
  process.stdout.write(line + "\n");
3388
4976
  }
3389
- function askYesNo(question) {
3390
- return new Promise((resolve3) => {
3391
- const rl = createInterface({ input: process.stdin, output: process.stdout });
3392
- rl.question(question, (answer) => {
3393
- rl.close();
3394
- resolve3(/^(y|yes)$/i.test(answer.trim()));
3395
- });
3396
- });
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])];
3397
4981
  }
3398
- async function runLogout(opts = {}) {
3399
- const out = opts.out ?? stdout3;
3400
- const home = deskbuddyHome();
3401
- out("deskbuddy logout \u4F1A\u505A\u8FD9\u4E24\u4EF6\u4E8B\uFF1A");
3402
- 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");
3403
- 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`);
3404
- out("");
3405
- out("\u5220\u6389\u4E4B\u540E\u518D\u7528\u5C31\u8981\u91CD\u65B0 deskbuddy setup \u626B\u7801\u3002");
3406
- 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");
3407
- out("\u4F46\u91CD\u65B0 setup \u4F1A\u6362\u4E00\u4E2A\u65B0\u7684 clientId\uFF0C\u65E7\u7684\u4E0D\u4F1A\u518D\u4E0A\u62A5\u3002");
3408
- if (!opts.yes) {
3409
- const confirm = opts.confirm ?? (() => askYesNo("\u786E\u8BA4\u6267\u884C\uFF1F(y/N) "));
3410
- if (!await confirm()) {
3411
- out("\u5DF2\u53D6\u6D88\uFF0C\u4EC0\u4E48\u90FD\u6CA1\u52A8\u3002");
3412
- 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;
3413
5004
  }
5005
+ enabled.delete(tool);
3414
5006
  }
3415
- await runDisable([], { out, brief: true, keepAutostart: opts.keepAutostart });
3416
- if (existsSync14(home)) {
3417
- 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
+ }
3418
5021
  }
3419
- out("");
3420
- out("\u2705 \u5DF2\u6E05\u9664\u672C\u673A\u7684 DeskBuddy \u6570\u636E\u3002");
3421
- 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
+ });
3422
5075
  }
3423
5076
 
3424
5077
  // src/commands/setup.ts
3425
5078
  import qrcode from "qrcode-terminal";
3426
5079
 
3427
5080
  // src/commands/remoteCwd.ts
3428
- import { existsSync as existsSync15, statSync as statSync5 } from "fs";
3429
- import { createInterface as createInterface2 } from "readline";
3430
- import { homedir as homedir5 } from "os";
3431
- import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
3432
- 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()) {
3433
5086
  const s = (raw || "").trim();
3434
5087
  if (!s) return null;
3435
- const expanded = s === "~" ? home : s.startsWith("~/") ? resolve2(home, s.slice(2)) : s;
3436
- 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);
3437
5090
  return abs;
3438
5091
  }
3439
5092
  function checkPath(abs) {
3440
- if (!existsSync15(abs)) return "missing";
5093
+ if (!existsSync17(abs)) return "missing";
3441
5094
  try {
3442
- return statSync5(abs).isDirectory() ? "ok" : "not_a_dir";
5095
+ return statSync7(abs).isDirectory() ? "ok" : "not_a_dir";
3443
5096
  } catch {
3444
5097
  return "missing";
3445
5098
  }
@@ -3470,7 +5123,7 @@ async function askRemoteCwd(deps) {
3470
5123
  }
3471
5124
  function terminalAsk(prompt) {
3472
5125
  if (!process.stdin.isTTY) return Promise.resolve(null);
3473
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
5126
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3474
5127
  return new Promise((res) => {
3475
5128
  rl.question(prompt, (answer) => {
3476
5129
  rl.close();
@@ -3551,11 +5204,11 @@ function parseApiBase(argv) {
3551
5204
  function sleep(ms) {
3552
5205
  return new Promise((r) => setTimeout(r, ms));
3553
5206
  }
3554
- function stdout4(line) {
5207
+ function stdout3(line) {
3555
5208
  process.stdout.write(line + "\n");
3556
5209
  }
3557
5210
  async function runSetup(opts = {}) {
3558
- const out = opts.out ?? stdout4;
5211
+ const out = opts.out ?? stdout3;
3559
5212
  const clientId = ensureClientId();
3560
5213
  if (opts.apiBase) applyApiBase(opts.apiBase);
3561
5214
  const cfg = loadConfig();
@@ -3588,25 +5241,250 @@ async function runSetup(opts = {}) {
3588
5241
  return 0;
3589
5242
  }
3590
5243
 
3591
- // src/commands/status.ts
3592
- function fmtTime(ms) {
3593
- const d = new Date(ms);
3594
- const p = (n) => String(n).padStart(2, "0");
3595
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
3596
- }
3597
- function stdout5(line) {
3598
- process.stdout.write(line + "\n");
3599
- }
3600
- var STATE_LABELS = {
3601
- watch: "\u8FDB\u884C\u4E2D",
3602
- ask_approval: "\u5F85\u786E\u8BA4",
3603
- done: "\u5DF2\u5B8C\u6210",
3604
- 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
3605
5253
  };
3606
- function relativeTime(from, now) {
3607
- const sec = Math.max(0, Math.round((now - from) / 1e3));
3608
- if (sec < 60) return `${sec} \u79D2\u524D`;
3609
- const min = Math.round(sec / 60);
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) {
5485
+ const sec = Math.max(0, Math.round((now - from) / 1e3));
5486
+ if (sec < 60) return `${sec} \u79D2\u524D`;
5487
+ const min = Math.round(sec / 60);
3610
5488
  if (min < 60) return `${min} \u5206\u949F\u524D`;
3611
5489
  const hour = Math.round(min / 60);
3612
5490
  if (hour < 24) return `${hour} \u5C0F\u65F6\u524D`;
@@ -3617,48 +5495,58 @@ function pad(s, width) {
3617
5495
  return s + " ".repeat(Math.max(1, width - w));
3618
5496
  }
3619
5497
  function formatStatus(input) {
3620
- const lines = [];
5498
+ const lines2 = [];
3621
5499
  const cfg = input.config;
3622
- lines.push("DeskBuddy \u72B6\u6001");
5500
+ lines2.push("DeskBuddy \u72B6\u6001");
3623
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";
3624
- lines.push(` ${pad("\u8D26\u53F7", 10)}${account}`);
3625
- 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`);
3626
5504
  if (cfg.boundAt && cfg.boundApiBase && cfg.boundApiBase !== cfg.apiBase) {
3627
- lines.push(
5505
+ lines2.push(
3628
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`
3629
5507
  );
3630
5508
  }
3631
5509
  if (input.envOverride?.active) {
3632
- lines.push(
5510
+ lines2.push(
3633
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`
3634
5512
  );
3635
5513
  }
3636
5514
  if (input.tools.length === 0) {
3637
- 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`);
3638
5516
  } else {
3639
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");
3640
- lines.push(` ${pad("\u5DF2\u542F\u7528", 10)}${desc}`);
5518
+ lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}${desc}`);
3641
5519
  }
3642
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`;
3643
- 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
+ }
3644
5532
  for (const t of input.tools) {
3645
- if (t.notice) lines.push(` ${t.notice}`);
5533
+ if (t.notice) lines2.push(` ${t.notice}`);
3646
5534
  }
3647
- lines.push("");
5535
+ lines2.push("");
3648
5536
  const sorted = [...input.sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3649
- 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`);
3650
5538
  if (sorted.length === 0) {
3651
- 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");
3652
5540
  }
3653
5541
  for (const s of sorted) {
3654
- lines.push(
5542
+ lines2.push(
3655
5543
  ` ${pad(s.tool, 14)}${pad(s.title || s.project || "-", 24)}${pad(STATE_LABELS[s.state] ?? s.state, 8)}${relativeTime(s.updatedAt, input.now)}`
3656
5544
  );
3657
5545
  }
3658
- lines.push("");
3659
- 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");
3660
- lines.push("\u4E0D\u4E0A\u62A5\uFF1Aprompt \u539F\u6587\u3001\u547D\u4EE4\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u4EE3\u7801\u5185\u5BB9");
3661
- 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");
3662
5550
  }
3663
5551
  function collectStatus(now = Date.now()) {
3664
5552
  const config = loadConfig();
@@ -3680,7 +5568,21 @@ function collectStatus(now = Date.now()) {
3680
5568
  });
3681
5569
  sessions.push(...readAliveStates(tool));
3682
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
+ }
3683
5583
  return {
5584
+ windowWrap,
5585
+ codexWrap,
3684
5586
  config,
3685
5587
  hookVersion: installedHookVersion(),
3686
5588
  currentVersion: packageVersion(),
@@ -3696,16 +5598,1038 @@ function runStatus(opts = {}) {
3696
5598
  return 0;
3697
5599
  }
3698
5600
 
5601
+ // src/commands/update.ts
5602
+ function stdout6(line) {
5603
+ process.stdout.write(line + "\n");
5604
+ }
5605
+ async function runUpdate(opts = {}) {
5606
+ const out = opts.out ?? stdout6;
5607
+ const cfg = loadConfig();
5608
+ if (!cfg.boundAt) {
5609
+ out("\u8FD8\u6CA1\u6709\u6388\u6743\u3002\u8BF7\u5148\u8FD0\u884C\uFF1Adeskbuddy setup");
5610
+ return 1;
5611
+ }
5612
+ if (cfg.enabledTools.length === 0) {
5613
+ out("\u672C\u673A\u8FD8\u6CA1\u6709\u542F\u7528\u4EFB\u4F55\u5DE5\u5177\uFF0C\u6CA1\u4EC0\u4E48\u8981\u5347\u7EA7\u7684\u3002\u7528 deskbuddy enable <tool> \u542F\u7528");
5614
+ return 1;
5615
+ }
5616
+ out(`\u5347\u7EA7\u5230 DeskBuddy ${packageVersion()}\uFF0C\u91CD\u88C5\u5DF2\u542F\u7528\u7684\uFF1A${cfg.enabledTools.join(", ")}`);
5617
+ out("");
5618
+ return runEnable([...cfg.enabledTools], opts);
5619
+ }
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
+
3699
6617
  // src/cli.ts
3700
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
3701
6619
 
3702
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
3703
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
3704
6624
  deskbuddy enable <tool[,tool]> \u5B89\u88C5\u91C7\u96C6 hook\uFF08\u5982 claude_code,codex\uFF09
3705
6625
  deskbuddy disable [tool[,tool]] \u79FB\u9664\u91C7\u96C6 hook\uFF08\u4E0D\u5E26\u53C2\u6570=\u5168\u90E8\uFF09
6626
+ deskbuddy update \u5347\u7EA7\u5230\u5F53\u524D\u8FD9\u7248\uFF1A\u91CD\u88C5\u5DF2\u542F\u7528\u5DE5\u5177\u7684 hook\u3001\u91CD\u542F daemon
6627
+ \uFF08npx deskbuddy@<\u7248\u672C> update\uFF09
3706
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
3707
6629
  deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001\u4E0A\u62A5\u4E86\u54EA\u4E9B\u5B57\u6BB5
3708
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
3709
6633
 
3710
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
3711
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
@@ -3714,7 +6638,7 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
3714
6638
  async function main(argv) {
3715
6639
  const cmd = argv[2];
3716
6640
  const arg = argv[3];
3717
- if (cmd && !["-h", "--help", "help", "-v", "--version"].includes(cmd)) {
6641
+ if (cmd && !["-h", "--help", "help", "-v", "--version", "wrap"].includes(cmd)) {
3718
6642
  try {
3719
6643
  syncHookFile();
3720
6644
  } catch {
@@ -3731,18 +6655,24 @@ async function main(argv) {
3731
6655
  case "--version":
3732
6656
  process.stdout.write(packageVersion() + "\n");
3733
6657
  return 0;
6658
+ case "install":
6659
+ return runInstall({ code: parseCode(argv), apiBase: parseApiBase(argv) });
3734
6660
  case "setup":
3735
6661
  return runSetup({ apiBase: parseApiBase(argv) });
3736
6662
  case "enable":
3737
6663
  return runEnable(parseTools(arg));
3738
6664
  case "disable":
3739
6665
  return runDisable(parseTools(arg));
6666
+ case "update":
6667
+ return runUpdate();
3740
6668
  case "daemon":
3741
6669
  return runDaemonCommand({ foreground: argv.includes("--foreground") || argv.includes("-f") });
3742
6670
  case "status":
3743
6671
  return runStatus();
3744
6672
  case "logout":
3745
6673
  return runLogout({ yes: argv.includes("--yes") || argv.includes("-y") });
6674
+ case "wrap":
6675
+ return runWrap(argv.slice(3));
3746
6676
  default:
3747
6677
  process.stderr.write(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}
3748
6678