sessix-server 0.4.1 → 0.4.3
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 +1321 -37
- package/dist/server.js +1315 -31
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,10 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
|
-
var
|
|
27
|
+
var import_node_os10 = require("os");
|
|
28
28
|
var import_node_fs4 = require("fs");
|
|
29
|
-
var
|
|
30
|
-
var
|
|
29
|
+
var import_node_path10 = require("path");
|
|
30
|
+
var import_node_child_process12 = require("child_process");
|
|
31
31
|
|
|
32
32
|
// src/i18n/locales/zh.ts
|
|
33
33
|
var zh = {
|
|
@@ -302,12 +302,12 @@ function t(key, params) {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// src/server.ts
|
|
305
|
-
var
|
|
306
|
-
var
|
|
307
|
-
var
|
|
308
|
-
var
|
|
309
|
-
var
|
|
310
|
-
var
|
|
305
|
+
var import_uuid9 = require("uuid");
|
|
306
|
+
var import_promises7 = require("fs/promises");
|
|
307
|
+
var import_node_os9 = require("os");
|
|
308
|
+
var import_node_path9 = require("path");
|
|
309
|
+
var import_node_child_process11 = require("child_process");
|
|
310
|
+
var import_node_util3 = require("util");
|
|
311
311
|
|
|
312
312
|
// src/providers/ProcessProvider.ts
|
|
313
313
|
var import_child_process = require("child_process");
|
|
@@ -538,6 +538,77 @@ var ProcessProvider = class {
|
|
|
538
538
|
getActiveSessions() {
|
|
539
539
|
return Array.from(this.activeSessions.values()).map((entry) => entry.session);
|
|
540
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* 清理空闲进程
|
|
543
|
+
*
|
|
544
|
+
* 找出所有 status='idle' 且 lastActiveAt 距今超过 maxIdleMs 的活跃进程,
|
|
545
|
+
* kill 进程释放内存。entry 保留在 activeSessions 中,用户下次 sendMessage
|
|
546
|
+
* 走 slow path 自动 --resume 重启进程。
|
|
547
|
+
*
|
|
548
|
+
* @returns 被 sweep 的 sessionId 列表
|
|
549
|
+
*/
|
|
550
|
+
async sweepIdleProcesses(maxIdleMs) {
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
const swept = [];
|
|
553
|
+
for (const [sessionId, entry] of this.activeSessions) {
|
|
554
|
+
if (entry.process.exitCode !== null || entry.process.signalCode !== null) continue;
|
|
555
|
+
if (entry.session.status !== "idle") continue;
|
|
556
|
+
if (now - entry.session.lastActiveAt < maxIdleMs) continue;
|
|
557
|
+
const idleMin = Math.round((now - entry.session.lastActiveAt) / 6e4);
|
|
558
|
+
console.log(`[ProcessProvider] sweeping idle process: ${sessionId} (idle ${idleMin}m)`);
|
|
559
|
+
try {
|
|
560
|
+
entry.process.stdin?.end();
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
await killProcessCrossPlatform(entry.process);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
console.error(`[ProcessProvider] sweep kill failed for ${sessionId}:`, err);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
swept.push(sessionId);
|
|
570
|
+
}
|
|
571
|
+
return swept;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* LRU 上限清理
|
|
575
|
+
*
|
|
576
|
+
* 当活跃进程数超过 maxAlive 时,按 lastActiveAt 升序(最久未用优先)kill
|
|
577
|
+
* 状态为 idle 的进程,直到活跃数回到上限以内。
|
|
578
|
+
* running / waiting_question 状态的进程永远不会被 kill。
|
|
579
|
+
*
|
|
580
|
+
* @returns 被 sweep 的 sessionId 列表
|
|
581
|
+
*/
|
|
582
|
+
async sweepLruProcesses(maxAlive) {
|
|
583
|
+
const swept = [];
|
|
584
|
+
if (maxAlive <= 0) return swept;
|
|
585
|
+
const aliveEntries = Array.from(this.activeSessions.entries()).filter(
|
|
586
|
+
([, e]) => e.process.exitCode === null && e.process.signalCode === null
|
|
587
|
+
);
|
|
588
|
+
if (aliveEntries.length <= maxAlive) return swept;
|
|
589
|
+
const idleSorted = aliveEntries.filter(([, e]) => e.session.status === "idle").sort((a, b) => a[1].session.lastActiveAt - b[1].session.lastActiveAt);
|
|
590
|
+
let aliveCount = aliveEntries.length;
|
|
591
|
+
for (const [sessionId, entry] of idleSorted) {
|
|
592
|
+
if (aliveCount <= maxAlive) break;
|
|
593
|
+
const idleMin = Math.round((Date.now() - entry.session.lastActiveAt) / 6e4);
|
|
594
|
+
console.log(`[ProcessProvider] LRU sweep: ${sessionId} (idle ${idleMin}m, alive=${aliveCount}/${maxAlive})`);
|
|
595
|
+
try {
|
|
596
|
+
entry.process.stdin?.end();
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
await killProcessCrossPlatform(entry.process);
|
|
601
|
+
swept.push(sessionId);
|
|
602
|
+
aliveCount--;
|
|
603
|
+
} catch (err) {
|
|
604
|
+
console.error(`[ProcessProvider] LRU kill failed for ${sessionId}:`, err);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (aliveCount > maxAlive) {
|
|
608
|
+
console.warn(`[ProcessProvider] LRU sweep: ${aliveCount} alive after sweep > limit ${maxAlive}; remaining are running/waiting`);
|
|
609
|
+
}
|
|
610
|
+
return swept;
|
|
611
|
+
}
|
|
541
612
|
// ============================================
|
|
542
613
|
// 私有方法
|
|
543
614
|
// ============================================
|
|
@@ -1765,6 +1836,20 @@ var SessionManager = class {
|
|
|
1765
1836
|
isBufferTruncated(sessionId) {
|
|
1766
1837
|
return this.bufferTruncated.has(sessionId);
|
|
1767
1838
|
}
|
|
1839
|
+
/**
|
|
1840
|
+
* 缩减指定会话的事件缓冲区到最后 N 条,并标记 truncated
|
|
1841
|
+
*
|
|
1842
|
+
* 用于空闲进程被 sweep 后释放内存:缓冲区只为新订阅者重放服务,
|
|
1843
|
+
* 进程已死的会话可以通过 JSONL 文件补全完整历史,不需要保留全部内存事件。
|
|
1844
|
+
* 设置 bufferTruncated 后,客户端 subscribe 收到 session_history 时会从 JSONL 补齐。
|
|
1845
|
+
*/
|
|
1846
|
+
shrinkSessionBuffer(sessionId, keepLast = 100) {
|
|
1847
|
+
const buffer = this.sessionEventBuffers.get(sessionId);
|
|
1848
|
+
if (!buffer || buffer.length <= keepLast) return;
|
|
1849
|
+
buffer.splice(0, buffer.length - keepLast);
|
|
1850
|
+
this.bufferTruncated.add(sessionId);
|
|
1851
|
+
console.log(`[SessionManager] Session ${sessionId}: buffer shrunk to ${keepLast}, marked truncated`);
|
|
1852
|
+
}
|
|
1768
1853
|
/**
|
|
1769
1854
|
* 获取会话的项目路径(用于截断时从 JSONL 补全历史)
|
|
1770
1855
|
*/
|
|
@@ -1850,6 +1935,54 @@ var SessionManager = class {
|
|
|
1850
1935
|
return stats ? { ...session, stats } : session;
|
|
1851
1936
|
});
|
|
1852
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* 接入 ApprovalProxy 的非阻塞 hook 通知,将其映射为 ServerEvent 转发。
|
|
1940
|
+
*
|
|
1941
|
+
* 仅转发为 shared 类型定义中的字段,不把 hook 原始 payload 透传出去(隐私 + 体积)。
|
|
1942
|
+
* 当前覆盖:
|
|
1943
|
+
* - PreCompact (`type: 'compact'`) → `session_compact`
|
|
1944
|
+
* - PermissionDenied (`type: 'permission_denied'`) → `permission_denied`
|
|
1945
|
+
* - Subagent (`type: 'subagent'`) → `subagent_event`(骨架预留,本任务不发)
|
|
1946
|
+
*/
|
|
1947
|
+
attachApprovalProxy(approvalProxy) {
|
|
1948
|
+
approvalProxy.onNotify((notification) => {
|
|
1949
|
+
const serverEvent = this.mapHookNotificationToServerEvent(notification);
|
|
1950
|
+
if (serverEvent) this.emit(serverEvent);
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
/** 将 hook 通知映射为 ServerEvent;不识别的 type 返回 null */
|
|
1954
|
+
mapHookNotificationToServerEvent(n) {
|
|
1955
|
+
switch (n.type) {
|
|
1956
|
+
case "compact": {
|
|
1957
|
+
const subtype = n.subtype === "completed" || n.subtype === "blocked" ? n.subtype : "started";
|
|
1958
|
+
return { type: "session_compact", sessionId: n.sessionId, subtype };
|
|
1959
|
+
}
|
|
1960
|
+
case "permission_denied": {
|
|
1961
|
+
const source = n.source === "hook" || n.source === "rule" ? n.source : "classifier";
|
|
1962
|
+
return {
|
|
1963
|
+
type: "permission_denied",
|
|
1964
|
+
sessionId: n.sessionId,
|
|
1965
|
+
toolName: n.toolName ?? "unknown",
|
|
1966
|
+
reason: n.reason ?? "",
|
|
1967
|
+
source
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
case "subagent": {
|
|
1971
|
+
const phase = n.subtype === "completed" ? "completed" : "started";
|
|
1972
|
+
return {
|
|
1973
|
+
type: "subagent_event",
|
|
1974
|
+
sessionId: n.sessionId,
|
|
1975
|
+
parentToolUseId: typeof n.parentToolUseId === "string" ? n.parentToolUseId : "",
|
|
1976
|
+
subAgentId: typeof n.subAgentId === "string" ? n.subAgentId : "",
|
|
1977
|
+
phase,
|
|
1978
|
+
task: typeof n.task === "string" ? n.task : void 0
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
default:
|
|
1982
|
+
console.warn(`[SessionManager] Unknown hook notification type: ${n.type}`);
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1853
1986
|
/**
|
|
1854
1987
|
* 注册事件回调(事件会被转发到 WsBridge)
|
|
1855
1988
|
*
|
|
@@ -2535,6 +2668,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2535
2668
|
approvalRequestCallbacks = [];
|
|
2536
2669
|
/** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
|
|
2537
2670
|
approvalResolvedCallbacks = [];
|
|
2671
|
+
/** Hook 通知回调(PreCompact / PermissionDenied / 等非阻塞 hook) */
|
|
2672
|
+
notifyCallbacks = [];
|
|
2538
2673
|
/** YOLO 模式状态:sessionId -> enabled */
|
|
2539
2674
|
yoloSessions = /* @__PURE__ */ new Map();
|
|
2540
2675
|
/** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
|
|
@@ -2596,6 +2731,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2596
2731
|
}
|
|
2597
2732
|
}
|
|
2598
2733
|
}
|
|
2734
|
+
/**
|
|
2735
|
+
* 注册非阻塞 hook 通知回调(如 PreCompact、PermissionDenied)
|
|
2736
|
+
*
|
|
2737
|
+
* 这些 hook 不需要返回决策给 Claude Code,仅作为 ServerEvent 转发到手机端。
|
|
2738
|
+
*/
|
|
2739
|
+
onNotify(callback) {
|
|
2740
|
+
this.notifyCallbacks.push(callback);
|
|
2741
|
+
}
|
|
2742
|
+
/** 触发所有 notify 回调(内部调用) */
|
|
2743
|
+
fireNotify(notification) {
|
|
2744
|
+
for (const callback of this.notifyCallbacks) {
|
|
2745
|
+
try {
|
|
2746
|
+
callback(notification);
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
console.error("[ApprovalProxy] Notify callback error:", err);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2599
2752
|
/** 设置状态信息提供者(用于 /health 端点) */
|
|
2600
2753
|
setStatusInfoProvider(provider) {
|
|
2601
2754
|
this.statusInfoProvider = provider;
|
|
@@ -2780,6 +2933,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2780
2933
|
const pathname = url.pathname;
|
|
2781
2934
|
if (req.method === "POST" && pathname === "/hook/approval") {
|
|
2782
2935
|
this.handleApprovalHook(req, res);
|
|
2936
|
+
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2937
|
+
this.handleHookNotify(req, res);
|
|
2783
2938
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2784
2939
|
this.handlePair(req, res);
|
|
2785
2940
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -2845,6 +3000,51 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2845
3000
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
2846
3001
|
}
|
|
2847
3002
|
}
|
|
3003
|
+
/**
|
|
3004
|
+
* 非阻塞 hook 通知端点
|
|
3005
|
+
*
|
|
3006
|
+
* 用于 PreCompact、PostCompact、PermissionDenied 等 fire-and-forget 的 hook:
|
|
3007
|
+
* 立即返回 {"ok":true},再异步分发到 notifyCallbacks(最终广播为 ServerEvent)。
|
|
3008
|
+
*
|
|
3009
|
+
* 鉴权策略:仅允许 loopback(本机 hook 脚本)访问,避免远端注入伪造事件。
|
|
3010
|
+
*/
|
|
3011
|
+
async handleHookNotify(req, res) {
|
|
3012
|
+
if (!this.isLoopbackRequest(req)) {
|
|
3013
|
+
this.sendJson(res, 403, { ok: false, reason: "forbidden" });
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
try {
|
|
3017
|
+
const body = await this.parseJsonBody(req);
|
|
3018
|
+
const sessionId = String(body.sessionId ?? "").trim();
|
|
3019
|
+
const type = String(body.type ?? "").trim();
|
|
3020
|
+
if (!sessionId || !type) {
|
|
3021
|
+
this.sendJson(res, 400, { error: "sessionId and type are required" });
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
const notification = {
|
|
3025
|
+
sessionId,
|
|
3026
|
+
type
|
|
3027
|
+
};
|
|
3028
|
+
if (typeof body.subtype === "string") notification.subtype = body.subtype;
|
|
3029
|
+
if (typeof body.toolName === "string") notification.toolName = body.toolName;
|
|
3030
|
+
if (typeof body.reason === "string") notification.reason = body.reason;
|
|
3031
|
+
if (typeof body.source === "string") notification.source = body.source;
|
|
3032
|
+
if (typeof body.parentToolUseId === "string") notification.parentToolUseId = body.parentToolUseId;
|
|
3033
|
+
if (typeof body.subAgentId === "string") notification.subAgentId = body.subAgentId;
|
|
3034
|
+
if (body.phase === "started" || body.phase === "completed") notification.phase = body.phase;
|
|
3035
|
+
if (typeof body.task === "string") notification.task = body.task;
|
|
3036
|
+
this.sendJson(res, 200, { ok: true });
|
|
3037
|
+
setImmediate(() => this.fireNotify(notification));
|
|
3038
|
+
} catch (err) {
|
|
3039
|
+
console.error("[ApprovalProxy] Hook notify failed:", err);
|
|
3040
|
+
this.sendJson(res, 200, { ok: false });
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
/** 判断请求是否来自本机 loopback(127.0.0.1 / ::1 / IPv4-mapped IPv6) */
|
|
3044
|
+
isLoopbackRequest(req) {
|
|
3045
|
+
const remoteAddress = req.socket.remoteAddress;
|
|
3046
|
+
return remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
|
|
3047
|
+
}
|
|
2848
3048
|
/** 健康检查端点 */
|
|
2849
3049
|
handleHealth(_req, res) {
|
|
2850
3050
|
const info = this.statusInfoProvider?.() ?? { connections: 0, activeSessions: 0 };
|
|
@@ -2877,9 +3077,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2877
3077
|
}
|
|
2878
3078
|
/** 返回连接 token(仅本机访问) */
|
|
2879
3079
|
handleToken(req, res) {
|
|
2880
|
-
|
|
2881
|
-
const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
|
|
2882
|
-
if (!isLocal) {
|
|
3080
|
+
if (!this.isLoopbackRequest(req)) {
|
|
2883
3081
|
this.sendJson(res, 403, { error: t("approval.forbidden") });
|
|
2884
3082
|
return;
|
|
2885
3083
|
}
|
|
@@ -3107,9 +3305,15 @@ var import_node_os6 = require("os");
|
|
|
3107
3305
|
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
3108
3306
|
var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
|
|
3109
3307
|
var PERMISSION_ACCEPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
|
|
3308
|
+
var COMPACT_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "compact-hook.js");
|
|
3309
|
+
var POST_COMPACT_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "post-compact-hook.js");
|
|
3310
|
+
var PERMISSION_DENIED_HOOK_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-denied-hook.js");
|
|
3110
3311
|
var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
|
|
3111
3312
|
var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
|
|
3112
3313
|
var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
|
|
3314
|
+
var COMPACT_HOOK_COMMAND = "node ~/.sessix/hooks/compact-hook.js";
|
|
3315
|
+
var POST_COMPACT_HOOK_COMMAND = "node ~/.sessix/hooks/post-compact-hook.js";
|
|
3316
|
+
var PERMISSION_DENIED_HOOK_COMMAND = "node ~/.sessix/hooks/permission-denied-hook.js";
|
|
3113
3317
|
var LEGACY_HOOK_COMMANDS = [
|
|
3114
3318
|
"~/.sessix/hooks/approval-hook.sh",
|
|
3115
3319
|
"~/.sessix/hooks/permission-accept.sh"
|
|
@@ -3169,6 +3373,112 @@ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
|
|
|
3169
3373
|
process.stdout.write('{"decision":"allow"}\\n')
|
|
3170
3374
|
process.exit(0)
|
|
3171
3375
|
`;
|
|
3376
|
+
var COMPACT_HOOK_TEMPLATE = `#!/usr/bin/env node
|
|
3377
|
+
// Sessix PreCompact \u901A\u77E5 hook
|
|
3378
|
+
// \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
|
|
3379
|
+
|
|
3380
|
+
const sessionId = process.env.SESSIX_SESSION_ID
|
|
3381
|
+
if (!sessionId) {
|
|
3382
|
+
process.stdout.write('{}')
|
|
3383
|
+
process.exit(0)
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
let raw = ''
|
|
3387
|
+
process.stdin.on('data', (chunk) => { raw += chunk })
|
|
3388
|
+
process.stdin.on('end', () => {
|
|
3389
|
+
// \u8BFB\u53D6\u5E76\u4E22\u5F03 stdin payload\uFF08PreCompact \u6807\u51C6\u8F93\u5165\uFF09\uFF0Cserver \u7AEF\u4E0D\u9700\u8981\u5B83
|
|
3390
|
+
try { JSON.parse(raw) } catch {}
|
|
3391
|
+
// fire-and-forget\uFF1A\u4E0D\u7B49\u5F85\u54CD\u5E94\uFF0C\u907F\u514D\u963B\u585E compact
|
|
3392
|
+
try {
|
|
3393
|
+
fetch('http://localhost:3746/hook/notify', {
|
|
3394
|
+
method: 'POST',
|
|
3395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3396
|
+
body: JSON.stringify({
|
|
3397
|
+
sessionId,
|
|
3398
|
+
type: 'compact',
|
|
3399
|
+
subtype: 'started',
|
|
3400
|
+
}),
|
|
3401
|
+
signal: AbortSignal.timeout(2000),
|
|
3402
|
+
}).catch(() => {})
|
|
3403
|
+
} catch {}
|
|
3404
|
+
// \u7ACB\u5373\u8FD4\u56DE\u7A7A JSON\uFF0C\u4E0D\u963B\u585E compact
|
|
3405
|
+
process.stdout.write('{}')
|
|
3406
|
+
process.exit(0)
|
|
3407
|
+
})
|
|
3408
|
+
`;
|
|
3409
|
+
var POST_COMPACT_HOOK_TEMPLATE = `#!/usr/bin/env node
|
|
3410
|
+
// Sessix PostCompact \u901A\u77E5 hook
|
|
3411
|
+
// \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
|
|
3412
|
+
|
|
3413
|
+
const sessionId = process.env.SESSIX_SESSION_ID
|
|
3414
|
+
if (!sessionId) {
|
|
3415
|
+
process.stdout.write('{}')
|
|
3416
|
+
process.exit(0)
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
let raw = ''
|
|
3420
|
+
process.stdin.on('data', (chunk) => { raw += chunk })
|
|
3421
|
+
process.stdin.on('end', () => {
|
|
3422
|
+
// \u8BFB\u53D6\u5E76\u4E22\u5F03 stdin payload\uFF0Cserver \u7AEF\u4E0D\u9700\u8981\u5B83
|
|
3423
|
+
try { JSON.parse(raw) } catch {}
|
|
3424
|
+
// fire-and-forget\uFF1A\u4E0D\u7B49\u5F85\u54CD\u5E94
|
|
3425
|
+
try {
|
|
3426
|
+
fetch('http://localhost:3746/hook/notify', {
|
|
3427
|
+
method: 'POST',
|
|
3428
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3429
|
+
body: JSON.stringify({
|
|
3430
|
+
sessionId,
|
|
3431
|
+
type: 'compact',
|
|
3432
|
+
subtype: 'completed',
|
|
3433
|
+
}),
|
|
3434
|
+
signal: AbortSignal.timeout(2000),
|
|
3435
|
+
}).catch(() => {})
|
|
3436
|
+
} catch {}
|
|
3437
|
+
process.stdout.write('{}')
|
|
3438
|
+
process.exit(0)
|
|
3439
|
+
})
|
|
3440
|
+
`;
|
|
3441
|
+
var PERMISSION_DENIED_HOOK_TEMPLATE = `#!/usr/bin/env node
|
|
3442
|
+
// Sessix PermissionDenied \u901A\u77E5 hook
|
|
3443
|
+
// \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
|
|
3444
|
+
|
|
3445
|
+
const sessionId = process.env.SESSIX_SESSION_ID
|
|
3446
|
+
if (!sessionId) {
|
|
3447
|
+
process.stdout.write('{}')
|
|
3448
|
+
process.exit(0)
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
let raw = ''
|
|
3452
|
+
process.stdin.on('data', (chunk) => { raw += chunk })
|
|
3453
|
+
process.stdin.on('end', () => {
|
|
3454
|
+
let payload = {}
|
|
3455
|
+
try { payload = JSON.parse(raw) } catch {}
|
|
3456
|
+
const toolName = String(payload.tool_name || 'unknown')
|
|
3457
|
+
// Claude Code PermissionDenied \u7531 classifier \u89E6\u53D1\uFF0C\u6240\u4EE5 source \u9ED8\u8BA4 'classifier'
|
|
3458
|
+
const reason = String(
|
|
3459
|
+
payload.reason ||
|
|
3460
|
+
payload.permission_decision_reason ||
|
|
3461
|
+
payload.permissionDecisionReason ||
|
|
3462
|
+
''
|
|
3463
|
+
)
|
|
3464
|
+
try {
|
|
3465
|
+
fetch('http://localhost:3746/hook/notify', {
|
|
3466
|
+
method: 'POST',
|
|
3467
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3468
|
+
body: JSON.stringify({
|
|
3469
|
+
sessionId,
|
|
3470
|
+
type: 'permission_denied',
|
|
3471
|
+
toolName,
|
|
3472
|
+
reason,
|
|
3473
|
+
source: 'classifier',
|
|
3474
|
+
}),
|
|
3475
|
+
signal: AbortSignal.timeout(2000),
|
|
3476
|
+
}).catch(() => {})
|
|
3477
|
+
} catch {}
|
|
3478
|
+
process.stdout.write('{}')
|
|
3479
|
+
process.exit(0)
|
|
3480
|
+
})
|
|
3481
|
+
`;
|
|
3172
3482
|
var HookInstaller = class {
|
|
3173
3483
|
/**
|
|
3174
3484
|
* 安装 hook
|
|
@@ -3182,8 +3492,14 @@ var HookInstaller = class {
|
|
|
3182
3492
|
await (0, import_promises2.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
|
|
3183
3493
|
await (0, import_promises2.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
|
|
3184
3494
|
await (0, import_promises2.writeFile)(PERMISSION_ACCEPT_PATH, PERMISSION_ACCEPT_TEMPLATE, "utf-8");
|
|
3495
|
+
await (0, import_promises2.writeFile)(COMPACT_HOOK_PATH, COMPACT_HOOK_TEMPLATE, "utf-8");
|
|
3496
|
+
await (0, import_promises2.writeFile)(POST_COMPACT_HOOK_PATH, POST_COMPACT_HOOK_TEMPLATE, "utf-8");
|
|
3497
|
+
await (0, import_promises2.writeFile)(PERMISSION_DENIED_HOOK_PATH, PERMISSION_DENIED_HOOK_TEMPLATE, "utf-8");
|
|
3185
3498
|
await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
|
|
3186
3499
|
await (0, import_promises2.chmod)(PERMISSION_ACCEPT_PATH, 493);
|
|
3500
|
+
await (0, import_promises2.chmod)(COMPACT_HOOK_PATH, 493);
|
|
3501
|
+
await (0, import_promises2.chmod)(POST_COMPACT_HOOK_PATH, 493);
|
|
3502
|
+
await (0, import_promises2.chmod)(PERMISSION_DENIED_HOOK_PATH, 493);
|
|
3187
3503
|
await this.addHookToSettings();
|
|
3188
3504
|
console.log("[HookInstaller] Hook installation complete");
|
|
3189
3505
|
}
|
|
@@ -3209,6 +3525,9 @@ var HookInstaller = class {
|
|
|
3209
3525
|
async isInstalled() {
|
|
3210
3526
|
let approvalScriptContent = "";
|
|
3211
3527
|
let permissionScriptExists = false;
|
|
3528
|
+
let compactScriptExists = false;
|
|
3529
|
+
let postCompactScriptExists = false;
|
|
3530
|
+
let permissionDeniedScriptExists = false;
|
|
3212
3531
|
try {
|
|
3213
3532
|
approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3214
3533
|
} catch {
|
|
@@ -3218,10 +3537,25 @@ var HookInstaller = class {
|
|
|
3218
3537
|
permissionScriptExists = true;
|
|
3219
3538
|
} catch {
|
|
3220
3539
|
}
|
|
3540
|
+
try {
|
|
3541
|
+
await (0, import_promises2.access)(COMPACT_HOOK_PATH);
|
|
3542
|
+
compactScriptExists = true;
|
|
3543
|
+
} catch {
|
|
3544
|
+
}
|
|
3545
|
+
try {
|
|
3546
|
+
await (0, import_promises2.access)(POST_COMPACT_HOOK_PATH);
|
|
3547
|
+
postCompactScriptExists = true;
|
|
3548
|
+
} catch {
|
|
3549
|
+
}
|
|
3550
|
+
try {
|
|
3551
|
+
await (0, import_promises2.access)(PERMISSION_DENIED_HOOK_PATH);
|
|
3552
|
+
permissionDeniedScriptExists = true;
|
|
3553
|
+
} catch {
|
|
3554
|
+
}
|
|
3221
3555
|
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
3222
3556
|
const settings = await this.readClaudeSettings();
|
|
3223
3557
|
const configExists = this.hasHookConfig(settings);
|
|
3224
|
-
return isLatestVersion && permissionScriptExists && configExists;
|
|
3558
|
+
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
|
|
3225
3559
|
}
|
|
3226
3560
|
// ============================================
|
|
3227
3561
|
// 内部方法
|
|
@@ -3259,6 +3593,36 @@ var HookInstaller = class {
|
|
|
3259
3593
|
});
|
|
3260
3594
|
changed = true;
|
|
3261
3595
|
}
|
|
3596
|
+
if (!this.hasPreCompactConfig(settings)) {
|
|
3597
|
+
if (!settings.hooks.PreCompact) {
|
|
3598
|
+
settings.hooks.PreCompact = [];
|
|
3599
|
+
}
|
|
3600
|
+
settings.hooks.PreCompact.push({
|
|
3601
|
+
matcher: "",
|
|
3602
|
+
hooks: [{ type: "command", command: COMPACT_HOOK_COMMAND }]
|
|
3603
|
+
});
|
|
3604
|
+
changed = true;
|
|
3605
|
+
}
|
|
3606
|
+
if (!this.hasPostCompactConfig(settings)) {
|
|
3607
|
+
if (!settings.hooks.PostCompact) {
|
|
3608
|
+
settings.hooks.PostCompact = [];
|
|
3609
|
+
}
|
|
3610
|
+
settings.hooks.PostCompact.push({
|
|
3611
|
+
matcher: "",
|
|
3612
|
+
hooks: [{ type: "command", command: POST_COMPACT_HOOK_COMMAND }]
|
|
3613
|
+
});
|
|
3614
|
+
changed = true;
|
|
3615
|
+
}
|
|
3616
|
+
if (!this.hasPermissionDeniedConfig(settings)) {
|
|
3617
|
+
if (!settings.hooks.PermissionDenied) {
|
|
3618
|
+
settings.hooks.PermissionDenied = [];
|
|
3619
|
+
}
|
|
3620
|
+
settings.hooks.PermissionDenied.push({
|
|
3621
|
+
matcher: "",
|
|
3622
|
+
hooks: [{ type: "command", command: PERMISSION_DENIED_HOOK_COMMAND }]
|
|
3623
|
+
});
|
|
3624
|
+
changed = true;
|
|
3625
|
+
}
|
|
3262
3626
|
if (changed) {
|
|
3263
3627
|
await this.writeClaudeSettings(settings);
|
|
3264
3628
|
} else {
|
|
@@ -3273,6 +3637,9 @@ var HookInstaller = class {
|
|
|
3273
3637
|
if (!settings.hooks) return;
|
|
3274
3638
|
this.removeHookCommand(settings, "PreToolUse", HOOK_COMMAND);
|
|
3275
3639
|
this.removeHookCommand(settings, "PermissionRequest", PERMISSION_ACCEPT_COMMAND);
|
|
3640
|
+
this.removeHookCommand(settings, "PreCompact", COMPACT_HOOK_COMMAND);
|
|
3641
|
+
this.removeHookCommand(settings, "PostCompact", POST_COMPACT_HOOK_COMMAND);
|
|
3642
|
+
this.removeHookCommand(settings, "PermissionDenied", PERMISSION_DENIED_HOOK_COMMAND);
|
|
3276
3643
|
if (Object.keys(settings.hooks).length === 0) {
|
|
3277
3644
|
delete settings.hooks;
|
|
3278
3645
|
}
|
|
@@ -3310,7 +3677,7 @@ var HookInstaller = class {
|
|
|
3310
3677
|
* 检查 settings 中是否已包含所有 Sessix hook 配置
|
|
3311
3678
|
*/
|
|
3312
3679
|
hasHookConfig(settings) {
|
|
3313
|
-
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
|
|
3680
|
+
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings) && this.hasPreCompactConfig(settings) && this.hasPostCompactConfig(settings) && this.hasPermissionDeniedConfig(settings);
|
|
3314
3681
|
}
|
|
3315
3682
|
/** 检查 PreToolUse 中是否有 approval-hook.js */
|
|
3316
3683
|
hasPreToolUseConfig(settings) {
|
|
@@ -3320,6 +3687,18 @@ var HookInstaller = class {
|
|
|
3320
3687
|
hasPermissionRequestConfig(settings) {
|
|
3321
3688
|
return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
|
|
3322
3689
|
}
|
|
3690
|
+
/** 检查 PreCompact 中是否有 compact-hook.js */
|
|
3691
|
+
hasPreCompactConfig(settings) {
|
|
3692
|
+
return this.hasHookEntry(settings?.hooks?.PreCompact, COMPACT_HOOK_COMMAND);
|
|
3693
|
+
}
|
|
3694
|
+
/** 检查 PostCompact 中是否有 post-compact-hook.js */
|
|
3695
|
+
hasPostCompactConfig(settings) {
|
|
3696
|
+
return this.hasHookEntry(settings?.hooks?.PostCompact, POST_COMPACT_HOOK_COMMAND);
|
|
3697
|
+
}
|
|
3698
|
+
/** 检查 PermissionDenied 中是否有 permission-denied-hook.js */
|
|
3699
|
+
hasPermissionDeniedConfig(settings) {
|
|
3700
|
+
return this.hasHookEntry(settings?.hooks?.PermissionDenied, PERMISSION_DENIED_HOOK_COMMAND);
|
|
3701
|
+
}
|
|
3323
3702
|
/** 检查 hook 数组中是否包含指定命令 */
|
|
3324
3703
|
hasHookEntry(hookArray, command) {
|
|
3325
3704
|
if (!Array.isArray(hookArray)) return false;
|
|
@@ -4395,7 +4774,7 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
4395
4774
|
};
|
|
4396
4775
|
|
|
4397
4776
|
// src/server.ts
|
|
4398
|
-
var
|
|
4777
|
+
var import_promises8 = require("fs/promises");
|
|
4399
4778
|
|
|
4400
4779
|
// src/terminal/TerminalExecutor.ts
|
|
4401
4780
|
var import_node_child_process7 = require("child_process");
|
|
@@ -4898,8 +5277,762 @@ function kindOrder(k) {
|
|
|
4898
5277
|
return k === "device" ? 0 : k === "simulator" ? 1 : k === "mac" ? 2 : 3;
|
|
4899
5278
|
}
|
|
4900
5279
|
|
|
4901
|
-
// src/
|
|
5280
|
+
// src/commands/CommandDiscovery.ts
|
|
5281
|
+
var import_promises5 = require("fs/promises");
|
|
5282
|
+
var import_node_path7 = require("path");
|
|
5283
|
+
var import_node_crypto = require("crypto");
|
|
5284
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
5285
|
+
var MAX_README_BYTES = 256 * 1024;
|
|
5286
|
+
var SUBPACKAGE_DIRS = ["packages", "apps", "crates", "services"];
|
|
5287
|
+
var MAX_SCAN_PER_DIR = 30;
|
|
5288
|
+
var CommandDiscovery = class {
|
|
5289
|
+
cache = /* @__PURE__ */ new Map();
|
|
5290
|
+
async scan(projectPath, refresh = false) {
|
|
5291
|
+
if (!refresh) {
|
|
5292
|
+
const hit = this.cache.get(projectPath);
|
|
5293
|
+
if (hit && hit.expiresAt > Date.now()) return hit.commands;
|
|
5294
|
+
}
|
|
5295
|
+
const collector = [];
|
|
5296
|
+
await Promise.all([
|
|
5297
|
+
this.scanPackageJson(projectPath, "", collector),
|
|
5298
|
+
this.scanMakefile(projectPath, "", collector),
|
|
5299
|
+
this.scanJustfile(projectPath, "", collector),
|
|
5300
|
+
this.scanCargo(projectPath, "", collector),
|
|
5301
|
+
this.scanCompose(projectPath, "", collector),
|
|
5302
|
+
this.scanReadme(projectPath, "README.md", "readme", collector),
|
|
5303
|
+
this.scanReadme(projectPath, "CLAUDE.md", "claude.md", collector)
|
|
5304
|
+
]);
|
|
5305
|
+
for (const sub of SUBPACKAGE_DIRS) {
|
|
5306
|
+
const subRoot = (0, import_node_path7.join)(projectPath, sub);
|
|
5307
|
+
let entries;
|
|
5308
|
+
try {
|
|
5309
|
+
entries = await (0, import_promises5.readdir)(subRoot);
|
|
5310
|
+
} catch {
|
|
5311
|
+
continue;
|
|
5312
|
+
}
|
|
5313
|
+
let scanned = 0;
|
|
5314
|
+
for (const name of entries) {
|
|
5315
|
+
if (name.startsWith(".") || scanned >= MAX_SCAN_PER_DIR) continue;
|
|
5316
|
+
const childAbs = (0, import_node_path7.join)(subRoot, name);
|
|
5317
|
+
try {
|
|
5318
|
+
const s = await (0, import_promises5.stat)(childAbs);
|
|
5319
|
+
if (!s.isDirectory()) continue;
|
|
5320
|
+
} catch {
|
|
5321
|
+
continue;
|
|
5322
|
+
}
|
|
5323
|
+
scanned++;
|
|
5324
|
+
const rel = `${sub}/${name}`;
|
|
5325
|
+
await Promise.all([
|
|
5326
|
+
this.scanPackageJson(projectPath, rel, collector),
|
|
5327
|
+
this.scanCargo(projectPath, rel, collector)
|
|
5328
|
+
]);
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5332
|
+
const deduped = [];
|
|
5333
|
+
for (const c of collector) {
|
|
5334
|
+
if (seen.has(c.id)) continue;
|
|
5335
|
+
seen.add(c.id);
|
|
5336
|
+
deduped.push(c);
|
|
5337
|
+
}
|
|
5338
|
+
deduped.sort((a, b) => {
|
|
5339
|
+
const ca = categoryWeight(a.category) - categoryWeight(b.category);
|
|
5340
|
+
if (ca !== 0) return ca;
|
|
5341
|
+
const sa = sourceWeight(a.source) - sourceWeight(b.source);
|
|
5342
|
+
if (sa !== 0) return sa;
|
|
5343
|
+
return a.title.localeCompare(b.title);
|
|
5344
|
+
});
|
|
5345
|
+
this.cache.set(projectPath, { commands: deduped, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
5346
|
+
return deduped;
|
|
5347
|
+
}
|
|
5348
|
+
invalidate(projectPath) {
|
|
5349
|
+
if (projectPath) this.cache.delete(projectPath);
|
|
5350
|
+
else this.cache.clear();
|
|
5351
|
+
}
|
|
5352
|
+
// ============================================
|
|
5353
|
+
// 各来源扫描器
|
|
5354
|
+
// ============================================
|
|
5355
|
+
async scanPackageJson(rootPath, subDir, out) {
|
|
5356
|
+
const file = subDir ? `${subDir}/package.json` : "package.json";
|
|
5357
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5358
|
+
let raw;
|
|
5359
|
+
try {
|
|
5360
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5361
|
+
} catch {
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
let pkg;
|
|
5365
|
+
try {
|
|
5366
|
+
pkg = JSON.parse(raw);
|
|
5367
|
+
} catch {
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
if (!pkg.scripts) return;
|
|
5371
|
+
for (const [name, script] of Object.entries(pkg.scripts)) {
|
|
5372
|
+
if (typeof script !== "string") continue;
|
|
5373
|
+
const command = subDir ? `npm --workspace=${subDir} run ${name}` : `npm run ${name}`;
|
|
5374
|
+
const title = subDir ? `${pkg.name ?? subDir.split("/").pop()}: ${name}` : name;
|
|
5375
|
+
out.push(makeCommand({
|
|
5376
|
+
title,
|
|
5377
|
+
command,
|
|
5378
|
+
cwd: "",
|
|
5379
|
+
source: "package.json",
|
|
5380
|
+
sourceFile: file,
|
|
5381
|
+
description: script,
|
|
5382
|
+
category: classifyByName(name) ?? classifyByCommand(script)
|
|
5383
|
+
}));
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
async scanMakefile(rootPath, subDir, out) {
|
|
5387
|
+
const file = subDir ? `${subDir}/Makefile` : "Makefile";
|
|
5388
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5389
|
+
let raw;
|
|
5390
|
+
try {
|
|
5391
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5392
|
+
} catch {
|
|
5393
|
+
return;
|
|
5394
|
+
}
|
|
5395
|
+
const lines = raw.split("\n");
|
|
5396
|
+
let lastComment;
|
|
5397
|
+
const targetRegex = /^([a-zA-Z][a-zA-Z0-9_-]*)\s*:(?!=)/;
|
|
5398
|
+
for (const line of lines) {
|
|
5399
|
+
const trim = line.trim();
|
|
5400
|
+
if (trim.startsWith("#")) {
|
|
5401
|
+
lastComment = trim.replace(/^#+\s?/, "").trim() || void 0;
|
|
5402
|
+
continue;
|
|
5403
|
+
}
|
|
5404
|
+
if (trim === "") {
|
|
5405
|
+
lastComment = void 0;
|
|
5406
|
+
continue;
|
|
5407
|
+
}
|
|
5408
|
+
const match = targetRegex.exec(line);
|
|
5409
|
+
if (!match) {
|
|
5410
|
+
lastComment = void 0;
|
|
5411
|
+
continue;
|
|
5412
|
+
}
|
|
5413
|
+
const target = match[1];
|
|
5414
|
+
if (target === ".PHONY" || target === "default" && trim.startsWith("default:")) continue;
|
|
5415
|
+
out.push(makeCommand({
|
|
5416
|
+
title: target,
|
|
5417
|
+
command: `make ${target}`,
|
|
5418
|
+
cwd: subDir,
|
|
5419
|
+
source: "makefile",
|
|
5420
|
+
sourceFile: file,
|
|
5421
|
+
description: lastComment,
|
|
5422
|
+
category: classifyByName(target)
|
|
5423
|
+
}));
|
|
5424
|
+
lastComment = void 0;
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
async scanJustfile(rootPath, subDir, out) {
|
|
5428
|
+
const file = subDir ? `${subDir}/justfile` : "justfile";
|
|
5429
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5430
|
+
let raw;
|
|
5431
|
+
try {
|
|
5432
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5433
|
+
} catch {
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
const lines = raw.split("\n");
|
|
5437
|
+
let lastComment;
|
|
5438
|
+
const recipeRegex = /^([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:[a-zA-Z0-9_=" ]*)?\s*:/;
|
|
5439
|
+
for (const line of lines) {
|
|
5440
|
+
const trim = line.trim();
|
|
5441
|
+
if (trim.startsWith("#")) {
|
|
5442
|
+
lastComment = trim.replace(/^#+\s?/, "").trim() || void 0;
|
|
5443
|
+
continue;
|
|
5444
|
+
}
|
|
5445
|
+
if (trim === "") {
|
|
5446
|
+
lastComment = void 0;
|
|
5447
|
+
continue;
|
|
5448
|
+
}
|
|
5449
|
+
if (line.startsWith(" ") || line.startsWith(" ")) continue;
|
|
5450
|
+
const match = recipeRegex.exec(line);
|
|
5451
|
+
if (!match) {
|
|
5452
|
+
lastComment = void 0;
|
|
5453
|
+
continue;
|
|
5454
|
+
}
|
|
5455
|
+
const recipe = match[1];
|
|
5456
|
+
out.push(makeCommand({
|
|
5457
|
+
title: recipe,
|
|
5458
|
+
command: `just ${recipe}`,
|
|
5459
|
+
cwd: subDir,
|
|
5460
|
+
source: "justfile",
|
|
5461
|
+
sourceFile: file,
|
|
5462
|
+
description: lastComment,
|
|
5463
|
+
category: classifyByName(recipe)
|
|
5464
|
+
}));
|
|
5465
|
+
lastComment = void 0;
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
async scanCargo(rootPath, subDir, out) {
|
|
5469
|
+
const file = subDir ? `${subDir}/Cargo.toml` : "Cargo.toml";
|
|
5470
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5471
|
+
try {
|
|
5472
|
+
await (0, import_promises5.stat)(abs);
|
|
5473
|
+
} catch {
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
5476
|
+
const presets = [
|
|
5477
|
+
{ title: "cargo build", command: "cargo build", category: "build", description: "Compile in debug mode" },
|
|
5478
|
+
{ title: "cargo build --release", command: "cargo build --release", category: "build", description: "Compile in release mode" },
|
|
5479
|
+
{ title: "cargo run", command: "cargo run", category: "dev", description: "Build and run" },
|
|
5480
|
+
{ title: "cargo test", command: "cargo test", category: "test", description: "Run all tests" },
|
|
5481
|
+
{ title: "cargo check", command: "cargo check", category: "lint", description: "Type-check without producing binary" },
|
|
5482
|
+
{ title: "cargo clippy", command: "cargo clippy", category: "lint", description: "Lint with clippy" },
|
|
5483
|
+
{ title: "cargo fmt", command: "cargo fmt", category: "lint", description: "Format source" }
|
|
5484
|
+
];
|
|
5485
|
+
for (const p of presets) {
|
|
5486
|
+
out.push(makeCommand({
|
|
5487
|
+
title: subDir ? `${subDir.split("/").pop()}: ${p.title}` : p.title,
|
|
5488
|
+
command: p.command,
|
|
5489
|
+
cwd: subDir,
|
|
5490
|
+
source: "cargo",
|
|
5491
|
+
sourceFile: file,
|
|
5492
|
+
description: p.description,
|
|
5493
|
+
category: p.category
|
|
5494
|
+
}));
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
async scanCompose(rootPath, subDir, out) {
|
|
5498
|
+
for (const name of ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
|
|
5499
|
+
const file = subDir ? `${subDir}/${name}` : name;
|
|
5500
|
+
try {
|
|
5501
|
+
await (0, import_promises5.stat)((0, import_node_path7.join)(rootPath, file));
|
|
5502
|
+
} catch {
|
|
5503
|
+
continue;
|
|
5504
|
+
}
|
|
5505
|
+
const presets = [
|
|
5506
|
+
{ title: "docker compose up", cmd: "docker compose up", cat: "dev", desc: "Start services" },
|
|
5507
|
+
{ title: "docker compose up -d", cmd: "docker compose up -d", cat: "dev", desc: "Start services in background" },
|
|
5508
|
+
{ title: "docker compose down", cmd: "docker compose down", cat: "other", desc: "Stop and remove services" },
|
|
5509
|
+
{ title: "docker compose build", cmd: "docker compose build", cat: "build", desc: "Build service images" },
|
|
5510
|
+
{ title: "docker compose logs -f", cmd: "docker compose logs -f", cat: "other", desc: "Tail service logs" }
|
|
5511
|
+
];
|
|
5512
|
+
for (const p of presets) {
|
|
5513
|
+
out.push(makeCommand({
|
|
5514
|
+
title: p.title,
|
|
5515
|
+
command: p.cmd,
|
|
5516
|
+
cwd: subDir,
|
|
5517
|
+
source: "compose",
|
|
5518
|
+
sourceFile: file,
|
|
5519
|
+
description: p.desc,
|
|
5520
|
+
category: p.cat
|
|
5521
|
+
}));
|
|
5522
|
+
}
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
async scanReadme(rootPath, fileName, source, out) {
|
|
5527
|
+
const abs = (0, import_node_path7.join)(rootPath, fileName);
|
|
5528
|
+
let raw;
|
|
5529
|
+
try {
|
|
5530
|
+
const s = await (0, import_promises5.stat)(abs);
|
|
5531
|
+
if (s.size > MAX_README_BYTES) return;
|
|
5532
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5533
|
+
} catch {
|
|
5534
|
+
return;
|
|
5535
|
+
}
|
|
5536
|
+
const fenceRegex = /```(?:bash|sh|shell|zsh)\s*\n([\s\S]*?)```/gi;
|
|
5537
|
+
let match;
|
|
5538
|
+
while ((match = fenceRegex.exec(raw)) !== null) {
|
|
5539
|
+
const block = match[1];
|
|
5540
|
+
const blockLines = block.split("\n");
|
|
5541
|
+
let blockHeading;
|
|
5542
|
+
const beforeText = raw.slice(0, match.index).split("\n").reverse();
|
|
5543
|
+
for (const prev of beforeText) {
|
|
5544
|
+
const t2 = prev.trim();
|
|
5545
|
+
if (t2 === "") continue;
|
|
5546
|
+
const head = /^#{1,6}\s+(.+)$/.exec(t2);
|
|
5547
|
+
if (head) blockHeading = head[1].replace(/[#*`]/g, "").trim();
|
|
5548
|
+
break;
|
|
5549
|
+
}
|
|
5550
|
+
const merged = [];
|
|
5551
|
+
let pending = "";
|
|
5552
|
+
for (const rawLine of blockLines) {
|
|
5553
|
+
if (rawLine.trimEnd().endsWith("\\")) {
|
|
5554
|
+
pending += rawLine.trimEnd().slice(0, -1) + " ";
|
|
5555
|
+
continue;
|
|
5556
|
+
}
|
|
5557
|
+
merged.push(pending + rawLine);
|
|
5558
|
+
pending = "";
|
|
5559
|
+
}
|
|
5560
|
+
if (pending) merged.push(pending);
|
|
5561
|
+
for (const rawLine of merged) {
|
|
5562
|
+
const cmd = sanitizeBashLine(rawLine);
|
|
5563
|
+
if (!cmd) continue;
|
|
5564
|
+
const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
|
|
5565
|
+
const title = synthesizeTitle(cleanCmd);
|
|
5566
|
+
out.push(makeCommand({
|
|
5567
|
+
title,
|
|
5568
|
+
command: cleanCmd,
|
|
5569
|
+
cwd: "",
|
|
5570
|
+
source,
|
|
5571
|
+
sourceFile: fileName,
|
|
5572
|
+
description: inlineComment ?? blockHeading,
|
|
5573
|
+
category: classifyByCommand(cleanCmd)
|
|
5574
|
+
}));
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
};
|
|
5579
|
+
function makeCommand(input) {
|
|
5580
|
+
const id = (0, import_node_crypto.createHash)("sha1").update(`${input.source}|${input.sourceFile}|${input.command}|${input.cwd}`).digest("hex").slice(0, 12);
|
|
5581
|
+
return {
|
|
5582
|
+
id,
|
|
5583
|
+
title: input.title,
|
|
5584
|
+
command: input.command,
|
|
5585
|
+
cwd: input.cwd,
|
|
5586
|
+
source: input.source,
|
|
5587
|
+
sourceFile: input.sourceFile,
|
|
5588
|
+
description: input.description,
|
|
5589
|
+
category: input.category ?? classifyByCommand(input.command) ?? "other"
|
|
5590
|
+
};
|
|
5591
|
+
}
|
|
5592
|
+
function sanitizeBashLine(line) {
|
|
5593
|
+
let l = line.trim();
|
|
5594
|
+
if (!l) return null;
|
|
5595
|
+
if (l.startsWith("#")) return null;
|
|
5596
|
+
l = l.replace(/^[$>]\s*/, "");
|
|
5597
|
+
if (!l) return null;
|
|
5598
|
+
if (/^[A-Z_]+=/.test(l) && !/\s/.test(l)) return null;
|
|
5599
|
+
if (l === "EOF" || l === "EOT") return null;
|
|
5600
|
+
if (l.startsWith("//") || l.startsWith("//#")) return null;
|
|
5601
|
+
if (l.length > 400) return null;
|
|
5602
|
+
if (/<.+>/.test(l) && /your[-_]/.test(l.toLowerCase())) return null;
|
|
5603
|
+
return l;
|
|
5604
|
+
}
|
|
5605
|
+
function synthesizeTitle(cmd) {
|
|
5606
|
+
let work = cmd;
|
|
5607
|
+
while (/^[A-Z_][A-Z0-9_]*=/.test(work)) {
|
|
5608
|
+
const m = /^[A-Z_][A-Z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+/.exec(work);
|
|
5609
|
+
if (!m) break;
|
|
5610
|
+
work = work.slice(m[0].length);
|
|
5611
|
+
}
|
|
5612
|
+
const cdMatch = /^cd\s+\S+\s*&&\s*(.+)$/.exec(work);
|
|
5613
|
+
if (cdMatch) work = cdMatch[1];
|
|
5614
|
+
const tokens = work.split(/\s+/).filter(Boolean);
|
|
5615
|
+
const head = tokens.slice(0, 3).join(" ");
|
|
5616
|
+
return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
|
|
5617
|
+
}
|
|
5618
|
+
function splitInlineComment(line) {
|
|
5619
|
+
let inSingle = false;
|
|
5620
|
+
let inDouble = false;
|
|
5621
|
+
for (let i = 0; i < line.length; i++) {
|
|
5622
|
+
const ch = line[i];
|
|
5623
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
5624
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
5625
|
+
else if (ch === "#" && !inSingle && !inDouble && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
5626
|
+
const cmd = line.slice(0, i).trim();
|
|
5627
|
+
const comment = line.slice(i + 1).trim();
|
|
5628
|
+
return { command: cmd, inlineComment: comment.length > 0 ? comment : void 0 };
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
return { command: line };
|
|
5632
|
+
}
|
|
5633
|
+
function classifyByName(name) {
|
|
5634
|
+
const lower = name.toLowerCase();
|
|
5635
|
+
if (/(^|[:_-])(build|compile|bundle|prebuild)([:_-]|$)/.test(lower)) return "build";
|
|
5636
|
+
if (/(^|[:_-])(test|spec|jest|vitest|e2e)([:_-]|$)/.test(lower)) return "test";
|
|
5637
|
+
if (/(^|[:_-])(dev|start|serve|watch|run)([:_-]|$)/.test(lower)) return "dev";
|
|
5638
|
+
if (/(^|[:_-])(lint|format|fmt|check|typecheck)([:_-]|$)/.test(lower)) return "lint";
|
|
5639
|
+
if (/(^|[:_-])(install|setup|init|bootstrap)([:_-]|$)/.test(lower)) return "install";
|
|
5640
|
+
if (/(^|[:_-])(deploy|publish|release|ship)([:_-]|$)/.test(lower)) return "deploy";
|
|
5641
|
+
return void 0;
|
|
5642
|
+
}
|
|
5643
|
+
function classifyByCommand(cmd) {
|
|
5644
|
+
const lower = cmd.toLowerCase();
|
|
5645
|
+
if (/\b(build|compile|bundle|prebuild|tsup|webpack|esbuild|vite build|next build)\b/.test(lower)) return "build";
|
|
5646
|
+
if (/\b(test|jest|vitest|mocha|pytest|cargo test|go test)\b/.test(lower)) return "test";
|
|
5647
|
+
if (/\b(dev|start|serve|watch|nodemon|tsx watch|next dev|expo start)\b/.test(lower)) return "dev";
|
|
5648
|
+
if (/\b(lint|eslint|tsc|tslint|fmt|format|prettier|clippy)\b/.test(lower)) return "lint";
|
|
5649
|
+
if (/\b(install|setup|bootstrap)\b/.test(lower) && !/\binstall\s+/.test(lower)) return "install";
|
|
5650
|
+
if (/^npm install\b|^pnpm install\b|^yarn install\b|^yarn\s*$|^pnpm\s*$/.test(lower)) return "install";
|
|
5651
|
+
if (/\b(deploy|publish|release)\b/.test(lower)) return "deploy";
|
|
5652
|
+
return "other";
|
|
5653
|
+
}
|
|
5654
|
+
function categoryWeight(c) {
|
|
5655
|
+
return {
|
|
5656
|
+
dev: 0,
|
|
5657
|
+
build: 1,
|
|
5658
|
+
test: 2,
|
|
5659
|
+
lint: 3,
|
|
5660
|
+
install: 4,
|
|
5661
|
+
deploy: 5,
|
|
5662
|
+
other: 6
|
|
5663
|
+
}[c];
|
|
5664
|
+
}
|
|
5665
|
+
function sourceWeight(s) {
|
|
5666
|
+
return {
|
|
5667
|
+
"package.json": 0,
|
|
5668
|
+
makefile: 1,
|
|
5669
|
+
justfile: 2,
|
|
5670
|
+
taskfile: 3,
|
|
5671
|
+
cargo: 4,
|
|
5672
|
+
compose: 5,
|
|
5673
|
+
readme: 6,
|
|
5674
|
+
"claude.md": 7
|
|
5675
|
+
}[s];
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5678
|
+
// src/git/GitExecutor.ts
|
|
4902
5679
|
var import_node_child_process9 = require("child_process");
|
|
5680
|
+
var import_node_util2 = require("util");
|
|
5681
|
+
var import_uuid7 = require("uuid");
|
|
5682
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process9.exec);
|
|
5683
|
+
var STATUS_TIMEOUT_MS = 15e3;
|
|
5684
|
+
var COMMIT_TIMEOUT_MS = 6e4;
|
|
5685
|
+
var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5686
|
+
var GitExecutor = class {
|
|
5687
|
+
eventCallbacks = [];
|
|
5688
|
+
onEvent(callback) {
|
|
5689
|
+
this.eventCallbacks.push(callback);
|
|
5690
|
+
return () => {
|
|
5691
|
+
const idx = this.eventCallbacks.indexOf(callback);
|
|
5692
|
+
if (idx !== -1) this.eventCallbacks.splice(idx, 1);
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
emit(event) {
|
|
5696
|
+
for (const cb of this.eventCallbacks) {
|
|
5697
|
+
try {
|
|
5698
|
+
cb(event);
|
|
5699
|
+
} catch (err) {
|
|
5700
|
+
console.error("[GitExecutor] Event callback error:", err);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
async detectStatus(projectPath) {
|
|
5705
|
+
const opts = { cwd: projectPath, timeout: STATUS_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 };
|
|
5706
|
+
try {
|
|
5707
|
+
await execAsync2("git rev-parse --is-inside-work-tree", opts);
|
|
5708
|
+
} catch {
|
|
5709
|
+
return { isRepo: false, hasRemote: false, changes: [] };
|
|
5710
|
+
}
|
|
5711
|
+
let root;
|
|
5712
|
+
try {
|
|
5713
|
+
const { stdout } = await execAsync2("git rev-parse --show-toplevel", opts);
|
|
5714
|
+
root = stdout.trim();
|
|
5715
|
+
} catch {
|
|
5716
|
+
}
|
|
5717
|
+
let branch;
|
|
5718
|
+
try {
|
|
5719
|
+
const { stdout } = await execAsync2("git symbolic-ref --short HEAD", opts);
|
|
5720
|
+
branch = stdout.trim();
|
|
5721
|
+
} catch {
|
|
5722
|
+
branch = void 0;
|
|
5723
|
+
}
|
|
5724
|
+
let upstream;
|
|
5725
|
+
try {
|
|
5726
|
+
const { stdout } = await execAsync2("git rev-parse --abbrev-ref --symbolic-full-name @{u}", opts);
|
|
5727
|
+
upstream = stdout.trim();
|
|
5728
|
+
} catch {
|
|
5729
|
+
}
|
|
5730
|
+
let hasRemote = false;
|
|
5731
|
+
try {
|
|
5732
|
+
const { stdout } = await execAsync2("git remote", opts);
|
|
5733
|
+
hasRemote = stdout.trim().length > 0;
|
|
5734
|
+
} catch {
|
|
5735
|
+
}
|
|
5736
|
+
let ahead;
|
|
5737
|
+
let behind;
|
|
5738
|
+
if (upstream) {
|
|
5739
|
+
try {
|
|
5740
|
+
const { stdout } = await execAsync2("git rev-list --left-right --count HEAD...@{u}", opts);
|
|
5741
|
+
const [a, b] = stdout.trim().split(/\s+/);
|
|
5742
|
+
ahead = Number(a);
|
|
5743
|
+
behind = Number(b);
|
|
5744
|
+
} catch {
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
const changes = await this.parsePorcelain(projectPath);
|
|
5748
|
+
return {
|
|
5749
|
+
isRepo: true,
|
|
5750
|
+
root,
|
|
5751
|
+
branch,
|
|
5752
|
+
upstream,
|
|
5753
|
+
hasRemote,
|
|
5754
|
+
changes,
|
|
5755
|
+
ahead,
|
|
5756
|
+
behind
|
|
5757
|
+
};
|
|
5758
|
+
}
|
|
5759
|
+
async parsePorcelain(projectPath) {
|
|
5760
|
+
let stdout;
|
|
5761
|
+
try {
|
|
5762
|
+
const r = await execAsync2("git status --porcelain=v1 -z", {
|
|
5763
|
+
cwd: projectPath,
|
|
5764
|
+
timeout: STATUS_TIMEOUT_MS,
|
|
5765
|
+
maxBuffer: 8 * 1024 * 1024
|
|
5766
|
+
});
|
|
5767
|
+
stdout = r.stdout;
|
|
5768
|
+
} catch {
|
|
5769
|
+
return [];
|
|
5770
|
+
}
|
|
5771
|
+
const changes = [];
|
|
5772
|
+
const records = stdout.split("\0");
|
|
5773
|
+
for (let i = 0; i < records.length; i++) {
|
|
5774
|
+
const rec = records[i];
|
|
5775
|
+
if (!rec) continue;
|
|
5776
|
+
if (rec.length < 3) continue;
|
|
5777
|
+
const x = rec.charAt(0);
|
|
5778
|
+
const y = rec.charAt(1);
|
|
5779
|
+
const path2 = rec.slice(3);
|
|
5780
|
+
const isRename = x === "R" || x === "C";
|
|
5781
|
+
if (isRename) {
|
|
5782
|
+
i += 1;
|
|
5783
|
+
}
|
|
5784
|
+
const untracked = x === "?" && y === "?";
|
|
5785
|
+
const staged = !untracked && x !== " " && x !== "?";
|
|
5786
|
+
changes.push({
|
|
5787
|
+
path: path2,
|
|
5788
|
+
staged,
|
|
5789
|
+
untracked,
|
|
5790
|
+
code: `${x}${y}`
|
|
5791
|
+
});
|
|
5792
|
+
}
|
|
5793
|
+
return changes;
|
|
5794
|
+
}
|
|
5795
|
+
/**
|
|
5796
|
+
* 执行 commit(可选连带 push)。
|
|
5797
|
+
* - 若提供 files:先 git add 这些路径
|
|
5798
|
+
* - 若未提供 files:默认 git add -A(提交所有变更)
|
|
5799
|
+
*/
|
|
5800
|
+
async commit(sessionId, projectPath, message, files, alsoPush) {
|
|
5801
|
+
const opId = (0, import_uuid7.v4)();
|
|
5802
|
+
this.runSequence(sessionId, opId, "commit", projectPath, [
|
|
5803
|
+
files && files.length > 0 ? ["git", "add", "--", ...files] : ["git", "add", "-A"],
|
|
5804
|
+
["git", "commit", "-m", message]
|
|
5805
|
+
], COMMIT_TIMEOUT_MS).then(async (ok) => {
|
|
5806
|
+
if (ok && alsoPush) {
|
|
5807
|
+
await this.runSequence(sessionId, opId, "push", projectPath, [
|
|
5808
|
+
["git", "push"]
|
|
5809
|
+
], PUSH_TIMEOUT_MS);
|
|
5810
|
+
}
|
|
5811
|
+
}).catch((err) => {
|
|
5812
|
+
console.error("[GitExecutor] commit error:", err);
|
|
5813
|
+
});
|
|
5814
|
+
return opId;
|
|
5815
|
+
}
|
|
5816
|
+
async push(sessionId, projectPath) {
|
|
5817
|
+
const opId = (0, import_uuid7.v4)();
|
|
5818
|
+
this.runSequence(sessionId, opId, "push", projectPath, [
|
|
5819
|
+
["git", "push"]
|
|
5820
|
+
], PUSH_TIMEOUT_MS).catch((err) => {
|
|
5821
|
+
console.error("[GitExecutor] push error:", err);
|
|
5822
|
+
});
|
|
5823
|
+
return opId;
|
|
5824
|
+
}
|
|
5825
|
+
/**
|
|
5826
|
+
* 顺序执行一组命令,任一失败则停止。返回是否全部成功。
|
|
5827
|
+
* 每条命令的输出和最后一条命令的退出事件统一打到同一 phase。
|
|
5828
|
+
*/
|
|
5829
|
+
async runSequence(sessionId, opId, phase, projectPath, commands, timeoutMs) {
|
|
5830
|
+
let lastCode = 0;
|
|
5831
|
+
let lastSignal = null;
|
|
5832
|
+
for (const cmd of commands) {
|
|
5833
|
+
const { code, signal } = await this.runOne(sessionId, opId, phase, projectPath, cmd, timeoutMs);
|
|
5834
|
+
lastCode = code;
|
|
5835
|
+
lastSignal = signal;
|
|
5836
|
+
if (code !== 0) break;
|
|
5837
|
+
}
|
|
5838
|
+
this.emit({ type: "git_exit", sessionId, opId, phase, code: lastCode, signal: lastSignal });
|
|
5839
|
+
return lastCode === 0;
|
|
5840
|
+
}
|
|
5841
|
+
runOne(sessionId, opId, phase, projectPath, cmd, timeoutMs) {
|
|
5842
|
+
return new Promise((resolve) => {
|
|
5843
|
+
const display = cmd.map((p) => /\s/.test(p) ? `"${p}"` : p).join(" ");
|
|
5844
|
+
this.emit({
|
|
5845
|
+
type: "git_output",
|
|
5846
|
+
sessionId,
|
|
5847
|
+
opId,
|
|
5848
|
+
phase,
|
|
5849
|
+
stream: "stdout",
|
|
5850
|
+
data: `$ ${display}
|
|
5851
|
+
`
|
|
5852
|
+
});
|
|
5853
|
+
let proc;
|
|
5854
|
+
try {
|
|
5855
|
+
proc = (0, import_node_child_process9.spawn)(cmd[0], cmd.slice(1), {
|
|
5856
|
+
cwd: projectPath,
|
|
5857
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5858
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
5859
|
+
});
|
|
5860
|
+
} catch (err) {
|
|
5861
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5862
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stderr", data: `[spawn error] ${msg}
|
|
5863
|
+
` });
|
|
5864
|
+
resolve({ code: 1, signal: null });
|
|
5865
|
+
return;
|
|
5866
|
+
}
|
|
5867
|
+
proc.stdout?.on("data", (chunk) => {
|
|
5868
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stdout", data: chunk.toString() });
|
|
5869
|
+
});
|
|
5870
|
+
proc.stderr?.on("data", (chunk) => {
|
|
5871
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stderr", data: chunk.toString() });
|
|
5872
|
+
});
|
|
5873
|
+
proc.on("error", (err) => {
|
|
5874
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stderr", data: `[error] ${err.message}
|
|
5875
|
+
` });
|
|
5876
|
+
});
|
|
5877
|
+
const timer = setTimeout(() => {
|
|
5878
|
+
try {
|
|
5879
|
+
proc.kill("SIGTERM");
|
|
5880
|
+
} catch {
|
|
5881
|
+
}
|
|
5882
|
+
}, timeoutMs);
|
|
5883
|
+
proc.on("exit", (code, signal) => {
|
|
5884
|
+
clearTimeout(timer);
|
|
5885
|
+
resolve({ code, signal });
|
|
5886
|
+
});
|
|
5887
|
+
});
|
|
5888
|
+
}
|
|
5889
|
+
};
|
|
5890
|
+
|
|
5891
|
+
// src/scheduling/ScheduledSessionManager.ts
|
|
5892
|
+
var import_promises6 = require("fs/promises");
|
|
5893
|
+
var import_node_os8 = require("os");
|
|
5894
|
+
var import_node_path8 = require("path");
|
|
5895
|
+
var import_uuid8 = require("uuid");
|
|
5896
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
5897
|
+
var ScheduledSessionManager = class {
|
|
5898
|
+
tasks = /* @__PURE__ */ new Map();
|
|
5899
|
+
storeFile;
|
|
5900
|
+
onFire;
|
|
5901
|
+
onChange;
|
|
5902
|
+
onFired;
|
|
5903
|
+
persistTimer = null;
|
|
5904
|
+
constructor(opts) {
|
|
5905
|
+
this.storeFile = opts.storeFile ?? (0, import_node_path8.join)((0, import_node_os8.homedir)(), ".sessix", "scheduled-sessions.json");
|
|
5906
|
+
this.onFire = opts.onFire;
|
|
5907
|
+
this.onChange = opts.onChange;
|
|
5908
|
+
this.onFired = opts.onFired;
|
|
5909
|
+
}
|
|
5910
|
+
/** 启动时从磁盘恢复任务表,已过期的立刻触发 */
|
|
5911
|
+
async load() {
|
|
5912
|
+
let raw;
|
|
5913
|
+
try {
|
|
5914
|
+
raw = await (0, import_promises6.readFile)(this.storeFile, "utf8");
|
|
5915
|
+
} catch {
|
|
5916
|
+
return;
|
|
5917
|
+
}
|
|
5918
|
+
let parsed;
|
|
5919
|
+
try {
|
|
5920
|
+
parsed = JSON.parse(raw);
|
|
5921
|
+
} catch {
|
|
5922
|
+
return;
|
|
5923
|
+
}
|
|
5924
|
+
if (!Array.isArray(parsed)) return;
|
|
5925
|
+
for (const item of parsed) {
|
|
5926
|
+
if (!isValidTask(item)) continue;
|
|
5927
|
+
this.scheduleTimer(item);
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
/** 注册一个定时任务(payload 由调用方校验) */
|
|
5931
|
+
schedule(scheduledAt, payload) {
|
|
5932
|
+
const task = {
|
|
5933
|
+
id: (0, import_uuid8.v4)(),
|
|
5934
|
+
scheduledAt,
|
|
5935
|
+
createdAt: Date.now(),
|
|
5936
|
+
payload
|
|
5937
|
+
};
|
|
5938
|
+
this.scheduleTimer(task);
|
|
5939
|
+
this.persist();
|
|
5940
|
+
this.notifyChange();
|
|
5941
|
+
return task;
|
|
5942
|
+
}
|
|
5943
|
+
/** 取消任务,返回是否成功 */
|
|
5944
|
+
cancel(id) {
|
|
5945
|
+
const entry = this.tasks.get(id);
|
|
5946
|
+
if (!entry) return false;
|
|
5947
|
+
clearTimeout(entry.timer);
|
|
5948
|
+
this.tasks.delete(id);
|
|
5949
|
+
this.persist();
|
|
5950
|
+
this.notifyChange();
|
|
5951
|
+
return true;
|
|
5952
|
+
}
|
|
5953
|
+
/** 列出所有未触发的任务(按时间升序) */
|
|
5954
|
+
list() {
|
|
5955
|
+
return [...this.tasks.values()].map((e) => e.task).sort((a, b) => a.scheduledAt - b.scheduledAt);
|
|
5956
|
+
}
|
|
5957
|
+
/** 优雅关闭(清空定时器,不删除磁盘任务) */
|
|
5958
|
+
destroy() {
|
|
5959
|
+
for (const { timer } of this.tasks.values()) clearTimeout(timer);
|
|
5960
|
+
this.tasks.clear();
|
|
5961
|
+
if (this.persistTimer) {
|
|
5962
|
+
clearTimeout(this.persistTimer);
|
|
5963
|
+
this.persistTimer = null;
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
// ============================================
|
|
5967
|
+
// 内部
|
|
5968
|
+
// ============================================
|
|
5969
|
+
scheduleTimer(task) {
|
|
5970
|
+
const delay = Math.max(0, task.scheduledAt - Date.now());
|
|
5971
|
+
const armDelay = Math.min(delay, MAX_TIMEOUT_MS);
|
|
5972
|
+
const timer = setTimeout(() => {
|
|
5973
|
+
const remaining = task.scheduledAt - Date.now();
|
|
5974
|
+
if (remaining > 1e3) {
|
|
5975
|
+
this.scheduleTimer(task);
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
this.fire(task).catch((err) => {
|
|
5979
|
+
console.error("[ScheduledSessionManager] fire error:", err);
|
|
5980
|
+
});
|
|
5981
|
+
}, armDelay);
|
|
5982
|
+
this.tasks.set(task.id, { task, timer });
|
|
5983
|
+
}
|
|
5984
|
+
async fire(task) {
|
|
5985
|
+
const entry = this.tasks.get(task.id);
|
|
5986
|
+
if (!entry) return;
|
|
5987
|
+
clearTimeout(entry.timer);
|
|
5988
|
+
this.tasks.delete(task.id);
|
|
5989
|
+
this.persist();
|
|
5990
|
+
this.notifyChange();
|
|
5991
|
+
try {
|
|
5992
|
+
const result = await this.onFire(task);
|
|
5993
|
+
this.onFired?.({ id: task.id, sessionId: result.sessionId });
|
|
5994
|
+
} catch (err) {
|
|
5995
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5996
|
+
console.error(`[ScheduledSessionManager] fire failed for task ${task.id}: ${message}`);
|
|
5997
|
+
this.onFired?.({ id: task.id, error: message });
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
notifyChange() {
|
|
6001
|
+
if (!this.onChange) return;
|
|
6002
|
+
this.onChange(this.list());
|
|
6003
|
+
}
|
|
6004
|
+
/** 防抖持久化(500ms) */
|
|
6005
|
+
persist() {
|
|
6006
|
+
if (this.persistTimer) clearTimeout(this.persistTimer);
|
|
6007
|
+
this.persistTimer = setTimeout(() => {
|
|
6008
|
+
this.persistTimer = null;
|
|
6009
|
+
const tasks = [...this.tasks.values()].map((e) => e.task);
|
|
6010
|
+
(0, import_promises6.mkdir)((0, import_node_path8.join)(this.storeFile, ".."), { recursive: true }).then(() => (0, import_promises6.writeFile)(this.storeFile, JSON.stringify(tasks, null, 2), "utf8")).catch((err) => {
|
|
6011
|
+
console.error("[ScheduledSessionManager] persist error:", err);
|
|
6012
|
+
});
|
|
6013
|
+
}, 500);
|
|
6014
|
+
}
|
|
6015
|
+
};
|
|
6016
|
+
function isValidTask(value) {
|
|
6017
|
+
if (!value || typeof value !== "object") return false;
|
|
6018
|
+
const v = value;
|
|
6019
|
+
if (typeof v.id !== "string" || typeof v.scheduledAt !== "number" || typeof v.createdAt !== "number") {
|
|
6020
|
+
return false;
|
|
6021
|
+
}
|
|
6022
|
+
const payload = v.payload;
|
|
6023
|
+
if (!payload || typeof payload !== "object") return false;
|
|
6024
|
+
const p = payload;
|
|
6025
|
+
if (p.kind === "create") {
|
|
6026
|
+
return typeof p.projectPath === "string" && typeof p.message === "string";
|
|
6027
|
+
}
|
|
6028
|
+
if (p.kind === "send") {
|
|
6029
|
+
return typeof p.sessionId === "string" && typeof p.message === "string";
|
|
6030
|
+
}
|
|
6031
|
+
return false;
|
|
6032
|
+
}
|
|
6033
|
+
|
|
6034
|
+
// src/utils/cliCapabilities.ts
|
|
6035
|
+
var import_node_child_process10 = require("child_process");
|
|
4903
6036
|
var DEFAULT_CAPABILITIES = {
|
|
4904
6037
|
effortLevels: ["low", "medium", "high", "xhigh", "max"]
|
|
4905
6038
|
};
|
|
@@ -4927,7 +6060,7 @@ async function parseCliCapabilities() {
|
|
|
4927
6060
|
}
|
|
4928
6061
|
function runCli(path2, args) {
|
|
4929
6062
|
return new Promise((resolve) => {
|
|
4930
|
-
(0,
|
|
6063
|
+
(0, import_node_child_process10.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
|
|
4931
6064
|
if (err) {
|
|
4932
6065
|
console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
|
|
4933
6066
|
resolve(null);
|
|
@@ -4941,11 +6074,11 @@ function runCli(path2, args) {
|
|
|
4941
6074
|
// src/server.ts
|
|
4942
6075
|
var WS_PORT = 3745;
|
|
4943
6076
|
var HTTP_PORT = 3746;
|
|
4944
|
-
var
|
|
6077
|
+
var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process11.exec);
|
|
4945
6078
|
async function killPortProcess(port) {
|
|
4946
6079
|
try {
|
|
4947
6080
|
if (isWindows) {
|
|
4948
|
-
const { stdout } = await
|
|
6081
|
+
const { stdout } = await execAsync3(
|
|
4949
6082
|
`netstat -ano | findstr :${port} | findstr LISTENING`
|
|
4950
6083
|
);
|
|
4951
6084
|
const pids = /* @__PURE__ */ new Set();
|
|
@@ -4955,14 +6088,14 @@ async function killPortProcess(port) {
|
|
|
4955
6088
|
if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
|
|
4956
6089
|
}
|
|
4957
6090
|
for (const pid of pids) {
|
|
4958
|
-
await
|
|
6091
|
+
await execAsync3(`taskkill /PID ${pid} /F`).catch(() => {
|
|
4959
6092
|
});
|
|
4960
6093
|
}
|
|
4961
6094
|
} else {
|
|
4962
|
-
const { stdout } = await
|
|
6095
|
+
const { stdout } = await execAsync3(`lsof -ti :${port}`);
|
|
4963
6096
|
const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
|
|
4964
6097
|
if (pids.length > 0) {
|
|
4965
|
-
await
|
|
6098
|
+
await execAsync3(`kill -9 ${pids.join(" ")}`);
|
|
4966
6099
|
}
|
|
4967
6100
|
}
|
|
4968
6101
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
@@ -4983,8 +6116,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
4983
6116
|
}
|
|
4984
6117
|
}
|
|
4985
6118
|
async function start(opts = {}) {
|
|
4986
|
-
const configDir = (0,
|
|
4987
|
-
const tokenFile = (0,
|
|
6119
|
+
const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
|
|
6120
|
+
const tokenFile = (0, import_node_path9.join)(configDir, "token");
|
|
4988
6121
|
let token;
|
|
4989
6122
|
if (opts.token !== void 0) {
|
|
4990
6123
|
token = opts.token;
|
|
@@ -4994,11 +6127,11 @@ async function start(opts = {}) {
|
|
|
4994
6127
|
token = envToken;
|
|
4995
6128
|
} else {
|
|
4996
6129
|
try {
|
|
4997
|
-
token = (await (0,
|
|
6130
|
+
token = (await (0, import_promises7.readFile)(tokenFile, "utf8")).trim();
|
|
4998
6131
|
} catch {
|
|
4999
|
-
token = (0,
|
|
5000
|
-
await (0,
|
|
5001
|
-
await (0,
|
|
6132
|
+
token = (0, import_uuid9.v4)();
|
|
6133
|
+
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
6134
|
+
await (0, import_promises7.writeFile)(tokenFile, token, "utf8");
|
|
5002
6135
|
}
|
|
5003
6136
|
}
|
|
5004
6137
|
}
|
|
@@ -5006,6 +6139,9 @@ async function start(opts = {}) {
|
|
|
5006
6139
|
const sessionManager = new SessionManager(providerFactory);
|
|
5007
6140
|
const terminalExecutor = new TerminalExecutor();
|
|
5008
6141
|
const xcodeBuildExecutor = new XcodeBuildExecutor();
|
|
6142
|
+
const commandDiscovery = new CommandDiscovery();
|
|
6143
|
+
const gitExecutor = new GitExecutor();
|
|
6144
|
+
let scheduledManager = null;
|
|
5009
6145
|
const approvalProxy = await createWithRetry(
|
|
5010
6146
|
"ApprovalProxy",
|
|
5011
6147
|
HTTP_PORT,
|
|
@@ -5044,11 +6180,12 @@ async function start(opts = {}) {
|
|
|
5044
6180
|
let mdnsService = null;
|
|
5045
6181
|
const pairingManager = new PairingManager({
|
|
5046
6182
|
token,
|
|
5047
|
-
serverName: (0,
|
|
6183
|
+
serverName: (0, import_node_os9.hostname)(),
|
|
5048
6184
|
version: "0.2.0",
|
|
5049
6185
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
5050
6186
|
});
|
|
5051
6187
|
approvalProxy.setPairingManager(pairingManager);
|
|
6188
|
+
sessionManager.attachApprovalProxy(approvalProxy);
|
|
5052
6189
|
const authManager = new AuthManager();
|
|
5053
6190
|
authManager.on("login_url", (url) => {
|
|
5054
6191
|
wsBridge.broadcast({ type: "auth_login_url", url });
|
|
@@ -5072,6 +6209,49 @@ async function start(opts = {}) {
|
|
|
5072
6209
|
const broadcastUnreadSessions = () => {
|
|
5073
6210
|
wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
5074
6211
|
};
|
|
6212
|
+
scheduledManager = new ScheduledSessionManager({
|
|
6213
|
+
onFire: async (task) => {
|
|
6214
|
+
const p = task.payload;
|
|
6215
|
+
if (p.kind === "create") {
|
|
6216
|
+
await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
|
|
6217
|
+
const session = await sessionManager.createSession(
|
|
6218
|
+
p.projectPath,
|
|
6219
|
+
p.message,
|
|
6220
|
+
p.resumeSessionId,
|
|
6221
|
+
p.newSessionId,
|
|
6222
|
+
p.model,
|
|
6223
|
+
p.permissionMode,
|
|
6224
|
+
p.effort,
|
|
6225
|
+
void 0,
|
|
6226
|
+
p.agentType
|
|
6227
|
+
);
|
|
6228
|
+
wsBridge.broadcast({ type: "session_list", sessions: sessionManager.getActiveSessions() });
|
|
6229
|
+
return { sessionId: session.id };
|
|
6230
|
+
}
|
|
6231
|
+
const active = sessionManager.getActiveSessions().find((s) => s.id === p.sessionId);
|
|
6232
|
+
if (active) {
|
|
6233
|
+
await sessionManager.sendMessage(p.sessionId, p.message, p.permissionMode);
|
|
6234
|
+
} else {
|
|
6235
|
+
await sessionManager.createSession(
|
|
6236
|
+
p.projectPath,
|
|
6237
|
+
p.message,
|
|
6238
|
+
p.sessionId,
|
|
6239
|
+
void 0,
|
|
6240
|
+
void 0,
|
|
6241
|
+
p.permissionMode
|
|
6242
|
+
);
|
|
6243
|
+
}
|
|
6244
|
+
wsBridge.broadcast({ type: "session_list", sessions: sessionManager.getActiveSessions() });
|
|
6245
|
+
return { sessionId: p.sessionId };
|
|
6246
|
+
},
|
|
6247
|
+
onChange: (tasks) => {
|
|
6248
|
+
wsBridge.broadcast({ type: "scheduled_session_list", tasks });
|
|
6249
|
+
},
|
|
6250
|
+
onFired: (event) => {
|
|
6251
|
+
wsBridge.broadcast({ type: "scheduled_session_fired", ...event });
|
|
6252
|
+
}
|
|
6253
|
+
});
|
|
6254
|
+
await scheduledManager.load();
|
|
5075
6255
|
wsBridge.onConnection(async (ws) => {
|
|
5076
6256
|
const result = await getProjects();
|
|
5077
6257
|
if (result.ok) {
|
|
@@ -5093,12 +6273,15 @@ async function start(opts = {}) {
|
|
|
5093
6273
|
if (cliCapabilities) {
|
|
5094
6274
|
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
|
|
5095
6275
|
}
|
|
6276
|
+
if (scheduledManager) {
|
|
6277
|
+
wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
|
|
6278
|
+
}
|
|
5096
6279
|
});
|
|
5097
6280
|
wsBridge.onClientEvent(async (event, ws) => {
|
|
5098
6281
|
try {
|
|
5099
6282
|
switch (event.type) {
|
|
5100
6283
|
case "create_session": {
|
|
5101
|
-
await (0,
|
|
6284
|
+
await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
|
|
5102
6285
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
5103
6286
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
5104
6287
|
await sessionManager.createSession(
|
|
@@ -5286,7 +6469,7 @@ async function start(opts = {}) {
|
|
|
5286
6469
|
if (!isStreaming) {
|
|
5287
6470
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
5288
6471
|
try {
|
|
5289
|
-
const fileStat = await (0,
|
|
6472
|
+
const fileStat = await (0, import_promises8.stat)(filePath);
|
|
5290
6473
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
5291
6474
|
} catch {
|
|
5292
6475
|
}
|
|
@@ -5449,6 +6632,66 @@ async function start(opts = {}) {
|
|
|
5449
6632
|
xcodeBuildExecutor.killInstall(event.installId);
|
|
5450
6633
|
break;
|
|
5451
6634
|
}
|
|
6635
|
+
case "schedule_session": {
|
|
6636
|
+
if (!scheduledManager) break;
|
|
6637
|
+
const scheduledAt = Number(event.scheduledAt);
|
|
6638
|
+
if (!Number.isFinite(scheduledAt)) {
|
|
6639
|
+
wsBridge.send(ws, { type: "error", code: "INVALID_MESSAGE", message: "Invalid scheduledAt" });
|
|
6640
|
+
break;
|
|
6641
|
+
}
|
|
6642
|
+
scheduledManager.schedule(scheduledAt, event.payload);
|
|
6643
|
+
break;
|
|
6644
|
+
}
|
|
6645
|
+
case "cancel_scheduled_session": {
|
|
6646
|
+
scheduledManager?.cancel(event.id);
|
|
6647
|
+
break;
|
|
6648
|
+
}
|
|
6649
|
+
case "list_scheduled_sessions": {
|
|
6650
|
+
if (scheduledManager) {
|
|
6651
|
+
wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
|
|
6652
|
+
}
|
|
6653
|
+
break;
|
|
6654
|
+
}
|
|
6655
|
+
case "git_status": {
|
|
6656
|
+
const status = await gitExecutor.detectStatus(event.projectPath);
|
|
6657
|
+
wsBridge.send(ws, { type: "git_status_result", sessionId: event.sessionId, status });
|
|
6658
|
+
break;
|
|
6659
|
+
}
|
|
6660
|
+
case "git_commit": {
|
|
6661
|
+
await gitExecutor.commit(
|
|
6662
|
+
event.sessionId,
|
|
6663
|
+
event.projectPath,
|
|
6664
|
+
event.message,
|
|
6665
|
+
event.files,
|
|
6666
|
+
event.alsoPush
|
|
6667
|
+
);
|
|
6668
|
+
break;
|
|
6669
|
+
}
|
|
6670
|
+
case "git_push": {
|
|
6671
|
+
await gitExecutor.push(event.sessionId, event.projectPath);
|
|
6672
|
+
break;
|
|
6673
|
+
}
|
|
6674
|
+
case "list_project_commands": {
|
|
6675
|
+
try {
|
|
6676
|
+
const commands = await commandDiscovery.scan(event.projectPath, event.refresh ?? false);
|
|
6677
|
+
wsBridge.send(ws, {
|
|
6678
|
+
type: "commands_result",
|
|
6679
|
+
sessionId: event.sessionId,
|
|
6680
|
+
projectPath: event.projectPath,
|
|
6681
|
+
commands
|
|
6682
|
+
});
|
|
6683
|
+
} catch (err) {
|
|
6684
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6685
|
+
wsBridge.send(ws, {
|
|
6686
|
+
type: "commands_result",
|
|
6687
|
+
sessionId: event.sessionId,
|
|
6688
|
+
projectPath: event.projectPath,
|
|
6689
|
+
commands: [],
|
|
6690
|
+
error: message
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6693
|
+
break;
|
|
6694
|
+
}
|
|
5452
6695
|
default: {
|
|
5453
6696
|
wsBridge.send(ws, {
|
|
5454
6697
|
type: "error",
|
|
@@ -5487,6 +6730,9 @@ async function start(opts = {}) {
|
|
|
5487
6730
|
xcodeBuildExecutor.onEvent((event) => {
|
|
5488
6731
|
wsBridge.broadcast(event);
|
|
5489
6732
|
});
|
|
6733
|
+
gitExecutor.onEvent((event) => {
|
|
6734
|
+
wsBridge.broadcast(event);
|
|
6735
|
+
});
|
|
5490
6736
|
wsBridge.onDisconnect(() => {
|
|
5491
6737
|
if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
|
|
5492
6738
|
approvalProxy.approveAll(t("server.phoneDisconnected"));
|
|
@@ -5576,6 +6822,42 @@ async function start(opts = {}) {
|
|
|
5576
6822
|
console.error(`[Server] ${t("server.hookInstallFailed")}`, err);
|
|
5577
6823
|
console.log(`[Server] ${t("server.hookContinue")}`);
|
|
5578
6824
|
}
|
|
6825
|
+
const idleTimeoutMs = Number(process.env.SESSIX_IDLE_TIMEOUT_MS ?? 30 * 60 * 1e3);
|
|
6826
|
+
const idleSweepIntervalMs = Number(process.env.SESSIX_IDLE_SWEEP_INTERVAL_MS ?? 5 * 60 * 1e3);
|
|
6827
|
+
const maxActiveProcesses = Number(process.env.SESSIX_MAX_ACTIVE_PROCESSES ?? 15);
|
|
6828
|
+
let idleSweepTimer = null;
|
|
6829
|
+
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0)) {
|
|
6830
|
+
idleSweepTimer = setInterval(async () => {
|
|
6831
|
+
try {
|
|
6832
|
+
let totalSwept = 0;
|
|
6833
|
+
const broadcastShrink = (sessionId) => {
|
|
6834
|
+
sessionManager.shrinkSessionBuffer(sessionId, 100);
|
|
6835
|
+
};
|
|
6836
|
+
for (const agentType of ["claude-code", "codex"]) {
|
|
6837
|
+
const provider = providerFactory.getProvider(agentType);
|
|
6838
|
+
if (idleTimeoutMs > 0 && typeof provider.sweepIdleProcesses === "function") {
|
|
6839
|
+
const swept = await provider.sweepIdleProcesses(idleTimeoutMs);
|
|
6840
|
+
swept.forEach(broadcastShrink);
|
|
6841
|
+
totalSwept += swept.length;
|
|
6842
|
+
}
|
|
6843
|
+
if (maxActiveProcesses > 0 && typeof provider.sweepLruProcesses === "function") {
|
|
6844
|
+
const swept = await provider.sweepLruProcesses(maxActiveProcesses);
|
|
6845
|
+
swept.forEach(broadcastShrink);
|
|
6846
|
+
totalSwept += swept.length;
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
if (totalSwept > 0) {
|
|
6850
|
+
console.log(`[Server] Idle GC: swept ${totalSwept} idle session(s)`);
|
|
6851
|
+
wsBridge.broadcast({
|
|
6852
|
+
type: "session_list",
|
|
6853
|
+
sessions: sessionManager.getActiveSessions()
|
|
6854
|
+
});
|
|
6855
|
+
}
|
|
6856
|
+
} catch (err) {
|
|
6857
|
+
console.error("[Server] Idle GC failed:", err);
|
|
6858
|
+
}
|
|
6859
|
+
}, idleSweepIntervalMs);
|
|
6860
|
+
}
|
|
5579
6861
|
const stop = async () => {
|
|
5580
6862
|
console.log(`[Server] ${t("server.shuttingDown")}`);
|
|
5581
6863
|
const errors = [];
|
|
@@ -5587,6 +6869,7 @@ async function start(opts = {}) {
|
|
|
5587
6869
|
errors.push(err);
|
|
5588
6870
|
}
|
|
5589
6871
|
};
|
|
6872
|
+
if (idleSweepTimer) clearInterval(idleSweepTimer);
|
|
5590
6873
|
await attempt(() => authManager.destroy(), "AuthManager");
|
|
5591
6874
|
await attempt(() => stopMdns(), "mDNS");
|
|
5592
6875
|
await attempt(() => pairingManager.destroy(), "PairingManager");
|
|
@@ -5597,6 +6880,7 @@ async function start(opts = {}) {
|
|
|
5597
6880
|
await attempt(() => xcodeBuildExecutor.destroy(), "XcodeBuildExecutor");
|
|
5598
6881
|
await attempt(() => notificationService.destroy(), "NotificationService");
|
|
5599
6882
|
await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
|
|
6883
|
+
await attempt(() => scheduledManager?.destroy(), "ScheduledSessionManager");
|
|
5600
6884
|
if (errors.length > 0) {
|
|
5601
6885
|
console.error(`[Server] ${t("server.shutdownWithErrors", { count: errors.length })}`);
|
|
5602
6886
|
throw errors[0];
|
|
@@ -5623,9 +6907,9 @@ async function start(opts = {}) {
|
|
|
5623
6907
|
openPairing: (duration) => pairingManager.open(duration),
|
|
5624
6908
|
closePairing: () => pairingManager.close(),
|
|
5625
6909
|
regenerateToken: async () => {
|
|
5626
|
-
const newToken = (0,
|
|
5627
|
-
await (0,
|
|
5628
|
-
await (0,
|
|
6910
|
+
const newToken = (0, import_uuid9.v4)();
|
|
6911
|
+
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
6912
|
+
await (0, import_promises7.writeFile)(tokenFile, newToken, "utf8");
|
|
5629
6913
|
instance.token = newToken;
|
|
5630
6914
|
wsBridge.updateToken(newToken);
|
|
5631
6915
|
approvalProxy.updateToken(newToken);
|
|
@@ -5642,7 +6926,7 @@ async function start(opts = {}) {
|
|
|
5642
6926
|
var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
5643
6927
|
function getPackageVersion() {
|
|
5644
6928
|
try {
|
|
5645
|
-
const pkg = JSON.parse((0, import_node_fs4.readFileSync)((0,
|
|
6929
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path10.join)(__dirname, "..", "package.json"), "utf8"));
|
|
5646
6930
|
return pkg.version ?? "0.0.0";
|
|
5647
6931
|
} catch {
|
|
5648
6932
|
return "0.0.0";
|
|
@@ -5680,7 +6964,7 @@ async function autoUpdateIfNeeded() {
|
|
|
5680
6964
|
console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
|
|
5681
6965
|
console.log();
|
|
5682
6966
|
try {
|
|
5683
|
-
(0,
|
|
6967
|
+
(0, import_node_child_process12.execFileSync)("npx", [`sessix-server@${latest}`], {
|
|
5684
6968
|
stdio: "inherit",
|
|
5685
6969
|
env: { ...process.env, __SESSIX_UPDATED: "1" }
|
|
5686
6970
|
});
|
|
@@ -5786,7 +7070,7 @@ ${t("startup.pairingReopened")}`);
|
|
|
5786
7070
|
}
|
|
5787
7071
|
}
|
|
5788
7072
|
function getLocalIp() {
|
|
5789
|
-
const interfaces = (0,
|
|
7073
|
+
const interfaces = (0, import_node_os10.networkInterfaces)();
|
|
5790
7074
|
for (const iface of Object.values(interfaces)) {
|
|
5791
7075
|
for (const addr of iface ?? []) {
|
|
5792
7076
|
if (addr.family === "IPv4" && !addr.internal) {
|