coding-tool-x 3.4.4 → 3.4.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 (26) hide show
  1. package/dist/web/assets/{Analytics-_Byi9M6y.js → Analytics-DFWyPf5C.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DIwosdtG.js → ConfigTemplates-BFE7hmKd.js} +1 -1
  3. package/dist/web/assets/{Home-DdNMuQ9c.js → Home-DZUuCrxk.js} +1 -1
  4. package/dist/web/assets/{PluginManager-iuY24cnW.js → PluginManager-WyGY2BQN.js} +1 -1
  5. package/dist/web/assets/{ProjectList-DSkMulzL.js → ProjectList-CBc0QawN.js} +1 -1
  6. package/dist/web/assets/{SessionList-B6pGquIr.js → SessionList-CdPR7QLq.js} +1 -1
  7. package/dist/web/assets/{SkillManager-CHtQX5r8.js → SkillManager-B5-DxQOS.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-gNPs-VaI.js → WorkspaceManager-C7yqFjpi.js} +1 -1
  9. package/dist/web/assets/index-BDsmoSfO.js +2 -0
  10. package/dist/web/assets/{index-pMqqe9ei.css → index-C1pzEgmj.css} +1 -1
  11. package/dist/web/index.html +2 -2
  12. package/package.json +2 -2
  13. package/src/server/api/claude-hooks.js +1 -0
  14. package/src/server/api/plugins.js +161 -14
  15. package/src/server/api/skills.js +62 -7
  16. package/src/server/codex-proxy-server.js +10 -2
  17. package/src/server/gemini-proxy-server.js +10 -2
  18. package/src/server/opencode-proxy-server.js +10 -2
  19. package/src/server/proxy-server.js +10 -2
  20. package/src/server/services/codex-channels.js +64 -21
  21. package/src/server/services/codex-env-manager.js +44 -28
  22. package/src/server/services/plugins-service.js +1060 -235
  23. package/src/server/services/proxy-runtime.js +129 -5
  24. package/src/server/services/server-shutdown.js +79 -0
  25. package/src/server/services/skill-service.js +142 -17
  26. package/dist/web/assets/index-DGjGCo37.js +0 -2
@@ -1,6 +1,18 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { PATHS } = require('../../config/paths');
4
+
5
+ const LOG_RECOVERY_BYTES = 1024 * 1024;
6
+ const LOG_FILE_PATH = path.join(PATHS.logs, 'cc-tool-out.log');
7
+ const PROXY_START_LOG_PATTERNS = {
8
+ claude: [
9
+ /Proxy server started on http:\/\/127\.0\.0\.1:\d+/,
10
+ /Claude 代理已自动启动,端口: \d+/
11
+ ],
12
+ codex: [/Codex proxy server started on http:\/\/127\.0\.0\.1:\d+/],
13
+ gemini: [/Gemini proxy server started on http:\/\/127\.0\.0\.1:\d+/],
14
+ opencode: [/OpenCode proxy server started on http:\/\/127\.0\.0\.1:\d+/]
15
+ };
4
16
 
5
17
  function getRuntimeFilePath(proxyType) {
6
18
  const filePath = PATHS.proxyRuntime?.[proxyType]
@@ -27,17 +39,129 @@ function saveProxyStartTime(proxyType, preserveExisting = false) {
27
39
  }
28
40
  }
29
41
 
30
- function getProxyStartTime(proxyType) {
42
+ function toValidStartTime(value) {
43
+ const parsed = Number(value);
44
+ if (!Number.isFinite(parsed) || parsed <= 0) {
45
+ return null;
46
+ }
47
+ return Math.floor(parsed);
48
+ }
49
+
50
+ function persistRecoveredStartTime(proxyType, startTime, recoveredFrom) {
51
+ const validStartTime = toValidStartTime(startTime);
52
+ if (!validStartTime) {
53
+ return null;
54
+ }
55
+
56
+ const runtimeFilePath = getRuntimeFilePath(proxyType);
57
+ const data = {
58
+ startTime: validStartTime,
59
+ type: proxyType,
60
+ recoveredFrom
61
+ };
62
+ fs.writeFileSync(runtimeFilePath, JSON.stringify(data, null, 2), 'utf8');
63
+ return validStartTime;
64
+ }
65
+
66
+ function readStoredProxyStartTime(proxyType) {
31
67
  try {
32
68
  const filePath = getRuntimeFilePath(proxyType);
33
69
  if (!fs.existsSync(filePath)) return null;
34
70
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
35
- return data.startTime || null;
71
+ return toValidStartTime(data.startTime);
36
72
  } catch (err) {
37
73
  return null;
38
74
  }
39
75
  }
40
76
 
77
+ function parseTimestampPrefix(line) {
78
+ const match = String(line || '').match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):/);
79
+ if (!match) {
80
+ return null;
81
+ }
82
+
83
+ const parsed = Date.parse(match[1].replace(' ', 'T'));
84
+ return toValidStartTime(parsed);
85
+ }
86
+
87
+ function recoverProxyStartTimeFromLogs(proxyType) {
88
+ try {
89
+ if (!fs.existsSync(LOG_FILE_PATH)) {
90
+ return null;
91
+ }
92
+
93
+ const patterns = PROXY_START_LOG_PATTERNS[proxyType];
94
+ if (!Array.isArray(patterns) || patterns.length === 0) {
95
+ return null;
96
+ }
97
+
98
+ const stat = fs.statSync(LOG_FILE_PATH);
99
+ const start = Math.max(0, stat.size - LOG_RECOVERY_BYTES);
100
+ const fd = fs.openSync(LOG_FILE_PATH, 'r');
101
+
102
+ try {
103
+ const length = stat.size - start;
104
+ const buffer = Buffer.alloc(length);
105
+ fs.readSync(fd, buffer, 0, length, start);
106
+
107
+ const lines = buffer.toString('utf8').split(/\r?\n/).reverse();
108
+ for (const line of lines) {
109
+ if (!patterns.some(pattern => pattern.test(line))) {
110
+ continue;
111
+ }
112
+
113
+ const timestamp = parseTimestampPrefix(line);
114
+ if (timestamp) {
115
+ return persistRecoveredStartTime(proxyType, timestamp, 'log');
116
+ }
117
+ }
118
+ } finally {
119
+ fs.closeSync(fd);
120
+ }
121
+ } catch (err) {
122
+ console.warn(`Failed to recover ${proxyType} proxy start time from logs:`, err.message);
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ function recoverProxyStartTime(proxyType) {
129
+ try {
130
+ const logRecoveredStartTime = recoverProxyStartTimeFromLogs(proxyType);
131
+ if (logRecoveredStartTime) {
132
+ return logRecoveredStartTime;
133
+ }
134
+
135
+ const activeChannelPath = PATHS.activeChannel?.[proxyType];
136
+ if (!activeChannelPath || !fs.existsSync(activeChannelPath)) {
137
+ return null;
138
+ }
139
+
140
+ const recoveredStartTime = toValidStartTime(fs.statSync(activeChannelPath).mtimeMs);
141
+ if (!recoveredStartTime) {
142
+ return null;
143
+ }
144
+
145
+ return persistRecoveredStartTime(proxyType, recoveredStartTime, 'active-channel');
146
+ } catch (err) {
147
+ console.warn(`Failed to recover ${proxyType} proxy start time from active channel:`, err.message);
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function getProxyStartTime(proxyType, options = {}) {
153
+ const storedStartTime = readStoredProxyStartTime(proxyType);
154
+ if (storedStartTime) {
155
+ return storedStartTime;
156
+ }
157
+
158
+ if (options.allowRecovery) {
159
+ return recoverProxyStartTime(proxyType);
160
+ }
161
+
162
+ return null;
163
+ }
164
+
41
165
  function clearProxyStartTime(proxyType) {
42
166
  try {
43
167
  const filePath = getRuntimeFilePath(proxyType);
@@ -49,9 +173,9 @@ function clearProxyStartTime(proxyType) {
49
173
  }
50
174
  }
51
175
 
52
- function getProxyRuntime(proxyType) {
53
- const startTime = getProxyStartTime(proxyType);
54
- return startTime ? Date.now() - startTime : null;
176
+ function getProxyRuntime(proxyType, options = {}) {
177
+ const startTime = getProxyStartTime(proxyType, options);
178
+ return startTime ? Math.max(0, Date.now() - startTime) : null;
55
179
  }
56
180
 
57
181
  function formatRuntime(ms) {
@@ -0,0 +1,79 @@
1
+ const SOCKET_TRACKER = Symbol('ccTool.socketTracker');
2
+
3
+ function attachServerShutdownHandling(server, options = {}) {
4
+ if (!server || server[SOCKET_TRACKER]) {
5
+ return server;
6
+ }
7
+
8
+ const sockets = new Set();
9
+ server.on('connection', (socket) => {
10
+ sockets.add(socket);
11
+ socket.on('close', () => {
12
+ sockets.delete(socket);
13
+ });
14
+ });
15
+
16
+ const keepAliveTimeout = Number.isFinite(options.keepAliveTimeout)
17
+ ? Math.max(0, options.keepAliveTimeout)
18
+ : 1000;
19
+ const headersTimeout = Number.isFinite(options.headersTimeout)
20
+ ? Math.max(keepAliveTimeout + 1000, options.headersTimeout)
21
+ : keepAliveTimeout + 1000;
22
+
23
+ server.keepAliveTimeout = keepAliveTimeout;
24
+ server.headersTimeout = headersTimeout;
25
+ server[SOCKET_TRACKER] = { sockets };
26
+ return server;
27
+ }
28
+
29
+ function expediteServerShutdown(server, options = {}) {
30
+ if (!server) {
31
+ return null;
32
+ }
33
+
34
+ try {
35
+ if (typeof server.closeIdleConnections === 'function') {
36
+ server.closeIdleConnections();
37
+ }
38
+ } catch {
39
+ // ignore idle-connection close failures
40
+ }
41
+
42
+ const delay = Number.isFinite(options.forceAfterMs)
43
+ ? Math.max(0, options.forceAfterMs)
44
+ : 300;
45
+
46
+ const timer = setTimeout(() => {
47
+ try {
48
+ if (typeof server.closeAllConnections === 'function') {
49
+ server.closeAllConnections();
50
+ }
51
+ } catch {
52
+ // ignore force-close failures and fallback to socket destroy
53
+ }
54
+
55
+ const tracker = server[SOCKET_TRACKER];
56
+ if (!tracker?.sockets) {
57
+ return;
58
+ }
59
+
60
+ for (const socket of tracker.sockets) {
61
+ try {
62
+ socket.destroy();
63
+ } catch {
64
+ // ignore socket destroy failures
65
+ }
66
+ }
67
+ }, delay);
68
+
69
+ if (typeof timer.unref === 'function') {
70
+ timer.unref();
71
+ }
72
+
73
+ return timer;
74
+ }
75
+
76
+ module.exports = {
77
+ attachServerShutdownHandling,
78
+ expediteServerShutdown
79
+ };
@@ -17,6 +17,7 @@ const AdmZip = require('adm-zip');
17
17
  const {
18
18
  parseSkillContent,
19
19
  } = require('./format-converter');
20
+ const { maskToken } = require('./oauth-utils');
20
21
  const { NATIVE_PATHS, HOME_DIR, PATHS } = require('../../config/paths');
21
22
 
22
23
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
@@ -47,6 +48,10 @@ function stripGitSuffix(value = '') {
47
48
  return String(value || '').replace(/\.git$/i, '');
48
49
  }
49
50
 
51
+ function normalizeRepoToken(token = '') {
52
+ return String(token || '').trim();
53
+ }
54
+
50
55
  function isWindowsAbsolutePath(input = '') {
51
56
  return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
52
57
  }
@@ -326,6 +331,13 @@ class SkillService {
326
331
  normalized.label = buildRepoLabel(normalized);
327
332
  normalized.id = buildRepoId(normalized);
328
333
 
334
+ if (provider !== 'local') {
335
+ const token = normalizeRepoToken(repo.token);
336
+ if (token) {
337
+ normalized.token = token;
338
+ }
339
+ }
340
+
329
341
  return normalized;
330
342
  }
331
343
 
@@ -358,6 +370,55 @@ class SkillService {
358
370
  fs.writeFileSync(this.reposConfigPath, JSON.stringify({ repos: normalizedRepos }, null, 2));
359
371
  }
360
372
 
373
+ toClientRepo(repo = {}) {
374
+ const normalizedRepo = this.normalizeRepoConfig(repo);
375
+ const token = normalizeRepoToken(normalizedRepo.token);
376
+ const clientRepo = {
377
+ ...normalizedRepo,
378
+ hasToken: Boolean(token),
379
+ tokenPreview: token ? maskToken(token) : ''
380
+ };
381
+ delete clientRepo.token;
382
+ return clientRepo;
383
+ }
384
+
385
+ getReposForClient(repos = null) {
386
+ const sourceRepos = Array.isArray(repos) ? repos : this.loadRepos();
387
+ return sourceRepos.map(repo => this.toClientRepo(repo));
388
+ }
389
+
390
+ findStoredRepo(repo = {}) {
391
+ const repoId = String(repo.id || repo.repoId || '').trim();
392
+ const repos = this.loadRepos();
393
+
394
+ if (repoId) {
395
+ return repos.find(candidate => candidate.id === repoId) || null;
396
+ }
397
+
398
+ try {
399
+ const normalizedRepo = this.normalizeRepoConfig(repo);
400
+ return repos.find(candidate => candidate.id === normalizedRepo.id) || null;
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+
406
+ resolveRepoToken(repo = null) {
407
+ if (!repo || typeof repo !== 'object') return null;
408
+
409
+ const directToken = normalizeRepoToken(repo.token);
410
+ if (directToken) {
411
+ return directToken;
412
+ }
413
+
414
+ const storedRepo = this.findStoredRepo(repo);
415
+ if (!storedRepo) {
416
+ return null;
417
+ }
418
+
419
+ return normalizeRepoToken(storedRepo.token) || null;
420
+ }
421
+
361
422
  /**
362
423
  * 添加仓库
363
424
  * @param {Object} repo - 仓库配置
@@ -435,6 +496,43 @@ class SkillService {
435
496
  return this.loadRepos();
436
497
  }
437
498
 
499
+ updateRepoAuth(owner, name, directory = '', token = '', clearToken = false, repoId = '') {
500
+ const repos = this.loadRepos();
501
+ const normalizedDirectory = normalizeRepoDirectory(directory);
502
+ const repo = repos.find(r => {
503
+ if (repoId) {
504
+ return r.id === repoId;
505
+ }
506
+ return (
507
+ (r.owner || '') === owner &&
508
+ (r.name || '') === name &&
509
+ normalizeRepoDirectory(r.directory) === normalizedDirectory
510
+ );
511
+ });
512
+
513
+ if (!repo) {
514
+ throw new Error('Repository not found');
515
+ }
516
+
517
+ if (repo.provider === 'local') {
518
+ throw new Error('Local repository does not support token auth');
519
+ }
520
+
521
+ if (clearToken) {
522
+ delete repo.token;
523
+ } else {
524
+ const normalizedToken = normalizeRepoToken(token);
525
+ if (!normalizedToken) {
526
+ throw new Error('Missing token');
527
+ }
528
+ repo.token = normalizedToken;
529
+ }
530
+
531
+ this.saveRepos(repos);
532
+ this.clearCache({ removeFile: true });
533
+ return this.loadRepos();
534
+ }
535
+
438
536
  /**
439
537
  * 获取所有技能列表(带缓存)
440
538
  */
@@ -872,7 +970,18 @@ class SkillService {
872
970
  }
873
971
  }
874
972
 
875
- getGitHubToken(host = DEFAULT_GITHUB_HOST) {
973
+ getGitHubToken(repoOrHost = DEFAULT_GITHUB_HOST) {
974
+ if (repoOrHost && typeof repoOrHost === 'object') {
975
+ const repoToken = this.resolveRepoToken(repoOrHost);
976
+ if (repoToken) {
977
+ return repoToken;
978
+ }
979
+ }
980
+
981
+ const host = typeof repoOrHost === 'string'
982
+ ? repoOrHost
983
+ : (repoOrHost?.host || DEFAULT_GITHUB_HOST);
984
+
876
985
  // 优先从环境变量获取
877
986
  if (process.env.GITHUB_TOKEN) {
878
987
  return process.env.GITHUB_TOKEN;
@@ -899,7 +1008,18 @@ class SkillService {
899
1008
  return this.getTokenFromGitCredential(host);
900
1009
  }
901
1010
 
902
- getGitLabToken(host = DEFAULT_GITLAB_HOST) {
1011
+ getGitLabToken(repoOrHost = DEFAULT_GITLAB_HOST) {
1012
+ if (repoOrHost && typeof repoOrHost === 'object') {
1013
+ const repoToken = this.resolveRepoToken(repoOrHost);
1014
+ if (repoToken) {
1015
+ return repoToken;
1016
+ }
1017
+ }
1018
+
1019
+ const host = typeof repoOrHost === 'string'
1020
+ ? repoOrHost
1021
+ : (repoOrHost?.host || DEFAULT_GITLAB_HOST);
1022
+
903
1023
  if (process.env.GITLAB_TOKEN) {
904
1024
  return process.env.GITLAB_TOKEN;
905
1025
  }
@@ -931,8 +1051,8 @@ class SkillService {
931
1051
  /**
932
1052
  * 通用 GitHub API 请求
933
1053
  */
934
- async fetchGitHubApi(url) {
935
- const token = this.getGitHubToken(url);
1054
+ async fetchGitHubApi(url, repo = null) {
1055
+ const token = this.getGitHubToken(repo || url);
936
1056
  const headers = {
937
1057
  'User-Agent': 'cc-cli-skill-service',
938
1058
  'Accept': 'application/vnd.github.v3+json'
@@ -971,15 +1091,15 @@ class SkillService {
971
1091
 
972
1092
  async fetchGitHubRepoTree(repo) {
973
1093
  const treeUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/git/trees/${repo.branch}?recursive=1`;
974
- const tree = await this.fetchGitHubApi(treeUrl);
1094
+ const tree = await this.fetchGitHubApi(treeUrl, repo);
975
1095
  if (tree?.truncated) {
976
1096
  console.warn(`[SkillService] GitHub tree truncated for ${repo.owner}/${repo.name}`);
977
1097
  }
978
1098
  return tree?.tree || [];
979
1099
  }
980
1100
 
981
- async fetchGitLabApi(url, { raw = false } = {}) {
982
- const token = this.getGitLabToken(url);
1101
+ async fetchGitLabApi(url, { raw = false, repo = null } = {}) {
1102
+ const token = this.getGitLabToken(repo || url);
983
1103
  const headers = {
984
1104
  'User-Agent': 'cc-cli-skill-service'
985
1105
  };
@@ -1033,7 +1153,7 @@ class SkillService {
1033
1153
 
1034
1154
  while (page) {
1035
1155
  const url = `${repo.host}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(repo.branch)}&recursive=true&per_page=100&page=${page}`;
1036
- const response = await this.fetchGitLabApi(url);
1156
+ const response = await this.fetchGitLabApi(url, { repo });
1037
1157
  tree.push(...(response.data || []).map(item => ({
1038
1158
  ...item,
1039
1159
  type: item.type === 'tree' ? 'tree' : 'blob'
@@ -1050,21 +1170,26 @@ class SkillService {
1050
1170
  const projectId = encodeURIComponent(repo.projectPath);
1051
1171
  const normalizedFilePath = encodeURIComponent(normalizeRepoPath(filePath));
1052
1172
  const url = `${repo.host}/api/v4/projects/${projectId}/repository/files/${normalizedFilePath}/raw?ref=${encodeURIComponent(repo.branch)}`;
1053
- return this.fetchGitLabApi(url, { raw: true });
1173
+ return this.fetchGitLabApi(url, { raw: true, repo });
1054
1174
  }
1055
1175
 
1056
1176
  /**
1057
1177
  * 使用 GitHub API 获取目录内容
1058
1178
  */
1059
- async fetchGitHubContents(owner, name, path, branch) {
1179
+ async fetchGitHubContents(owner, name, path, branch, repo = null) {
1060
1180
  const url = `https://api.github.com/repos/${owner}/${name}/contents/${path}?ref=${branch}`;
1181
+ const token = this.getGitHubToken(repo || url);
1182
+ const headers = {
1183
+ 'User-Agent': 'cc-cli-skill-service',
1184
+ 'Accept': 'application/vnd.github.v3+json'
1185
+ };
1186
+ if (token) {
1187
+ headers.Authorization = `token ${token}`;
1188
+ }
1061
1189
 
1062
1190
  return new Promise((resolve, reject) => {
1063
1191
  const req = https.get(url, {
1064
- headers: {
1065
- 'User-Agent': 'cc-cli-skill-service',
1066
- 'Accept': 'application/vnd.github.v3+json'
1067
- },
1192
+ headers,
1068
1193
  timeout: 15000
1069
1194
  }, (res) => {
1070
1195
  let data = '';
@@ -1136,7 +1261,7 @@ class SkillService {
1136
1261
  if (dir.name.startsWith('.') || dir.name === 'node_modules') continue;
1137
1262
 
1138
1263
  try {
1139
- const subContents = await this.fetchGitHubContents(repo.owner, repo.name, dir.path, repo.branch);
1264
+ const subContents = await this.fetchGitHubContents(repo.owner, repo.name, dir.path, repo.branch, repo);
1140
1265
  await this.scanRepoContents(subContents, repo, dir.path, skills);
1141
1266
  } catch (err) {
1142
1267
  // 忽略子目录错误,继续扫描
@@ -1359,13 +1484,13 @@ class SkillService {
1359
1484
  if (normalizedRepo.provider === 'gitlab') {
1360
1485
  const projectId = encodeURIComponent(normalizedRepo.projectPath);
1361
1486
  zipUrl = `${normalizedRepo.host}/api/v4/projects/${projectId}/repository/archive.zip?sha=${encodeURIComponent(normalizedRepo.branch)}`;
1362
- const token = this.getGitLabToken(normalizedRepo.host);
1487
+ const token = this.getGitLabToken(normalizedRepo);
1363
1488
  if (token) {
1364
1489
  zipHeaders['PRIVATE-TOKEN'] = token;
1365
1490
  }
1366
1491
  } else {
1367
1492
  zipUrl = `https://api.github.com/repos/${normalizedRepo.owner}/${normalizedRepo.name}/zipball/${encodeURIComponent(normalizedRepo.branch)}`;
1368
- const token = this.getGitHubToken(normalizedRepo.host);
1493
+ const token = this.getGitHubToken(normalizedRepo);
1369
1494
  zipHeaders.Accept = 'application/vnd.github+json';
1370
1495
  if (token) {
1371
1496
  zipHeaders.Authorization = `token ${token}`;