sessix-server 0.2.5 → 0.2.7
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 +386 -158
- package/dist/server.d.ts +2 -0
- package/dist/server.js +361 -152
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
27
|
+
var import_node_os7 = require("os");
|
|
28
|
+
var import_node_fs3 = require("fs");
|
|
29
|
+
var import_node_path6 = require("path");
|
|
30
30
|
|
|
31
31
|
// src/i18n/locales/zh.ts
|
|
32
32
|
var zh = {
|
|
@@ -46,7 +46,10 @@ var zh = {
|
|
|
46
46
|
autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
47
47
|
autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
|
|
48
48
|
pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
|
|
49
|
+
pressT: " \u{1F511} \u6309 t \u91CD\u7F6E Token\uFF08\u6CC4\u9732\u540E\u5237\u65B0\uFF09",
|
|
49
50
|
pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
|
|
51
|
+
tokenRegenerated: "\u{1F511} Token \u5DF2\u91CD\u7F6E\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u914D\u5BF9",
|
|
52
|
+
tokenRegenerateFailed: "Token \u91CD\u7F6E\u5931\u8D25:",
|
|
50
53
|
updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
|
|
51
54
|
receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
52
55
|
goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
|
|
@@ -75,7 +78,8 @@ var zh = {
|
|
|
75
78
|
activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
|
|
76
79
|
activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
|
|
77
80
|
activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
|
|
78
|
-
noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
|
|
81
|
+
noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B",
|
|
82
|
+
tokenRegenerated: "Token \u5DF2\u91CD\u7F6E: {{token}}"
|
|
79
83
|
},
|
|
80
84
|
ws: {
|
|
81
85
|
started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
|
|
@@ -152,7 +156,10 @@ var en = {
|
|
|
152
156
|
autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
153
157
|
autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
|
|
154
158
|
pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
|
|
159
|
+
pressT: " \u{1F511} Press t to regenerate token (refresh after leak)",
|
|
155
160
|
pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
|
|
161
|
+
tokenRegenerated: "\u{1F511} Token regenerated, all clients disconnected. Scan QR to re-pair",
|
|
162
|
+
tokenRegenerateFailed: "Token regeneration failed:",
|
|
156
163
|
updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
|
|
157
164
|
receivedSignal: "Received {{signal}}, graceful shutdown...",
|
|
158
165
|
goodbye: "All services closed, goodbye!",
|
|
@@ -181,7 +188,8 @@ var en = {
|
|
|
181
188
|
activityPushEnabled: "ActivityKit Push enabled",
|
|
182
189
|
activityPushFailed: "ActivityKit Push init failed:",
|
|
183
190
|
activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
|
|
184
|
-
noActiveLoginProcess: "No active login process"
|
|
191
|
+
noActiveLoginProcess: "No active login process",
|
|
192
|
+
tokenRegenerated: "Token regenerated: {{token}}"
|
|
185
193
|
},
|
|
186
194
|
ws: {
|
|
187
195
|
started: "WebSocket server started on port {{port}}",
|
|
@@ -287,40 +295,89 @@ function t(key, params) {
|
|
|
287
295
|
}
|
|
288
296
|
|
|
289
297
|
// src/server.ts
|
|
290
|
-
var
|
|
298
|
+
var import_uuid5 = require("uuid");
|
|
291
299
|
var import_promises4 = require("fs/promises");
|
|
292
|
-
var
|
|
293
|
-
var
|
|
294
|
-
var
|
|
300
|
+
var import_node_os6 = require("os");
|
|
301
|
+
var import_node_path5 = require("path");
|
|
302
|
+
var import_node_child_process5 = require("child_process");
|
|
295
303
|
var import_node_util = require("util");
|
|
296
304
|
|
|
297
305
|
// src/providers/ProcessProvider.ts
|
|
298
|
-
var
|
|
306
|
+
var import_child_process = require("child_process");
|
|
299
307
|
var import_readline = require("readline");
|
|
300
308
|
var import_events = require("events");
|
|
301
|
-
var
|
|
309
|
+
var import_node_os2 = require("os");
|
|
302
310
|
var import_uuid = require("uuid");
|
|
303
311
|
|
|
304
312
|
// src/utils/claudePath.ts
|
|
305
|
-
var
|
|
313
|
+
var import_node_child_process2 = require("child_process");
|
|
314
|
+
var import_node_fs = require("fs");
|
|
315
|
+
var import_node_path = require("path");
|
|
316
|
+
var import_node_os = require("os");
|
|
317
|
+
|
|
318
|
+
// src/utils/platform.ts
|
|
319
|
+
var import_node_child_process = require("child_process");
|
|
320
|
+
var isWindows = process.platform === "win32";
|
|
321
|
+
function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
324
|
+
resolve();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const onExit = () => {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
resolve();
|
|
330
|
+
};
|
|
331
|
+
proc.once("exit", onExit);
|
|
332
|
+
if (isWindows) {
|
|
333
|
+
if (proc.pid) {
|
|
334
|
+
(0, import_node_child_process.spawn)("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
proc.kill("SIGTERM");
|
|
338
|
+
}
|
|
339
|
+
const timer = setTimeout(() => {
|
|
340
|
+
if (proc.exitCode === null && proc.signalCode === null) {
|
|
341
|
+
if (!isWindows) {
|
|
342
|
+
proc.kill("SIGKILL");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
resolve();
|
|
346
|
+
}, timeoutMs);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function isNormalExit(code, signal) {
|
|
350
|
+
if (code === 0) return true;
|
|
351
|
+
if (isWindows) {
|
|
352
|
+
return code === 1;
|
|
353
|
+
}
|
|
354
|
+
return code === 143 || signal === "SIGTERM";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/utils/claudePath.ts
|
|
306
358
|
function findClaudePath() {
|
|
307
359
|
try {
|
|
308
|
-
|
|
360
|
+
const cmd = isWindows ? "where claude" : "which claude";
|
|
361
|
+
return (0, import_node_child_process2.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
309
362
|
} catch {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
363
|
+
}
|
|
364
|
+
const candidates = isWindows ? [
|
|
365
|
+
(0, import_node_path.join)(process.env.LOCALAPPDATA ?? "", "Programs", "claude", "claude.exe"),
|
|
366
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local", "Programs", "claude", "claude.exe"),
|
|
367
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "local", "claude.exe")
|
|
368
|
+
] : [
|
|
369
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "bin", "claude"),
|
|
370
|
+
"/usr/local/bin/claude",
|
|
371
|
+
"/opt/homebrew/bin/claude"
|
|
372
|
+
];
|
|
373
|
+
for (const candidate of candidates) {
|
|
374
|
+
try {
|
|
375
|
+
(0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
|
|
376
|
+
return candidate;
|
|
377
|
+
} catch {
|
|
321
378
|
}
|
|
322
|
-
return "claude";
|
|
323
379
|
}
|
|
380
|
+
return "claude";
|
|
324
381
|
}
|
|
325
382
|
|
|
326
383
|
// src/providers/ProcessProvider.ts
|
|
@@ -393,19 +450,7 @@ var ProcessProvider = class {
|
|
|
393
450
|
entry.process.stdin?.end();
|
|
394
451
|
} catch {
|
|
395
452
|
}
|
|
396
|
-
entry.process
|
|
397
|
-
await new Promise((resolve) => {
|
|
398
|
-
const timeout = setTimeout(() => {
|
|
399
|
-
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
400
|
-
entry.process.kill("SIGKILL");
|
|
401
|
-
}
|
|
402
|
-
resolve();
|
|
403
|
-
}, 3e3);
|
|
404
|
-
entry.process.once("exit", () => {
|
|
405
|
-
clearTimeout(timeout);
|
|
406
|
-
resolve();
|
|
407
|
-
});
|
|
408
|
-
});
|
|
453
|
+
await killProcessCrossPlatform(entry.process);
|
|
409
454
|
}
|
|
410
455
|
this.emittedQuestionToolUseIds.delete(sessionId);
|
|
411
456
|
this.activeSessions.delete(sessionId);
|
|
@@ -435,7 +480,7 @@ var ProcessProvider = class {
|
|
|
435
480
|
entry.process.stdin?.end();
|
|
436
481
|
} catch {
|
|
437
482
|
}
|
|
438
|
-
entry.process
|
|
483
|
+
killProcessCrossPlatform(entry.process);
|
|
439
484
|
}
|
|
440
485
|
} else {
|
|
441
486
|
console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
|
|
@@ -517,7 +562,7 @@ var ProcessProvider = class {
|
|
|
517
562
|
}
|
|
518
563
|
const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
|
|
519
564
|
delete env.CLAUDECODE;
|
|
520
|
-
const proc = (0,
|
|
565
|
+
const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
|
|
521
566
|
cwd: projectPath,
|
|
522
567
|
env,
|
|
523
568
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -663,7 +708,7 @@ var ProcessProvider = class {
|
|
|
663
708
|
entry.session.lastActiveAt = Date.now();
|
|
664
709
|
const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
|
|
665
710
|
if (alreadyHasResult) return;
|
|
666
|
-
const isNormal = code
|
|
711
|
+
const isNormal = isNormalExit(code, signal);
|
|
667
712
|
entry.session.status = isNormal ? "idle" : "error";
|
|
668
713
|
if (!isNormal) {
|
|
669
714
|
console.error(
|
|
@@ -729,8 +774,8 @@ ${context}`;
|
|
|
729
774
|
return new Promise((resolve, reject) => {
|
|
730
775
|
const env = { ...process.env };
|
|
731
776
|
delete env.CLAUDECODE;
|
|
732
|
-
const proc = (0,
|
|
733
|
-
cwd: (0,
|
|
777
|
+
const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
|
|
778
|
+
cwd: (0, import_node_os2.homedir)(),
|
|
734
779
|
env,
|
|
735
780
|
stdio: ["pipe", "pipe", "pipe"]
|
|
736
781
|
});
|
|
@@ -1501,6 +1546,13 @@ var WsBridge = class _WsBridge {
|
|
|
1501
1546
|
getConnectionCount() {
|
|
1502
1547
|
return this.wss.clients.size;
|
|
1503
1548
|
}
|
|
1549
|
+
/** 更新 token 并断开所有现有连接(token 刷新后需重新配对) */
|
|
1550
|
+
updateToken(newToken) {
|
|
1551
|
+
this.token = newToken;
|
|
1552
|
+
for (const ws of this.wss.clients) {
|
|
1553
|
+
ws.close(4001, "Token regenerated");
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1504
1556
|
/** 优雅关闭 WebSocket 服务 */
|
|
1505
1557
|
close() {
|
|
1506
1558
|
return new Promise((resolve, reject) => {
|
|
@@ -1622,15 +1674,15 @@ var WsBridge = class _WsBridge {
|
|
|
1622
1674
|
|
|
1623
1675
|
// src/approval/ApprovalProxy.ts
|
|
1624
1676
|
var import_node_http = __toESM(require("http"));
|
|
1625
|
-
var
|
|
1626
|
-
var
|
|
1627
|
-
var
|
|
1677
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
1678
|
+
var import_node_path2 = __toESM(require("path"));
|
|
1679
|
+
var import_node_os3 = __toESM(require("os"));
|
|
1628
1680
|
var import_uuid3 = require("uuid");
|
|
1629
1681
|
var ApprovalProxy = class _ApprovalProxy {
|
|
1630
1682
|
server;
|
|
1631
1683
|
token;
|
|
1632
1684
|
port;
|
|
1633
|
-
settingsPath =
|
|
1685
|
+
settingsPath = import_node_path2.default.join(import_node_os3.default.homedir(), ".claude", "settings.json");
|
|
1634
1686
|
/** 待处理的审批请求:requestId -> { resolve, timer, request } */
|
|
1635
1687
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1636
1688
|
/** 审批请求回调(通知外部推送到手机) */
|
|
@@ -1735,7 +1787,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1735
1787
|
isToolInClaudeSettings(toolName, projectPath) {
|
|
1736
1788
|
const checkPath = (filepath) => {
|
|
1737
1789
|
try {
|
|
1738
|
-
const raw =
|
|
1790
|
+
const raw = import_node_fs2.default.readFileSync(filepath, "utf-8");
|
|
1739
1791
|
const settings = JSON.parse(raw);
|
|
1740
1792
|
const allow = settings?.permissions?.allow ?? [];
|
|
1741
1793
|
return allow.some((entry) => {
|
|
@@ -1749,24 +1801,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1749
1801
|
}
|
|
1750
1802
|
};
|
|
1751
1803
|
if (projectPath) {
|
|
1752
|
-
const projectSettingsPath =
|
|
1804
|
+
const projectSettingsPath = import_node_path2.default.join(projectPath, ".claude", "settings.json");
|
|
1753
1805
|
if (checkPath(projectSettingsPath)) return true;
|
|
1754
1806
|
}
|
|
1755
1807
|
return checkPath(this.settingsPath);
|
|
1756
1808
|
}
|
|
1757
1809
|
/** 将工具写入 settings.json permissions.allow(项目级或全局) */
|
|
1758
1810
|
addToClaudeSettings(projectPath, toolName) {
|
|
1759
|
-
const targetPath = projectPath ?
|
|
1811
|
+
const targetPath = projectPath ? import_node_path2.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
|
|
1760
1812
|
try {
|
|
1761
1813
|
if (projectPath) {
|
|
1762
|
-
const dir =
|
|
1763
|
-
if (!
|
|
1764
|
-
|
|
1814
|
+
const dir = import_node_path2.default.dirname(targetPath);
|
|
1815
|
+
if (!import_node_fs2.default.existsSync(dir)) {
|
|
1816
|
+
import_node_fs2.default.mkdirSync(dir, { recursive: true });
|
|
1765
1817
|
}
|
|
1766
1818
|
}
|
|
1767
1819
|
let settings = {};
|
|
1768
1820
|
try {
|
|
1769
|
-
settings = JSON.parse(
|
|
1821
|
+
settings = JSON.parse(import_node_fs2.default.readFileSync(targetPath, "utf-8"));
|
|
1770
1822
|
} catch {
|
|
1771
1823
|
}
|
|
1772
1824
|
if (!settings.permissions) {
|
|
@@ -1780,7 +1832,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1780
1832
|
const entry = `${toolName}(*)`;
|
|
1781
1833
|
if (!allow.includes(entry)) {
|
|
1782
1834
|
allow.push(entry);
|
|
1783
|
-
|
|
1835
|
+
import_node_fs2.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1784
1836
|
const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
|
|
1785
1837
|
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
|
|
1786
1838
|
}
|
|
@@ -1940,6 +1992,10 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1940
1992
|
});
|
|
1941
1993
|
}
|
|
1942
1994
|
}
|
|
1995
|
+
/** 更新 token(token 刷新时调用) */
|
|
1996
|
+
updateToken(newToken) {
|
|
1997
|
+
this.token = newToken;
|
|
1998
|
+
}
|
|
1943
1999
|
/** 返回连接 token(仅本机访问) */
|
|
1944
2000
|
handleToken(req, res) {
|
|
1945
2001
|
const remoteAddress = req.socket.remoteAddress;
|
|
@@ -2004,11 +2060,12 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2004
2060
|
|
|
2005
2061
|
// src/mdns/MdnsService.ts
|
|
2006
2062
|
var import_bonjour_service = __toESM(require("bonjour-service"));
|
|
2007
|
-
var
|
|
2063
|
+
var import_node_os4 = require("os");
|
|
2008
2064
|
function getLanAddresses() {
|
|
2009
2065
|
const results = [];
|
|
2010
|
-
for (const [name, addrs] of Object.entries((0,
|
|
2011
|
-
if (name.startsWith("utun") || name
|
|
2066
|
+
for (const [name, addrs] of Object.entries((0, import_node_os4.networkInterfaces)())) {
|
|
2067
|
+
if (name.startsWith("utun") || name === "lo") continue;
|
|
2068
|
+
if (isWindows && (name.startsWith("vEthernet") || name.includes("Loopback"))) continue;
|
|
2012
2069
|
for (const addr of addrs ?? []) {
|
|
2013
2070
|
if (addr.family === "IPv4" && !addr.internal) {
|
|
2014
2071
|
results.push(addr.address);
|
|
@@ -2110,72 +2167,62 @@ var MdnsService = class {
|
|
|
2110
2167
|
|
|
2111
2168
|
// src/hooks/HookInstaller.ts
|
|
2112
2169
|
var import_promises2 = require("fs/promises");
|
|
2113
|
-
var
|
|
2114
|
-
var
|
|
2115
|
-
var SESSIX_HOOKS_DIR = (0,
|
|
2116
|
-
var HOOK_SCRIPT_PATH = (0,
|
|
2117
|
-
var PERMISSION_ACCEPT_PATH = (0,
|
|
2118
|
-
var CLAUDE_SETTINGS_PATH = (0,
|
|
2119
|
-
var HOOK_COMMAND = "~/.sessix/hooks/approval-hook.
|
|
2120
|
-
var PERMISSION_ACCEPT_COMMAND = "~/.sessix/hooks/permission-accept.
|
|
2121
|
-
var
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
# \u4ECE stdin \u8BFB\u53D6 hook payload
|
|
2130
|
-
PAYLOAD=$(cat)
|
|
2131
|
-
|
|
2132
|
-
# \u83B7\u53D6\u9879\u76EE\u8DEF\u5F84\uFF08\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09
|
|
2133
|
-
PROJECT_PATH="$PWD"
|
|
2134
|
-
|
|
2135
|
-
# \u53D1\u9001\u5BA1\u6279\u8BF7\u6C42\u5230 Sessix \u670D\u52A1\u5668\uFF08\u957F\u8F6E\u8BE2\uFF0C\u8D85\u65F6\u65F6\u95F4 > 300s\uFF09
|
|
2136
|
-
RESPONSE=$(curl -s -X POST "http://localhost:3746/hook/approval" \\
|
|
2137
|
-
-H "Content-Type: application/json" \\
|
|
2138
|
-
-d "{\\"sessionId\\": \\"$SESSIX_SESSION_ID\\", \\"projectPath\\": \\"$PROJECT_PATH\\", \\"payload\\": $PAYLOAD}" \\
|
|
2139
|
-
--max-time 320 \\
|
|
2140
|
-
2>/dev/null)
|
|
2141
|
-
|
|
2142
|
-
if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
|
|
2143
|
-
# \u5982\u679C Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C\uFF08exit 0 = \u6279\u51C6\uFF09
|
|
2144
|
-
exit 0
|
|
2145
|
-
fi
|
|
2170
|
+
var import_node_path3 = require("path");
|
|
2171
|
+
var import_node_os5 = require("os");
|
|
2172
|
+
var SESSIX_HOOKS_DIR = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".sessix", "hooks");
|
|
2173
|
+
var HOOK_SCRIPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
|
|
2174
|
+
var PERMISSION_ACCEPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
|
|
2175
|
+
var CLAUDE_SETTINGS_PATH = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude", "settings.json");
|
|
2176
|
+
var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
|
|
2177
|
+
var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
|
|
2178
|
+
var LEGACY_HOOK_COMMANDS = [
|
|
2179
|
+
"~/.sessix/hooks/approval-hook.sh",
|
|
2180
|
+
"~/.sessix/hooks/permission-accept.sh"
|
|
2181
|
+
];
|
|
2182
|
+
var HOOK_SCRIPT_TEMPLATE = `#!/usr/bin/env node
|
|
2183
|
+
// Sessix Approval Hook
|
|
2184
|
+
// \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
|
|
2146
2185
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2186
|
+
const sessionId = process.env.SESSIX_SESSION_ID
|
|
2187
|
+
if (!sessionId) process.exit(0)
|
|
2149
2188
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2189
|
+
let payload = ''
|
|
2190
|
+
process.stdin.on('data', (chunk) => { payload += chunk })
|
|
2191
|
+
process.stdin.on('end', async () => {
|
|
2192
|
+
try {
|
|
2193
|
+
const res = await fetch('http://localhost:3746/hook/approval', {
|
|
2194
|
+
method: 'POST',
|
|
2195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2196
|
+
body: JSON.stringify({
|
|
2197
|
+
sessionId,
|
|
2198
|
+
projectPath: process.cwd(),
|
|
2199
|
+
payload: JSON.parse(payload),
|
|
2200
|
+
}),
|
|
2201
|
+
signal: AbortSignal.timeout(320000),
|
|
2202
|
+
})
|
|
2203
|
+
const data = await res.json()
|
|
2204
|
+
process.exit(data.decision === 'deny' ? 1 : 0)
|
|
2205
|
+
} catch {
|
|
2206
|
+
// Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C
|
|
2207
|
+
process.exit(0)
|
|
2208
|
+
}
|
|
2209
|
+
})
|
|
2160
2210
|
`;
|
|
2161
|
-
var PERMISSION_ACCEPT_TEMPLATE = `#!/bin/
|
|
2162
|
-
|
|
2163
|
-
|
|
2211
|
+
var PERMISSION_ACCEPT_TEMPLATE = `#!/usr/bin/env node
|
|
2212
|
+
// Sessix PermissionRequest \u515C\u5E95
|
|
2213
|
+
// \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
|
|
2164
2214
|
|
|
2165
|
-
if
|
|
2166
|
-
exit 0
|
|
2167
|
-
fi
|
|
2215
|
+
if (!process.env.SESSIX_SESSION_ID) process.exit(0)
|
|
2168
2216
|
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
exit 0
|
|
2217
|
+
process.stdout.write('{"decision":"allow"}\\n')
|
|
2218
|
+
process.exit(0)
|
|
2172
2219
|
`;
|
|
2173
2220
|
var HookInstaller = class {
|
|
2174
2221
|
/**
|
|
2175
2222
|
* 安装 hook
|
|
2176
2223
|
*
|
|
2177
2224
|
* 1. 创建 ~/.sessix/hooks/ 目录
|
|
2178
|
-
* 2. 写入 approval-hook.
|
|
2225
|
+
* 2. 写入 approval-hook.js 脚本
|
|
2179
2226
|
* 3. 赋予执行权限
|
|
2180
2227
|
* 4. 更新 Claude Code settings.json 添加 hook 配置
|
|
2181
2228
|
*/
|
|
@@ -2228,6 +2275,10 @@ var HookInstaller = class {
|
|
|
2228
2275
|
async addHookToSettings() {
|
|
2229
2276
|
let settings = await this.readClaudeSettings();
|
|
2230
2277
|
let changed = false;
|
|
2278
|
+
for (const cmd of LEGACY_HOOK_COMMANDS) {
|
|
2279
|
+
this.removeHookCommand(settings, "PreToolUse", cmd);
|
|
2280
|
+
this.removeHookCommand(settings, "PermissionRequest", cmd);
|
|
2281
|
+
}
|
|
2231
2282
|
if (!settings.hooks) {
|
|
2232
2283
|
settings.hooks = {};
|
|
2233
2284
|
}
|
|
@@ -2295,7 +2346,7 @@ var HookInstaller = class {
|
|
|
2295
2346
|
* 写入 Claude Code settings.json
|
|
2296
2347
|
*/
|
|
2297
2348
|
async writeClaudeSettings(settings) {
|
|
2298
|
-
await (0, import_promises2.mkdir)((0,
|
|
2349
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude"), { recursive: true });
|
|
2299
2350
|
await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2300
2351
|
}
|
|
2301
2352
|
/**
|
|
@@ -2304,11 +2355,11 @@ var HookInstaller = class {
|
|
|
2304
2355
|
hasHookConfig(settings) {
|
|
2305
2356
|
return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
|
|
2306
2357
|
}
|
|
2307
|
-
/** 检查 PreToolUse 中是否有 approval-hook.
|
|
2358
|
+
/** 检查 PreToolUse 中是否有 approval-hook.js */
|
|
2308
2359
|
hasPreToolUseConfig(settings) {
|
|
2309
2360
|
return this.hasHookEntry(settings?.hooks?.PreToolUse, HOOK_COMMAND);
|
|
2310
2361
|
}
|
|
2311
|
-
/** 检查 PermissionRequest 中是否有 permission-accept.
|
|
2362
|
+
/** 检查 PermissionRequest 中是否有 permission-accept.js */
|
|
2312
2363
|
hasPermissionRequestConfig(settings) {
|
|
2313
2364
|
return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
|
|
2314
2365
|
}
|
|
@@ -2322,7 +2373,7 @@ var HookInstaller = class {
|
|
|
2322
2373
|
};
|
|
2323
2374
|
|
|
2324
2375
|
// src/notification/NotificationService.ts
|
|
2325
|
-
var
|
|
2376
|
+
var import_node_path4 = require("path");
|
|
2326
2377
|
var NotificationService = class {
|
|
2327
2378
|
constructor(sessionManager, expoChannel = null) {
|
|
2328
2379
|
this.sessionManager = sessionManager;
|
|
@@ -2418,7 +2469,7 @@ var NotificationService = class {
|
|
|
2418
2469
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
2419
2470
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
2420
2471
|
const categoryId = isDangerous ? "APPROVAL_DANGEROUS" : "APPROVAL_NORMAL";
|
|
2421
|
-
const projectName = (0,
|
|
2472
|
+
const projectName = (0, import_node_path4.basename)(
|
|
2422
2473
|
this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId)?.projectPath ?? ""
|
|
2423
2474
|
);
|
|
2424
2475
|
const pushTitle = isDangerous ? `\u26A0\uFE0F ${title}` : title;
|
|
@@ -2474,7 +2525,7 @@ var NotificationService = class {
|
|
|
2474
2525
|
/** 从审批请求中提取操作目标的简短描述 */
|
|
2475
2526
|
extractTarget(request) {
|
|
2476
2527
|
const input = request.toolInput;
|
|
2477
|
-
if (input.file_path) return (0,
|
|
2528
|
+
if (input.file_path) return (0, import_node_path4.basename)(String(input.file_path));
|
|
2478
2529
|
if (input.command) return String(input.command).slice(0, 40);
|
|
2479
2530
|
return request.description.slice(0, 40);
|
|
2480
2531
|
}
|
|
@@ -2577,7 +2628,7 @@ var NotificationService = class {
|
|
|
2577
2628
|
getSessionTitle(sessionId) {
|
|
2578
2629
|
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
2579
2630
|
if (!session) return "Unknown";
|
|
2580
|
-
return session.summary ?? (0,
|
|
2631
|
+
return session.summary ?? (0, import_node_path4.basename)(session.projectPath);
|
|
2581
2632
|
}
|
|
2582
2633
|
/** 获取会话的 YOLO 模式状态 */
|
|
2583
2634
|
getYoloMode(sessionId) {
|
|
@@ -2585,9 +2636,9 @@ var NotificationService = class {
|
|
|
2585
2636
|
}
|
|
2586
2637
|
};
|
|
2587
2638
|
|
|
2588
|
-
// src/notification/
|
|
2589
|
-
var
|
|
2590
|
-
var
|
|
2639
|
+
// src/notification/DesktopNotificationChannel.ts
|
|
2640
|
+
var import_node_child_process3 = require("child_process");
|
|
2641
|
+
var DesktopNotificationChannel = class {
|
|
2591
2642
|
isAvailable() {
|
|
2592
2643
|
return process.platform === "darwin";
|
|
2593
2644
|
}
|
|
@@ -2598,9 +2649,9 @@ var MacNotificationChannel = class {
|
|
|
2598
2649
|
const sound = payload.sound ?? "Ping";
|
|
2599
2650
|
const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
|
|
2600
2651
|
return new Promise((resolve) => {
|
|
2601
|
-
(0,
|
|
2652
|
+
(0, import_node_child_process3.execFile)("osascript", ["-e", script], (err) => {
|
|
2602
2653
|
if (err) {
|
|
2603
|
-
console.warn("[
|
|
2654
|
+
console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
|
|
2604
2655
|
}
|
|
2605
2656
|
resolve();
|
|
2606
2657
|
});
|
|
@@ -3213,6 +3264,9 @@ var PairingManager = class {
|
|
|
3213
3264
|
this.close();
|
|
3214
3265
|
return result;
|
|
3215
3266
|
}
|
|
3267
|
+
updateToken(newToken) {
|
|
3268
|
+
this.token = newToken;
|
|
3269
|
+
}
|
|
3216
3270
|
getRemainingSeconds() {
|
|
3217
3271
|
if (this._state !== "open") return 0;
|
|
3218
3272
|
return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
|
|
@@ -3226,11 +3280,11 @@ var PairingManager = class {
|
|
|
3226
3280
|
};
|
|
3227
3281
|
|
|
3228
3282
|
// src/auth/AuthManager.ts
|
|
3283
|
+
var import_child_process2 = require("child_process");
|
|
3229
3284
|
var import_child_process3 = require("child_process");
|
|
3230
|
-
var import_child_process4 = require("child_process");
|
|
3231
3285
|
var import_util = require("util");
|
|
3232
3286
|
var import_events2 = require("events");
|
|
3233
|
-
var execFileAsync = (0, import_util.promisify)(
|
|
3287
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
3234
3288
|
var CLAUDE_PATH2 = findClaudePath();
|
|
3235
3289
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3236
3290
|
var AuthManager = class extends import_events2.EventEmitter {
|
|
@@ -3261,7 +3315,7 @@ var AuthManager = class extends import_events2.EventEmitter {
|
|
|
3261
3315
|
}
|
|
3262
3316
|
this.clearLoginTimeout();
|
|
3263
3317
|
this.urlSent = false;
|
|
3264
|
-
const proc = (0,
|
|
3318
|
+
const proc = (0, import_child_process2.spawn)(CLAUDE_PATH2, ["auth", "login"], {
|
|
3265
3319
|
env: { ...process.env, BROWSER: "echo" },
|
|
3266
3320
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3267
3321
|
});
|
|
@@ -3345,17 +3399,122 @@ var AuthManager = class extends import_events2.EventEmitter {
|
|
|
3345
3399
|
|
|
3346
3400
|
// src/server.ts
|
|
3347
3401
|
var import_promises5 = require("fs/promises");
|
|
3402
|
+
|
|
3403
|
+
// src/terminal/TerminalExecutor.ts
|
|
3404
|
+
var import_node_child_process4 = require("child_process");
|
|
3405
|
+
var import_uuid4 = require("uuid");
|
|
3406
|
+
var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3407
|
+
var TerminalExecutor = class {
|
|
3408
|
+
processes = /* @__PURE__ */ new Map();
|
|
3409
|
+
eventCallbacks = [];
|
|
3410
|
+
onEvent(callback) {
|
|
3411
|
+
this.eventCallbacks.push(callback);
|
|
3412
|
+
return () => {
|
|
3413
|
+
const idx = this.eventCallbacks.indexOf(callback);
|
|
3414
|
+
if (idx !== -1) this.eventCallbacks.splice(idx, 1);
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
emit(event) {
|
|
3418
|
+
for (const cb of this.eventCallbacks) {
|
|
3419
|
+
try {
|
|
3420
|
+
cb(event);
|
|
3421
|
+
} catch (err) {
|
|
3422
|
+
console.error("[TerminalExecutor] Event callback error:", err);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
exec(sessionId, command, cwd) {
|
|
3427
|
+
const execId = (0, import_uuid4.v4)();
|
|
3428
|
+
const shell = isWindows ? "powershell" : "bash";
|
|
3429
|
+
const args = isWindows ? ["-Command", command] : ["-c", command];
|
|
3430
|
+
const proc = (0, import_node_child_process4.spawn)(shell, args, {
|
|
3431
|
+
cwd,
|
|
3432
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3433
|
+
env: { ...process.env }
|
|
3434
|
+
});
|
|
3435
|
+
this.processes.set(execId, proc);
|
|
3436
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3437
|
+
this.emit({
|
|
3438
|
+
type: "terminal_output",
|
|
3439
|
+
sessionId,
|
|
3440
|
+
execId,
|
|
3441
|
+
stream: "stdout",
|
|
3442
|
+
data: chunk.toString()
|
|
3443
|
+
});
|
|
3444
|
+
});
|
|
3445
|
+
proc.stderr?.on("data", (chunk) => {
|
|
3446
|
+
this.emit({
|
|
3447
|
+
type: "terminal_output",
|
|
3448
|
+
sessionId,
|
|
3449
|
+
execId,
|
|
3450
|
+
stream: "stderr",
|
|
3451
|
+
data: chunk.toString()
|
|
3452
|
+
});
|
|
3453
|
+
});
|
|
3454
|
+
proc.on("exit", (code, signal) => {
|
|
3455
|
+
clearTimeout(timer);
|
|
3456
|
+
this.processes.delete(execId);
|
|
3457
|
+
this.emit({
|
|
3458
|
+
type: "terminal_exit",
|
|
3459
|
+
sessionId,
|
|
3460
|
+
execId,
|
|
3461
|
+
code,
|
|
3462
|
+
signal
|
|
3463
|
+
});
|
|
3464
|
+
});
|
|
3465
|
+
const timer = setTimeout(() => {
|
|
3466
|
+
if (this.processes.has(execId)) {
|
|
3467
|
+
killProcessCrossPlatform(proc);
|
|
3468
|
+
}
|
|
3469
|
+
}, EXEC_TIMEOUT_MS);
|
|
3470
|
+
console.log(`[TerminalExecutor] exec ${execId}: ${command.substring(0, 100)} (cwd: ${cwd})`);
|
|
3471
|
+
return execId;
|
|
3472
|
+
}
|
|
3473
|
+
kill(execId) {
|
|
3474
|
+
const proc = this.processes.get(execId);
|
|
3475
|
+
if (proc) {
|
|
3476
|
+
killProcessCrossPlatform(proc);
|
|
3477
|
+
console.log(`[TerminalExecutor] kill ${execId}`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
destroy() {
|
|
3481
|
+
for (const [execId, proc] of this.processes) {
|
|
3482
|
+
killProcessCrossPlatform(proc);
|
|
3483
|
+
console.log(`[TerminalExecutor] cleanup ${execId}`);
|
|
3484
|
+
}
|
|
3485
|
+
this.processes.clear();
|
|
3486
|
+
this.eventCallbacks.length = 0;
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
|
|
3490
|
+
// src/server.ts
|
|
3348
3491
|
var WS_PORT = 3745;
|
|
3349
3492
|
var HTTP_PORT = 3746;
|
|
3350
|
-
var execAsync = (0, import_node_util.promisify)(
|
|
3493
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process5.exec);
|
|
3351
3494
|
async function killPortProcess(port) {
|
|
3352
3495
|
try {
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3496
|
+
if (isWindows) {
|
|
3497
|
+
const { stdout } = await execAsync(
|
|
3498
|
+
`netstat -ano | findstr :${port} | findstr LISTENING`
|
|
3499
|
+
);
|
|
3500
|
+
const pids = /* @__PURE__ */ new Set();
|
|
3501
|
+
for (const line of stdout.trim().split("\n")) {
|
|
3502
|
+
const parts = line.trim().split(/\s+/);
|
|
3503
|
+
const pid = parts[parts.length - 1];
|
|
3504
|
+
if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
|
|
3505
|
+
}
|
|
3506
|
+
for (const pid of pids) {
|
|
3507
|
+
await execAsync(`taskkill /PID ${pid} /F`).catch(() => {
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
} else {
|
|
3511
|
+
const { stdout } = await execAsync(`lsof -ti :${port}`);
|
|
3512
|
+
const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
|
|
3513
|
+
if (pids.length > 0) {
|
|
3514
|
+
await execAsync(`kill -9 ${pids.join(" ")}`);
|
|
3515
|
+
}
|
|
3358
3516
|
}
|
|
3517
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
3359
3518
|
} catch {
|
|
3360
3519
|
}
|
|
3361
3520
|
}
|
|
@@ -3373,8 +3532,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
3373
3532
|
}
|
|
3374
3533
|
}
|
|
3375
3534
|
async function start(opts = {}) {
|
|
3376
|
-
const configDir = (0,
|
|
3377
|
-
const tokenFile = (0,
|
|
3535
|
+
const configDir = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".sessix");
|
|
3536
|
+
const tokenFile = (0, import_node_path5.join)(configDir, "token");
|
|
3378
3537
|
let token;
|
|
3379
3538
|
if (opts.token !== void 0) {
|
|
3380
3539
|
token = opts.token;
|
|
@@ -3386,7 +3545,7 @@ async function start(opts = {}) {
|
|
|
3386
3545
|
try {
|
|
3387
3546
|
token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
|
|
3388
3547
|
} catch {
|
|
3389
|
-
token = (0,
|
|
3548
|
+
token = (0, import_uuid5.v4)();
|
|
3390
3549
|
await (0, import_promises4.mkdir)(configDir, { recursive: true });
|
|
3391
3550
|
await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
|
|
3392
3551
|
}
|
|
@@ -3394,10 +3553,24 @@ async function start(opts = {}) {
|
|
|
3394
3553
|
}
|
|
3395
3554
|
const provider = new ProcessProvider();
|
|
3396
3555
|
const sessionManager = new SessionManager(provider);
|
|
3556
|
+
const terminalExecutor = new TerminalExecutor();
|
|
3557
|
+
const wsBridge = await createWithRetry(
|
|
3558
|
+
"WsBridge",
|
|
3559
|
+
WS_PORT,
|
|
3560
|
+
() => WsBridge.create({ port: WS_PORT, token })
|
|
3561
|
+
);
|
|
3562
|
+
const unreadSessionIds = /* @__PURE__ */ new Set();
|
|
3563
|
+
sessionManager.onEvent((event) => {
|
|
3564
|
+
if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
|
|
3565
|
+
if (!wsBridge.isViewingSession(event.sessionId)) {
|
|
3566
|
+
unreadSessionIds.add(event.sessionId);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
});
|
|
3397
3570
|
const expoChannel = new ExpoNotificationChannel();
|
|
3398
3571
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
3399
3572
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
3400
|
-
notificationService.addChannel("mac", new
|
|
3573
|
+
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
3401
3574
|
if (opts.activityPush) {
|
|
3402
3575
|
try {
|
|
3403
3576
|
const activityChannel = new ActivityPushChannel(opts.activityPush);
|
|
@@ -3408,11 +3581,6 @@ async function start(opts = {}) {
|
|
|
3408
3581
|
console.log(`[Server] ${t("server.activityPushContinue")}`);
|
|
3409
3582
|
}
|
|
3410
3583
|
}
|
|
3411
|
-
const wsBridge = await createWithRetry(
|
|
3412
|
-
"WsBridge",
|
|
3413
|
-
WS_PORT,
|
|
3414
|
-
() => WsBridge.create({ port: WS_PORT, token })
|
|
3415
|
-
);
|
|
3416
3584
|
const sessionFileWatcher = new SessionFileWatcher((event) => {
|
|
3417
3585
|
wsBridge.broadcast(event);
|
|
3418
3586
|
});
|
|
@@ -3424,7 +3592,7 @@ async function start(opts = {}) {
|
|
|
3424
3592
|
let mdnsService = null;
|
|
3425
3593
|
const pairingManager = new PairingManager({
|
|
3426
3594
|
token,
|
|
3427
|
-
serverName: (0,
|
|
3595
|
+
serverName: (0, import_node_os6.hostname)(),
|
|
3428
3596
|
version: "0.2.0",
|
|
3429
3597
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
3430
3598
|
});
|
|
@@ -3441,7 +3609,6 @@ async function start(opts = {}) {
|
|
|
3441
3609
|
});
|
|
3442
3610
|
}
|
|
3443
3611
|
});
|
|
3444
|
-
const unreadSessionIds = /* @__PURE__ */ new Set();
|
|
3445
3612
|
notificationService.setGlobalPendingCountProvider(
|
|
3446
3613
|
() => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
|
|
3447
3614
|
);
|
|
@@ -3472,6 +3639,8 @@ async function start(opts = {}) {
|
|
|
3472
3639
|
switch (event.type) {
|
|
3473
3640
|
case "create_session": {
|
|
3474
3641
|
await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
|
|
3642
|
+
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
3643
|
+
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
3475
3644
|
await sessionManager.createSession(
|
|
3476
3645
|
event.projectPath,
|
|
3477
3646
|
event.message,
|
|
@@ -3489,6 +3658,7 @@ async function start(opts = {}) {
|
|
|
3489
3658
|
break;
|
|
3490
3659
|
}
|
|
3491
3660
|
case "send_message": {
|
|
3661
|
+
sessionFileWatcher.unwatch(event.sessionId);
|
|
3492
3662
|
await sessionManager.sendMessage(event.sessionId, event.message, event.permissionMode, event.images);
|
|
3493
3663
|
wsBridge.broadcast({
|
|
3494
3664
|
type: "session_list",
|
|
@@ -3577,6 +3747,10 @@ async function start(opts = {}) {
|
|
|
3577
3747
|
code: "PROJECT_LIST_ERROR"
|
|
3578
3748
|
});
|
|
3579
3749
|
}
|
|
3750
|
+
wsBridge.send(ws, {
|
|
3751
|
+
type: "session_list",
|
|
3752
|
+
sessions: sessionManager.getActiveSessions()
|
|
3753
|
+
});
|
|
3580
3754
|
break;
|
|
3581
3755
|
}
|
|
3582
3756
|
case "list_sessions": {
|
|
@@ -3671,6 +3845,20 @@ async function start(opts = {}) {
|
|
|
3671
3845
|
notificationService.setSoundPreferences(event.preferences);
|
|
3672
3846
|
break;
|
|
3673
3847
|
}
|
|
3848
|
+
case "terminal_exec": {
|
|
3849
|
+
const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
|
|
3850
|
+
const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
|
|
3851
|
+
if (!cwd) {
|
|
3852
|
+
wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
|
|
3853
|
+
break;
|
|
3854
|
+
}
|
|
3855
|
+
terminalExecutor.exec(event.sessionId, event.command, cwd);
|
|
3856
|
+
break;
|
|
3857
|
+
}
|
|
3858
|
+
case "terminal_kill": {
|
|
3859
|
+
terminalExecutor.kill(event.execId);
|
|
3860
|
+
break;
|
|
3861
|
+
}
|
|
3674
3862
|
case "register_activity_push_token": {
|
|
3675
3863
|
notificationService.addActivityPushToken(event.sessionId, event.token);
|
|
3676
3864
|
break;
|
|
@@ -3742,12 +3930,14 @@ async function start(opts = {}) {
|
|
|
3742
3930
|
sessionManager.onEvent((event) => {
|
|
3743
3931
|
wsBridge.broadcast(event);
|
|
3744
3932
|
if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
|
|
3745
|
-
if (
|
|
3746
|
-
unreadSessionIds.add(event.sessionId);
|
|
3933
|
+
if (unreadSessionIds.has(event.sessionId)) {
|
|
3747
3934
|
broadcastUnreadSessions();
|
|
3748
3935
|
}
|
|
3749
3936
|
}
|
|
3750
3937
|
});
|
|
3938
|
+
terminalExecutor.onEvent((event) => {
|
|
3939
|
+
wsBridge.broadcast(event);
|
|
3940
|
+
});
|
|
3751
3941
|
wsBridge.onDisconnect(() => {
|
|
3752
3942
|
if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
|
|
3753
3943
|
approvalProxy.approveAll(t("server.phoneDisconnected"));
|
|
@@ -3794,12 +3984,17 @@ async function start(opts = {}) {
|
|
|
3794
3984
|
}));
|
|
3795
3985
|
const startMdns = () => {
|
|
3796
3986
|
if (mdnsService) return;
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3987
|
+
try {
|
|
3988
|
+
mdnsService = new MdnsService({
|
|
3989
|
+
wsPort: WS_PORT,
|
|
3990
|
+
httpPort: HTTP_PORT,
|
|
3991
|
+
pairing: pairingManager.state
|
|
3992
|
+
});
|
|
3993
|
+
mdnsService.start();
|
|
3994
|
+
} catch (err) {
|
|
3995
|
+
console.warn(`[Server] mDNS failed to start (non-fatal): ${err.message}`);
|
|
3996
|
+
mdnsService = null;
|
|
3997
|
+
}
|
|
3803
3998
|
};
|
|
3804
3999
|
const stopMdns = () => {
|
|
3805
4000
|
if (!mdnsService) return;
|
|
@@ -3842,6 +4037,7 @@ async function start(opts = {}) {
|
|
|
3842
4037
|
await attempt(() => wsBridge.close(), "WebSocket");
|
|
3843
4038
|
await attempt(() => approvalProxy.close(), "ApprovalProxy");
|
|
3844
4039
|
await attempt(() => sessionManager.destroy(), "SessionManager");
|
|
4040
|
+
await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
|
|
3845
4041
|
await attempt(() => notificationService.destroy(), "NotificationService");
|
|
3846
4042
|
await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
|
|
3847
4043
|
if (errors.length > 0) {
|
|
@@ -3850,7 +4046,7 @@ async function start(opts = {}) {
|
|
|
3850
4046
|
}
|
|
3851
4047
|
console.log(`[Server] ${t("server.shutdownComplete")}`);
|
|
3852
4048
|
};
|
|
3853
|
-
|
|
4049
|
+
const instance = {
|
|
3854
4050
|
token,
|
|
3855
4051
|
wsPort: WS_PORT,
|
|
3856
4052
|
httpPort: HTTP_PORT,
|
|
@@ -3868,15 +4064,28 @@ async function start(opts = {}) {
|
|
|
3868
4064
|
}
|
|
3869
4065
|
},
|
|
3870
4066
|
openPairing: (duration) => pairingManager.open(duration),
|
|
3871
|
-
closePairing: () => pairingManager.close()
|
|
4067
|
+
closePairing: () => pairingManager.close(),
|
|
4068
|
+
regenerateToken: async () => {
|
|
4069
|
+
const newToken = (0, import_uuid5.v4)();
|
|
4070
|
+
await (0, import_promises4.mkdir)(configDir, { recursive: true });
|
|
4071
|
+
await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
|
|
4072
|
+
instance.token = newToken;
|
|
4073
|
+
wsBridge.updateToken(newToken);
|
|
4074
|
+
approvalProxy.updateToken(newToken);
|
|
4075
|
+
pairingManager.updateToken(newToken);
|
|
4076
|
+
pairingManager.open();
|
|
4077
|
+
console.log(`[Server] ${t("server.tokenRegenerated", { token: newToken })}`);
|
|
4078
|
+
return newToken;
|
|
4079
|
+
}
|
|
3872
4080
|
};
|
|
4081
|
+
return instance;
|
|
3873
4082
|
}
|
|
3874
4083
|
|
|
3875
4084
|
// src/index.ts
|
|
3876
4085
|
var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
3877
4086
|
function getPackageVersion() {
|
|
3878
4087
|
try {
|
|
3879
|
-
const pkg = JSON.parse((0,
|
|
4088
|
+
const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf8"));
|
|
3880
4089
|
return pkg.version ?? "0.0.0";
|
|
3881
4090
|
} catch {
|
|
3882
4091
|
return "0.0.0";
|
|
@@ -3941,6 +4150,7 @@ async function main() {
|
|
|
3941
4150
|
console.log(t("startup.waitingConnection"));
|
|
3942
4151
|
console.log();
|
|
3943
4152
|
console.log(t("startup.pairingOpen"));
|
|
4153
|
+
console.log(t("startup.pressT"));
|
|
3944
4154
|
console.log();
|
|
3945
4155
|
fetchLatestVersion().then((latest) => {
|
|
3946
4156
|
if (!latest || latest === PKG_VERSION) return;
|
|
@@ -3971,6 +4181,23 @@ async function main() {
|
|
|
3971
4181
|
console.log(`
|
|
3972
4182
|
${t("startup.pairingReopened")}`);
|
|
3973
4183
|
}
|
|
4184
|
+
if (key === "t" || key === "T") {
|
|
4185
|
+
server.regenerateToken().then((newToken) => {
|
|
4186
|
+
console.log();
|
|
4187
|
+
console.log(` ${t("startup.tokenRegenerated")}`);
|
|
4188
|
+
console.log(t("startup.token", { token: newToken }));
|
|
4189
|
+
console.log();
|
|
4190
|
+
const newQrUrl = buildQrUrl(getLocalIp(), server.wsPort, newToken);
|
|
4191
|
+
console.log(t("startup.scanToPair"));
|
|
4192
|
+
import_qrcode_terminal.default.generate(newQrUrl, { small: true }, (qr) => {
|
|
4193
|
+
qr.split("\n").forEach((line) => console.log(` ${line}`));
|
|
4194
|
+
});
|
|
4195
|
+
console.log();
|
|
4196
|
+
}).catch((err) => {
|
|
4197
|
+
console.error(`
|
|
4198
|
+
${t("startup.tokenRegenerateFailed")}`, err);
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
3974
4201
|
if (key === "") {
|
|
3975
4202
|
shutdown("SIGINT");
|
|
3976
4203
|
}
|
|
@@ -3978,10 +4205,11 @@ ${t("startup.pairingReopened")}`);
|
|
|
3978
4205
|
} else {
|
|
3979
4206
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
3980
4207
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
4208
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
3981
4209
|
}
|
|
3982
4210
|
}
|
|
3983
4211
|
function getLocalIp() {
|
|
3984
|
-
const interfaces = (0,
|
|
4212
|
+
const interfaces = (0, import_node_os7.networkInterfaces)();
|
|
3985
4213
|
for (const iface of Object.values(interfaces)) {
|
|
3986
4214
|
for (const addr of iface ?? []) {
|
|
3987
4215
|
if (addr.family === "IPv4" && !addr.internal) {
|
|
@@ -3989,7 +4217,7 @@ function getLocalIp() {
|
|
|
3989
4217
|
}
|
|
3990
4218
|
}
|
|
3991
4219
|
}
|
|
3992
|
-
return "<your-
|
|
4220
|
+
return "<your-ip>";
|
|
3993
4221
|
}
|
|
3994
4222
|
function buildQrUrl(ip, wsPort, token) {
|
|
3995
4223
|
const base = `sessix://${ip}:${wsPort}`;
|