sessix-server 0.2.5 → 0.2.6

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.
Files changed (3) hide show
  1. package/dist/index.js +198 -144
  2. package/dist/server.js +191 -138
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/index.ts
27
- var import_node_os6 = require("os");
28
- var import_node_fs2 = require("fs");
29
- var import_node_path5 = require("path");
27
+ var import_node_os7 = require("os");
28
+ var import_node_fs3 = require("fs");
29
+ var import_node_path6 = require("path");
30
30
 
31
31
  // src/i18n/locales/zh.ts
32
32
  var zh = {
@@ -289,38 +289,87 @@ function t(key, params) {
289
289
  // src/server.ts
290
290
  var import_uuid4 = require("uuid");
291
291
  var import_promises4 = require("fs/promises");
292
- var import_node_os5 = require("os");
293
- var import_node_path4 = require("path");
294
- var import_node_child_process2 = require("child_process");
292
+ var import_node_os6 = require("os");
293
+ var import_node_path5 = require("path");
294
+ var import_node_child_process4 = require("child_process");
295
295
  var import_node_util = require("util");
296
296
 
297
297
  // src/providers/ProcessProvider.ts
298
- var import_child_process2 = require("child_process");
298
+ var import_child_process = require("child_process");
299
299
  var import_readline = require("readline");
300
300
  var import_events = require("events");
301
- var import_node_os = require("os");
301
+ var import_node_os2 = require("os");
302
302
  var import_uuid = require("uuid");
303
303
 
304
304
  // src/utils/claudePath.ts
305
- var import_child_process = require("child_process");
305
+ var import_node_child_process2 = require("child_process");
306
+ var import_node_fs = require("fs");
307
+ var import_node_path = require("path");
308
+ var import_node_os = require("os");
309
+
310
+ // src/utils/platform.ts
311
+ var import_node_child_process = require("child_process");
312
+ var isWindows = process.platform === "win32";
313
+ function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
314
+ return new Promise((resolve) => {
315
+ if (proc.exitCode !== null || proc.signalCode !== null) {
316
+ resolve();
317
+ return;
318
+ }
319
+ const onExit = () => {
320
+ clearTimeout(timer);
321
+ resolve();
322
+ };
323
+ proc.once("exit", onExit);
324
+ if (isWindows) {
325
+ if (proc.pid) {
326
+ (0, import_node_child_process.spawn)("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
327
+ }
328
+ } else {
329
+ proc.kill("SIGTERM");
330
+ }
331
+ const timer = setTimeout(() => {
332
+ if (proc.exitCode === null && proc.signalCode === null) {
333
+ if (!isWindows) {
334
+ proc.kill("SIGKILL");
335
+ }
336
+ }
337
+ resolve();
338
+ }, timeoutMs);
339
+ });
340
+ }
341
+ function isNormalExit(code, signal) {
342
+ if (code === 0) return true;
343
+ if (isWindows) {
344
+ return code === 1;
345
+ }
346
+ return code === 143 || signal === "SIGTERM";
347
+ }
348
+
349
+ // src/utils/claudePath.ts
306
350
  function findClaudePath() {
307
351
  try {
308
- return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
352
+ const cmd = isWindows ? "where claude" : "which claude";
353
+ return (0, import_node_child_process2.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
309
354
  } catch {
310
- const candidates = [
311
- `${process.env.HOME}/.local/bin/claude`,
312
- "/usr/local/bin/claude",
313
- "/opt/homebrew/bin/claude"
314
- ];
315
- for (const candidate of candidates) {
316
- try {
317
- (0, import_child_process.execSync)(`test -x "${candidate}"`);
318
- return candidate;
319
- } catch {
320
- }
355
+ }
356
+ const candidates = isWindows ? [
357
+ (0, import_node_path.join)(process.env.LOCALAPPDATA ?? "", "Programs", "claude", "claude.exe"),
358
+ (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local", "Programs", "claude", "claude.exe"),
359
+ (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "local", "claude.exe")
360
+ ] : [
361
+ (0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "bin", "claude"),
362
+ "/usr/local/bin/claude",
363
+ "/opt/homebrew/bin/claude"
364
+ ];
365
+ for (const candidate of candidates) {
366
+ try {
367
+ (0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
368
+ return candidate;
369
+ } catch {
321
370
  }
322
- return "claude";
323
371
  }
372
+ return "claude";
324
373
  }
325
374
 
326
375
  // src/providers/ProcessProvider.ts
@@ -393,19 +442,7 @@ var ProcessProvider = class {
393
442
  entry.process.stdin?.end();
394
443
  } catch {
395
444
  }
396
- entry.process.kill("SIGTERM");
397
- await new Promise((resolve) => {
398
- const timeout = setTimeout(() => {
399
- if (entry.process.exitCode === null && entry.process.signalCode === null) {
400
- entry.process.kill("SIGKILL");
401
- }
402
- resolve();
403
- }, 3e3);
404
- entry.process.once("exit", () => {
405
- clearTimeout(timeout);
406
- resolve();
407
- });
408
- });
445
+ await killProcessCrossPlatform(entry.process);
409
446
  }
410
447
  this.emittedQuestionToolUseIds.delete(sessionId);
411
448
  this.activeSessions.delete(sessionId);
@@ -435,7 +472,7 @@ var ProcessProvider = class {
435
472
  entry.process.stdin?.end();
436
473
  } catch {
437
474
  }
438
- entry.process.kill("SIGTERM");
475
+ killProcessCrossPlatform(entry.process);
439
476
  }
440
477
  } else {
441
478
  console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
@@ -517,7 +554,7 @@ var ProcessProvider = class {
517
554
  }
518
555
  const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
519
556
  delete env.CLAUDECODE;
520
- const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
557
+ const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
521
558
  cwd: projectPath,
522
559
  env,
523
560
  stdio: ["pipe", "pipe", "pipe"]
@@ -663,7 +700,7 @@ var ProcessProvider = class {
663
700
  entry.session.lastActiveAt = Date.now();
664
701
  const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
665
702
  if (alreadyHasResult) return;
666
- const isNormal = code === 0 || code === 143 || signal === "SIGTERM";
703
+ const isNormal = isNormalExit(code, signal);
667
704
  entry.session.status = isNormal ? "idle" : "error";
668
705
  if (!isNormal) {
669
706
  console.error(
@@ -729,8 +766,8 @@ ${context}`;
729
766
  return new Promise((resolve, reject) => {
730
767
  const env = { ...process.env };
731
768
  delete env.CLAUDECODE;
732
- const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
733
- cwd: (0, import_node_os.homedir)(),
769
+ const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
770
+ cwd: (0, import_node_os2.homedir)(),
734
771
  env,
735
772
  stdio: ["pipe", "pipe", "pipe"]
736
773
  });
@@ -1622,15 +1659,15 @@ var WsBridge = class _WsBridge {
1622
1659
 
1623
1660
  // src/approval/ApprovalProxy.ts
1624
1661
  var import_node_http = __toESM(require("http"));
1625
- var import_node_fs = __toESM(require("fs"));
1626
- var import_node_path = __toESM(require("path"));
1627
- var import_node_os2 = __toESM(require("os"));
1662
+ var import_node_fs2 = __toESM(require("fs"));
1663
+ var import_node_path2 = __toESM(require("path"));
1664
+ var import_node_os3 = __toESM(require("os"));
1628
1665
  var import_uuid3 = require("uuid");
1629
1666
  var ApprovalProxy = class _ApprovalProxy {
1630
1667
  server;
1631
1668
  token;
1632
1669
  port;
1633
- settingsPath = import_node_path.default.join(import_node_os2.default.homedir(), ".claude", "settings.json");
1670
+ settingsPath = import_node_path2.default.join(import_node_os3.default.homedir(), ".claude", "settings.json");
1634
1671
  /** 待处理的审批请求:requestId -> { resolve, timer, request } */
1635
1672
  pendingApprovals = /* @__PURE__ */ new Map();
1636
1673
  /** 审批请求回调(通知外部推送到手机) */
@@ -1735,7 +1772,7 @@ var ApprovalProxy = class _ApprovalProxy {
1735
1772
  isToolInClaudeSettings(toolName, projectPath) {
1736
1773
  const checkPath = (filepath) => {
1737
1774
  try {
1738
- const raw = import_node_fs.default.readFileSync(filepath, "utf-8");
1775
+ const raw = import_node_fs2.default.readFileSync(filepath, "utf-8");
1739
1776
  const settings = JSON.parse(raw);
1740
1777
  const allow = settings?.permissions?.allow ?? [];
1741
1778
  return allow.some((entry) => {
@@ -1749,24 +1786,24 @@ var ApprovalProxy = class _ApprovalProxy {
1749
1786
  }
1750
1787
  };
1751
1788
  if (projectPath) {
1752
- const projectSettingsPath = import_node_path.default.join(projectPath, ".claude", "settings.json");
1789
+ const projectSettingsPath = import_node_path2.default.join(projectPath, ".claude", "settings.json");
1753
1790
  if (checkPath(projectSettingsPath)) return true;
1754
1791
  }
1755
1792
  return checkPath(this.settingsPath);
1756
1793
  }
1757
1794
  /** 将工具写入 settings.json permissions.allow(项目级或全局) */
1758
1795
  addToClaudeSettings(projectPath, toolName) {
1759
- const targetPath = projectPath ? import_node_path.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
1796
+ const targetPath = projectPath ? import_node_path2.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
1760
1797
  try {
1761
1798
  if (projectPath) {
1762
- const dir = import_node_path.default.dirname(targetPath);
1763
- if (!import_node_fs.default.existsSync(dir)) {
1764
- import_node_fs.default.mkdirSync(dir, { recursive: true });
1799
+ const dir = import_node_path2.default.dirname(targetPath);
1800
+ if (!import_node_fs2.default.existsSync(dir)) {
1801
+ import_node_fs2.default.mkdirSync(dir, { recursive: true });
1765
1802
  }
1766
1803
  }
1767
1804
  let settings = {};
1768
1805
  try {
1769
- settings = JSON.parse(import_node_fs.default.readFileSync(targetPath, "utf-8"));
1806
+ settings = JSON.parse(import_node_fs2.default.readFileSync(targetPath, "utf-8"));
1770
1807
  } catch {
1771
1808
  }
1772
1809
  if (!settings.permissions) {
@@ -1780,7 +1817,7 @@ var ApprovalProxy = class _ApprovalProxy {
1780
1817
  const entry = `${toolName}(*)`;
1781
1818
  if (!allow.includes(entry)) {
1782
1819
  allow.push(entry);
1783
- import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1820
+ import_node_fs2.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1784
1821
  const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
1785
1822
  console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
1786
1823
  }
@@ -2004,11 +2041,12 @@ var ApprovalProxy = class _ApprovalProxy {
2004
2041
 
2005
2042
  // src/mdns/MdnsService.ts
2006
2043
  var import_bonjour_service = __toESM(require("bonjour-service"));
2007
- var import_node_os3 = require("os");
2044
+ var import_node_os4 = require("os");
2008
2045
  function getLanAddresses() {
2009
2046
  const results = [];
2010
- for (const [name, addrs] of Object.entries((0, import_node_os3.networkInterfaces)())) {
2011
- if (name.startsWith("utun") || name.startsWith("lo")) continue;
2047
+ for (const [name, addrs] of Object.entries((0, import_node_os4.networkInterfaces)())) {
2048
+ if (name.startsWith("utun") || name === "lo") continue;
2049
+ if (isWindows && (name.startsWith("vEthernet") || name.includes("Loopback"))) continue;
2012
2050
  for (const addr of addrs ?? []) {
2013
2051
  if (addr.family === "IPv4" && !addr.internal) {
2014
2052
  results.push(addr.address);
@@ -2110,72 +2148,62 @@ var MdnsService = class {
2110
2148
 
2111
2149
  // src/hooks/HookInstaller.ts
2112
2150
  var import_promises2 = require("fs/promises");
2113
- var import_node_path2 = require("path");
2114
- var import_node_os4 = require("os");
2115
- var SESSIX_HOOKS_DIR = (0, import_node_path2.join)((0, import_node_os4.homedir)(), ".sessix", "hooks");
2116
- var HOOK_SCRIPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "approval-hook.sh");
2117
- var PERMISSION_ACCEPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "permission-accept.sh");
2118
- var CLAUDE_SETTINGS_PATH = (0, import_node_path2.join)((0, import_node_os4.homedir)(), ".claude", "settings.json");
2119
- var HOOK_COMMAND = "~/.sessix/hooks/approval-hook.sh";
2120
- var PERMISSION_ACCEPT_COMMAND = "~/.sessix/hooks/permission-accept.sh";
2121
- var HOOK_SCRIPT_TEMPLATE = `#!/bin/bash
2122
- # Sessix Approval Hook
2123
- # \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
2124
-
2125
- if [ -z "$SESSIX_SESSION_ID" ]; then
2126
- exit 0
2127
- fi
2128
-
2129
- # \u4ECE stdin \u8BFB\u53D6 hook payload
2130
- PAYLOAD=$(cat)
2131
-
2132
- # \u83B7\u53D6\u9879\u76EE\u8DEF\u5F84\uFF08\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09
2133
- PROJECT_PATH="$PWD"
2134
-
2135
- # \u53D1\u9001\u5BA1\u6279\u8BF7\u6C42\u5230 Sessix \u670D\u52A1\u5668\uFF08\u957F\u8F6E\u8BE2\uFF0C\u8D85\u65F6\u65F6\u95F4 > 300s\uFF09
2136
- RESPONSE=$(curl -s -X POST "http://localhost:3746/hook/approval" \\
2137
- -H "Content-Type: application/json" \\
2138
- -d "{\\"sessionId\\": \\"$SESSIX_SESSION_ID\\", \\"projectPath\\": \\"$PROJECT_PATH\\", \\"payload\\": $PAYLOAD}" \\
2139
- --max-time 320 \\
2140
- 2>/dev/null)
2141
-
2142
- if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
2143
- # \u5982\u679C Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C\uFF08exit 0 = \u6279\u51C6\uFF09
2144
- exit 0
2145
- fi
2151
+ var import_node_path3 = require("path");
2152
+ var import_node_os5 = require("os");
2153
+ var SESSIX_HOOKS_DIR = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".sessix", "hooks");
2154
+ var HOOK_SCRIPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
2155
+ var PERMISSION_ACCEPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
2156
+ var CLAUDE_SETTINGS_PATH = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude", "settings.json");
2157
+ var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
2158
+ var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
2159
+ var LEGACY_HOOK_COMMANDS = [
2160
+ "~/.sessix/hooks/approval-hook.sh",
2161
+ "~/.sessix/hooks/permission-accept.sh"
2162
+ ];
2163
+ var HOOK_SCRIPT_TEMPLATE = `#!/usr/bin/env node
2164
+ // Sessix Approval Hook
2165
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
2146
2166
 
2147
- # \u89E3\u6790\u670D\u52A1\u5668\u54CD\u5E94
2148
- DECISION=$(echo "$RESPONSE" | grep -o '\\"decision\\":\\"[^"]*\\"' | cut -d'"' -f4)
2167
+ const sessionId = process.env.SESSIX_SESSION_ID
2168
+ if (!sessionId) process.exit(0)
2149
2169
 
2150
- if [ "$DECISION" = "allow" ]; then
2151
- # \u7528\u6237\u6279\u51C6\u6216\u670D\u52A1\u5668\u8D85\u65F6\u81EA\u52A8\u6279\u51C6
2152
- exit 0
2153
- elif [ "$DECISION" = "deny" ]; then
2154
- # \u7528\u6237\u660E\u786E\u62D2\u7EDD
2155
- exit 1
2156
- else
2157
- # \u672A\u77E5\u54CD\u5E94\uFF0C\u9ED8\u8BA4\u653E\u884C
2158
- exit 0
2159
- fi
2170
+ let payload = ''
2171
+ process.stdin.on('data', (chunk) => { payload += chunk })
2172
+ process.stdin.on('end', async () => {
2173
+ try {
2174
+ const res = await fetch('http://localhost:3746/hook/approval', {
2175
+ method: 'POST',
2176
+ headers: { 'Content-Type': 'application/json' },
2177
+ body: JSON.stringify({
2178
+ sessionId,
2179
+ projectPath: process.cwd(),
2180
+ payload: JSON.parse(payload),
2181
+ }),
2182
+ signal: AbortSignal.timeout(320000),
2183
+ })
2184
+ const data = await res.json()
2185
+ process.exit(data.decision === 'deny' ? 1 : 0)
2186
+ } catch {
2187
+ // Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C
2188
+ process.exit(0)
2189
+ }
2190
+ })
2160
2191
  `;
2161
- var PERMISSION_ACCEPT_TEMPLATE = `#!/bin/bash
2162
- # Sessix PermissionRequest \u515C\u5E95
2163
- # \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
2192
+ var PERMISSION_ACCEPT_TEMPLATE = `#!/usr/bin/env node
2193
+ // Sessix PermissionRequest \u515C\u5E95
2194
+ // \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
2164
2195
 
2165
- if [ -z "$SESSIX_SESSION_ID" ]; then
2166
- exit 0
2167
- fi
2196
+ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
2168
2197
 
2169
- # \u8F93\u51FA JSON \u51B3\u7B56\uFF0C\u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42
2170
- echo '{"decision":"allow"}'
2171
- exit 0
2198
+ process.stdout.write('{"decision":"allow"}\\n')
2199
+ process.exit(0)
2172
2200
  `;
2173
2201
  var HookInstaller = class {
2174
2202
  /**
2175
2203
  * 安装 hook
2176
2204
  *
2177
2205
  * 1. 创建 ~/.sessix/hooks/ 目录
2178
- * 2. 写入 approval-hook.sh 脚本
2206
+ * 2. 写入 approval-hook.js 脚本
2179
2207
  * 3. 赋予执行权限
2180
2208
  * 4. 更新 Claude Code settings.json 添加 hook 配置
2181
2209
  */
@@ -2228,6 +2256,10 @@ var HookInstaller = class {
2228
2256
  async addHookToSettings() {
2229
2257
  let settings = await this.readClaudeSettings();
2230
2258
  let changed = false;
2259
+ for (const cmd of LEGACY_HOOK_COMMANDS) {
2260
+ this.removeHookCommand(settings, "PreToolUse", cmd);
2261
+ this.removeHookCommand(settings, "PermissionRequest", cmd);
2262
+ }
2231
2263
  if (!settings.hooks) {
2232
2264
  settings.hooks = {};
2233
2265
  }
@@ -2295,7 +2327,7 @@ var HookInstaller = class {
2295
2327
  * 写入 Claude Code settings.json
2296
2328
  */
2297
2329
  async writeClaudeSettings(settings) {
2298
- await (0, import_promises2.mkdir)((0, import_node_path2.join)((0, import_node_os4.homedir)(), ".claude"), { recursive: true });
2330
+ await (0, import_promises2.mkdir)((0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude"), { recursive: true });
2299
2331
  await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2300
2332
  }
2301
2333
  /**
@@ -2304,11 +2336,11 @@ var HookInstaller = class {
2304
2336
  hasHookConfig(settings) {
2305
2337
  return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
2306
2338
  }
2307
- /** 检查 PreToolUse 中是否有 approval-hook.sh */
2339
+ /** 检查 PreToolUse 中是否有 approval-hook.js */
2308
2340
  hasPreToolUseConfig(settings) {
2309
2341
  return this.hasHookEntry(settings?.hooks?.PreToolUse, HOOK_COMMAND);
2310
2342
  }
2311
- /** 检查 PermissionRequest 中是否有 permission-accept.sh */
2343
+ /** 检查 PermissionRequest 中是否有 permission-accept.js */
2312
2344
  hasPermissionRequestConfig(settings) {
2313
2345
  return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
2314
2346
  }
@@ -2322,7 +2354,7 @@ var HookInstaller = class {
2322
2354
  };
2323
2355
 
2324
2356
  // src/notification/NotificationService.ts
2325
- var import_node_path3 = require("path");
2357
+ var import_node_path4 = require("path");
2326
2358
  var NotificationService = class {
2327
2359
  constructor(sessionManager, expoChannel = null) {
2328
2360
  this.sessionManager = sessionManager;
@@ -2418,7 +2450,7 @@ var NotificationService = class {
2418
2450
  const dangerLevel = this.getDangerLevel(request.toolName);
2419
2451
  const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
2420
2452
  const categoryId = isDangerous ? "APPROVAL_DANGEROUS" : "APPROVAL_NORMAL";
2421
- const projectName = (0, import_node_path3.basename)(
2453
+ const projectName = (0, import_node_path4.basename)(
2422
2454
  this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId)?.projectPath ?? ""
2423
2455
  );
2424
2456
  const pushTitle = isDangerous ? `\u26A0\uFE0F ${title}` : title;
@@ -2474,7 +2506,7 @@ var NotificationService = class {
2474
2506
  /** 从审批请求中提取操作目标的简短描述 */
2475
2507
  extractTarget(request) {
2476
2508
  const input = request.toolInput;
2477
- if (input.file_path) return (0, import_node_path3.basename)(String(input.file_path));
2509
+ if (input.file_path) return (0, import_node_path4.basename)(String(input.file_path));
2478
2510
  if (input.command) return String(input.command).slice(0, 40);
2479
2511
  return request.description.slice(0, 40);
2480
2512
  }
@@ -2577,7 +2609,7 @@ var NotificationService = class {
2577
2609
  getSessionTitle(sessionId) {
2578
2610
  const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
2579
2611
  if (!session) return "Unknown";
2580
- return session.summary ?? (0, import_node_path3.basename)(session.projectPath);
2612
+ return session.summary ?? (0, import_node_path4.basename)(session.projectPath);
2581
2613
  }
2582
2614
  /** 获取会话的 YOLO 模式状态 */
2583
2615
  getYoloMode(sessionId) {
@@ -2585,9 +2617,9 @@ var NotificationService = class {
2585
2617
  }
2586
2618
  };
2587
2619
 
2588
- // src/notification/MacNotificationChannel.ts
2589
- var import_node_child_process = require("child_process");
2590
- var MacNotificationChannel = class {
2620
+ // src/notification/DesktopNotificationChannel.ts
2621
+ var import_node_child_process3 = require("child_process");
2622
+ var DesktopNotificationChannel = class {
2591
2623
  isAvailable() {
2592
2624
  return process.platform === "darwin";
2593
2625
  }
@@ -2598,9 +2630,9 @@ var MacNotificationChannel = class {
2598
2630
  const sound = payload.sound ?? "Ping";
2599
2631
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
2600
2632
  return new Promise((resolve) => {
2601
- (0, import_node_child_process.execFile)("osascript", ["-e", script], (err) => {
2633
+ (0, import_node_child_process3.execFile)("osascript", ["-e", script], (err) => {
2602
2634
  if (err) {
2603
- console.warn("[MacNotificationChannel] Send notification failed:", err.message);
2635
+ console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
2604
2636
  }
2605
2637
  resolve();
2606
2638
  });
@@ -3226,11 +3258,11 @@ var PairingManager = class {
3226
3258
  };
3227
3259
 
3228
3260
  // src/auth/AuthManager.ts
3261
+ var import_child_process2 = require("child_process");
3229
3262
  var import_child_process3 = require("child_process");
3230
- var import_child_process4 = require("child_process");
3231
3263
  var import_util = require("util");
3232
3264
  var import_events2 = require("events");
3233
- var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3265
+ var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3234
3266
  var CLAUDE_PATH2 = findClaudePath();
3235
3267
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3236
3268
  var AuthManager = class extends import_events2.EventEmitter {
@@ -3261,7 +3293,7 @@ var AuthManager = class extends import_events2.EventEmitter {
3261
3293
  }
3262
3294
  this.clearLoginTimeout();
3263
3295
  this.urlSent = false;
3264
- const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3296
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3265
3297
  env: { ...process.env, BROWSER: "echo" },
3266
3298
  stdio: ["pipe", "pipe", "pipe"]
3267
3299
  });
@@ -3347,15 +3379,31 @@ var AuthManager = class extends import_events2.EventEmitter {
3347
3379
  var import_promises5 = require("fs/promises");
3348
3380
  var WS_PORT = 3745;
3349
3381
  var HTTP_PORT = 3746;
3350
- var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
3382
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
3351
3383
  async function killPortProcess(port) {
3352
3384
  try {
3353
- const { stdout } = await execAsync(`lsof -ti :${port}`);
3354
- const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
3355
- if (pids.length > 0) {
3356
- await execAsync(`kill -9 ${pids.join(" ")}`);
3357
- await new Promise((resolve) => setTimeout(resolve, 600));
3385
+ if (isWindows) {
3386
+ const { stdout } = await execAsync(
3387
+ `netstat -ano | findstr :${port} | findstr LISTENING`
3388
+ );
3389
+ const pids = /* @__PURE__ */ new Set();
3390
+ for (const line of stdout.trim().split("\n")) {
3391
+ const parts = line.trim().split(/\s+/);
3392
+ const pid = parts[parts.length - 1];
3393
+ if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
3394
+ }
3395
+ for (const pid of pids) {
3396
+ await execAsync(`taskkill /PID ${pid} /F`).catch(() => {
3397
+ });
3398
+ }
3399
+ } else {
3400
+ const { stdout } = await execAsync(`lsof -ti :${port}`);
3401
+ const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
3402
+ if (pids.length > 0) {
3403
+ await execAsync(`kill -9 ${pids.join(" ")}`);
3404
+ }
3358
3405
  }
3406
+ await new Promise((resolve) => setTimeout(resolve, 600));
3359
3407
  } catch {
3360
3408
  }
3361
3409
  }
@@ -3373,8 +3421,8 @@ async function createWithRetry(label, port, factory) {
3373
3421
  }
3374
3422
  }
3375
3423
  async function start(opts = {}) {
3376
- const configDir = (0, import_node_path4.join)((0, import_node_os5.homedir)(), ".sessix");
3377
- const tokenFile = (0, import_node_path4.join)(configDir, "token");
3424
+ const configDir = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".sessix");
3425
+ const tokenFile = (0, import_node_path5.join)(configDir, "token");
3378
3426
  let token;
3379
3427
  if (opts.token !== void 0) {
3380
3428
  token = opts.token;
@@ -3397,7 +3445,7 @@ async function start(opts = {}) {
3397
3445
  const expoChannel = new ExpoNotificationChannel();
3398
3446
  const notificationService = new NotificationService(sessionManager, expoChannel);
3399
3447
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
3400
- notificationService.addChannel("mac", new MacNotificationChannel(), opts.enableMacNotification !== false);
3448
+ notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
3401
3449
  if (opts.activityPush) {
3402
3450
  try {
3403
3451
  const activityChannel = new ActivityPushChannel(opts.activityPush);
@@ -3424,7 +3472,7 @@ async function start(opts = {}) {
3424
3472
  let mdnsService = null;
3425
3473
  const pairingManager = new PairingManager({
3426
3474
  token,
3427
- serverName: (0, import_node_os5.hostname)(),
3475
+ serverName: (0, import_node_os6.hostname)(),
3428
3476
  version: "0.2.0",
3429
3477
  onStateChange: (state) => mdnsService?.updatePairingState(state)
3430
3478
  });
@@ -3794,12 +3842,17 @@ async function start(opts = {}) {
3794
3842
  }));
3795
3843
  const startMdns = () => {
3796
3844
  if (mdnsService) return;
3797
- mdnsService = new MdnsService({
3798
- wsPort: WS_PORT,
3799
- httpPort: HTTP_PORT,
3800
- pairing: pairingManager.state
3801
- });
3802
- mdnsService.start();
3845
+ try {
3846
+ mdnsService = new MdnsService({
3847
+ wsPort: WS_PORT,
3848
+ httpPort: HTTP_PORT,
3849
+ pairing: pairingManager.state
3850
+ });
3851
+ mdnsService.start();
3852
+ } catch (err) {
3853
+ console.warn(`[Server] mDNS failed to start (non-fatal): ${err.message}`);
3854
+ mdnsService = null;
3855
+ }
3803
3856
  };
3804
3857
  const stopMdns = () => {
3805
3858
  if (!mdnsService) return;
@@ -3876,7 +3929,7 @@ async function start(opts = {}) {
3876
3929
  var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
3877
3930
  function getPackageVersion() {
3878
3931
  try {
3879
- const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf8"));
3932
+ const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf8"));
3880
3933
  return pkg.version ?? "0.0.0";
3881
3934
  } catch {
3882
3935
  return "0.0.0";
@@ -3978,10 +4031,11 @@ ${t("startup.pairingReopened")}`);
3978
4031
  } else {
3979
4032
  process.on("SIGINT", () => shutdown("SIGINT"));
3980
4033
  process.on("SIGTERM", () => shutdown("SIGTERM"));
4034
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
3981
4035
  }
3982
4036
  }
3983
4037
  function getLocalIp() {
3984
- const interfaces = (0, import_node_os6.networkInterfaces)();
4038
+ const interfaces = (0, import_node_os7.networkInterfaces)();
3985
4039
  for (const iface of Object.values(interfaces)) {
3986
4040
  for (const addr of iface ?? []) {
3987
4041
  if (addr.family === "IPv4" && !addr.internal) {
@@ -3989,7 +4043,7 @@ function getLocalIp() {
3989
4043
  }
3990
4044
  }
3991
4045
  }
3992
- return "<your-mac-ip>";
4046
+ return "<your-ip>";
3993
4047
  }
3994
4048
  function buildQrUrl(ip, wsPort, token) {
3995
4049
  const base = `sessix://${ip}:${wsPort}`;
package/dist/server.js CHANGED
@@ -295,38 +295,87 @@ function t(key, params) {
295
295
  // src/server.ts
296
296
  var import_uuid4 = require("uuid");
297
297
  var import_promises4 = require("fs/promises");
298
- var import_node_os5 = require("os");
299
- var import_node_path4 = require("path");
300
- var import_node_child_process2 = require("child_process");
298
+ var import_node_os6 = require("os");
299
+ var import_node_path5 = require("path");
300
+ var import_node_child_process4 = require("child_process");
301
301
  var import_node_util = require("util");
302
302
 
303
303
  // src/providers/ProcessProvider.ts
304
- var import_child_process2 = require("child_process");
304
+ var import_child_process = require("child_process");
305
305
  var import_readline = require("readline");
306
306
  var import_events = require("events");
307
- var import_node_os = require("os");
307
+ var import_node_os2 = require("os");
308
308
  var import_uuid = require("uuid");
309
309
 
310
310
  // src/utils/claudePath.ts
311
- var import_child_process = require("child_process");
311
+ var import_node_child_process2 = require("child_process");
312
+ var import_node_fs = require("fs");
313
+ var import_node_path = require("path");
314
+ var import_node_os = require("os");
315
+
316
+ // src/utils/platform.ts
317
+ var import_node_child_process = require("child_process");
318
+ var isWindows = process.platform === "win32";
319
+ function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
320
+ return new Promise((resolve) => {
321
+ if (proc.exitCode !== null || proc.signalCode !== null) {
322
+ resolve();
323
+ return;
324
+ }
325
+ const onExit = () => {
326
+ clearTimeout(timer);
327
+ resolve();
328
+ };
329
+ proc.once("exit", onExit);
330
+ if (isWindows) {
331
+ if (proc.pid) {
332
+ (0, import_node_child_process.spawn)("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
333
+ }
334
+ } else {
335
+ proc.kill("SIGTERM");
336
+ }
337
+ const timer = setTimeout(() => {
338
+ if (proc.exitCode === null && proc.signalCode === null) {
339
+ if (!isWindows) {
340
+ proc.kill("SIGKILL");
341
+ }
342
+ }
343
+ resolve();
344
+ }, timeoutMs);
345
+ });
346
+ }
347
+ function isNormalExit(code, signal) {
348
+ if (code === 0) return true;
349
+ if (isWindows) {
350
+ return code === 1;
351
+ }
352
+ return code === 143 || signal === "SIGTERM";
353
+ }
354
+
355
+ // src/utils/claudePath.ts
312
356
  function findClaudePath() {
313
357
  try {
314
- return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
358
+ const cmd = isWindows ? "where claude" : "which claude";
359
+ return (0, import_node_child_process2.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
315
360
  } catch {
316
- const candidates = [
317
- `${process.env.HOME}/.local/bin/claude`,
318
- "/usr/local/bin/claude",
319
- "/opt/homebrew/bin/claude"
320
- ];
321
- for (const candidate of candidates) {
322
- try {
323
- (0, import_child_process.execSync)(`test -x "${candidate}"`);
324
- return candidate;
325
- } catch {
326
- }
361
+ }
362
+ const candidates = isWindows ? [
363
+ (0, import_node_path.join)(process.env.LOCALAPPDATA ?? "", "Programs", "claude", "claude.exe"),
364
+ (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local", "Programs", "claude", "claude.exe"),
365
+ (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "local", "claude.exe")
366
+ ] : [
367
+ (0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "bin", "claude"),
368
+ "/usr/local/bin/claude",
369
+ "/opt/homebrew/bin/claude"
370
+ ];
371
+ for (const candidate of candidates) {
372
+ try {
373
+ (0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
374
+ return candidate;
375
+ } catch {
327
376
  }
328
- return "claude";
329
377
  }
378
+ return "claude";
330
379
  }
331
380
 
332
381
  // src/providers/ProcessProvider.ts
@@ -399,19 +448,7 @@ var ProcessProvider = class {
399
448
  entry.process.stdin?.end();
400
449
  } catch {
401
450
  }
402
- entry.process.kill("SIGTERM");
403
- await new Promise((resolve) => {
404
- const timeout = setTimeout(() => {
405
- if (entry.process.exitCode === null && entry.process.signalCode === null) {
406
- entry.process.kill("SIGKILL");
407
- }
408
- resolve();
409
- }, 3e3);
410
- entry.process.once("exit", () => {
411
- clearTimeout(timeout);
412
- resolve();
413
- });
414
- });
451
+ await killProcessCrossPlatform(entry.process);
415
452
  }
416
453
  this.emittedQuestionToolUseIds.delete(sessionId);
417
454
  this.activeSessions.delete(sessionId);
@@ -441,7 +478,7 @@ var ProcessProvider = class {
441
478
  entry.process.stdin?.end();
442
479
  } catch {
443
480
  }
444
- entry.process.kill("SIGTERM");
481
+ killProcessCrossPlatform(entry.process);
445
482
  }
446
483
  } else {
447
484
  console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
@@ -523,7 +560,7 @@ var ProcessProvider = class {
523
560
  }
524
561
  const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
525
562
  delete env.CLAUDECODE;
526
- const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
563
+ const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
527
564
  cwd: projectPath,
528
565
  env,
529
566
  stdio: ["pipe", "pipe", "pipe"]
@@ -669,7 +706,7 @@ var ProcessProvider = class {
669
706
  entry.session.lastActiveAt = Date.now();
670
707
  const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
671
708
  if (alreadyHasResult) return;
672
- const isNormal = code === 0 || code === 143 || signal === "SIGTERM";
709
+ const isNormal = isNormalExit(code, signal);
673
710
  entry.session.status = isNormal ? "idle" : "error";
674
711
  if (!isNormal) {
675
712
  console.error(
@@ -735,8 +772,8 @@ ${context}`;
735
772
  return new Promise((resolve, reject) => {
736
773
  const env = { ...process.env };
737
774
  delete env.CLAUDECODE;
738
- const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
739
- cwd: (0, import_node_os.homedir)(),
775
+ const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
776
+ cwd: (0, import_node_os2.homedir)(),
740
777
  env,
741
778
  stdio: ["pipe", "pipe", "pipe"]
742
779
  });
@@ -1628,15 +1665,15 @@ var WsBridge = class _WsBridge {
1628
1665
 
1629
1666
  // src/approval/ApprovalProxy.ts
1630
1667
  var import_node_http = __toESM(require("http"));
1631
- var import_node_fs = __toESM(require("fs"));
1632
- var import_node_path = __toESM(require("path"));
1633
- var import_node_os2 = __toESM(require("os"));
1668
+ var import_node_fs2 = __toESM(require("fs"));
1669
+ var import_node_path2 = __toESM(require("path"));
1670
+ var import_node_os3 = __toESM(require("os"));
1634
1671
  var import_uuid3 = require("uuid");
1635
1672
  var ApprovalProxy = class _ApprovalProxy {
1636
1673
  server;
1637
1674
  token;
1638
1675
  port;
1639
- settingsPath = import_node_path.default.join(import_node_os2.default.homedir(), ".claude", "settings.json");
1676
+ settingsPath = import_node_path2.default.join(import_node_os3.default.homedir(), ".claude", "settings.json");
1640
1677
  /** 待处理的审批请求:requestId -> { resolve, timer, request } */
1641
1678
  pendingApprovals = /* @__PURE__ */ new Map();
1642
1679
  /** 审批请求回调(通知外部推送到手机) */
@@ -1741,7 +1778,7 @@ var ApprovalProxy = class _ApprovalProxy {
1741
1778
  isToolInClaudeSettings(toolName, projectPath) {
1742
1779
  const checkPath = (filepath) => {
1743
1780
  try {
1744
- const raw = import_node_fs.default.readFileSync(filepath, "utf-8");
1781
+ const raw = import_node_fs2.default.readFileSync(filepath, "utf-8");
1745
1782
  const settings = JSON.parse(raw);
1746
1783
  const allow = settings?.permissions?.allow ?? [];
1747
1784
  return allow.some((entry) => {
@@ -1755,24 +1792,24 @@ var ApprovalProxy = class _ApprovalProxy {
1755
1792
  }
1756
1793
  };
1757
1794
  if (projectPath) {
1758
- const projectSettingsPath = import_node_path.default.join(projectPath, ".claude", "settings.json");
1795
+ const projectSettingsPath = import_node_path2.default.join(projectPath, ".claude", "settings.json");
1759
1796
  if (checkPath(projectSettingsPath)) return true;
1760
1797
  }
1761
1798
  return checkPath(this.settingsPath);
1762
1799
  }
1763
1800
  /** 将工具写入 settings.json permissions.allow(项目级或全局) */
1764
1801
  addToClaudeSettings(projectPath, toolName) {
1765
- const targetPath = projectPath ? import_node_path.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
1802
+ const targetPath = projectPath ? import_node_path2.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
1766
1803
  try {
1767
1804
  if (projectPath) {
1768
- const dir = import_node_path.default.dirname(targetPath);
1769
- if (!import_node_fs.default.existsSync(dir)) {
1770
- import_node_fs.default.mkdirSync(dir, { recursive: true });
1805
+ const dir = import_node_path2.default.dirname(targetPath);
1806
+ if (!import_node_fs2.default.existsSync(dir)) {
1807
+ import_node_fs2.default.mkdirSync(dir, { recursive: true });
1771
1808
  }
1772
1809
  }
1773
1810
  let settings = {};
1774
1811
  try {
1775
- settings = JSON.parse(import_node_fs.default.readFileSync(targetPath, "utf-8"));
1812
+ settings = JSON.parse(import_node_fs2.default.readFileSync(targetPath, "utf-8"));
1776
1813
  } catch {
1777
1814
  }
1778
1815
  if (!settings.permissions) {
@@ -1786,7 +1823,7 @@ var ApprovalProxy = class _ApprovalProxy {
1786
1823
  const entry = `${toolName}(*)`;
1787
1824
  if (!allow.includes(entry)) {
1788
1825
  allow.push(entry);
1789
- import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1826
+ import_node_fs2.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1790
1827
  const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
1791
1828
  console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
1792
1829
  }
@@ -2010,11 +2047,12 @@ var ApprovalProxy = class _ApprovalProxy {
2010
2047
 
2011
2048
  // src/mdns/MdnsService.ts
2012
2049
  var import_bonjour_service = __toESM(require("bonjour-service"));
2013
- var import_node_os3 = require("os");
2050
+ var import_node_os4 = require("os");
2014
2051
  function getLanAddresses() {
2015
2052
  const results = [];
2016
- for (const [name, addrs] of Object.entries((0, import_node_os3.networkInterfaces)())) {
2017
- if (name.startsWith("utun") || name.startsWith("lo")) continue;
2053
+ for (const [name, addrs] of Object.entries((0, import_node_os4.networkInterfaces)())) {
2054
+ if (name.startsWith("utun") || name === "lo") continue;
2055
+ if (isWindows && (name.startsWith("vEthernet") || name.includes("Loopback"))) continue;
2018
2056
  for (const addr of addrs ?? []) {
2019
2057
  if (addr.family === "IPv4" && !addr.internal) {
2020
2058
  results.push(addr.address);
@@ -2116,72 +2154,62 @@ var MdnsService = class {
2116
2154
 
2117
2155
  // src/hooks/HookInstaller.ts
2118
2156
  var import_promises2 = require("fs/promises");
2119
- var import_node_path2 = require("path");
2120
- var import_node_os4 = require("os");
2121
- var SESSIX_HOOKS_DIR = (0, import_node_path2.join)((0, import_node_os4.homedir)(), ".sessix", "hooks");
2122
- var HOOK_SCRIPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "approval-hook.sh");
2123
- var PERMISSION_ACCEPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "permission-accept.sh");
2124
- var CLAUDE_SETTINGS_PATH = (0, import_node_path2.join)((0, import_node_os4.homedir)(), ".claude", "settings.json");
2125
- var HOOK_COMMAND = "~/.sessix/hooks/approval-hook.sh";
2126
- var PERMISSION_ACCEPT_COMMAND = "~/.sessix/hooks/permission-accept.sh";
2127
- var HOOK_SCRIPT_TEMPLATE = `#!/bin/bash
2128
- # Sessix Approval Hook
2129
- # \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
2130
-
2131
- if [ -z "$SESSIX_SESSION_ID" ]; then
2132
- exit 0
2133
- fi
2134
-
2135
- # \u4ECE stdin \u8BFB\u53D6 hook payload
2136
- PAYLOAD=$(cat)
2137
-
2138
- # \u83B7\u53D6\u9879\u76EE\u8DEF\u5F84\uFF08\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09
2139
- PROJECT_PATH="$PWD"
2140
-
2141
- # \u53D1\u9001\u5BA1\u6279\u8BF7\u6C42\u5230 Sessix \u670D\u52A1\u5668\uFF08\u957F\u8F6E\u8BE2\uFF0C\u8D85\u65F6\u65F6\u95F4 > 300s\uFF09
2142
- RESPONSE=$(curl -s -X POST "http://localhost:3746/hook/approval" \\
2143
- -H "Content-Type: application/json" \\
2144
- -d "{\\"sessionId\\": \\"$SESSIX_SESSION_ID\\", \\"projectPath\\": \\"$PROJECT_PATH\\", \\"payload\\": $PAYLOAD}" \\
2145
- --max-time 320 \\
2146
- 2>/dev/null)
2147
-
2148
- if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
2149
- # \u5982\u679C Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C\uFF08exit 0 = \u6279\u51C6\uFF09
2150
- exit 0
2151
- fi
2157
+ var import_node_path3 = require("path");
2158
+ var import_node_os5 = require("os");
2159
+ var SESSIX_HOOKS_DIR = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".sessix", "hooks");
2160
+ var HOOK_SCRIPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
2161
+ var PERMISSION_ACCEPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
2162
+ var CLAUDE_SETTINGS_PATH = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude", "settings.json");
2163
+ var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
2164
+ var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
2165
+ var LEGACY_HOOK_COMMANDS = [
2166
+ "~/.sessix/hooks/approval-hook.sh",
2167
+ "~/.sessix/hooks/permission-accept.sh"
2168
+ ];
2169
+ var HOOK_SCRIPT_TEMPLATE = `#!/usr/bin/env node
2170
+ // Sessix Approval Hook
2171
+ // \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
2152
2172
 
2153
- # \u89E3\u6790\u670D\u52A1\u5668\u54CD\u5E94
2154
- DECISION=$(echo "$RESPONSE" | grep -o '\\"decision\\":\\"[^"]*\\"' | cut -d'"' -f4)
2173
+ const sessionId = process.env.SESSIX_SESSION_ID
2174
+ if (!sessionId) process.exit(0)
2155
2175
 
2156
- if [ "$DECISION" = "allow" ]; then
2157
- # \u7528\u6237\u6279\u51C6\u6216\u670D\u52A1\u5668\u8D85\u65F6\u81EA\u52A8\u6279\u51C6
2158
- exit 0
2159
- elif [ "$DECISION" = "deny" ]; then
2160
- # \u7528\u6237\u660E\u786E\u62D2\u7EDD
2161
- exit 1
2162
- else
2163
- # \u672A\u77E5\u54CD\u5E94\uFF0C\u9ED8\u8BA4\u653E\u884C
2164
- exit 0
2165
- fi
2176
+ let payload = ''
2177
+ process.stdin.on('data', (chunk) => { payload += chunk })
2178
+ process.stdin.on('end', async () => {
2179
+ try {
2180
+ const res = await fetch('http://localhost:3746/hook/approval', {
2181
+ method: 'POST',
2182
+ headers: { 'Content-Type': 'application/json' },
2183
+ body: JSON.stringify({
2184
+ sessionId,
2185
+ projectPath: process.cwd(),
2186
+ payload: JSON.parse(payload),
2187
+ }),
2188
+ signal: AbortSignal.timeout(320000),
2189
+ })
2190
+ const data = await res.json()
2191
+ process.exit(data.decision === 'deny' ? 1 : 0)
2192
+ } catch {
2193
+ // Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C
2194
+ process.exit(0)
2195
+ }
2196
+ })
2166
2197
  `;
2167
- var PERMISSION_ACCEPT_TEMPLATE = `#!/bin/bash
2168
- # Sessix PermissionRequest \u515C\u5E95
2169
- # \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
2198
+ var PERMISSION_ACCEPT_TEMPLATE = `#!/usr/bin/env node
2199
+ // Sessix PermissionRequest \u515C\u5E95
2200
+ // \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
2170
2201
 
2171
- if [ -z "$SESSIX_SESSION_ID" ]; then
2172
- exit 0
2173
- fi
2202
+ if (!process.env.SESSIX_SESSION_ID) process.exit(0)
2174
2203
 
2175
- # \u8F93\u51FA JSON \u51B3\u7B56\uFF0C\u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42
2176
- echo '{"decision":"allow"}'
2177
- exit 0
2204
+ process.stdout.write('{"decision":"allow"}\\n')
2205
+ process.exit(0)
2178
2206
  `;
2179
2207
  var HookInstaller = class {
2180
2208
  /**
2181
2209
  * 安装 hook
2182
2210
  *
2183
2211
  * 1. 创建 ~/.sessix/hooks/ 目录
2184
- * 2. 写入 approval-hook.sh 脚本
2212
+ * 2. 写入 approval-hook.js 脚本
2185
2213
  * 3. 赋予执行权限
2186
2214
  * 4. 更新 Claude Code settings.json 添加 hook 配置
2187
2215
  */
@@ -2234,6 +2262,10 @@ var HookInstaller = class {
2234
2262
  async addHookToSettings() {
2235
2263
  let settings = await this.readClaudeSettings();
2236
2264
  let changed = false;
2265
+ for (const cmd of LEGACY_HOOK_COMMANDS) {
2266
+ this.removeHookCommand(settings, "PreToolUse", cmd);
2267
+ this.removeHookCommand(settings, "PermissionRequest", cmd);
2268
+ }
2237
2269
  if (!settings.hooks) {
2238
2270
  settings.hooks = {};
2239
2271
  }
@@ -2301,7 +2333,7 @@ var HookInstaller = class {
2301
2333
  * 写入 Claude Code settings.json
2302
2334
  */
2303
2335
  async writeClaudeSettings(settings) {
2304
- await (0, import_promises2.mkdir)((0, import_node_path2.join)((0, import_node_os4.homedir)(), ".claude"), { recursive: true });
2336
+ await (0, import_promises2.mkdir)((0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude"), { recursive: true });
2305
2337
  await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2306
2338
  }
2307
2339
  /**
@@ -2310,11 +2342,11 @@ var HookInstaller = class {
2310
2342
  hasHookConfig(settings) {
2311
2343
  return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
2312
2344
  }
2313
- /** 检查 PreToolUse 中是否有 approval-hook.sh */
2345
+ /** 检查 PreToolUse 中是否有 approval-hook.js */
2314
2346
  hasPreToolUseConfig(settings) {
2315
2347
  return this.hasHookEntry(settings?.hooks?.PreToolUse, HOOK_COMMAND);
2316
2348
  }
2317
- /** 检查 PermissionRequest 中是否有 permission-accept.sh */
2349
+ /** 检查 PermissionRequest 中是否有 permission-accept.js */
2318
2350
  hasPermissionRequestConfig(settings) {
2319
2351
  return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
2320
2352
  }
@@ -2328,7 +2360,7 @@ var HookInstaller = class {
2328
2360
  };
2329
2361
 
2330
2362
  // src/notification/NotificationService.ts
2331
- var import_node_path3 = require("path");
2363
+ var import_node_path4 = require("path");
2332
2364
  var NotificationService = class {
2333
2365
  constructor(sessionManager, expoChannel = null) {
2334
2366
  this.sessionManager = sessionManager;
@@ -2424,7 +2456,7 @@ var NotificationService = class {
2424
2456
  const dangerLevel = this.getDangerLevel(request.toolName);
2425
2457
  const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
2426
2458
  const categoryId = isDangerous ? "APPROVAL_DANGEROUS" : "APPROVAL_NORMAL";
2427
- const projectName = (0, import_node_path3.basename)(
2459
+ const projectName = (0, import_node_path4.basename)(
2428
2460
  this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId)?.projectPath ?? ""
2429
2461
  );
2430
2462
  const pushTitle = isDangerous ? `\u26A0\uFE0F ${title}` : title;
@@ -2480,7 +2512,7 @@ var NotificationService = class {
2480
2512
  /** 从审批请求中提取操作目标的简短描述 */
2481
2513
  extractTarget(request) {
2482
2514
  const input = request.toolInput;
2483
- if (input.file_path) return (0, import_node_path3.basename)(String(input.file_path));
2515
+ if (input.file_path) return (0, import_node_path4.basename)(String(input.file_path));
2484
2516
  if (input.command) return String(input.command).slice(0, 40);
2485
2517
  return request.description.slice(0, 40);
2486
2518
  }
@@ -2583,7 +2615,7 @@ var NotificationService = class {
2583
2615
  getSessionTitle(sessionId) {
2584
2616
  const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
2585
2617
  if (!session) return "Unknown";
2586
- return session.summary ?? (0, import_node_path3.basename)(session.projectPath);
2618
+ return session.summary ?? (0, import_node_path4.basename)(session.projectPath);
2587
2619
  }
2588
2620
  /** 获取会话的 YOLO 模式状态 */
2589
2621
  getYoloMode(sessionId) {
@@ -2591,9 +2623,9 @@ var NotificationService = class {
2591
2623
  }
2592
2624
  };
2593
2625
 
2594
- // src/notification/MacNotificationChannel.ts
2595
- var import_node_child_process = require("child_process");
2596
- var MacNotificationChannel = class {
2626
+ // src/notification/DesktopNotificationChannel.ts
2627
+ var import_node_child_process3 = require("child_process");
2628
+ var DesktopNotificationChannel = class {
2597
2629
  isAvailable() {
2598
2630
  return process.platform === "darwin";
2599
2631
  }
@@ -2604,9 +2636,9 @@ var MacNotificationChannel = class {
2604
2636
  const sound = payload.sound ?? "Ping";
2605
2637
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
2606
2638
  return new Promise((resolve) => {
2607
- (0, import_node_child_process.execFile)("osascript", ["-e", script], (err) => {
2639
+ (0, import_node_child_process3.execFile)("osascript", ["-e", script], (err) => {
2608
2640
  if (err) {
2609
- console.warn("[MacNotificationChannel] Send notification failed:", err.message);
2641
+ console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
2610
2642
  }
2611
2643
  resolve();
2612
2644
  });
@@ -3232,11 +3264,11 @@ var PairingManager = class {
3232
3264
  };
3233
3265
 
3234
3266
  // src/auth/AuthManager.ts
3267
+ var import_child_process2 = require("child_process");
3235
3268
  var import_child_process3 = require("child_process");
3236
- var import_child_process4 = require("child_process");
3237
3269
  var import_util = require("util");
3238
3270
  var import_events2 = require("events");
3239
- var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3271
+ var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3240
3272
  var CLAUDE_PATH2 = findClaudePath();
3241
3273
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3242
3274
  var AuthManager = class extends import_events2.EventEmitter {
@@ -3267,7 +3299,7 @@ var AuthManager = class extends import_events2.EventEmitter {
3267
3299
  }
3268
3300
  this.clearLoginTimeout();
3269
3301
  this.urlSent = false;
3270
- const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3302
+ const proc = (0, import_child_process2.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3271
3303
  env: { ...process.env, BROWSER: "echo" },
3272
3304
  stdio: ["pipe", "pipe", "pipe"]
3273
3305
  });
@@ -3353,15 +3385,31 @@ var AuthManager = class extends import_events2.EventEmitter {
3353
3385
  var import_promises5 = require("fs/promises");
3354
3386
  var WS_PORT = 3745;
3355
3387
  var HTTP_PORT = 3746;
3356
- var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
3388
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
3357
3389
  async function killPortProcess(port) {
3358
3390
  try {
3359
- const { stdout } = await execAsync(`lsof -ti :${port}`);
3360
- const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
3361
- if (pids.length > 0) {
3362
- await execAsync(`kill -9 ${pids.join(" ")}`);
3363
- await new Promise((resolve) => setTimeout(resolve, 600));
3391
+ if (isWindows) {
3392
+ const { stdout } = await execAsync(
3393
+ `netstat -ano | findstr :${port} | findstr LISTENING`
3394
+ );
3395
+ const pids = /* @__PURE__ */ new Set();
3396
+ for (const line of stdout.trim().split("\n")) {
3397
+ const parts = line.trim().split(/\s+/);
3398
+ const pid = parts[parts.length - 1];
3399
+ if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
3400
+ }
3401
+ for (const pid of pids) {
3402
+ await execAsync(`taskkill /PID ${pid} /F`).catch(() => {
3403
+ });
3404
+ }
3405
+ } else {
3406
+ const { stdout } = await execAsync(`lsof -ti :${port}`);
3407
+ const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
3408
+ if (pids.length > 0) {
3409
+ await execAsync(`kill -9 ${pids.join(" ")}`);
3410
+ }
3364
3411
  }
3412
+ await new Promise((resolve) => setTimeout(resolve, 600));
3365
3413
  } catch {
3366
3414
  }
3367
3415
  }
@@ -3379,8 +3427,8 @@ async function createWithRetry(label, port, factory) {
3379
3427
  }
3380
3428
  }
3381
3429
  async function start(opts = {}) {
3382
- const configDir = (0, import_node_path4.join)((0, import_node_os5.homedir)(), ".sessix");
3383
- const tokenFile = (0, import_node_path4.join)(configDir, "token");
3430
+ const configDir = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".sessix");
3431
+ const tokenFile = (0, import_node_path5.join)(configDir, "token");
3384
3432
  let token;
3385
3433
  if (opts.token !== void 0) {
3386
3434
  token = opts.token;
@@ -3403,7 +3451,7 @@ async function start(opts = {}) {
3403
3451
  const expoChannel = new ExpoNotificationChannel();
3404
3452
  const notificationService = new NotificationService(sessionManager, expoChannel);
3405
3453
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
3406
- notificationService.addChannel("mac", new MacNotificationChannel(), opts.enableMacNotification !== false);
3454
+ notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
3407
3455
  if (opts.activityPush) {
3408
3456
  try {
3409
3457
  const activityChannel = new ActivityPushChannel(opts.activityPush);
@@ -3430,7 +3478,7 @@ async function start(opts = {}) {
3430
3478
  let mdnsService = null;
3431
3479
  const pairingManager = new PairingManager({
3432
3480
  token,
3433
- serverName: (0, import_node_os5.hostname)(),
3481
+ serverName: (0, import_node_os6.hostname)(),
3434
3482
  version: "0.2.0",
3435
3483
  onStateChange: (state) => mdnsService?.updatePairingState(state)
3436
3484
  });
@@ -3800,12 +3848,17 @@ async function start(opts = {}) {
3800
3848
  }));
3801
3849
  const startMdns = () => {
3802
3850
  if (mdnsService) return;
3803
- mdnsService = new MdnsService({
3804
- wsPort: WS_PORT,
3805
- httpPort: HTTP_PORT,
3806
- pairing: pairingManager.state
3807
- });
3808
- mdnsService.start();
3851
+ try {
3852
+ mdnsService = new MdnsService({
3853
+ wsPort: WS_PORT,
3854
+ httpPort: HTTP_PORT,
3855
+ pairing: pairingManager.state
3856
+ });
3857
+ mdnsService.start();
3858
+ } catch (err) {
3859
+ console.warn(`[Server] mDNS failed to start (non-fatal): ${err.message}`);
3860
+ mdnsService = null;
3861
+ }
3809
3862
  };
3810
3863
  const stopMdns = () => {
3811
3864
  if (!mdnsService) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },