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 +316 -7
- package/dist/server.js +316 -7
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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",
|