deskbuddy 0.1.0 → 0.2.0-beta.1

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