@co0ontty/wand 2.3.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "b2955462eae33bdb72522709e3b5961ba769ce0f",
3
- "builtAt": "2026-07-01T23:51:19.505Z",
4
- "version": "2.3.1",
2
+ "commit": "066cdba4a30243d0950ceeb4988754f198bec6db",
3
+ "builtAt": "2026-07-04T05:33:22.797Z",
4
+ "version": "2.3.2",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { request as httpsRequest } from "node:https";
7
7
  import net from "node:net";
8
8
  import process from "node:process";
9
9
  import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
10
- import { isPidAlive, readLiveInstance, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
10
+ import { isPidAlive, readPidfile, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
12
  async function main() {
13
13
  const args = process.argv.slice(2);
@@ -392,9 +392,29 @@ async function handlePortInUse(config, configPath, useTui) {
392
392
  return true;
393
393
  }
394
394
  async function discoverAttachableInstance(configPath) {
395
- const live = readLiveInstance(configPath);
396
- if (live)
397
- return live;
395
+ const live = readPidfile(configPath);
396
+ if (live && live.pid !== process.pid) {
397
+ if (!isPidAlive(live.pid)) {
398
+ removePidfile(configPath);
399
+ removeSocketFile(configPath);
400
+ }
401
+ else if (live.socket && existsSync(live.socket)) {
402
+ const snapshot = await readIpcSnapshot(live.socket);
403
+ if (snapshot &&
404
+ snapshot.header.pid === live.pid &&
405
+ snapshot.header.configPath === configPath) {
406
+ return pidInfoFromSnapshot(snapshot);
407
+ }
408
+ if (!pidCommandLooksLikeWand(live.pid, configPath)) {
409
+ removePidfile(configPath);
410
+ removeSocketFile(configPath);
411
+ }
412
+ }
413
+ else if (!pidCommandLooksLikeWand(live.pid, configPath)) {
414
+ removePidfile(configPath);
415
+ removeSocketFile(configPath);
416
+ }
417
+ }
398
418
  const sockPath = socketPath(configPath);
399
419
  if (!sockPath || !existsSync(sockPath))
400
420
  return null;
@@ -404,8 +424,13 @@ async function discoverAttachableInstance(configPath) {
404
424
  const pid = snapshot.header.pid;
405
425
  if (!isPidAlive(pid))
406
426
  return null;
427
+ if (snapshot.header.configPath !== configPath)
428
+ return null;
429
+ return pidInfoFromSnapshot(snapshot);
430
+ }
431
+ function pidInfoFromSnapshot(snapshot) {
407
432
  return {
408
- pid,
433
+ pid: snapshot.header.pid,
409
434
  version: snapshot.header.version,
410
435
  startedAt: snapshot.header.startedAtMs,
411
436
  url: snapshot.header.url,
@@ -413,9 +438,28 @@ async function discoverAttachableInstance(configPath) {
413
438
  bindAddr: snapshot.header.bindAddr,
414
439
  configPath: snapshot.header.configPath,
415
440
  dbPath: snapshot.header.dbPath,
416
- socket: sockPath,
441
+ socket: socketPath(snapshot.header.configPath),
417
442
  };
418
443
  }
444
+ function pidCommandLooksLikeWand(pid, configPath) {
445
+ try {
446
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
447
+ encoding: "utf8",
448
+ timeout: 1000,
449
+ });
450
+ if (result.status !== 0)
451
+ return false;
452
+ const command = result.stdout || "";
453
+ return (command.includes(configPath) &&
454
+ (/\bwand\b/.test(command) ||
455
+ command.includes("/wand ") ||
456
+ command.includes("/cli.js web") ||
457
+ command.includes("src/cli.ts web")));
458
+ }
459
+ catch {
460
+ return false;
461
+ }
462
+ }
419
463
  function readIpcSnapshot(sockPath) {
420
464
  return new Promise((resolve) => {
421
465
  const sock = net.createConnection({ path: sockPath });
@@ -18,8 +18,9 @@ export interface RelaunchPlan {
18
18
  /**
19
19
  * 计算重启方式。
20
20
  *
21
- * - systemd 托管(存在 INVOCATION_ID)且已装服务 → "exit-only":仅退出,由 unit 里
22
- * (更新自修复后可能刚被重写的)ExecStart 重新拉起,避免 spawn 抢 pidfile 的竞态。
21
+ * - systemd / launchd 托管且已装服务 → "exit-only":仅退出,由 unit/plist
22
+ * (更新自修复后可能刚被重写的)ExecStart/ProgramArguments 重新拉起,
23
+ * 避免 spawn 抢 pidfile 的竞态。
23
24
  * - 否则 → "spawn":bin 优先用刚装好的全局 CLI(更新后能跑到新版),回退 argv[1]。
24
25
  */
25
26
  export declare function computeRelaunch(opts: {
package/dist/relaunch.js CHANGED
@@ -2,13 +2,15 @@ import process from "node:process";
2
2
  /**
3
3
  * 计算重启方式。
4
4
  *
5
- * - systemd 托管(存在 INVOCATION_ID)且已装服务 → "exit-only":仅退出,由 unit 里
6
- * (更新自修复后可能刚被重写的)ExecStart 重新拉起,避免 spawn 抢 pidfile 的竞态。
5
+ * - systemd / launchd 托管且已装服务 → "exit-only":仅退出,由 unit/plist
6
+ * (更新自修复后可能刚被重写的)ExecStart/ProgramArguments 重新拉起,
7
+ * 避免 spawn 抢 pidfile 的竞态。
7
8
  * - 否则 → "spawn":bin 优先用刚装好的全局 CLI(更新后能跑到新版),回退 argv[1]。
8
9
  */
9
10
  export function computeRelaunch(opts) {
10
11
  const managedBySystemd = !!process.env.INVOCATION_ID;
11
- if (managedBySystemd && opts.serviceInstalled) {
12
+ const managedByLaunchd = process.platform === "darwin" && process.env.XPC_SERVICE_NAME === "com.wand.web";
13
+ if ((managedBySystemd || managedByLaunchd) && opts.serviceInstalled) {
12
14
  return { mode: "exit-only" };
13
15
  }
14
16
  const bin = opts.globalCli ?? process.argv[1] ?? "";
@@ -65,7 +65,7 @@ export function thinkingEffortToCodexReasoningEffort(effort) {
65
65
  switch (effort) {
66
66
  case "standard": return "low";
67
67
  case "deep": return "medium";
68
- case "max": return "high";
68
+ case "max": return "xhigh";
69
69
  case "off": return "minimal";
70
70
  default: return null;
71
71
  }
@@ -1128,7 +1128,7 @@ export class StructuredSessionManager {
1128
1128
  if (modelChoice && modelChoice !== "default") {
1129
1129
  args.push("--model", modelChoice);
1130
1130
  }
1131
- // 思考深度 → model_reasoning_effort(off → minimal,standard → low,deep → medium,max → high
1131
+ // 思考深度 → model_reasoning_effort(off → minimal,standard → low,deep → medium,max → xhigh
1132
1132
  // Newer Codex CLI versions removed the old dedicated exec flag, but still
1133
1133
  // accept config overrides through `-c`.
1134
1134
  const reasoningEffort = thinkingEffortToCodexReasoningEffort(session.thinkingEffort);
@@ -2765,12 +2765,16 @@ export class StructuredSessionManager {
2765
2765
  const aggregatedOutput = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
2766
2766
  const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
2767
2767
  const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
2768
+ const input = { command, status };
2769
+ if (exitCode !== null)
2770
+ input.exit_code = exitCode;
2768
2771
  if (!completed) {
2769
2772
  return [{
2770
2773
  type: "tool_use",
2771
2774
  id,
2772
2775
  name: "Bash",
2773
- input: { command, status },
2776
+ description: "running",
2777
+ input,
2774
2778
  }];
2775
2779
  }
2776
2780
  // codex 的 status 可能是 declined(sandbox 拒了命令)/ failed(执行失败)—
@@ -2780,12 +2784,21 @@ export class StructuredSessionManager {
2780
2784
  const fallbackText = status === "declined"
2781
2785
  ? "command declined by sandbox"
2782
2786
  : (exitCode === null ? "" : `exit_code: ${exitCode}`);
2783
- return [{
2787
+ return [
2788
+ {
2789
+ type: "tool_use",
2790
+ id,
2791
+ name: "Bash",
2792
+ description: exitCode === null ? status : `${status} · exit ${exitCode}`,
2793
+ input,
2794
+ },
2795
+ {
2784
2796
  type: "tool_result",
2785
2797
  tool_use_id: id,
2786
2798
  content: aggregatedOutput || fallbackText,
2787
2799
  is_error: isError,
2788
- }];
2800
+ },
2801
+ ];
2789
2802
  }
2790
2803
  if (type === "file_change") {
2791
2804
  // 注意:codex exec stream 没有 old_string/new_string——只给 path + kind。
@@ -2806,16 +2819,16 @@ export class StructuredSessionManager {
2806
2819
  let input;
2807
2820
  if (kind === "add") {
2808
2821
  toolName = "Write";
2809
- input = { file_path: path, content: "" };
2822
+ input = { file_path: path, content: "", kind, status };
2810
2823
  }
2811
2824
  else if (kind === "delete") {
2812
2825
  // 复用 Bash 终端卡,rm 语义直观
2813
2826
  toolName = "Bash";
2814
- input = { command: `rm ${path}`, description: `delete ${path}`, status };
2827
+ input = { command: `rm ${path}`, description: `delete ${path}`, kind, status };
2815
2828
  }
2816
2829
  else {
2817
2830
  toolName = "Edit";
2818
- input = { file_path: path, old_string: "", new_string: "" };
2831
+ input = { file_path: path, old_string: "", new_string: "", kind, status };
2819
2832
  }
2820
2833
  if (!completed) {
2821
2834
  blocks.push({ type: "tool_use", id: subId, name: toolName, input });
@@ -2839,12 +2852,14 @@ export class StructuredSessionManager {
2839
2852
  const errObj = item.error && typeof item.error === "object" ? item.error : null;
2840
2853
  const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
2841
2854
  const isError = !!errObj || status === "failed";
2855
+ const input = { ...args, status };
2842
2856
  if (!completed) {
2843
2857
  return [{
2844
2858
  type: "tool_use",
2845
2859
  id,
2846
2860
  name: `${server}__${tool}`,
2847
- input: args,
2861
+ description: status,
2862
+ input,
2848
2863
  }];
2849
2864
  }
2850
2865
  let resultText = "";
@@ -2856,29 +2871,58 @@ export class StructuredSessionManager {
2856
2871
  const inner = this.extractCodexText(resultRec.content);
2857
2872
  resultText = inner || JSON.stringify(resultRec).slice(0, 4096);
2858
2873
  }
2859
- return [{
2874
+ return [
2875
+ {
2876
+ type: "tool_use",
2877
+ id,
2878
+ name: `${server}__${tool}`,
2879
+ description: status,
2880
+ input,
2881
+ },
2882
+ {
2860
2883
  type: "tool_result",
2861
2884
  tool_use_id: id,
2862
2885
  content: resultText,
2863
2886
  is_error: isError,
2864
- }];
2887
+ },
2888
+ ];
2865
2889
  }
2866
2890
  if (type === "web_search") {
2867
2891
  const query = typeof item.query === "string" ? item.query : "";
2892
+ const action = item.action && typeof item.action === "object" ? item.action : null;
2893
+ const actionType = action && typeof action.type === "string" ? action.type : "";
2894
+ const queries = action && Array.isArray(action.queries)
2895
+ ? action.queries.filter((v) => typeof v === "string")
2896
+ : [];
2897
+ const input = { query };
2898
+ if (actionType)
2899
+ input.action = actionType;
2900
+ if (queries.length > 0)
2901
+ input.queries = queries;
2868
2902
  if (!completed) {
2869
2903
  return [{
2870
2904
  type: "tool_use",
2871
2905
  id,
2872
2906
  name: "WebSearch",
2873
- input: { query },
2907
+ description: actionType || "searching",
2908
+ input,
2874
2909
  }];
2875
2910
  }
2876
- return [{
2911
+ return [
2912
+ {
2913
+ type: "tool_use",
2914
+ id,
2915
+ name: "WebSearch",
2916
+ description: actionType || "completed",
2917
+ input,
2918
+ },
2919
+ {
2877
2920
  type: "tool_result",
2878
2921
  tool_use_id: id,
2879
2922
  // codex 不在 exec 流里回 search 结果,这里给个占位让 UI 卡片完成态。
2880
- content: query ? `query: ${query}` : "",
2881
- }];
2923
+ content: queries.length > 0 ? queries.map((q) => `query: ${q}`).join("\n") : (query ? `query: ${query}` : ""),
2924
+ },
2925
+ ];
2882
2926
  }
2883
2927
  if (type === "collab_tool_call") {
2884
2928
  // codex 的子-agent 编排(spawn_agent / send_input / wait / close_agent)。
@@ -5,8 +5,7 @@
5
5
  * 日志面板:因为日志在主进程里,attach 端没法直接看;这里改成"活动流"——
6
6
  * 监听 snapshot 差分,把会话起止 / 总数变化打到 log 面板。
7
7
  */
8
- import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, uninstallService, } from "./commands.js";
9
- import { spawnSync } from "node:child_process";
8
+ import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, serviceRestart, uninstallService, } from "./commands.js";
10
9
  import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
11
10
  import { IpcClient } from "./ipc-client.js";
12
11
  import { buildLayout } from "./layout.js";
@@ -159,36 +158,18 @@ export function startAttachTui(deps) {
159
158
  void handleUninstallService(); });
160
159
  async function handleRestart() {
161
160
  const installed = safeServiceInstalled();
162
- if (installed && process.platform === "linux") {
161
+ if (installed) {
163
162
  const ok = await layout.confirm({
164
163
  title: "重启 wand 服务",
165
- body: "将执行 systemctl --user restart wand.service。当前 attach 会话会随主进程重启被踢掉。",
164
+ body: "将重启已安装的 wand systemd / launchd 服务。当前 attach 会话会随主进程重启短暂断开。",
166
165
  });
167
166
  if (!ok)
168
167
  return;
169
- layout.showToast("systemctl --user restart wand.service …", "info", 3000);
170
- const r = spawnSync("systemctl", ["--user", "restart", "wand.service"], { encoding: "utf8" });
171
- if (r.status === 0) {
172
- layout.showToast("已请求重启,IPC 会自动重连", "success", 3000);
173
- }
174
- else {
175
- layout.showToast(`systemctl 失败 (exit ${r.status})`, "error", 4000);
176
- layout.showDetail("systemctl 输出", (r.stdout || "") + "\n" + (r.stderr || ""));
177
- }
178
- return;
179
- }
180
- if (installed && process.platform === "darwin") {
181
- const ok = await layout.confirm({
182
- title: "重启 wand 服务",
183
- body: "将依次 launchctl unload / load com.wand.web。",
184
- });
185
- if (!ok)
186
- return;
187
- const plist = `${process.env.HOME}/Library/LaunchAgents/com.wand.web.plist`;
188
- const u = spawnSync("launchctl", ["unload", plist], { encoding: "utf8" });
189
- const l = spawnSync("launchctl", ["load", plist], { encoding: "utf8" });
190
- const ok2 = u.status === 0 && l.status === 0;
191
- layout.showToast(ok2 ? "已请求 launchd 重启" : "launchctl 调用失败", ok2 ? "success" : "error", 3500);
168
+ layout.showToast("正在请求服务重启…", "info", 3000);
169
+ const r = await runOffMicrotask(() => serviceRestart());
170
+ layout.showToast(r.ok ? "已请求重启,IPC 会自动重连" : r.message, r.ok ? "success" : "error", 4000);
171
+ if (r.detail)
172
+ layout.showDetail(r.ok ? "服务重启输出" : "服务重启失败", r.detail);
192
173
  return;
193
174
  }
194
175
  // 没注册成服务:尝试通过 IPC 让主进程自我退出,再由用户手动重启
@@ -156,6 +156,8 @@ function clipboardCandidates() {
156
156
  { cmd: "xsel", args: ["--clipboard", "--input"] },
157
157
  ];
158
158
  }
159
+ // ─── 系统服务(systemd system / user / launchd) ─────────────────────────
160
+ const LAUNCHD_LABEL = "com.wand.web";
159
161
  export const DEFAULT_SERVICE_SCOPE = "system";
160
162
  /** 当前 process 是不是 root(POSIX)。Windows 永远返回 false。 */
161
163
  function isRoot() {
@@ -314,26 +316,35 @@ function launchdStatus(scope) {
314
316
  platform: "darwin",
315
317
  };
316
318
  }
317
- // launchctl list 输出三列:PID Status Label
318
- const list = spawnSync("launchctl", ["list", "com.wand.web"], { encoding: "utf8" });
319
- if (list.status !== 0) {
319
+ const target = launchdTarget(scope);
320
+ const printed = spawnSync("launchctl", ["print", target], { encoding: "utf8", timeout: 10_000 });
321
+ if (printed.status !== 0) {
320
322
  return {
321
323
  installed: true,
322
324
  state: "inactive",
323
- description: `[${scope}] loaded 但未在运行(launchctl list 找不到)`,
324
- raw: list.stderr || "",
325
+ description: `[${scope}] installed 但未 bootstrap · ${target}`,
326
+ raw: ((printed.stdout || "") + "\n" + (printed.stderr || "")).trim(),
325
327
  platform: "darwin",
326
328
  };
327
329
  }
328
- const text = list.stdout;
329
- const pidMatch = text.match(/"PID"\s*=\s*(\d+);/);
330
- const exitMatch = text.match(/"LastExitStatus"\s*=\s*(-?\d+);/);
331
- const pid = pidMatch ? Number(pidMatch[1]) : 0;
332
- const lastExit = exitMatch ? Number(exitMatch[1]) : 0;
333
- const desc = pid > 0 ? `[${scope}] running · PID ${pid}` : `[${scope}] stopped (last exit=${lastExit})`;
330
+ const text = printed.stdout || "";
331
+ const state = matchLaunchdField(text, "state") || "loaded";
332
+ const pid = Number(matchLaunchdField(text, "pid") || "0");
333
+ const lastExit = matchLaunchdField(text, "last exit code");
334
+ const normalized = pid > 0 || state === "running"
335
+ ? "active"
336
+ : state === "failed"
337
+ ? "failed"
338
+ : "inactive";
339
+ const tail = pid > 0
340
+ ? ` · PID ${pid}`
341
+ : lastExit
342
+ ? ` · last exit ${lastExit}`
343
+ : "";
344
+ const desc = `[${scope}] ${state}${tail} · ${target}`;
334
345
  return {
335
346
  installed: true,
336
- state: pid > 0 ? "active" : "inactive",
347
+ state: normalized,
337
348
  description: desc,
338
349
  raw: text,
339
350
  platform: "darwin",
@@ -361,39 +372,114 @@ function launchctlLoad(scope) {
361
372
  const plist = servicePathFor(scope);
362
373
  if (!existsSync(plist))
363
374
  return { ok: false, message: `未安装 (${plist} 不存在)` };
364
- const r = spawnSync("launchctl", ["load", "-w", plist], { encoding: "utf8", timeout: 10_000 });
365
- if (r.status === 0)
366
- return { ok: true, message: `已 launchctl load (${scope})` };
367
- return {
368
- ok: false,
369
- message: `launchctl load 失败 (exit ${r.status})`,
370
- detail: ((r.stdout || "") + "\n" + (r.stderr || "")).trim(),
371
- };
375
+ return launchdBootstrap(scope, plist, `已启动 launchd ${scope}`);
372
376
  }
373
377
  function launchctlUnload(scope) {
374
378
  const plist = servicePathFor(scope);
375
379
  if (!existsSync(plist))
376
380
  return { ok: false, message: `未安装 (${plist} 不存在)` };
377
- const r = spawnSync("launchctl", ["unload", plist], { encoding: "utf8", timeout: 10_000 });
378
- if (r.status === 0)
379
- return { ok: true, message: `已 launchctl unload (${scope})` };
381
+ return launchdBootout(scope, plist, `已停止 launchd ${scope}`);
382
+ }
383
+ function launchdRestart(scope) {
384
+ const target = launchdTarget(scope);
385
+ const kicked = spawnSync("launchctl", ["kickstart", "-k", target], { encoding: "utf8", timeout: 10_000 });
386
+ if (kicked.status === 0)
387
+ return { ok: true, message: `已重启 launchd ${scope}: ${target}` };
388
+ const started = launchctlLoad(scope);
389
+ if (started.ok)
390
+ return { ok: true, message: `已 bootstrap 并启动 launchd ${scope}: ${target}` };
380
391
  return {
381
392
  ok: false,
382
- message: `launchctl unload 失败 (exit ${r.status})`,
383
- detail: ((r.stdout || "") + "\n" + (r.stderr || "")).trim(),
393
+ message: `launchd 重启失败 (${target})`,
394
+ detail: [
395
+ `kickstart: ${formatSpawnResult(kicked)}`,
396
+ `bootstrap: ${started.message}`,
397
+ started.detail ?? "",
398
+ ].filter(Boolean).join("\n"),
384
399
  };
385
400
  }
386
- function launchdRestart(scope) {
387
- const stop = launchctlUnload(scope);
388
- const start = launchctlLoad(scope);
389
- if (stop.ok && start.ok)
390
- return { ok: true, message: `已 launchd 重启 (${scope})` };
401
+ function launchdDomain(scope) {
402
+ return scope === "user" ? `gui/${currentUid()}` : "system";
403
+ }
404
+ function launchdTarget(scope) {
405
+ return `${launchdDomain(scope)}/${LAUNCHD_LABEL}`;
406
+ }
407
+ function currentUid() {
408
+ const fn = process.getuid;
409
+ if (typeof fn === "function") {
410
+ try {
411
+ return fn.call(process);
412
+ }
413
+ catch { /* fall through */ }
414
+ }
415
+ const id = spawnSync("id", ["-u"], { encoding: "utf8", timeout: 3000 });
416
+ const uid = Number((id.stdout || "").trim());
417
+ return Number.isInteger(uid) && uid >= 0 ? uid : 0;
418
+ }
419
+ function matchLaunchdField(text, field) {
420
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
421
+ const match = text.match(new RegExp(`^\\s*${escaped} = (.+)$`, "m"));
422
+ return match?.[1]?.trim() ?? null;
423
+ }
424
+ function launchdBootstrap(scope, plist, successMessage) {
425
+ const domain = launchdDomain(scope);
426
+ const target = launchdTarget(scope);
427
+ const bootout = spawnSync("launchctl", ["bootout", domain, plist], { encoding: "utf8", timeout: 10_000 });
428
+ const bootstrap = spawnSync("launchctl", ["bootstrap", domain, plist], { encoding: "utf8", timeout: 10_000 });
429
+ const enable = spawnSync("launchctl", ["enable", target], { encoding: "utf8", timeout: 10_000 });
430
+ const kickstart = spawnSync("launchctl", ["kickstart", "-k", target], { encoding: "utf8", timeout: 10_000 });
431
+ const detail = [
432
+ `target: ${target}`,
433
+ `bootout: ${formatSpawnResult(bootout)}`,
434
+ `bootstrap: ${formatSpawnResult(bootstrap)}`,
435
+ `enable: ${formatSpawnResult(enable)}`,
436
+ `kickstart: ${formatSpawnResult(kickstart)}`,
437
+ ].join("\n");
438
+ if (bootstrap.status !== 0 && !launchdIsAlreadyBootstrapped(bootstrap)) {
439
+ return { ok: false, message: `launchctl bootstrap 失败 (${target})`, detail };
440
+ }
441
+ if (enable.status !== 0) {
442
+ return { ok: false, message: `launchctl enable 失败 (${target})`, detail };
443
+ }
444
+ if (kickstart.status !== 0) {
445
+ const status = launchdStatus(scope);
446
+ if (status.state !== "active") {
447
+ return { ok: false, message: `launchctl kickstart 失败 (${target})`, detail };
448
+ }
449
+ }
450
+ return { ok: true, message: successMessage, detail };
451
+ }
452
+ function launchdBootout(scope, plist, successMessage) {
453
+ const domain = launchdDomain(scope);
454
+ const target = launchdTarget(scope);
455
+ const bootout = spawnSync("launchctl", ["bootout", domain, plist], { encoding: "utf8", timeout: 10_000 });
456
+ if (bootout.status === 0 || launchdIsNotBootstrapped(bootout)) {
457
+ return {
458
+ ok: true,
459
+ message: successMessage,
460
+ detail: `target: ${target}\nbootout: ${formatSpawnResult(bootout)}`,
461
+ };
462
+ }
391
463
  return {
392
464
  ok: false,
393
- message: "launchd 重启失败",
394
- detail: `unload: ${stop.message}\nload: ${start.message}`,
465
+ message: `launchctl bootout 失败 (${target})`,
466
+ detail: formatSpawnResult(bootout),
395
467
  };
396
468
  }
469
+ function launchdIsAlreadyBootstrapped(result) {
470
+ const text = `${result.stdout || ""}\n${result.stderr || ""}`;
471
+ return /already bootstrapped|service already loaded|Bootstrap failed:\s*5/i.test(text);
472
+ }
473
+ function launchdIsNotBootstrapped(result) {
474
+ const text = `${result.stdout || ""}\n${result.stderr || ""}`;
475
+ return /No such process|service is not loaded|Bootstrap failed:\s*3/i.test(text);
476
+ }
477
+ function formatSpawnResult(result) {
478
+ const text = ((result.stdout || "") + "\n" + (result.stderr || "")).trim();
479
+ return result.status === 0
480
+ ? "ok"
481
+ : `failed (${text || `exit ${result.status}`})`;
482
+ }
397
483
  function unsupported() {
398
484
  return { ok: false, message: `当前平台 ${process.platform} 不支持服务管理` };
399
485
  }
@@ -714,17 +800,18 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
714
800
  catch (err) {
715
801
  return { ok: false, message: `写入 plist 失败: ${getErrorMessage(err)}` };
716
802
  }
717
- const load = spawnSync("launchctl", ["load", "-w", plistPath], { encoding: "utf8" });
718
- if (load.status !== 0) {
803
+ const started = launchdBootstrap(scope, plistPath, `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}`);
804
+ if (!started.ok) {
719
805
  return {
720
806
  ok: false,
721
- message: "已写入 plist,但 launchctl load 失败",
722
- detail: load.stderr.trim() || `exit ${load.status}`,
807
+ message: "已写入 plist,但 launchctl 启动失败",
808
+ detail: started.detail,
723
809
  };
724
810
  }
725
811
  return {
726
812
  ok: true,
727
813
  message: `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}`,
814
+ detail: started.detail,
728
815
  };
729
816
  }
730
817
  function uninstallLaunchdService(scope) {
@@ -732,7 +819,8 @@ function uninstallLaunchdService(scope) {
732
819
  if (!existsSync(plistPath)) {
733
820
  return { ok: false, message: `未检测到已安装的 launchd ${scope} 服务` };
734
821
  }
735
- const unload = spawnSync("launchctl", ["unload", "-w", plistPath], { encoding: "utf8" });
822
+ const stopped = launchdBootout(scope, plistPath, `已停止 launchd ${scope}`);
823
+ const disabled = spawnSync("launchctl", ["disable", launchdTarget(scope)], { encoding: "utf8", timeout: 10_000 });
736
824
  try {
737
825
  unlinkSync(plistPath);
738
826
  }
@@ -742,7 +830,10 @@ function uninstallLaunchdService(scope) {
742
830
  return {
743
831
  ok: true,
744
832
  message: `已卸载 launchd ${scope === "user" ? "用户代理" : "系统守护"}`,
745
- detail: unload.status === 0 ? "unload: ok" : `unload: ${unload.stderr.trim()}`,
833
+ detail: [
834
+ stopped.detail ?? stopped.message,
835
+ `disable: ${formatSpawnResult(disabled)}`,
836
+ ].join("\n"),
746
837
  };
747
838
  }
748
839
  // ─── 工具 ────────────────────────────────────────────────────────────────
package/dist/types.d.ts CHANGED
@@ -441,7 +441,7 @@ export interface SessionSnapshot {
441
441
  * - off: 不覆盖默认思考深度(SDK: 不传 thinking;Claude CLI: auto/default;Codex: model_reasoning_effort minimal)
442
442
  * - standard: 标准(SDK: budget 4096;Claude CLI: low;Codex: low)
443
443
  * - deep: 深度(SDK: budget 16000;Claude CLI: medium;Codex: medium)
444
- * - max: 最深(SDK: budget 31999;Claude CLI: max;Codex: high
444
+ * - max: 最深(SDK: budget 31999;Claude CLI: max;Codex: xhigh
445
445
  */
446
446
  thinkingEffort?: "off" | "standard" | "deep" | "max" | null;
447
447
  /** 当前 PTY 列宽,由最近一次 resize 决定。前端用它来判断本端 fit 是否需要校准。 */