coding-tool-x 3.3.3 → 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 (69) hide show
  1. package/dist/web/assets/{Analytics-DtR00OYP.js → Analytics-B6CWdkhx.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DWiSFOp5.js → ConfigTemplates-BW6LEgd8.js} +1 -1
  3. package/dist/web/assets/{Home-DUu2mGb6.js → Home-B2B2gS2-.js} +1 -1
  4. package/dist/web/assets/{PluginManager-DsJ1KtNr.js → PluginManager-Bqc7ldY-.js} +1 -1
  5. package/dist/web/assets/{ProjectList-CzTJaBJb.js → ProjectList-BFdZZm_8.js} +1 -1
  6. package/dist/web/assets/{SessionList-D1ovPZ0I.js → SessionList-B_Tp37kM.js} +1 -1
  7. package/dist/web/assets/{SkillManager-DqpDTc2c.js → SkillManager-ul2rcS3o.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-Dj28-3G5.js → WorkspaceManager-Dp5Jvdtu.js} +1 -1
  9. package/dist/web/assets/index-CSBDZxYn.js +2 -0
  10. package/dist/web/assets/index-DxRneGyu.css +1 -0
  11. package/dist/web/index.html +2 -2
  12. package/package.json +2 -1
  13. package/src/commands/doctor.js +3 -3
  14. package/src/commands/export-config.js +2 -2
  15. package/src/commands/logs.js +42 -9
  16. package/src/commands/port-config.js +2 -2
  17. package/src/commands/switch.js +2 -2
  18. package/src/config/default.js +4 -1
  19. package/src/config/loader.js +5 -2
  20. package/src/config/paths.js +25 -21
  21. package/src/reset-config.js +3 -5
  22. package/src/server/api/agents.js +2 -3
  23. package/src/server/api/claude-hooks.js +92 -12
  24. package/src/server/api/codex-sessions.js +6 -5
  25. package/src/server/api/opencode-sessions.js +2 -2
  26. package/src/server/api/pm2-autostart.js +20 -8
  27. package/src/server/api/proxy.js +2 -3
  28. package/src/server/api/sessions.js +42 -12
  29. package/src/server/index.js +5 -9
  30. package/src/server/opencode-proxy-server.js +11 -1
  31. package/src/server/services/agents-service.js +6 -3
  32. package/src/server/services/channels.js +6 -7
  33. package/src/server/services/codex-channels.js +4 -1
  34. package/src/server/services/codex-config.js +4 -1
  35. package/src/server/services/codex-parser.js +31 -4
  36. package/src/server/services/codex-settings-manager.js +18 -9
  37. package/src/server/services/commands-service.js +2 -2
  38. package/src/server/services/config-export-service.js +7 -6
  39. package/src/server/services/config-registry-service.js +7 -6
  40. package/src/server/services/config-sync-manager.js +2 -2
  41. package/src/server/services/config-sync-service.js +2 -2
  42. package/src/server/services/env-checker.js +2 -2
  43. package/src/server/services/favorites.js +3 -4
  44. package/src/server/services/gemini-channels.js +4 -4
  45. package/src/server/services/gemini-config.js +2 -2
  46. package/src/server/services/gemini-sessions.js +3 -3
  47. package/src/server/services/gemini-settings-manager.js +5 -5
  48. package/src/server/services/mcp-service.js +7 -4
  49. package/src/server/services/model-detector.js +2 -2
  50. package/src/server/services/opencode-channels.js +5 -5
  51. package/src/server/services/opencode-sessions.js +28 -3
  52. package/src/server/services/plugins-service.js +3 -4
  53. package/src/server/services/prompts-service.js +7 -4
  54. package/src/server/services/proxy-runtime.js +2 -2
  55. package/src/server/services/repo-scanner-base.js +2 -2
  56. package/src/server/services/request-logger.js +2 -2
  57. package/src/server/services/security-config.js +2 -2
  58. package/src/server/services/session-cache.js +2 -2
  59. package/src/server/services/session-converter.js +9 -4
  60. package/src/server/services/sessions.js +8 -5
  61. package/src/server/services/settings-manager.js +3 -4
  62. package/src/server/services/skill-service.js +5 -5
  63. package/src/server/services/statistics-service.js +2 -2
  64. package/src/server/services/ui-config.js +3 -4
  65. package/src/server/websocket-server.js +2 -2
  66. package/src/utils/home-dir.js +82 -0
  67. package/src/utils/port-helper.js +34 -12
  68. package/dist/web/assets/index-CaKktouI.js +0 -2
  69. package/dist/web/assets/index-DZjDFGqR.css +0 -1
@@ -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() {
@@ -91,7 +94,7 @@ function generateSystemNotificationCommand(type) {
91
94
  if (type === 'dialog') {
92
95
  return `powershell -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('Claude Code 任务已完成 | 等待交互', 'Coding Tool', 'OK', 'Information')"`;
93
96
  } else {
94
- return `powershell -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40)"`;
97
+ return `powershell -NoProfile -Command "try { [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null; $xml = New-Object Windows.Data.Xml.Dom.XmlDocument; $xml.LoadXml('<toast><visual><binding template=\\"ToastGeneric\\"><text>Coding Tool</text><text>任务已完成 | 等待交互</text></binding></visual><audio src=\\"ms-winsoundevent:Notification.Default\\"/></toast>'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) } catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40) }"`;
95
98
  }
96
99
  } else {
97
100
  // Linux
@@ -192,6 +195,52 @@ try {
192
195
  return script;
193
196
  }
194
197
 
198
+ function parseNotifyTypeMarker(command) {
199
+ const marker = command.match(/--cc-notify-type=(['"])?(dialog|notification)\1/i);
200
+ return marker ? marker[2].toLowerCase() : null;
201
+ }
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
+
239
+ function buildStopHookCommand(type) {
240
+ const notifyType = type === 'dialog' ? 'dialog' : 'notification';
241
+ return `node "${NOTIFY_SCRIPT_PATH}" --cc-notify-type=${notifyType}`;
242
+ }
243
+
195
244
  // 写入通知脚本
196
245
  function writeNotifyScript(config) {
197
246
  try {
@@ -222,6 +271,11 @@ function parseStopHookStatus(settings) {
222
271
  }
223
272
 
224
273
  const command = stopHook.hooks[0].command || '';
274
+ const markerType = parseNotifyTypeMarker(command);
275
+
276
+ if (markerType) {
277
+ return { enabled: true, type: markerType };
278
+ }
225
279
 
226
280
  // 判断通知类型(跨平台检测)
227
281
  const isDialog = command.includes('display dialog') ||
@@ -229,7 +283,9 @@ function parseStopHookStatus(settings) {
229
283
  command.includes('zenity --info');
230
284
  const isNotification = command.includes('display notification') ||
231
285
  command.includes('Popup') ||
232
- command.includes('notify-send');
286
+ command.includes('notify-send') ||
287
+ command.includes('ToastNotificationManager') ||
288
+ command.includes('CreateToastNotifier');
233
289
 
234
290
  // 检查是否是我们的通知脚本
235
291
  const isOurScript = command.includes('notify-hook.js');
@@ -285,7 +341,9 @@ function updateStopHook(systemNotification, feishu) {
285
341
  }
286
342
  } else {
287
343
  // 生成并写入通知脚本
288
- writeNotifyScript({ systemNotification, feishu });
344
+ if (!writeNotifyScript({ systemNotification, feishu })) {
345
+ return false;
346
+ }
289
347
 
290
348
  // 更新 Stop hook 指向通知脚本
291
349
  settings.hooks = settings.hooks || {};
@@ -294,7 +352,7 @@ function updateStopHook(systemNotification, feishu) {
294
352
  hooks: [
295
353
  {
296
354
  type: 'command',
297
- command: `node "${NOTIFY_SCRIPT_PATH}"`
355
+ command: buildStopHookCommand(systemNotification?.type)
298
356
  }
299
357
  ]
300
358
  }
@@ -318,9 +376,22 @@ function initDefaultHooks() {
318
376
  const settings = readClaudeSettings();
319
377
  const currentStatus = parseStopHookStatus(settings);
320
378
 
321
- // 如果已经有 Stop hook 配置,不覆盖
379
+ // 如果已经有 Stop hook 配置,优先尝试自愈旧路径,再决定是否跳过
322
380
  if (currentStatus.enabled) {
323
- 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
+ }
324
395
  return;
325
396
  }
326
397
 
@@ -478,3 +549,12 @@ router.post('/test', (req, res) => {
478
549
  // 导出初始化函数供服务启动时调用
479
550
  module.exports = router;
480
551
  module.exports.initDefaultHooks = initDefaultHooks;
552
+ module.exports._test = {
553
+ generateSystemNotificationCommand,
554
+ parseStopHookStatus,
555
+ parseNotifyTypeMarker,
556
+ buildStopHookCommand,
557
+ normalizeWindowsHomePath,
558
+ resolvePreferredHomeDir,
559
+ shouldRepairStopHook
560
+ };
@@ -243,7 +243,7 @@ module.exports = (config) => {
243
243
  type: 'assistant',
244
244
  content: msg.content || '[空消息]',
245
245
  timestamp: msg.timestamp,
246
- model: session.provider || 'codex'
246
+ model: msg.model || session.provider || 'codex'
247
247
  });
248
248
  }
249
249
  // 推理内容
@@ -252,7 +252,7 @@ module.exports = (config) => {
252
252
  type: 'assistant',
253
253
  content: `**[推理]**\n${msg.content || '[空推理]'}`,
254
254
  timestamp: msg.timestamp,
255
- model: session.provider || 'codex'
255
+ model: msg.model || session.provider || 'codex'
256
256
  });
257
257
  }
258
258
  // 工具调用
@@ -264,7 +264,7 @@ module.exports = (config) => {
264
264
  type: 'assistant',
265
265
  content: `**[调用工具: ${msg.name}]**\n\`\`\`json\n${argsStr}\n\`\`\``,
266
266
  timestamp: msg.timestamp,
267
- model: session.provider || 'codex'
267
+ model: msg.model || session.provider || 'codex'
268
268
  });
269
269
  }
270
270
  // 工具输出
@@ -284,10 +284,11 @@ module.exports = (config) => {
284
284
  }
285
285
 
286
286
  convertedMessages.push({
287
- type: 'user',
287
+ type: 'assistant',
288
+ subtype: 'tool_result',
288
289
  content: `**[工具结果]**\n\`\`\`\n${outputStr}\n\`\`\``,
289
290
  timestamp: msg.timestamp,
290
- model: null
291
+ model: msg.model || session.provider || 'codex'
291
292
  });
292
293
  }
293
294
  }
@@ -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);
@@ -279,6 +280,7 @@ module.exports = (config) => {
279
280
 
280
281
  const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
281
282
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
283
+ let lastAssistantModel = null;
282
284
 
283
285
  try {
284
286
  for await (const line of rl) {
@@ -297,31 +299,45 @@ module.exports = (config) => {
297
299
  }
298
300
 
299
301
  if (json.type === 'user' || json.type === 'assistant') {
302
+ const resolvedModel = json.message?.model || json.model || lastAssistantModel || null;
303
+ const messageId = json.message?.id || json.uuid || null;
300
304
  const message = {
301
305
  type: json.type,
302
306
  content: null,
303
307
  timestamp: json.timestamp || null,
304
- model: json.model || null
308
+ model: resolvedModel,
309
+ messageId,
310
+ subtype: null
305
311
  };
312
+ let deferredToolResultContent = '';
306
313
 
307
314
  if (json.type === 'user') {
308
315
  if (typeof json.message?.content === 'string') {
309
316
  message.content = json.message.content;
310
317
  } else if (Array.isArray(json.message?.content)) {
311
- const parts = [];
318
+ const userParts = [];
319
+ const toolResultParts = [];
312
320
  for (const item of json.message.content) {
313
321
  if (item.type === 'text' && item.text) {
314
- parts.push(item.text);
322
+ userParts.push(item.text);
315
323
  } else if (item.type === 'tool_result') {
316
324
  const resultContent = typeof item.content === 'string'
317
325
  ? item.content
318
326
  : JSON.stringify(item.content, null, 2);
319
- parts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
327
+ toolResultParts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
320
328
  } else if (item.type === 'image') {
321
- parts.push('[图片]');
329
+ userParts.push('[图片]');
322
330
  }
323
331
  }
324
- message.content = parts.join('\n\n') || '[工具交互]';
332
+
333
+ if (userParts.length > 0) {
334
+ message.content = userParts.join('\n\n');
335
+ }
336
+
337
+ // Claude tool_result is carried in a "user" envelope, but should be rendered as AI tool output.
338
+ if (toolResultParts.length > 0) {
339
+ deferredToolResultContent = toolResultParts.join('\n\n');
340
+ }
325
341
  }
326
342
  } else if (json.type === 'assistant') {
327
343
  if (Array.isArray(json.message?.content)) {
@@ -340,11 +356,26 @@ module.exports = (config) => {
340
356
  } else if (typeof json.message?.content === 'string') {
341
357
  message.content = json.message.content;
342
358
  }
359
+
360
+ if (message.model) {
361
+ lastAssistantModel = message.model;
362
+ }
343
363
  }
344
364
 
345
365
  if (message.content && message.content !== 'Warmup') {
346
366
  allMessages.push(message);
347
367
  }
368
+
369
+ if (deferredToolResultContent) {
370
+ allMessages.push({
371
+ type: 'assistant',
372
+ subtype: 'tool_result',
373
+ content: deferredToolResultContent,
374
+ timestamp: json.timestamp || null,
375
+ model: resolvedModel,
376
+ messageId: messageId ? `${messageId}-tool-result` : null
377
+ });
378
+ }
348
379
  }
349
380
  } catch (err) {
350
381
  // Skip invalid lines
@@ -393,7 +424,6 @@ module.exports = (config) => {
393
424
  const { projectName, sessionId } = req.params;
394
425
  const path = require('path');
395
426
  const fs = require('fs');
396
- const os = require('os');
397
427
 
398
428
  // Parse real project path (important for cross-project sessions)
399
429
  const { fullPath } = parseRealProjectPath(projectName);
@@ -406,7 +436,7 @@ module.exports = (config) => {
406
436
  const possiblePaths = [
407
437
  projectSessionFile,
408
438
  // Location 2: User's .claude/projects directory (ClaudeCode default)
409
- path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
439
+ path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
410
440
  ];
411
441
 
412
442
  for (const testPath of possiblePaths) {
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
5
  const { loadConfig } = require('../config/loader');
6
- const { ensureStorageDirMigrated } = require('../config/paths');
6
+ const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
7
7
  const { startWebSocketServer: attachWebSocketServer } = require('./websocket-server');
8
8
  const { isPortInUse, killProcessByPort, waitForPortRelease } = require('../utils/port-helper');
9
9
  const { isProxyConfig } = require('./services/settings-manager');
@@ -266,14 +266,10 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
266
266
  // 自动恢复代理状态
267
267
  function autoRestoreProxies() {
268
268
  const config = loadConfig();
269
- const os = require('os');
270
269
  const fs = require('fs');
271
- const path = require('path');
272
-
273
- const ccToolDir = path.join(os.homedir(), '.cc-tool');
274
270
 
275
271
  // 检查 Claude 代理状态文件
276
- const claudeActiveFile = path.join(ccToolDir, 'active-channel.json');
272
+ const claudeActiveFile = PATHS.activeChannel.claude;
277
273
  if (fs.existsSync(claudeActiveFile)) {
278
274
  console.log(chalk.cyan('\n🔄 检测到 Claude 代理状态文件,正在自动启动...'));
279
275
  const proxyPort = config.ports?.proxy || 20088;
@@ -287,7 +283,7 @@ function autoRestoreProxies() {
287
283
  }
288
284
 
289
285
  // 检查 Codex 代理状态文件
290
- const codexActiveFile = path.join(ccToolDir, 'codex-active-channel.json');
286
+ const codexActiveFile = PATHS.activeChannel.codex;
291
287
  if (fs.existsSync(codexActiveFile)) {
292
288
  console.log(chalk.cyan('\n🔄 检测到 Codex 代理状态文件,正在自动启动...'));
293
289
  const codexProxyPort = config.ports?.codexProxy || 20089;
@@ -312,7 +308,7 @@ function autoRestoreProxies() {
312
308
  }
313
309
 
314
310
  // 检查 Gemini 代理状态文件
315
- const geminiActiveFile = path.join(ccToolDir, 'gemini-active-channel.json');
311
+ const geminiActiveFile = PATHS.activeChannel.gemini;
316
312
  if (fs.existsSync(geminiActiveFile)) {
317
313
  console.log(chalk.cyan('\n🔄 检测到 Gemini 代理状态文件,正在自动启动...'));
318
314
  const geminiProxyPort = config.ports?.geminiProxy || 20090;
@@ -332,7 +328,7 @@ function autoRestoreProxies() {
332
328
  }
333
329
 
334
330
  // 检查 OpenCode 代理状态文件
335
- const opencodeActiveFile = path.join(ccToolDir, 'opencode-active-channel.json');
331
+ const opencodeActiveFile = PATHS.activeChannel.opencode;
336
332
  if (fs.existsSync(opencodeActiveFile)) {
337
333
  console.log(chalk.cyan('\n🔄 检测到 OpenCode 代理状态文件,正在自动启动...'));
338
334
  const opencodeProxyPort = config.ports?.opencodeProxy || 20091;