coding-tool-x 3.3.4 → 3.3.5

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 (52) hide show
  1. package/package.json +2 -1
  2. package/src/commands/doctor.js +3 -3
  3. package/src/commands/export-config.js +2 -2
  4. package/src/commands/logs.js +42 -9
  5. package/src/commands/port-config.js +2 -2
  6. package/src/commands/switch.js +2 -2
  7. package/src/config/default.js +4 -1
  8. package/src/config/loader.js +5 -2
  9. package/src/config/paths.js +25 -21
  10. package/src/reset-config.js +3 -5
  11. package/src/server/api/agents.js +2 -3
  12. package/src/server/api/claude-hooks.js +67 -10
  13. package/src/server/api/opencode-sessions.js +2 -2
  14. package/src/server/api/pm2-autostart.js +20 -8
  15. package/src/server/api/proxy.js +2 -3
  16. package/src/server/api/sessions.js +6 -6
  17. package/src/server/index.js +5 -9
  18. package/src/server/services/agents-service.js +6 -3
  19. package/src/server/services/channels.js +6 -7
  20. package/src/server/services/codex-channels.js +4 -1
  21. package/src/server/services/codex-config.js +4 -1
  22. package/src/server/services/codex-settings-manager.js +18 -9
  23. package/src/server/services/commands-service.js +2 -2
  24. package/src/server/services/config-export-service.js +7 -6
  25. package/src/server/services/config-registry-service.js +7 -6
  26. package/src/server/services/config-sync-manager.js +2 -2
  27. package/src/server/services/config-sync-service.js +2 -2
  28. package/src/server/services/env-checker.js +2 -2
  29. package/src/server/services/favorites.js +3 -4
  30. package/src/server/services/gemini-channels.js +4 -4
  31. package/src/server/services/gemini-config.js +2 -2
  32. package/src/server/services/gemini-sessions.js +3 -3
  33. package/src/server/services/gemini-settings-manager.js +5 -5
  34. package/src/server/services/mcp-service.js +7 -4
  35. package/src/server/services/model-detector.js +2 -2
  36. package/src/server/services/opencode-channels.js +5 -5
  37. package/src/server/services/plugins-service.js +3 -4
  38. package/src/server/services/prompts-service.js +7 -4
  39. package/src/server/services/proxy-runtime.js +2 -2
  40. package/src/server/services/repo-scanner-base.js +2 -2
  41. package/src/server/services/request-logger.js +2 -2
  42. package/src/server/services/security-config.js +2 -2
  43. package/src/server/services/session-cache.js +2 -2
  44. package/src/server/services/session-converter.js +9 -4
  45. package/src/server/services/sessions.js +8 -5
  46. package/src/server/services/settings-manager.js +3 -4
  47. package/src/server/services/skill-service.js +5 -5
  48. package/src/server/services/statistics-service.js +2 -2
  49. package/src/server/services/ui-config.js +3 -4
  50. package/src/server/websocket-server.js +2 -2
  51. package/src/utils/home-dir.js +82 -0
  52. package/src/utils/port-helper.js +34 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.3.4",
3
+ "version": "3.3.5",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "test:basic": "node scripts/test-basic.js",
13
13
  "test:api": "node scripts/test-api-consistency.js",
14
14
  "test:codex-agents": "node scripts/test-codex-agents.js",
15
+ "test:windows": "node scripts/test-windows-regression.js",
15
16
  "benchmark:codex": "node scripts/benchmark-codex-loading.js",
16
17
  "build:web": "cd src/web && npm run build",
17
18
  "dev:web": "cd src/web && npm run dev",
@@ -1,10 +1,10 @@
1
1
  const chalk = require('chalk');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const os = require('os');
5
4
  const { exec } = require('child_process');
6
5
  const { promisify } = require('util');
7
6
  const { loadConfig, getConfigFilePath } = require('../config/loader');
7
+ const { PATHS, NATIVE_PATHS } = require('../config/paths');
8
8
  const { isPortInUse } = require('../utils/port-helper');
9
9
 
10
10
  const execAsync = promisify(exec);
@@ -185,7 +185,7 @@ async function checkPorts() {
185
185
  * 检查 Claude Code 配置
186
186
  */
187
187
  async function checkClaudeConfig() {
188
- const settingsPath = path.join(os.homedir(), '.claude/settings.json');
188
+ const settingsPath = NATIVE_PATHS.claude.settings;
189
189
  const exists = fs.existsSync(settingsPath);
190
190
 
191
191
  if (exists) {
@@ -208,7 +208,7 @@ async function checkClaudeConfig() {
208
208
  * 检查日志目录
209
209
  */
210
210
  async function checkLogsDirectory() {
211
- const logsDir = path.join(os.homedir(), '.cc-tool', 'logs');
211
+ const logsDir = PATHS.logs;
212
212
  const exists = fs.existsSync(logsDir);
213
213
 
214
214
  if (exists) {
@@ -1,8 +1,8 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const archiver = require('archiver');
5
4
  const chalk = require('chalk');
5
+ const { HOME_DIR } = require('../config/paths');
6
6
 
7
7
  /**
8
8
  * 导出 Claude Code 配置为 ZIP 压缩包
@@ -14,7 +14,7 @@ async function exportConfig(options = {}) {
14
14
  try {
15
15
  console.log(chalk.blue('🚀 开始导出 Claude Code 配置...'));
16
16
 
17
- const homeDir = os.homedir();
17
+ const homeDir = HOME_DIR;
18
18
  const claudeDir = path.join(homeDir, '.claude');
19
19
  const currentDir = process.cwd();
20
20
 
@@ -1,10 +1,10 @@
1
1
  const chalk = require('chalk');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const os = require('os');
5
4
  const { spawn } = require('child_process');
5
+ const { PATHS } = require('../config/paths');
6
6
 
7
- const LOGS_DIR = path.join(os.homedir(), '.cc-tool', 'logs');
7
+ const LOGS_DIR = PATHS.logs;
8
8
 
9
9
  const LOG_FILES = {
10
10
  ui: 'cc-tool-out.log',
@@ -145,29 +145,59 @@ function showLastLines(filePath, lines) {
145
145
  /**
146
146
  * 实时跟踪日志文件
147
147
  */
148
+ function buildFollowProcessSpec(filePath, runtimePlatform = process.platform) {
149
+ if (runtimePlatform === 'win32') {
150
+ return {
151
+ command: 'powershell',
152
+ args: [
153
+ '-NoProfile',
154
+ '-Command',
155
+ `Get-Content -Path '${String(filePath).replace(/'/g, "''")}' -Tail 50 -Wait`
156
+ ],
157
+ options: { windowsHide: true }
158
+ };
159
+ }
160
+
161
+ return {
162
+ command: 'tail',
163
+ args: ['-n', '50', '-f', filePath],
164
+ options: {}
165
+ };
166
+ }
167
+
148
168
  function tailFile(filePath) {
149
169
  console.log(chalk.gray('按 Ctrl+C 停止跟踪\n'));
150
170
 
151
- const tail = spawn('tail', ['-f', filePath]);
171
+ const followSpec = buildFollowProcessSpec(filePath);
172
+ const isWindows = followSpec.command.toLowerCase() === 'powershell';
173
+ const followProcess = spawn(followSpec.command, followSpec.args, followSpec.options);
152
174
 
153
- tail.stdout.on('data', (data) => {
175
+ followProcess.stdout.on('data', (data) => {
154
176
  process.stdout.write(data.toString());
155
177
  });
156
178
 
157
- tail.stderr.on('data', (data) => {
179
+ followProcess.stderr.on('data', (data) => {
158
180
  process.stderr.write(chalk.red(data.toString()));
159
181
  });
160
182
 
161
- tail.on('error', (err) => {
183
+ followProcess.on('error', (err) => {
162
184
  console.error(chalk.red(`\n❌ 跟踪日志失败: ${err.message}\n`));
185
+ if (isWindows) {
186
+ console.log(chalk.gray('提示: 请确认系统可用 powershell 命令。\n'));
187
+ }
163
188
  process.exit(1);
164
189
  });
165
190
 
166
191
  // 处理退出信号
167
- process.on('SIGINT', () => {
168
- tail.kill();
192
+ const handleSigint = () => {
193
+ followProcess.kill();
169
194
  console.log(chalk.gray('\n\n已停止跟踪日志\n'));
170
195
  process.exit(0);
196
+ };
197
+
198
+ process.once('SIGINT', handleSigint);
199
+ followProcess.once('close', () => {
200
+ process.removeListener('SIGINT', handleSigint);
171
201
  });
172
202
  }
173
203
 
@@ -257,5 +287,8 @@ function getTypeColor(type) {
257
287
  }
258
288
 
259
289
  module.exports = {
260
- handleLogs
290
+ handleLogs,
291
+ _test: {
292
+ buildFollowProcessSpec
293
+ }
261
294
  };
@@ -1,8 +1,8 @@
1
1
  // 端口配置命令
2
2
  const chalk = require('chalk');
3
3
  const inquirer = require('inquirer');
4
- const os = require('os');
5
4
  const { loadConfig, saveConfig } = require('../config/loader');
5
+ const { HOME_DIR } = require('../config/paths');
6
6
 
7
7
  /**
8
8
  * 配置端口
@@ -112,7 +112,7 @@ async function handlePortConfig() {
112
112
  // 保存配置(保留其余字段)
113
113
  saveConfig({
114
114
  ...config,
115
- projectsDir: config.projectsDir.replace(os.homedir(), '~'),
115
+ projectsDir: config.projectsDir.replace(HOME_DIR, '~'),
116
116
  ports: config.ports,
117
117
  });
118
118
 
@@ -1,9 +1,9 @@
1
1
  // 切换项目命令
2
2
  const chalk = require('chalk');
3
- const os = require('os');
4
3
  const { getAvailableProjects } = require('../utils/session');
5
4
  const { promptSelectProject } = require('../ui/prompts');
6
5
  const { saveConfig } = require('../config/loader');
6
+ const { HOME_DIR } = require('../config/paths');
7
7
 
8
8
  /**
9
9
  * 切换项目
@@ -31,7 +31,7 @@ async function switchProject(config) {
31
31
  // 保存到配置文件(保留其余字段)
32
32
  saveConfig({
33
33
  ...config,
34
- projectsDir: config.projectsDir.replace(os.homedir(), '~')
34
+ projectsDir: config.projectsDir.replace(HOME_DIR, '~')
35
35
  });
36
36
 
37
37
  // 使用解析后的名称显示
@@ -1,10 +1,13 @@
1
1
  // 默认配置
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const { resolvePreferredHomeDir } = require('../utils/home-dir');
4
5
  const modelMetadataConfig = require('./model-metadata.json');
5
6
 
7
+ const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
8
+
6
9
  const DEFAULT_CONFIG = {
7
- projectsDir: path.join(os.homedir(), '.claude', 'projects'),
10
+ projectsDir: path.join(HOME_DIR, '.claude', 'projects'),
8
11
  defaultProject: null,
9
12
  maxDisplaySessions: 100,
10
13
  pageSize: 15,
@@ -4,11 +4,14 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const DEFAULT_CONFIG = require('./default');
6
6
  const { PATHS, ensureStorageDirMigrated } = require('./paths');
7
+ const { resolvePreferredHomeDir } = require('../utils/home-dir');
7
8
  const eventBus = require('../plugins/event-bus');
8
9
 
10
+ const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
11
+
9
12
  const LEGACY_CONFIG_FILES = [
10
13
  path.join(__dirname, '../../config.json'),
11
- path.join(os.homedir(), '.claude', 'config.json')
14
+ path.join(HOME_DIR, '.claude', 'config.json')
12
15
  ];
13
16
 
14
17
  function getConfigFilePath() {
@@ -21,7 +24,7 @@ function getConfigFilePath() {
21
24
  */
22
25
  function expandHome(filepath) {
23
26
  if (filepath.startsWith('~')) {
24
- return path.join(os.homedir(), filepath.slice(1));
27
+ return path.join(HOME_DIR, filepath.slice(1));
25
28
  }
26
29
  return filepath;
27
30
  }
@@ -3,16 +3,19 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const { resolvePreferredHomeDir } = require('../utils/home-dir');
7
+
8
+ const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
6
9
 
7
10
  // 基础目录
8
- const CC_TOOL_BASE_DIR = path.join(os.homedir(), '.cc-tool');
11
+ const CC_TOOL_BASE_DIR = path.join(HOME_DIR, '.cc-tool');
9
12
  // 兼容旧变量名,避免外部调用方断裂
10
13
  const CTX_BASE_DIR = CC_TOOL_BASE_DIR;
11
14
 
12
15
  // 旧目录(升级时自动合并到 ~/.cc-tool)
13
16
  const LEGACY_BASE_DIRS = [
14
- path.join(os.homedir(), '.claude', 'ctx'),
15
- path.join(os.homedir(), '.claude', 'cc-tool')
17
+ path.join(HOME_DIR, '.claude', 'ctx'),
18
+ path.join(HOME_DIR, '.claude', 'cc-tool')
16
19
  ];
17
20
 
18
21
  let migrationChecked = false;
@@ -127,7 +130,7 @@ const PATHS = {
127
130
  notifyHook: path.join(CC_TOOL_BASE_DIR, 'notify-hook.js'),
128
131
 
129
132
  // Skills 安装目录(注意:这个仍使用 Claude 原生路径)
130
- skills: path.join(os.homedir(), '.claude', 'skills'),
133
+ skills: path.join(HOME_DIR, '.claude', 'skills'),
131
134
 
132
135
  // MCP 配置(注意:这个仍使用 Claude 原生路径)
133
136
  mcpConfig: path.join(CC_TOOL_BASE_DIR, 'mcp-config.json'),
@@ -148,41 +151,42 @@ const PATHS = {
148
151
  const NATIVE_PATHS = {
149
152
  // Claude Code 原生配置
150
153
  claude: {
151
- settings: path.join(os.homedir(), '.claude', 'settings.json'),
152
- settingsBackup: path.join(os.homedir(), '.claude', 'settings.json.cc-tool-backup'),
153
- projects: path.join(os.homedir(), '.claude', 'projects')
154
+ settings: path.join(HOME_DIR, '.claude', 'settings.json'),
155
+ settingsBackup: path.join(HOME_DIR, '.claude', 'settings.json.cc-tool-backup'),
156
+ projects: path.join(HOME_DIR, '.claude', 'projects')
154
157
  },
155
158
 
156
159
  // Codex 原生配置
157
160
  codex: {
158
- config: path.join(os.homedir(), '.codex', 'config.toml'),
159
- configBackup: path.join(os.homedir(), '.codex', 'config.toml.cc-tool-backup'),
160
- auth: path.join(os.homedir(), '.codex', 'auth.json'),
161
- authBackup: path.join(os.homedir(), '.codex', 'auth.json.cc-tool-backup'),
162
- sessions: path.join(os.homedir(), '.codex', 'sessions')
161
+ config: path.join(HOME_DIR, '.codex', 'config.toml'),
162
+ configBackup: path.join(HOME_DIR, '.codex', 'config.toml.cc-tool-backup'),
163
+ auth: path.join(HOME_DIR, '.codex', 'auth.json'),
164
+ authBackup: path.join(HOME_DIR, '.codex', 'auth.json.cc-tool-backup'),
165
+ sessions: path.join(HOME_DIR, '.codex', 'sessions')
163
166
  },
164
167
 
165
168
  // Gemini 原生配置
166
169
  gemini: {
167
- env: path.join(os.homedir(), '.gemini', '.env'),
168
- envBackup: path.join(os.homedir(), '.gemini', '.env.cc-tool-backup'),
169
- tmp: path.join(os.homedir(), '.gemini', 'tmp')
170
+ env: path.join(HOME_DIR, '.gemini', '.env'),
171
+ envBackup: path.join(HOME_DIR, '.gemini', '.env.cc-tool-backup'),
172
+ tmp: path.join(HOME_DIR, '.gemini', 'tmp')
170
173
  },
171
174
 
172
175
  // OpenCode 原生配置
173
176
  opencode: {
174
- data: path.join(os.homedir(), '.local', 'share', 'opencode'),
175
- config: path.join(os.homedir(), '.config', 'opencode'),
176
- sessions: path.join(os.homedir(), '.local', 'share', 'opencode', 'storage', 'session'),
177
- projects: path.join(os.homedir(), '.local', 'share', 'opencode', 'storage', 'project'),
178
- messages: path.join(os.homedir(), '.local', 'share', 'opencode', 'storage', 'message'),
179
- log: path.join(os.homedir(), '.local', 'share', 'opencode', 'log')
177
+ data: path.join(HOME_DIR, '.local', 'share', 'opencode'),
178
+ config: path.join(HOME_DIR, '.config', 'opencode'),
179
+ sessions: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'session'),
180
+ projects: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'project'),
181
+ messages: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'message'),
182
+ log: path.join(HOME_DIR, '.local', 'share', 'opencode', 'log')
180
183
  }
181
184
  };
182
185
 
183
186
  module.exports = {
184
187
  PATHS,
185
188
  NATIVE_PATHS,
189
+ HOME_DIR,
186
190
  CTX_BASE_DIR,
187
191
  CC_TOOL_BASE_DIR,
188
192
  LEGACY_BASE_DIRS,
@@ -1,7 +1,5 @@
1
1
  const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const { PATHS, ensureStorageDirMigrated } = require('./config/paths');
2
+ const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('./config/paths');
5
3
 
6
4
  // 恢复配置到默认状态
7
5
  async function resetConfig() {
@@ -25,8 +23,8 @@ async function resetConfig() {
25
23
  }
26
24
 
27
25
  // 2. 检查并恢复 settings.json
28
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
29
- const backupPath = path.join(os.homedir(), '.claude', 'settings.json.cc-tool-backup');
26
+ const settingsPath = NATIVE_PATHS.claude.settings;
27
+ const backupPath = NATIVE_PATHS.claude.settingsBackup;
30
28
 
31
29
  if (fs.existsSync(backupPath)) {
32
30
  console.log('发现备份文件,正在恢复...');
@@ -6,15 +6,14 @@
6
6
 
7
7
  const express = require('express');
8
8
  const fs = require('fs');
9
- const os = require('os');
10
9
  const path = require('path');
11
10
  const { AgentsService } = require('../services/agents-service');
12
- const { PATHS } = require('../../config/paths');
11
+ const { PATHS, HOME_DIR } = require('../../config/paths');
13
12
 
14
13
  const router = express.Router();
15
14
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
16
15
  const agentServices = new Map();
17
- const DEFAULT_PROJECT_ALLOWED_ROOTS = [os.homedir(), process.cwd()];
16
+ const DEFAULT_PROJECT_ALLOWED_ROOTS = [HOME_DIR, process.cwd()];
18
17
 
19
18
  function isSupportedPlatform(platform) {
20
19
  return SUPPORTED_PLATFORMS.includes(platform);
@@ -5,18 +5,21 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const https = require('https');
7
7
  const http = require('http');
8
+ const { resolvePreferredHomeDir, normalizeWindowsHomePath } = require('../../utils/home-dir');
9
+
10
+ // 检测操作系统
11
+ const platform = os.platform(); // 'darwin' | 'win32' | 'linux'
12
+
13
+ const HOME_DIR = resolvePreferredHomeDir(platform, process.env, os.homedir());
8
14
 
9
15
  // Claude settings.json 路径
10
- const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
16
+ const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
11
17
 
12
18
  // UI 配置路径(记录用户是否主动关闭过、飞书配置等)
13
- const UI_CONFIG_PATH = path.join(os.homedir(), '.cc-tool', 'ui-config.json');
19
+ const UI_CONFIG_PATH = path.join(HOME_DIR, '.cc-tool', 'ui-config.json');
14
20
 
15
21
  // 通知脚本路径(用于飞书通知)
16
- const NOTIFY_SCRIPT_PATH = path.join(os.homedir(), '.cc-tool', 'notify-hook.js');
17
-
18
- // 检测操作系统
19
- const platform = os.platform(); // 'darwin' | 'win32' | 'linux'
22
+ const NOTIFY_SCRIPT_PATH = path.join(HOME_DIR, '.cc-tool', 'notify-hook.js');
20
23
 
21
24
  // 读取 Claude settings.json
22
25
  function readClaudeSettings() {
@@ -197,6 +200,42 @@ function parseNotifyTypeMarker(command) {
197
200
  return marker ? marker[2].toLowerCase() : null;
198
201
  }
199
202
 
203
+ function getStopHookCommand(settings) {
204
+ const hooks = settings?.hooks?.Stop;
205
+ if (!Array.isArray(hooks) || hooks.length === 0) {
206
+ return '';
207
+ }
208
+ const firstHook = hooks[0]?.hooks;
209
+ if (!Array.isArray(firstHook) || firstHook.length === 0) {
210
+ return '';
211
+ }
212
+ return firstHook[0]?.command || '';
213
+ }
214
+
215
+ function normalizePathForCompare(rawPath) {
216
+ return String(rawPath || '').replace(/\\/g, '/');
217
+ }
218
+
219
+ function shouldRepairStopHook(settings, expectedScriptPath = NOTIFY_SCRIPT_PATH, fileExists = fs.existsSync) {
220
+ const command = getStopHookCommand(settings);
221
+ if (!command || !command.includes('notify-hook.js')) {
222
+ return false;
223
+ }
224
+
225
+ const markerType = parseNotifyTypeMarker(command);
226
+ if (!markerType) {
227
+ return false;
228
+ }
229
+
230
+ const normalizedCommand = normalizePathForCompare(command);
231
+ const normalizedExpected = normalizePathForCompare(expectedScriptPath);
232
+ if (!normalizedCommand.includes(normalizedExpected)) {
233
+ return true;
234
+ }
235
+
236
+ return !fileExists(expectedScriptPath);
237
+ }
238
+
200
239
  function buildStopHookCommand(type) {
201
240
  const notifyType = type === 'dialog' ? 'dialog' : 'notification';
202
241
  return `node "${NOTIFY_SCRIPT_PATH}" --cc-notify-type=${notifyType}`;
@@ -302,7 +341,9 @@ function updateStopHook(systemNotification, feishu) {
302
341
  }
303
342
  } else {
304
343
  // 生成并写入通知脚本
305
- writeNotifyScript({ systemNotification, feishu });
344
+ if (!writeNotifyScript({ systemNotification, feishu })) {
345
+ return false;
346
+ }
306
347
 
307
348
  // 更新 Stop hook 指向通知脚本
308
349
  settings.hooks = settings.hooks || {};
@@ -335,9 +376,22 @@ function initDefaultHooks() {
335
376
  const settings = readClaudeSettings();
336
377
  const currentStatus = parseStopHookStatus(settings);
337
378
 
338
- // 如果已经有 Stop hook 配置,不覆盖
379
+ // 如果已经有 Stop hook 配置,优先尝试自愈旧路径,再决定是否跳过
339
380
  if (currentStatus.enabled) {
340
- console.log('[Claude Hooks] 已存在 Stop hook 配置,跳过初始化');
381
+ if (shouldRepairStopHook(settings)) {
382
+ const systemNotification = {
383
+ enabled: true,
384
+ type: currentStatus.type || 'notification'
385
+ };
386
+ const feishu = getFeishuConfig();
387
+ if (updateStopHook(systemNotification, feishu)) {
388
+ console.log('[Claude Hooks] 检测到旧版 Stop hook 路径,已自动修复');
389
+ } else {
390
+ console.warn('[Claude Hooks] Stop hook 路径修复失败,保留原配置');
391
+ }
392
+ } else {
393
+ console.log('[Claude Hooks] 已存在 Stop hook 配置,跳过初始化');
394
+ }
341
395
  return;
342
396
  }
343
397
 
@@ -499,5 +553,8 @@ module.exports._test = {
499
553
  generateSystemNotificationCommand,
500
554
  parseStopHookStatus,
501
555
  parseNotifyTypeMarker,
502
- buildStopHookCommand
556
+ buildStopHookCommand,
557
+ normalizeWindowsHomePath,
558
+ resolvePreferredHomeDir,
559
+ shouldRepairStopHook
503
560
  };
@@ -14,7 +14,7 @@ const {
14
14
  } = require('../services/opencode-sessions');
15
15
  const { loadAliases } = require('../services/alias');
16
16
  const { broadcastLog } = require('../websocket-server');
17
- const os = require('os');
17
+ const { HOME_DIR } = require('../../config/paths');
18
18
 
19
19
  function isNotFoundError(error) {
20
20
  if (!error || !error.message) {
@@ -295,7 +295,7 @@ module.exports = (config) => {
295
295
 
296
296
  const projects = getProjects();
297
297
  const project = projects.find(p => p.name === projectName);
298
- const cwd = session.directory || project?.fullPath || os.homedir();
298
+ const cwd = session.directory || project?.fullPath || HOME_DIR;
299
299
  const command = `opencode -r ${sessionId}`;
300
300
  const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
301
301
  const copyCommand = `cd ${quotedCwd} && ${command}`;
@@ -3,11 +3,18 @@ const { exec } = require('child_process');
3
3
  const { promisify } = require('util');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
- const os = require('os');
7
6
  const pm2 = require('pm2');
7
+ const { HOME_DIR } = require('../../config/paths');
8
8
 
9
9
  const execAsync = promisify(exec);
10
10
 
11
+ function getExecOptions(timeout = 30000, runtimePlatform = process.platform) {
12
+ if (runtimePlatform === 'win32') {
13
+ return { timeout };
14
+ }
15
+ return { shell: '/bin/bash', timeout };
16
+ }
17
+
11
18
  /**
12
19
  * Check if PM2 autostart is enabled
13
20
  * by looking for PM2 startup script in system
@@ -18,7 +25,7 @@ async function checkAutoStartStatus() {
18
25
 
19
26
  if (platform === 'darwin') {
20
27
  // macOS - check for LaunchDaemon
21
- const launchDaemonsPath = path.join(os.homedir(), 'Library/LaunchDaemons');
28
+ const launchDaemonsPath = path.join(HOME_DIR, 'Library/LaunchDaemons');
22
29
  const pm2Files = fs.existsSync(launchDaemonsPath)
23
30
  ? fs.readdirSync(launchDaemonsPath).filter(f => f.includes('pm2'))
24
31
  : [];
@@ -27,11 +34,11 @@ async function checkAutoStartStatus() {
27
34
  } else if (platform === 'linux') {
28
35
  // Linux - check for systemd service
29
36
  const systemdPath = '/etc/systemd/system/pm2-root.service';
30
- const userSystemdPath = path.join(os.homedir(), '.config/systemd/user/pm2-*.service');
37
+ const userSystemdPath = path.join(HOME_DIR, '.config/systemd/user/pm2-*.service');
31
38
 
32
39
  const rootExists = fs.existsSync(systemdPath);
33
- const userExists = fs.existsSync(path.join(os.homedir(), '.config/systemd/user')) &&
34
- fs.readdirSync(path.join(os.homedir(), '.config/systemd/user')).some(f => f.includes('pm2'));
40
+ const userExists = fs.existsSync(path.join(HOME_DIR, '.config/systemd/user')) &&
41
+ fs.readdirSync(path.join(HOME_DIR, '.config/systemd/user')).some(f => f.includes('pm2'));
35
42
 
36
43
  return { enabled: rootExists || userExists, platform: 'linux' };
37
44
  } else if (platform === 'win32') {
@@ -104,7 +111,7 @@ async function enableAutoStart() {
104
111
 
105
112
  console.log(`Running startup command: ${command}`);
106
113
 
107
- exec(command, { shell: '/bin/bash', timeout: 30000 }, (execErr, stdout, stderr) => {
114
+ exec(command, getExecOptions(30000), (execErr, stdout, stderr) => {
108
115
  pm2.disconnect();
109
116
 
110
117
  if (execErr) {
@@ -163,7 +170,7 @@ async function disableAutoStart() {
163
170
 
164
171
  console.log(`Running unstartup command: ${command}`);
165
172
 
166
- exec(command, { shell: '/bin/bash', timeout: 30000 }, (execErr, stdout, stderr) => {
173
+ exec(command, getExecOptions(30000), (execErr, stdout, stderr) => {
167
174
  pm2.disconnect();
168
175
 
169
176
  if (execErr) {
@@ -195,7 +202,7 @@ async function disableAutoStart() {
195
202
  });
196
203
  }
197
204
 
198
- module.exports = () => {
205
+ function createPm2AutostartRouter() {
199
206
  const router = express.Router();
200
207
 
201
208
  /**
@@ -266,4 +273,9 @@ module.exports = () => {
266
273
  });
267
274
 
268
275
  return router;
276
+ }
277
+
278
+ module.exports = createPm2AutostartRouter;
279
+ module.exports._test = {
280
+ getExecOptions
269
281
  };
@@ -12,10 +12,9 @@ const {
12
12
  } = require('../services/settings-manager');
13
13
  const { getAllChannels } = require('../services/channels');
14
14
  const { clearAllLogs } = require('../websocket-server');
15
- const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
15
+ const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('../../config/paths');
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
- const os = require('os');
19
18
 
20
19
  function sanitizeChannelForResponse(channel) {
21
20
  if (!channel) return null;
@@ -285,7 +284,7 @@ router.post('/stop', async (req, res) => {
285
284
 
286
285
  // 3. 删除备份文件和active-channel.json
287
286
  if (hasBackup()) {
288
- const backupPath = path.join(os.homedir(), '.claude', 'settings.json.cc-tool-backup');
287
+ const backupPath = NATIVE_PATHS.claude.settingsBackup;
289
288
  if (fs.existsSync(backupPath)) {
290
289
  fs.unlinkSync(backupPath);
291
290
  console.log('✅ Removed backup file');
@@ -2,11 +2,12 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
- const os = require('os');
6
5
  const readline = require('readline');
7
6
  const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
8
7
  const { loadAliases } = require('../services/alias');
9
8
  const { broadcastLog } = require('../websocket-server');
9
+ const { NATIVE_PATHS } = require('../../config/paths');
10
+ const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
10
11
 
11
12
  module.exports = (config) => {
12
13
  // GET /api/sessions/search/global - Search sessions across all projects
@@ -120,12 +121,12 @@ module.exports = (config) => {
120
121
  const year = now.getFullYear();
121
122
  const month = String(now.getMonth() + 1).padStart(2, '0');
122
123
  const day = String(now.getDate()).padStart(2, '0');
123
- sessionDir = path.join(os.homedir(), '.codex', 'sessions', String(year), month, day);
124
+ sessionDir = path.join(NATIVE_PATHS.codex.sessions, String(year), month, day);
124
125
  sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
125
126
  } else if (toolType === 'gemini') {
126
127
  // Gemini: ~/.gemini/tmp/{hash}/chats/{sessionId}.json
127
128
  const pathHash = crypto.createHash('sha256').update(fullPath).digest('hex');
128
- sessionDir = path.join(os.homedir(), '.gemini', 'tmp', pathHash, 'chats');
129
+ sessionDir = path.join(NATIVE_PATHS.gemini.tmp, pathHash, 'chats');
129
130
  sessionFile = path.join(sessionDir, `${newSessionId}.json`);
130
131
  } else {
131
132
  return res.status(400).json({ error: 'Invalid toolType. Must be claude, codex, or gemini' });
@@ -243,7 +244,7 @@ module.exports = (config) => {
243
244
  let sessionFile = null;
244
245
  const possiblePaths = [
245
246
  path.join(fullPath, '.claude', 'sessions', sessionId + '.jsonl'),
246
- path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
247
+ path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
247
248
  ];
248
249
 
249
250
  console.log(`[Messages API] Trying paths:`, possiblePaths);
@@ -423,7 +424,6 @@ module.exports = (config) => {
423
424
  const { projectName, sessionId } = req.params;
424
425
  const path = require('path');
425
426
  const fs = require('fs');
426
- const os = require('os');
427
427
 
428
428
  // Parse real project path (important for cross-project sessions)
429
429
  const { fullPath } = parseRealProjectPath(projectName);
@@ -436,7 +436,7 @@ module.exports = (config) => {
436
436
  const possiblePaths = [
437
437
  projectSessionFile,
438
438
  // Location 2: User's .claude/projects directory (ClaudeCode default)
439
- path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
439
+ path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
440
440
  ];
441
441
 
442
442
  for (const testPath of possiblePaths) {