sessix-server 0.1.3 → 0.2.1

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
@@ -43,6 +43,8 @@ var zh = {
43
43
  autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
44
44
  autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
45
45
  autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
46
+ pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
47
+ pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
46
48
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
47
49
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
48
50
  shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
@@ -69,7 +71,8 @@ var zh = {
69
71
  restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
70
72
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
71
73
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
72
- activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
74
+ activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
75
+ noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
73
76
  },
74
77
  ws: {
75
78
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -144,6 +147,8 @@ var en = {
144
147
  autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
145
148
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
146
149
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
150
+ pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
151
+ pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
147
152
  receivedSignal: "Received {{signal}}, graceful shutdown...",
148
153
  goodbye: "All services closed, goodbye!",
149
154
  shutdownError: "Shutdown error:",
@@ -170,7 +175,8 @@ var en = {
170
175
  restarting: "Restarting {{label}}...",
171
176
  activityPushEnabled: "ActivityKit Push enabled",
172
177
  activityPushFailed: "ActivityKit Push init failed:",
173
- activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
178
+ activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
179
+ noActiveLoginProcess: "No active login process"
174
180
  },
175
181
  ws: {
176
182
  started: "WebSocket server started on port {{port}}",
@@ -283,11 +289,14 @@ var import_node_child_process2 = require("child_process");
283
289
  var import_node_util = require("util");
284
290
 
285
291
  // src/providers/ProcessProvider.ts
286
- var import_child_process = require("child_process");
292
+ var import_child_process2 = require("child_process");
287
293
  var import_readline = require("readline");
288
294
  var import_events = require("events");
289
295
  var import_node_os = require("os");
290
296
  var import_uuid = require("uuid");
297
+
298
+ // src/utils/claudePath.ts
299
+ var import_child_process = require("child_process");
291
300
  function findClaudePath() {
292
301
  try {
293
302
  return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
@@ -307,6 +316,8 @@ function findClaudePath() {
307
316
  return "claude";
308
317
  }
309
318
  }
319
+
320
+ // src/providers/ProcessProvider.ts
310
321
  var CLAUDE_PATH = findClaudePath();
311
322
  var ProcessProvider = class {
312
323
  /** 活跃会话映射表:sessionId -> { session, process } */
@@ -499,7 +510,7 @@ var ProcessProvider = class {
499
510
  }
500
511
  const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
501
512
  delete env.CLAUDECODE;
502
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
513
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
503
514
  cwd: projectPath,
504
515
  env,
505
516
  stdio: ["pipe", "pipe", "pipe"]
@@ -706,7 +717,7 @@ ${context}`;
706
717
  return new Promise((resolve, reject) => {
707
718
  const env = { ...process.env };
708
719
  delete env.CLAUDECODE;
709
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
720
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
710
721
  cwd: (0, import_node_os.homedir)(),
711
722
  env,
712
723
  stdio: ["pipe", "pipe", "pipe"]
@@ -1617,6 +1628,7 @@ var ApprovalProxy = class _ApprovalProxy {
1617
1628
  alwaysAllowedTools = /* @__PURE__ */ new Set();
1618
1629
  /** 获取状态信息的回调(由外部注入) */
1619
1630
  statusInfoProvider = null;
1631
+ pairingManager = null;
1620
1632
  constructor(options) {
1621
1633
  this.token = options.token;
1622
1634
  this.port = options.port;
@@ -1651,6 +1663,10 @@ var ApprovalProxy = class _ApprovalProxy {
1651
1663
  setStatusInfoProvider(provider) {
1652
1664
  this.statusInfoProvider = provider;
1653
1665
  }
1666
+ /** 设置配对管理器 */
1667
+ setPairingManager(manager) {
1668
+ this.pairingManager = manager;
1669
+ }
1654
1670
  /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1655
1671
  setYoloMode(sessionId, enabled) {
1656
1672
  this.yoloSessions.set(sessionId, enabled);
@@ -1811,6 +1827,8 @@ var ApprovalProxy = class _ApprovalProxy {
1811
1827
  const pathname = url.pathname;
1812
1828
  if (req.method === "POST" && pathname === "/hook/approval") {
1813
1829
  this.handleApprovalHook(req, res);
1830
+ } else if (req.method === "POST" && pathname === "/pair") {
1831
+ this.handlePair(req, res);
1814
1832
  } else if (req.method === "GET" && pathname === "/health") {
1815
1833
  this.handleHealth(req, res);
1816
1834
  } else if (req.method === "GET" && pathname === "/token") {
@@ -1881,6 +1899,23 @@ var ApprovalProxy = class _ApprovalProxy {
1881
1899
  activeSessions: info.activeSessions
1882
1900
  });
1883
1901
  }
1902
+ /** 配对端点:配对窗口开放时返回 token */
1903
+ handlePair(_req, res) {
1904
+ if (!this.pairingManager) {
1905
+ this.sendJson(res, 503, { error: "pairing_unavailable" });
1906
+ return;
1907
+ }
1908
+ const result = this.pairingManager.tryPair();
1909
+ if (result) {
1910
+ console.log("[ApprovalProxy] Device paired successfully");
1911
+ this.sendJson(res, 200, result);
1912
+ } else {
1913
+ this.sendJson(res, 403, {
1914
+ error: "pairing_closed",
1915
+ message: "Pairing window is closed. Restart server or press p to reopen."
1916
+ });
1917
+ }
1918
+ }
1884
1919
  /** 返回连接 token(仅本机访问) */
1885
1920
  handleToken(req, res) {
1886
1921
  const remoteAddress = req.socket.remoteAddress;
@@ -1951,12 +1986,12 @@ var MdnsService = class {
1951
1986
  wsPort;
1952
1987
  httpPort;
1953
1988
  version;
1954
- token;
1989
+ pairing;
1955
1990
  constructor(options) {
1956
1991
  this.wsPort = options.wsPort;
1957
1992
  this.httpPort = options.httpPort;
1958
1993
  this.version = options.version ?? "0.1.0";
1959
- this.token = options.token ?? "";
1994
+ this.pairing = options.pairing ?? "closed";
1960
1995
  }
1961
1996
  /**
1962
1997
  * 启动 mDNS 广播
@@ -1974,7 +2009,8 @@ var MdnsService = class {
1974
2009
  txt: {
1975
2010
  version: this.version,
1976
2011
  httpPort: String(this.httpPort),
1977
- token: this.token
2012
+ wsPort: String(this.wsPort),
2013
+ pairing: this.pairing
1978
2014
  }
1979
2015
  });
1980
2016
  console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
@@ -1995,6 +2031,29 @@ var MdnsService = class {
1995
2031
  }
1996
2032
  console.log(`[MdnsService] ${t("mdns.closed")}`);
1997
2033
  }
2034
+ /**
2035
+ * 更新配对状态(重新发布 mDNS 服务)
2036
+ */
2037
+ updatePairingState(state) {
2038
+ this.pairing = state;
2039
+ if (!this.bonjour) return;
2040
+ if (this.service) {
2041
+ this.service.stop?.(() => {
2042
+ });
2043
+ this.service = null;
2044
+ }
2045
+ this.service = this.bonjour.publish({
2046
+ name: "Sessix",
2047
+ type: "sessix",
2048
+ port: this.wsPort,
2049
+ txt: {
2050
+ version: this.version,
2051
+ httpPort: String(this.httpPort),
2052
+ wsPort: String(this.wsPort),
2053
+ pairing: state
2054
+ }
2055
+ });
2056
+ }
1998
2057
  };
1999
2058
 
2000
2059
  // src/hooks/HookInstaller.ts
@@ -2240,8 +2299,8 @@ var NotificationService = class {
2240
2299
  if (entry) entry.enabled = enabled;
2241
2300
  }
2242
2301
  /** 注册手机 push token(连接建立时由 WsBridge 调用) */
2243
- addPushToken(token) {
2244
- this.expoChannel?.addToken(token);
2302
+ addPushToken(token, ws) {
2303
+ this.expoChannel?.addToken(token, ws);
2245
2304
  }
2246
2305
  /** 移除手机 push token(断线时或手机主动注销时调用) */
2247
2306
  removePushToken(token) {
@@ -2481,17 +2540,21 @@ var MacNotificationChannel = class {
2481
2540
  var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
2482
2541
  var ExpoNotificationChannel = class {
2483
2542
  tokens = /* @__PURE__ */ new Set();
2543
+ /** push token → WebSocket 连接映射,用于前台抑制 */
2544
+ tokenWsMap = /* @__PURE__ */ new Map();
2484
2545
  /** per-token 通知音效偏好 */
2485
2546
  soundPreferences = /* @__PURE__ */ new Map();
2486
2547
  isAvailable() {
2487
2548
  return this.tokens.size > 0;
2488
2549
  }
2489
- addToken(token) {
2550
+ addToken(token, ws) {
2490
2551
  this.tokens.add(token);
2552
+ if (ws) this.tokenWsMap.set(token, ws);
2491
2553
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
2492
2554
  }
2493
2555
  removeToken(token) {
2494
2556
  this.tokens.delete(token);
2557
+ this.tokenWsMap.delete(token);
2495
2558
  this.soundPreferences.delete(token);
2496
2559
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
2497
2560
  }
@@ -2504,7 +2567,12 @@ var ExpoNotificationChannel = class {
2504
2567
  }
2505
2568
  async send(payload) {
2506
2569
  if (this.tokens.size === 0) return;
2507
- const messages = Array.from(this.tokens).map((to) => {
2570
+ const offlineTokens = Array.from(this.tokens).filter((token) => {
2571
+ const ws = this.tokenWsMap.get(token);
2572
+ return !ws || ws.readyState !== ws.OPEN;
2573
+ });
2574
+ if (offlineTokens.length === 0) return;
2575
+ const messages = offlineTokens.map((to) => {
2508
2576
  let sound = payload.sound ?? "default";
2509
2577
  const prefs = this.soundPreferences.get(to);
2510
2578
  if (prefs) {
@@ -2524,7 +2592,7 @@ var ExpoNotificationChannel = class {
2524
2592
  };
2525
2593
  });
2526
2594
  try {
2527
- console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
2595
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
2528
2596
  const res = await fetch(EXPO_PUSH_API, {
2529
2597
  method: "POST",
2530
2598
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -3027,6 +3095,180 @@ async function countJsonlFilesWithMtime(dirPath) {
3027
3095
  }
3028
3096
  }
3029
3097
 
3098
+ // src/pairing/PairingManager.ts
3099
+ var PairingManager = class {
3100
+ _state = "closed";
3101
+ timer = null;
3102
+ deadline = 0;
3103
+ token;
3104
+ serverName;
3105
+ version;
3106
+ defaultDuration;
3107
+ onStateChange;
3108
+ constructor(opts) {
3109
+ this.token = opts.token;
3110
+ this.serverName = opts.serverName;
3111
+ this.version = opts.version;
3112
+ this.defaultDuration = opts.defaultDuration ?? 3e5;
3113
+ this.onStateChange = opts.onStateChange;
3114
+ }
3115
+ get state() {
3116
+ return this._state;
3117
+ }
3118
+ open(duration) {
3119
+ const ms = duration ?? this.defaultDuration;
3120
+ if (this.timer) clearTimeout(this.timer);
3121
+ this._state = "open";
3122
+ this.deadline = Date.now() + ms;
3123
+ this.timer = setTimeout(() => this.close(), ms);
3124
+ this.onStateChange("open");
3125
+ }
3126
+ close() {
3127
+ if (this.timer) {
3128
+ clearTimeout(this.timer);
3129
+ this.timer = null;
3130
+ }
3131
+ if (this._state === "closed") return;
3132
+ this._state = "closed";
3133
+ this.deadline = 0;
3134
+ this.onStateChange("closed");
3135
+ }
3136
+ tryPair() {
3137
+ if (this._state !== "open") return null;
3138
+ const result = { token: this.token, serverName: this.serverName, version: this.version };
3139
+ this.close();
3140
+ return result;
3141
+ }
3142
+ getRemainingSeconds() {
3143
+ if (this._state !== "open") return 0;
3144
+ return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
3145
+ }
3146
+ destroy() {
3147
+ if (this.timer) {
3148
+ clearTimeout(this.timer);
3149
+ this.timer = null;
3150
+ }
3151
+ }
3152
+ };
3153
+
3154
+ // src/auth/AuthManager.ts
3155
+ var import_child_process3 = require("child_process");
3156
+ var import_child_process4 = require("child_process");
3157
+ var import_util = require("util");
3158
+ var import_events2 = require("events");
3159
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3160
+ var CLAUDE_PATH2 = findClaudePath();
3161
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3162
+ var AuthManager = class extends import_events2.EventEmitter {
3163
+ loginProcess = null;
3164
+ loginTimeout = null;
3165
+ urlSent = false;
3166
+ /** 检查当前 Claude CLI 认证状态(异步,不阻塞事件循环) */
3167
+ async checkAuth() {
3168
+ try {
3169
+ const { stdout } = await execFileAsync(CLAUDE_PATH2, ["auth", "status"], {
3170
+ timeout: 1e4
3171
+ });
3172
+ const parsed = JSON.parse(stdout.trim());
3173
+ return {
3174
+ loggedIn: !!parsed.loggedIn,
3175
+ email: parsed.email,
3176
+ authMethod: parsed.authMethod
3177
+ };
3178
+ } catch {
3179
+ return { loggedIn: false };
3180
+ }
3181
+ }
3182
+ /** 启动登录流程,捕获 URL 并通过事件推送 */
3183
+ async startLogin() {
3184
+ if (this.loginProcess) {
3185
+ this.loginProcess.kill();
3186
+ this.loginProcess = null;
3187
+ }
3188
+ this.clearLoginTimeout();
3189
+ this.urlSent = false;
3190
+ const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3191
+ env: { ...process.env, BROWSER: "echo" },
3192
+ stdio: ["pipe", "pipe", "pipe"]
3193
+ });
3194
+ this.loginProcess = proc;
3195
+ const handleOutput = (data) => {
3196
+ const text = data.toString();
3197
+ console.log(`[AuthManager] login output: ${text.trim()}`);
3198
+ if (!this.urlSent) {
3199
+ const url = this.extractUrl(text);
3200
+ if (url) {
3201
+ this.urlSent = true;
3202
+ console.log(`[AuthManager] \u6355\u83B7\u5230\u767B\u5F55 URL: ${url}`);
3203
+ this.emit("login_url", url);
3204
+ }
3205
+ }
3206
+ };
3207
+ proc.stdout?.on("data", handleOutput);
3208
+ proc.stderr?.on("data", handleOutput);
3209
+ proc.on("exit", (code) => {
3210
+ console.log(`[AuthManager] login process exited with code ${code}`);
3211
+ this.loginProcess = null;
3212
+ this.clearLoginTimeout();
3213
+ this.checkAuth().then((status) => {
3214
+ if (status.loggedIn) {
3215
+ this.emit("login_result", { success: true });
3216
+ } else if (code !== 0) {
3217
+ this.emit("login_result", { success: false, error: `Exit code: ${code}` });
3218
+ }
3219
+ });
3220
+ });
3221
+ proc.on("error", (err) => {
3222
+ console.error(`[AuthManager] login process error:`, err.message);
3223
+ this.loginProcess = null;
3224
+ this.clearLoginTimeout();
3225
+ this.emit("login_result", { success: false, error: err.message });
3226
+ });
3227
+ this.loginTimeout = setTimeout(() => {
3228
+ if (this.loginProcess) {
3229
+ console.warn("[AuthManager] login process timed out, killing");
3230
+ this.loginProcess.kill();
3231
+ this.loginProcess = null;
3232
+ this.emit("login_result", { success: false, error: "Login timed out" });
3233
+ }
3234
+ }, LOGIN_TIMEOUT_MS);
3235
+ }
3236
+ /** 提交授权码到登录进程的 stdin */
3237
+ submitCode(code) {
3238
+ if (!this.loginProcess?.stdin?.writable) {
3239
+ console.warn("[AuthManager] No active login process");
3240
+ return false;
3241
+ }
3242
+ console.log(`[AuthManager] \u63D0\u4EA4\u6388\u6743\u7801`);
3243
+ this.loginProcess.stdin.write(code + "\n");
3244
+ return true;
3245
+ }
3246
+ /** 是否有登录进程在运行 */
3247
+ get isLoginInProgress() {
3248
+ return this.loginProcess !== null;
3249
+ }
3250
+ /** 清理资源 */
3251
+ destroy() {
3252
+ this.clearLoginTimeout();
3253
+ if (this.loginProcess) {
3254
+ this.loginProcess.kill();
3255
+ this.loginProcess = null;
3256
+ }
3257
+ this.removeAllListeners();
3258
+ }
3259
+ clearLoginTimeout() {
3260
+ if (this.loginTimeout) {
3261
+ clearTimeout(this.loginTimeout);
3262
+ this.loginTimeout = null;
3263
+ }
3264
+ }
3265
+ /** 从文本中提取 URL */
3266
+ extractUrl(text) {
3267
+ const match = text.match(/https?:\/\/[^\s"'<>]+/);
3268
+ return match ? match[0] : null;
3269
+ }
3270
+ };
3271
+
3030
3272
  // src/server.ts
3031
3273
  var import_promises5 = require("fs/promises");
3032
3274
  var WS_PORT = 3745;
@@ -3105,9 +3347,32 @@ async function start(opts = {}) {
3105
3347
  HTTP_PORT,
3106
3348
  () => ApprovalProxy.create({ port: HTTP_PORT, token })
3107
3349
  );
3350
+ let mdnsService = null;
3351
+ const pairingManager = new PairingManager({
3352
+ token,
3353
+ serverName: (0, import_node_os4.hostname)(),
3354
+ version: "0.2.0",
3355
+ onStateChange: (state) => mdnsService?.updatePairingState(state)
3356
+ });
3357
+ approvalProxy.setPairingManager(pairingManager);
3358
+ if (opts.enablePairing !== false) {
3359
+ pairingManager.open();
3360
+ }
3361
+ const authManager = new AuthManager();
3362
+ authManager.on("login_url", (url) => {
3363
+ wsBridge.broadcast({ type: "auth_login_url", url });
3364
+ });
3365
+ authManager.on("login_result", (result) => {
3366
+ wsBridge.broadcast({ type: "auth_login_result", success: result.success, error: result.error });
3367
+ if (result.success) {
3368
+ authManager.checkAuth().then((status) => {
3369
+ wsBridge.broadcast({ type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3370
+ });
3371
+ }
3372
+ });
3108
3373
  const unreadSessionIds = /* @__PURE__ */ new Set();
3109
3374
  notificationService.setGlobalPendingCountProvider(
3110
- () => approvalProxy.getPendingCount() + unreadSessionIds.size
3375
+ () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3111
3376
  );
3112
3377
  const broadcastUnreadSessions = () => {
3113
3378
  wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
@@ -3322,7 +3587,7 @@ async function start(opts = {}) {
3322
3587
  break;
3323
3588
  }
3324
3589
  case "register_push_token": {
3325
- notificationService.addPushToken(event.token);
3590
+ notificationService.addPushToken(event.token, ws);
3326
3591
  break;
3327
3592
  }
3328
3593
  case "unregister_push_token": {
@@ -3361,6 +3626,22 @@ async function start(opts = {}) {
3361
3626
  approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
3362
3627
  break;
3363
3628
  }
3629
+ case "check_auth": {
3630
+ const status = await authManager.checkAuth();
3631
+ wsBridge.send(ws, { type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3632
+ break;
3633
+ }
3634
+ case "start_auth_login": {
3635
+ await authManager.startLogin();
3636
+ break;
3637
+ }
3638
+ case "submit_auth_code": {
3639
+ const submitted = authManager.submitCode(event.code);
3640
+ if (!submitted) {
3641
+ wsBridge.send(ws, { type: "auth_login_result", success: false, error: t("server.noActiveLoginProcess") });
3642
+ }
3643
+ break;
3644
+ }
3364
3645
  default: {
3365
3646
  wsBridge.send(ws, {
3366
3647
  type: "error",
@@ -3438,10 +3719,13 @@ async function start(opts = {}) {
3438
3719
  connections: wsBridge.getConnectionCount(),
3439
3720
  activeSessions: sessionManager.getActiveSessions().length
3440
3721
  }));
3441
- let mdnsService = null;
3442
3722
  const startMdns = () => {
3443
3723
  if (mdnsService) return;
3444
- mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
3724
+ mdnsService = new MdnsService({
3725
+ wsPort: WS_PORT,
3726
+ httpPort: HTTP_PORT,
3727
+ pairing: pairingManager.state
3728
+ });
3445
3729
  mdnsService.start();
3446
3730
  };
3447
3731
  const stopMdns = () => {
@@ -3476,7 +3760,9 @@ async function start(opts = {}) {
3476
3760
  errors.push(err);
3477
3761
  }
3478
3762
  };
3763
+ await attempt(() => authManager.destroy(), "AuthManager");
3479
3764
  await attempt(() => stopMdns(), "mDNS");
3765
+ await attempt(() => pairingManager.destroy(), "PairingManager");
3480
3766
  await attempt(() => wsBridge.close(), "WebSocket");
3481
3767
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3482
3768
  await attempt(() => sessionManager.destroy(), "SessionManager");
@@ -3504,7 +3790,9 @@ async function start(opts = {}) {
3504
3790
  } else {
3505
3791
  stopMdns();
3506
3792
  }
3507
- }
3793
+ },
3794
+ openPairing: (duration) => pairingManager.open(duration),
3795
+ closePairing: () => pairingManager.close()
3508
3796
  };
3509
3797
  }
3510
3798
 
@@ -3552,6 +3840,8 @@ async function main() {
3552
3840
  console.log();
3553
3841
  console.log(t("startup.waitingConnection"));
3554
3842
  console.log();
3843
+ console.log(t("startup.pairingOpen"));
3844
+ console.log();
3555
3845
  const shutdown = async (signal) => {
3556
3846
  console.log(`
3557
3847
  [Main] ${t("startup.receivedSignal", { signal })}`);
@@ -3564,8 +3854,24 @@ async function main() {
3564
3854
  process.exit(1);
3565
3855
  }
3566
3856
  };
3567
- process.on("SIGINT", () => shutdown("SIGINT"));
3568
- process.on("SIGTERM", () => shutdown("SIGTERM"));
3857
+ if (process.stdin.isTTY) {
3858
+ process.stdin.setRawMode(true);
3859
+ process.stdin.resume();
3860
+ process.stdin.setEncoding("utf8");
3861
+ process.stdin.on("data", (key) => {
3862
+ if (key === "p" || key === "P") {
3863
+ server.openPairing();
3864
+ console.log(`
3865
+ ${t("startup.pairingReopened")}`);
3866
+ }
3867
+ if (key === "") {
3868
+ shutdown("SIGINT");
3869
+ }
3870
+ });
3871
+ } else {
3872
+ process.on("SIGINT", () => shutdown("SIGINT"));
3873
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
3874
+ }
3569
3875
  }
3570
3876
  function getLocalIp() {
3571
3877
  const interfaces = (0, import_node_os5.networkInterfaces)();
package/dist/server.d.ts CHANGED
@@ -15,6 +15,10 @@ interface ServerInstance {
15
15
  onServerEvent: (cb: (event: ServerEvent) => void) => () => void;
16
16
  /** 运行时切换 mDNS 自动发现 */
17
17
  setAutoConnect: (enabled: boolean) => void;
18
+ /** 运行时开启配对窗口 */
19
+ openPairing: (duration?: number) => void;
20
+ /** 运行时关闭配对窗口 */
21
+ closePairing: () => void;
18
22
  }
19
23
  interface ServerOptions {
20
24
  /** 覆盖 token(默认读取 ~/.sessix/token 或自动生成) */
@@ -30,6 +34,8 @@ interface ServerOptions {
30
34
  };
31
35
  /** 是否启用 mDNS 自动发现(默认 true) */
32
36
  enableAutoConnect?: boolean;
37
+ /** 是否启用配对模式(默认 true,Electron 传 false) */
38
+ enablePairing?: boolean;
33
39
  }
34
40
  declare function start(opts?: ServerOptions): Promise<ServerInstance>;
35
41
 
package/dist/server.js CHANGED
@@ -51,6 +51,8 @@ var zh = {
51
51
  autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
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
+ 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
+ pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
54
56
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
55
57
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
56
58
  shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
@@ -77,7 +79,8 @@ var zh = {
77
79
  restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
78
80
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
79
81
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
80
- activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
82
+ activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
83
+ noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
81
84
  },
82
85
  ws: {
83
86
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -152,6 +155,8 @@ var en = {
152
155
  autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
153
156
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
154
157
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
158
+ pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
159
+ pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
155
160
  receivedSignal: "Received {{signal}}, graceful shutdown...",
156
161
  goodbye: "All services closed, goodbye!",
157
162
  shutdownError: "Shutdown error:",
@@ -178,7 +183,8 @@ var en = {
178
183
  restarting: "Restarting {{label}}...",
179
184
  activityPushEnabled: "ActivityKit Push enabled",
180
185
  activityPushFailed: "ActivityKit Push init failed:",
181
- activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
186
+ activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
187
+ noActiveLoginProcess: "No active login process"
182
188
  },
183
189
  ws: {
184
190
  started: "WebSocket server started on port {{port}}",
@@ -291,11 +297,14 @@ var import_node_child_process2 = require("child_process");
291
297
  var import_node_util = require("util");
292
298
 
293
299
  // src/providers/ProcessProvider.ts
294
- var import_child_process = require("child_process");
300
+ var import_child_process2 = require("child_process");
295
301
  var import_readline = require("readline");
296
302
  var import_events = require("events");
297
303
  var import_node_os = require("os");
298
304
  var import_uuid = require("uuid");
305
+
306
+ // src/utils/claudePath.ts
307
+ var import_child_process = require("child_process");
299
308
  function findClaudePath() {
300
309
  try {
301
310
  return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
@@ -315,6 +324,8 @@ function findClaudePath() {
315
324
  return "claude";
316
325
  }
317
326
  }
327
+
328
+ // src/providers/ProcessProvider.ts
318
329
  var CLAUDE_PATH = findClaudePath();
319
330
  var ProcessProvider = class {
320
331
  /** 活跃会话映射表:sessionId -> { session, process } */
@@ -507,7 +518,7 @@ var ProcessProvider = class {
507
518
  }
508
519
  const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
509
520
  delete env.CLAUDECODE;
510
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
521
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
511
522
  cwd: projectPath,
512
523
  env,
513
524
  stdio: ["pipe", "pipe", "pipe"]
@@ -714,7 +725,7 @@ ${context}`;
714
725
  return new Promise((resolve, reject) => {
715
726
  const env = { ...process.env };
716
727
  delete env.CLAUDECODE;
717
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
728
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
718
729
  cwd: (0, import_node_os.homedir)(),
719
730
  env,
720
731
  stdio: ["pipe", "pipe", "pipe"]
@@ -1625,6 +1636,7 @@ var ApprovalProxy = class _ApprovalProxy {
1625
1636
  alwaysAllowedTools = /* @__PURE__ */ new Set();
1626
1637
  /** 获取状态信息的回调(由外部注入) */
1627
1638
  statusInfoProvider = null;
1639
+ pairingManager = null;
1628
1640
  constructor(options) {
1629
1641
  this.token = options.token;
1630
1642
  this.port = options.port;
@@ -1659,6 +1671,10 @@ var ApprovalProxy = class _ApprovalProxy {
1659
1671
  setStatusInfoProvider(provider) {
1660
1672
  this.statusInfoProvider = provider;
1661
1673
  }
1674
+ /** 设置配对管理器 */
1675
+ setPairingManager(manager) {
1676
+ this.pairingManager = manager;
1677
+ }
1662
1678
  /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1663
1679
  setYoloMode(sessionId, enabled) {
1664
1680
  this.yoloSessions.set(sessionId, enabled);
@@ -1819,6 +1835,8 @@ var ApprovalProxy = class _ApprovalProxy {
1819
1835
  const pathname = url.pathname;
1820
1836
  if (req.method === "POST" && pathname === "/hook/approval") {
1821
1837
  this.handleApprovalHook(req, res);
1838
+ } else if (req.method === "POST" && pathname === "/pair") {
1839
+ this.handlePair(req, res);
1822
1840
  } else if (req.method === "GET" && pathname === "/health") {
1823
1841
  this.handleHealth(req, res);
1824
1842
  } else if (req.method === "GET" && pathname === "/token") {
@@ -1889,6 +1907,23 @@ var ApprovalProxy = class _ApprovalProxy {
1889
1907
  activeSessions: info.activeSessions
1890
1908
  });
1891
1909
  }
1910
+ /** 配对端点:配对窗口开放时返回 token */
1911
+ handlePair(_req, res) {
1912
+ if (!this.pairingManager) {
1913
+ this.sendJson(res, 503, { error: "pairing_unavailable" });
1914
+ return;
1915
+ }
1916
+ const result = this.pairingManager.tryPair();
1917
+ if (result) {
1918
+ console.log("[ApprovalProxy] Device paired successfully");
1919
+ this.sendJson(res, 200, result);
1920
+ } else {
1921
+ this.sendJson(res, 403, {
1922
+ error: "pairing_closed",
1923
+ message: "Pairing window is closed. Restart server or press p to reopen."
1924
+ });
1925
+ }
1926
+ }
1892
1927
  /** 返回连接 token(仅本机访问) */
1893
1928
  handleToken(req, res) {
1894
1929
  const remoteAddress = req.socket.remoteAddress;
@@ -1959,12 +1994,12 @@ var MdnsService = class {
1959
1994
  wsPort;
1960
1995
  httpPort;
1961
1996
  version;
1962
- token;
1997
+ pairing;
1963
1998
  constructor(options) {
1964
1999
  this.wsPort = options.wsPort;
1965
2000
  this.httpPort = options.httpPort;
1966
2001
  this.version = options.version ?? "0.1.0";
1967
- this.token = options.token ?? "";
2002
+ this.pairing = options.pairing ?? "closed";
1968
2003
  }
1969
2004
  /**
1970
2005
  * 启动 mDNS 广播
@@ -1982,7 +2017,8 @@ var MdnsService = class {
1982
2017
  txt: {
1983
2018
  version: this.version,
1984
2019
  httpPort: String(this.httpPort),
1985
- token: this.token
2020
+ wsPort: String(this.wsPort),
2021
+ pairing: this.pairing
1986
2022
  }
1987
2023
  });
1988
2024
  console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
@@ -2003,6 +2039,29 @@ var MdnsService = class {
2003
2039
  }
2004
2040
  console.log(`[MdnsService] ${t("mdns.closed")}`);
2005
2041
  }
2042
+ /**
2043
+ * 更新配对状态(重新发布 mDNS 服务)
2044
+ */
2045
+ updatePairingState(state) {
2046
+ this.pairing = state;
2047
+ if (!this.bonjour) return;
2048
+ if (this.service) {
2049
+ this.service.stop?.(() => {
2050
+ });
2051
+ this.service = null;
2052
+ }
2053
+ this.service = this.bonjour.publish({
2054
+ name: "Sessix",
2055
+ type: "sessix",
2056
+ port: this.wsPort,
2057
+ txt: {
2058
+ version: this.version,
2059
+ httpPort: String(this.httpPort),
2060
+ wsPort: String(this.wsPort),
2061
+ pairing: state
2062
+ }
2063
+ });
2064
+ }
2006
2065
  };
2007
2066
 
2008
2067
  // src/hooks/HookInstaller.ts
@@ -2248,8 +2307,8 @@ var NotificationService = class {
2248
2307
  if (entry) entry.enabled = enabled;
2249
2308
  }
2250
2309
  /** 注册手机 push token(连接建立时由 WsBridge 调用) */
2251
- addPushToken(token) {
2252
- this.expoChannel?.addToken(token);
2310
+ addPushToken(token, ws) {
2311
+ this.expoChannel?.addToken(token, ws);
2253
2312
  }
2254
2313
  /** 移除手机 push token(断线时或手机主动注销时调用) */
2255
2314
  removePushToken(token) {
@@ -2489,17 +2548,21 @@ var MacNotificationChannel = class {
2489
2548
  var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
2490
2549
  var ExpoNotificationChannel = class {
2491
2550
  tokens = /* @__PURE__ */ new Set();
2551
+ /** push token → WebSocket 连接映射,用于前台抑制 */
2552
+ tokenWsMap = /* @__PURE__ */ new Map();
2492
2553
  /** per-token 通知音效偏好 */
2493
2554
  soundPreferences = /* @__PURE__ */ new Map();
2494
2555
  isAvailable() {
2495
2556
  return this.tokens.size > 0;
2496
2557
  }
2497
- addToken(token) {
2558
+ addToken(token, ws) {
2498
2559
  this.tokens.add(token);
2560
+ if (ws) this.tokenWsMap.set(token, ws);
2499
2561
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
2500
2562
  }
2501
2563
  removeToken(token) {
2502
2564
  this.tokens.delete(token);
2565
+ this.tokenWsMap.delete(token);
2503
2566
  this.soundPreferences.delete(token);
2504
2567
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
2505
2568
  }
@@ -2512,7 +2575,12 @@ var ExpoNotificationChannel = class {
2512
2575
  }
2513
2576
  async send(payload) {
2514
2577
  if (this.tokens.size === 0) return;
2515
- const messages = Array.from(this.tokens).map((to) => {
2578
+ const offlineTokens = Array.from(this.tokens).filter((token) => {
2579
+ const ws = this.tokenWsMap.get(token);
2580
+ return !ws || ws.readyState !== ws.OPEN;
2581
+ });
2582
+ if (offlineTokens.length === 0) return;
2583
+ const messages = offlineTokens.map((to) => {
2516
2584
  let sound = payload.sound ?? "default";
2517
2585
  const prefs = this.soundPreferences.get(to);
2518
2586
  if (prefs) {
@@ -2532,7 +2600,7 @@ var ExpoNotificationChannel = class {
2532
2600
  };
2533
2601
  });
2534
2602
  try {
2535
- console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
2603
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
2536
2604
  const res = await fetch(EXPO_PUSH_API, {
2537
2605
  method: "POST",
2538
2606
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -3035,6 +3103,180 @@ async function countJsonlFilesWithMtime(dirPath) {
3035
3103
  }
3036
3104
  }
3037
3105
 
3106
+ // src/pairing/PairingManager.ts
3107
+ var PairingManager = class {
3108
+ _state = "closed";
3109
+ timer = null;
3110
+ deadline = 0;
3111
+ token;
3112
+ serverName;
3113
+ version;
3114
+ defaultDuration;
3115
+ onStateChange;
3116
+ constructor(opts) {
3117
+ this.token = opts.token;
3118
+ this.serverName = opts.serverName;
3119
+ this.version = opts.version;
3120
+ this.defaultDuration = opts.defaultDuration ?? 3e5;
3121
+ this.onStateChange = opts.onStateChange;
3122
+ }
3123
+ get state() {
3124
+ return this._state;
3125
+ }
3126
+ open(duration) {
3127
+ const ms = duration ?? this.defaultDuration;
3128
+ if (this.timer) clearTimeout(this.timer);
3129
+ this._state = "open";
3130
+ this.deadline = Date.now() + ms;
3131
+ this.timer = setTimeout(() => this.close(), ms);
3132
+ this.onStateChange("open");
3133
+ }
3134
+ close() {
3135
+ if (this.timer) {
3136
+ clearTimeout(this.timer);
3137
+ this.timer = null;
3138
+ }
3139
+ if (this._state === "closed") return;
3140
+ this._state = "closed";
3141
+ this.deadline = 0;
3142
+ this.onStateChange("closed");
3143
+ }
3144
+ tryPair() {
3145
+ if (this._state !== "open") return null;
3146
+ const result = { token: this.token, serverName: this.serverName, version: this.version };
3147
+ this.close();
3148
+ return result;
3149
+ }
3150
+ getRemainingSeconds() {
3151
+ if (this._state !== "open") return 0;
3152
+ return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
3153
+ }
3154
+ destroy() {
3155
+ if (this.timer) {
3156
+ clearTimeout(this.timer);
3157
+ this.timer = null;
3158
+ }
3159
+ }
3160
+ };
3161
+
3162
+ // src/auth/AuthManager.ts
3163
+ var import_child_process3 = require("child_process");
3164
+ var import_child_process4 = require("child_process");
3165
+ var import_util = require("util");
3166
+ var import_events2 = require("events");
3167
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3168
+ var CLAUDE_PATH2 = findClaudePath();
3169
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3170
+ var AuthManager = class extends import_events2.EventEmitter {
3171
+ loginProcess = null;
3172
+ loginTimeout = null;
3173
+ urlSent = false;
3174
+ /** 检查当前 Claude CLI 认证状态(异步,不阻塞事件循环) */
3175
+ async checkAuth() {
3176
+ try {
3177
+ const { stdout } = await execFileAsync(CLAUDE_PATH2, ["auth", "status"], {
3178
+ timeout: 1e4
3179
+ });
3180
+ const parsed = JSON.parse(stdout.trim());
3181
+ return {
3182
+ loggedIn: !!parsed.loggedIn,
3183
+ email: parsed.email,
3184
+ authMethod: parsed.authMethod
3185
+ };
3186
+ } catch {
3187
+ return { loggedIn: false };
3188
+ }
3189
+ }
3190
+ /** 启动登录流程,捕获 URL 并通过事件推送 */
3191
+ async startLogin() {
3192
+ if (this.loginProcess) {
3193
+ this.loginProcess.kill();
3194
+ this.loginProcess = null;
3195
+ }
3196
+ this.clearLoginTimeout();
3197
+ this.urlSent = false;
3198
+ const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3199
+ env: { ...process.env, BROWSER: "echo" },
3200
+ stdio: ["pipe", "pipe", "pipe"]
3201
+ });
3202
+ this.loginProcess = proc;
3203
+ const handleOutput = (data) => {
3204
+ const text = data.toString();
3205
+ console.log(`[AuthManager] login output: ${text.trim()}`);
3206
+ if (!this.urlSent) {
3207
+ const url = this.extractUrl(text);
3208
+ if (url) {
3209
+ this.urlSent = true;
3210
+ console.log(`[AuthManager] \u6355\u83B7\u5230\u767B\u5F55 URL: ${url}`);
3211
+ this.emit("login_url", url);
3212
+ }
3213
+ }
3214
+ };
3215
+ proc.stdout?.on("data", handleOutput);
3216
+ proc.stderr?.on("data", handleOutput);
3217
+ proc.on("exit", (code) => {
3218
+ console.log(`[AuthManager] login process exited with code ${code}`);
3219
+ this.loginProcess = null;
3220
+ this.clearLoginTimeout();
3221
+ this.checkAuth().then((status) => {
3222
+ if (status.loggedIn) {
3223
+ this.emit("login_result", { success: true });
3224
+ } else if (code !== 0) {
3225
+ this.emit("login_result", { success: false, error: `Exit code: ${code}` });
3226
+ }
3227
+ });
3228
+ });
3229
+ proc.on("error", (err) => {
3230
+ console.error(`[AuthManager] login process error:`, err.message);
3231
+ this.loginProcess = null;
3232
+ this.clearLoginTimeout();
3233
+ this.emit("login_result", { success: false, error: err.message });
3234
+ });
3235
+ this.loginTimeout = setTimeout(() => {
3236
+ if (this.loginProcess) {
3237
+ console.warn("[AuthManager] login process timed out, killing");
3238
+ this.loginProcess.kill();
3239
+ this.loginProcess = null;
3240
+ this.emit("login_result", { success: false, error: "Login timed out" });
3241
+ }
3242
+ }, LOGIN_TIMEOUT_MS);
3243
+ }
3244
+ /** 提交授权码到登录进程的 stdin */
3245
+ submitCode(code) {
3246
+ if (!this.loginProcess?.stdin?.writable) {
3247
+ console.warn("[AuthManager] No active login process");
3248
+ return false;
3249
+ }
3250
+ console.log(`[AuthManager] \u63D0\u4EA4\u6388\u6743\u7801`);
3251
+ this.loginProcess.stdin.write(code + "\n");
3252
+ return true;
3253
+ }
3254
+ /** 是否有登录进程在运行 */
3255
+ get isLoginInProgress() {
3256
+ return this.loginProcess !== null;
3257
+ }
3258
+ /** 清理资源 */
3259
+ destroy() {
3260
+ this.clearLoginTimeout();
3261
+ if (this.loginProcess) {
3262
+ this.loginProcess.kill();
3263
+ this.loginProcess = null;
3264
+ }
3265
+ this.removeAllListeners();
3266
+ }
3267
+ clearLoginTimeout() {
3268
+ if (this.loginTimeout) {
3269
+ clearTimeout(this.loginTimeout);
3270
+ this.loginTimeout = null;
3271
+ }
3272
+ }
3273
+ /** 从文本中提取 URL */
3274
+ extractUrl(text) {
3275
+ const match = text.match(/https?:\/\/[^\s"'<>]+/);
3276
+ return match ? match[0] : null;
3277
+ }
3278
+ };
3279
+
3038
3280
  // src/server.ts
3039
3281
  var import_promises5 = require("fs/promises");
3040
3282
  var WS_PORT = 3745;
@@ -3113,9 +3355,32 @@ async function start(opts = {}) {
3113
3355
  HTTP_PORT,
3114
3356
  () => ApprovalProxy.create({ port: HTTP_PORT, token })
3115
3357
  );
3358
+ let mdnsService = null;
3359
+ const pairingManager = new PairingManager({
3360
+ token,
3361
+ serverName: (0, import_node_os4.hostname)(),
3362
+ version: "0.2.0",
3363
+ onStateChange: (state) => mdnsService?.updatePairingState(state)
3364
+ });
3365
+ approvalProxy.setPairingManager(pairingManager);
3366
+ if (opts.enablePairing !== false) {
3367
+ pairingManager.open();
3368
+ }
3369
+ const authManager = new AuthManager();
3370
+ authManager.on("login_url", (url) => {
3371
+ wsBridge.broadcast({ type: "auth_login_url", url });
3372
+ });
3373
+ authManager.on("login_result", (result) => {
3374
+ wsBridge.broadcast({ type: "auth_login_result", success: result.success, error: result.error });
3375
+ if (result.success) {
3376
+ authManager.checkAuth().then((status) => {
3377
+ wsBridge.broadcast({ type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3378
+ });
3379
+ }
3380
+ });
3116
3381
  const unreadSessionIds = /* @__PURE__ */ new Set();
3117
3382
  notificationService.setGlobalPendingCountProvider(
3118
- () => approvalProxy.getPendingCount() + unreadSessionIds.size
3383
+ () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3119
3384
  );
3120
3385
  const broadcastUnreadSessions = () => {
3121
3386
  wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
@@ -3330,7 +3595,7 @@ async function start(opts = {}) {
3330
3595
  break;
3331
3596
  }
3332
3597
  case "register_push_token": {
3333
- notificationService.addPushToken(event.token);
3598
+ notificationService.addPushToken(event.token, ws);
3334
3599
  break;
3335
3600
  }
3336
3601
  case "unregister_push_token": {
@@ -3369,6 +3634,22 @@ async function start(opts = {}) {
3369
3634
  approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
3370
3635
  break;
3371
3636
  }
3637
+ case "check_auth": {
3638
+ const status = await authManager.checkAuth();
3639
+ wsBridge.send(ws, { type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3640
+ break;
3641
+ }
3642
+ case "start_auth_login": {
3643
+ await authManager.startLogin();
3644
+ break;
3645
+ }
3646
+ case "submit_auth_code": {
3647
+ const submitted = authManager.submitCode(event.code);
3648
+ if (!submitted) {
3649
+ wsBridge.send(ws, { type: "auth_login_result", success: false, error: t("server.noActiveLoginProcess") });
3650
+ }
3651
+ break;
3652
+ }
3372
3653
  default: {
3373
3654
  wsBridge.send(ws, {
3374
3655
  type: "error",
@@ -3446,10 +3727,13 @@ async function start(opts = {}) {
3446
3727
  connections: wsBridge.getConnectionCount(),
3447
3728
  activeSessions: sessionManager.getActiveSessions().length
3448
3729
  }));
3449
- let mdnsService = null;
3450
3730
  const startMdns = () => {
3451
3731
  if (mdnsService) return;
3452
- mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
3732
+ mdnsService = new MdnsService({
3733
+ wsPort: WS_PORT,
3734
+ httpPort: HTTP_PORT,
3735
+ pairing: pairingManager.state
3736
+ });
3453
3737
  mdnsService.start();
3454
3738
  };
3455
3739
  const stopMdns = () => {
@@ -3484,7 +3768,9 @@ async function start(opts = {}) {
3484
3768
  errors.push(err);
3485
3769
  }
3486
3770
  };
3771
+ await attempt(() => authManager.destroy(), "AuthManager");
3487
3772
  await attempt(() => stopMdns(), "mDNS");
3773
+ await attempt(() => pairingManager.destroy(), "PairingManager");
3488
3774
  await attempt(() => wsBridge.close(), "WebSocket");
3489
3775
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3490
3776
  await attempt(() => sessionManager.destroy(), "SessionManager");
@@ -3512,7 +3798,9 @@ async function start(opts = {}) {
3512
3798
  } else {
3513
3799
  stopMdns();
3514
3800
  }
3515
- }
3801
+ },
3802
+ openPairing: (duration) => pairingManager.open(duration),
3803
+ closePairing: () => pairingManager.close()
3516
3804
  };
3517
3805
  }
3518
3806
  // Annotate the CommonJS export names for ESM import in node:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },
@@ -26,7 +26,6 @@
26
26
  "start": "node dist/index.js"
27
27
  },
28
28
  "dependencies": {
29
- "@sessix/shared": "*",
30
29
  "ws": "^8.18.0",
31
30
  "bonjour-service": "^1.3.0",
32
31
  "chokidar": "^4.0.0",
@@ -34,6 +33,7 @@
34
33
  "qrcode-terminal": "^0.12.0"
35
34
  },
36
35
  "devDependencies": {
36
+ "@sessix/shared": "*",
37
37
  "@types/ws": "^8.5.0",
38
38
  "@types/uuid": "^10.0.0",
39
39
  "@types/qrcode-terminal": "^0.12.2",