sessix-server 0.4.1 → 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 +300 -5
- package/dist/server.js +300 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1850,6 +1850,54 @@ var SessionManager = class {
|
|
|
1850
1850
|
return stats ? { ...session, stats } : session;
|
|
1851
1851
|
});
|
|
1852
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
|
+
}
|
|
1853
1901
|
/**
|
|
1854
1902
|
* 注册事件回调(事件会被转发到 WsBridge)
|
|
1855
1903
|
*
|
|
@@ -2535,6 +2583,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2535
2583
|
approvalRequestCallbacks = [];
|
|
2536
2584
|
/** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
|
|
2537
2585
|
approvalResolvedCallbacks = [];
|
|
2586
|
+
/** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
|
|
2587
|
+
notifyCallbacks = [];
|
|
2538
2588
|
/** YOLO 模式状态:sessionId -> enabled */
|
|
2539
2589
|
yoloSessions = /* @__PURE__ */ new Map();
|
|
2540
2590
|
/** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
|
|
@@ -2596,6 +2646,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2596
2646
|
}
|
|
2597
2647
|
}
|
|
2598
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
|
+
}
|
|
2599
2667
|
/** 设置状态信息提供者(用于 /health 端点) */
|
|
2600
2668
|
setStatusInfoProvider(provider) {
|
|
2601
2669
|
this.statusInfoProvider = provider;
|
|
@@ -2780,6 +2848,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2780
2848
|
const pathname = url.pathname;
|
|
2781
2849
|
if (req.method === "POST" && pathname === "/hook/approval") {
|
|
2782
2850
|
this.handleApprovalHook(req, res);
|
|
2851
|
+
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2852
|
+
this.handleHookNotify(req, res);
|
|
2783
2853
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2784
2854
|
this.handlePair(req, res);
|
|
2785
2855
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -2845,6 +2915,51 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2845
2915
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
2846
2916
|
}
|
|
2847
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
|
+
}
|
|
2848
2963
|
/** 健康检查端点 */
|
|
2849
2964
|
handleHealth(_req, res) {
|
|
2850
2965
|
const info = this.statusInfoProvider?.() ?? { connections: 0, activeSessions: 0 };
|
|
@@ -2877,9 +2992,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2877
2992
|
}
|
|
2878
2993
|
/** 返回连接 token(仅本机访问) */
|
|
2879
2994
|
handleToken(req, res) {
|
|
2880
|
-
|
|
2881
|
-
const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
|
|
2882
|
-
if (!isLocal) {
|
|
2995
|
+
if (!this.isLoopbackRequest(req)) {
|
|
2883
2996
|
this.sendJson(res, 403, { error: t("approval.forbidden") });
|
|
2884
2997
|
return;
|
|
2885
2998
|
}
|
|
@@ -3107,9 +3220,15 @@ var import_node_os6 = require("os");
|
|
|
3107
3220
|
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
3108
3221
|
var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
|
|
3109
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");
|
|
3110
3226
|
var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
|
|
3111
3227
|
var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
|
|
3112
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";
|
|
3113
3232
|
var LEGACY_HOOK_COMMANDS = [
|
|
3114
3233
|
"~/.sessix/hooks/approval-hook.sh",
|
|
3115
3234
|
"~/.sessix/hooks/permission-accept.sh"
|
|
@@ -3169,6 +3288,112 @@ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
|
|
|
3169
3288
|
process.stdout.write('{"decision":"allow"}\\n')
|
|
3170
3289
|
process.exit(0)
|
|
3171
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
|
+
`;
|
|
3172
3397
|
var HookInstaller = class {
|
|
3173
3398
|
/**
|
|
3174
3399
|
* 安装 hook
|
|
@@ -3182,8 +3407,14 @@ var HookInstaller = class {
|
|
|
3182
3407
|
await (0, import_promises2.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
|
|
3183
3408
|
await (0, import_promises2.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
|
|
3184
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");
|
|
3185
3413
|
await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
|
|
3186
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);
|
|
3187
3418
|
await this.addHookToSettings();
|
|
3188
3419
|
console.log("[HookInstaller] Hook installation complete");
|
|
3189
3420
|
}
|
|
@@ -3209,6 +3440,9 @@ var HookInstaller = class {
|
|
|
3209
3440
|
async isInstalled() {
|
|
3210
3441
|
let approvalScriptContent = "";
|
|
3211
3442
|
let permissionScriptExists = false;
|
|
3443
|
+
let compactScriptExists = false;
|
|
3444
|
+
let postCompactScriptExists = false;
|
|
3445
|
+
let permissionDeniedScriptExists = false;
|
|
3212
3446
|
try {
|
|
3213
3447
|
approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3214
3448
|
} catch {
|
|
@@ -3218,10 +3452,25 @@ var HookInstaller = class {
|
|
|
3218
3452
|
permissionScriptExists = true;
|
|
3219
3453
|
} catch {
|
|
3220
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
|
+
}
|
|
3221
3470
|
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
3222
3471
|
const settings = await this.readClaudeSettings();
|
|
3223
3472
|
const configExists = this.hasHookConfig(settings);
|
|
3224
|
-
return isLatestVersion && permissionScriptExists && configExists;
|
|
3473
|
+
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
|
|
3225
3474
|
}
|
|
3226
3475
|
// ============================================
|
|
3227
3476
|
// 内部方法
|
|
@@ -3259,6 +3508,36 @@ var HookInstaller = class {
|
|
|
3259
3508
|
});
|
|
3260
3509
|
changed = true;
|
|
3261
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
|
+
}
|
|
3262
3541
|
if (changed) {
|
|
3263
3542
|
await this.writeClaudeSettings(settings);
|
|
3264
3543
|
} else {
|
|
@@ -3273,6 +3552,9 @@ var HookInstaller = class {
|
|
|
3273
3552
|
if (!settings.hooks) return;
|
|
3274
3553
|
this.removeHookCommand(settings, "PreToolUse", HOOK_COMMAND);
|
|
3275
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);
|
|
3276
3558
|
if (Object.keys(settings.hooks).length === 0) {
|
|
3277
3559
|
delete settings.hooks;
|
|
3278
3560
|
}
|
|
@@ -3310,7 +3592,7 @@ var HookInstaller = class {
|
|
|
3310
3592
|
* 检查 settings 中是否已包含所有 Sessix hook 配置
|
|
3311
3593
|
*/
|
|
3312
3594
|
hasHookConfig(settings) {
|
|
3313
|
-
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
|
|
3595
|
+
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings) && this.hasPreCompactConfig(settings) && this.hasPostCompactConfig(settings) && this.hasPermissionDeniedConfig(settings);
|
|
3314
3596
|
}
|
|
3315
3597
|
/** 检查 PreToolUse 中是否有 approval-hook.js */
|
|
3316
3598
|
hasPreToolUseConfig(settings) {
|
|
@@ -3320,6 +3602,18 @@ var HookInstaller = class {
|
|
|
3320
3602
|
hasPermissionRequestConfig(settings) {
|
|
3321
3603
|
return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
|
|
3322
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
|
+
}
|
|
3323
3617
|
/** 检查 hook 数组中是否包含指定命令 */
|
|
3324
3618
|
hasHookEntry(hookArray, command) {
|
|
3325
3619
|
if (!Array.isArray(hookArray)) return false;
|
|
@@ -5049,6 +5343,7 @@ async function start(opts = {}) {
|
|
|
5049
5343
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
5050
5344
|
});
|
|
5051
5345
|
approvalProxy.setPairingManager(pairingManager);
|
|
5346
|
+
sessionManager.attachApprovalProxy(approvalProxy);
|
|
5052
5347
|
const authManager = new AuthManager();
|
|
5053
5348
|
authManager.on("login_url", (url) => {
|
|
5054
5349
|
wsBridge.broadcast({ type: "auth_login_url", url });
|
package/dist/server.js
CHANGED
|
@@ -1855,6 +1855,54 @@ var SessionManager = class {
|
|
|
1855
1855
|
return stats ? { ...session, stats } : session;
|
|
1856
1856
|
});
|
|
1857
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
|
+
}
|
|
1858
1906
|
/**
|
|
1859
1907
|
* 注册事件回调(事件会被转发到 WsBridge)
|
|
1860
1908
|
*
|
|
@@ -2540,6 +2588,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2540
2588
|
approvalRequestCallbacks = [];
|
|
2541
2589
|
/** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
|
|
2542
2590
|
approvalResolvedCallbacks = [];
|
|
2591
|
+
/** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
|
|
2592
|
+
notifyCallbacks = [];
|
|
2543
2593
|
/** YOLO 模式状态:sessionId -> enabled */
|
|
2544
2594
|
yoloSessions = /* @__PURE__ */ new Map();
|
|
2545
2595
|
/** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
|
|
@@ -2601,6 +2651,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2601
2651
|
}
|
|
2602
2652
|
}
|
|
2603
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
|
+
}
|
|
2604
2672
|
/** 设置状态信息提供者(用于 /health 端点) */
|
|
2605
2673
|
setStatusInfoProvider(provider) {
|
|
2606
2674
|
this.statusInfoProvider = provider;
|
|
@@ -2785,6 +2853,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2785
2853
|
const pathname = url.pathname;
|
|
2786
2854
|
if (req.method === "POST" && pathname === "/hook/approval") {
|
|
2787
2855
|
this.handleApprovalHook(req, res);
|
|
2856
|
+
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2857
|
+
this.handleHookNotify(req, res);
|
|
2788
2858
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2789
2859
|
this.handlePair(req, res);
|
|
2790
2860
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -2850,6 +2920,51 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2850
2920
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
2851
2921
|
}
|
|
2852
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
|
+
}
|
|
2853
2968
|
/** 健康检查端点 */
|
|
2854
2969
|
handleHealth(_req, res) {
|
|
2855
2970
|
const info = this.statusInfoProvider?.() ?? { connections: 0, activeSessions: 0 };
|
|
@@ -2882,9 +2997,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2882
2997
|
}
|
|
2883
2998
|
/** 返回连接 token(仅本机访问) */
|
|
2884
2999
|
handleToken(req, res) {
|
|
2885
|
-
|
|
2886
|
-
const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
|
|
2887
|
-
if (!isLocal) {
|
|
3000
|
+
if (!this.isLoopbackRequest(req)) {
|
|
2888
3001
|
this.sendJson(res, 403, { error: t("approval.forbidden") });
|
|
2889
3002
|
return;
|
|
2890
3003
|
}
|
|
@@ -3112,9 +3225,15 @@ var import_node_os6 = require("os");
|
|
|
3112
3225
|
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
3113
3226
|
var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
|
|
3114
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");
|
|
3115
3231
|
var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
|
|
3116
3232
|
var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
|
|
3117
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";
|
|
3118
3237
|
var LEGACY_HOOK_COMMANDS = [
|
|
3119
3238
|
"~/.sessix/hooks/approval-hook.sh",
|
|
3120
3239
|
"~/.sessix/hooks/permission-accept.sh"
|
|
@@ -3174,6 +3293,112 @@ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
|
|
|
3174
3293
|
process.stdout.write('{"decision":"allow"}\\n')
|
|
3175
3294
|
process.exit(0)
|
|
3176
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
|
+
`;
|
|
3177
3402
|
var HookInstaller = class {
|
|
3178
3403
|
/**
|
|
3179
3404
|
* 安装 hook
|
|
@@ -3187,8 +3412,14 @@ var HookInstaller = class {
|
|
|
3187
3412
|
await (0, import_promises2.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
|
|
3188
3413
|
await (0, import_promises2.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
|
|
3189
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");
|
|
3190
3418
|
await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
|
|
3191
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);
|
|
3192
3423
|
await this.addHookToSettings();
|
|
3193
3424
|
console.log("[HookInstaller] Hook installation complete");
|
|
3194
3425
|
}
|
|
@@ -3214,6 +3445,9 @@ var HookInstaller = class {
|
|
|
3214
3445
|
async isInstalled() {
|
|
3215
3446
|
let approvalScriptContent = "";
|
|
3216
3447
|
let permissionScriptExists = false;
|
|
3448
|
+
let compactScriptExists = false;
|
|
3449
|
+
let postCompactScriptExists = false;
|
|
3450
|
+
let permissionDeniedScriptExists = false;
|
|
3217
3451
|
try {
|
|
3218
3452
|
approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3219
3453
|
} catch {
|
|
@@ -3223,10 +3457,25 @@ var HookInstaller = class {
|
|
|
3223
3457
|
permissionScriptExists = true;
|
|
3224
3458
|
} catch {
|
|
3225
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
|
+
}
|
|
3226
3475
|
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
3227
3476
|
const settings = await this.readClaudeSettings();
|
|
3228
3477
|
const configExists = this.hasHookConfig(settings);
|
|
3229
|
-
return isLatestVersion && permissionScriptExists && configExists;
|
|
3478
|
+
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
|
|
3230
3479
|
}
|
|
3231
3480
|
// ============================================
|
|
3232
3481
|
// 内部方法
|
|
@@ -3264,6 +3513,36 @@ var HookInstaller = class {
|
|
|
3264
3513
|
});
|
|
3265
3514
|
changed = true;
|
|
3266
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
|
+
}
|
|
3267
3546
|
if (changed) {
|
|
3268
3547
|
await this.writeClaudeSettings(settings);
|
|
3269
3548
|
} else {
|
|
@@ -3278,6 +3557,9 @@ var HookInstaller = class {
|
|
|
3278
3557
|
if (!settings.hooks) return;
|
|
3279
3558
|
this.removeHookCommand(settings, "PreToolUse", HOOK_COMMAND);
|
|
3280
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);
|
|
3281
3563
|
if (Object.keys(settings.hooks).length === 0) {
|
|
3282
3564
|
delete settings.hooks;
|
|
3283
3565
|
}
|
|
@@ -3315,7 +3597,7 @@ var HookInstaller = class {
|
|
|
3315
3597
|
* 检查 settings 中是否已包含所有 Sessix hook 配置
|
|
3316
3598
|
*/
|
|
3317
3599
|
hasHookConfig(settings) {
|
|
3318
|
-
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
|
|
3600
|
+
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings) && this.hasPreCompactConfig(settings) && this.hasPostCompactConfig(settings) && this.hasPermissionDeniedConfig(settings);
|
|
3319
3601
|
}
|
|
3320
3602
|
/** 检查 PreToolUse 中是否有 approval-hook.js */
|
|
3321
3603
|
hasPreToolUseConfig(settings) {
|
|
@@ -3325,6 +3607,18 @@ var HookInstaller = class {
|
|
|
3325
3607
|
hasPermissionRequestConfig(settings) {
|
|
3326
3608
|
return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
|
|
3327
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
|
+
}
|
|
3328
3622
|
/** 检查 hook 数组中是否包含指定命令 */
|
|
3329
3623
|
hasHookEntry(hookArray, command) {
|
|
3330
3624
|
if (!Array.isArray(hookArray)) return false;
|
|
@@ -5054,6 +5348,7 @@ async function start(opts = {}) {
|
|
|
5054
5348
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
5055
5349
|
});
|
|
5056
5350
|
approvalProxy.setPairingManager(pairingManager);
|
|
5351
|
+
sessionManager.attachApprovalProxy(approvalProxy);
|
|
5057
5352
|
const authManager = new AuthManager();
|
|
5058
5353
|
authManager.on("login_url", (url) => {
|
|
5059
5354
|
wsBridge.broadcast({ type: "auth_login_url", url });
|