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 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 import_node_os6 = require("os");
28
- var import_node_fs2 = require("fs");
29
- var import_node_path5 = require("path");
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 import_uuid4 = require("uuid");
298
+ var import_uuid5 = require("uuid");
291
299
  var import_promises4 = require("fs/promises");
292
- var import_node_os5 = require("os");
293
- var import_node_path4 = require("path");
294
- var import_node_child_process2 = require("child_process");
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 import_child_process2 = require("child_process");
306
+ var import_child_process = require("child_process");
299
307
  var import_readline = require("readline");
300
308
  var import_events = require("events");
301
- var import_node_os = require("os");
309
+ var import_node_os2 = require("os");
302
310
  var import_uuid = require("uuid");
303
311
 
304
312
  // src/utils/claudePath.ts
305
- var import_child_process = require("child_process");
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
- return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
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
- const candidates = [
311
- `${process.env.HOME}/.local/bin/claude`,
312
- "/usr/local/bin/claude",
313
- "/opt/homebrew/bin/claude"
314
- ];
315
- for (const candidate of candidates) {
316
- try {
317
- (0, import_child_process.execSync)(`test -x "${candidate}"`);
318
- return candidate;
319
- } catch {
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.kill("SIGTERM");
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.kill("SIGTERM");
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, import_child_process2.spawn)(CLAUDE_PATH, args, {
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 === 0 || code === 143 || signal === "SIGTERM";
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, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
733
- cwd: (0, import_node_os.homedir)(),
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 import_node_fs = __toESM(require("fs"));
1626
- var import_node_path = __toESM(require("path"));
1627
- var import_node_os2 = __toESM(require("os"));
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 = import_node_path.default.join(import_node_os2.default.homedir(), ".claude", "settings.json");
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 = import_node_fs.default.readFileSync(filepath, "utf-8");
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 = import_node_path.default.join(projectPath, ".claude", "settings.json");
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 ? import_node_path.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
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 = import_node_path.default.dirname(targetPath);
1763
- if (!import_node_fs.default.existsSync(dir)) {
1764
- import_node_fs.default.mkdirSync(dir, { recursive: true });
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(import_node_fs.default.readFileSync(targetPath, "utf-8"));
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
- import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
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 import_node_os3 = require("os");
2063
+ var import_node_os4 = require("os");
2008
2064
  function getLanAddresses() {
2009
2065
  const results = [];
2010
- for (const [name, addrs] of Object.entries((0, import_node_os3.networkInterfaces)())) {
2011
- if (name.startsWith("utun") || name.startsWith("lo")) continue;
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 import_node_path2 = require("path");
2114
- var import_node_os4 = require("os");
2115
- var SESSIX_HOOKS_DIR = (0, import_node_path2.join)((0, import_node_os4.homedir)(), ".sessix", "hooks");
2116
- var HOOK_SCRIPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "approval-hook.sh");
2117
- var PERMISSION_ACCEPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "permission-accept.sh");
2118
- var CLAUDE_SETTINGS_PATH = (0, import_node_path2.join)((0, import_node_os4.homedir)(), ".claude", "settings.json");
2119
- var HOOK_COMMAND = "~/.sessix/hooks/approval-hook.sh";
2120
- var PERMISSION_ACCEPT_COMMAND = "~/.sessix/hooks/permission-accept.sh";
2121
- var HOOK_SCRIPT_TEMPLATE = `#!/bin/bash
2122
- # Sessix Approval Hook
2123
- # \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
2124
-
2125
- if [ -z "$SESSIX_SESSION_ID" ]; then
2126
- exit 0
2127
- fi
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
- # \u89E3\u6790\u670D\u52A1\u5668\u54CD\u5E94
2148
- DECISION=$(echo "$RESPONSE" | grep -o '\\"decision\\":\\"[^"]*\\"' | cut -d'"' -f4)
2186
+ const sessionId = process.env.SESSIX_SESSION_ID
2187
+ if (!sessionId) process.exit(0)
2149
2188
 
2150
- if [ "$DECISION" = "allow" ]; then
2151
- # \u7528\u6237\u6279\u51C6\u6216\u670D\u52A1\u5668\u8D85\u65F6\u81EA\u52A8\u6279\u51C6
2152
- exit 0
2153
- elif [ "$DECISION" = "deny" ]; then
2154
- # \u7528\u6237\u660E\u786E\u62D2\u7EDD
2155
- exit 1
2156
- else
2157
- # \u672A\u77E5\u54CD\u5E94\uFF0C\u9ED8\u8BA4\u653E\u884C
2158
- exit 0
2159
- fi
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/bash
2162
- # Sessix PermissionRequest \u515C\u5E95
2163
- # \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
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 [ -z "$SESSIX_SESSION_ID" ]; then
2166
- exit 0
2167
- fi
2215
+ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
2168
2216
 
2169
- # \u8F93\u51FA JSON \u51B3\u7B56\uFF0C\u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42
2170
- echo '{"decision":"allow"}'
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.sh 脚本
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, import_node_path2.join)((0, import_node_os4.homedir)(), ".claude"), { recursive: true });
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.sh */
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.sh */
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 import_node_path3 = require("path");
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, import_node_path3.basename)(
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, import_node_path3.basename)(String(input.file_path));
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, import_node_path3.basename)(session.projectPath);
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/MacNotificationChannel.ts
2589
- var import_node_child_process = require("child_process");
2590
- var MacNotificationChannel = class {
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, import_node_child_process.execFile)("osascript", ["-e", script], (err) => {
2652
+ (0, import_node_child_process3.execFile)("osascript", ["-e", script], (err) => {
2602
2653
  if (err) {
2603
- console.warn("[MacNotificationChannel] Send notification failed:", err.message);
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)(import_child_process4.execFile);
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, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
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)(import_node_child_process2.exec);
3493
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process5.exec);
3351
3494
  async function killPortProcess(port) {
3352
3495
  try {
3353
- const { stdout } = await execAsync(`lsof -ti :${port}`);
3354
- const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
3355
- if (pids.length > 0) {
3356
- await execAsync(`kill -9 ${pids.join(" ")}`);
3357
- await new Promise((resolve) => setTimeout(resolve, 600));
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, import_node_path4.join)((0, import_node_os5.homedir)(), ".sessix");
3377
- const tokenFile = (0, import_node_path4.join)(configDir, "token");
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, import_uuid4.v4)();
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 MacNotificationChannel(), opts.enableMacNotification !== false);
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, import_node_os5.hostname)(),
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 (!wsBridge.isViewingSession(event.sessionId)) {
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
- mdnsService = new MdnsService({
3798
- wsPort: WS_PORT,
3799
- httpPort: HTTP_PORT,
3800
- pairing: pairingManager.state
3801
- });
3802
- mdnsService.start();
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
- return {
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, import_node_fs2.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf8"));
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, import_node_os6.networkInterfaces)();
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-mac-ip>";
4220
+ return "<your-ip>";
3993
4221
  }
3994
4222
  function buildQrUrl(ip, wsPort, token) {
3995
4223
  const base = `sessix://${ip}:${wsPort}`;