sessix-server 0.2.6 → 0.2.8

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
@@ -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,11 +295,11 @@ 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
300
  var import_node_os6 = require("os");
293
301
  var import_node_path5 = require("path");
294
- var import_node_child_process4 = require("child_process");
302
+ var import_node_child_process6 = require("child_process");
295
303
  var import_node_util = require("util");
296
304
 
297
305
  // src/providers/ProcessProvider.ts
@@ -1538,6 +1546,13 @@ var WsBridge = class _WsBridge {
1538
1546
  getConnectionCount() {
1539
1547
  return this.wss.clients.size;
1540
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
+ }
1541
1556
  /** 优雅关闭 WebSocket 服务 */
1542
1557
  close() {
1543
1558
  return new Promise((resolve, reject) => {
@@ -1977,6 +1992,10 @@ var ApprovalProxy = class _ApprovalProxy {
1977
1992
  });
1978
1993
  }
1979
1994
  }
1995
+ /** 更新 token(token 刷新时调用) */
1996
+ updateToken(newToken) {
1997
+ this.token = newToken;
1998
+ }
1980
1999
  /** 返回连接 token(仅本机访问) */
1981
2000
  handleToken(req, res) {
1982
2001
  const remoteAddress = req.socket.remoteAddress;
@@ -2040,79 +2059,120 @@ var ApprovalProxy = class _ApprovalProxy {
2040
2059
  };
2041
2060
 
2042
2061
  // src/mdns/MdnsService.ts
2043
- var import_bonjour_service = __toESM(require("bonjour-service"));
2062
+ var import_node_child_process3 = require("child_process");
2044
2063
  var import_node_os4 = require("os");
2045
- function getLanAddresses() {
2046
- const results = [];
2047
- for (const [name, addrs] of Object.entries((0, import_node_os4.networkInterfaces)())) {
2048
- if (name.startsWith("utun") || name === "lo") continue;
2049
- if (isWindows && (name.startsWith("vEthernet") || name.includes("Loopback"))) continue;
2050
- for (const addr of addrs ?? []) {
2051
- if (addr.family === "IPv4" && !addr.internal) {
2052
- results.push(addr.address);
2053
- }
2054
- }
2055
- }
2056
- return results;
2064
+ function buildTxtArgs(txt) {
2065
+ return Object.entries(txt).map(([k, v]) => `${k}=${v}`);
2057
2066
  }
2058
2067
  var MdnsService = class {
2059
- bonjour = null;
2060
- service = null;
2068
+ proc = null;
2069
+ bonjourInstance = null;
2070
+ bonjourService = null;
2061
2071
  wsPort;
2062
2072
  httpPort;
2063
2073
  version;
2064
2074
  pairing;
2075
+ useDnsSd;
2065
2076
  constructor(options) {
2066
2077
  this.wsPort = options.wsPort;
2067
2078
  this.httpPort = options.httpPort;
2068
2079
  this.version = options.version ?? "0.1.0";
2069
2080
  this.pairing = options.pairing ?? "closed";
2081
+ this.useDnsSd = (0, import_node_os4.platform)() === "darwin";
2082
+ }
2083
+ getTxt() {
2084
+ return {
2085
+ version: this.version,
2086
+ httpPort: String(this.httpPort),
2087
+ wsPort: String(this.wsPort),
2088
+ pairing: this.pairing
2089
+ };
2070
2090
  }
2071
2091
  /**
2072
2092
  * 启动 mDNS 广播
2073
2093
  */
2074
2094
  start() {
2075
- if (this.bonjour) {
2095
+ if (this.useDnsSd) {
2096
+ this.startDnsSd();
2097
+ } else {
2098
+ this.startBonjour();
2099
+ }
2100
+ }
2101
+ startDnsSd() {
2102
+ if (this.proc) {
2076
2103
  console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
2077
2104
  return;
2078
2105
  }
2079
- const lanAddrs = getLanAddresses();
2080
- const onMdnsError = (err) => {
2081
- console.warn(`[MdnsService] mDNS error (non-fatal): ${err.message}`);
2082
- this.stop();
2083
- };
2084
- const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
2085
- this.bonjour = new import_bonjour_service.default(opts, onMdnsError);
2086
- this.bonjour.server?.mdns?.on("error", onMdnsError);
2087
- if (lanAddrs.length > 0) {
2088
- console.log(`[MdnsService] ${t("mdns.boundInterface", { ip: lanAddrs[0] })}`);
2089
- }
2090
- this.service = this.bonjour.publish({
2091
- name: "Sessix",
2092
- type: "sessix",
2093
- port: this.wsPort,
2094
- txt: {
2095
- version: this.version,
2096
- httpPort: String(this.httpPort),
2097
- wsPort: String(this.wsPort),
2098
- pairing: this.pairing
2106
+ const args = [
2107
+ "-R",
2108
+ "Sessix",
2109
+ "_sessix._tcp",
2110
+ "local",
2111
+ String(this.wsPort),
2112
+ ...buildTxtArgs(this.getTxt())
2113
+ ];
2114
+ this.proc = (0, import_node_child_process3.spawn)("dns-sd", args, { stdio: "ignore" });
2115
+ this.proc.on("error", (err) => {
2116
+ console.warn(`[MdnsService] dns-sd failed, falling back to bonjour-service: ${err.message}`);
2117
+ this.proc = null;
2118
+ this.useDnsSd = false;
2119
+ this.startBonjour();
2120
+ });
2121
+ this.proc.on("exit", (code) => {
2122
+ if (code !== null && code !== 0) {
2123
+ console.warn(`[MdnsService] dns-sd exited with code ${code}`);
2099
2124
  }
2125
+ this.proc = null;
2100
2126
  });
2101
- console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
2127
+ console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })} (dns-sd)`);
2128
+ }
2129
+ async startBonjour() {
2130
+ if (this.bonjourInstance) {
2131
+ console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
2132
+ return;
2133
+ }
2134
+ try {
2135
+ const { default: Bonjour } = await import("bonjour-service");
2136
+ const { networkInterfaces: networkInterfaces2 } = await import("os");
2137
+ const lanAddrs = getLanAddresses(networkInterfaces2);
2138
+ const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
2139
+ const onError = (err) => {
2140
+ if (err.code === "EADDRINUSE") return;
2141
+ console.warn(`[MdnsService] mDNS error (non-fatal): ${err.message}`);
2142
+ };
2143
+ this.bonjourInstance = new Bonjour(opts, onError);
2144
+ this.bonjourInstance.server?.mdns?.on("error", onError);
2145
+ if (lanAddrs.length > 0) {
2146
+ console.log(`[MdnsService] ${t("mdns.boundInterface", { ip: lanAddrs[0] })}`);
2147
+ }
2148
+ this.bonjourService = this.bonjourInstance.publish({
2149
+ name: "Sessix",
2150
+ type: "sessix",
2151
+ port: this.wsPort,
2152
+ txt: this.getTxt()
2153
+ });
2154
+ console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })} (bonjour-service)`);
2155
+ } catch (err) {
2156
+ console.warn(`[MdnsService] bonjour-service failed: ${err.message}`);
2157
+ }
2102
2158
  }
2103
2159
  /**
2104
2160
  * 停止 mDNS 广播
2105
2161
  */
2106
2162
  stop() {
2107
- if (this.service) {
2108
- this.service.stop?.(() => {
2163
+ if (this.proc) {
2164
+ this.proc.kill();
2165
+ this.proc = null;
2166
+ }
2167
+ if (this.bonjourService) {
2168
+ this.bonjourService.stop?.(() => {
2109
2169
  console.log(`[MdnsService] ${t("mdns.stopped")}`);
2110
2170
  });
2111
- this.service = null;
2171
+ this.bonjourService = null;
2112
2172
  }
2113
- if (this.bonjour) {
2114
- this.bonjour.destroy();
2115
- this.bonjour = null;
2173
+ if (this.bonjourInstance) {
2174
+ this.bonjourInstance.destroy();
2175
+ this.bonjourInstance = null;
2116
2176
  }
2117
2177
  console.log(`[MdnsService] ${t("mdns.closed")}`);
2118
2178
  }
@@ -2121,30 +2181,45 @@ var MdnsService = class {
2121
2181
  */
2122
2182
  updatePairingState(state) {
2123
2183
  this.pairing = state;
2124
- if (!this.bonjour) return;
2184
+ if (this.useDnsSd) {
2185
+ if (this.proc) {
2186
+ this.proc.kill();
2187
+ this.proc = null;
2188
+ }
2189
+ this.startDnsSd();
2190
+ return;
2191
+ }
2192
+ if (!this.bonjourInstance) return;
2125
2193
  const republish = () => {
2126
- if (!this.bonjour) return;
2127
- this.service = this.bonjour.publish({
2194
+ if (!this.bonjourInstance) return;
2195
+ this.bonjourService = this.bonjourInstance.publish({
2128
2196
  name: "Sessix",
2129
2197
  type: "sessix",
2130
2198
  port: this.wsPort,
2131
- txt: {
2132
- version: this.version,
2133
- httpPort: String(this.httpPort),
2134
- wsPort: String(this.wsPort),
2135
- pairing: state
2136
- }
2199
+ txt: this.getTxt()
2137
2200
  });
2138
2201
  };
2139
- if (this.service) {
2140
- const old = this.service;
2141
- this.service = null;
2202
+ if (this.bonjourService) {
2203
+ const old = this.bonjourService;
2204
+ this.bonjourService = null;
2142
2205
  old.stop?.(() => republish());
2143
2206
  } else {
2144
2207
  republish();
2145
2208
  }
2146
2209
  }
2147
2210
  };
2211
+ function getLanAddresses(networkInterfacesFn) {
2212
+ const results = [];
2213
+ for (const [name, addrs] of Object.entries(networkInterfacesFn())) {
2214
+ if (name.startsWith("utun") || name === "lo") continue;
2215
+ for (const addr of addrs ?? []) {
2216
+ if (addr.family === "IPv4" && !addr.internal) {
2217
+ results.push(addr.address);
2218
+ }
2219
+ }
2220
+ }
2221
+ return results;
2222
+ }
2148
2223
 
2149
2224
  // src/hooks/HookInstaller.ts
2150
2225
  var import_promises2 = require("fs/promises");
@@ -2618,7 +2693,7 @@ var NotificationService = class {
2618
2693
  };
2619
2694
 
2620
2695
  // src/notification/DesktopNotificationChannel.ts
2621
- var import_node_child_process3 = require("child_process");
2696
+ var import_node_child_process4 = require("child_process");
2622
2697
  var DesktopNotificationChannel = class {
2623
2698
  isAvailable() {
2624
2699
  return process.platform === "darwin";
@@ -2630,7 +2705,7 @@ var DesktopNotificationChannel = class {
2630
2705
  const sound = payload.sound ?? "Ping";
2631
2706
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
2632
2707
  return new Promise((resolve) => {
2633
- (0, import_node_child_process3.execFile)("osascript", ["-e", script], (err) => {
2708
+ (0, import_node_child_process4.execFile)("osascript", ["-e", script], (err) => {
2634
2709
  if (err) {
2635
2710
  console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
2636
2711
  }
@@ -3245,6 +3320,9 @@ var PairingManager = class {
3245
3320
  this.close();
3246
3321
  return result;
3247
3322
  }
3323
+ updateToken(newToken) {
3324
+ this.token = newToken;
3325
+ }
3248
3326
  getRemainingSeconds() {
3249
3327
  if (this._state !== "open") return 0;
3250
3328
  return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
@@ -3377,9 +3455,98 @@ var AuthManager = class extends import_events2.EventEmitter {
3377
3455
 
3378
3456
  // src/server.ts
3379
3457
  var import_promises5 = require("fs/promises");
3458
+
3459
+ // src/terminal/TerminalExecutor.ts
3460
+ var import_node_child_process5 = require("child_process");
3461
+ var import_uuid4 = require("uuid");
3462
+ var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
3463
+ var TerminalExecutor = class {
3464
+ processes = /* @__PURE__ */ new Map();
3465
+ eventCallbacks = [];
3466
+ onEvent(callback) {
3467
+ this.eventCallbacks.push(callback);
3468
+ return () => {
3469
+ const idx = this.eventCallbacks.indexOf(callback);
3470
+ if (idx !== -1) this.eventCallbacks.splice(idx, 1);
3471
+ };
3472
+ }
3473
+ emit(event) {
3474
+ for (const cb of this.eventCallbacks) {
3475
+ try {
3476
+ cb(event);
3477
+ } catch (err) {
3478
+ console.error("[TerminalExecutor] Event callback error:", err);
3479
+ }
3480
+ }
3481
+ }
3482
+ exec(sessionId, command, cwd) {
3483
+ const execId = (0, import_uuid4.v4)();
3484
+ const shell = isWindows ? "powershell" : "bash";
3485
+ const args = isWindows ? ["-Command", command] : ["-c", command];
3486
+ const proc = (0, import_node_child_process5.spawn)(shell, args, {
3487
+ cwd,
3488
+ stdio: ["ignore", "pipe", "pipe"],
3489
+ env: { ...process.env }
3490
+ });
3491
+ this.processes.set(execId, proc);
3492
+ proc.stdout?.on("data", (chunk) => {
3493
+ this.emit({
3494
+ type: "terminal_output",
3495
+ sessionId,
3496
+ execId,
3497
+ stream: "stdout",
3498
+ data: chunk.toString()
3499
+ });
3500
+ });
3501
+ proc.stderr?.on("data", (chunk) => {
3502
+ this.emit({
3503
+ type: "terminal_output",
3504
+ sessionId,
3505
+ execId,
3506
+ stream: "stderr",
3507
+ data: chunk.toString()
3508
+ });
3509
+ });
3510
+ proc.on("exit", (code, signal) => {
3511
+ clearTimeout(timer);
3512
+ this.processes.delete(execId);
3513
+ this.emit({
3514
+ type: "terminal_exit",
3515
+ sessionId,
3516
+ execId,
3517
+ code,
3518
+ signal
3519
+ });
3520
+ });
3521
+ const timer = setTimeout(() => {
3522
+ if (this.processes.has(execId)) {
3523
+ killProcessCrossPlatform(proc);
3524
+ }
3525
+ }, EXEC_TIMEOUT_MS);
3526
+ console.log(`[TerminalExecutor] exec ${execId}: ${command.substring(0, 100)} (cwd: ${cwd})`);
3527
+ return execId;
3528
+ }
3529
+ kill(execId) {
3530
+ const proc = this.processes.get(execId);
3531
+ if (proc) {
3532
+ killProcessCrossPlatform(proc);
3533
+ console.log(`[TerminalExecutor] kill ${execId}`);
3534
+ }
3535
+ }
3536
+ destroy() {
3537
+ for (const [execId, proc] of this.processes) {
3538
+ killProcessCrossPlatform(proc);
3539
+ console.log(`[TerminalExecutor] cleanup ${execId}`);
3540
+ }
3541
+ this.processes.clear();
3542
+ this.eventCallbacks.length = 0;
3543
+ }
3544
+ };
3545
+
3546
+ // src/server.ts
3380
3547
  var WS_PORT = 3745;
3381
3548
  var HTTP_PORT = 3746;
3382
- var execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
3549
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process6.exec);
3383
3550
  async function killPortProcess(port) {
3384
3551
  try {
3385
3552
  if (isWindows) {
@@ -3434,7 +3601,7 @@ async function start(opts = {}) {
3434
3601
  try {
3435
3602
  token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
3436
3603
  } catch {
3437
- token = (0, import_uuid4.v4)();
3604
+ token = (0, import_uuid5.v4)();
3438
3605
  await (0, import_promises4.mkdir)(configDir, { recursive: true });
3439
3606
  await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
3440
3607
  }
@@ -3442,6 +3609,20 @@ async function start(opts = {}) {
3442
3609
  }
3443
3610
  const provider = new ProcessProvider();
3444
3611
  const sessionManager = new SessionManager(provider);
3612
+ const terminalExecutor = new TerminalExecutor();
3613
+ const wsBridge = await createWithRetry(
3614
+ "WsBridge",
3615
+ WS_PORT,
3616
+ () => WsBridge.create({ port: WS_PORT, token })
3617
+ );
3618
+ const unreadSessionIds = /* @__PURE__ */ new Set();
3619
+ sessionManager.onEvent((event) => {
3620
+ if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3621
+ if (!wsBridge.isViewingSession(event.sessionId)) {
3622
+ unreadSessionIds.add(event.sessionId);
3623
+ }
3624
+ }
3625
+ });
3445
3626
  const expoChannel = new ExpoNotificationChannel();
3446
3627
  const notificationService = new NotificationService(sessionManager, expoChannel);
3447
3628
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
@@ -3456,11 +3637,6 @@ async function start(opts = {}) {
3456
3637
  console.log(`[Server] ${t("server.activityPushContinue")}`);
3457
3638
  }
3458
3639
  }
3459
- const wsBridge = await createWithRetry(
3460
- "WsBridge",
3461
- WS_PORT,
3462
- () => WsBridge.create({ port: WS_PORT, token })
3463
- );
3464
3640
  const sessionFileWatcher = new SessionFileWatcher((event) => {
3465
3641
  wsBridge.broadcast(event);
3466
3642
  });
@@ -3489,7 +3665,6 @@ async function start(opts = {}) {
3489
3665
  });
3490
3666
  }
3491
3667
  });
3492
- const unreadSessionIds = /* @__PURE__ */ new Set();
3493
3668
  notificationService.setGlobalPendingCountProvider(
3494
3669
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3495
3670
  );
@@ -3520,6 +3695,8 @@ async function start(opts = {}) {
3520
3695
  switch (event.type) {
3521
3696
  case "create_session": {
3522
3697
  await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
3698
+ const resumeId = event.resumeSessionId ?? event.newSessionId;
3699
+ if (resumeId) sessionFileWatcher.unwatch(resumeId);
3523
3700
  await sessionManager.createSession(
3524
3701
  event.projectPath,
3525
3702
  event.message,
@@ -3537,6 +3714,7 @@ async function start(opts = {}) {
3537
3714
  break;
3538
3715
  }
3539
3716
  case "send_message": {
3717
+ sessionFileWatcher.unwatch(event.sessionId);
3540
3718
  await sessionManager.sendMessage(event.sessionId, event.message, event.permissionMode, event.images);
3541
3719
  wsBridge.broadcast({
3542
3720
  type: "session_list",
@@ -3625,6 +3803,10 @@ async function start(opts = {}) {
3625
3803
  code: "PROJECT_LIST_ERROR"
3626
3804
  });
3627
3805
  }
3806
+ wsBridge.send(ws, {
3807
+ type: "session_list",
3808
+ sessions: sessionManager.getActiveSessions()
3809
+ });
3628
3810
  break;
3629
3811
  }
3630
3812
  case "list_sessions": {
@@ -3719,6 +3901,20 @@ async function start(opts = {}) {
3719
3901
  notificationService.setSoundPreferences(event.preferences);
3720
3902
  break;
3721
3903
  }
3904
+ case "terminal_exec": {
3905
+ const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3906
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
3907
+ if (!cwd) {
3908
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
3909
+ break;
3910
+ }
3911
+ terminalExecutor.exec(event.sessionId, event.command, cwd);
3912
+ break;
3913
+ }
3914
+ case "terminal_kill": {
3915
+ terminalExecutor.kill(event.execId);
3916
+ break;
3917
+ }
3722
3918
  case "register_activity_push_token": {
3723
3919
  notificationService.addActivityPushToken(event.sessionId, event.token);
3724
3920
  break;
@@ -3790,12 +3986,14 @@ async function start(opts = {}) {
3790
3986
  sessionManager.onEvent((event) => {
3791
3987
  wsBridge.broadcast(event);
3792
3988
  if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3793
- if (!wsBridge.isViewingSession(event.sessionId)) {
3794
- unreadSessionIds.add(event.sessionId);
3989
+ if (unreadSessionIds.has(event.sessionId)) {
3795
3990
  broadcastUnreadSessions();
3796
3991
  }
3797
3992
  }
3798
3993
  });
3994
+ terminalExecutor.onEvent((event) => {
3995
+ wsBridge.broadcast(event);
3996
+ });
3799
3997
  wsBridge.onDisconnect(() => {
3800
3998
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
3801
3999
  approvalProxy.approveAll(t("server.phoneDisconnected"));
@@ -3895,6 +4093,7 @@ async function start(opts = {}) {
3895
4093
  await attempt(() => wsBridge.close(), "WebSocket");
3896
4094
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3897
4095
  await attempt(() => sessionManager.destroy(), "SessionManager");
4096
+ await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
3898
4097
  await attempt(() => notificationService.destroy(), "NotificationService");
3899
4098
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3900
4099
  if (errors.length > 0) {
@@ -3903,7 +4102,7 @@ async function start(opts = {}) {
3903
4102
  }
3904
4103
  console.log(`[Server] ${t("server.shutdownComplete")}`);
3905
4104
  };
3906
- return {
4105
+ const instance = {
3907
4106
  token,
3908
4107
  wsPort: WS_PORT,
3909
4108
  httpPort: HTTP_PORT,
@@ -3921,8 +4120,21 @@ async function start(opts = {}) {
3921
4120
  }
3922
4121
  },
3923
4122
  openPairing: (duration) => pairingManager.open(duration),
3924
- closePairing: () => pairingManager.close()
4123
+ closePairing: () => pairingManager.close(),
4124
+ regenerateToken: async () => {
4125
+ const newToken = (0, import_uuid5.v4)();
4126
+ await (0, import_promises4.mkdir)(configDir, { recursive: true });
4127
+ await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
4128
+ instance.token = newToken;
4129
+ wsBridge.updateToken(newToken);
4130
+ approvalProxy.updateToken(newToken);
4131
+ pairingManager.updateToken(newToken);
4132
+ pairingManager.open();
4133
+ console.log(`[Server] ${t("server.tokenRegenerated", { token: newToken })}`);
4134
+ return newToken;
4135
+ }
3925
4136
  };
4137
+ return instance;
3926
4138
  }
3927
4139
 
3928
4140
  // src/index.ts
@@ -3994,6 +4206,7 @@ async function main() {
3994
4206
  console.log(t("startup.waitingConnection"));
3995
4207
  console.log();
3996
4208
  console.log(t("startup.pairingOpen"));
4209
+ console.log(t("startup.pressT"));
3997
4210
  console.log();
3998
4211
  fetchLatestVersion().then((latest) => {
3999
4212
  if (!latest || latest === PKG_VERSION) return;
@@ -4024,6 +4237,23 @@ async function main() {
4024
4237
  console.log(`
4025
4238
  ${t("startup.pairingReopened")}`);
4026
4239
  }
4240
+ if (key === "t" || key === "T") {
4241
+ server.regenerateToken().then((newToken) => {
4242
+ console.log();
4243
+ console.log(` ${t("startup.tokenRegenerated")}`);
4244
+ console.log(t("startup.token", { token: newToken }));
4245
+ console.log();
4246
+ const newQrUrl = buildQrUrl(getLocalIp(), server.wsPort, newToken);
4247
+ console.log(t("startup.scanToPair"));
4248
+ import_qrcode_terminal.default.generate(newQrUrl, { small: true }, (qr) => {
4249
+ qr.split("\n").forEach((line) => console.log(` ${line}`));
4250
+ });
4251
+ console.log();
4252
+ }).catch((err) => {
4253
+ console.error(`
4254
+ ${t("startup.tokenRegenerateFailed")}`, err);
4255
+ });
4256
+ }
4027
4257
  if (key === "") {
4028
4258
  shutdown("SIGINT");
4029
4259
  }
package/dist/server.d.ts CHANGED
@@ -19,6 +19,8 @@ interface ServerInstance {
19
19
  openPairing: (duration?: number) => void;
20
20
  /** 运行时关闭配对窗口 */
21
21
  closePairing: () => void;
22
+ /** 重新生成 token(泄露后刷新),断开所有客户端并开启配对窗口 */
23
+ regenerateToken: () => Promise<string>;
22
24
  }
23
25
  interface ServerOptions {
24
26
  /** 覆盖 token(默认读取 ~/.sessix/token 或自动生成) */
package/dist/server.js CHANGED
@@ -52,7 +52,10 @@ var zh = {
52
52
  autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
53
53
  autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
54
54
  pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
55
+ pressT: " \u{1F511} \u6309 t \u91CD\u7F6E Token\uFF08\u6CC4\u9732\u540E\u5237\u65B0\uFF09",
55
56
  pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
57
+ tokenRegenerated: "\u{1F511} Token \u5DF2\u91CD\u7F6E\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u914D\u5BF9",
58
+ tokenRegenerateFailed: "Token \u91CD\u7F6E\u5931\u8D25:",
56
59
  updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
57
60
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
58
61
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
@@ -81,7 +84,8 @@ var zh = {
81
84
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
82
85
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
83
86
  activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
84
- noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
87
+ noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B",
88
+ tokenRegenerated: "Token \u5DF2\u91CD\u7F6E: {{token}}"
85
89
  },
86
90
  ws: {
87
91
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -158,7 +162,10 @@ var en = {
158
162
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
159
163
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
160
164
  pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
165
+ pressT: " \u{1F511} Press t to regenerate token (refresh after leak)",
161
166
  pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
167
+ tokenRegenerated: "\u{1F511} Token regenerated, all clients disconnected. Scan QR to re-pair",
168
+ tokenRegenerateFailed: "Token regeneration failed:",
162
169
  updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
163
170
  receivedSignal: "Received {{signal}}, graceful shutdown...",
164
171
  goodbye: "All services closed, goodbye!",
@@ -187,7 +194,8 @@ var en = {
187
194
  activityPushEnabled: "ActivityKit Push enabled",
188
195
  activityPushFailed: "ActivityKit Push init failed:",
189
196
  activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
190
- noActiveLoginProcess: "No active login process"
197
+ noActiveLoginProcess: "No active login process",
198
+ tokenRegenerated: "Token regenerated: {{token}}"
191
199
  },
192
200
  ws: {
193
201
  started: "WebSocket server started on port {{port}}",
@@ -293,11 +301,11 @@ function t(key, params) {
293
301
  }
294
302
 
295
303
  // src/server.ts
296
- var import_uuid4 = require("uuid");
304
+ var import_uuid5 = require("uuid");
297
305
  var import_promises4 = require("fs/promises");
298
306
  var import_node_os6 = require("os");
299
307
  var import_node_path5 = require("path");
300
- var import_node_child_process4 = require("child_process");
308
+ var import_node_child_process6 = require("child_process");
301
309
  var import_node_util = require("util");
302
310
 
303
311
  // src/providers/ProcessProvider.ts
@@ -1544,6 +1552,13 @@ var WsBridge = class _WsBridge {
1544
1552
  getConnectionCount() {
1545
1553
  return this.wss.clients.size;
1546
1554
  }
1555
+ /** 更新 token 并断开所有现有连接(token 刷新后需重新配对) */
1556
+ updateToken(newToken) {
1557
+ this.token = newToken;
1558
+ for (const ws of this.wss.clients) {
1559
+ ws.close(4001, "Token regenerated");
1560
+ }
1561
+ }
1547
1562
  /** 优雅关闭 WebSocket 服务 */
1548
1563
  close() {
1549
1564
  return new Promise((resolve, reject) => {
@@ -1983,6 +1998,10 @@ var ApprovalProxy = class _ApprovalProxy {
1983
1998
  });
1984
1999
  }
1985
2000
  }
2001
+ /** 更新 token(token 刷新时调用) */
2002
+ updateToken(newToken) {
2003
+ this.token = newToken;
2004
+ }
1986
2005
  /** 返回连接 token(仅本机访问) */
1987
2006
  handleToken(req, res) {
1988
2007
  const remoteAddress = req.socket.remoteAddress;
@@ -2046,79 +2065,120 @@ var ApprovalProxy = class _ApprovalProxy {
2046
2065
  };
2047
2066
 
2048
2067
  // src/mdns/MdnsService.ts
2049
- var import_bonjour_service = __toESM(require("bonjour-service"));
2068
+ var import_node_child_process3 = require("child_process");
2050
2069
  var import_node_os4 = require("os");
2051
- function getLanAddresses() {
2052
- const results = [];
2053
- for (const [name, addrs] of Object.entries((0, import_node_os4.networkInterfaces)())) {
2054
- if (name.startsWith("utun") || name === "lo") continue;
2055
- if (isWindows && (name.startsWith("vEthernet") || name.includes("Loopback"))) continue;
2056
- for (const addr of addrs ?? []) {
2057
- if (addr.family === "IPv4" && !addr.internal) {
2058
- results.push(addr.address);
2059
- }
2060
- }
2061
- }
2062
- return results;
2070
+ function buildTxtArgs(txt) {
2071
+ return Object.entries(txt).map(([k, v]) => `${k}=${v}`);
2063
2072
  }
2064
2073
  var MdnsService = class {
2065
- bonjour = null;
2066
- service = null;
2074
+ proc = null;
2075
+ bonjourInstance = null;
2076
+ bonjourService = null;
2067
2077
  wsPort;
2068
2078
  httpPort;
2069
2079
  version;
2070
2080
  pairing;
2081
+ useDnsSd;
2071
2082
  constructor(options) {
2072
2083
  this.wsPort = options.wsPort;
2073
2084
  this.httpPort = options.httpPort;
2074
2085
  this.version = options.version ?? "0.1.0";
2075
2086
  this.pairing = options.pairing ?? "closed";
2087
+ this.useDnsSd = (0, import_node_os4.platform)() === "darwin";
2088
+ }
2089
+ getTxt() {
2090
+ return {
2091
+ version: this.version,
2092
+ httpPort: String(this.httpPort),
2093
+ wsPort: String(this.wsPort),
2094
+ pairing: this.pairing
2095
+ };
2076
2096
  }
2077
2097
  /**
2078
2098
  * 启动 mDNS 广播
2079
2099
  */
2080
2100
  start() {
2081
- if (this.bonjour) {
2101
+ if (this.useDnsSd) {
2102
+ this.startDnsSd();
2103
+ } else {
2104
+ this.startBonjour();
2105
+ }
2106
+ }
2107
+ startDnsSd() {
2108
+ if (this.proc) {
2082
2109
  console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
2083
2110
  return;
2084
2111
  }
2085
- const lanAddrs = getLanAddresses();
2086
- const onMdnsError = (err) => {
2087
- console.warn(`[MdnsService] mDNS error (non-fatal): ${err.message}`);
2088
- this.stop();
2089
- };
2090
- const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
2091
- this.bonjour = new import_bonjour_service.default(opts, onMdnsError);
2092
- this.bonjour.server?.mdns?.on("error", onMdnsError);
2093
- if (lanAddrs.length > 0) {
2094
- console.log(`[MdnsService] ${t("mdns.boundInterface", { ip: lanAddrs[0] })}`);
2095
- }
2096
- this.service = this.bonjour.publish({
2097
- name: "Sessix",
2098
- type: "sessix",
2099
- port: this.wsPort,
2100
- txt: {
2101
- version: this.version,
2102
- httpPort: String(this.httpPort),
2103
- wsPort: String(this.wsPort),
2104
- pairing: this.pairing
2112
+ const args = [
2113
+ "-R",
2114
+ "Sessix",
2115
+ "_sessix._tcp",
2116
+ "local",
2117
+ String(this.wsPort),
2118
+ ...buildTxtArgs(this.getTxt())
2119
+ ];
2120
+ this.proc = (0, import_node_child_process3.spawn)("dns-sd", args, { stdio: "ignore" });
2121
+ this.proc.on("error", (err) => {
2122
+ console.warn(`[MdnsService] dns-sd failed, falling back to bonjour-service: ${err.message}`);
2123
+ this.proc = null;
2124
+ this.useDnsSd = false;
2125
+ this.startBonjour();
2126
+ });
2127
+ this.proc.on("exit", (code) => {
2128
+ if (code !== null && code !== 0) {
2129
+ console.warn(`[MdnsService] dns-sd exited with code ${code}`);
2105
2130
  }
2131
+ this.proc = null;
2106
2132
  });
2107
- console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
2133
+ console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })} (dns-sd)`);
2134
+ }
2135
+ async startBonjour() {
2136
+ if (this.bonjourInstance) {
2137
+ console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
2138
+ return;
2139
+ }
2140
+ try {
2141
+ const { default: Bonjour } = await import("bonjour-service");
2142
+ const { networkInterfaces } = await import("os");
2143
+ const lanAddrs = getLanAddresses(networkInterfaces);
2144
+ const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
2145
+ const onError = (err) => {
2146
+ if (err.code === "EADDRINUSE") return;
2147
+ console.warn(`[MdnsService] mDNS error (non-fatal): ${err.message}`);
2148
+ };
2149
+ this.bonjourInstance = new Bonjour(opts, onError);
2150
+ this.bonjourInstance.server?.mdns?.on("error", onError);
2151
+ if (lanAddrs.length > 0) {
2152
+ console.log(`[MdnsService] ${t("mdns.boundInterface", { ip: lanAddrs[0] })}`);
2153
+ }
2154
+ this.bonjourService = this.bonjourInstance.publish({
2155
+ name: "Sessix",
2156
+ type: "sessix",
2157
+ port: this.wsPort,
2158
+ txt: this.getTxt()
2159
+ });
2160
+ console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })} (bonjour-service)`);
2161
+ } catch (err) {
2162
+ console.warn(`[MdnsService] bonjour-service failed: ${err.message}`);
2163
+ }
2108
2164
  }
2109
2165
  /**
2110
2166
  * 停止 mDNS 广播
2111
2167
  */
2112
2168
  stop() {
2113
- if (this.service) {
2114
- this.service.stop?.(() => {
2169
+ if (this.proc) {
2170
+ this.proc.kill();
2171
+ this.proc = null;
2172
+ }
2173
+ if (this.bonjourService) {
2174
+ this.bonjourService.stop?.(() => {
2115
2175
  console.log(`[MdnsService] ${t("mdns.stopped")}`);
2116
2176
  });
2117
- this.service = null;
2177
+ this.bonjourService = null;
2118
2178
  }
2119
- if (this.bonjour) {
2120
- this.bonjour.destroy();
2121
- this.bonjour = null;
2179
+ if (this.bonjourInstance) {
2180
+ this.bonjourInstance.destroy();
2181
+ this.bonjourInstance = null;
2122
2182
  }
2123
2183
  console.log(`[MdnsService] ${t("mdns.closed")}`);
2124
2184
  }
@@ -2127,30 +2187,45 @@ var MdnsService = class {
2127
2187
  */
2128
2188
  updatePairingState(state) {
2129
2189
  this.pairing = state;
2130
- if (!this.bonjour) return;
2190
+ if (this.useDnsSd) {
2191
+ if (this.proc) {
2192
+ this.proc.kill();
2193
+ this.proc = null;
2194
+ }
2195
+ this.startDnsSd();
2196
+ return;
2197
+ }
2198
+ if (!this.bonjourInstance) return;
2131
2199
  const republish = () => {
2132
- if (!this.bonjour) return;
2133
- this.service = this.bonjour.publish({
2200
+ if (!this.bonjourInstance) return;
2201
+ this.bonjourService = this.bonjourInstance.publish({
2134
2202
  name: "Sessix",
2135
2203
  type: "sessix",
2136
2204
  port: this.wsPort,
2137
- txt: {
2138
- version: this.version,
2139
- httpPort: String(this.httpPort),
2140
- wsPort: String(this.wsPort),
2141
- pairing: state
2142
- }
2205
+ txt: this.getTxt()
2143
2206
  });
2144
2207
  };
2145
- if (this.service) {
2146
- const old = this.service;
2147
- this.service = null;
2208
+ if (this.bonjourService) {
2209
+ const old = this.bonjourService;
2210
+ this.bonjourService = null;
2148
2211
  old.stop?.(() => republish());
2149
2212
  } else {
2150
2213
  republish();
2151
2214
  }
2152
2215
  }
2153
2216
  };
2217
+ function getLanAddresses(networkInterfacesFn) {
2218
+ const results = [];
2219
+ for (const [name, addrs] of Object.entries(networkInterfacesFn())) {
2220
+ if (name.startsWith("utun") || name === "lo") continue;
2221
+ for (const addr of addrs ?? []) {
2222
+ if (addr.family === "IPv4" && !addr.internal) {
2223
+ results.push(addr.address);
2224
+ }
2225
+ }
2226
+ }
2227
+ return results;
2228
+ }
2154
2229
 
2155
2230
  // src/hooks/HookInstaller.ts
2156
2231
  var import_promises2 = require("fs/promises");
@@ -2624,7 +2699,7 @@ var NotificationService = class {
2624
2699
  };
2625
2700
 
2626
2701
  // src/notification/DesktopNotificationChannel.ts
2627
- var import_node_child_process3 = require("child_process");
2702
+ var import_node_child_process4 = require("child_process");
2628
2703
  var DesktopNotificationChannel = class {
2629
2704
  isAvailable() {
2630
2705
  return process.platform === "darwin";
@@ -2636,7 +2711,7 @@ var DesktopNotificationChannel = class {
2636
2711
  const sound = payload.sound ?? "Ping";
2637
2712
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
2638
2713
  return new Promise((resolve) => {
2639
- (0, import_node_child_process3.execFile)("osascript", ["-e", script], (err) => {
2714
+ (0, import_node_child_process4.execFile)("osascript", ["-e", script], (err) => {
2640
2715
  if (err) {
2641
2716
  console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
2642
2717
  }
@@ -3251,6 +3326,9 @@ var PairingManager = class {
3251
3326
  this.close();
3252
3327
  return result;
3253
3328
  }
3329
+ updateToken(newToken) {
3330
+ this.token = newToken;
3331
+ }
3254
3332
  getRemainingSeconds() {
3255
3333
  if (this._state !== "open") return 0;
3256
3334
  return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
@@ -3383,9 +3461,98 @@ var AuthManager = class extends import_events2.EventEmitter {
3383
3461
 
3384
3462
  // src/server.ts
3385
3463
  var import_promises5 = require("fs/promises");
3464
+
3465
+ // src/terminal/TerminalExecutor.ts
3466
+ var import_node_child_process5 = require("child_process");
3467
+ var import_uuid4 = require("uuid");
3468
+ var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
3469
+ var TerminalExecutor = class {
3470
+ processes = /* @__PURE__ */ new Map();
3471
+ eventCallbacks = [];
3472
+ onEvent(callback) {
3473
+ this.eventCallbacks.push(callback);
3474
+ return () => {
3475
+ const idx = this.eventCallbacks.indexOf(callback);
3476
+ if (idx !== -1) this.eventCallbacks.splice(idx, 1);
3477
+ };
3478
+ }
3479
+ emit(event) {
3480
+ for (const cb of this.eventCallbacks) {
3481
+ try {
3482
+ cb(event);
3483
+ } catch (err) {
3484
+ console.error("[TerminalExecutor] Event callback error:", err);
3485
+ }
3486
+ }
3487
+ }
3488
+ exec(sessionId, command, cwd) {
3489
+ const execId = (0, import_uuid4.v4)();
3490
+ const shell = isWindows ? "powershell" : "bash";
3491
+ const args = isWindows ? ["-Command", command] : ["-c", command];
3492
+ const proc = (0, import_node_child_process5.spawn)(shell, args, {
3493
+ cwd,
3494
+ stdio: ["ignore", "pipe", "pipe"],
3495
+ env: { ...process.env }
3496
+ });
3497
+ this.processes.set(execId, proc);
3498
+ proc.stdout?.on("data", (chunk) => {
3499
+ this.emit({
3500
+ type: "terminal_output",
3501
+ sessionId,
3502
+ execId,
3503
+ stream: "stdout",
3504
+ data: chunk.toString()
3505
+ });
3506
+ });
3507
+ proc.stderr?.on("data", (chunk) => {
3508
+ this.emit({
3509
+ type: "terminal_output",
3510
+ sessionId,
3511
+ execId,
3512
+ stream: "stderr",
3513
+ data: chunk.toString()
3514
+ });
3515
+ });
3516
+ proc.on("exit", (code, signal) => {
3517
+ clearTimeout(timer);
3518
+ this.processes.delete(execId);
3519
+ this.emit({
3520
+ type: "terminal_exit",
3521
+ sessionId,
3522
+ execId,
3523
+ code,
3524
+ signal
3525
+ });
3526
+ });
3527
+ const timer = setTimeout(() => {
3528
+ if (this.processes.has(execId)) {
3529
+ killProcessCrossPlatform(proc);
3530
+ }
3531
+ }, EXEC_TIMEOUT_MS);
3532
+ console.log(`[TerminalExecutor] exec ${execId}: ${command.substring(0, 100)} (cwd: ${cwd})`);
3533
+ return execId;
3534
+ }
3535
+ kill(execId) {
3536
+ const proc = this.processes.get(execId);
3537
+ if (proc) {
3538
+ killProcessCrossPlatform(proc);
3539
+ console.log(`[TerminalExecutor] kill ${execId}`);
3540
+ }
3541
+ }
3542
+ destroy() {
3543
+ for (const [execId, proc] of this.processes) {
3544
+ killProcessCrossPlatform(proc);
3545
+ console.log(`[TerminalExecutor] cleanup ${execId}`);
3546
+ }
3547
+ this.processes.clear();
3548
+ this.eventCallbacks.length = 0;
3549
+ }
3550
+ };
3551
+
3552
+ // src/server.ts
3386
3553
  var WS_PORT = 3745;
3387
3554
  var HTTP_PORT = 3746;
3388
- var execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
3555
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process6.exec);
3389
3556
  async function killPortProcess(port) {
3390
3557
  try {
3391
3558
  if (isWindows) {
@@ -3440,7 +3607,7 @@ async function start(opts = {}) {
3440
3607
  try {
3441
3608
  token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
3442
3609
  } catch {
3443
- token = (0, import_uuid4.v4)();
3610
+ token = (0, import_uuid5.v4)();
3444
3611
  await (0, import_promises4.mkdir)(configDir, { recursive: true });
3445
3612
  await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
3446
3613
  }
@@ -3448,6 +3615,20 @@ async function start(opts = {}) {
3448
3615
  }
3449
3616
  const provider = new ProcessProvider();
3450
3617
  const sessionManager = new SessionManager(provider);
3618
+ const terminalExecutor = new TerminalExecutor();
3619
+ const wsBridge = await createWithRetry(
3620
+ "WsBridge",
3621
+ WS_PORT,
3622
+ () => WsBridge.create({ port: WS_PORT, token })
3623
+ );
3624
+ const unreadSessionIds = /* @__PURE__ */ new Set();
3625
+ sessionManager.onEvent((event) => {
3626
+ if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3627
+ if (!wsBridge.isViewingSession(event.sessionId)) {
3628
+ unreadSessionIds.add(event.sessionId);
3629
+ }
3630
+ }
3631
+ });
3451
3632
  const expoChannel = new ExpoNotificationChannel();
3452
3633
  const notificationService = new NotificationService(sessionManager, expoChannel);
3453
3634
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
@@ -3462,11 +3643,6 @@ async function start(opts = {}) {
3462
3643
  console.log(`[Server] ${t("server.activityPushContinue")}`);
3463
3644
  }
3464
3645
  }
3465
- const wsBridge = await createWithRetry(
3466
- "WsBridge",
3467
- WS_PORT,
3468
- () => WsBridge.create({ port: WS_PORT, token })
3469
- );
3470
3646
  const sessionFileWatcher = new SessionFileWatcher((event) => {
3471
3647
  wsBridge.broadcast(event);
3472
3648
  });
@@ -3495,7 +3671,6 @@ async function start(opts = {}) {
3495
3671
  });
3496
3672
  }
3497
3673
  });
3498
- const unreadSessionIds = /* @__PURE__ */ new Set();
3499
3674
  notificationService.setGlobalPendingCountProvider(
3500
3675
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3501
3676
  );
@@ -3526,6 +3701,8 @@ async function start(opts = {}) {
3526
3701
  switch (event.type) {
3527
3702
  case "create_session": {
3528
3703
  await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
3704
+ const resumeId = event.resumeSessionId ?? event.newSessionId;
3705
+ if (resumeId) sessionFileWatcher.unwatch(resumeId);
3529
3706
  await sessionManager.createSession(
3530
3707
  event.projectPath,
3531
3708
  event.message,
@@ -3543,6 +3720,7 @@ async function start(opts = {}) {
3543
3720
  break;
3544
3721
  }
3545
3722
  case "send_message": {
3723
+ sessionFileWatcher.unwatch(event.sessionId);
3546
3724
  await sessionManager.sendMessage(event.sessionId, event.message, event.permissionMode, event.images);
3547
3725
  wsBridge.broadcast({
3548
3726
  type: "session_list",
@@ -3631,6 +3809,10 @@ async function start(opts = {}) {
3631
3809
  code: "PROJECT_LIST_ERROR"
3632
3810
  });
3633
3811
  }
3812
+ wsBridge.send(ws, {
3813
+ type: "session_list",
3814
+ sessions: sessionManager.getActiveSessions()
3815
+ });
3634
3816
  break;
3635
3817
  }
3636
3818
  case "list_sessions": {
@@ -3725,6 +3907,20 @@ async function start(opts = {}) {
3725
3907
  notificationService.setSoundPreferences(event.preferences);
3726
3908
  break;
3727
3909
  }
3910
+ case "terminal_exec": {
3911
+ const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3912
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
3913
+ if (!cwd) {
3914
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
3915
+ break;
3916
+ }
3917
+ terminalExecutor.exec(event.sessionId, event.command, cwd);
3918
+ break;
3919
+ }
3920
+ case "terminal_kill": {
3921
+ terminalExecutor.kill(event.execId);
3922
+ break;
3923
+ }
3728
3924
  case "register_activity_push_token": {
3729
3925
  notificationService.addActivityPushToken(event.sessionId, event.token);
3730
3926
  break;
@@ -3796,12 +3992,14 @@ async function start(opts = {}) {
3796
3992
  sessionManager.onEvent((event) => {
3797
3993
  wsBridge.broadcast(event);
3798
3994
  if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3799
- if (!wsBridge.isViewingSession(event.sessionId)) {
3800
- unreadSessionIds.add(event.sessionId);
3995
+ if (unreadSessionIds.has(event.sessionId)) {
3801
3996
  broadcastUnreadSessions();
3802
3997
  }
3803
3998
  }
3804
3999
  });
4000
+ terminalExecutor.onEvent((event) => {
4001
+ wsBridge.broadcast(event);
4002
+ });
3805
4003
  wsBridge.onDisconnect(() => {
3806
4004
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
3807
4005
  approvalProxy.approveAll(t("server.phoneDisconnected"));
@@ -3901,6 +4099,7 @@ async function start(opts = {}) {
3901
4099
  await attempt(() => wsBridge.close(), "WebSocket");
3902
4100
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3903
4101
  await attempt(() => sessionManager.destroy(), "SessionManager");
4102
+ await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
3904
4103
  await attempt(() => notificationService.destroy(), "NotificationService");
3905
4104
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3906
4105
  if (errors.length > 0) {
@@ -3909,7 +4108,7 @@ async function start(opts = {}) {
3909
4108
  }
3910
4109
  console.log(`[Server] ${t("server.shutdownComplete")}`);
3911
4110
  };
3912
- return {
4111
+ const instance = {
3913
4112
  token,
3914
4113
  wsPort: WS_PORT,
3915
4114
  httpPort: HTTP_PORT,
@@ -3927,8 +4126,21 @@ async function start(opts = {}) {
3927
4126
  }
3928
4127
  },
3929
4128
  openPairing: (duration) => pairingManager.open(duration),
3930
- closePairing: () => pairingManager.close()
4129
+ closePairing: () => pairingManager.close(),
4130
+ regenerateToken: async () => {
4131
+ const newToken = (0, import_uuid5.v4)();
4132
+ await (0, import_promises4.mkdir)(configDir, { recursive: true });
4133
+ await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
4134
+ instance.token = newToken;
4135
+ wsBridge.updateToken(newToken);
4136
+ approvalProxy.updateToken(newToken);
4137
+ pairingManager.updateToken(newToken);
4138
+ pairingManager.open();
4139
+ console.log(`[Server] ${t("server.tokenRegenerated", { token: newToken })}`);
4140
+ return newToken;
4141
+ }
3931
4142
  };
4143
+ return instance;
3932
4144
  }
3933
4145
  // Annotate the CommonJS export names for ESM import in node:
3934
4146
  0 && (module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },