deskbuddy 0.1.0 → 0.2.0-beta.0

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,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/commands/daemon.ts
4
+ import { writeFileSync as writeFileSync11, rmSync as rmSync4 } from "fs";
5
+
3
6
  // src/core/config.ts
4
7
  import { randomUUID } from "crypto";
5
8
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
9
  import { homedir, hostname } from "os";
7
10
  import { join } from "path";
11
+ function defaultRemoteCwd(home = homedir()) {
12
+ return join(home, "Desktop");
13
+ }
8
14
  var PROD_API_BASE = "https://api.shifengintelli.com";
9
15
  var TEST_API_BASE = "https://test-api.shifengintelli.com";
10
16
  var DEFAULT_API_BASE = process.env.DESKBUDDY_API_BASE || PROD_API_BASE;
@@ -38,6 +44,16 @@ function claudeHome() {
38
44
  function claudeSettingsPath() {
39
45
  return join(claudeHome(), "settings.json");
40
46
  }
47
+ function workbuddySettingsPath() {
48
+ return join(process.env.CODEBUDDY_CONFIG_DIR || join(homedir(), ".workbuddy"), "settings.json");
49
+ }
50
+ function qoderSettingsPath() {
51
+ return join(process.env.DESKBUDDY_QODER_DIR || join(homedir(), ".qoder-cn"), "settings.json");
52
+ }
53
+ function opencodePluginPath() {
54
+ const base = process.env.DESKBUDDY_OPENCODE_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
55
+ return join(base, "plugin", "deskbuddy.js");
56
+ }
41
57
  function codexHome() {
42
58
  return process.env.CODEX_HOME || join(homedir(), ".codex");
43
59
  }
@@ -64,7 +80,10 @@ function loadConfig() {
64
80
  // ⚠️ 这里是逐字段重建对象,不是展开 parsed —— 新增字段【必须】在这里也加一行,
65
81
  // 否则它写得进 config.json、却永远读不出来(saveConfig 存的是本函数的返回值,
66
82
  // 于是下一次保存还会把它抹掉),而且全程不报错
67
- boundApiBase: parsed.boundApiBase
83
+ boundApiBase: parsed.boundApiBase,
84
+ remoteCwd: parsed.remoteCwd,
85
+ // 不认识的值一律回落默认,不能让一个手滑写错的配置变成"全放行"或"全拦"
86
+ approvalPolicy: parsed.approvalPolicy === "all" ? "all" : parsed.approvalPolicy === "commands" ? "commands" : void 0
68
87
  };
69
88
  } catch {
70
89
  return fallback;
@@ -85,33 +104,310 @@ function ensureClientId() {
85
104
  return config.clientId;
86
105
  }
87
106
 
88
- // src/core/registry.ts
89
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
90
- import { join as join3 } from "path";
107
+ // src/daemon/autostart.ts
108
+ import { execFile } from "child_process";
109
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
110
+ import { homedir as homedir2 } from "os";
111
+ import { fileURLToPath } from "url";
112
+ import { basename, dirname, join as join2 } from "path";
113
+ import { promisify } from "util";
114
+ var run = promisify(execFile);
115
+ var LAUNCH_LABEL = "com.deskbuddy.daemon";
116
+ var SYSTEMD_UNIT = "deskbuddy.service";
117
+ var SCHTASK_NAME = "DeskBuddyDaemon";
118
+ var CLI_ENTRY = "cli.js";
119
+ function defaultCliPath(argv1 = process.argv[1]) {
120
+ const sibling = fileURLToPath(new URL("./" + CLI_ENTRY, import.meta.url));
121
+ if (typeof argv1 === "string" && basename(argv1) === CLI_ENTRY) return argv1;
122
+ return sibling;
123
+ }
124
+ function xmlEscape(s) {
125
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
126
+ }
127
+ function launchAgentPlist(t) {
128
+ return `<?xml version="1.0" encoding="UTF-8"?>
129
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
130
+ <plist version="1.0">
131
+ <dict>
132
+ <key>Label</key><string>${LAUNCH_LABEL}</string>
133
+ <key>ProgramArguments</key>
134
+ <array>
135
+ <string>${xmlEscape(t.execPath)}</string>
136
+ <string>${xmlEscape(t.cliPath)}</string>
137
+ <string>daemon</string>
138
+ </array>
139
+ <key>RunAtLoad</key><true/>
140
+ <key>KeepAlive</key>
141
+ <dict><key>SuccessfulExit</key><false/></dict>
142
+ <key>ProcessType</key><string>Background</string>
143
+ </dict>
144
+ </plist>
145
+ `;
146
+ }
147
+ function systemdUnit(t) {
148
+ return `[Unit]
149
+ Description=DeskBuddy daemon
91
150
 
92
- // src/core/api.ts
151
+ [Service]
152
+ ExecStart=${t.execPath} ${t.cliPath} daemon
153
+ Restart=on-failure
154
+ RestartSec=10
155
+
156
+ [Install]
157
+ WantedBy=default.target
158
+ `;
159
+ }
160
+ function launchAgentPath(home = homedir2()) {
161
+ return join2(home, "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
162
+ }
163
+ function systemdUnitPath(home = homedir2()) {
164
+ return join2(home, ".config", "systemd", "user", SYSTEMD_UNIT);
165
+ }
166
+ function write(path, content) {
167
+ mkdirSync2(dirname(path), { recursive: true });
168
+ writeFileSync2(path, content, "utf8");
169
+ }
170
+ async function installAutostart(t, platform = process.platform) {
171
+ try {
172
+ if (platform === "darwin") {
173
+ const p = launchAgentPath();
174
+ write(p, launchAgentPlist(t));
175
+ await run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCH_LABEL}`]).catch(() => {
176
+ });
177
+ await run("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, p]);
178
+ return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08launchd\uFF09" };
179
+ }
180
+ if (platform === "linux") {
181
+ write(systemdUnitPath(), systemdUnit(t));
182
+ await run("systemctl", ["--user", "daemon-reload"]);
183
+ await run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
184
+ return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08systemd --user\uFF09" };
185
+ }
186
+ 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" };
200
+ }
201
+ 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
+ } catch (e) {
203
+ return { ok: false, message: `\u5F00\u673A\u81EA\u542F\u6CA1\u8BBE\u7F6E\u6210\u529F\uFF08${e.message}\uFF09\uFF0C\u53EF\u4EE5\u624B\u52A8\u8DD1 deskbuddy daemon` };
204
+ }
205
+ }
206
+ async function uninstallAutostart(platform = process.platform) {
207
+ try {
208
+ if (platform === "darwin") {
209
+ await run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCH_LABEL}`]).catch(() => {
210
+ });
211
+ const p = launchAgentPath();
212
+ if (existsSync2(p)) rmSync(p, { force: true });
213
+ return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
214
+ }
215
+ if (platform === "linux") {
216
+ await run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]).catch(() => {
217
+ });
218
+ const p = systemdUnitPath();
219
+ if (existsSync2(p)) rmSync(p, { force: true });
220
+ await run("systemctl", ["--user", "daemon-reload"]).catch(() => {
221
+ });
222
+ return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
223
+ }
224
+ if (platform === "win32") {
225
+ await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME]).catch(() => {
226
+ });
227
+ return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
228
+ }
229
+ return { ok: true, message: "" };
230
+ } catch (e) {
231
+ return { ok: false, message: `\u53D6\u6D88\u5F00\u673A\u81EA\u542F\u65F6\u51FA\u9519\uFF08${e.message}\uFF09\uFF0C\u53EF\u80FD\u9700\u8981\u624B\u52A8\u6E05\u7406` };
232
+ }
233
+ }
234
+ function pidFilePath() {
235
+ return join2(deskbuddyHome(), "daemon.pid");
236
+ }
237
+
238
+ // src/daemon/index.ts
239
+ import { randomBytes as randomBytes5 } from "crypto";
240
+ import { existsSync as existsSync10 } from "fs";
241
+ import { basename as basename6 } from "path";
242
+ import WebSocket2 from "ws";
243
+
244
+ // src/adapters/ocPlugin.ts
245
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
246
+ import { dirname as dirname2, join as join3 } from "path";
247
+ var OC_MARKER = "// deskbuddy:opencode-plugin";
248
+ function ocPluginSource(pluginModule, nodePath) {
249
+ return [
250
+ OC_MARKER,
251
+ "// \u7531 deskbuddy enable opencode \u751F\u6210\uFF0Cdeskbuddy disable opencode \u4F1A\u5220\u6389\u5B83\u3002",
252
+ `import { make } from ${JSON.stringify(pluginModule)}`,
253
+ `export const DeskBuddy = make({ node: ${JSON.stringify(nodePath)} })`,
254
+ ""
255
+ ].join("\n");
256
+ }
257
+ function ocPluginModule(hookPath) {
258
+ return join3(dirname2(hookPath), "opencode.mjs");
259
+ }
260
+ function installOcPlugin(pluginPath, hookPath, nodePath) {
261
+ if (existsSync3(pluginPath) && !readFileSync2(pluginPath, "utf8").includes(OC_MARKER)) {
262
+ throw new Error(`${pluginPath} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F DeskBuddy \u751F\u6210\u7684\uFF0C\u8BF7\u5148\u632A\u8D70\u518D\u91CD\u8BD5`);
263
+ }
264
+ mkdirSync3(dirname2(pluginPath), { recursive: true });
265
+ const tmp = pluginPath + ".deskbuddy.tmp";
266
+ writeFileSync3(tmp, ocPluginSource(ocPluginModule(hookPath), nodePath), "utf8");
267
+ renameSync2(tmp, pluginPath);
268
+ }
269
+ function uninstallOcPlugin(pluginPath) {
270
+ try {
271
+ if (readFileSync2(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
272
+ } catch {
273
+ }
274
+ }
275
+ function isOcPluginInstalled(pluginPath) {
276
+ try {
277
+ return readFileSync2(pluginPath, "utf8").includes(OC_MARKER);
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+ var OC_NOTICE = " \u5DF2\u5F00\u7740\u7684 opencode \u7A97\u53E3\u91CD\u5F00\u540E\u751F\u6548";
283
+
284
+ // 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";
286
+ import { join as join4 } from "path";
287
+
288
+ // src/core/process.ts
93
289
  import { execFileSync } from "child_process";
290
+ function processName(pid) {
291
+ try {
292
+ const out = execFileSync("ps", ["-p", String(pid), "-o", "comm="], {
293
+ encoding: "utf8",
294
+ stdio: ["ignore", "pipe", "ignore"]
295
+ });
296
+ const name = out.trim();
297
+ return name === "" ? null : name;
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+ function isAlive(pid, expectName) {
303
+ if (!Number.isFinite(pid) || pid <= 0) return false;
304
+ try {
305
+ process.kill(pid, 0);
306
+ } catch {
307
+ return false;
308
+ }
309
+ if (!expectName) return true;
310
+ const name = processName(pid);
311
+ return name != null && name.toLowerCase().includes(expectName.toLowerCase());
312
+ }
313
+
314
+ // src/core/state.ts
315
+ function stateDir() {
316
+ return join4(deskbuddyHome(), "state");
317
+ }
318
+ function statePath(sessionId) {
319
+ const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
320
+ return join4(stateDir(), `${safe || "unknown"}.json`);
321
+ }
322
+ function readState(sessionId) {
323
+ try {
324
+ return JSON.parse(readFileSync3(statePath(sessionId), "utf8"));
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ function removeState(sessionId) {
330
+ try {
331
+ rmSync2(statePath(sessionId), { force: true });
332
+ } catch {
333
+ }
334
+ }
335
+ function readAliveStates(tool) {
336
+ if (!existsSync4(stateDir())) return [];
337
+ const alive = [];
338
+ for (const file of readdirSync(stateDir())) {
339
+ if (!file.endsWith(".json")) continue;
340
+ let s;
341
+ try {
342
+ s = JSON.parse(readFileSync3(join4(stateDir(), file), "utf8"));
343
+ } catch {
344
+ rmSync2(join4(stateDir(), file), { force: true });
345
+ continue;
346
+ }
347
+ if (s.tool !== tool) continue;
348
+ if (s.pid && !isAlive(s.pid, s.pidName)) {
349
+ removeState(s.sessionId);
350
+ continue;
351
+ }
352
+ alive.push(s);
353
+ }
354
+ return alive;
355
+ }
356
+ function noticeDir() {
357
+ return join4(deskbuddyHome(), "notice");
358
+ }
359
+ function writeNotice(sessionId, text, now = Date.now()) {
360
+ try {
361
+ const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
362
+ if (!safe) return;
363
+ mkdirSync4(noticeDir(), { recursive: true });
364
+ const file = join4(noticeDir(), `${safe}.json`);
365
+ writeFileSync4(file + ".tmp", JSON.stringify({ text, at: now }), "utf8");
366
+ renameSync3(file + ".tmp", file);
367
+ } catch {
368
+ }
369
+ }
370
+ var NOTICE_TTL_MS = 60 * 60 * 1e3;
371
+ var ALIAS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
372
+ function resolveAlias(sessionId, now = Date.now()) {
373
+ let map = {};
374
+ try {
375
+ map = JSON.parse(readFileSync3(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
376
+ } catch {
377
+ return sessionId;
378
+ }
379
+ let cur = sessionId;
380
+ for (let i = 0; i < 5; i++) {
381
+ const a = map[cur];
382
+ if (!a || typeof a.to !== "string" || !a.to || typeof a.at !== "number" || now - a.at >= ALIAS_TTL_MS) break;
383
+ cur = a.to;
384
+ }
385
+ return cur;
386
+ }
387
+
388
+ // src/core/api.ts
389
+ import { execFileSync as execFileSync2 } from "child_process";
94
390
  import { createHash, randomBytes, randomUUID as randomUUID2 } from "crypto";
95
- import { readFileSync as readFileSync3 } from "fs";
391
+ import { readFileSync as readFileSync5 } from "fs";
96
392
  import { release } from "os";
97
393
 
98
394
  // src/core/hookFile.ts
99
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
395
+ import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
100
396
  import { createRequire } from "module";
101
- import { dirname, join as join2 } from "path";
102
- import { fileURLToPath } from "url";
103
- var here = dirname(fileURLToPath(import.meta.url));
397
+ import { dirname as dirname3, join as join5 } from "path";
398
+ import { fileURLToPath as fileURLToPath2 } from "url";
399
+ var here = dirname3(fileURLToPath2(import.meta.url));
104
400
  function packagedHookPath() {
105
401
  const candidates = [
106
- join2(here, "hook.cjs"),
402
+ join5(here, "hook.cjs"),
107
403
  // dist/cli.js 旁边(发布形态)
108
- join2(here, "..", "hook", "hook.cjs"),
404
+ join5(here, "..", "hook", "hook.cjs"),
109
405
  // dist/../hook/
110
- join2(here, "..", "..", "hook", "hook.cjs")
406
+ join5(here, "..", "..", "hook", "hook.cjs")
111
407
  // src/core/../../hook/(开发形态)
112
408
  ];
113
409
  for (const c of candidates) {
114
- if (existsSync2(c)) return c;
410
+ if (existsSync5(c)) return c;
115
411
  }
116
412
  throw new Error("\u627E\u4E0D\u5230\u5305\u5185\u7684 hook.cjs\uFF0C\u5B89\u88C5\u5305\u53EF\u80FD\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5 deskbuddy");
117
413
  }
@@ -127,23 +423,41 @@ function packageVersion() {
127
423
  return "0.0.0-dev";
128
424
  }
129
425
  function hookVersionPath(target = installedHookPath()) {
130
- return join2(dirname(target), "hook-version");
426
+ return join5(dirname3(target), "hook-version");
131
427
  }
132
428
  function installedHookVersion(target = installedHookPath()) {
133
429
  try {
134
- return readFileSync2(hookVersionPath(target), "utf8").trim() || null;
430
+ return readFileSync4(hookVersionPath(target), "utf8").trim() || null;
135
431
  } catch {
136
432
  return null;
137
433
  }
138
434
  }
435
+ function atomicCopy(from, to) {
436
+ const tmp = `${to}.${process.pid}.tmp`;
437
+ copyFileSync(from, tmp);
438
+ renameSync4(tmp, to);
439
+ }
139
440
  function syncHookFile(opts = {}) {
140
441
  const target = opts.target ?? installedHookPath();
141
442
  const version = opts.version ?? packageVersion();
142
- if (installedHookVersion(target) === version && existsSync2(target)) return false;
443
+ const SIBLINGS = ["gate.cjs", "opencode.mjs"];
444
+ const siblingMissing = () => SIBLINGS.some((name) => {
445
+ if (existsSync5(join5(dirname3(target), name))) return false;
446
+ try {
447
+ return existsSync5(join5(dirname3(opts.source ?? packagedHookPath()), name));
448
+ } catch {
449
+ return false;
450
+ }
451
+ });
452
+ if (installedHookVersion(target) === version && existsSync5(target) && !siblingMissing()) return false;
143
453
  const source = opts.source ?? packagedHookPath();
144
- mkdirSync2(dirname(target), { recursive: true });
145
- copyFileSync(source, target);
146
- writeFileSync2(hookVersionPath(target), version, "utf8");
454
+ mkdirSync5(dirname3(target), { recursive: true });
455
+ atomicCopy(source, target);
456
+ for (const name of SIBLINGS) {
457
+ const from = join5(dirname3(source), name);
458
+ if (existsSync5(from)) atomicCopy(from, join5(dirname3(target), name));
459
+ }
460
+ writeFileSync5(hookVersionPath(target), version, "utf8");
147
461
  return true;
148
462
  }
149
463
 
@@ -156,6 +470,12 @@ var SERVICES = {
156
470
  /** App 扫码确认时调的,CLI 用不到,列在这里只为对照 */
157
471
  confirmAuth: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyconfirmauthapi"
158
472
  };
473
+ var DIRS_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyclientdirsreportapi";
474
+ var APPROVAL_SERVICES = {
475
+ create: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcreateapi",
476
+ cancel: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcancelapi",
477
+ sync: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalsyncapi"
478
+ };
159
479
  var APP_ID = process.env.DESKBUDDY_APPID || "deskbuddy-cli";
160
480
  var APP_SECRET = process.env.DESKBUDDY_APPSECRET || "3aeb6e0afc144e188dd3ede5c7865643";
161
481
  var GATEWAY_VERSION = "1.0";
@@ -217,12 +537,12 @@ function humanOs(platform = process.platform) {
217
537
  return known[platform] ?? platform;
218
538
  }
219
539
  var defaultOsVersionDeps = {
220
- runSwVers: () => execFileSync("sw_vers", ["-productVersion"], {
540
+ runSwVers: () => execFileSync2("sw_vers", ["-productVersion"], {
221
541
  encoding: "utf8",
222
542
  stdio: ["ignore", "pipe", "ignore"],
223
543
  timeout: 2e3
224
544
  }),
225
- readOsRelease: () => readFileSync3("/etc/os-release", "utf8"),
545
+ readOsRelease: () => readFileSync5("/etc/os-release", "utf8"),
226
546
  kernelRelease: () => release()
227
547
  };
228
548
  function productOsVersion(platform = process.platform, deps = defaultOsVersionDeps) {
@@ -237,57 +557,2391 @@ function productOsVersion(platform = process.platform, deps = defaultOsVersionDe
237
557
  if (v) return v;
238
558
  }
239
559
  }
240
- } catch {
560
+ } catch {
561
+ }
562
+ try {
563
+ return deps.kernelRelease();
564
+ } catch {
565
+ return "";
566
+ }
567
+ }
568
+ var machineInfoCache = null;
569
+ function machineInfo() {
570
+ if (!machineInfoCache) {
571
+ machineInfoCache = {
572
+ os: humanOs(),
573
+ osVersion: productOsVersion(),
574
+ arch: process.arch,
575
+ cliVersion: packageVersion()
576
+ };
577
+ }
578
+ return machineInfoCache;
579
+ }
580
+ async function call(service, data, timeoutMs = DEFAULT_TIMEOUT_MS) {
581
+ const cfg = loadConfig();
582
+ const body = buildGatewayBody(service, data);
583
+ const controller = new AbortController();
584
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
585
+ try {
586
+ const res = await fetch(`${cfg.apiBase}/gateway`, {
587
+ method: "POST",
588
+ headers: buildHeaders(cfg, body),
589
+ body,
590
+ // 与签名用的是同一个字符串
591
+ signal: controller.signal
592
+ });
593
+ const json = await res.json();
594
+ if (!isSuccessCode(json?.code)) {
595
+ throw new ApiError(String(json?.code), json?.message ?? "");
596
+ }
597
+ return json?.data;
598
+ } finally {
599
+ clearTimeout(timer);
600
+ }
601
+ }
602
+ var api = {
603
+ authCode: (clientId, hostName) => call(SERVICES.authCode, { clientId, hostName, ...machineInfo() }),
604
+ authToken: (authCode) => call(SERVICES.authToken, { authCode }),
605
+ toolRegistry: () => call(SERVICES.toolRegistry, {}),
606
+ // 🔴 2 秒超时:这条调用发生在 hook 链路上,慢一点就是拖慢用户的 agent
607
+ report: (payload) => call(SERVICES.sessionReport, payload, 2e3),
608
+ approvalCreate: (payload) => call(APPROVAL_SERVICES.create, payload),
609
+ approvalCancel: (approvalId, reason) => call(APPROVAL_SERVICES.cancel, { approvalId, reason }),
610
+ approvalSync: (approvalIds) => call(APPROVAL_SERVICES.sync, { approvalIds }),
611
+ dirsReport: (payload) => call(DIRS_SERVICE, payload)
612
+ };
613
+
614
+ // src/core/report.ts
615
+ async function postReport(payload) {
616
+ try {
617
+ await api.report(payload);
618
+ } catch {
619
+ }
620
+ }
621
+
622
+ // src/core/snapshot.ts
623
+ import { basename as basename2 } from "path";
624
+ var KEEPALIVE_MS = 2 * 60 * 1e3;
625
+ var MAX_SESSIONS = 20;
626
+ function buildSnapshot(states, tool, cfg, nowMs) {
627
+ const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => ({
628
+ sessionId: s.sessionId,
629
+ title: s.title,
630
+ // 隐私:只带目录名,不带全路径——全路径会暴露公司目录结构与用户名
631
+ project: basename2(s.project || ""),
632
+ state: s.state,
633
+ startedAt: s.startedAt,
634
+ updatedAt: s.updatedAt,
635
+ interactive: s.interactive === true
636
+ }));
637
+ return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions };
638
+ }
639
+
640
+ // src/core/dirs.ts
641
+ import { createHmac, randomBytes as randomBytes2 } from "crypto";
642
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
643
+ import { basename as basename3, join as join6 } from "path";
644
+ var DIRS_REPORT_MAX = 10;
645
+ function dirsPath() {
646
+ return join6(deskbuddyHome(), "dirs.json");
647
+ }
648
+ function dirSecret() {
649
+ const file = join6(deskbuddyHome(), "dir-secret");
650
+ try {
651
+ const s2 = readFileSync6(file, "utf8").trim();
652
+ if (s2) return s2;
653
+ } catch {
654
+ }
655
+ const s = randomBytes2(32).toString("hex");
656
+ try {
657
+ mkdirSync6(deskbuddyHome(), { recursive: true });
658
+ writeFileSync6(file, s, { encoding: "utf8", mode: 384 });
659
+ chmodSync(file, 384);
660
+ } catch {
661
+ }
662
+ return s;
663
+ }
664
+ function dirRef(path) {
665
+ return createHmac("sha256", dirSecret()).update(path).digest("hex").slice(0, 16);
666
+ }
667
+ function readMap() {
668
+ try {
669
+ const m = JSON.parse(readFileSync6(dirsPath(), "utf8"));
670
+ const out = {};
671
+ for (const [k, v] of Object.entries(m ?? {})) if (typeof v === "number") out[k] = v;
672
+ return out;
673
+ } catch {
674
+ return {};
675
+ }
676
+ }
677
+ function recentDirs(limit = DIRS_REPORT_MAX, exists = existsSync6) {
678
+ const list = Object.entries(readMap()).filter(([p]) => p && exists(p)).sort((a, b) => b[1] - a[1]).slice(0, limit);
679
+ const counts = /* @__PURE__ */ new Map();
680
+ for (const [p] of list) counts.set(basename3(p), (counts.get(basename3(p)) ?? 0) + 1);
681
+ return list.map(([p, at]) => {
682
+ const ref = dirRef(p);
683
+ const name = basename3(p) || "/";
684
+ return { path: p, ref, name: (counts.get(basename3(p)) ?? 0) > 1 ? `${name} \xB7 ${ref.slice(0, 4)}` : name, lastUsedAt: at };
685
+ });
686
+ }
687
+ function dirOf(ref, exists = existsSync6) {
688
+ for (const p of Object.keys(readMap())) if (dirRef(p) === ref && exists(p)) return p;
689
+ return "";
690
+ }
691
+
692
+ // src/daemon/backoff.ts
693
+ var BASE_DELAY_MS = 1e3;
694
+ var MAX_DELAY_MS = 6e4;
695
+ var JITTER_RATIO = 0.2;
696
+ function backoffDelay(attempt, rand = Math.random) {
697
+ const n = Math.max(1, Math.floor(attempt));
698
+ const base = Math.min(BASE_DELAY_MS * 2 ** (n - 1), MAX_DELAY_MS);
699
+ const jitter = base * JITTER_RATIO * (rand() * 2 - 1);
700
+ return Math.max(100, Math.round(base + jitter));
701
+ }
702
+
703
+ // src/daemon/command.ts
704
+ var SERVICE_COMMAND = "deskbuddy.command";
705
+ var TYPE_TEXT = "text";
706
+ var MAX_TEXT_LENGTH = 2e3;
707
+ var MAX_ID_LENGTH = 64;
708
+ function str(v) {
709
+ return typeof v === "string" ? v : "";
710
+ }
711
+ function parseMillis(v) {
712
+ if (typeof v === "number" && Number.isFinite(v)) return v;
713
+ const s = str(v).trim();
714
+ if (!/^\d{1,16}$/.test(s)) return 0;
715
+ const n = Number(s);
716
+ return Number.isSafeInteger(n) ? n : 0;
717
+ }
718
+ function isValidId(v) {
719
+ return v.length > 0 && v.length <= MAX_ID_LENGTH && /^[0-9a-zA-Z_-]+$/.test(v);
720
+ }
721
+ function sanitizeText(raw) {
722
+ return raw.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "").trim();
723
+ }
724
+ function parseCommand(frame) {
725
+ const f = frame;
726
+ if (!f || str(f.service) !== SERVICE_COMMAND) return null;
727
+ const d = f.data;
728
+ if (!d || typeof d !== "object") return null;
729
+ const instructionId = str(d.instructionId);
730
+ if (!isValidId(instructionId)) return null;
731
+ const type = str(d.type);
732
+ if (type !== TYPE_TEXT) return null;
733
+ const tool = str(d.tool);
734
+ if (!isValidId(tool)) return null;
735
+ const sessionId = str(d.sessionId);
736
+ if (sessionId && !isValidId(sessionId)) return null;
737
+ const text = sanitizeText(str(d.text));
738
+ if (!text || text.length > MAX_TEXT_LENGTH) return null;
739
+ const rawDir = str(d.dirRef);
740
+ const dirRef2 = !sessionId && /^[0-9a-f]{16}$/.test(rawDir) ? rawDir : "";
741
+ return { instructionId, type, sessionId, tool, text, expireAt: parseMillis(d.expireAt), dirRef: dirRef2 };
742
+ }
743
+ var TYPE_APPROVAL = "approval";
744
+ function parseApprovalFrame(frame) {
745
+ const f = frame;
746
+ if (!f || str(f.service) !== SERVICE_COMMAND) return null;
747
+ const d = f.data;
748
+ if (!d || typeof d !== "object" || str(d.type) !== TYPE_APPROVAL) return null;
749
+ const instructionId = str(d.instructionId);
750
+ if (!isValidId(instructionId)) return null;
751
+ const approvalId = str(d.approvalId);
752
+ if (!isValidId(approvalId)) return null;
753
+ const decision = str(d.decision);
754
+ if (decision !== "allow" && decision !== "deny" && decision !== "allow_always") return null;
755
+ return { instructionId, approvalId, decision, deviceName: cleanDeviceName(d.deviceName) };
756
+ }
757
+ function cleanDeviceName(v) {
758
+ return typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 32) : "";
759
+ }
760
+ function isExpired(cmd, now = Date.now()) {
761
+ return cmd.expireAt > 0 && now > cmd.expireAt;
762
+ }
763
+
764
+ // src/daemon/executor.ts
765
+ import { execFile as execFile2 } from "child_process";
766
+ import { homedir as homedir4 } from "os";
767
+ import { join as join8 } from "path";
768
+
769
+ // src/daemon/gate/settings.ts
770
+ import { existsSync as existsSync7 } from "fs";
771
+ import { fileURLToPath as fileURLToPath3 } from "url";
772
+ var GATE_HOOK_TIMEOUT_S = 7 * 24 * 60 * 60;
773
+ function waitModeFor(tool) {
774
+ return tool === "claude_code" ? "defer" : "hold";
775
+ }
776
+ function shQuote(s) {
777
+ return `'${s.replace(/'/g, `'\\''`)}'`;
778
+ }
779
+ function defaultGateScript() {
780
+ const candidates = [
781
+ fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
782
+ fileURLToPath3(new URL("../../../hook/gate.cjs", import.meta.url))
783
+ ];
784
+ return candidates.find((p) => existsSync7(p)) ?? candidates[0];
785
+ }
786
+ function gateCommand(t) {
787
+ return `${shQuote(t.nodePath)} ${shQuote(t.gateScript)} --run ${t.runId} || exit 2`;
788
+ }
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
+ });
797
+ }
798
+ function codexGateArgs(t) {
799
+ const hook = `hooks.PreToolUse=[{matcher="*",hooks=[{type="command",command=${JSON.stringify(gateCommand(t))},timeout=${GATE_HOOK_TIMEOUT_S}}]}]`;
800
+ return [
801
+ "--dangerously-bypass-hook-trust",
802
+ "-c",
803
+ 'sandbox_mode="workspace-write"',
804
+ "-c",
805
+ "sandbox_workspace_write.network_access=true",
806
+ "-c",
807
+ hook
808
+ ];
809
+ }
810
+
811
+ // src/daemon/locate.ts
812
+ import { accessSync, constants, existsSync as existsSync8, statSync } from "fs";
813
+ import { homedir as homedir3 } from "os";
814
+ import { delimiter, dirname as dirname4, join as join7 } from "path";
815
+ var defaultLocateDeps = () => ({
816
+ home: homedir3(),
817
+ path: process.env.PATH ?? "",
818
+ nodePath: process.execPath,
819
+ isExecutable: (p) => {
820
+ try {
821
+ if (!statSync(p).isFile()) return false;
822
+ accessSync(p, constants.X_OK);
823
+ return true;
824
+ } catch {
825
+ return false;
826
+ }
827
+ },
828
+ exists: (p) => existsSync8(p)
829
+ });
830
+ function extraBinDirs(home, nodePath = "") {
831
+ return [
832
+ join7(home, ".local", "bin"),
833
+ join7(home, ".claude", "local"),
834
+ join7(home, ".npm-global", "bin"),
835
+ join7(home, ".bun", "bin"),
836
+ join7(home, ".volta", "bin"),
837
+ "/opt/homebrew/bin",
838
+ "/usr/local/bin",
839
+ ...nodePath ? [dirname4(nodePath)] : []
840
+ ];
841
+ }
842
+ 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;
847
+ }
848
+ return null;
849
+ }
850
+ function appDirs(home) {
851
+ return ["/Applications", join7(home, "Applications")];
852
+ }
853
+ var WORKBUDDY_CLI_IN_APP = "WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy";
854
+ 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
+ function locate(tool, d = defaultLocateDeps()) {
856
+ const plain = (name) => {
857
+ const f = whichIn(name, d);
858
+ return f ? { file: f, prefixArgs: [] } : null;
859
+ };
860
+ switch (tool) {
861
+ case "claude_code":
862
+ return plain("claude");
863
+ case "codex":
864
+ return plain("codex");
865
+ case "workbuddy": {
866
+ for (const dir of appDirs(d.home)) {
867
+ const p = join7(dir, WORKBUDDY_CLI_IN_APP);
868
+ if (d.isExecutable(p)) return { file: p, prefixArgs: [] };
869
+ }
870
+ return plain("codebuddy");
871
+ }
872
+ case "qoder": {
873
+ for (const dir of appDirs(d.home)) {
874
+ const p = join7(dir, QODER_CN_WORKER_IN_APP);
875
+ if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
876
+ }
877
+ return plain("qodercli");
878
+ }
879
+ case "opencode": {
880
+ const p = join7(d.home, ".opencode", "bin", "opencode");
881
+ if (d.isExecutable(p)) return { file: p, prefixArgs: [] };
882
+ return plain("opencode");
883
+ }
884
+ default:
885
+ return null;
886
+ }
887
+ }
888
+ function childPath(d = defaultLocateDeps()) {
889
+ 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);
891
+ }
892
+
893
+ // src/daemon/executor.ts
894
+ var EXEC_TIMEOUT_MS = 10 * 60 * 1e3;
895
+ var EXEC_MAX_BUFFER = 32 * 1024 * 1024;
896
+ function claudePlan(cmd, gate) {
897
+ const args = [];
898
+ if (cmd.sessionId) args.push("--resume", cmd.sessionId);
899
+ args.push("-p");
900
+ if (cmd.text) args.push(cmd.text);
901
+ args.push("--output-format", "json");
902
+ if (gate) args.push("--settings", ccSettingsJson(gate));
903
+ return args;
904
+ }
905
+ function codexPlan(cmd, gate) {
906
+ const args = ["exec"];
907
+ if (cmd.sessionId) args.push("resume", cmd.sessionId);
908
+ args.push("--json", "--skip-git-repo-check");
909
+ if (gate) args.push(...codexGateArgs(gate));
910
+ args.push(cmd.text);
911
+ return args;
912
+ }
913
+ function workbuddyPlan(cmd, gate) {
914
+ const args = [];
915
+ if (cmd.sessionId) args.push("-r", cmd.sessionId);
916
+ args.push("-p", cmd.text, "--output-format", "json");
917
+ if (gate) args.push("--settings", ccSettingsJson(gate));
918
+ return args;
919
+ }
920
+ function qoderPlan(cmd, gate) {
921
+ const args = [];
922
+ if (cmd.sessionId) args.push("-r", cmd.sessionId);
923
+ args.push("-p", cmd.text, "-o", "json", "--permission-mode", "bypass_permissions");
924
+ args.push("--settings", ccSettingsJson(gate));
925
+ return args;
926
+ }
927
+ var OPENCODE_GATED_KEYS = [
928
+ "*",
929
+ "bash",
930
+ "edit",
931
+ "write",
932
+ "read",
933
+ "glob",
934
+ "grep",
935
+ "list",
936
+ "task",
937
+ "webfetch",
938
+ "websearch",
939
+ "codesearch",
940
+ "external_directory",
941
+ "todowrite",
942
+ "todoread",
943
+ "lsp",
944
+ "skill",
945
+ "doom_loop",
946
+ "patch",
947
+ "apply_patch"
948
+ ];
949
+ var DENY_RULES = Object.fromEntries(OPENCODE_GATED_KEYS.map((k) => [k, "deny"]));
950
+ var OPENCODE_DENY_ALL = JSON.stringify({ permission: DENY_RULES, agent: { build: { permission: DENY_RULES } } });
951
+ function opencodePlan(cmd) {
952
+ const args = ["run", "--format", "json", "--agent", "build"];
953
+ if (cmd.sessionId) args.push("-s", cmd.sessionId);
954
+ args.push("--", cmd.text);
955
+ return args;
956
+ }
957
+ var TOOL_BINARY = {
958
+ claude_code: "claude",
959
+ codex: "codex",
960
+ workbuddy: "codebuddy",
961
+ qoder: "qodercli",
962
+ opencode: "opencode"
963
+ };
964
+ function buildSpawnPlan(cmd, cwd, gate, home = homedir4()) {
965
+ const file = TOOL_BINARY[cmd.tool];
966
+ if (!file) return null;
967
+ switch (cmd.tool) {
968
+ case "codex":
969
+ return { file, args: codexPlan(cmd, gate), cwd };
970
+ case "workbuddy":
971
+ return { file, args: workbuddyPlan(cmd, gate), cwd, env: { CODEBUDDY_CONFIG_DIR: join8(home, ".workbuddy") } };
972
+ case "qoder":
973
+ return gate ? { file, args: qoderPlan(cmd, gate), cwd } : null;
974
+ case "opencode":
975
+ return gate ? {
976
+ file,
977
+ args: opencodePlan(cmd),
978
+ cwd,
979
+ env: { DESKBUDDY_GATE_RUN: gate.runId, DESKBUDDY_NODE: gate.nodePath, OPENCODE_CONFIG_CONTENT: OPENCODE_DENY_ALL }
980
+ } : null;
981
+ default:
982
+ return { file, args: claudePlan(cmd, gate), cwd };
983
+ }
984
+ }
985
+ function materialize(plan, tool, d = defaultLocateDeps()) {
986
+ const loc = locate(tool, d);
987
+ if (!loc) return null;
988
+ return {
989
+ file: loc.file,
990
+ args: [...loc.prefixArgs, ...plan.args],
991
+ cwd: plan.cwd,
992
+ env: { ...plan.env, PATH: childPath(d) }
993
+ };
994
+ }
995
+ function isDeferred(stdout6) {
996
+ try {
997
+ return JSON.parse(stdout6).stop_reason === "tool_deferred";
998
+ } catch {
999
+ return false;
1000
+ }
1001
+ }
1002
+ function parseClaudeSessionId(stdout6) {
1003
+ const pick = (o) => {
1004
+ const d = o ?? {};
1005
+ const v = d.session_id ?? d.sessionId;
1006
+ return typeof v === "string" && v ? v : null;
1007
+ };
1008
+ try {
1009
+ const d = JSON.parse(stdout6);
1010
+ if (Array.isArray(d)) {
1011
+ for (const m of d) {
1012
+ const id = pick(m);
1013
+ if (id) return id;
1014
+ }
1015
+ return null;
1016
+ }
1017
+ return pick(d);
1018
+ } catch {
1019
+ return null;
1020
+ }
1021
+ }
1022
+ function parseCodexSessionId(stdout6) {
1023
+ for (const line of stdout6.split("\n")) {
1024
+ const t = line.trim();
1025
+ if (!t) continue;
1026
+ try {
1027
+ const d = JSON.parse(t);
1028
+ if (d.type === "thread.started" && typeof d.thread_id === "string" && d.thread_id) {
1029
+ return d.thread_id;
1030
+ }
1031
+ } catch {
1032
+ continue;
1033
+ }
1034
+ }
1035
+ return null;
1036
+ }
1037
+ function parseOpencodeSessionId(stdout6) {
1038
+ for (const line of stdout6.split("\n")) {
1039
+ const t = line.trim();
1040
+ if (!t) continue;
1041
+ try {
1042
+ const d = JSON.parse(t);
1043
+ const v = d.sessionID ?? d.part?.sessionID;
1044
+ if (typeof v === "string" && /^ses_[0-9A-Za-z]{1,64}$/.test(v)) return v;
1045
+ } catch {
1046
+ continue;
1047
+ }
1048
+ }
1049
+ return null;
1050
+ }
1051
+ function parseSessionId(tool, stdout6) {
1052
+ if (tool === "codex") return parseCodexSessionId(stdout6);
1053
+ if (tool === "opencode") return parseOpencodeSessionId(stdout6);
1054
+ return parseClaudeSessionId(stdout6);
1055
+ }
1056
+ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
1057
+ let remaining = limitMs;
1058
+ let startedAt = Date.now();
1059
+ let paused = 0;
1060
+ let timer = null;
1061
+ let finished = false;
1062
+ let child = null;
1063
+ const arm = () => {
1064
+ startedAt = Date.now();
1065
+ timer = setTimeout(() => {
1066
+ timedOut = true;
1067
+ child?.kill("SIGTERM");
1068
+ }, Math.max(0, remaining));
1069
+ };
1070
+ let timedOut = false;
1071
+ const done = new Promise((resolve3) => {
1072
+ child = execFile2(
1073
+ plan.file,
1074
+ plan.args,
1075
+ {
1076
+ cwd: plan.cwd,
1077
+ maxBuffer: EXEC_MAX_BUFFER,
1078
+ // agent 自己要读 PATH 里的东西,环境原样继承
1079
+ env: { ...process.env, ...plan.env }
1080
+ },
1081
+ (err, stdout6) => {
1082
+ finished = true;
1083
+ if (timer) clearTimeout(timer);
1084
+ const out = String(stdout6 ?? "");
1085
+ if (err || timedOut) {
1086
+ 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 });
1088
+ return;
1089
+ }
1090
+ resolve3({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
1091
+ }
1092
+ );
1093
+ child?.stdin?.end();
1094
+ arm();
1095
+ });
1096
+ return {
1097
+ done,
1098
+ pauseTimeout() {
1099
+ if (finished) return;
1100
+ if (paused++ === 0 && timer) {
1101
+ clearTimeout(timer);
1102
+ timer = null;
1103
+ remaining -= Date.now() - startedAt;
1104
+ }
1105
+ },
1106
+ resumeTimeout() {
1107
+ if (finished || paused === 0) return;
1108
+ if (--paused === 0) arm();
1109
+ }
1110
+ };
1111
+ }
1112
+
1113
+ // src/daemon/gate/approvals.ts
1114
+ import { createHash as createHash2 } from "crypto";
1115
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
1116
+ import { join as join9 } from "path";
1117
+ var RETAIN_MS = 7 * 24 * 60 * 60 * 1e3;
1118
+ function fingerprint(toolName, toolInput) {
1119
+ let input = "";
1120
+ try {
1121
+ input = JSON.stringify(toolInput ?? null);
1122
+ } catch {
1123
+ input = String(toolInput);
1124
+ }
1125
+ return createHash2("sha256").update(toolName).update("\0").update(input).digest("hex");
1126
+ }
1127
+ function storePath() {
1128
+ return join9(deskbuddyHome(), "approvals.json");
1129
+ }
1130
+ var ApprovalStore = class _ApprovalStore {
1131
+ #records;
1132
+ constructor(records = []) {
1133
+ this.#records = records;
1134
+ }
1135
+ static load() {
1136
+ try {
1137
+ const raw = JSON.parse(readFileSync7(storePath(), "utf8"));
1138
+ return new _ApprovalStore(Array.isArray(raw) ? raw : []);
1139
+ } catch {
1140
+ return new _ApprovalStore();
1141
+ }
1142
+ }
1143
+ save() {
1144
+ try {
1145
+ mkdirSync7(deskbuddyHome(), { recursive: true });
1146
+ const tmp = storePath() + ".tmp";
1147
+ writeFileSync7(tmp, JSON.stringify(this.#records), { encoding: "utf8", mode: 384 });
1148
+ renameSync5(tmp, storePath());
1149
+ } catch {
1150
+ }
1151
+ }
1152
+ add(r) {
1153
+ this.#records.push(r);
1154
+ }
1155
+ get(approvalId) {
1156
+ return this.#records.find((r) => r.approvalId === approvalId);
1157
+ }
1158
+ all() {
1159
+ return this.#records;
1160
+ }
1161
+ pending() {
1162
+ return this.#records.filter((r) => r.status === "pending");
1163
+ }
1164
+ pendingForSession(sessionId) {
1165
+ return sessionId ? this.#records.filter((r) => r.status === "pending" && r.sessionId === sessionId) : [];
1166
+ }
1167
+ /**
1168
+ * 同会话、同指纹、还没被用掉的最新一条。gate 在 defer 续跑时靠它决定放行还是拒绝。
1169
+ * 取最新的:同一条命令被拒过一次、用户后来又让它跑并批准了,应当以后者为准。
1170
+ */
1171
+ latestFor(sessionId, fp) {
1172
+ for (let i = this.#records.length - 1; i >= 0; i--) {
1173
+ const r = this.#records[i];
1174
+ if (r.mode === "window") continue;
1175
+ if (r.sessionId === sessionId && r.fingerprint === fp && !r.consumed) return r;
1176
+ }
1177
+ return void 0;
1178
+ }
1179
+ /** 清掉超期的。pending 超期也清 —— 服务端那边同样视为过期了 */
1180
+ prune(now = Date.now()) {
1181
+ const before = this.#records.length;
1182
+ this.#records = this.#records.filter((r) => now - r.createdAt < RETAIN_MS);
1183
+ return before - this.#records.length;
1184
+ }
1185
+ };
1186
+
1187
+ // src/daemon/gate/backend.ts
1188
+ import { basename as basename4 } from "path";
1189
+ function serverBackend(client = api) {
1190
+ return {
1191
+ create: (r) => client.approvalCreate({
1192
+ approvalId: r.approvalId,
1193
+ tool: r.tool,
1194
+ sessionId: r.sessionId,
1195
+ kind: r.kind,
1196
+ summary: r.summary,
1197
+ cwdName: basename4(r.cwd),
1198
+ source: r.mode === "window" ? "window" : "remote",
1199
+ ...r.remember ? { remember: r.remember } : {}
1200
+ }),
1201
+ cancel: (approvalId, reason) => client.approvalCancel(approvalId, reason),
1202
+ sync: async (ids) => {
1203
+ const res = await client.approvalSync(ids);
1204
+ const out = [];
1205
+ for (const a of res?.approvals ?? []) {
1206
+ if (typeof a?.approvalId !== "string") continue;
1207
+ const d = mapStatus(a.status);
1208
+ if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName) });
1209
+ }
1210
+ return out;
1211
+ }
1212
+ };
1213
+ }
1214
+ function mapStatus(s) {
1215
+ switch (s) {
1216
+ case "APPROVED":
1217
+ return "allow";
1218
+ case "DENIED":
1219
+ return "deny";
1220
+ case "CANCELLED":
1221
+ return "cancelled";
1222
+ default:
1223
+ return null;
1224
+ }
1225
+ }
1226
+
1227
+ // src/daemon/gate/coordinator.ts
1228
+ import { randomUUID as randomUUID3 } from "crypto";
1229
+ import { existsSync as existsSync9, realpathSync } from "fs";
1230
+ import { basename as basename5, dirname as dirname5, join as join10 } from "path";
1231
+
1232
+ // src/daemon/gate/classify.ts
1233
+ import { isAbsolute, relative, resolve, sep } from "path";
1234
+ var SUMMARY_MAX = 200;
1235
+ var READ_TOOLS = /* @__PURE__ */ new Set([
1236
+ "Read",
1237
+ "Glob",
1238
+ "Grep",
1239
+ "LS",
1240
+ "NotebookRead",
1241
+ "TodoWrite",
1242
+ // Task 起的子智能体,它自己的每次工具调用还会再过一遍本关卡
1243
+ "Task",
1244
+ "WebSearch",
1245
+ "WebFetch"
1246
+ ]);
1247
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
1248
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash", "shell", "exec_command"]);
1249
+ var PATCH_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
1250
+ function str2(v) {
1251
+ return typeof v === "string" ? v : "";
1252
+ }
1253
+ function truncate(s, max = SUMMARY_MAX) {
1254
+ const oneLine = s.replace(/\s+/g, " ").trim();
1255
+ if (oneLine.length <= max) return oneLine;
1256
+ const keep = max - 3;
1257
+ const head = Math.ceil(keep * 0.55);
1258
+ return `${oneLine.slice(0, head)} \u2026 ${oneLine.slice(oneLine.length - (keep - head))}`;
1259
+ }
1260
+ var PROTECTED_DIRS = /* @__PURE__ */ new Set([
1261
+ ".git",
1262
+ ".claude",
1263
+ ".codex",
1264
+ ".qoder",
1265
+ ".qoder-cn",
1266
+ ".codebuddy",
1267
+ ".workbuddy",
1268
+ ".vscode",
1269
+ ".idea",
1270
+ ".husky"
1271
+ ]);
1272
+ var PROTECTED_FILES = /* @__PURE__ */ new Set([".envrc", ".mcp.json", ".gitmodules", ".gitattributes"]);
1273
+ function isProtectedPath(cwd, p) {
1274
+ const segs = resolve(cwd || "/", p).split(sep).filter(Boolean).map((s) => s.toLowerCase());
1275
+ if (segs.some((s) => PROTECTED_DIRS.has(s))) return true;
1276
+ return PROTECTED_FILES.has(segs[segs.length - 1] ?? "");
1277
+ }
1278
+ function isInside(cwd, p) {
1279
+ if (!cwd || !p) return false;
1280
+ const rel = relative(resolve(cwd), resolve(cwd, p));
1281
+ if (rel === "") return true;
1282
+ return !rel.startsWith("..") && !isAbsolute(rel);
1283
+ }
1284
+ function patchPaths(patch) {
1285
+ const out = [];
1286
+ for (const line of patch.split("\n")) {
1287
+ const m = /^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/.exec(line.trim());
1288
+ if (m) out.push(m[1].trim());
1289
+ }
1290
+ return out;
1291
+ }
1292
+ var SHELL_META = /[;&><`$()\n\r\\'"*?[\]{}~]/;
1293
+ var GIT_READ = /* @__PURE__ */ new Set(["status", "log", "diff", "show"]);
1294
+ var GIT_BRANCH_FLAGS = /* @__PURE__ */ new Set(["-a", "-v", "-vv", "-r", "--list", "--all"]);
1295
+ var FIND_DANGER = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir", "-delete", "-fprint", "-fprint0", "-fprintf", "-fls"]);
1296
+ function segmentIsReadOnly(seg) {
1297
+ const t = seg.trim().split(/\s+/).filter(Boolean);
1298
+ if (t.length === 0) return false;
1299
+ const [cmd, ...rest] = t;
1300
+ switch (cmd) {
1301
+ case "ls":
1302
+ case "cat":
1303
+ case "head":
1304
+ case "tail":
1305
+ case "wc":
1306
+ case "pwd":
1307
+ case "grep":
1308
+ return true;
1309
+ case "rg":
1310
+ return !rest.some((a) => a.startsWith("--pre") || a.startsWith("--hostname-bin"));
1311
+ case "find":
1312
+ return !rest.some((a) => FIND_DANGER.has(a));
1313
+ case "sed":
1314
+ return rest[0] === "-n" && /^[0-9]+(,[0-9]+)?p$/.test(rest[1] ?? "") && rest.slice(2).every((a) => !a.startsWith("-"));
1315
+ case "git": {
1316
+ const [sub, ...args] = rest;
1317
+ if (args.some((a) => a.startsWith("--output"))) return false;
1318
+ if (GIT_READ.has(sub)) return true;
1319
+ if (sub === "branch") return args.every((a) => GIT_BRANCH_FLAGS.has(a));
1320
+ return false;
1321
+ }
1322
+ default:
1323
+ return false;
1324
+ }
1325
+ }
1326
+ function isReadOnlyCommand(command) {
1327
+ const c = command.trim();
1328
+ if (!c || SHELL_META.test(c)) return false;
1329
+ return c.split("|").every(segmentIsReadOnly);
1330
+ }
1331
+ function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
1332
+ const roots = (Array.isArray(cwd) ? cwd : [cwd]).filter(Boolean);
1333
+ const real = opts.real;
1334
+ const inside = (p) => {
1335
+ 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));
1339
+ };
1340
+ const rel = (p) => {
1341
+ const r = roots.find((x) => isInside(x, p)) ?? roots[0] ?? "";
1342
+ return relative(resolve(r), resolve(r, p));
1343
+ };
1344
+ const input = toolInput && typeof toolInput === "object" ? toolInput : {};
1345
+ if (READ_TOOLS.has(toolName)) return { action: "auto" };
1346
+ if (SHELL_TOOLS.has(toolName)) {
1347
+ const command = str2(input.command);
1348
+ if (isReadOnlyCommand(command)) return { action: "auto" };
1349
+ return { action: "ask", kind: "command", summary: truncate(command || toolName) };
1350
+ }
1351
+ const paths = EDIT_TOOLS.has(toolName) ? [str2(input.file_path) || str2(input.notebook_path)].filter(Boolean) : PATCH_TOOLS.has(toolName) ? patchPaths(str2(input.command) || str2(input.patch) || str2(input.input)) : null;
1352
+ if (paths) {
1353
+ const allInside = paths.length > 0 && paths.every(inside);
1354
+ const touchesProtected = paths.some(
1355
+ (p) => isProtectedPath(roots[0] ?? "/", p) || (real ? isProtectedPath("/", real(resolve(roots[0] ?? "/", p))) : false)
1356
+ );
1357
+ if (policy === "commands" && allInside && !touchesProtected) return { action: "auto" };
1358
+ const shown = paths.map((p) => inside(p) ? rel(p) : p);
1359
+ return { action: "ask", kind: "edit", summary: truncate(shown.join(", ") || toolName) };
1360
+ }
1361
+ let detail = "";
1362
+ try {
1363
+ detail = JSON.stringify(input);
1364
+ } catch {
1365
+ detail = "";
1366
+ }
1367
+ return { action: "ask", kind: "other", summary: truncate(`${toolName} ${detail}`) };
1368
+ }
1369
+ function describe(toolName, toolInput, cwd) {
1370
+ const v = classify(toolName, toolInput, cwd, "all");
1371
+ if (v.action === "ask") return { kind: v.kind, summary: v.summary };
1372
+ const input = toolInput && typeof toolInput === "object" ? toolInput : {};
1373
+ const detail = str2(input.command) || str2(input.file_path) || str2(input.path) || str2(input.pattern) || str2(input.url);
1374
+ return { kind: SHELL_TOOLS.has(toolName) ? "command" : "other", summary: truncate(detail ? `${toolName} ${detail}` : toolName) };
1375
+ }
1376
+
1377
+ // src/daemon/gate/procs.ts
1378
+ import { execFileSync as execFileSync3 } from "child_process";
1379
+ function listDescendants(rootPid) {
1380
+ let out = "";
1381
+ try {
1382
+ out = execFileSync3("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
1383
+ encoding: "utf8",
1384
+ stdio: ["ignore", "pipe", "ignore"],
1385
+ maxBuffer: 16 * 1024 * 1024
1386
+ });
1387
+ } catch {
1388
+ return [];
1389
+ }
1390
+ const kids = /* @__PURE__ */ new Map();
1391
+ for (const line of out.split("\n")) {
1392
+ 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);
1398
+ }
1399
+ const res = [];
1400
+ const stack = [rootPid];
1401
+ while (stack.length > 0) {
1402
+ for (const c of kids.get(stack.pop()) ?? []) {
1403
+ res.push(c);
1404
+ stack.push(c.pid);
1405
+ }
1406
+ }
1407
+ return res;
1408
+ }
1409
+ function normalizeCmd(s) {
1410
+ return s.replace(/['"\\]/g, "").replace(/\s+/g, " ").trim();
1411
+ }
1412
+ var CMD_MATCH_MIN = 6;
1413
+ var CMD_MATCH_LEN = 80;
1414
+
1415
+ // src/daemon/gate/coordinator.ts
1416
+ function pickRememberSuggestion(suggestions) {
1417
+ if (!Array.isArray(suggestions)) return null;
1418
+ for (const s of suggestions) {
1419
+ if (!s || s.type !== "addRules" || s.behavior !== "allow" || !Array.isArray(s.rules) || s.rules.length === 0) continue;
1420
+ if (s.destination !== "session" && s.destination !== "localSettings") continue;
1421
+ const raw = s.rules;
1422
+ if (raw.some((r) => typeof r?.ruleContent !== "string" || !r.ruleContent)) continue;
1423
+ const rules = raw.map((r) => r.ruleContent);
1424
+ const scope = s.destination === "session" ? "\u8FD9\u6B21\u4F1A\u8BDD\u91CC" : "\u8FD9\u4E2A\u9879\u76EE\u91CC";
1425
+ const text = `\u4EE5\u540E\u5728${scope}\u90FD\u5141\u8BB8\uFF1A${rules.join("\u3001")}`;
1426
+ return { suggestion: s, text: text.length > 100 ? text.slice(0, 99) + "\u2026" : text };
1427
+ }
1428
+ return null;
1429
+ }
1430
+ var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
1431
+ function windowNotice(decision, summary, deviceName = "") {
1432
+ const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
1433
+ const what = summary.length > 60 ? summary.slice(0, 59) + "\u2026" : summary;
1434
+ return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
1435
+ }
1436
+ var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
1437
+ var WINDOW_SETTLED_EVENTS = /* @__PURE__ */ new Set(["UserPromptSubmit", "Stop"]);
1438
+ var WINDOW_TOOL_DONE_EVENTS = /* @__PURE__ */ new Set(["PostToolUse", "PostToolUseFailure"]);
1439
+ var WINDOW_PROC_WATCH_MS = 10 * 60 * 1e3;
1440
+ var REASON_DENIED = "\u7528\u6237\u5728\u8BBE\u5907\u4E0A\u62D2\u7EDD\u4E86\u8FD9\u4E00\u6B65";
1441
+ var REASON_SUPERSEDED = "\u7528\u6237\u5728\u8BBE\u5907\u4E0A\u4E0B\u4E86\u65B0\u7684\u6307\u4EE4\uFF0C\u6CA1\u6709\u6279\u51C6\u8FD9\u4E00\u6B65";
1442
+ var REASON_UNKNOWN_RUN = "\u8FD9\u4E0D\u662F DeskBuddy \u5B88\u62A4\u8FDB\u7A0B\u53D1\u8D77\u7684\u4EFB\u52A1";
1443
+ var ApprovalCoordinator = class {
1444
+ #d;
1445
+ #runs = /* @__PURE__ */ new Map();
1446
+ /** hold 模式下挂着的 gate 请求:approvalId → 让它返回 */
1447
+ #waiters = /* @__PURE__ */ new Map();
1448
+ /** 窗口待批准的撤卡依据:approvalId → 工具名 + 创建时状态文件在不在 */
1449
+ #windows = /* @__PURE__ */ new Map();
1450
+ constructor(deps) {
1451
+ this.#d = deps;
1452
+ }
1453
+ #now() {
1454
+ return this.#d.now ? this.#d.now() : Date.now();
1455
+ }
1456
+ registerRun(run2) {
1457
+ this.#runs.set(run2.runId, run2);
1458
+ }
1459
+ unregisterRun(runId) {
1460
+ this.#runs.delete(runId);
1461
+ }
1462
+ /** run 学到的会话 id(新建任务是从 gate 请求里学到的)。没有返回空串 */
1463
+ sessionOf(runId) {
1464
+ return this.#runs.get(runId)?.sessionId ?? "";
1465
+ }
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 ?? "");
1469
+ if (!run2) return { decision: "deny", reason: REASON_UNKNOWN_RUN };
1470
+ 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 || "";
1473
+ if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
1474
+ const v = classify(toolName, req.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
1475
+ if (v.action === "auto") return { decision: "allow" };
1476
+ const fp = fingerprint(toolName, req.hook.tool_input);
1477
+ const store = this.#d.store;
1478
+ if (run2.mode === "defer" && sessionId) {
1479
+ const prev = store.latestFor(sessionId, fp);
1480
+ if (prev && prev.status !== "pending") {
1481
+ prev.consumed = true;
1482
+ store.save();
1483
+ if (prev.status === "approved") return { decision: "allow" };
1484
+ return { decision: "deny", reason: prev.status === "cancelled" ? REASON_SUPERSEDED : REASON_DENIED };
1485
+ }
1486
+ if (prev) return { decision: "defer", reason: "\u7B49\u5F85\u8BBE\u5907\u6388\u6743" };
1487
+ }
1488
+ const rec = {
1489
+ approvalId: randomUUID3(),
1490
+ runId: run2.runId,
1491
+ tool: run2.tool,
1492
+ sessionId,
1493
+ cwd: run2.cwd,
1494
+ fingerprint: fp,
1495
+ kind: v.kind,
1496
+ summary: v.summary,
1497
+ mode: run2.mode,
1498
+ status: "pending",
1499
+ createdAt: this.#now()
1500
+ };
1501
+ store.add(rec);
1502
+ store.save();
1503
+ let early = null;
1504
+ let settle;
1505
+ const decided = new Promise((resolve3) => settle = resolve3);
1506
+ this.#waiters.set(rec.approvalId, (r) => {
1507
+ early = r;
1508
+ settle(r);
1509
+ });
1510
+ try {
1511
+ await this.#d.backend.create(rec);
1512
+ } catch (e) {
1513
+ this.#waiters.delete(rec.approvalId);
1514
+ rec.status = "cancelled";
1515
+ rec.consumed = true;
1516
+ store.save();
1517
+ this.#d.log(`\u6388\u6743\u8BF7\u6C42 ${rec.approvalId} \u53D1\u4E0D\u5230\u670D\u52A1\u7AEF\uFF1A${e.message}`);
1518
+ void this.#cancelRemote(rec.approvalId, "\u6388\u6743\u8BF7\u6C42\u53D1\u9001\u5931\u8D25");
1519
+ return { decision: "deny", reason: "\u6388\u6743\u8BF7\u6C42\u6CA1\u80FD\u53D1\u5230\u8BBE\u5907\u4E0A\uFF0C\u8FD9\u4E00\u6B65\u6CA1\u6709\u6267\u884C" };
1520
+ }
1521
+ this.#d.log(`\u6388\u6743\u8BF7\u6C42 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 tool=${rec.tool} kind=${rec.kind} mode=${rec.mode}`);
1522
+ if (early) return early;
1523
+ if (run2.mode === "defer") {
1524
+ this.#waiters.delete(rec.approvalId);
1525
+ return { decision: "defer", reason: "\u7B49\u5F85\u8BBE\u5907\u6388\u6743" };
1526
+ }
1527
+ run2.onWaitStart?.();
1528
+ try {
1529
+ return await new Promise((resolve3) => {
1530
+ void decided.then(resolve3);
1531
+ const onAbort = () => {
1532
+ if (!this.#waiters.delete(rec.approvalId)) return;
1533
+ if (rec.status === "pending") {
1534
+ rec.status = "cancelled";
1535
+ rec.consumed = true;
1536
+ store.save();
1537
+ void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
1538
+ }
1539
+ resolve3({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
1540
+ };
1541
+ if (signal.aborted) onAbort();
1542
+ else signal.addEventListener("abort", onAbort, { once: true });
1543
+ });
1544
+ } finally {
1545
+ run2.onWaitEnd?.();
1546
+ }
1547
+ }
1548
+ /**
1549
+ * 用户自己窗口里的授权框(S10)。挂住直到设备答复,或人在电脑上先处理了({@link scanWindows})。
1550
+ *
1551
+ * 🔴 这里的所有"失败"都回 `none`(不表态)而不是 deny:窗口里的授权框照常在,
1552
+ * 由电脑前的人决定。回 deny 等于替用户点了拒绝 —— daemon 一出问题,他窗口里每个授权都会被自动拒掉
1553
+ */
1554
+ async #onWindow(hook, signal, tool) {
1555
+ const none = { decision: "none" };
1556
+ const toolName = typeof hook.tool_name === "string" ? hook.tool_name : "";
1557
+ const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
1558
+ const cwd = typeof hook.cwd === "string" ? hook.cwd : "";
1559
+ if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
1560
+ for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
1561
+ const store = this.#d.store;
1562
+ const { kind, summary } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
1563
+ const remember = tool === "claude_code" ? pickRememberSuggestion(hook.permission_suggestions) : null;
1564
+ const rec = {
1565
+ approvalId: randomUUID3(),
1566
+ runId: "",
1567
+ tool,
1568
+ sessionId,
1569
+ cwd,
1570
+ fingerprint: fingerprint(toolName, hook.tool_input),
1571
+ kind,
1572
+ summary,
1573
+ mode: "window",
1574
+ status: "pending",
1575
+ createdAt: this.#now(),
1576
+ ...remember ? { remember: remember.text, rememberSuggestion: remember.suggestion } : {}
1577
+ };
1578
+ store.add(rec);
1579
+ store.save();
1580
+ const st0 = this.#readState(sessionId);
1581
+ const rawCmd = hook.tool_input && typeof hook.tool_input === "object" ? hook.tool_input.command : void 0;
1582
+ const cmd = typeof rawCmd === "string" ? normalizeCmd(rawCmd).slice(0, CMD_MATCH_LEN) : "";
1583
+ const pid = typeof st0?.pid === "number" && st0.pid > 0 ? st0.pid : void 0;
1584
+ const watchProcs = kind === "command" && pid !== void 0 && cmd.length >= CMD_MATCH_MIN;
1585
+ this.#windows.set(rec.approvalId, {
1586
+ toolName,
1587
+ hadState: st0 != null,
1588
+ ...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
1589
+ });
1590
+ let settle;
1591
+ const decided = new Promise((resolve3) => settle = resolve3);
1592
+ this.#waiters.set(rec.approvalId, settle);
1593
+ try {
1594
+ await this.#d.backend.create(rec);
1595
+ } catch (e) {
1596
+ this.#waiters.delete(rec.approvalId);
1597
+ this.#windows.delete(rec.approvalId);
1598
+ rec.status = "cancelled";
1599
+ rec.consumed = true;
1600
+ store.save();
1601
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u53D1\u4E0D\u5230\u670D\u52A1\u7AEF\uFF1A${e.message}`);
1602
+ void this.#cancelRemote(rec.approvalId, "\u6388\u6743\u8BF7\u6C42\u53D1\u9001\u5931\u8D25");
1603
+ return none;
1604
+ }
1605
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
1606
+ 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);
1609
+ const onAbort = () => {
1610
+ if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
1611
+ resolve3(none);
1612
+ };
1613
+ if (signal.aborted) onAbort();
1614
+ else signal.addEventListener("abort", onAbort, { once: true });
1615
+ });
1616
+ }
1617
+ /**
1618
+ * 每秒调一次:窗口待批准里,人已经在电脑上处理掉的,撤掉设备卡片、放掉挂着的 hook(S10 W3)。
1619
+ * 依据是 hook.cjs 落的状态文件里授权创建之后的事件({@link WINDOW_SETTLED_EVENTS}、同名工具的 PostToolUse),
1620
+ * 或者状态文件被删了(SessionEnd)
1621
+ */
1622
+ scanWindows() {
1623
+ const procCache = /* @__PURE__ */ new Map();
1624
+ const procsOf = (pid) => {
1625
+ let v = procCache.get(pid);
1626
+ if (!v) procCache.set(pid, v = this.#descendants(pid));
1627
+ return v;
1628
+ };
1629
+ for (const rec of this.#d.store.pending()) {
1630
+ if (rec.mode !== "window") continue;
1631
+ const w = this.#windows.get(rec.approvalId);
1632
+ if (!w) continue;
1633
+ const st = this.#readState(rec.sessionId);
1634
+ if (!st) {
1635
+ if (w.hadState) this.#settleWindow(rec, "\u4F1A\u8BDD\u5DF2\u7ED3\u675F");
1636
+ continue;
1637
+ }
1638
+ const fp = rec.fingerprint.slice(0, 16);
1639
+ const settled = (st.recent ?? []).some(
1640
+ (e) => e.at > rec.createdAt && (WINDOW_SETTLED_EVENTS.has(e.e) || // 跑完的必须正是这一步:并行的同名工具不算(评审 #2)。老版 hook 没有 fp 时退回只比工具名
1641
+ WINDOW_TOOL_DONE_EVENTS.has(e.e) && e.tool === w.toolName && (e.fp === void 0 || e.fp === fp))
1642
+ );
1643
+ if (settled) {
1644
+ this.#settleWindow(rec, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
1645
+ continue;
1646
+ }
1647
+ if (w.pid !== void 0 && w.cmd && w.baseline && this.#now() - rec.createdAt < WINDOW_PROC_WATCH_MS) {
1648
+ const started = procsOf(w.pid).some(
1649
+ (p) => !w.baseline?.has(p.pid) && normalizeCmd(p.args).includes(w.cmd)
1650
+ );
1651
+ if (started) this.#settleWindow(rec, "\u7535\u8111\u4E0A\u5DF2\u5141\u8BB8\uFF0C\u547D\u4EE4\u5DF2\u5F00\u59CB\u6267\u884C");
1652
+ }
1653
+ }
1654
+ }
1655
+ #descendants(pid) {
1656
+ try {
1657
+ return (this.#d.listDescendants ?? listDescendants)(pid);
1658
+ } catch {
1659
+ return [];
1660
+ }
1661
+ }
1662
+ /** 窗口待批准作废:本地回 none、撤设备卡片 */
1663
+ #settleWindow(rec, reason) {
1664
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u64A4\u9500\uFF1A${reason}`);
1665
+ rec.status = "cancelled";
1666
+ rec.decidedAt = this.#now();
1667
+ rec.consumed = true;
1668
+ this.#d.store.save();
1669
+ this.#windows.delete(rec.approvalId);
1670
+ const waiter = this.#waiters.get(rec.approvalId);
1671
+ if (waiter) {
1672
+ this.#waiters.delete(rec.approvalId);
1673
+ waiter({ decision: "none" });
1674
+ }
1675
+ void this.#cancelRemote(rec.approvalId, reason);
1676
+ }
1677
+ #readState(sessionId) {
1678
+ try {
1679
+ return (this.#d.readState ?? readState)(sessionId);
1680
+ } catch {
1681
+ return null;
1682
+ }
1683
+ }
1684
+ /** 设备上按了允许/拒绝(经服务端下发,或重连后 sync 补到) */
1685
+ onDecision(approvalId, decision, deviceName = "") {
1686
+ const rec = this.#d.store.get(approvalId);
1687
+ if (!rec) {
1688
+ this.#d.log(`\u6536\u5230\u672A\u77E5\u6388\u6743 ${approvalId} \u7684\u51B3\u5B9A\uFF0C\u5FFD\u7565`);
1689
+ return;
1690
+ }
1691
+ if (rec.status !== "pending") return;
1692
+ const always = decision === "allow_always" && rec.mode === "window" && rec.rememberSuggestion !== void 0;
1693
+ if (decision === "allow_always") decision = "allow";
1694
+ rec.status = decision === "allow" ? "approved" : "denied";
1695
+ rec.decidedAt = this.#now();
1696
+ const waiter = this.#waiters.get(approvalId);
1697
+ if (waiter) {
1698
+ this.#waiters.delete(approvalId);
1699
+ this.#windows.delete(approvalId);
1700
+ rec.consumed = true;
1701
+ this.#d.store.save();
1702
+ if (rec.mode === "window") {
1703
+ const notice = windowNotice(decision, rec.summary, deviceName) + (always ? "\uFF08\u5DF2\u8BB0\u4F4F\uFF09" : "");
1704
+ try {
1705
+ ;
1706
+ (this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
1707
+ } catch {
1708
+ }
1709
+ waiter(
1710
+ decision === "allow" ? { decision: "allow", notice, ...always ? { updatedPermissions: [rec.rememberSuggestion] } : {} } : (
1711
+ // 🔴 交给模型的原因里不带设备名:名字是家庭成员在 App 里起的,别让它进提示词。设备名只进给人看的回显
1712
+ { decision: "deny", reason: "\u7528\u6237\u5728 DeskBuddy \u8BBE\u5907\u4E0A\u62D2\u7EDD\u4E86\u8FD9\u4E00\u6B65\uFF0C\u4E0D\u8981\u91CD\u8BD5\u8FD9\u4E2A\u64CD\u4F5C", notice }
1713
+ )
1714
+ );
1715
+ } else {
1716
+ waiter(decision === "allow" ? { decision: "allow" } : { decision: "deny", reason: REASON_DENIED });
1717
+ }
1718
+ return;
1719
+ }
1720
+ this.#d.store.save();
1721
+ if (rec.mode === "window") {
1722
+ rec.consumed = true;
1723
+ this.#windows.delete(approvalId);
1724
+ this.#d.store.save();
1725
+ return;
1726
+ }
1727
+ if (rec.mode === "defer") {
1728
+ this.#d.resume(rec, "deferred");
1729
+ return;
1730
+ }
1731
+ rec.consumed = true;
1732
+ this.#d.store.save();
1733
+ if (decision === "allow") this.#d.resume(rec, "orphan");
1734
+ }
1735
+ /**
1736
+ * 同一会话来了新指令:等待中的那一步按"没批准"处理(spec E9,和窗口里弹框时直接打字一样)。
1737
+ * 🔴 必须在新指令【入队之前】调:hold 模式下旧进程还占着这个会话的串行队列,
1738
+ * 不先放掉它,新指令会排在一个永远等不到批准的进程后面。
1739
+ */
1740
+ cancelSession(sessionId) {
1741
+ for (const run2 of this.#runs.values()) {
1742
+ if (run2.sessionId === sessionId) run2.superseded = true;
1743
+ }
1744
+ for (const rec of this.#d.store.pendingForSession(sessionId)) {
1745
+ rec.status = "cancelled";
1746
+ rec.decidedAt = this.#now();
1747
+ const waiter = this.#waiters.get(rec.approvalId);
1748
+ if (waiter) {
1749
+ this.#waiters.delete(rec.approvalId);
1750
+ rec.consumed = true;
1751
+ waiter(rec.mode === "window" ? { decision: "none" } : { decision: "deny", reason: REASON_SUPERSEDED });
1752
+ }
1753
+ if (rec.mode === "window") {
1754
+ rec.consumed = true;
1755
+ this.#windows.delete(rec.approvalId);
1756
+ }
1757
+ void this.#cancelRemote(rec.approvalId, "\u540C\u4E00\u4F1A\u8BDD\u6536\u5230\u4E86\u65B0\u6307\u4EE4");
1758
+ }
1759
+ this.#d.store.save();
1760
+ }
1761
+ /**
1762
+ * 一次 run 结束(评审 S3):它名下还在原地等(hold)的请求全部撤掉。
1763
+ * 智能体被 SIGKILL 时它起的 gate 子进程未必跟着死,socket 一直开着、abort 永远不来 ——
1764
+ * 不在这里兜,等待表和设备上的卡片就一直留着。
1765
+ * defer 的不动:进程正常退出后本来就该留着等批准。
1766
+ */
1767
+ endRun(runId) {
1768
+ this.#runs.delete(runId);
1769
+ let changed = false;
1770
+ for (const rec of this.#d.store.pending()) {
1771
+ if (rec.runId !== runId || rec.mode !== "hold") continue;
1772
+ rec.status = "cancelled";
1773
+ rec.decidedAt = this.#now();
1774
+ rec.consumed = true;
1775
+ changed = true;
1776
+ const waiter = this.#waiters.get(rec.approvalId);
1777
+ if (waiter) {
1778
+ this.#waiters.delete(rec.approvalId);
1779
+ waiter({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
1780
+ }
1781
+ void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
1782
+ }
1783
+ if (changed) this.#d.store.save();
1784
+ }
1785
+ /**
1786
+ * 这个会话的一次 run 跑完了:在它【开始之前】就已有结果、却还没被用掉的记录,作废(评审 S2)。
1787
+ *
1788
+ * 🔴 否则一条已批准的记录可能留 7 天(续跑失败 / 模型换了写法没再发同一个调用),
1789
+ * 之后同一会话里一模一样的命令会被它静默放行。
1790
+ * 只作废 run 开始前就定了的:run 期间才到的决定,对应的续跑可能还排在队里没跑
1791
+ */
1792
+ settleSession(sessionId, runStartedAt) {
1793
+ if (!sessionId) return;
1794
+ let changed = false;
1795
+ for (const rec of this.#d.store.all()) {
1796
+ if (rec.sessionId !== sessionId || rec.consumed || rec.status === "pending") continue;
1797
+ if ((rec.decidedAt ?? 0) >= runStartedAt) continue;
1798
+ rec.consumed = true;
1799
+ changed = true;
1800
+ }
1801
+ if (changed) this.#d.store.save();
1802
+ }
1803
+ /** 重连后补收离线期间做的决定 */
1804
+ async syncPending() {
1805
+ for (const rec of this.#d.store.pending()) {
1806
+ if (rec.mode === "window" && !this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "\u5B88\u62A4\u8FDB\u7A0B\u91CD\u542F\u8FC7");
1807
+ }
1808
+ for (const rec of this.#d.store.all()) {
1809
+ if (rec.cancelPending) void this.#cancelRemote(rec.approvalId, rec.cancelPending);
1810
+ }
1811
+ const ids = this.#d.store.pending().map((r) => r.approvalId);
1812
+ for (let i = 0; i < ids.length; i += 50) {
1813
+ try {
1814
+ const got = await this.#d.backend.sync(ids.slice(i, i + 50));
1815
+ for (const g of got) {
1816
+ if (g.decision === "cancelled") this.#markCancelled(g.approvalId);
1817
+ else this.onDecision(g.approvalId, g.decision, g.deviceName);
1818
+ }
1819
+ } catch (e) {
1820
+ this.#d.log(`\u540C\u6B65\u6388\u6743\u72B6\u6001\u5931\u8D25\uFF1A${e.message}`);
1821
+ return;
1822
+ }
1823
+ }
1824
+ }
1825
+ /**
1826
+ * 服务端说这条已经取消了(本地的 cancel 已送达,但本地记录没落上盘之类):
1827
+ * 本地跟着标掉,不续跑。hold 还挂着的话按拒绝放掉
1828
+ */
1829
+ #markCancelled(approvalId) {
1830
+ const rec = this.#d.store.get(approvalId);
1831
+ if (!rec || rec.status !== "pending") return;
1832
+ rec.status = "cancelled";
1833
+ rec.decidedAt = this.#now();
1834
+ const waiter = this.#waiters.get(approvalId);
1835
+ if (waiter) {
1836
+ this.#waiters.delete(approvalId);
1837
+ rec.consumed = true;
1838
+ waiter(rec.mode === "window" ? { decision: "none" } : { decision: "deny", reason: REASON_SUPERSEDED });
1839
+ }
1840
+ if (rec.mode === "window") {
1841
+ rec.consumed = true;
1842
+ this.#windows.delete(approvalId);
1843
+ }
1844
+ this.#d.store.save();
1845
+ }
1846
+ async #cancelRemote(approvalId, reason) {
1847
+ const rec = this.#d.store.get(approvalId);
1848
+ try {
1849
+ await this.#d.backend.cancel(approvalId, reason);
1850
+ if (rec?.cancelPending) {
1851
+ rec.cancelPending = void 0;
1852
+ this.#d.store.save();
1853
+ }
1854
+ } catch (e) {
1855
+ if (rec) {
1856
+ rec.cancelPending = reason;
1857
+ this.#d.store.save();
1858
+ }
1859
+ this.#d.log(`\u64A4\u9500\u6388\u6743\u8BF7\u6C42 ${approvalId} \u5931\u8D25\uFF0C\u91CD\u8FDE\u540E\u91CD\u8BD5\uFF1A${e.message}`);
1860
+ }
1861
+ }
1862
+ /** 只给测试和诊断用 */
1863
+ get waiting() {
1864
+ return this.#waiters.size;
1865
+ }
1866
+ };
1867
+ function realpathNearest(p) {
1868
+ let cur = p;
1869
+ const tail = [];
1870
+ for (let i = 0; i < 64; i++) {
1871
+ try {
1872
+ if (existsSync9(cur)) return join10(realpathSync(cur), ...tail.reverse());
1873
+ } catch {
1874
+ return p;
1875
+ }
1876
+ const parent = dirname5(cur);
1877
+ if (parent === cur) return p;
1878
+ tail.push(basename5(cur));
1879
+ cur = parent;
1880
+ }
1881
+ return p;
1882
+ }
1883
+ function cwdForms(cwd) {
1884
+ try {
1885
+ const real = realpathSync(cwd);
1886
+ return real === cwd ? [cwd] : [cwd, real];
1887
+ } catch {
1888
+ return [cwd];
1889
+ }
1890
+ }
1891
+
1892
+ // src/daemon/gate/server.ts
1893
+ import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1894
+ import { createServer } from "http";
1895
+ import { mkdirSync as mkdirSync8, renameSync as renameSync6, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
1896
+ import { join as join11 } from "path";
1897
+ var MAX_BODY_BYTES = 8 * 1024 * 1024;
1898
+ function gateConfPath() {
1899
+ return join11(deskbuddyHome(), "gate.json");
1900
+ }
1901
+ function tokenEquals(a, b) {
1902
+ const x = Buffer.from(a);
1903
+ const y = Buffer.from(b);
1904
+ return x.length === y.length && timingSafeEqual(x, y);
1905
+ }
1906
+ var GateServer = class {
1907
+ #handler;
1908
+ #token = randomBytes3(32).toString("hex");
1909
+ #server = null;
1910
+ constructor(handler) {
1911
+ this.#handler = handler;
1912
+ }
1913
+ get token() {
1914
+ return this.#token;
1915
+ }
1916
+ /** 起监听并写 gate.json。返回实际端口 */
1917
+ async start() {
1918
+ const server = createServer((req, res) => void this.#serve(req, res));
1919
+ await new Promise((resolve3, reject) => {
1920
+ server.once("error", reject);
1921
+ server.listen(0, "127.0.0.1", () => resolve3());
1922
+ });
1923
+ this.#server = server;
1924
+ const addr = server.address();
1925
+ const port = typeof addr === "object" && addr ? addr.port : 0;
1926
+ mkdirSync8(deskbuddyHome(), { recursive: true });
1927
+ const tmp = gateConfPath() + ".tmp";
1928
+ writeFileSync8(tmp, JSON.stringify({ port, token: this.#token, pid: process.pid }), {
1929
+ encoding: "utf8",
1930
+ mode: 384
1931
+ });
1932
+ renameSync6(tmp, gateConfPath());
1933
+ return port;
1934
+ }
1935
+ async stop() {
1936
+ try {
1937
+ rmSync3(gateConfPath(), { force: true });
1938
+ } catch {
1939
+ }
1940
+ const s = this.#server;
1941
+ this.#server = null;
1942
+ if (s) await new Promise((resolve3) => s.close(() => resolve3()));
1943
+ }
1944
+ async #serve(req, res) {
1945
+ const reply = (status, body2) => {
1946
+ if (res.writableEnded || res.destroyed) return;
1947
+ res.writeHead(status, { "content-type": "application/json" });
1948
+ res.end(body2 ? JSON.stringify(body2) : "");
1949
+ };
1950
+ if (req.method !== "POST" || req.url !== "/gate") return reply(404);
1951
+ const token = req.headers["x-gate-token"];
1952
+ if (typeof token !== "string" || !tokenEquals(token, this.#token)) return reply(403);
1953
+ let body = "";
1954
+ let size = 0;
1955
+ try {
1956
+ for await (const chunk of req) {
1957
+ size += chunk.length;
1958
+ if (size > MAX_BODY_BYTES) return reply(413);
1959
+ body += String(chunk);
1960
+ }
1961
+ } catch {
1962
+ return reply(400);
1963
+ }
1964
+ let parsed;
1965
+ try {
1966
+ parsed = JSON.parse(body);
1967
+ } catch {
1968
+ return reply(400);
1969
+ }
1970
+ if (!parsed.hook || typeof parsed.hook !== "object") return reply(400);
1971
+ const window = parsed.mode === "window";
1972
+ if (!window && typeof parsed.runId !== "string") return reply(400);
1973
+ const ac = new AbortController();
1974
+ res.on("close", () => {
1975
+ if (!res.writableEnded) ac.abort();
1976
+ });
1977
+ try {
1978
+ const req2 = window ? {
1979
+ window: true,
1980
+ tool: typeof parsed.tool === "string" && /^[a-z_]{1,32}$/.test(parsed.tool) ? parsed.tool : "claude_code",
1981
+ hook: parsed.hook
1982
+ } : { runId: parsed.runId, hook: parsed.hook };
1983
+ const out = await this.#handler(req2, ac.signal);
1984
+ reply(200, out);
1985
+ } catch {
1986
+ reply(500);
1987
+ }
1988
+ }
1989
+ };
1990
+
1991
+ // src/daemon/handshake.ts
1992
+ import { randomBytes as randomBytes4 } from "crypto";
1993
+ var PARAM = {
1994
+ appId: "ShiFeng-AppId",
1995
+ timestamp: "ShiFeng-Timestamp",
1996
+ signature: "ShiFeng-Signature",
1997
+ deviceId: "ShiFeng-Device-ID",
1998
+ deviceName: "ShiFeng-Device-Name",
1999
+ nonce: "ShiFeng-Nonce"
2000
+ };
2001
+ var WS_PATH = "/deskbuddy";
2002
+ function sanitizeParamValue(value) {
2003
+ return (value || "").replace(/[=&]/g, "-");
2004
+ }
2005
+ function buildHandshakeParts(cfg, opts = {}) {
2006
+ const timestamp = opts.timestamp || String(Date.now());
2007
+ const nonce = opts.nonce || randomBytes4(8).toString("hex");
2008
+ const deviceId = sanitizeParamValue(cfg.clientId);
2009
+ const deviceName = sanitizeParamValue(cfg.hostName);
2010
+ return {
2011
+ timestamp,
2012
+ nonce,
2013
+ deviceId,
2014
+ deviceName,
2015
+ signature: sign({
2016
+ appId: APP_ID,
2017
+ appSecret: APP_SECRET,
2018
+ timestamp,
2019
+ deviceId,
2020
+ deviceName,
2021
+ nonce,
2022
+ body: ""
2023
+ // ← 建连没有 body,服务端也是拿空串参与签名
2024
+ })
2025
+ };
2026
+ }
2027
+ function toWsOrigin(apiBase) {
2028
+ const trimmed = (apiBase || "").replace(/\/+$/, "");
2029
+ if (trimmed.startsWith("https://")) return "wss://" + trimmed.slice("https://".length);
2030
+ if (trimmed.startsWith("http://")) return "ws://" + trimmed.slice("http://".length);
2031
+ return trimmed;
2032
+ }
2033
+ function buildHandshakeUrl(cfg, opts = {}) {
2034
+ const p = buildHandshakeParts(cfg, opts);
2035
+ const query = [
2036
+ [PARAM.appId, APP_ID],
2037
+ [PARAM.timestamp, p.timestamp],
2038
+ [PARAM.signature, p.signature],
2039
+ [PARAM.deviceId, p.deviceId],
2040
+ [PARAM.deviceName, p.deviceName],
2041
+ [PARAM.nonce, p.nonce]
2042
+ ].map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
2043
+ return `${toWsOrigin(cfg.apiBase)}${WS_PATH}?${query}`;
2044
+ }
2045
+
2046
+ // src/daemon/log.ts
2047
+ import { appendFileSync, mkdirSync as mkdirSync9, statSync as statSync2, truncateSync } from "fs";
2048
+ import { join as join12 } from "path";
2049
+ var MAX_LOG_BYTES = 2 * 1024 * 1024;
2050
+ function logPath() {
2051
+ return join12(deskbuddyHome(), "daemon.log");
2052
+ }
2053
+ function rotateIfNeeded(path) {
2054
+ try {
2055
+ if (statSync2(path).size > MAX_LOG_BYTES) truncateSync(path, 0);
2056
+ } catch {
2057
+ }
2058
+ }
2059
+ function log(line) {
2060
+ const stamped = `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
2061
+ `;
2062
+ try {
2063
+ mkdirSync9(deskbuddyHome(), { recursive: true });
2064
+ const p = logPath();
2065
+ rotateIfNeeded(p);
2066
+ appendFileSync(p, stamped, "utf8");
2067
+ } catch {
2068
+ }
2069
+ if (process.env.DESKBUDDY_DAEMON_FOREGROUND === "1") {
2070
+ process.stdout.write(stamped);
2071
+ }
2072
+ }
2073
+ function daemonLogPath() {
2074
+ return logPath();
2075
+ }
2076
+
2077
+ // src/daemon/queue.ts
2078
+ var SessionQueue = class {
2079
+ /** sessionId → 该会话的队尾 promise。空闲后会被清掉,不会无限增长 */
2080
+ #tails = /* @__PURE__ */ new Map();
2081
+ /**
2082
+ * 把一个任务排到该 sessionId 的队尾。返回的 promise 在**这个任务**结束时落定
2083
+ * (成功或失败都落定为 fulfilled,失败信息由调用方在 job 内部自己处理)。
2084
+ *
2085
+ * 🔴 链上必须 catch:一条指令抛异常而不吞掉的话,这个 sessionId 的队尾会变成
2086
+ * rejected promise,后续所有指令都会跟着被拒 —— 一次失败让这个会话**永久**
2087
+ * 收不到指令,而且没有任何报错。
2088
+ */
2089
+ enqueue(sessionId, job) {
2090
+ const key = sessionId || "";
2091
+ const prev = this.#tails.get(key) ?? Promise.resolve();
2092
+ const next = prev.then(job).catch(() => {
2093
+ });
2094
+ this.#tails.set(key, next);
2095
+ void next.then(() => {
2096
+ if (this.#tails.get(key) === next) this.#tails.delete(key);
2097
+ });
2098
+ return next;
2099
+ }
2100
+ /** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
2101
+ get activeSessions() {
2102
+ return this.#tails.size;
2103
+ }
2104
+ /** 等所有会话都排空。只给测试和优雅退出用 */
2105
+ async drain() {
2106
+ while (this.#tails.size > 0) {
2107
+ await Promise.all([...this.#tails.values()]);
2108
+ }
2109
+ }
2110
+ };
2111
+
2112
+ // src/daemon/remote.ts
2113
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync8, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
2114
+ import { join as join13 } from "path";
2115
+ function handleDir() {
2116
+ return join13(deskbuddyHome(), "remote");
2117
+ }
2118
+ function handlePath(tool) {
2119
+ const safe = (tool || "").replace(/[^0-9a-zA-Z_-]/g, "");
2120
+ return join13(handleDir(), `${safe || "unknown"}.json`);
2121
+ }
2122
+ function readHandle(tool) {
2123
+ try {
2124
+ const h = JSON.parse(readFileSync8(handlePath(tool), "utf8"));
2125
+ if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
2126
+ if (typeof h?.cwd !== "string" || !h.cwd) return null;
2127
+ return h;
2128
+ } catch {
2129
+ return null;
2130
+ }
2131
+ }
2132
+ function writeHandle(tool, handle) {
2133
+ try {
2134
+ mkdirSync10(handleDir(), { recursive: true });
2135
+ const tmp = handlePath(tool) + ".tmp";
2136
+ writeFileSync9(tmp, JSON.stringify(handle), "utf8");
2137
+ renameSync7(tmp, handlePath(tool));
2138
+ } catch {
2139
+ }
2140
+ }
2141
+ function handleIsUsable(handle) {
2142
+ const s = readState(handle.sessionId);
2143
+ if (!s) return false;
2144
+ if (s.pid && !isAlive(s.pid, s.pidName)) return false;
2145
+ return true;
2146
+ }
2147
+
2148
+ // src/daemon/resolve.ts
2149
+ function resolveTarget(cmd, deps) {
2150
+ if (cmd.sessionId) {
2151
+ const sessionId = deps.alias ? deps.alias(cmd.sessionId) : cmd.sessionId;
2152
+ const state = deps.readState(sessionId);
2153
+ if (!state) return { ok: false, reason: "session_gone" };
2154
+ if (!state.cwd) return { ok: false, reason: "no_cwd" };
2155
+ if (deps.windowOpen?.(state)) return { ok: false, reason: "window_open" };
2156
+ return { ok: true, sessionId, cwd: state.cwd, handleKey: cmd.tool };
2157
+ }
2158
+ let dirCwd = "";
2159
+ if (cmd.dirRef) {
2160
+ dirCwd = deps.dirOf ? deps.dirOf(cmd.dirRef) : "";
2161
+ if (!dirCwd) return { ok: false, reason: "dir_gone" };
2162
+ }
2163
+ const handleKey = cmd.dirRef ? `${cmd.tool}_${cmd.dirRef}` : cmd.tool;
2164
+ const handle = deps.readHandle(handleKey);
2165
+ const handleState = handle ? deps.readState(handle.sessionId) : null;
2166
+ const handleInWindow = !!(handleState && deps.windowOpen?.(handleState));
2167
+ if (handle && !handleInWindow && deps.handleIsUsable(handle)) {
2168
+ return { ok: true, sessionId: handle.sessionId, cwd: handle.cwd, handleKey };
2169
+ }
2170
+ const cwd = dirCwd || deps.remoteCwd();
2171
+ if (!cwd) return { ok: false, reason: "no_remote_cwd" };
2172
+ return { ok: true, sessionId: "", cwd, handleKey };
2173
+ }
2174
+ function failureMessage(reason) {
2175
+ switch (reason) {
2176
+ case "session_gone":
2177
+ return "\u8FD9\u4E2A\u4F1A\u8BDD\u5DF2\u7ECF\u4E0D\u5728\u4E86";
2178
+ case "no_cwd":
2179
+ return "\u8FD9\u4E2A\u4F1A\u8BDD\u9700\u8981\u66F4\u65B0 DeskBuddy \u540E\u624D\u80FD\u8FDC\u7A0B\u64CD\u4F5C";
2180
+ case "no_remote_cwd":
2181
+ return "\u8FD8\u6CA1\u8BBE\u7F6E\u8FDC\u7A0B\u4EFB\u52A1\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u8DD1\u4E00\u6B21 deskbuddy setup";
2182
+ case "window_open":
2183
+ return "\u8FD9\u4E2A\u4EFB\u52A1\u6B63\u5728\u7535\u8111\u4E0A\u6253\u5F00\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u7EE7\u7EED\uFF0C\u6216\u65B0\u5EFA\u4EFB\u52A1";
2184
+ case "dir_gone":
2185
+ return "\u7535\u8111\u4E0A\u627E\u4E0D\u5230\u8FD9\u4E2A\u76EE\u5F55\u4E86\uFF0C\u8BF7\u6362\u4E00\u4E2A\u76EE\u5F55";
2186
+ }
2187
+ }
2188
+
2189
+ // src/daemon/seen.ts
2190
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync9, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "fs";
2191
+ import { join as join14 } from "path";
2192
+ var SEEN_CAPACITY = 200;
2193
+ function seenPath() {
2194
+ return join14(deskbuddyHome(), "seen.json");
2195
+ }
2196
+ var SeenIds = class _SeenIds {
2197
+ #ids = [];
2198
+ #set = /* @__PURE__ */ new Set();
2199
+ constructor(initial = []) {
2200
+ for (const id of initial) this.#push(id);
2201
+ }
2202
+ #push(id) {
2203
+ if (this.#set.has(id)) return;
2204
+ this.#ids.push(id);
2205
+ this.#set.add(id);
2206
+ while (this.#ids.length > SEEN_CAPACITY) {
2207
+ const dropped = this.#ids.shift();
2208
+ if (dropped !== void 0) this.#set.delete(dropped);
2209
+ }
2210
+ }
2211
+ has(id) {
2212
+ return this.#set.has(id);
2213
+ }
2214
+ /** 记下来。返回 true 表示这是**新的**(之前没见过) */
2215
+ remember(id) {
2216
+ if (this.#set.has(id)) return false;
2217
+ this.#push(id);
2218
+ return true;
2219
+ }
2220
+ snapshot() {
2221
+ return [...this.#ids];
2222
+ }
2223
+ static load() {
2224
+ try {
2225
+ const raw = JSON.parse(readFileSync9(seenPath(), "utf8"));
2226
+ return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
2227
+ } catch {
2228
+ return new _SeenIds();
2229
+ }
2230
+ }
2231
+ save() {
2232
+ try {
2233
+ mkdirSync11(deskbuddyHome(), { recursive: true });
2234
+ const tmp = seenPath() + ".tmp";
2235
+ writeFileSync10(tmp, JSON.stringify({ ids: this.#ids }), "utf8");
2236
+ renameSync8(tmp, seenPath());
2237
+ } catch {
2238
+ }
2239
+ }
2240
+ };
2241
+
2242
+ // src/daemon/codexWatch.ts
2243
+ import { execFileSync as execFileSync4 } from "child_process";
2244
+ import WebSocket from "ws";
2245
+ var RESUME_RETRY_MS = 1e3;
2246
+ var RESUME_MAX_TRIES = 120;
2247
+ var REQUEST_TIMEOUT_MS = 30 * 1e3;
2248
+ function isLoopbackWs(url) {
2249
+ try {
2250
+ const u = new URL(url);
2251
+ return (u.protocol === "ws:" || u.protocol === "wss:") && ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
2252
+ } catch {
2253
+ return false;
2254
+ }
2255
+ }
2256
+ function parseAppServerUrls(psOutput) {
2257
+ const urls = [];
2258
+ for (const line of psOutput.split("\n")) {
2259
+ if (!/\bcodex\b/.test(line) || !/\bapp-server\b/.test(line)) continue;
2260
+ const m = line.match(/--listen[= ](ws:\/\/\S+)/);
2261
+ if (m && isLoopbackWs(m[1]) && !urls.includes(m[1])) urls.push(m[1]);
2262
+ }
2263
+ return urls;
2264
+ }
2265
+ function discoverAppServer(configured) {
2266
+ if (configured) return isLoopbackWs(configured) ? configured : null;
2267
+ try {
2268
+ const out = execFileSync4("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
2269
+ return parseAppServerUrls(out)[0] ?? null;
2270
+ } catch {
2271
+ return null;
2272
+ }
2273
+ }
2274
+ function unwrapShell(command) {
2275
+ const m = command.match(/^\/bin\/(?:ba|z)?sh -lc '([\s\S]*)'$/);
2276
+ return m ? m[1].replace(/'\\''/g, "'") : command;
2277
+ }
2278
+ var CodexWatch = class {
2279
+ #d;
2280
+ #ws = null;
2281
+ #url = "";
2282
+ #nextId = 1;
2283
+ #calls = /* @__PURE__ */ new Map();
2284
+ /** 已订阅的会话 */
2285
+ #subscribed = /* @__PURE__ */ new Set();
2286
+ /** 正在重试订阅的会话 → 已试次数 */
2287
+ #retrying = /* @__PURE__ */ new Map();
2288
+ #cwd = /* @__PURE__ */ new Map();
2289
+ /** fileChange 条目 → 改动的文件(授权请求本身不带文件,从 item/started 记) */
2290
+ #changes = /* @__PURE__ */ new Map();
2291
+ /** 服务端请求 id → 放掉等待的开关、所属会话 */
2292
+ #pending = /* @__PURE__ */ new Map();
2293
+ #stopped = false;
2294
+ /** 已空闲的会话(有卡片在等时暂缓退订,卡片了结后补退) */
2295
+ #idle = /* @__PURE__ */ new Set();
2296
+ /** 连接代数:断线重连后,上一条连接遗留的订阅重试不再跑 */
2297
+ #gen = 0;
2298
+ constructor(deps) {
2299
+ this.#d = { connect: (url) => new WebSocket(url), ...deps };
2300
+ }
2301
+ get connected() {
2302
+ return this.#ws !== null;
2303
+ }
2304
+ /** 定时调:没连上就找一次 app-server 连上去;连着时 codex 被 disable 了就断开 */
2305
+ tick() {
2306
+ if (this.#stopped) return;
2307
+ if (this.#ws) {
2308
+ if (this.#d.enabled && !this.#d.enabled()) this.#teardown();
2309
+ return;
2310
+ }
2311
+ const url = (this.#d.discover ?? (() => discoverAppServer()))();
2312
+ if (!url || !isLoopbackWs(url)) return;
2313
+ this.#open(url);
2314
+ }
2315
+ stop() {
2316
+ this.#stopped = true;
2317
+ this.#teardown();
2318
+ }
2319
+ #open(url) {
2320
+ let ws;
2321
+ try {
2322
+ ws = this.#d.connect(url);
2323
+ } catch {
2324
+ return;
2325
+ }
2326
+ this.#ws = ws;
2327
+ this.#url = url;
2328
+ this.#gen++;
2329
+ ws.on("open", () => void this.#onOpen());
2330
+ ws.on("message", (buf) => this.#onMessage(String(buf)));
2331
+ ws.on("close", () => this.#teardown());
2332
+ ws.on("error", () => this.#teardown());
2333
+ }
2334
+ #teardown() {
2335
+ const ws = this.#ws;
2336
+ if (!ws) return;
2337
+ this.#ws = null;
2338
+ try {
2339
+ ws.terminate();
2340
+ } catch {
2341
+ }
2342
+ for (const c of this.#calls.values()) {
2343
+ clearTimeout(c.timer);
2344
+ c.resolve({ error: { message: "disconnected" } });
2345
+ }
2346
+ this.#calls.clear();
2347
+ for (const { ac } of this.#pending.values()) ac.abort();
2348
+ this.#pending.clear();
2349
+ this.#subscribed.clear();
2350
+ this.#retrying.clear();
2351
+ this.#cwd.clear();
2352
+ this.#changes.clear();
2353
+ this.#idle.clear();
2354
+ this.#d.log(`\u5DF2\u65AD\u5F00 Codex app-server ${this.#url}`);
2355
+ }
2356
+ #send(obj) {
2357
+ try {
2358
+ this.#ws?.send(JSON.stringify({ jsonrpc: "2.0", ...obj }));
2359
+ } catch {
2360
+ }
2361
+ }
2362
+ #request(method, params) {
2363
+ const id = this.#nextId++;
2364
+ return new Promise((resolve3) => {
2365
+ const timer = setTimeout(() => {
2366
+ this.#calls.delete(id);
2367
+ resolve3({ error: { message: "timeout" } });
2368
+ }, REQUEST_TIMEOUT_MS);
2369
+ timer.unref?.();
2370
+ this.#calls.set(id, { resolve: resolve3, timer });
2371
+ this.#send({ id, method, params });
2372
+ });
2373
+ }
2374
+ async #onOpen() {
2375
+ const r = await this.#request("initialize", { clientInfo: { name: "deskbuddy", version: "1" } });
2376
+ if (r.error) {
2377
+ this.#d.log(`Codex app-server \u521D\u59CB\u5316\u5931\u8D25\uFF1A${JSON.stringify(r.error).slice(0, 120)}`);
2378
+ this.#teardown();
2379
+ return;
2380
+ }
2381
+ this.#send({ method: "initialized" });
2382
+ this.#d.log(`\u5DF2\u8FDE\u4E0A Codex app-server ${this.#url}\uFF0C\u7A97\u53E3\u6388\u6743\u53EF\u63A8\u5230\u8BBE\u5907`);
2383
+ }
2384
+ #subscribe(threadId) {
2385
+ if (this.#subscribed.has(threadId) || this.#retrying.has(threadId)) return;
2386
+ this.#retrying.set(threadId, 0);
2387
+ const gen = this.#gen;
2388
+ const attempt = async () => {
2389
+ 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 });
2393
+ return;
2394
+ }
2395
+ if (!r.error) {
2396
+ this.#retrying.delete(threadId);
2397
+ this.#subscribed.add(threadId);
2398
+ const cwd = r.result?.thread?.cwd;
2399
+ if (typeof cwd === "string") this.#cwd.set(threadId, cwd);
2400
+ return;
2401
+ }
2402
+ const n = (this.#retrying.get(threadId) ?? 0) + 1;
2403
+ if (n >= RESUME_MAX_TRIES) {
2404
+ this.#retrying.delete(threadId);
2405
+ return;
2406
+ }
2407
+ this.#retrying.set(threadId, n);
2408
+ setTimeout(() => void attempt(), RESUME_RETRY_MS).unref?.();
2409
+ };
2410
+ void attempt();
2411
+ }
2412
+ /** 会话空闲了:退订,让 app-server 能在窗口关掉后卸载它。还有设备卡片在等的先不退 */
2413
+ #unsubscribe(threadId) {
2414
+ this.#retrying.delete(threadId);
2415
+ if (!this.#subscribed.has(threadId)) return;
2416
+ for (const p of this.#pending.values()) if (p.threadId === threadId) return;
2417
+ this.#subscribed.delete(threadId);
2418
+ void this.#request("thread/unsubscribe", { threadId });
2419
+ }
2420
+ #onMessage(raw) {
2421
+ let m;
2422
+ try {
2423
+ m = JSON.parse(raw);
2424
+ } catch {
2425
+ return;
2426
+ }
2427
+ const id = m.id;
2428
+ const method = typeof m.method === "string" ? m.method : "";
2429
+ if (id !== void 0 && !method) {
2430
+ const c = this.#calls.get(id);
2431
+ if (c) {
2432
+ clearTimeout(c.timer);
2433
+ this.#calls.delete(id);
2434
+ c.resolve(m);
2435
+ }
2436
+ return;
2437
+ }
2438
+ const params = m.params && typeof m.params === "object" ? m.params : {};
2439
+ if (id !== void 0) {
2440
+ this.#onServerRequest(id, method, params);
2441
+ return;
2442
+ }
2443
+ this.#onNotification(method, params);
2444
+ }
2445
+ #onNotification(method, p) {
2446
+ switch (method) {
2447
+ case "thread/started": {
2448
+ const t = p.thread;
2449
+ if (t && typeof t.id === "string" && typeof t.cwd === "string") this.#cwd.set(t.id, t.cwd);
2450
+ return;
2451
+ }
2452
+ case "thread/status/changed": {
2453
+ const id = typeof p.threadId === "string" ? p.threadId : "";
2454
+ const type = p.status?.type;
2455
+ if (!id) return;
2456
+ if (type === "active") {
2457
+ this.#idle.delete(id);
2458
+ this.#subscribe(id);
2459
+ } else {
2460
+ this.#idle.add(id);
2461
+ this.#unsubscribe(id);
2462
+ }
2463
+ return;
2464
+ }
2465
+ case "thread/closed":
2466
+ 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);
2471
+ }
2472
+ return;
2473
+ case "item/started": {
2474
+ const item = p.item;
2475
+ if (item && item.type === "fileChange" && typeof item.id === "string" && Array.isArray(item.changes)) {
2476
+ const paths = item.changes.map((c) => typeof c?.path === "string" ? c.path : "").filter(Boolean);
2477
+ this.#changes.set(item.id, paths);
2478
+ }
2479
+ return;
2480
+ }
2481
+ case "item/completed": {
2482
+ const item = p.item;
2483
+ if (item && typeof item.id === "string") this.#changes.delete(item.id);
2484
+ return;
2485
+ }
2486
+ case "serverRequest/resolved": {
2487
+ const key = String(p.requestId);
2488
+ const pend = this.#pending.get(key);
2489
+ if (pend) {
2490
+ this.#pending.delete(key);
2491
+ pend.ac.abort();
2492
+ if (this.#idle.has(pend.threadId)) this.#unsubscribe(pend.threadId);
2493
+ }
2494
+ return;
2495
+ }
2496
+ default:
2497
+ }
241
2498
  }
242
- try {
243
- return deps.kernelRelease();
244
- } catch {
245
- return "";
2499
+ #onServerRequest(id, method, p) {
2500
+ const threadId = typeof p.threadId === "string" ? p.threadId : "";
2501
+ if (!threadId) return;
2502
+ let hook;
2503
+ if (method === "item/commandExecution/requestApproval") {
2504
+ const command = typeof p.command === "string" ? unwrapShell(p.command) : "";
2505
+ if (!command) return;
2506
+ hook = { tool_name: "Bash", tool_input: { command }, cwd: typeof p.cwd === "string" ? p.cwd : this.#cwd.get(threadId) ?? "" };
2507
+ } else if (method === "item/fileChange/requestApproval") {
2508
+ if (typeof p.grantRoot === "string" && p.grantRoot) return;
2509
+ const paths = typeof p.itemId === "string" && this.#changes.get(p.itemId) || [];
2510
+ const shown = paths.join(", ");
2511
+ if (!shown) return;
2512
+ hook = { tool_name: "Edit", tool_input: { file_path: shown }, cwd: this.#cwd.get(threadId) ?? "" };
2513
+ } else {
2514
+ return;
2515
+ }
2516
+ const key = String(id);
2517
+ const ac = new AbortController();
2518
+ const entry = { ac, threadId };
2519
+ this.#pending.set(key, entry);
2520
+ const req = {
2521
+ window: true,
2522
+ tool: "codex",
2523
+ hook: { hook_event_name: "PermissionRequest", session_id: threadId, ...hook }
2524
+ };
2525
+ const settled = () => {
2526
+ if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
2527
+ };
2528
+ void this.#d.gate(req, ac.signal).then((reply) => {
2529
+ if (this.#pending.get(key) !== entry) return;
2530
+ this.#pending.delete(key);
2531
+ if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
2532
+ else if (reply.decision === "deny") this.#send({ id, result: { decision: "decline" } });
2533
+ settled();
2534
+ }).catch(() => {
2535
+ if (this.#pending.get(key) === entry) this.#pending.delete(key);
2536
+ settled();
2537
+ });
246
2538
  }
247
- }
248
- var machineInfoCache = null;
249
- function machineInfo() {
250
- if (!machineInfoCache) {
251
- machineInfoCache = {
252
- os: humanOs(),
253
- osVersion: productOsVersion(),
254
- arch: process.arch,
255
- cliVersion: packageVersion()
2539
+ };
2540
+
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()));
2553
+ }
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`;
2556
+ }
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
256
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));
257
2599
  }
258
- return machineInfoCache;
2600
+ /** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
2601
+ async startGate() {
2602
+ const port = await this.#gate.start();
2603
+ log(`\u6388\u6743\u7AEF\u70B9\u5DF2\u5C31\u7EEA 127.0.0.1:${port}`);
2604
+ this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
2605
+ this.#windowScan.unref();
2606
+ this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
2607
+ 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);
2616
+ this.#codexTimer.unref();
2617
+ return port;
2618
+ }
2619
+ #lastDirs = "";
2620
+ #dirsTick = 0;
2621
+ /**
2622
+ * 上报最近目录(二期 P6):连上时一次,之后每 10 分钟;内容没变不报。
2623
+ * 🔴 只报目录名与 ref,全路径不出本机
2624
+ */
2625
+ async #reportDirs(force = false) {
2626
+ const cfg = loadConfig();
2627
+ if (!cfg.clientId || !cfg.boundAt) return;
2628
+ const dirs = recentDirs().map(({ ref, name, lastUsedAt }) => ({ ref, name, lastUsedAt }));
2629
+ const fp = JSON.stringify(dirs.map((d) => [d.ref, d.name]));
2630
+ if (!force && fp === this.#lastDirs) return;
2631
+ try {
2632
+ await api.dirsReport({ clientId: cfg.clientId, dirs });
2633
+ this.#lastDirs = fp;
2634
+ } catch (e) {
2635
+ log(`\u4E0A\u62A5\u6700\u8FD1\u76EE\u5F55\u5931\u8D25\uFF1A${e.message}`);
2636
+ }
2637
+ }
2638
+ /** 按已启用的工具各报一次存活会话快照(readAliveStates 顺手剔除死进程) */
2639
+ async #reportAlive() {
2640
+ const cfg = loadConfig();
2641
+ if (!cfg.clientId || !cfg.boundAt) return;
2642
+ for (const tool of cfg.enabledTools) {
2643
+ await postReport(buildSnapshot(readAliveStates(tool), tool, cfg, this.#deps.now()));
2644
+ }
2645
+ if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
2646
+ }
2647
+ get coordinator() {
2648
+ return this.#coord;
2649
+ }
2650
+ stop() {
2651
+ this.#stopped = true;
2652
+ this.#clearIdle();
2653
+ if (this.#windowScan) clearInterval(this.#windowScan);
2654
+ if (this.#aliveReport) clearInterval(this.#aliveReport);
2655
+ if (this.#codexTimer) clearInterval(this.#codexTimer);
2656
+ this.#codex?.stop();
2657
+ this.#ws?.close();
2658
+ void this.#gate.stop();
2659
+ }
2660
+ #clearIdle() {
2661
+ if (this.#idleTimer) {
2662
+ clearTimeout(this.#idleTimer);
2663
+ this.#idleTimer = null;
2664
+ }
2665
+ }
2666
+ /**
2667
+ * 空闲超时兜底。
2668
+ *
2669
+ * 光靠 ws 的 'close' 事件不够:网络被静默掐断(睡眠唤醒、切 Wi-Fi、NAT 超时)时
2670
+ * TCP 连接会僵在那儿,close 事件永远不来,于是守护进程"看起来连着"、实际收不到
2671
+ * 任何指令,而且没有任何症状。
2672
+ */
2673
+ #armIdle() {
2674
+ this.#clearIdle();
2675
+ this.#idleTimer = setTimeout(() => {
2676
+ log("idle \u8D85\u65F6\u672A\u6536\u5230\u4EFB\u4F55\u5E27\uFF0C\u4E3B\u52A8\u65AD\u5F00\u91CD\u8FDE");
2677
+ this.#ws?.terminate();
2678
+ }, IDLE_TIMEOUT_MS);
2679
+ }
2680
+ connect() {
2681
+ if (this.#stopped) return;
2682
+ const cfg = loadConfig();
2683
+ if (!cfg.boundAt) {
2684
+ log("\u5C1A\u672A\u6388\u6743\uFF08\u6CA1\u6709 boundAt\uFF09\uFF0C\u4E0D\u5EFA\u8FDE\u3002\u5148\u8DD1 deskbuddy setup");
2685
+ return;
2686
+ }
2687
+ const url = buildHandshakeUrl(cfg);
2688
+ log(`\u5EFA\u8FDE\u4E2D ${cfg.apiBase}`);
2689
+ let ws;
2690
+ try {
2691
+ ws = this.#deps.connect(url);
2692
+ } catch (e) {
2693
+ this.#scheduleReconnect(`\u5EFA\u8FDE\u629B\u5F02\u5E38: ${e.message}`);
2694
+ return;
2695
+ }
2696
+ this.#ws = ws;
2697
+ ws.on("open", () => {
2698
+ this.#attempt = 0;
2699
+ log("\u5DF2\u8FDE\u4E0A");
2700
+ this.#armIdle();
2701
+ void this.#coord.syncPending();
2702
+ void this.#reportDirs(true);
2703
+ });
2704
+ ws.on("message", (raw) => {
2705
+ this.#armIdle();
2706
+ this.#onMessage(String(raw));
2707
+ });
2708
+ ws.on("error", (e) => log(`\u8FDE\u63A5\u51FA\u9519: ${e.message}`));
2709
+ ws.on("close", (code, reason) => {
2710
+ this.#clearIdle();
2711
+ this.#ws = null;
2712
+ if (code === POLICY_VIOLATION) {
2713
+ log(`\u670D\u52A1\u7AEF\u62D2\u7EDD\u8FDE\u63A5(${code} ${reason?.toString() || ""})\uFF1A\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u505C\u6B62\u91CD\u8FDE\u3002\u8BF7\u91CD\u65B0\u8FD0\u884C deskbuddy setup`);
2714
+ this.#stopped = true;
2715
+ return;
2716
+ }
2717
+ this.#scheduleReconnect(`\u8FDE\u63A5\u5173\u95ED(${code})`);
2718
+ });
2719
+ }
2720
+ #sendPong() {
2721
+ try {
2722
+ this.#ws?.send(PONG_FRAME);
2723
+ } catch (e) {
2724
+ log(`\u56DE pong \u5931\u8D25: ${e.message}`);
2725
+ }
2726
+ }
2727
+ #scheduleReconnect(why) {
2728
+ if (this.#stopped) return;
2729
+ this.#attempt++;
2730
+ const delay = backoffDelay(this.#attempt);
2731
+ log(`${why}\uFF0C${delay}ms \u540E\u91CD\u8FDE\uFF08\u7B2C ${this.#attempt} \u6B21\uFF09`);
2732
+ setTimeout(() => this.connect(), delay).unref?.();
2733
+ }
2734
+ #onMessage(raw) {
2735
+ let frame;
2736
+ try {
2737
+ frame = JSON.parse(raw);
2738
+ } catch {
2739
+ return;
2740
+ }
2741
+ if (frame?.service === PING_FRAME_SERVICE) {
2742
+ this.#sendPong();
2743
+ return;
2744
+ }
2745
+ const ap = parseApprovalFrame(frame);
2746
+ if (ap) {
2747
+ if (!this.#seen.remember(ap.instructionId)) return;
2748
+ this.#seen.save();
2749
+ log(`\u6536\u5230\u6388\u6743\u51B3\u5B9A ${ap.approvalId} ${ap.decision}`);
2750
+ this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName);
2751
+ return;
2752
+ }
2753
+ const cmd = parseCommand(frame);
2754
+ if (!cmd) return;
2755
+ if (!this.#seen.remember(cmd.instructionId)) {
2756
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u5904\u7406\u8FC7\uFF0C\u4E22\u5F03`);
2757
+ return;
2758
+ }
2759
+ this.#seen.save();
2760
+ if (isExpired(cmd, this.#deps.now())) {
2761
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u8FC7\u671F\uFF0C\u4E22\u5F03`);
2762
+ return;
2763
+ }
2764
+ const target = resolveTarget(cmd, {
2765
+ readState,
2766
+ readHandle,
2767
+ handleIsUsable,
2768
+ remoteCwd: () => loadConfig().remoteCwd || "",
2769
+ alias: resolveAlias,
2770
+ dirOf: (ref) => dirOf(ref),
2771
+ windowOpen: (s) => s.interactive === true && !!s.pid && isAlive(s.pid, s.pidName)
2772
+ });
2773
+ if (!target.ok) {
2774
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u65E0\u6CD5\u6267\u884C\uFF1A${failureMessage(target.reason)}`);
2775
+ return;
2776
+ }
2777
+ if (target.sessionId) this.#coord.cancelSession(target.sessionId);
2778
+ this.#queue.enqueue(target.sessionId, () => this.#execute(cmd, target.sessionId, target.cwd, target.handleKey));
2779
+ }
2780
+ async #execute(cmd, sessionId, cwd, handleKey = cmd.tool) {
2781
+ if (sessionId) {
2782
+ const fresh = readState(sessionId);
2783
+ if (!fresh) {
2784
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u4F1A\u8BDD ${sessionId} \u5728\u6267\u884C\u524D\u5DF2\u6D88\u5931`);
2785
+ return;
2786
+ }
2787
+ }
2788
+ const runId = newRunId();
2789
+ const plan = buildSpawnPlan({ ...cmd, sessionId }, cwd, this.#gateTarget(runId));
2790
+ if (!plan) {
2791
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u672C\u673A\u4E0D\u652F\u6301\u8FDC\u7A0B\u6267\u884C ${cmd.tool}`);
2792
+ return;
2793
+ }
2794
+ 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`);
2798
+ return;
2799
+ }
2800
+ if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
2801
+ if (!result.ok) {
2802
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error}`);
2803
+ }
2804
+ if (!sessionId) {
2805
+ const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
2806
+ if (newId) {
2807
+ writeHandle(handleKey, { sessionId: newId, cwd, createdAt: this.#deps.now() });
2808
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u5EFA\u51FA\u4F1A\u8BDD ${newId}\uFF0C\u5DF2\u8BB0\u4E3A ${cmd.tool} \u7684\u8FDC\u7A0B\u4F1A\u8BDD`);
2809
+ } else {
2810
+ log(`\u6307\u4EE4 ${cmd.instructionId} \u672A\u80FD\u89E3\u6790\u51FA\u4F1A\u8BDD id\uFF0C\u4E0B\u6B21\u4ECD\u4F1A\u65B0\u5EFA`);
2811
+ }
2812
+ }
2813
+ }
2814
+ #gateTarget(runId) {
2815
+ return { runId, nodePath: this.#deps.nodePath, gateScript: this.#deps.gateScript };
2816
+ }
2817
+ /** 设备上有了决定、需要续会话。排进该会话的串行队列,和普通指令一样不会并发打同一个会话 */
2818
+ #scheduleResume(r, kind) {
2819
+ if (!r.sessionId) {
2820
+ log(`\u6388\u6743 ${r.approvalId} \u6CA1\u6709\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u7EED\u8DD1`);
2821
+ return;
2822
+ }
2823
+ const origin = this.#runsDone.get(r.runId) ?? Promise.resolve();
2824
+ void origin.then(() => this.#queue.enqueue(r.sessionId, () => this.#resume(r, kind)));
2825
+ }
2826
+ async #resume(r, kind) {
2827
+ if (kind === "deferred" && r.consumed) {
2828
+ log(`\u6388\u6743 ${r.approvalId} \u5DF2\u5728\u524D\u4E00\u6B21\u7EED\u8DD1\u4E2D\u5904\u7406\uFF0C\u8DF3\u8FC7`);
2829
+ return;
2830
+ }
2831
+ const runId = newRunId();
2832
+ const text = kind === "deferred" ? "" : orphanResumeText(r.summary);
2833
+ const plan = buildSpawnPlan(
2834
+ { instructionId: `resume-${r.approvalId}`, type: "text", sessionId: r.sessionId, tool: r.tool, text, expireAt: 0 },
2835
+ r.cwd,
2836
+ this.#gateTarget(runId)
2837
+ );
2838
+ if (!plan) {
2839
+ log(`\u6388\u6743 ${r.approvalId} \u7EED\u8DD1\u653E\u5F03\uFF1A\u672C\u673A\u4E0D\u652F\u6301\u8FDC\u7A0B\u6267\u884C ${r.tool}`);
2840
+ return;
2841
+ }
2842
+ log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId}\uFF08\u6388\u6743 ${r.approvalId} ${r.status}\uFF0C${kind}\uFF09`);
2843
+ const result = await this.#run(plan, r.tool, runId, r.sessionId, r.cwd);
2844
+ if (!r.consumed) {
2845
+ r.consumed = true;
2846
+ this.#store.save();
2847
+ }
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`);
2849
+ 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
+ else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
2851
+ else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
2852
+ }
2853
+ /**
2854
+ * 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
2855
+ * 找不到可执行文件返回 null。
2856
+ */
2857
+ async #run(plan, tool, runId, sessionId, cwd) {
2858
+ if (!existsSync10(this.#deps.nodePath) || !existsSync10(this.#deps.gateScript)) {
2859
+ 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;
2861
+ }
2862
+ if (tool === "opencode" && !this.#deps.opencodeReady()) {
2863
+ 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;
2865
+ }
2866
+ const real = this.#deps.materialize(plan, tool);
2867
+ if (!real) return null;
2868
+ const startedAt = this.#deps.now();
2869
+ const rc = startCommand(real, tool);
2870
+ this.#coord.registerRun({
2871
+ runId,
2872
+ tool,
2873
+ cwd,
2874
+ mode: waitModeFor(tool),
2875
+ sessionId,
2876
+ // 原地等设备批准的时间不算执行时间(spec §5.5)
2877
+ onWaitStart: () => rc.pauseTimeout(),
2878
+ onWaitEnd: () => rc.resumeTimeout()
2879
+ });
2880
+ this.#runsDone.set(runId, rc.done);
2881
+ try {
2882
+ return await rc.done;
2883
+ } finally {
2884
+ this.#runsDone.delete(runId);
2885
+ const sid = sessionId || this.#coord.sessionOf(runId);
2886
+ this.#coord.endRun(runId);
2887
+ this.#coord.settleSession(sid, startedAt);
2888
+ }
2889
+ }
2890
+ };
2891
+ function newRunId() {
2892
+ return randomBytes5(12).toString("hex");
259
2893
  }
260
- async function call(service, data, timeoutMs = DEFAULT_TIMEOUT_MS) {
2894
+ async function runDaemon(deps = {}) {
2895
+ const d = new Daemon(deps);
2896
+ await d.startGate();
2897
+ d.connect();
2898
+ return d;
2899
+ }
2900
+
2901
+ // src/commands/daemon.ts
2902
+ async function runDaemonCommand(opts = {}) {
261
2903
  const cfg = loadConfig();
262
- const body = buildGatewayBody(service, data);
263
- const controller = new AbortController();
264
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2904
+ if (!cfg.boundAt) {
2905
+ process.stderr.write("\u2717 \u8FD8\u6CA1\u6388\u6743\u3002\u5148\u8DD1 deskbuddy setup\n");
2906
+ return 1;
2907
+ }
2908
+ if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
265
2909
  try {
266
- const res = await fetch(`${cfg.apiBase}/gateway`, {
267
- method: "POST",
268
- headers: buildHeaders(cfg, body),
269
- body,
270
- // 与签名用的是同一个字符串
271
- signal: controller.signal
272
- });
273
- const json = await res.json();
274
- if (!isSuccessCode(json?.code)) {
275
- throw new ApiError(String(json?.code), json?.message ?? "");
2910
+ writeFileSync11(pidFilePath(), String(process.pid), "utf8");
2911
+ } catch {
2912
+ }
2913
+ let daemon;
2914
+ try {
2915
+ daemon = await runDaemon();
2916
+ } catch (e) {
2917
+ log(`\u5B88\u62A4\u8FDB\u7A0B\u542F\u52A8\u5931\u8D25\uFF1A${e.message}`);
2918
+ process.stderr.write(`\u2717 \u5B88\u62A4\u8FDB\u7A0B\u542F\u52A8\u5931\u8D25\uFF1A${e.message}
2919
+ `);
2920
+ return 1;
2921
+ }
2922
+ const shutdown = (sig) => {
2923
+ log(`\u6536\u5230 ${sig}\uFF0C\u9000\u51FA`);
2924
+ daemon.stop();
2925
+ try {
2926
+ rmSync4(pidFilePath(), { force: true });
2927
+ } catch {
276
2928
  }
277
- return json?.data;
278
- } finally {
279
- clearTimeout(timer);
2929
+ process.exit(0);
2930
+ };
2931
+ process.on("SIGINT", () => shutdown("SIGINT"));
2932
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2933
+ if (opts.foreground) {
2934
+ process.stdout.write(`\u5B88\u62A4\u8FDB\u7A0B\u5DF2\u542F\u52A8\uFF08pid ${process.pid}\uFF09\uFF0C\u65E5\u5FD7\uFF1A${daemonLogPath()}
2935
+ \u6309 Ctrl+C \u9000\u51FA
2936
+ `);
280
2937
  }
2938
+ return new Promise(() => {
2939
+ });
281
2940
  }
282
- var api = {
283
- authCode: (clientId, hostName) => call(SERVICES.authCode, { clientId, hostName, ...machineInfo() }),
284
- authToken: (authCode) => call(SERVICES.authToken, { authCode }),
285
- toolRegistry: () => call(SERVICES.toolRegistry, {}),
286
- // 🔴 2 秒超时:这条调用发生在 hook 链路上,慢一点就是拖慢用户的 agent
287
- report: (payload) => call(SERVICES.sessionReport, payload, 2e3)
288
- };
289
2941
 
290
2942
  // src/core/registry.ts
2943
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync12 } from "fs";
2944
+ import { join as join15 } from "path";
291
2945
  var FALLBACK = {
292
2946
  version: "builtin",
293
2947
  tools: {
@@ -295,7 +2949,16 @@ var FALLBACK = {
295
2949
  format: "cc-hook",
296
2950
  processNames: ["claude"],
297
2951
  titleSources: ["cc-ai-title", "git-branch", "project"],
298
- events: { UserPromptSubmit: "watch", PreToolUse: "watch", PostToolUse: "watch", Stop: "done" },
2952
+ events: {
2953
+ UserPromptSubmit: "watch",
2954
+ PreToolUse: "watch",
2955
+ PostToolUse: "watch",
2956
+ // 工具跑失败时 Claude Code 发这个而不是 PostToolUse;窗口待批准撤卡也要认它
2957
+ PostToolUseFailure: "watch",
2958
+ Stop: "done",
2959
+ // 窗口里弹授权框的那一刻就是待确认(S10),不等之后那条权限通知
2960
+ PermissionRequest: "ask_approval"
2961
+ },
299
2962
  lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
300
2963
  },
301
2964
  codex: {
@@ -312,15 +2975,55 @@ var FALLBACK = {
312
2975
  PermissionRequest: "ask_approval"
313
2976
  },
314
2977
  lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
2978
+ },
2979
+ workbuddy: {
2980
+ format: "cc-hook",
2981
+ processNames: ["WorkBuddy"],
2982
+ titleSources: ["git-branch", "project"],
2983
+ events: {
2984
+ UserPromptSubmit: "watch",
2985
+ PreToolUse: "watch",
2986
+ PostToolUse: "watch",
2987
+ Stop: "done",
2988
+ PermissionRequest: "ask_approval"
2989
+ },
2990
+ lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
2991
+ },
2992
+ qoder: {
2993
+ format: "cc-hook",
2994
+ processNames: ["Qoder CN"],
2995
+ titleSources: ["git-branch", "project"],
2996
+ events: {
2997
+ UserPromptSubmit: "watch",
2998
+ PreToolUse: "watch",
2999
+ PostToolUse: "watch",
3000
+ Stop: "done",
3001
+ PermissionRequest: "ask_approval"
3002
+ },
3003
+ lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
3004
+ },
3005
+ opencode: {
3006
+ format: "oc-plugin",
3007
+ processNames: ["opencode"],
3008
+ // opencode 自带的会话标题是模型对提问的摘要,违反隐私约束(spec §6.4),不用
3009
+ titleSources: ["git-branch", "project"],
3010
+ events: {
3011
+ UserPromptSubmit: "watch",
3012
+ PreToolUse: "watch",
3013
+ PostToolUse: "watch",
3014
+ Stop: "done",
3015
+ PermissionRequest: "ask_approval"
3016
+ },
3017
+ lifecycle: { SessionEnd: "remove" }
315
3018
  }
316
3019
  }
317
3020
  };
318
3021
  function registryCachePath() {
319
- return join3(deskbuddyHome(), "tool-registry.json");
3022
+ return join15(deskbuddyHome(), "tool-registry.json");
320
3023
  }
321
3024
  function loadRegistry() {
322
3025
  try {
323
- return JSON.parse(readFileSync4(registryCachePath(), "utf8"));
3026
+ return JSON.parse(readFileSync10(registryCachePath(), "utf8"));
324
3027
  } catch {
325
3028
  return FALLBACK;
326
3029
  }
@@ -328,15 +3031,15 @@ function loadRegistry() {
328
3031
  async function refreshRegistry() {
329
3032
  try {
330
3033
  const registry = await api.toolRegistry();
331
- mkdirSync3(deskbuddyHome(), { recursive: true });
332
- writeFileSync3(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
3034
+ mkdirSync12(deskbuddyHome(), { recursive: true });
3035
+ writeFileSync12(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
333
3036
  } catch {
334
3037
  }
335
3038
  }
336
3039
 
337
3040
  // src/adapters/ccHook.ts
338
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
339
- import { dirname as dirname2 } from "path";
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";
340
3043
  var CC_EVENTS = [
341
3044
  "UserPromptSubmit",
342
3045
  "PreToolUse",
@@ -346,8 +3049,8 @@ var CC_EVENTS = [
346
3049
  "SessionEnd"
347
3050
  ];
348
3051
  function readSettings(path) {
349
- if (!existsSync3(path)) return {};
350
- const raw = readFileSync5(path, "utf8");
3052
+ if (!existsSync11(path)) return {};
3053
+ const raw = readFileSync11(path, "utf8");
351
3054
  if (raw.trim() === "") return {};
352
3055
  try {
353
3056
  return JSON.parse(raw);
@@ -356,17 +3059,17 @@ function readSettings(path) {
356
3059
  }
357
3060
  }
358
3061
  function writeSettings(path, settings) {
359
- mkdirSync4(dirname2(path), { recursive: true });
3062
+ mkdirSync13(dirname6(path), { recursive: true });
360
3063
  let mode;
361
3064
  try {
362
- mode = statSync(path).mode & 511;
3065
+ mode = statSync3(path).mode & 511;
363
3066
  } catch {
364
3067
  mode = void 0;
365
3068
  }
366
3069
  const tmp = path + ".deskbuddy.tmp";
367
- writeFileSync4(tmp, JSON.stringify(settings, null, 2), "utf8");
368
- if (mode !== void 0) chmodSync(tmp, mode);
369
- renameSync2(tmp, path);
3070
+ writeFileSync13(tmp, JSON.stringify(settings, null, 2), "utf8");
3071
+ if (mode !== void 0) chmodSync2(tmp, mode);
3072
+ renameSync9(tmp, path);
370
3073
  }
371
3074
  function installCcHook(settingsPath, hookPath, events = CC_EVENTS, tool = "claude_code") {
372
3075
  const settings = readSettings(settingsPath);
@@ -379,14 +3082,30 @@ function installCcHook(settingsPath, hookPath, events = CC_EVENTS, tool = "claud
379
3082
  }
380
3083
  writeSettings(settingsPath, settings);
381
3084
  }
3085
+ var WINDOW_GATE_TIMEOUT_S = 7 * 24 * 60 * 60;
3086
+ function shQuote2(s) {
3087
+ return `'${s.replace(/'/g, `'\\''`)}'`;
3088
+ }
3089
+ function installCcWindowGate(settingsPath, gatePath, tool = "claude_code") {
3090
+ const settings = readSettings(settingsPath);
3091
+ settings.hooks ??= {};
3092
+ const groups = settings.hooks.PermissionRequest ??= [];
3093
+ if (groups.some((g) => g.hooks?.some((h) => h.command?.includes(gatePath)))) return;
3094
+ const toolArg = tool === "claude_code" ? "" : ` --tool ${tool}`;
3095
+ groups.push({
3096
+ hooks: [{ type: "command", command: `node ${shQuote2(gatePath)} --window${toolArg}`, timeout: WINDOW_GATE_TIMEOUT_S }]
3097
+ });
3098
+ writeSettings(settingsPath, settings);
3099
+ }
382
3100
  function uninstallCcHook(settingsPath, hookPath) {
383
3101
  const settings = readSettings(settingsPath);
384
3102
  if (!settings.hooks) return;
3103
+ const gatePath = join16(dirname6(hookPath), "gate.cjs");
385
3104
  for (const event of Object.keys(settings.hooks)) {
386
3105
  const groups = settings.hooks[event] ?? [];
387
3106
  const kept = [];
388
3107
  for (const g of groups) {
389
- const hooks = (g.hooks ?? []).filter((h) => !h.command?.includes(hookPath));
3108
+ const hooks = (g.hooks ?? []).filter((h) => !h.command?.includes(hookPath) && !h.command?.includes(gatePath));
390
3109
  if (hooks.length > 0) kept.push({ ...g, hooks });
391
3110
  }
392
3111
  settings.hooks[event] = kept;
@@ -395,8 +3114,8 @@ function uninstallCcHook(settingsPath, hookPath) {
395
3114
  }
396
3115
 
397
3116
  // src/adapters/codexHook.ts
398
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync3, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
399
- import { dirname as dirname3, join as join4 } from "path";
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";
400
3119
  var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
401
3120
  var CODEX_MARKER_END = "# <<< deskbuddy <<<";
402
3121
  var CODEX_EVENTS = [
@@ -407,9 +3126,9 @@ var CODEX_EVENTS = [
407
3126
  "SessionEnd",
408
3127
  "PermissionRequest"
409
3128
  ];
410
- 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";
3129
+ 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";
411
3130
  function codexConfigPath() {
412
- return join4(codexHome(), "config.toml");
3131
+ return join17(codexHome(), "config.toml");
413
3132
  }
414
3133
  function renderBlock(hookPath, events, tool) {
415
3134
  const command = JSON.stringify(`node ${hookPath} ${tool}`);
@@ -428,17 +3147,17 @@ function renderBlock(hookPath, events, tool) {
428
3147
  return lines.join("\n");
429
3148
  }
430
3149
  function writeAtomic(path, content) {
431
- mkdirSync5(dirname3(path), { recursive: true });
3150
+ mkdirSync14(dirname7(path), { recursive: true });
432
3151
  let mode;
433
3152
  try {
434
- mode = statSync2(path).mode & 511;
3153
+ mode = statSync4(path).mode & 511;
435
3154
  } catch {
436
3155
  mode = void 0;
437
3156
  }
438
3157
  const tmp = path + ".deskbuddy.tmp";
439
- writeFileSync5(tmp, content, "utf8");
440
- if (mode !== void 0) chmodSync2(tmp, mode);
441
- renameSync3(tmp, path);
3158
+ writeFileSync14(tmp, content, "utf8");
3159
+ if (mode !== void 0) chmodSync3(tmp, mode);
3160
+ renameSync10(tmp, path);
442
3161
  }
443
3162
  function assertSafeToAppend(raw, path) {
444
3163
  for (const quote of ['"""', "'''"]) {
@@ -448,7 +3167,7 @@ function assertSafeToAppend(raw, path) {
448
3167
  }
449
3168
  }
450
3169
  function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex") {
451
- const raw = existsSync4(configPath2) ? readFileSync6(configPath2, "utf8") : "";
3170
+ const raw = existsSync12(configPath2) ? readFileSync12(configPath2, "utf8") : "";
452
3171
  if (raw.includes(CODEX_MARKER_BEGIN)) return;
453
3172
  assertSafeToAppend(raw, configPath2);
454
3173
  const block = renderBlock(hookPath, events, tool);
@@ -456,8 +3175,8 @@ function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "
456
3175
  writeAtomic(configPath2, head + block + "\n");
457
3176
  }
458
3177
  function uninstallCodexHook(configPath2) {
459
- if (!existsSync4(configPath2)) return;
460
- const raw = readFileSync6(configPath2, "utf8");
3178
+ if (!existsSync12(configPath2)) return;
3179
+ const raw = readFileSync12(configPath2, "utf8");
461
3180
  const lines = raw.split("\n");
462
3181
  const begin = lines.findIndex((l) => l.trim() === CODEX_MARKER_BEGIN);
463
3182
  if (begin === -1) return;
@@ -471,31 +3190,55 @@ function uninstallCodexHook(configPath2) {
471
3190
  }
472
3191
 
473
3192
  // src/commands/tools.ts
474
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
3193
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
475
3194
  function fileContains(path, needle) {
476
3195
  try {
477
- if (!existsSync5(path)) return false;
478
- return readFileSync7(path, "utf8").includes(needle);
3196
+ if (!existsSync13(path)) return false;
3197
+ return readFileSync13(path, "utf8").includes(needle);
479
3198
  } catch {
480
3199
  return false;
481
3200
  }
482
3201
  }
3202
+ var CC_CONFIG_PATHS = {
3203
+ claude_code: claudeSettingsPath,
3204
+ workbuddy: workbuddySettingsPath,
3205
+ qoder: qoderSettingsPath
3206
+ };
483
3207
  var ADAPTERS = {
484
3208
  "cc-hook": {
485
- configPath: claudeSettingsPath,
3209
+ configPath: (tool) => CC_CONFIG_PATHS[tool]?.() ?? null,
486
3210
  install: (p, hook, events, tool) => installCcHook(p, hook, events, tool),
487
3211
  uninstall: (p, hook) => uninstallCcHook(p, hook),
488
3212
  isInstalled: (p, hook) => fileContains(p, hook),
489
- defaultEvents: CC_EVENTS
3213
+ defaultEvents: CC_EVENTS,
3214
+ // 实测 PermissionRequest 与窗口授权框并行的:Claude Code(S10 §2)、Qoder CN 桌面端(二期 V8)。
3215
+ // WorkBuddy 桌面端弹授权框不触发 PermissionRequest;Codex 的 hook 会阻塞授权框——都不装
3216
+ installWindowGate: (p, gate, tool) => {
3217
+ if (tool !== "claude_code" && tool !== "qoder") return false;
3218
+ installCcWindowGate(p, gate, tool);
3219
+ return true;
3220
+ }
490
3221
  },
491
3222
  "codex-toml": {
492
- configPath: codexConfigPath,
3223
+ configPath: (tool) => tool === "codex" ? codexConfigPath() : null,
493
3224
  install: (p, hook, events, tool) => installCodexHook(p, hook, events, tool),
494
3225
  uninstall: (p) => uninstallCodexHook(p),
495
3226
  isInstalled: (p) => fileContains(p, CODEX_MARKER_BEGIN),
496
3227
  defaultEvents: CODEX_EVENTS,
497
3228
  // 🔴 不打这句的话,用户会遇到「装成功了但没数据」,然后当成装失败
498
3229
  notice: CODEX_TRUST_NOTICE
3230
+ },
3231
+ // opencode(二期 P9):插件形态,事件翻译、窗口确认、远程关卡都在插件里;events 参数用不上
3232
+ "oc-plugin": {
3233
+ configPath: (tool) => tool === "opencode" ? opencodePluginPath() : null,
3234
+ // 🔴 node 用 PATH 上稳定的那个(/opt/homebrew/bin/node 这种链接),同 daemon 的 nodePath
3235
+ install: (p, hook) => installOcPlugin(p, hook, whichIn("node", defaultLocateDeps()) ?? process.execPath),
3236
+ uninstall: (p) => uninstallOcPlugin(p),
3237
+ isInstalled: (p) => isOcPluginInstalled(p),
3238
+ defaultEvents: [],
3239
+ notice: OC_NOTICE,
3240
+ // 窗口确认就在插件里,不用另装
3241
+ installWindowGate: () => true
499
3242
  }
500
3243
  };
501
3244
  function adapterFor(registry, tool) {
@@ -539,7 +3282,8 @@ async function runDisable(tools, opts = {}) {
539
3282
  if (!adapter) {
540
3283
  continue;
541
3284
  }
542
- const configPath2 = adapter.configPath();
3285
+ const configPath2 = adapter.configPath(tool);
3286
+ if (!configPath2) continue;
543
3287
  try {
544
3288
  adapter.uninstall(configPath2, hookPath);
545
3289
  out(`\u2705 ${tool}\uFF1A\u5DF2\u4ECE ${configPath2} \u79FB\u9664`);
@@ -552,6 +3296,10 @@ async function runDisable(tools, opts = {}) {
552
3296
  }
553
3297
  cfg.enabledTools = [...enabled];
554
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}`);
3302
+ }
555
3303
  if (!opts.brief) {
556
3304
  out("");
557
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");
@@ -561,6 +3309,7 @@ async function runDisable(tools, opts = {}) {
561
3309
  }
562
3310
 
563
3311
  // src/commands/enable.ts
3312
+ import { dirname as dirname8, join as join18 } from "path";
564
3313
  function stdout2(line) {
565
3314
  process.stdout.write(line + "\n");
566
3315
  }
@@ -592,7 +3341,12 @@ async function runEnable(tools, opts = {}) {
592
3341
  failed += 1;
593
3342
  continue;
594
3343
  }
595
- const configPath2 = adapter.configPath();
3344
+ const configPath2 = adapter.configPath(tool);
3345
+ if (!configPath2) {
3346
+ 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;
3348
+ continue;
3349
+ }
596
3350
  const events = subscribedEvents(registry, tool, adapter.defaultEvents);
597
3351
  try {
598
3352
  adapter.install(configPath2, hookPath, events, tool);
@@ -603,28 +3357,41 @@ async function runEnable(tools, opts = {}) {
603
3357
  }
604
3358
  enabled.add(tool);
605
3359
  out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
3360
+ try {
3361
+ if (adapter.installWindowGate?.(configPath2, join18(dirname8(hookPath), "gate.cjs"), tool)) {
3362
+ 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
+ }
3364
+ } catch (e) {
3365
+ out(`\u26A0\uFE0F ${tool}\uFF1A\u8BBE\u5907\u786E\u8BA4\u6388\u6743\u6CA1\u88C5\u4E0A\uFF1A${e.message}`);
3366
+ }
606
3367
  if (adapter.notice) out(adapter.notice);
607
3368
  }
608
3369
  cfg.enabledTools = [...enabled];
609
3370
  saveConfig(cfg);
3371
+ if (!opts.skipAutostart) {
3372
+ const r = await installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() });
3373
+ out("");
3374
+ out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
3375
+ }
610
3376
  out("");
611
- out("\u88C5\u5B8C\u4E86\u3002\u6CA1\u6709\u5E38\u9A7B\u8FDB\u7A0B \u2014\u2014 \u53EA\u662F\u5F80\u5404\u667A\u80FD\u4F53\u7684 hook \u914D\u7F6E\u91CC\u52A0\u4E86\u4E00\u6761\u8BB0\u5F55\u3002");
3377
+ out("\u88C5\u5B8C\u4E86\u3002\u72B6\u6001\u91C7\u96C6\u9760 hook\uFF0C\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF1B");
3378
+ 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");
612
3379
  out("\u968F\u65F6\u53EF\u4EE5 deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001deskbuddy disable \u5E72\u51C0\u79FB\u9664\u3002");
613
3380
  return failed > 0 ? 1 : 0;
614
3381
  }
615
3382
 
616
3383
  // src/commands/logout.ts
617
3384
  import { createInterface } from "readline";
618
- import { existsSync as existsSync6, rmSync } from "fs";
3385
+ import { existsSync as existsSync14, rmSync as rmSync5 } from "fs";
619
3386
  function stdout3(line) {
620
3387
  process.stdout.write(line + "\n");
621
3388
  }
622
3389
  function askYesNo(question) {
623
- return new Promise((resolve) => {
3390
+ return new Promise((resolve3) => {
624
3391
  const rl = createInterface({ input: process.stdin, output: process.stdout });
625
3392
  rl.question(question, (answer) => {
626
3393
  rl.close();
627
- resolve(/^(y|yes)$/i.test(answer.trim()));
3394
+ resolve3(/^(y|yes)$/i.test(answer.trim()));
628
3395
  });
629
3396
  });
630
3397
  }
@@ -645,9 +3412,9 @@ async function runLogout(opts = {}) {
645
3412
  return 1;
646
3413
  }
647
3414
  }
648
- await runDisable([], { out, brief: true });
649
- if (existsSync6(home)) {
650
- rmSync(home, { recursive: true, force: true });
3415
+ await runDisable([], { out, brief: true, keepAutostart: opts.keepAutostart });
3416
+ if (existsSync14(home)) {
3417
+ rmSync5(home, { recursive: true, force: true });
651
3418
  }
652
3419
  out("");
653
3420
  out("\u2705 \u5DF2\u6E05\u9664\u672C\u673A\u7684 DeskBuddy \u6570\u636E\u3002");
@@ -656,6 +3423,63 @@ async function runLogout(opts = {}) {
656
3423
 
657
3424
  // src/commands/setup.ts
658
3425
  import qrcode from "qrcode-terminal";
3426
+
3427
+ // 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()) {
3433
+ const s = (raw || "").trim();
3434
+ 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);
3437
+ return abs;
3438
+ }
3439
+ function checkPath(abs) {
3440
+ if (!existsSync15(abs)) return "missing";
3441
+ try {
3442
+ return statSync5(abs).isDirectory() ? "ok" : "not_a_dir";
3443
+ } catch {
3444
+ return "missing";
3445
+ }
3446
+ }
3447
+ function resolveAnswer(raw, deps) {
3448
+ const check = deps.check ?? checkPath;
3449
+ const abs = normalizePath(raw, deps.home) ?? deps.defaultPath;
3450
+ const verdict = check(abs);
3451
+ return verdict === "ok" ? { ok: true, path: abs } : { ok: false, reason: verdict, path: abs };
3452
+ }
3453
+ function verdictMessage(reason, path) {
3454
+ return reason === "not_a_dir" ? `${path} \u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55` : `${path} \u4E0D\u5B58\u5728`;
3455
+ }
3456
+ var MAX_ATTEMPTS = 3;
3457
+ async function askRemoteCwd(deps) {
3458
+ const defaultPath = deps.defaultPath ?? defaultRemoteCwd();
3459
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
3460
+ const raw = await deps.ask(`\u8FDC\u7A0B\u300C\u65B0\u5EFA\u4EFB\u52A1\u300D\u7684\u5DE5\u4F5C\u76EE\u5F55 [${defaultPath}]\uFF1A`);
3461
+ if (raw === null) {
3462
+ return defaultPath;
3463
+ }
3464
+ const r = resolveAnswer(raw, { defaultPath, check: deps.check });
3465
+ if (r.ok) return r.path;
3466
+ deps.out(` \u2717 ${verdictMessage(r.reason, r.path)}\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165`);
3467
+ }
3468
+ deps.out(` \u7528\u9ED8\u8BA4\u503C\uFF1A${defaultPath}`);
3469
+ return defaultPath;
3470
+ }
3471
+ function terminalAsk(prompt) {
3472
+ if (!process.stdin.isTTY) return Promise.resolve(null);
3473
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3474
+ return new Promise((res) => {
3475
+ rl.question(prompt, (answer) => {
3476
+ rl.close();
3477
+ res(answer);
3478
+ });
3479
+ });
3480
+ }
3481
+
3482
+ // src/commands/setup.ts
659
3483
  var AUTH_CODE_INVALID = "8250";
660
3484
  var DEFAULT_INTERVAL_SEC = 5;
661
3485
  var SETUP_TIMEOUT_MS = 5 * 6e4;
@@ -752,76 +3576,18 @@ async function runSetup(opts = {}) {
752
3576
  markBound();
753
3577
  await refreshRegistry();
754
3578
  out("");
3579
+ out("\u5728\u786C\u4EF6\u4E0A\u300C\u65B0\u5EFA\u4EFB\u52A1\u300D\u65F6\uFF0Cagent \u4F1A\u5728\u4E0B\u9762\u8FD9\u4E2A\u76EE\u5F55\u91CC\u5E72\u6D3B\u3002");
3580
+ out("\uFF08\u56DE\u590D\u67D0\u4E2A\u5DF2\u6709\u4F1A\u8BDD\u65F6\u7528\u7684\u662F\u90A3\u4E2A\u4F1A\u8BDD\u81EA\u5DF1\u7684\u76EE\u5F55\uFF0C\u4E0D\u53D7\u8FD9\u91CC\u5F71\u54CD\uFF09");
3581
+ const remoteCwd = await askRemoteCwd({ out, ask: terminalAsk });
3582
+ const bound = loadConfig();
3583
+ bound.remoteCwd = remoteCwd;
3584
+ saveConfig(bound);
3585
+ out(` \u5DF2\u8BB0\u4E0B\uFF1A${remoteCwd}`);
3586
+ out("");
755
3587
  out("\u2705 \u6388\u6743\u6210\u529F\u3002\u63A5\u4E0B\u6765\u8FD0\u884C\uFF1Adeskbuddy enable claude_code,codex");
756
3588
  return 0;
757
3589
  }
758
3590
 
759
- // src/core/state.ts
760
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
761
- import { join as join5 } from "path";
762
-
763
- // src/core/process.ts
764
- import { execFileSync as execFileSync2 } from "child_process";
765
- function processName(pid) {
766
- try {
767
- const out = execFileSync2("ps", ["-p", String(pid), "-o", "comm="], {
768
- encoding: "utf8",
769
- stdio: ["ignore", "pipe", "ignore"]
770
- });
771
- const name = out.trim();
772
- return name === "" ? null : name;
773
- } catch {
774
- return null;
775
- }
776
- }
777
- function isAlive(pid, expectName) {
778
- if (!Number.isFinite(pid) || pid <= 0) return false;
779
- try {
780
- process.kill(pid, 0);
781
- } catch {
782
- return false;
783
- }
784
- if (!expectName) return true;
785
- const name = processName(pid);
786
- return name != null && name.toLowerCase().includes(expectName.toLowerCase());
787
- }
788
-
789
- // src/core/state.ts
790
- function stateDir() {
791
- return join5(deskbuddyHome(), "state");
792
- }
793
- function statePath(sessionId) {
794
- const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
795
- return join5(stateDir(), `${safe || "unknown"}.json`);
796
- }
797
- function removeState(sessionId) {
798
- try {
799
- rmSync2(statePath(sessionId), { force: true });
800
- } catch {
801
- }
802
- }
803
- function readAliveStates(tool) {
804
- if (!existsSync7(stateDir())) return [];
805
- const alive = [];
806
- for (const file of readdirSync(stateDir())) {
807
- if (!file.endsWith(".json")) continue;
808
- let s;
809
- try {
810
- s = JSON.parse(readFileSync8(join5(stateDir(), file), "utf8"));
811
- } catch {
812
- rmSync2(join5(stateDir(), file), { force: true });
813
- continue;
814
- }
815
- if (s.tool !== tool) continue;
816
- if (s.pid && !isAlive(s.pid, s.pidName)) {
817
- removeState(s.sessionId);
818
- continue;
819
- }
820
- alive.push(s);
821
- }
822
- return alive;
823
- }
824
-
825
3591
  // src/commands/status.ts
826
3592
  function fmtTime(ms) {
827
3593
  const d = new Date(ms);
@@ -903,7 +3669,8 @@ function collectStatus(now = Date.now()) {
903
3669
  for (const tool of config.enabledTools) {
904
3670
  const adapter = adapterFor(registry, tool);
905
3671
  if (!adapter) continue;
906
- const configPath2 = adapter.configPath();
3672
+ const configPath2 = adapter.configPath(tool);
3673
+ if (!configPath2) continue;
907
3674
  const installed = adapter.isInstalled(configPath2, hookPath);
908
3675
  tools.push({
909
3676
  tool,
@@ -936,10 +3703,12 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
936
3703
  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
937
3704
  deskbuddy enable <tool[,tool]> \u5B89\u88C5\u91C7\u96C6 hook\uFF08\u5982 claude_code,codex\uFF09
938
3705
  deskbuddy disable [tool[,tool]] \u79FB\u9664\u91C7\u96C6 hook\uFF08\u4E0D\u5E26\u53C2\u6570=\u5168\u90E8\uFF09
3706
+ 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
939
3707
  deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001\u4E0A\u62A5\u4E86\u54EA\u4E9B\u5B57\u6BB5
940
3708
  deskbuddy logout [--yes] \u79FB\u9664 hook \u5E76\u6E05\u9664\u672C\u673A\u6570\u636E
941
3709
 
942
- \u88C5\u5B8C\u6CA1\u6709\u4EFB\u4F55\u5E38\u9A7B\u8FDB\u7A0B\uFF1A\u53EA\u662F\u5F80\u5404\u667A\u80FD\u4F53\u7684 hook \u914D\u7F6E\u91CC\u52A0\u4E86\u4E00\u6761\u8BB0\u5F55\u3002
3710
+ \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
+ \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
943
3712
  \u5F53\u524D\u8FDE\u7684\u662F\u54EA\u4E2A\u73AF\u5883\uFF0C\u7528 deskbuddy status \u770B\u7B2C\u4E8C\u884C\u3002
944
3713
  `;
945
3714
  async function main(argv) {
@@ -968,6 +3737,8 @@ async function main(argv) {
968
3737
  return runEnable(parseTools(arg));
969
3738
  case "disable":
970
3739
  return runDisable(parseTools(arg));
3740
+ case "daemon":
3741
+ return runDaemonCommand({ foreground: argv.includes("--foreground") || argv.includes("-f") });
971
3742
  case "status":
972
3743
  return runStatus();
973
3744
  case "logout":