sessix-server 0.1.4 → 0.2.2

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
@@ -25,6 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/index.ts
27
27
  var import_node_os5 = require("os");
28
+ var import_node_fs2 = require("fs");
29
+ var import_node_path5 = require("path");
28
30
 
29
31
  // src/i18n/locales/zh.ts
30
32
  var zh = {
@@ -43,6 +45,9 @@ var zh = {
43
45
  autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
44
46
  autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
45
47
  autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
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
+ pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
50
+ updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
46
51
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
47
52
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
48
53
  shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
@@ -69,7 +74,8 @@ var zh = {
69
74
  restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
70
75
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
71
76
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
72
- activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
77
+ 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"
73
79
  },
74
80
  ws: {
75
81
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -144,6 +150,9 @@ var en = {
144
150
  autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
145
151
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
146
152
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
153
+ pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
154
+ pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
155
+ updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
147
156
  receivedSignal: "Received {{signal}}, graceful shutdown...",
148
157
  goodbye: "All services closed, goodbye!",
149
158
  shutdownError: "Shutdown error:",
@@ -170,7 +179,8 @@ var en = {
170
179
  restarting: "Restarting {{label}}...",
171
180
  activityPushEnabled: "ActivityKit Push enabled",
172
181
  activityPushFailed: "ActivityKit Push init failed:",
173
- activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
182
+ activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
183
+ noActiveLoginProcess: "No active login process"
174
184
  },
175
185
  ws: {
176
186
  started: "WebSocket server started on port {{port}}",
@@ -283,11 +293,14 @@ var import_node_child_process2 = require("child_process");
283
293
  var import_node_util = require("util");
284
294
 
285
295
  // src/providers/ProcessProvider.ts
286
- var import_child_process = require("child_process");
296
+ var import_child_process2 = require("child_process");
287
297
  var import_readline = require("readline");
288
298
  var import_events = require("events");
289
299
  var import_node_os = require("os");
290
300
  var import_uuid = require("uuid");
301
+
302
+ // src/utils/claudePath.ts
303
+ var import_child_process = require("child_process");
291
304
  function findClaudePath() {
292
305
  try {
293
306
  return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
@@ -307,6 +320,8 @@ function findClaudePath() {
307
320
  return "claude";
308
321
  }
309
322
  }
323
+
324
+ // src/providers/ProcessProvider.ts
310
325
  var CLAUDE_PATH = findClaudePath();
311
326
  var ProcessProvider = class {
312
327
  /** 活跃会话映射表:sessionId -> { session, process } */
@@ -499,7 +514,7 @@ var ProcessProvider = class {
499
514
  }
500
515
  const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
501
516
  delete env.CLAUDECODE;
502
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
517
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
503
518
  cwd: projectPath,
504
519
  env,
505
520
  stdio: ["pipe", "pipe", "pipe"]
@@ -706,7 +721,7 @@ ${context}`;
706
721
  return new Promise((resolve, reject) => {
707
722
  const env = { ...process.env };
708
723
  delete env.CLAUDECODE;
709
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
724
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
710
725
  cwd: (0, import_node_os.homedir)(),
711
726
  env,
712
727
  stdio: ["pipe", "pipe", "pipe"]
@@ -1617,6 +1632,7 @@ var ApprovalProxy = class _ApprovalProxy {
1617
1632
  alwaysAllowedTools = /* @__PURE__ */ new Set();
1618
1633
  /** 获取状态信息的回调(由外部注入) */
1619
1634
  statusInfoProvider = null;
1635
+ pairingManager = null;
1620
1636
  constructor(options) {
1621
1637
  this.token = options.token;
1622
1638
  this.port = options.port;
@@ -1651,6 +1667,10 @@ var ApprovalProxy = class _ApprovalProxy {
1651
1667
  setStatusInfoProvider(provider) {
1652
1668
  this.statusInfoProvider = provider;
1653
1669
  }
1670
+ /** 设置配对管理器 */
1671
+ setPairingManager(manager) {
1672
+ this.pairingManager = manager;
1673
+ }
1654
1674
  /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1655
1675
  setYoloMode(sessionId, enabled) {
1656
1676
  this.yoloSessions.set(sessionId, enabled);
@@ -1811,6 +1831,8 @@ var ApprovalProxy = class _ApprovalProxy {
1811
1831
  const pathname = url.pathname;
1812
1832
  if (req.method === "POST" && pathname === "/hook/approval") {
1813
1833
  this.handleApprovalHook(req, res);
1834
+ } else if (req.method === "POST" && pathname === "/pair") {
1835
+ this.handlePair(req, res);
1814
1836
  } else if (req.method === "GET" && pathname === "/health") {
1815
1837
  this.handleHealth(req, res);
1816
1838
  } else if (req.method === "GET" && pathname === "/token") {
@@ -1881,6 +1903,23 @@ var ApprovalProxy = class _ApprovalProxy {
1881
1903
  activeSessions: info.activeSessions
1882
1904
  });
1883
1905
  }
1906
+ /** 配对端点:配对窗口开放时返回 token */
1907
+ handlePair(_req, res) {
1908
+ if (!this.pairingManager) {
1909
+ this.sendJson(res, 503, { error: "pairing_unavailable" });
1910
+ return;
1911
+ }
1912
+ const result = this.pairingManager.tryPair();
1913
+ if (result) {
1914
+ console.log("[ApprovalProxy] Device paired successfully");
1915
+ this.sendJson(res, 200, result);
1916
+ } else {
1917
+ this.sendJson(res, 403, {
1918
+ error: "pairing_closed",
1919
+ message: "Pairing window is closed. Restart server or press p to reopen."
1920
+ });
1921
+ }
1922
+ }
1884
1923
  /** 返回连接 token(仅本机访问) */
1885
1924
  handleToken(req, res) {
1886
1925
  const remoteAddress = req.socket.remoteAddress;
@@ -1951,12 +1990,12 @@ var MdnsService = class {
1951
1990
  wsPort;
1952
1991
  httpPort;
1953
1992
  version;
1954
- token;
1993
+ pairing;
1955
1994
  constructor(options) {
1956
1995
  this.wsPort = options.wsPort;
1957
1996
  this.httpPort = options.httpPort;
1958
1997
  this.version = options.version ?? "0.1.0";
1959
- this.token = options.token ?? "";
1998
+ this.pairing = options.pairing ?? "closed";
1960
1999
  }
1961
2000
  /**
1962
2001
  * 启动 mDNS 广播
@@ -1974,7 +2013,8 @@ var MdnsService = class {
1974
2013
  txt: {
1975
2014
  version: this.version,
1976
2015
  httpPort: String(this.httpPort),
1977
- token: this.token
2016
+ wsPort: String(this.wsPort),
2017
+ pairing: this.pairing
1978
2018
  }
1979
2019
  });
1980
2020
  console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
@@ -1995,6 +2035,34 @@ var MdnsService = class {
1995
2035
  }
1996
2036
  console.log(`[MdnsService] ${t("mdns.closed")}`);
1997
2037
  }
2038
+ /**
2039
+ * 更新配对状态(重新发布 mDNS 服务)
2040
+ */
2041
+ updatePairingState(state) {
2042
+ this.pairing = state;
2043
+ if (!this.bonjour) return;
2044
+ const republish = () => {
2045
+ if (!this.bonjour) return;
2046
+ this.service = this.bonjour.publish({
2047
+ name: "Sessix",
2048
+ type: "sessix",
2049
+ port: this.wsPort,
2050
+ txt: {
2051
+ version: this.version,
2052
+ httpPort: String(this.httpPort),
2053
+ wsPort: String(this.wsPort),
2054
+ pairing: state
2055
+ }
2056
+ });
2057
+ };
2058
+ if (this.service) {
2059
+ const old = this.service;
2060
+ this.service = null;
2061
+ old.stop?.(() => republish());
2062
+ } else {
2063
+ republish();
2064
+ }
2065
+ }
1998
2066
  };
1999
2067
 
2000
2068
  // src/hooks/HookInstaller.ts
@@ -2240,8 +2308,8 @@ var NotificationService = class {
2240
2308
  if (entry) entry.enabled = enabled;
2241
2309
  }
2242
2310
  /** 注册手机 push token(连接建立时由 WsBridge 调用) */
2243
- addPushToken(token) {
2244
- this.expoChannel?.addToken(token);
2311
+ addPushToken(token, ws) {
2312
+ this.expoChannel?.addToken(token, ws);
2245
2313
  }
2246
2314
  /** 移除手机 push token(断线时或手机主动注销时调用) */
2247
2315
  removePushToken(token) {
@@ -2481,17 +2549,21 @@ var MacNotificationChannel = class {
2481
2549
  var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
2482
2550
  var ExpoNotificationChannel = class {
2483
2551
  tokens = /* @__PURE__ */ new Set();
2552
+ /** push token → WebSocket 连接映射,用于前台抑制 */
2553
+ tokenWsMap = /* @__PURE__ */ new Map();
2484
2554
  /** per-token 通知音效偏好 */
2485
2555
  soundPreferences = /* @__PURE__ */ new Map();
2486
2556
  isAvailable() {
2487
2557
  return this.tokens.size > 0;
2488
2558
  }
2489
- addToken(token) {
2559
+ addToken(token, ws) {
2490
2560
  this.tokens.add(token);
2561
+ if (ws) this.tokenWsMap.set(token, ws);
2491
2562
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
2492
2563
  }
2493
2564
  removeToken(token) {
2494
2565
  this.tokens.delete(token);
2566
+ this.tokenWsMap.delete(token);
2495
2567
  this.soundPreferences.delete(token);
2496
2568
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
2497
2569
  }
@@ -2504,7 +2576,12 @@ var ExpoNotificationChannel = class {
2504
2576
  }
2505
2577
  async send(payload) {
2506
2578
  if (this.tokens.size === 0) return;
2507
- const messages = Array.from(this.tokens).map((to) => {
2579
+ const offlineTokens = Array.from(this.tokens).filter((token) => {
2580
+ const ws = this.tokenWsMap.get(token);
2581
+ return !ws || ws.readyState !== ws.OPEN;
2582
+ });
2583
+ if (offlineTokens.length === 0) return;
2584
+ const messages = offlineTokens.map((to) => {
2508
2585
  let sound = payload.sound ?? "default";
2509
2586
  const prefs = this.soundPreferences.get(to);
2510
2587
  if (prefs) {
@@ -2524,7 +2601,7 @@ var ExpoNotificationChannel = class {
2524
2601
  };
2525
2602
  });
2526
2603
  try {
2527
- console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
2604
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
2528
2605
  const res = await fetch(EXPO_PUSH_API, {
2529
2606
  method: "POST",
2530
2607
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -3027,6 +3104,180 @@ async function countJsonlFilesWithMtime(dirPath) {
3027
3104
  }
3028
3105
  }
3029
3106
 
3107
+ // src/pairing/PairingManager.ts
3108
+ var PairingManager = class {
3109
+ _state = "closed";
3110
+ timer = null;
3111
+ deadline = 0;
3112
+ token;
3113
+ serverName;
3114
+ version;
3115
+ defaultDuration;
3116
+ onStateChange;
3117
+ constructor(opts) {
3118
+ this.token = opts.token;
3119
+ this.serverName = opts.serverName;
3120
+ this.version = opts.version;
3121
+ this.defaultDuration = opts.defaultDuration ?? 3e5;
3122
+ this.onStateChange = opts.onStateChange;
3123
+ }
3124
+ get state() {
3125
+ return this._state;
3126
+ }
3127
+ open(duration) {
3128
+ const ms = duration ?? this.defaultDuration;
3129
+ if (this.timer) clearTimeout(this.timer);
3130
+ this._state = "open";
3131
+ this.deadline = Date.now() + ms;
3132
+ this.timer = setTimeout(() => this.close(), ms);
3133
+ this.onStateChange("open");
3134
+ }
3135
+ close() {
3136
+ if (this.timer) {
3137
+ clearTimeout(this.timer);
3138
+ this.timer = null;
3139
+ }
3140
+ if (this._state === "closed") return;
3141
+ this._state = "closed";
3142
+ this.deadline = 0;
3143
+ this.onStateChange("closed");
3144
+ }
3145
+ tryPair() {
3146
+ if (this._state !== "open") return null;
3147
+ const result = { token: this.token, serverName: this.serverName, version: this.version };
3148
+ this.close();
3149
+ return result;
3150
+ }
3151
+ getRemainingSeconds() {
3152
+ if (this._state !== "open") return 0;
3153
+ return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
3154
+ }
3155
+ destroy() {
3156
+ if (this.timer) {
3157
+ clearTimeout(this.timer);
3158
+ this.timer = null;
3159
+ }
3160
+ }
3161
+ };
3162
+
3163
+ // src/auth/AuthManager.ts
3164
+ var import_child_process3 = require("child_process");
3165
+ var import_child_process4 = require("child_process");
3166
+ var import_util = require("util");
3167
+ var import_events2 = require("events");
3168
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3169
+ var CLAUDE_PATH2 = findClaudePath();
3170
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3171
+ var AuthManager = class extends import_events2.EventEmitter {
3172
+ loginProcess = null;
3173
+ loginTimeout = null;
3174
+ urlSent = false;
3175
+ /** 检查当前 Claude CLI 认证状态(异步,不阻塞事件循环) */
3176
+ async checkAuth() {
3177
+ try {
3178
+ const { stdout } = await execFileAsync(CLAUDE_PATH2, ["auth", "status"], {
3179
+ timeout: 1e4
3180
+ });
3181
+ const parsed = JSON.parse(stdout.trim());
3182
+ return {
3183
+ loggedIn: !!parsed.loggedIn,
3184
+ email: parsed.email,
3185
+ authMethod: parsed.authMethod
3186
+ };
3187
+ } catch {
3188
+ return { loggedIn: false };
3189
+ }
3190
+ }
3191
+ /** 启动登录流程,捕获 URL 并通过事件推送 */
3192
+ async startLogin() {
3193
+ if (this.loginProcess) {
3194
+ this.loginProcess.kill();
3195
+ this.loginProcess = null;
3196
+ }
3197
+ this.clearLoginTimeout();
3198
+ this.urlSent = false;
3199
+ const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3200
+ env: { ...process.env, BROWSER: "echo" },
3201
+ stdio: ["pipe", "pipe", "pipe"]
3202
+ });
3203
+ this.loginProcess = proc;
3204
+ const handleOutput = (data) => {
3205
+ const text = data.toString();
3206
+ console.log(`[AuthManager] login output: ${text.trim()}`);
3207
+ if (!this.urlSent) {
3208
+ const url = this.extractUrl(text);
3209
+ if (url) {
3210
+ this.urlSent = true;
3211
+ console.log(`[AuthManager] \u6355\u83B7\u5230\u767B\u5F55 URL: ${url}`);
3212
+ this.emit("login_url", url);
3213
+ }
3214
+ }
3215
+ };
3216
+ proc.stdout?.on("data", handleOutput);
3217
+ proc.stderr?.on("data", handleOutput);
3218
+ proc.on("exit", (code) => {
3219
+ console.log(`[AuthManager] login process exited with code ${code}`);
3220
+ this.loginProcess = null;
3221
+ this.clearLoginTimeout();
3222
+ this.checkAuth().then((status) => {
3223
+ if (status.loggedIn) {
3224
+ this.emit("login_result", { success: true });
3225
+ } else if (code !== 0) {
3226
+ this.emit("login_result", { success: false, error: `Exit code: ${code}` });
3227
+ }
3228
+ });
3229
+ });
3230
+ proc.on("error", (err) => {
3231
+ console.error(`[AuthManager] login process error:`, err.message);
3232
+ this.loginProcess = null;
3233
+ this.clearLoginTimeout();
3234
+ this.emit("login_result", { success: false, error: err.message });
3235
+ });
3236
+ this.loginTimeout = setTimeout(() => {
3237
+ if (this.loginProcess) {
3238
+ console.warn("[AuthManager] login process timed out, killing");
3239
+ this.loginProcess.kill();
3240
+ this.loginProcess = null;
3241
+ this.emit("login_result", { success: false, error: "Login timed out" });
3242
+ }
3243
+ }, LOGIN_TIMEOUT_MS);
3244
+ }
3245
+ /** 提交授权码到登录进程的 stdin */
3246
+ submitCode(code) {
3247
+ if (!this.loginProcess?.stdin?.writable) {
3248
+ console.warn("[AuthManager] No active login process");
3249
+ return false;
3250
+ }
3251
+ console.log(`[AuthManager] \u63D0\u4EA4\u6388\u6743\u7801`);
3252
+ this.loginProcess.stdin.write(code + "\n");
3253
+ return true;
3254
+ }
3255
+ /** 是否有登录进程在运行 */
3256
+ get isLoginInProgress() {
3257
+ return this.loginProcess !== null;
3258
+ }
3259
+ /** 清理资源 */
3260
+ destroy() {
3261
+ this.clearLoginTimeout();
3262
+ if (this.loginProcess) {
3263
+ this.loginProcess.kill();
3264
+ this.loginProcess = null;
3265
+ }
3266
+ this.removeAllListeners();
3267
+ }
3268
+ clearLoginTimeout() {
3269
+ if (this.loginTimeout) {
3270
+ clearTimeout(this.loginTimeout);
3271
+ this.loginTimeout = null;
3272
+ }
3273
+ }
3274
+ /** 从文本中提取 URL */
3275
+ extractUrl(text) {
3276
+ const match = text.match(/https?:\/\/[^\s"'<>]+/);
3277
+ return match ? match[0] : null;
3278
+ }
3279
+ };
3280
+
3030
3281
  // src/server.ts
3031
3282
  var import_promises5 = require("fs/promises");
3032
3283
  var WS_PORT = 3745;
@@ -3105,9 +3356,29 @@ async function start(opts = {}) {
3105
3356
  HTTP_PORT,
3106
3357
  () => ApprovalProxy.create({ port: HTTP_PORT, token })
3107
3358
  );
3359
+ let mdnsService = null;
3360
+ const pairingManager = new PairingManager({
3361
+ token,
3362
+ serverName: (0, import_node_os4.hostname)(),
3363
+ version: "0.2.0",
3364
+ onStateChange: (state) => mdnsService?.updatePairingState(state)
3365
+ });
3366
+ approvalProxy.setPairingManager(pairingManager);
3367
+ const authManager = new AuthManager();
3368
+ authManager.on("login_url", (url) => {
3369
+ wsBridge.broadcast({ type: "auth_login_url", url });
3370
+ });
3371
+ authManager.on("login_result", (result) => {
3372
+ wsBridge.broadcast({ type: "auth_login_result", success: result.success, error: result.error });
3373
+ if (result.success) {
3374
+ authManager.checkAuth().then((status) => {
3375
+ wsBridge.broadcast({ type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3376
+ });
3377
+ }
3378
+ });
3108
3379
  const unreadSessionIds = /* @__PURE__ */ new Set();
3109
3380
  notificationService.setGlobalPendingCountProvider(
3110
- () => approvalProxy.getPendingCount() + unreadSessionIds.size
3381
+ () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3111
3382
  );
3112
3383
  const broadcastUnreadSessions = () => {
3113
3384
  wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
@@ -3322,7 +3593,7 @@ async function start(opts = {}) {
3322
3593
  break;
3323
3594
  }
3324
3595
  case "register_push_token": {
3325
- notificationService.addPushToken(event.token);
3596
+ notificationService.addPushToken(event.token, ws);
3326
3597
  break;
3327
3598
  }
3328
3599
  case "unregister_push_token": {
@@ -3361,6 +3632,22 @@ async function start(opts = {}) {
3361
3632
  approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
3362
3633
  break;
3363
3634
  }
3635
+ case "check_auth": {
3636
+ const status = await authManager.checkAuth();
3637
+ wsBridge.send(ws, { type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3638
+ break;
3639
+ }
3640
+ case "start_auth_login": {
3641
+ await authManager.startLogin();
3642
+ break;
3643
+ }
3644
+ case "submit_auth_code": {
3645
+ const submitted = authManager.submitCode(event.code);
3646
+ if (!submitted) {
3647
+ wsBridge.send(ws, { type: "auth_login_result", success: false, error: t("server.noActiveLoginProcess") });
3648
+ }
3649
+ break;
3650
+ }
3364
3651
  default: {
3365
3652
  wsBridge.send(ws, {
3366
3653
  type: "error",
@@ -3438,10 +3725,13 @@ async function start(opts = {}) {
3438
3725
  connections: wsBridge.getConnectionCount(),
3439
3726
  activeSessions: sessionManager.getActiveSessions().length
3440
3727
  }));
3441
- let mdnsService = null;
3442
3728
  const startMdns = () => {
3443
3729
  if (mdnsService) return;
3444
- mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
3730
+ mdnsService = new MdnsService({
3731
+ wsPort: WS_PORT,
3732
+ httpPort: HTTP_PORT,
3733
+ pairing: pairingManager.state
3734
+ });
3445
3735
  mdnsService.start();
3446
3736
  };
3447
3737
  const stopMdns = () => {
@@ -3449,6 +3739,9 @@ async function start(opts = {}) {
3449
3739
  mdnsService.stop();
3450
3740
  mdnsService = null;
3451
3741
  };
3742
+ if (opts.enablePairing !== false) {
3743
+ pairingManager.open();
3744
+ }
3452
3745
  if (opts.enableAutoConnect !== false) {
3453
3746
  startMdns();
3454
3747
  }
@@ -3476,7 +3769,9 @@ async function start(opts = {}) {
3476
3769
  errors.push(err);
3477
3770
  }
3478
3771
  };
3772
+ await attempt(() => authManager.destroy(), "AuthManager");
3479
3773
  await attempt(() => stopMdns(), "mDNS");
3774
+ await attempt(() => pairingManager.destroy(), "PairingManager");
3480
3775
  await attempt(() => wsBridge.close(), "WebSocket");
3481
3776
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3482
3777
  await attempt(() => sessionManager.destroy(), "SessionManager");
@@ -3504,15 +3799,41 @@ async function start(opts = {}) {
3504
3799
  } else {
3505
3800
  stopMdns();
3506
3801
  }
3507
- }
3802
+ },
3803
+ openPairing: (duration) => pairingManager.open(duration),
3804
+ closePairing: () => pairingManager.close()
3508
3805
  };
3509
3806
  }
3510
3807
 
3511
3808
  // src/index.ts
3512
3809
  var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
3810
+ function getPackageVersion() {
3811
+ try {
3812
+ const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf8"));
3813
+ return pkg.version ?? "0.0.0";
3814
+ } catch {
3815
+ return "0.0.0";
3816
+ }
3817
+ }
3818
+ var PKG_VERSION = getPackageVersion();
3819
+ async function fetchLatestVersion() {
3820
+ try {
3821
+ const controller = new AbortController();
3822
+ const timer = setTimeout(() => controller.abort(), 5e3);
3823
+ const res = await fetch("https://registry.npmjs.org/sessix-server/latest", {
3824
+ signal: controller.signal
3825
+ });
3826
+ clearTimeout(timer);
3827
+ if (!res.ok) return null;
3828
+ const data = await res.json();
3829
+ return data.version ?? null;
3830
+ } catch {
3831
+ return null;
3832
+ }
3833
+ }
3513
3834
  async function main() {
3514
3835
  console.log("=".repeat(50));
3515
- console.log(t("startup.banner"));
3836
+ console.log(`${t("startup.banner")} v${PKG_VERSION}`);
3516
3837
  console.log("=".repeat(50));
3517
3838
  console.log();
3518
3839
  const enableAutoConnect = process.env.SESSIX_AUTO_CONNECT !== "false";
@@ -3552,6 +3873,15 @@ async function main() {
3552
3873
  console.log();
3553
3874
  console.log(t("startup.waitingConnection"));
3554
3875
  console.log();
3876
+ console.log(t("startup.pairingOpen"));
3877
+ console.log();
3878
+ fetchLatestVersion().then((latest) => {
3879
+ if (!latest || latest === PKG_VERSION) return;
3880
+ console.log();
3881
+ console.log(` \u{1F4E6} ${t("startup.updateAvailable", { current: PKG_VERSION, latest })}`);
3882
+ console.log(` npx sessix-server@latest`);
3883
+ console.log();
3884
+ });
3555
3885
  const shutdown = async (signal) => {
3556
3886
  console.log(`
3557
3887
  [Main] ${t("startup.receivedSignal", { signal })}`);
@@ -3564,8 +3894,24 @@ async function main() {
3564
3894
  process.exit(1);
3565
3895
  }
3566
3896
  };
3567
- process.on("SIGINT", () => shutdown("SIGINT"));
3568
- process.on("SIGTERM", () => shutdown("SIGTERM"));
3897
+ if (process.stdin.isTTY) {
3898
+ process.stdin.setRawMode(true);
3899
+ process.stdin.resume();
3900
+ process.stdin.setEncoding("utf8");
3901
+ process.stdin.on("data", (key) => {
3902
+ if (key === "p" || key === "P") {
3903
+ server.openPairing();
3904
+ console.log(`
3905
+ ${t("startup.pairingReopened")}`);
3906
+ }
3907
+ if (key === "") {
3908
+ shutdown("SIGINT");
3909
+ }
3910
+ });
3911
+ } else {
3912
+ process.on("SIGINT", () => shutdown("SIGINT"));
3913
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
3914
+ }
3569
3915
  }
3570
3916
  function getLocalIp() {
3571
3917
  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,9 @@ 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",
56
+ updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
54
57
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
55
58
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
56
59
  shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
@@ -77,7 +80,8 @@ var zh = {
77
80
  restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
78
81
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
79
82
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
80
- activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
83
+ 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"
81
85
  },
82
86
  ws: {
83
87
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -152,6 +156,9 @@ var en = {
152
156
  autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
153
157
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
154
158
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
159
+ pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
160
+ pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
161
+ updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
155
162
  receivedSignal: "Received {{signal}}, graceful shutdown...",
156
163
  goodbye: "All services closed, goodbye!",
157
164
  shutdownError: "Shutdown error:",
@@ -178,7 +185,8 @@ var en = {
178
185
  restarting: "Restarting {{label}}...",
179
186
  activityPushEnabled: "ActivityKit Push enabled",
180
187
  activityPushFailed: "ActivityKit Push init failed:",
181
- activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
188
+ activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
189
+ noActiveLoginProcess: "No active login process"
182
190
  },
183
191
  ws: {
184
192
  started: "WebSocket server started on port {{port}}",
@@ -291,11 +299,14 @@ var import_node_child_process2 = require("child_process");
291
299
  var import_node_util = require("util");
292
300
 
293
301
  // src/providers/ProcessProvider.ts
294
- var import_child_process = require("child_process");
302
+ var import_child_process2 = require("child_process");
295
303
  var import_readline = require("readline");
296
304
  var import_events = require("events");
297
305
  var import_node_os = require("os");
298
306
  var import_uuid = require("uuid");
307
+
308
+ // src/utils/claudePath.ts
309
+ var import_child_process = require("child_process");
299
310
  function findClaudePath() {
300
311
  try {
301
312
  return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
@@ -315,6 +326,8 @@ function findClaudePath() {
315
326
  return "claude";
316
327
  }
317
328
  }
329
+
330
+ // src/providers/ProcessProvider.ts
318
331
  var CLAUDE_PATH = findClaudePath();
319
332
  var ProcessProvider = class {
320
333
  /** 活跃会话映射表:sessionId -> { session, process } */
@@ -507,7 +520,7 @@ var ProcessProvider = class {
507
520
  }
508
521
  const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
509
522
  delete env.CLAUDECODE;
510
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
523
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
511
524
  cwd: projectPath,
512
525
  env,
513
526
  stdio: ["pipe", "pipe", "pipe"]
@@ -714,7 +727,7 @@ ${context}`;
714
727
  return new Promise((resolve, reject) => {
715
728
  const env = { ...process.env };
716
729
  delete env.CLAUDECODE;
717
- const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
730
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
718
731
  cwd: (0, import_node_os.homedir)(),
719
732
  env,
720
733
  stdio: ["pipe", "pipe", "pipe"]
@@ -1625,6 +1638,7 @@ var ApprovalProxy = class _ApprovalProxy {
1625
1638
  alwaysAllowedTools = /* @__PURE__ */ new Set();
1626
1639
  /** 获取状态信息的回调(由外部注入) */
1627
1640
  statusInfoProvider = null;
1641
+ pairingManager = null;
1628
1642
  constructor(options) {
1629
1643
  this.token = options.token;
1630
1644
  this.port = options.port;
@@ -1659,6 +1673,10 @@ var ApprovalProxy = class _ApprovalProxy {
1659
1673
  setStatusInfoProvider(provider) {
1660
1674
  this.statusInfoProvider = provider;
1661
1675
  }
1676
+ /** 设置配对管理器 */
1677
+ setPairingManager(manager) {
1678
+ this.pairingManager = manager;
1679
+ }
1662
1680
  /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1663
1681
  setYoloMode(sessionId, enabled) {
1664
1682
  this.yoloSessions.set(sessionId, enabled);
@@ -1819,6 +1837,8 @@ var ApprovalProxy = class _ApprovalProxy {
1819
1837
  const pathname = url.pathname;
1820
1838
  if (req.method === "POST" && pathname === "/hook/approval") {
1821
1839
  this.handleApprovalHook(req, res);
1840
+ } else if (req.method === "POST" && pathname === "/pair") {
1841
+ this.handlePair(req, res);
1822
1842
  } else if (req.method === "GET" && pathname === "/health") {
1823
1843
  this.handleHealth(req, res);
1824
1844
  } else if (req.method === "GET" && pathname === "/token") {
@@ -1889,6 +1909,23 @@ var ApprovalProxy = class _ApprovalProxy {
1889
1909
  activeSessions: info.activeSessions
1890
1910
  });
1891
1911
  }
1912
+ /** 配对端点:配对窗口开放时返回 token */
1913
+ handlePair(_req, res) {
1914
+ if (!this.pairingManager) {
1915
+ this.sendJson(res, 503, { error: "pairing_unavailable" });
1916
+ return;
1917
+ }
1918
+ const result = this.pairingManager.tryPair();
1919
+ if (result) {
1920
+ console.log("[ApprovalProxy] Device paired successfully");
1921
+ this.sendJson(res, 200, result);
1922
+ } else {
1923
+ this.sendJson(res, 403, {
1924
+ error: "pairing_closed",
1925
+ message: "Pairing window is closed. Restart server or press p to reopen."
1926
+ });
1927
+ }
1928
+ }
1892
1929
  /** 返回连接 token(仅本机访问) */
1893
1930
  handleToken(req, res) {
1894
1931
  const remoteAddress = req.socket.remoteAddress;
@@ -1959,12 +1996,12 @@ var MdnsService = class {
1959
1996
  wsPort;
1960
1997
  httpPort;
1961
1998
  version;
1962
- token;
1999
+ pairing;
1963
2000
  constructor(options) {
1964
2001
  this.wsPort = options.wsPort;
1965
2002
  this.httpPort = options.httpPort;
1966
2003
  this.version = options.version ?? "0.1.0";
1967
- this.token = options.token ?? "";
2004
+ this.pairing = options.pairing ?? "closed";
1968
2005
  }
1969
2006
  /**
1970
2007
  * 启动 mDNS 广播
@@ -1982,7 +2019,8 @@ var MdnsService = class {
1982
2019
  txt: {
1983
2020
  version: this.version,
1984
2021
  httpPort: String(this.httpPort),
1985
- token: this.token
2022
+ wsPort: String(this.wsPort),
2023
+ pairing: this.pairing
1986
2024
  }
1987
2025
  });
1988
2026
  console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
@@ -2003,6 +2041,34 @@ var MdnsService = class {
2003
2041
  }
2004
2042
  console.log(`[MdnsService] ${t("mdns.closed")}`);
2005
2043
  }
2044
+ /**
2045
+ * 更新配对状态(重新发布 mDNS 服务)
2046
+ */
2047
+ updatePairingState(state) {
2048
+ this.pairing = state;
2049
+ if (!this.bonjour) return;
2050
+ const republish = () => {
2051
+ if (!this.bonjour) return;
2052
+ this.service = this.bonjour.publish({
2053
+ name: "Sessix",
2054
+ type: "sessix",
2055
+ port: this.wsPort,
2056
+ txt: {
2057
+ version: this.version,
2058
+ httpPort: String(this.httpPort),
2059
+ wsPort: String(this.wsPort),
2060
+ pairing: state
2061
+ }
2062
+ });
2063
+ };
2064
+ if (this.service) {
2065
+ const old = this.service;
2066
+ this.service = null;
2067
+ old.stop?.(() => republish());
2068
+ } else {
2069
+ republish();
2070
+ }
2071
+ }
2006
2072
  };
2007
2073
 
2008
2074
  // src/hooks/HookInstaller.ts
@@ -2248,8 +2314,8 @@ var NotificationService = class {
2248
2314
  if (entry) entry.enabled = enabled;
2249
2315
  }
2250
2316
  /** 注册手机 push token(连接建立时由 WsBridge 调用) */
2251
- addPushToken(token) {
2252
- this.expoChannel?.addToken(token);
2317
+ addPushToken(token, ws) {
2318
+ this.expoChannel?.addToken(token, ws);
2253
2319
  }
2254
2320
  /** 移除手机 push token(断线时或手机主动注销时调用) */
2255
2321
  removePushToken(token) {
@@ -2489,17 +2555,21 @@ var MacNotificationChannel = class {
2489
2555
  var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
2490
2556
  var ExpoNotificationChannel = class {
2491
2557
  tokens = /* @__PURE__ */ new Set();
2558
+ /** push token → WebSocket 连接映射,用于前台抑制 */
2559
+ tokenWsMap = /* @__PURE__ */ new Map();
2492
2560
  /** per-token 通知音效偏好 */
2493
2561
  soundPreferences = /* @__PURE__ */ new Map();
2494
2562
  isAvailable() {
2495
2563
  return this.tokens.size > 0;
2496
2564
  }
2497
- addToken(token) {
2565
+ addToken(token, ws) {
2498
2566
  this.tokens.add(token);
2567
+ if (ws) this.tokenWsMap.set(token, ws);
2499
2568
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
2500
2569
  }
2501
2570
  removeToken(token) {
2502
2571
  this.tokens.delete(token);
2572
+ this.tokenWsMap.delete(token);
2503
2573
  this.soundPreferences.delete(token);
2504
2574
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
2505
2575
  }
@@ -2512,7 +2582,12 @@ var ExpoNotificationChannel = class {
2512
2582
  }
2513
2583
  async send(payload) {
2514
2584
  if (this.tokens.size === 0) return;
2515
- const messages = Array.from(this.tokens).map((to) => {
2585
+ const offlineTokens = Array.from(this.tokens).filter((token) => {
2586
+ const ws = this.tokenWsMap.get(token);
2587
+ return !ws || ws.readyState !== ws.OPEN;
2588
+ });
2589
+ if (offlineTokens.length === 0) return;
2590
+ const messages = offlineTokens.map((to) => {
2516
2591
  let sound = payload.sound ?? "default";
2517
2592
  const prefs = this.soundPreferences.get(to);
2518
2593
  if (prefs) {
@@ -2532,7 +2607,7 @@ var ExpoNotificationChannel = class {
2532
2607
  };
2533
2608
  });
2534
2609
  try {
2535
- console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
2610
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
2536
2611
  const res = await fetch(EXPO_PUSH_API, {
2537
2612
  method: "POST",
2538
2613
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -3035,6 +3110,180 @@ async function countJsonlFilesWithMtime(dirPath) {
3035
3110
  }
3036
3111
  }
3037
3112
 
3113
+ // src/pairing/PairingManager.ts
3114
+ var PairingManager = class {
3115
+ _state = "closed";
3116
+ timer = null;
3117
+ deadline = 0;
3118
+ token;
3119
+ serverName;
3120
+ version;
3121
+ defaultDuration;
3122
+ onStateChange;
3123
+ constructor(opts) {
3124
+ this.token = opts.token;
3125
+ this.serverName = opts.serverName;
3126
+ this.version = opts.version;
3127
+ this.defaultDuration = opts.defaultDuration ?? 3e5;
3128
+ this.onStateChange = opts.onStateChange;
3129
+ }
3130
+ get state() {
3131
+ return this._state;
3132
+ }
3133
+ open(duration) {
3134
+ const ms = duration ?? this.defaultDuration;
3135
+ if (this.timer) clearTimeout(this.timer);
3136
+ this._state = "open";
3137
+ this.deadline = Date.now() + ms;
3138
+ this.timer = setTimeout(() => this.close(), ms);
3139
+ this.onStateChange("open");
3140
+ }
3141
+ close() {
3142
+ if (this.timer) {
3143
+ clearTimeout(this.timer);
3144
+ this.timer = null;
3145
+ }
3146
+ if (this._state === "closed") return;
3147
+ this._state = "closed";
3148
+ this.deadline = 0;
3149
+ this.onStateChange("closed");
3150
+ }
3151
+ tryPair() {
3152
+ if (this._state !== "open") return null;
3153
+ const result = { token: this.token, serverName: this.serverName, version: this.version };
3154
+ this.close();
3155
+ return result;
3156
+ }
3157
+ getRemainingSeconds() {
3158
+ if (this._state !== "open") return 0;
3159
+ return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
3160
+ }
3161
+ destroy() {
3162
+ if (this.timer) {
3163
+ clearTimeout(this.timer);
3164
+ this.timer = null;
3165
+ }
3166
+ }
3167
+ };
3168
+
3169
+ // src/auth/AuthManager.ts
3170
+ var import_child_process3 = require("child_process");
3171
+ var import_child_process4 = require("child_process");
3172
+ var import_util = require("util");
3173
+ var import_events2 = require("events");
3174
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3175
+ var CLAUDE_PATH2 = findClaudePath();
3176
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3177
+ var AuthManager = class extends import_events2.EventEmitter {
3178
+ loginProcess = null;
3179
+ loginTimeout = null;
3180
+ urlSent = false;
3181
+ /** 检查当前 Claude CLI 认证状态(异步,不阻塞事件循环) */
3182
+ async checkAuth() {
3183
+ try {
3184
+ const { stdout } = await execFileAsync(CLAUDE_PATH2, ["auth", "status"], {
3185
+ timeout: 1e4
3186
+ });
3187
+ const parsed = JSON.parse(stdout.trim());
3188
+ return {
3189
+ loggedIn: !!parsed.loggedIn,
3190
+ email: parsed.email,
3191
+ authMethod: parsed.authMethod
3192
+ };
3193
+ } catch {
3194
+ return { loggedIn: false };
3195
+ }
3196
+ }
3197
+ /** 启动登录流程,捕获 URL 并通过事件推送 */
3198
+ async startLogin() {
3199
+ if (this.loginProcess) {
3200
+ this.loginProcess.kill();
3201
+ this.loginProcess = null;
3202
+ }
3203
+ this.clearLoginTimeout();
3204
+ this.urlSent = false;
3205
+ const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3206
+ env: { ...process.env, BROWSER: "echo" },
3207
+ stdio: ["pipe", "pipe", "pipe"]
3208
+ });
3209
+ this.loginProcess = proc;
3210
+ const handleOutput = (data) => {
3211
+ const text = data.toString();
3212
+ console.log(`[AuthManager] login output: ${text.trim()}`);
3213
+ if (!this.urlSent) {
3214
+ const url = this.extractUrl(text);
3215
+ if (url) {
3216
+ this.urlSent = true;
3217
+ console.log(`[AuthManager] \u6355\u83B7\u5230\u767B\u5F55 URL: ${url}`);
3218
+ this.emit("login_url", url);
3219
+ }
3220
+ }
3221
+ };
3222
+ proc.stdout?.on("data", handleOutput);
3223
+ proc.stderr?.on("data", handleOutput);
3224
+ proc.on("exit", (code) => {
3225
+ console.log(`[AuthManager] login process exited with code ${code}`);
3226
+ this.loginProcess = null;
3227
+ this.clearLoginTimeout();
3228
+ this.checkAuth().then((status) => {
3229
+ if (status.loggedIn) {
3230
+ this.emit("login_result", { success: true });
3231
+ } else if (code !== 0) {
3232
+ this.emit("login_result", { success: false, error: `Exit code: ${code}` });
3233
+ }
3234
+ });
3235
+ });
3236
+ proc.on("error", (err) => {
3237
+ console.error(`[AuthManager] login process error:`, err.message);
3238
+ this.loginProcess = null;
3239
+ this.clearLoginTimeout();
3240
+ this.emit("login_result", { success: false, error: err.message });
3241
+ });
3242
+ this.loginTimeout = setTimeout(() => {
3243
+ if (this.loginProcess) {
3244
+ console.warn("[AuthManager] login process timed out, killing");
3245
+ this.loginProcess.kill();
3246
+ this.loginProcess = null;
3247
+ this.emit("login_result", { success: false, error: "Login timed out" });
3248
+ }
3249
+ }, LOGIN_TIMEOUT_MS);
3250
+ }
3251
+ /** 提交授权码到登录进程的 stdin */
3252
+ submitCode(code) {
3253
+ if (!this.loginProcess?.stdin?.writable) {
3254
+ console.warn("[AuthManager] No active login process");
3255
+ return false;
3256
+ }
3257
+ console.log(`[AuthManager] \u63D0\u4EA4\u6388\u6743\u7801`);
3258
+ this.loginProcess.stdin.write(code + "\n");
3259
+ return true;
3260
+ }
3261
+ /** 是否有登录进程在运行 */
3262
+ get isLoginInProgress() {
3263
+ return this.loginProcess !== null;
3264
+ }
3265
+ /** 清理资源 */
3266
+ destroy() {
3267
+ this.clearLoginTimeout();
3268
+ if (this.loginProcess) {
3269
+ this.loginProcess.kill();
3270
+ this.loginProcess = null;
3271
+ }
3272
+ this.removeAllListeners();
3273
+ }
3274
+ clearLoginTimeout() {
3275
+ if (this.loginTimeout) {
3276
+ clearTimeout(this.loginTimeout);
3277
+ this.loginTimeout = null;
3278
+ }
3279
+ }
3280
+ /** 从文本中提取 URL */
3281
+ extractUrl(text) {
3282
+ const match = text.match(/https?:\/\/[^\s"'<>]+/);
3283
+ return match ? match[0] : null;
3284
+ }
3285
+ };
3286
+
3038
3287
  // src/server.ts
3039
3288
  var import_promises5 = require("fs/promises");
3040
3289
  var WS_PORT = 3745;
@@ -3113,9 +3362,29 @@ async function start(opts = {}) {
3113
3362
  HTTP_PORT,
3114
3363
  () => ApprovalProxy.create({ port: HTTP_PORT, token })
3115
3364
  );
3365
+ let mdnsService = null;
3366
+ const pairingManager = new PairingManager({
3367
+ token,
3368
+ serverName: (0, import_node_os4.hostname)(),
3369
+ version: "0.2.0",
3370
+ onStateChange: (state) => mdnsService?.updatePairingState(state)
3371
+ });
3372
+ approvalProxy.setPairingManager(pairingManager);
3373
+ const authManager = new AuthManager();
3374
+ authManager.on("login_url", (url) => {
3375
+ wsBridge.broadcast({ type: "auth_login_url", url });
3376
+ });
3377
+ authManager.on("login_result", (result) => {
3378
+ wsBridge.broadcast({ type: "auth_login_result", success: result.success, error: result.error });
3379
+ if (result.success) {
3380
+ authManager.checkAuth().then((status) => {
3381
+ wsBridge.broadcast({ type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3382
+ });
3383
+ }
3384
+ });
3116
3385
  const unreadSessionIds = /* @__PURE__ */ new Set();
3117
3386
  notificationService.setGlobalPendingCountProvider(
3118
- () => approvalProxy.getPendingCount() + unreadSessionIds.size
3387
+ () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3119
3388
  );
3120
3389
  const broadcastUnreadSessions = () => {
3121
3390
  wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
@@ -3330,7 +3599,7 @@ async function start(opts = {}) {
3330
3599
  break;
3331
3600
  }
3332
3601
  case "register_push_token": {
3333
- notificationService.addPushToken(event.token);
3602
+ notificationService.addPushToken(event.token, ws);
3334
3603
  break;
3335
3604
  }
3336
3605
  case "unregister_push_token": {
@@ -3369,6 +3638,22 @@ async function start(opts = {}) {
3369
3638
  approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
3370
3639
  break;
3371
3640
  }
3641
+ case "check_auth": {
3642
+ const status = await authManager.checkAuth();
3643
+ wsBridge.send(ws, { type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
3644
+ break;
3645
+ }
3646
+ case "start_auth_login": {
3647
+ await authManager.startLogin();
3648
+ break;
3649
+ }
3650
+ case "submit_auth_code": {
3651
+ const submitted = authManager.submitCode(event.code);
3652
+ if (!submitted) {
3653
+ wsBridge.send(ws, { type: "auth_login_result", success: false, error: t("server.noActiveLoginProcess") });
3654
+ }
3655
+ break;
3656
+ }
3372
3657
  default: {
3373
3658
  wsBridge.send(ws, {
3374
3659
  type: "error",
@@ -3446,10 +3731,13 @@ async function start(opts = {}) {
3446
3731
  connections: wsBridge.getConnectionCount(),
3447
3732
  activeSessions: sessionManager.getActiveSessions().length
3448
3733
  }));
3449
- let mdnsService = null;
3450
3734
  const startMdns = () => {
3451
3735
  if (mdnsService) return;
3452
- mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
3736
+ mdnsService = new MdnsService({
3737
+ wsPort: WS_PORT,
3738
+ httpPort: HTTP_PORT,
3739
+ pairing: pairingManager.state
3740
+ });
3453
3741
  mdnsService.start();
3454
3742
  };
3455
3743
  const stopMdns = () => {
@@ -3457,6 +3745,9 @@ async function start(opts = {}) {
3457
3745
  mdnsService.stop();
3458
3746
  mdnsService = null;
3459
3747
  };
3748
+ if (opts.enablePairing !== false) {
3749
+ pairingManager.open();
3750
+ }
3460
3751
  if (opts.enableAutoConnect !== false) {
3461
3752
  startMdns();
3462
3753
  }
@@ -3484,7 +3775,9 @@ async function start(opts = {}) {
3484
3775
  errors.push(err);
3485
3776
  }
3486
3777
  };
3778
+ await attempt(() => authManager.destroy(), "AuthManager");
3487
3779
  await attempt(() => stopMdns(), "mDNS");
3780
+ await attempt(() => pairingManager.destroy(), "PairingManager");
3488
3781
  await attempt(() => wsBridge.close(), "WebSocket");
3489
3782
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3490
3783
  await attempt(() => sessionManager.destroy(), "SessionManager");
@@ -3512,7 +3805,9 @@ async function start(opts = {}) {
3512
3805
  } else {
3513
3806
  stopMdns();
3514
3807
  }
3515
- }
3808
+ },
3809
+ openPairing: (duration) => pairingManager.open(duration),
3810
+ closePairing: () => pairingManager.close()
3516
3811
  };
3517
3812
  }
3518
3813
  // 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.4",
3
+ "version": "0.2.2",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },
@@ -8,9 +8,9 @@
8
8
  "types": "./dist/server.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
+ "types": "./dist/server.d.ts",
11
12
  "import": "./dist/server.js",
12
- "require": "./dist/server.js",
13
- "types": "./dist/server.d.ts"
13
+ "require": "./dist/server.js"
14
14
  },
15
15
  "./cli": "./dist/index.js"
16
16
  },