sessix-server 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1650,7 +1650,14 @@ var SessionManager = class {
1650
1650
  runningStartedAt = /* @__PURE__ */ new Map();
1651
1651
  /** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
1652
1652
  pendingAssistantEvents = /* @__PURE__ */ new Map();
1653
- /** 标记哪些会话的缓冲区曾被截断(溢出过 BUFFER_MAX) */
1653
+ /**
1654
+ * 标记哪些会话的缓冲区不能代表完整历史,需要从 JSONL 补全。
1655
+ * 两种情况会被标记:
1656
+ * 1. 缓冲区溢出过 BUFFER_MAX(旧事件被丢弃)
1657
+ * 2. 会话是通过 --resume 启动的(缓冲区只有恢复后的新事件,完整历史在 JSONL 中)
1658
+ * 例如:服务器重启后用户继续聊天,sendMessage 走 resume 路径再次创建会话,
1659
+ * 此时 buffer 只有 system init + 少量新事件,不能用它替换手机端已加载的完整 turns。
1660
+ */
1654
1661
  bufferTruncated = /* @__PURE__ */ new Set();
1655
1662
  /** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
1656
1663
  sessionProjectPaths = /* @__PURE__ */ new Map();
@@ -1708,6 +1715,9 @@ var SessionManager = class {
1708
1715
  this.sessionAgentType.set(session.id, resolvedAgentType);
1709
1716
  this.lastBroadcastStatus.set(session.id, session.status);
1710
1717
  this.sessionProjectPaths.set(session.id, projectPath);
1718
+ if (resumeSessionId) {
1719
+ this.bufferTruncated.add(session.id);
1720
+ }
1711
1721
  this.unsubscribeSession(session.id);
1712
1722
  this.subscribeToSession(session.id);
1713
1723
  console.log(`[SessionManager] Session created: ${session.id} (project: ${projectPath})`);
@@ -1840,6 +1850,54 @@ var SessionManager = class {
1840
1850
  return stats ? { ...session, stats } : session;
1841
1851
  });
1842
1852
  }
1853
+ /**
1854
+ * 接入 ApprovalProxy 的非阻塞 hook 通知,将其映射为 ServerEvent 转发。
1855
+ *
1856
+ * 仅转发为 shared 类型定义中的字段,不把 hook 原始 payload 透传出去(隐私 + 体积)。
1857
+ * 当前覆盖:
1858
+ * - PreCompact (`type: 'compact'`) → `session_compact`
1859
+ * - PermissionDenied (`type: 'permission_denied'`) → `permission_denied`
1860
+ * - Subagent (`type: 'subagent'`) → `subagent_event`(骨架预留,本任务不发)
1861
+ */
1862
+ attachApprovalProxy(approvalProxy) {
1863
+ approvalProxy.onNotify((notification) => {
1864
+ const serverEvent = this.mapHookNotificationToServerEvent(notification);
1865
+ if (serverEvent) this.emit(serverEvent);
1866
+ });
1867
+ }
1868
+ /** 将 hook 通知映射为 ServerEvent;不识别的 type 返回 null */
1869
+ mapHookNotificationToServerEvent(n) {
1870
+ switch (n.type) {
1871
+ case "compact": {
1872
+ const subtype = n.subtype === "completed" || n.subtype === "blocked" ? n.subtype : "started";
1873
+ return { type: "session_compact", sessionId: n.sessionId, subtype };
1874
+ }
1875
+ case "permission_denied": {
1876
+ const source = n.source === "hook" || n.source === "rule" ? n.source : "classifier";
1877
+ return {
1878
+ type: "permission_denied",
1879
+ sessionId: n.sessionId,
1880
+ toolName: n.toolName ?? "unknown",
1881
+ reason: n.reason ?? "",
1882
+ source
1883
+ };
1884
+ }
1885
+ case "subagent": {
1886
+ const phase = n.subtype === "completed" ? "completed" : "started";
1887
+ return {
1888
+ type: "subagent_event",
1889
+ sessionId: n.sessionId,
1890
+ parentToolUseId: typeof n.parentToolUseId === "string" ? n.parentToolUseId : "",
1891
+ subAgentId: typeof n.subAgentId === "string" ? n.subAgentId : "",
1892
+ phase,
1893
+ task: typeof n.task === "string" ? n.task : void 0
1894
+ };
1895
+ }
1896
+ default:
1897
+ console.warn(`[SessionManager] Unknown hook notification type: ${n.type}`);
1898
+ return null;
1899
+ }
1900
+ }
1843
1901
  /**
1844
1902
  * 注册事件回调(事件会被转发到 WsBridge)
1845
1903
  *
@@ -2525,6 +2583,8 @@ var ApprovalProxy = class _ApprovalProxy {
2525
2583
  approvalRequestCallbacks = [];
2526
2584
  /** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
2527
2585
  approvalResolvedCallbacks = [];
2586
+ /** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
2587
+ notifyCallbacks = [];
2528
2588
  /** YOLO 模式状态:sessionId -> enabled */
2529
2589
  yoloSessions = /* @__PURE__ */ new Map();
2530
2590
  /** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
@@ -2586,6 +2646,24 @@ var ApprovalProxy = class _ApprovalProxy {
2586
2646
  }
2587
2647
  }
2588
2648
  }
2649
+ /**
2650
+ * 注册非阻塞 hook 通知回调(如 PreCompact、PermissionDenied)
2651
+ *
2652
+ * 这些 hook 不需要返回决策给 Claude Code,仅作为 ServerEvent 转发到手机端。
2653
+ */
2654
+ onNotify(callback) {
2655
+ this.notifyCallbacks.push(callback);
2656
+ }
2657
+ /** 触发所有 notify 回调(内部调用) */
2658
+ fireNotify(notification) {
2659
+ for (const callback of this.notifyCallbacks) {
2660
+ try {
2661
+ callback(notification);
2662
+ } catch (err) {
2663
+ console.error("[ApprovalProxy] Notify callback error:", err);
2664
+ }
2665
+ }
2666
+ }
2589
2667
  /** 设置状态信息提供者(用于 /health 端点) */
2590
2668
  setStatusInfoProvider(provider) {
2591
2669
  this.statusInfoProvider = provider;
@@ -2770,6 +2848,8 @@ var ApprovalProxy = class _ApprovalProxy {
2770
2848
  const pathname = url.pathname;
2771
2849
  if (req.method === "POST" && pathname === "/hook/approval") {
2772
2850
  this.handleApprovalHook(req, res);
2851
+ } else if (req.method === "POST" && pathname === "/hook/notify") {
2852
+ this.handleHookNotify(req, res);
2773
2853
  } else if (req.method === "POST" && pathname === "/pair") {
2774
2854
  this.handlePair(req, res);
2775
2855
  } else if (req.method === "GET" && pathname === "/health") {
@@ -2835,6 +2915,51 @@ var ApprovalProxy = class _ApprovalProxy {
2835
2915
  this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
2836
2916
  }
2837
2917
  }
2918
+ /**
2919
+ * 非阻塞 hook 通知端点
2920
+ *
2921
+ * 用于 PreCompact、PostCompact、PermissionDenied 等 fire-and-forget 的 hook:
2922
+ * 立即返回 {"ok":true},再异步分发到 notifyCallbacks(最终广播为 ServerEvent)。
2923
+ *
2924
+ * 鉴权策略:仅允许 loopback(本机 hook 脚本)访问,避免远端注入伪造事件。
2925
+ */
2926
+ async handleHookNotify(req, res) {
2927
+ if (!this.isLoopbackRequest(req)) {
2928
+ this.sendJson(res, 403, { ok: false, reason: "forbidden" });
2929
+ return;
2930
+ }
2931
+ try {
2932
+ const body = await this.parseJsonBody(req);
2933
+ const sessionId = String(body.sessionId ?? "").trim();
2934
+ const type = String(body.type ?? "").trim();
2935
+ if (!sessionId || !type) {
2936
+ this.sendJson(res, 400, { error: "sessionId and type are required" });
2937
+ return;
2938
+ }
2939
+ const notification = {
2940
+ sessionId,
2941
+ type
2942
+ };
2943
+ if (typeof body.subtype === "string") notification.subtype = body.subtype;
2944
+ if (typeof body.toolName === "string") notification.toolName = body.toolName;
2945
+ if (typeof body.reason === "string") notification.reason = body.reason;
2946
+ if (typeof body.source === "string") notification.source = body.source;
2947
+ if (typeof body.parentToolUseId === "string") notification.parentToolUseId = body.parentToolUseId;
2948
+ if (typeof body.subAgentId === "string") notification.subAgentId = body.subAgentId;
2949
+ if (body.phase === "started" || body.phase === "completed") notification.phase = body.phase;
2950
+ if (typeof body.task === "string") notification.task = body.task;
2951
+ this.sendJson(res, 200, { ok: true });
2952
+ setImmediate(() => this.fireNotify(notification));
2953
+ } catch (err) {
2954
+ console.error("[ApprovalProxy] Hook notify failed:", err);
2955
+ this.sendJson(res, 200, { ok: false });
2956
+ }
2957
+ }
2958
+ /** 判断请求是否来自本机 loopback(127.0.0.1 / ::1 / IPv4-mapped IPv6) */
2959
+ isLoopbackRequest(req) {
2960
+ const remoteAddress = req.socket.remoteAddress;
2961
+ return remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
2962
+ }
2838
2963
  /** 健康检查端点 */
2839
2964
  handleHealth(_req, res) {
2840
2965
  const info = this.statusInfoProvider?.() ?? { connections: 0, activeSessions: 0 };
@@ -2867,9 +2992,7 @@ var ApprovalProxy = class _ApprovalProxy {
2867
2992
  }
2868
2993
  /** 返回连接 token(仅本机访问) */
2869
2994
  handleToken(req, res) {
2870
- const remoteAddress = req.socket.remoteAddress;
2871
- const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
2872
- if (!isLocal) {
2995
+ if (!this.isLoopbackRequest(req)) {
2873
2996
  this.sendJson(res, 403, { error: t("approval.forbidden") });
2874
2997
  return;
2875
2998
  }
@@ -3097,9 +3220,15 @@ var import_node_os6 = require("os");
3097
3220
  var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
3098
3221
  var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
3099
3222
  var PERMISSION_ACCEPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
3223
+ var COMPACT_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "compact-hook.js");
3224
+ var POST_COMPACT_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "post-compact-hook.js");
3225
+ var PERMISSION_DENIED_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-denied-hook.js");
3100
3226
  var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
3101
3227
  var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
3102
3228
  var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
3229
+ var COMPACT_HOOK_COMMAND = "node ~/.sessix/hooks/compact-hook.js";
3230
+ var POST_COMPACT_HOOK_COMMAND = "node ~/.sessix/hooks/post-compact-hook.js";
3231
+ var PERMISSION_DENIED_HOOK_COMMAND = "node ~/.sessix/hooks/permission-denied-hook.js";
3103
3232
  var LEGACY_HOOK_COMMANDS = [
3104
3233
  "~/.sessix/hooks/approval-hook.sh",
3105
3234
  "~/.sessix/hooks/permission-accept.sh"
@@ -3159,6 +3288,112 @@ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
3159
3288
  process.stdout.write('{"decision":"allow"}\\n')
3160
3289
  process.exit(0)
3161
3290
  `;
3291
+ var COMPACT_HOOK_TEMPLATE = `#!/usr/bin/env node
3292
+ // Sessix PreCompact \u901A\u77E5 hook
3293
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
3294
+
3295
+ const sessionId = process.env.SESSIX_SESSION_ID
3296
+ if (!sessionId) {
3297
+ process.stdout.write('{}')
3298
+ process.exit(0)
3299
+ }
3300
+
3301
+ let raw = ''
3302
+ process.stdin.on('data', (chunk) => { raw += chunk })
3303
+ process.stdin.on('end', () => {
3304
+ // \u8BFB\u53D6\u5E76\u4E22\u5F03 stdin payload\uFF08PreCompact \u6807\u51C6\u8F93\u5165\uFF09\uFF0Cserver \u7AEF\u4E0D\u9700\u8981\u5B83
3305
+ try { JSON.parse(raw) } catch {}
3306
+ // fire-and-forget\uFF1A\u4E0D\u7B49\u5F85\u54CD\u5E94\uFF0C\u907F\u514D\u963B\u585E compact
3307
+ try {
3308
+ fetch('http://localhost:3746/hook/notify', {
3309
+ method: 'POST',
3310
+ headers: { 'Content-Type': 'application/json' },
3311
+ body: JSON.stringify({
3312
+ sessionId,
3313
+ type: 'compact',
3314
+ subtype: 'started',
3315
+ }),
3316
+ signal: AbortSignal.timeout(2000),
3317
+ }).catch(() => {})
3318
+ } catch {}
3319
+ // \u7ACB\u5373\u8FD4\u56DE\u7A7A JSON\uFF0C\u4E0D\u963B\u585E compact
3320
+ process.stdout.write('{}')
3321
+ process.exit(0)
3322
+ })
3323
+ `;
3324
+ var POST_COMPACT_HOOK_TEMPLATE = `#!/usr/bin/env node
3325
+ // Sessix PostCompact \u901A\u77E5 hook
3326
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
3327
+
3328
+ const sessionId = process.env.SESSIX_SESSION_ID
3329
+ if (!sessionId) {
3330
+ process.stdout.write('{}')
3331
+ process.exit(0)
3332
+ }
3333
+
3334
+ let raw = ''
3335
+ process.stdin.on('data', (chunk) => { raw += chunk })
3336
+ process.stdin.on('end', () => {
3337
+ // \u8BFB\u53D6\u5E76\u4E22\u5F03 stdin payload\uFF0Cserver \u7AEF\u4E0D\u9700\u8981\u5B83
3338
+ try { JSON.parse(raw) } catch {}
3339
+ // fire-and-forget\uFF1A\u4E0D\u7B49\u5F85\u54CD\u5E94
3340
+ try {
3341
+ fetch('http://localhost:3746/hook/notify', {
3342
+ method: 'POST',
3343
+ headers: { 'Content-Type': 'application/json' },
3344
+ body: JSON.stringify({
3345
+ sessionId,
3346
+ type: 'compact',
3347
+ subtype: 'completed',
3348
+ }),
3349
+ signal: AbortSignal.timeout(2000),
3350
+ }).catch(() => {})
3351
+ } catch {}
3352
+ process.stdout.write('{}')
3353
+ process.exit(0)
3354
+ })
3355
+ `;
3356
+ var PERMISSION_DENIED_HOOK_TEMPLATE = `#!/usr/bin/env node
3357
+ // Sessix PermissionDenied \u901A\u77E5 hook
3358
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
3359
+
3360
+ const sessionId = process.env.SESSIX_SESSION_ID
3361
+ if (!sessionId) {
3362
+ process.stdout.write('{}')
3363
+ process.exit(0)
3364
+ }
3365
+
3366
+ let raw = ''
3367
+ process.stdin.on('data', (chunk) => { raw += chunk })
3368
+ process.stdin.on('end', () => {
3369
+ let payload = {}
3370
+ try { payload = JSON.parse(raw) } catch {}
3371
+ const toolName = String(payload.tool_name || 'unknown')
3372
+ // Claude Code PermissionDenied \u7531 classifier \u89E6\u53D1\uFF0C\u6240\u4EE5 source \u9ED8\u8BA4 'classifier'
3373
+ const reason = String(
3374
+ payload.reason ||
3375
+ payload.permission_decision_reason ||
3376
+ payload.permissionDecisionReason ||
3377
+ ''
3378
+ )
3379
+ try {
3380
+ fetch('http://localhost:3746/hook/notify', {
3381
+ method: 'POST',
3382
+ headers: { 'Content-Type': 'application/json' },
3383
+ body: JSON.stringify({
3384
+ sessionId,
3385
+ type: 'permission_denied',
3386
+ toolName,
3387
+ reason,
3388
+ source: 'classifier',
3389
+ }),
3390
+ signal: AbortSignal.timeout(2000),
3391
+ }).catch(() => {})
3392
+ } catch {}
3393
+ process.stdout.write('{}')
3394
+ process.exit(0)
3395
+ })
3396
+ `;
3162
3397
  var HookInstaller = class {
3163
3398
  /**
3164
3399
  * 安装 hook
@@ -3172,8 +3407,14 @@ var HookInstaller = class {
3172
3407
  await (0, import_promises2.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
3173
3408
  await (0, import_promises2.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
3174
3409
  await (0, import_promises2.writeFile)(PERMISSION_ACCEPT_PATH, PERMISSION_ACCEPT_TEMPLATE, "utf-8");
3410
+ await (0, import_promises2.writeFile)(COMPACT_HOOK_PATH, COMPACT_HOOK_TEMPLATE, "utf-8");
3411
+ await (0, import_promises2.writeFile)(POST_COMPACT_HOOK_PATH, POST_COMPACT_HOOK_TEMPLATE, "utf-8");
3412
+ await (0, import_promises2.writeFile)(PERMISSION_DENIED_HOOK_PATH, PERMISSION_DENIED_HOOK_TEMPLATE, "utf-8");
3175
3413
  await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
3176
3414
  await (0, import_promises2.chmod)(PERMISSION_ACCEPT_PATH, 493);
3415
+ await (0, import_promises2.chmod)(COMPACT_HOOK_PATH, 493);
3416
+ await (0, import_promises2.chmod)(POST_COMPACT_HOOK_PATH, 493);
3417
+ await (0, import_promises2.chmod)(PERMISSION_DENIED_HOOK_PATH, 493);
3177
3418
  await this.addHookToSettings();
3178
3419
  console.log("[HookInstaller] Hook installation complete");
3179
3420
  }
@@ -3199,6 +3440,9 @@ var HookInstaller = class {
3199
3440
  async isInstalled() {
3200
3441
  let approvalScriptContent = "";
3201
3442
  let permissionScriptExists = false;
3443
+ let compactScriptExists = false;
3444
+ let postCompactScriptExists = false;
3445
+ let permissionDeniedScriptExists = false;
3202
3446
  try {
3203
3447
  approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
3204
3448
  } catch {
@@ -3208,10 +3452,25 @@ var HookInstaller = class {
3208
3452
  permissionScriptExists = true;
3209
3453
  } catch {
3210
3454
  }
3455
+ try {
3456
+ await (0, import_promises2.access)(COMPACT_HOOK_PATH);
3457
+ compactScriptExists = true;
3458
+ } catch {
3459
+ }
3460
+ try {
3461
+ await (0, import_promises2.access)(POST_COMPACT_HOOK_PATH);
3462
+ postCompactScriptExists = true;
3463
+ } catch {
3464
+ }
3465
+ try {
3466
+ await (0, import_promises2.access)(PERMISSION_DENIED_HOOK_PATH);
3467
+ permissionDeniedScriptExists = true;
3468
+ } catch {
3469
+ }
3211
3470
  const isLatestVersion = approvalScriptContent.includes("permissionDecision");
3212
3471
  const settings = await this.readClaudeSettings();
3213
3472
  const configExists = this.hasHookConfig(settings);
3214
- return isLatestVersion && permissionScriptExists && configExists;
3473
+ return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
3215
3474
  }
3216
3475
  // ============================================
3217
3476
  // 内部方法
@@ -3249,6 +3508,36 @@ var HookInstaller = class {
3249
3508
  });
3250
3509
  changed = true;
3251
3510
  }
3511
+ if (!this.hasPreCompactConfig(settings)) {
3512
+ if (!settings.hooks.PreCompact) {
3513
+ settings.hooks.PreCompact = [];
3514
+ }
3515
+ settings.hooks.PreCompact.push({
3516
+ matcher: "",
3517
+ hooks: [{ type: "command", command: COMPACT_HOOK_COMMAND }]
3518
+ });
3519
+ changed = true;
3520
+ }
3521
+ if (!this.hasPostCompactConfig(settings)) {
3522
+ if (!settings.hooks.PostCompact) {
3523
+ settings.hooks.PostCompact = [];
3524
+ }
3525
+ settings.hooks.PostCompact.push({
3526
+ matcher: "",
3527
+ hooks: [{ type: "command", command: POST_COMPACT_HOOK_COMMAND }]
3528
+ });
3529
+ changed = true;
3530
+ }
3531
+ if (!this.hasPermissionDeniedConfig(settings)) {
3532
+ if (!settings.hooks.PermissionDenied) {
3533
+ settings.hooks.PermissionDenied = [];
3534
+ }
3535
+ settings.hooks.PermissionDenied.push({
3536
+ matcher: "",
3537
+ hooks: [{ type: "command", command: PERMISSION_DENIED_HOOK_COMMAND }]
3538
+ });
3539
+ changed = true;
3540
+ }
3252
3541
  if (changed) {
3253
3542
  await this.writeClaudeSettings(settings);
3254
3543
  } else {
@@ -3263,6 +3552,9 @@ var HookInstaller = class {
3263
3552
  if (!settings.hooks) return;
3264
3553
  this.removeHookCommand(settings, "PreToolUse", HOOK_COMMAND);
3265
3554
  this.removeHookCommand(settings, "PermissionRequest", PERMISSION_ACCEPT_COMMAND);
3555
+ this.removeHookCommand(settings, "PreCompact", COMPACT_HOOK_COMMAND);
3556
+ this.removeHookCommand(settings, "PostCompact", POST_COMPACT_HOOK_COMMAND);
3557
+ this.removeHookCommand(settings, "PermissionDenied", PERMISSION_DENIED_HOOK_COMMAND);
3266
3558
  if (Object.keys(settings.hooks).length === 0) {
3267
3559
  delete settings.hooks;
3268
3560
  }
@@ -3300,7 +3592,7 @@ var HookInstaller = class {
3300
3592
  * 检查 settings 中是否已包含所有 Sessix hook 配置
3301
3593
  */
3302
3594
  hasHookConfig(settings) {
3303
- return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
3595
+ return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings) && this.hasPreCompactConfig(settings) && this.hasPostCompactConfig(settings) && this.hasPermissionDeniedConfig(settings);
3304
3596
  }
3305
3597
  /** 检查 PreToolUse 中是否有 approval-hook.js */
3306
3598
  hasPreToolUseConfig(settings) {
@@ -3310,6 +3602,18 @@ var HookInstaller = class {
3310
3602
  hasPermissionRequestConfig(settings) {
3311
3603
  return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
3312
3604
  }
3605
+ /** 检查 PreCompact 中是否有 compact-hook.js */
3606
+ hasPreCompactConfig(settings) {
3607
+ return this.hasHookEntry(settings?.hooks?.PreCompact, COMPACT_HOOK_COMMAND);
3608
+ }
3609
+ /** 检查 PostCompact 中是否有 post-compact-hook.js */
3610
+ hasPostCompactConfig(settings) {
3611
+ return this.hasHookEntry(settings?.hooks?.PostCompact, POST_COMPACT_HOOK_COMMAND);
3612
+ }
3613
+ /** 检查 PermissionDenied 中是否有 permission-denied-hook.js */
3614
+ hasPermissionDeniedConfig(settings) {
3615
+ return this.hasHookEntry(settings?.hooks?.PermissionDenied, PERMISSION_DENIED_HOOK_COMMAND);
3616
+ }
3313
3617
  /** 检查 hook 数组中是否包含指定命令 */
3314
3618
  hasHookEntry(hookArray, command) {
3315
3619
  if (!Array.isArray(hookArray)) return false;
@@ -4747,7 +5051,7 @@ ${e.stderr ?? ""}`);
4747
5051
  if (destinationKind === "simulator") {
4748
5052
  installCmd = ["xcrun", "simctl", "install", destinationId, appPath];
4749
5053
  } else if (destinationKind === "device") {
4750
- installCmd = ["xcrun", "devicectl", "device", "install", "app", "--device-id", destinationId, appPath];
5054
+ installCmd = ["xcrun", "devicectl", "device", "install", "app", "--device", destinationId, appPath];
4751
5055
  } else if (destinationKind === "mac") {
4752
5056
  installCmd = ["open", appPath];
4753
5057
  } else {
@@ -5039,6 +5343,7 @@ async function start(opts = {}) {
5039
5343
  onStateChange: (state) => mdnsService?.updatePairingState(state)
5040
5344
  });
5041
5345
  approvalProxy.setPairingManager(pairingManager);
5346
+ sessionManager.attachApprovalProxy(approvalProxy);
5042
5347
  const authManager = new AuthManager();
5043
5348
  authManager.on("login_url", (url) => {
5044
5349
  wsBridge.broadcast({ type: "auth_login_url", url });
@@ -5435,6 +5740,10 @@ async function start(opts = {}) {
5435
5740
  await xcodeBuildExecutor.install(event.sessionId, event.projectPath);
5436
5741
  break;
5437
5742
  }
5743
+ case "xcode_install_kill": {
5744
+ xcodeBuildExecutor.killInstall(event.installId);
5745
+ break;
5746
+ }
5438
5747
  default: {
5439
5748
  wsBridge.send(ws, {
5440
5749
  type: "error",
package/dist/server.js CHANGED
@@ -1655,7 +1655,14 @@ var SessionManager = class {
1655
1655
  runningStartedAt = /* @__PURE__ */ new Map();
1656
1656
  /** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
1657
1657
  pendingAssistantEvents = /* @__PURE__ */ new Map();
1658
- /** 标记哪些会话的缓冲区曾被截断(溢出过 BUFFER_MAX) */
1658
+ /**
1659
+ * 标记哪些会话的缓冲区不能代表完整历史,需要从 JSONL 补全。
1660
+ * 两种情况会被标记:
1661
+ * 1. 缓冲区溢出过 BUFFER_MAX(旧事件被丢弃)
1662
+ * 2. 会话是通过 --resume 启动的(缓冲区只有恢复后的新事件,完整历史在 JSONL 中)
1663
+ * 例如:服务器重启后用户继续聊天,sendMessage 走 resume 路径再次创建会话,
1664
+ * 此时 buffer 只有 system init + 少量新事件,不能用它替换手机端已加载的完整 turns。
1665
+ */
1659
1666
  bufferTruncated = /* @__PURE__ */ new Set();
1660
1667
  /** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
1661
1668
  sessionProjectPaths = /* @__PURE__ */ new Map();
@@ -1713,6 +1720,9 @@ var SessionManager = class {
1713
1720
  this.sessionAgentType.set(session.id, resolvedAgentType);
1714
1721
  this.lastBroadcastStatus.set(session.id, session.status);
1715
1722
  this.sessionProjectPaths.set(session.id, projectPath);
1723
+ if (resumeSessionId) {
1724
+ this.bufferTruncated.add(session.id);
1725
+ }
1716
1726
  this.unsubscribeSession(session.id);
1717
1727
  this.subscribeToSession(session.id);
1718
1728
  console.log(`[SessionManager] Session created: ${session.id} (project: ${projectPath})`);
@@ -1845,6 +1855,54 @@ var SessionManager = class {
1845
1855
  return stats ? { ...session, stats } : session;
1846
1856
  });
1847
1857
  }
1858
+ /**
1859
+ * 接入 ApprovalProxy 的非阻塞 hook 通知,将其映射为 ServerEvent 转发。
1860
+ *
1861
+ * 仅转发为 shared 类型定义中的字段,不把 hook 原始 payload 透传出去(隐私 + 体积)。
1862
+ * 当前覆盖:
1863
+ * - PreCompact (`type: 'compact'`) → `session_compact`
1864
+ * - PermissionDenied (`type: 'permission_denied'`) → `permission_denied`
1865
+ * - Subagent (`type: 'subagent'`) → `subagent_event`(骨架预留,本任务不发)
1866
+ */
1867
+ attachApprovalProxy(approvalProxy) {
1868
+ approvalProxy.onNotify((notification) => {
1869
+ const serverEvent = this.mapHookNotificationToServerEvent(notification);
1870
+ if (serverEvent) this.emit(serverEvent);
1871
+ });
1872
+ }
1873
+ /** 将 hook 通知映射为 ServerEvent;不识别的 type 返回 null */
1874
+ mapHookNotificationToServerEvent(n) {
1875
+ switch (n.type) {
1876
+ case "compact": {
1877
+ const subtype = n.subtype === "completed" || n.subtype === "blocked" ? n.subtype : "started";
1878
+ return { type: "session_compact", sessionId: n.sessionId, subtype };
1879
+ }
1880
+ case "permission_denied": {
1881
+ const source = n.source === "hook" || n.source === "rule" ? n.source : "classifier";
1882
+ return {
1883
+ type: "permission_denied",
1884
+ sessionId: n.sessionId,
1885
+ toolName: n.toolName ?? "unknown",
1886
+ reason: n.reason ?? "",
1887
+ source
1888
+ };
1889
+ }
1890
+ case "subagent": {
1891
+ const phase = n.subtype === "completed" ? "completed" : "started";
1892
+ return {
1893
+ type: "subagent_event",
1894
+ sessionId: n.sessionId,
1895
+ parentToolUseId: typeof n.parentToolUseId === "string" ? n.parentToolUseId : "",
1896
+ subAgentId: typeof n.subAgentId === "string" ? n.subAgentId : "",
1897
+ phase,
1898
+ task: typeof n.task === "string" ? n.task : void 0
1899
+ };
1900
+ }
1901
+ default:
1902
+ console.warn(`[SessionManager] Unknown hook notification type: ${n.type}`);
1903
+ return null;
1904
+ }
1905
+ }
1848
1906
  /**
1849
1907
  * 注册事件回调(事件会被转发到 WsBridge)
1850
1908
  *
@@ -2530,6 +2588,8 @@ var ApprovalProxy = class _ApprovalProxy {
2530
2588
  approvalRequestCallbacks = [];
2531
2589
  /** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
2532
2590
  approvalResolvedCallbacks = [];
2591
+ /** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
2592
+ notifyCallbacks = [];
2533
2593
  /** YOLO 模式状态:sessionId -> enabled */
2534
2594
  yoloSessions = /* @__PURE__ */ new Map();
2535
2595
  /** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
@@ -2591,6 +2651,24 @@ var ApprovalProxy = class _ApprovalProxy {
2591
2651
  }
2592
2652
  }
2593
2653
  }
2654
+ /**
2655
+ * 注册非阻塞 hook 通知回调(如 PreCompact、PermissionDenied)
2656
+ *
2657
+ * 这些 hook 不需要返回决策给 Claude Code,仅作为 ServerEvent 转发到手机端。
2658
+ */
2659
+ onNotify(callback) {
2660
+ this.notifyCallbacks.push(callback);
2661
+ }
2662
+ /** 触发所有 notify 回调(内部调用) */
2663
+ fireNotify(notification) {
2664
+ for (const callback of this.notifyCallbacks) {
2665
+ try {
2666
+ callback(notification);
2667
+ } catch (err) {
2668
+ console.error("[ApprovalProxy] Notify callback error:", err);
2669
+ }
2670
+ }
2671
+ }
2594
2672
  /** 设置状态信息提供者(用于 /health 端点) */
2595
2673
  setStatusInfoProvider(provider) {
2596
2674
  this.statusInfoProvider = provider;
@@ -2775,6 +2853,8 @@ var ApprovalProxy = class _ApprovalProxy {
2775
2853
  const pathname = url.pathname;
2776
2854
  if (req.method === "POST" && pathname === "/hook/approval") {
2777
2855
  this.handleApprovalHook(req, res);
2856
+ } else if (req.method === "POST" && pathname === "/hook/notify") {
2857
+ this.handleHookNotify(req, res);
2778
2858
  } else if (req.method === "POST" && pathname === "/pair") {
2779
2859
  this.handlePair(req, res);
2780
2860
  } else if (req.method === "GET" && pathname === "/health") {
@@ -2840,6 +2920,51 @@ var ApprovalProxy = class _ApprovalProxy {
2840
2920
  this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
2841
2921
  }
2842
2922
  }
2923
+ /**
2924
+ * 非阻塞 hook 通知端点
2925
+ *
2926
+ * 用于 PreCompact、PostCompact、PermissionDenied 等 fire-and-forget 的 hook:
2927
+ * 立即返回 {"ok":true},再异步分发到 notifyCallbacks(最终广播为 ServerEvent)。
2928
+ *
2929
+ * 鉴权策略:仅允许 loopback(本机 hook 脚本)访问,避免远端注入伪造事件。
2930
+ */
2931
+ async handleHookNotify(req, res) {
2932
+ if (!this.isLoopbackRequest(req)) {
2933
+ this.sendJson(res, 403, { ok: false, reason: "forbidden" });
2934
+ return;
2935
+ }
2936
+ try {
2937
+ const body = await this.parseJsonBody(req);
2938
+ const sessionId = String(body.sessionId ?? "").trim();
2939
+ const type = String(body.type ?? "").trim();
2940
+ if (!sessionId || !type) {
2941
+ this.sendJson(res, 400, { error: "sessionId and type are required" });
2942
+ return;
2943
+ }
2944
+ const notification = {
2945
+ sessionId,
2946
+ type
2947
+ };
2948
+ if (typeof body.subtype === "string") notification.subtype = body.subtype;
2949
+ if (typeof body.toolName === "string") notification.toolName = body.toolName;
2950
+ if (typeof body.reason === "string") notification.reason = body.reason;
2951
+ if (typeof body.source === "string") notification.source = body.source;
2952
+ if (typeof body.parentToolUseId === "string") notification.parentToolUseId = body.parentToolUseId;
2953
+ if (typeof body.subAgentId === "string") notification.subAgentId = body.subAgentId;
2954
+ if (body.phase === "started" || body.phase === "completed") notification.phase = body.phase;
2955
+ if (typeof body.task === "string") notification.task = body.task;
2956
+ this.sendJson(res, 200, { ok: true });
2957
+ setImmediate(() => this.fireNotify(notification));
2958
+ } catch (err) {
2959
+ console.error("[ApprovalProxy] Hook notify failed:", err);
2960
+ this.sendJson(res, 200, { ok: false });
2961
+ }
2962
+ }
2963
+ /** 判断请求是否来自本机 loopback(127.0.0.1 / ::1 / IPv4-mapped IPv6) */
2964
+ isLoopbackRequest(req) {
2965
+ const remoteAddress = req.socket.remoteAddress;
2966
+ return remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
2967
+ }
2843
2968
  /** 健康检查端点 */
2844
2969
  handleHealth(_req, res) {
2845
2970
  const info = this.statusInfoProvider?.() ?? { connections: 0, activeSessions: 0 };
@@ -2872,9 +2997,7 @@ var ApprovalProxy = class _ApprovalProxy {
2872
2997
  }
2873
2998
  /** 返回连接 token(仅本机访问) */
2874
2999
  handleToken(req, res) {
2875
- const remoteAddress = req.socket.remoteAddress;
2876
- const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
2877
- if (!isLocal) {
3000
+ if (!this.isLoopbackRequest(req)) {
2878
3001
  this.sendJson(res, 403, { error: t("approval.forbidden") });
2879
3002
  return;
2880
3003
  }
@@ -3102,9 +3225,15 @@ var import_node_os6 = require("os");
3102
3225
  var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
3103
3226
  var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
3104
3227
  var PERMISSION_ACCEPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
3228
+ var COMPACT_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "compact-hook.js");
3229
+ var POST_COMPACT_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "post-compact-hook.js");
3230
+ var PERMISSION_DENIED_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-denied-hook.js");
3105
3231
  var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
3106
3232
  var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
3107
3233
  var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
3234
+ var COMPACT_HOOK_COMMAND = "node ~/.sessix/hooks/compact-hook.js";
3235
+ var POST_COMPACT_HOOK_COMMAND = "node ~/.sessix/hooks/post-compact-hook.js";
3236
+ var PERMISSION_DENIED_HOOK_COMMAND = "node ~/.sessix/hooks/permission-denied-hook.js";
3108
3237
  var LEGACY_HOOK_COMMANDS = [
3109
3238
  "~/.sessix/hooks/approval-hook.sh",
3110
3239
  "~/.sessix/hooks/permission-accept.sh"
@@ -3164,6 +3293,112 @@ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
3164
3293
  process.stdout.write('{"decision":"allow"}\\n')
3165
3294
  process.exit(0)
3166
3295
  `;
3296
+ var COMPACT_HOOK_TEMPLATE = `#!/usr/bin/env node
3297
+ // Sessix PreCompact \u901A\u77E5 hook
3298
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
3299
+
3300
+ const sessionId = process.env.SESSIX_SESSION_ID
3301
+ if (!sessionId) {
3302
+ process.stdout.write('{}')
3303
+ process.exit(0)
3304
+ }
3305
+
3306
+ let raw = ''
3307
+ process.stdin.on('data', (chunk) => { raw += chunk })
3308
+ process.stdin.on('end', () => {
3309
+ // \u8BFB\u53D6\u5E76\u4E22\u5F03 stdin payload\uFF08PreCompact \u6807\u51C6\u8F93\u5165\uFF09\uFF0Cserver \u7AEF\u4E0D\u9700\u8981\u5B83
3310
+ try { JSON.parse(raw) } catch {}
3311
+ // fire-and-forget\uFF1A\u4E0D\u7B49\u5F85\u54CD\u5E94\uFF0C\u907F\u514D\u963B\u585E compact
3312
+ try {
3313
+ fetch('http://localhost:3746/hook/notify', {
3314
+ method: 'POST',
3315
+ headers: { 'Content-Type': 'application/json' },
3316
+ body: JSON.stringify({
3317
+ sessionId,
3318
+ type: 'compact',
3319
+ subtype: 'started',
3320
+ }),
3321
+ signal: AbortSignal.timeout(2000),
3322
+ }).catch(() => {})
3323
+ } catch {}
3324
+ // \u7ACB\u5373\u8FD4\u56DE\u7A7A JSON\uFF0C\u4E0D\u963B\u585E compact
3325
+ process.stdout.write('{}')
3326
+ process.exit(0)
3327
+ })
3328
+ `;
3329
+ var POST_COMPACT_HOOK_TEMPLATE = `#!/usr/bin/env node
3330
+ // Sessix PostCompact \u901A\u77E5 hook
3331
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
3332
+
3333
+ const sessionId = process.env.SESSIX_SESSION_ID
3334
+ if (!sessionId) {
3335
+ process.stdout.write('{}')
3336
+ process.exit(0)
3337
+ }
3338
+
3339
+ let raw = ''
3340
+ process.stdin.on('data', (chunk) => { raw += chunk })
3341
+ process.stdin.on('end', () => {
3342
+ // \u8BFB\u53D6\u5E76\u4E22\u5F03 stdin payload\uFF0Cserver \u7AEF\u4E0D\u9700\u8981\u5B83
3343
+ try { JSON.parse(raw) } catch {}
3344
+ // fire-and-forget\uFF1A\u4E0D\u7B49\u5F85\u54CD\u5E94
3345
+ try {
3346
+ fetch('http://localhost:3746/hook/notify', {
3347
+ method: 'POST',
3348
+ headers: { 'Content-Type': 'application/json' },
3349
+ body: JSON.stringify({
3350
+ sessionId,
3351
+ type: 'compact',
3352
+ subtype: 'completed',
3353
+ }),
3354
+ signal: AbortSignal.timeout(2000),
3355
+ }).catch(() => {})
3356
+ } catch {}
3357
+ process.stdout.write('{}')
3358
+ process.exit(0)
3359
+ })
3360
+ `;
3361
+ var PERMISSION_DENIED_HOOK_TEMPLATE = `#!/usr/bin/env node
3362
+ // Sessix PermissionDenied \u901A\u77E5 hook
3363
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
3364
+
3365
+ const sessionId = process.env.SESSIX_SESSION_ID
3366
+ if (!sessionId) {
3367
+ process.stdout.write('{}')
3368
+ process.exit(0)
3369
+ }
3370
+
3371
+ let raw = ''
3372
+ process.stdin.on('data', (chunk) => { raw += chunk })
3373
+ process.stdin.on('end', () => {
3374
+ let payload = {}
3375
+ try { payload = JSON.parse(raw) } catch {}
3376
+ const toolName = String(payload.tool_name || 'unknown')
3377
+ // Claude Code PermissionDenied \u7531 classifier \u89E6\u53D1\uFF0C\u6240\u4EE5 source \u9ED8\u8BA4 'classifier'
3378
+ const reason = String(
3379
+ payload.reason ||
3380
+ payload.permission_decision_reason ||
3381
+ payload.permissionDecisionReason ||
3382
+ ''
3383
+ )
3384
+ try {
3385
+ fetch('http://localhost:3746/hook/notify', {
3386
+ method: 'POST',
3387
+ headers: { 'Content-Type': 'application/json' },
3388
+ body: JSON.stringify({
3389
+ sessionId,
3390
+ type: 'permission_denied',
3391
+ toolName,
3392
+ reason,
3393
+ source: 'classifier',
3394
+ }),
3395
+ signal: AbortSignal.timeout(2000),
3396
+ }).catch(() => {})
3397
+ } catch {}
3398
+ process.stdout.write('{}')
3399
+ process.exit(0)
3400
+ })
3401
+ `;
3167
3402
  var HookInstaller = class {
3168
3403
  /**
3169
3404
  * 安装 hook
@@ -3177,8 +3412,14 @@ var HookInstaller = class {
3177
3412
  await (0, import_promises2.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
3178
3413
  await (0, import_promises2.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
3179
3414
  await (0, import_promises2.writeFile)(PERMISSION_ACCEPT_PATH, PERMISSION_ACCEPT_TEMPLATE, "utf-8");
3415
+ await (0, import_promises2.writeFile)(COMPACT_HOOK_PATH, COMPACT_HOOK_TEMPLATE, "utf-8");
3416
+ await (0, import_promises2.writeFile)(POST_COMPACT_HOOK_PATH, POST_COMPACT_HOOK_TEMPLATE, "utf-8");
3417
+ await (0, import_promises2.writeFile)(PERMISSION_DENIED_HOOK_PATH, PERMISSION_DENIED_HOOK_TEMPLATE, "utf-8");
3180
3418
  await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
3181
3419
  await (0, import_promises2.chmod)(PERMISSION_ACCEPT_PATH, 493);
3420
+ await (0, import_promises2.chmod)(COMPACT_HOOK_PATH, 493);
3421
+ await (0, import_promises2.chmod)(POST_COMPACT_HOOK_PATH, 493);
3422
+ await (0, import_promises2.chmod)(PERMISSION_DENIED_HOOK_PATH, 493);
3182
3423
  await this.addHookToSettings();
3183
3424
  console.log("[HookInstaller] Hook installation complete");
3184
3425
  }
@@ -3204,6 +3445,9 @@ var HookInstaller = class {
3204
3445
  async isInstalled() {
3205
3446
  let approvalScriptContent = "";
3206
3447
  let permissionScriptExists = false;
3448
+ let compactScriptExists = false;
3449
+ let postCompactScriptExists = false;
3450
+ let permissionDeniedScriptExists = false;
3207
3451
  try {
3208
3452
  approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
3209
3453
  } catch {
@@ -3213,10 +3457,25 @@ var HookInstaller = class {
3213
3457
  permissionScriptExists = true;
3214
3458
  } catch {
3215
3459
  }
3460
+ try {
3461
+ await (0, import_promises2.access)(COMPACT_HOOK_PATH);
3462
+ compactScriptExists = true;
3463
+ } catch {
3464
+ }
3465
+ try {
3466
+ await (0, import_promises2.access)(POST_COMPACT_HOOK_PATH);
3467
+ postCompactScriptExists = true;
3468
+ } catch {
3469
+ }
3470
+ try {
3471
+ await (0, import_promises2.access)(PERMISSION_DENIED_HOOK_PATH);
3472
+ permissionDeniedScriptExists = true;
3473
+ } catch {
3474
+ }
3216
3475
  const isLatestVersion = approvalScriptContent.includes("permissionDecision");
3217
3476
  const settings = await this.readClaudeSettings();
3218
3477
  const configExists = this.hasHookConfig(settings);
3219
- return isLatestVersion && permissionScriptExists && configExists;
3478
+ return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
3220
3479
  }
3221
3480
  // ============================================
3222
3481
  // 内部方法
@@ -3254,6 +3513,36 @@ var HookInstaller = class {
3254
3513
  });
3255
3514
  changed = true;
3256
3515
  }
3516
+ if (!this.hasPreCompactConfig(settings)) {
3517
+ if (!settings.hooks.PreCompact) {
3518
+ settings.hooks.PreCompact = [];
3519
+ }
3520
+ settings.hooks.PreCompact.push({
3521
+ matcher: "",
3522
+ hooks: [{ type: "command", command: COMPACT_HOOK_COMMAND }]
3523
+ });
3524
+ changed = true;
3525
+ }
3526
+ if (!this.hasPostCompactConfig(settings)) {
3527
+ if (!settings.hooks.PostCompact) {
3528
+ settings.hooks.PostCompact = [];
3529
+ }
3530
+ settings.hooks.PostCompact.push({
3531
+ matcher: "",
3532
+ hooks: [{ type: "command", command: POST_COMPACT_HOOK_COMMAND }]
3533
+ });
3534
+ changed = true;
3535
+ }
3536
+ if (!this.hasPermissionDeniedConfig(settings)) {
3537
+ if (!settings.hooks.PermissionDenied) {
3538
+ settings.hooks.PermissionDenied = [];
3539
+ }
3540
+ settings.hooks.PermissionDenied.push({
3541
+ matcher: "",
3542
+ hooks: [{ type: "command", command: PERMISSION_DENIED_HOOK_COMMAND }]
3543
+ });
3544
+ changed = true;
3545
+ }
3257
3546
  if (changed) {
3258
3547
  await this.writeClaudeSettings(settings);
3259
3548
  } else {
@@ -3268,6 +3557,9 @@ var HookInstaller = class {
3268
3557
  if (!settings.hooks) return;
3269
3558
  this.removeHookCommand(settings, "PreToolUse", HOOK_COMMAND);
3270
3559
  this.removeHookCommand(settings, "PermissionRequest", PERMISSION_ACCEPT_COMMAND);
3560
+ this.removeHookCommand(settings, "PreCompact", COMPACT_HOOK_COMMAND);
3561
+ this.removeHookCommand(settings, "PostCompact", POST_COMPACT_HOOK_COMMAND);
3562
+ this.removeHookCommand(settings, "PermissionDenied", PERMISSION_DENIED_HOOK_COMMAND);
3271
3563
  if (Object.keys(settings.hooks).length === 0) {
3272
3564
  delete settings.hooks;
3273
3565
  }
@@ -3305,7 +3597,7 @@ var HookInstaller = class {
3305
3597
  * 检查 settings 中是否已包含所有 Sessix hook 配置
3306
3598
  */
3307
3599
  hasHookConfig(settings) {
3308
- return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
3600
+ return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings) && this.hasPreCompactConfig(settings) && this.hasPostCompactConfig(settings) && this.hasPermissionDeniedConfig(settings);
3309
3601
  }
3310
3602
  /** 检查 PreToolUse 中是否有 approval-hook.js */
3311
3603
  hasPreToolUseConfig(settings) {
@@ -3315,6 +3607,18 @@ var HookInstaller = class {
3315
3607
  hasPermissionRequestConfig(settings) {
3316
3608
  return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
3317
3609
  }
3610
+ /** 检查 PreCompact 中是否有 compact-hook.js */
3611
+ hasPreCompactConfig(settings) {
3612
+ return this.hasHookEntry(settings?.hooks?.PreCompact, COMPACT_HOOK_COMMAND);
3613
+ }
3614
+ /** 检查 PostCompact 中是否有 post-compact-hook.js */
3615
+ hasPostCompactConfig(settings) {
3616
+ return this.hasHookEntry(settings?.hooks?.PostCompact, POST_COMPACT_HOOK_COMMAND);
3617
+ }
3618
+ /** 检查 PermissionDenied 中是否有 permission-denied-hook.js */
3619
+ hasPermissionDeniedConfig(settings) {
3620
+ return this.hasHookEntry(settings?.hooks?.PermissionDenied, PERMISSION_DENIED_HOOK_COMMAND);
3621
+ }
3318
3622
  /** 检查 hook 数组中是否包含指定命令 */
3319
3623
  hasHookEntry(hookArray, command) {
3320
3624
  if (!Array.isArray(hookArray)) return false;
@@ -4752,7 +5056,7 @@ ${e.stderr ?? ""}`);
4752
5056
  if (destinationKind === "simulator") {
4753
5057
  installCmd = ["xcrun", "simctl", "install", destinationId, appPath];
4754
5058
  } else if (destinationKind === "device") {
4755
- installCmd = ["xcrun", "devicectl", "device", "install", "app", "--device-id", destinationId, appPath];
5059
+ installCmd = ["xcrun", "devicectl", "device", "install", "app", "--device", destinationId, appPath];
4756
5060
  } else if (destinationKind === "mac") {
4757
5061
  installCmd = ["open", appPath];
4758
5062
  } else {
@@ -5044,6 +5348,7 @@ async function start(opts = {}) {
5044
5348
  onStateChange: (state) => mdnsService?.updatePairingState(state)
5045
5349
  });
5046
5350
  approvalProxy.setPairingManager(pairingManager);
5351
+ sessionManager.attachApprovalProxy(approvalProxy);
5047
5352
  const authManager = new AuthManager();
5048
5353
  authManager.on("login_url", (url) => {
5049
5354
  wsBridge.broadcast({ type: "auth_login_url", url });
@@ -5440,6 +5745,10 @@ async function start(opts = {}) {
5440
5745
  await xcodeBuildExecutor.install(event.sessionId, event.projectPath);
5441
5746
  break;
5442
5747
  }
5748
+ case "xcode_install_kill": {
5749
+ xcodeBuildExecutor.killInstall(event.installId);
5750
+ break;
5751
+ }
5443
5752
  default: {
5444
5753
  wsBridge.send(ws, {
5445
5754
  type: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "bin": {
5
5
  "sessix-server": "dist/index.js"
6
6
  },