coding-tool-x 3.3.7 → 3.3.9

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 (89) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
  5. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  6. package/dist/web/assets/Home-D7KX7iF8.js +1 -0
  7. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
  8. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
  9. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
  10. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
  12. package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
  13. package/dist/web/assets/index-CL-qpoJ_.js +2 -0
  14. package/dist/web/assets/index-D_5dRFOL.css +1 -0
  15. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  16. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  17. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  18. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  19. package/dist/web/index.html +7 -7
  20. package/docs/home.png +0 -0
  21. package/package.json +14 -5
  22. package/src/commands/daemon.js +3 -2
  23. package/src/commands/security.js +1 -2
  24. package/src/commands/toggle-proxy.js +100 -5
  25. package/src/config/paths.js +718 -90
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/channels.js +9 -0
  28. package/src/server/api/claude-hooks.js +13 -8
  29. package/src/server/api/codex-channels.js +9 -0
  30. package/src/server/api/codex-proxy.js +27 -15
  31. package/src/server/api/gemini-proxy.js +22 -11
  32. package/src/server/api/hooks.js +45 -0
  33. package/src/server/api/oauth-credentials.js +163 -0
  34. package/src/server/api/opencode-proxy.js +22 -10
  35. package/src/server/api/plugins.js +2 -1
  36. package/src/server/api/proxy.js +39 -44
  37. package/src/server/api/skills.js +91 -13
  38. package/src/server/api/ui-config.js +5 -0
  39. package/src/server/codex-proxy-server.js +90 -70
  40. package/src/server/gemini-proxy-server.js +107 -88
  41. package/src/server/index.js +2 -0
  42. package/src/server/opencode-proxy-server.js +381 -225
  43. package/src/server/proxy-server.js +86 -60
  44. package/src/server/services/alias.js +3 -3
  45. package/src/server/services/channels.js +21 -24
  46. package/src/server/services/codex-channels.js +158 -255
  47. package/src/server/services/codex-config.js +2 -5
  48. package/src/server/services/codex-env-manager.js +423 -0
  49. package/src/server/services/codex-settings-manager.js +21 -357
  50. package/src/server/services/codex-statistics-service.js +3 -27
  51. package/src/server/services/config-export-service.js +43 -9
  52. package/src/server/services/config-registry-service.js +3 -2
  53. package/src/server/services/config-sync-manager.js +1 -1
  54. package/src/server/services/favorites.js +4 -3
  55. package/src/server/services/gemini-channels.js +14 -12
  56. package/src/server/services/gemini-statistics-service.js +3 -25
  57. package/src/server/services/mcp-service.js +35 -19
  58. package/src/server/services/model-detector.js +4 -3
  59. package/src/server/services/native-keychain.js +243 -0
  60. package/src/server/services/native-oauth-adapters.js +891 -0
  61. package/src/server/services/network-access.js +39 -1
  62. package/src/server/services/notification-hooks.js +951 -0
  63. package/src/server/services/oauth-credentials-service.js +786 -0
  64. package/src/server/services/oauth-utils.js +49 -0
  65. package/src/server/services/opencode-channels.js +19 -15
  66. package/src/server/services/opencode-sessions.js +2 -2
  67. package/src/server/services/opencode-settings-manager.js +169 -16
  68. package/src/server/services/opencode-statistics-service.js +3 -27
  69. package/src/server/services/plugins-service.js +115 -15
  70. package/src/server/services/prompts-service.js +2 -3
  71. package/src/server/services/proxy-log-helper.js +242 -0
  72. package/src/server/services/proxy-runtime.js +6 -4
  73. package/src/server/services/repo-scanner-base.js +12 -4
  74. package/src/server/services/request-logger.js +7 -7
  75. package/src/server/services/security-config.js +4 -4
  76. package/src/server/services/session-cache.js +2 -2
  77. package/src/server/services/sessions.js +2 -2
  78. package/src/server/services/settings-manager.js +13 -0
  79. package/src/server/services/skill-service.js +867 -368
  80. package/src/server/services/statistics-service.js +5 -5
  81. package/src/server/services/ui-config.js +4 -3
  82. package/src/server/services/workspace-service.js +1 -1
  83. package/src/server/websocket-server.js +5 -4
  84. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  85. package/dist/web/assets/Home-obifg_9E.js +0 -1
  86. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  87. package/dist/web/assets/index-eEmjZKWP.css +0 -1
  88. package/docs/bannel.png +0 -0
  89. package/docs/model-redirection.md +0 -251
@@ -0,0 +1,423 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+ const { PATHS, HOME_DIR } = require('../../config/paths');
5
+
6
+ const PROFILE_MARKER_START = '# >>> coding-tool codex env >>>';
7
+ const PROFILE_MARKER_END = '# <<< coding-tool codex env <<<';
8
+
9
+ function defaultEnvFilePath(configDir) {
10
+ return path.join(configDir, 'codex-env.sh');
11
+ }
12
+
13
+ function defaultStateFilePath(configDir) {
14
+ return path.join(configDir, 'codex-env-state.json');
15
+ }
16
+
17
+ function ensureDir(dirPath) {
18
+ if (!fs.existsSync(dirPath)) {
19
+ fs.mkdirSync(dirPath, { recursive: true });
20
+ }
21
+ }
22
+
23
+ function hasValue(value) {
24
+ return String(value || '').trim() !== '';
25
+ }
26
+
27
+ function shellQuote(value) {
28
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
29
+ }
30
+
31
+ function powershellQuote(value) {
32
+ return `'${String(value).replace(/'/g, "''")}'`;
33
+ }
34
+
35
+ function buildHomeRelativeShellPath(filePath, homeDir) {
36
+ const normalizedHome = path.resolve(homeDir);
37
+ const normalizedFilePath = path.resolve(filePath);
38
+ if (normalizedFilePath === normalizedHome) {
39
+ return '$HOME';
40
+ }
41
+ if (normalizedFilePath.startsWith(`${normalizedHome}${path.sep}`)) {
42
+ const relativePath = normalizedFilePath.slice(normalizedHome.length).replace(/\\/g, '/');
43
+ return `$HOME${relativePath}`;
44
+ }
45
+ return normalizedFilePath.replace(/\\/g, '/');
46
+ }
47
+
48
+ function buildSourceSnippet(envFilePath, homeDir) {
49
+ const shellPath = buildHomeRelativeShellPath(envFilePath, homeDir);
50
+ return [
51
+ PROFILE_MARKER_START,
52
+ `[ -f "${shellPath}" ] && . "${shellPath}"`,
53
+ PROFILE_MARKER_END
54
+ ].join('\n');
55
+ }
56
+
57
+ function stripManagedBlock(content = '') {
58
+ if (!content.includes(PROFILE_MARKER_START)) {
59
+ return content;
60
+ }
61
+ const pattern = new RegExp(
62
+ `\\n?${escapeRegex(PROFILE_MARKER_START)}[\\s\\S]*?${escapeRegex(PROFILE_MARKER_END)}\\n?`,
63
+ 'g'
64
+ );
65
+ return content.replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
66
+ }
67
+
68
+ function escapeRegex(value) {
69
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
70
+ }
71
+
72
+ function upsertManagedBlock(content, snippet) {
73
+ const stripped = stripManagedBlock(content);
74
+ if (!stripped.trim()) {
75
+ return `${snippet}\n`;
76
+ }
77
+ return `${stripped.trimEnd()}\n\n${snippet}\n`;
78
+ }
79
+
80
+ function readJsonFile(filePath, fallbackValue) {
81
+ try {
82
+ if (!fs.existsSync(filePath)) {
83
+ return fallbackValue;
84
+ }
85
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
86
+ } catch {
87
+ return fallbackValue;
88
+ }
89
+ }
90
+
91
+ function writeJsonFile(filePath, payload) {
92
+ ensureDir(path.dirname(filePath));
93
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
94
+ }
95
+
96
+ function readState(stateFilePath) {
97
+ const state = readJsonFile(stateFilePath, {
98
+ version: 1,
99
+ values: {},
100
+ profiles: []
101
+ });
102
+ return {
103
+ version: 1,
104
+ values: state && typeof state.values === 'object' ? state.values : {},
105
+ profiles: Array.isArray(state?.profiles) ? state.profiles : []
106
+ };
107
+ }
108
+
109
+ function normalizeEnvMap(envMap = {}) {
110
+ const normalized = {};
111
+ for (const [key, value] of Object.entries(envMap || {})) {
112
+ if (!key || !hasValue(value)) continue;
113
+ normalized[String(key).trim()] = String(value);
114
+ }
115
+ return normalized;
116
+ }
117
+
118
+ function buildNextEnvValues(previousValues, envMap, { replace = true, removeKeys = [] } = {}) {
119
+ const nextValues = replace
120
+ ? {}
121
+ : { ...previousValues };
122
+
123
+ if (replace) {
124
+ Object.assign(nextValues, normalizeEnvMap(envMap));
125
+ } else {
126
+ const normalizedIncoming = normalizeEnvMap(envMap);
127
+ for (const [key, value] of Object.entries(normalizedIncoming)) {
128
+ nextValues[key] = value;
129
+ }
130
+ }
131
+
132
+ for (const key of removeKeys || []) {
133
+ delete nextValues[key];
134
+ }
135
+
136
+ for (const [key, value] of Object.entries(nextValues)) {
137
+ if (!hasValue(value)) {
138
+ delete nextValues[key];
139
+ }
140
+ }
141
+
142
+ return nextValues;
143
+ }
144
+
145
+ function getPosixProfileCandidates(homeDir, shellEnv = process.env) {
146
+ const candidates = [
147
+ '.bashrc',
148
+ '.bash_profile',
149
+ '.bash_login',
150
+ '.zshrc',
151
+ '.zshenv',
152
+ '.zprofile',
153
+ '.zlogin',
154
+ '.profile'
155
+ ].map(fileName => path.join(homeDir, fileName));
156
+
157
+ const shell = String(shellEnv.SHELL || '').toLowerCase();
158
+ const preferred = shell.includes('zsh')
159
+ ? path.join(homeDir, '.zshrc')
160
+ : shell.includes('bash')
161
+ ? path.join(homeDir, '.bashrc')
162
+ : path.join(homeDir, '.profile');
163
+
164
+ return {
165
+ preferred,
166
+ candidates
167
+ };
168
+ }
169
+
170
+ function writeTextFileIfChanged(filePath, content) {
171
+ const previous = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
172
+ if (previous === content) {
173
+ return false;
174
+ }
175
+ ensureDir(path.dirname(filePath));
176
+ fs.writeFileSync(filePath, content, 'utf8');
177
+ return true;
178
+ }
179
+
180
+ function syncPosixEnvironment(nextValues, previousState, options) {
181
+ const {
182
+ runtime,
183
+ homeDir,
184
+ envFilePath,
185
+ stateFilePath,
186
+ shellEnv,
187
+ execSync
188
+ } = options;
189
+ const { preferred, candidates } = getPosixProfileCandidates(homeDir, shellEnv);
190
+ const nextKeys = Object.keys(nextValues).sort();
191
+ const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
192
+ const managedProfiles = new Set(previousProfiles);
193
+ const sourceSnippet = buildSourceSnippet(envFilePath, homeDir);
194
+ let changed = false;
195
+
196
+ if (nextKeys.length > 0) {
197
+ const envContent = [
198
+ '# Managed by Coding-Tool for Codex env_key providers',
199
+ ...nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`),
200
+ ''
201
+ ].join('\n');
202
+ changed = writeTextFileIfChanged(envFilePath, envContent) || changed;
203
+
204
+ const existingProfiles = candidates.filter(filePath => fs.existsSync(filePath));
205
+ if (managedProfiles.size === 0) {
206
+ if (existingProfiles.length > 0) {
207
+ for (const profilePath of existingProfiles) {
208
+ managedProfiles.add(profilePath);
209
+ }
210
+ } else {
211
+ managedProfiles.add(preferred);
212
+ }
213
+ }
214
+
215
+ for (const profilePath of managedProfiles) {
216
+ const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
217
+ const nextContent = upsertManagedBlock(currentContent, sourceSnippet);
218
+ changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
219
+ }
220
+ } else {
221
+ if (fs.existsSync(envFilePath)) {
222
+ fs.unlinkSync(envFilePath);
223
+ changed = true;
224
+ }
225
+
226
+ const cleanupTargets = new Set([
227
+ ...previousProfiles,
228
+ ...candidates.filter(filePath => fs.existsSync(filePath))
229
+ ]);
230
+
231
+ for (const profilePath of cleanupTargets) {
232
+ if (!fs.existsSync(profilePath)) continue;
233
+ const currentContent = fs.readFileSync(profilePath, 'utf8');
234
+ const nextContent = stripManagedBlock(currentContent);
235
+ const finalContent = nextContent ? `${nextContent}\n` : '';
236
+ changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
237
+ }
238
+ managedProfiles.clear();
239
+ }
240
+
241
+ for (const key of Object.keys(previousState.values || {})) {
242
+ if (!Object.prototype.hasOwnProperty.call(nextValues, key)) {
243
+ delete process.env[key];
244
+ }
245
+ }
246
+ for (const [key, value] of Object.entries(nextValues)) {
247
+ process.env[key] = value;
248
+ }
249
+
250
+ if (runtime === 'darwin') {
251
+ applyLaunchctlEnvironment(previousState.values || {}, nextValues, execSync);
252
+ }
253
+
254
+ if (nextKeys.length > 0) {
255
+ writeJsonFile(stateFilePath, {
256
+ version: 1,
257
+ values: nextValues,
258
+ profiles: Array.from(managedProfiles)
259
+ });
260
+ } else if (fs.existsSync(stateFilePath)) {
261
+ fs.unlinkSync(stateFilePath);
262
+ }
263
+
264
+ return {
265
+ changed,
266
+ reloadRequired: changed,
267
+ isFirstTime: Object.keys(previousState.values || {}).length === 0 && nextKeys.length > 0,
268
+ sourceCommand: nextKeys.length > 0 ? `source "${buildHomeRelativeShellPath(envFilePath, homeDir)}"` : null,
269
+ shellConfigPath: managedProfiles.size > 0 ? Array.from(managedProfiles)[0] : null,
270
+ shellConfigPaths: Array.from(managedProfiles),
271
+ envFilePath: nextKeys.length > 0 ? envFilePath : null,
272
+ managedKeys: nextKeys
273
+ };
274
+ }
275
+
276
+ function applyLaunchctlEnvironment(previousValues, nextValues, execSync) {
277
+ const previousKeys = new Set(Object.keys(previousValues || {}));
278
+ for (const [key, value] of Object.entries(nextValues || {})) {
279
+ if (previousValues[key] === value) {
280
+ previousKeys.delete(key);
281
+ continue;
282
+ }
283
+ runLaunchctlCommand(['setenv', key, value], execSync);
284
+ previousKeys.delete(key);
285
+ }
286
+ for (const key of previousKeys) {
287
+ runLaunchctlCommand(['unsetenv', key], execSync);
288
+ }
289
+ }
290
+
291
+ function runLaunchctlCommand(args, execSync) {
292
+ try {
293
+ execSync('launchctl', args, {
294
+ stdio: ['ignore', 'ignore', 'ignore'],
295
+ timeout: 3000
296
+ });
297
+ } catch {
298
+ // ignore launchctl failures; shell profile remains the durable source
299
+ }
300
+ }
301
+
302
+ function syncWindowsEnvironment(nextValues, previousState, options) {
303
+ const { stateFilePath, execSync } = options;
304
+ const nextKeys = Object.keys(nextValues).sort();
305
+ const previousValues = previousState.values || {};
306
+ let changed = false;
307
+
308
+ for (const [key, value] of Object.entries(nextValues)) {
309
+ if (previousValues[key] === value) {
310
+ process.env[key] = value;
311
+ continue;
312
+ }
313
+ setWindowsUserEnv(key, value, execSync);
314
+ process.env[key] = value;
315
+ changed = true;
316
+ }
317
+
318
+ for (const key of Object.keys(previousValues)) {
319
+ if (Object.prototype.hasOwnProperty.call(nextValues, key)) {
320
+ continue;
321
+ }
322
+ removeWindowsUserEnv(key, execSync);
323
+ delete process.env[key];
324
+ changed = true;
325
+ }
326
+
327
+ if (nextKeys.length > 0) {
328
+ writeJsonFile(stateFilePath, {
329
+ version: 1,
330
+ values: nextValues,
331
+ profiles: []
332
+ });
333
+ } else if (fs.existsSync(stateFilePath)) {
334
+ fs.unlinkSync(stateFilePath);
335
+ }
336
+
337
+ return {
338
+ changed,
339
+ reloadRequired: changed,
340
+ isFirstTime: Object.keys(previousValues).length === 0 && nextKeys.length > 0,
341
+ sourceCommand: null,
342
+ shellConfigPath: null,
343
+ shellConfigPaths: [],
344
+ envFilePath: null,
345
+ managedKeys: nextKeys
346
+ };
347
+ }
348
+
349
+ function runWindowsEnvCommand(script, execSync) {
350
+ const candidates = ['powershell', 'pwsh'];
351
+ let lastError = null;
352
+ for (const command of candidates) {
353
+ try {
354
+ execSync(command, ['-NoProfile', '-NonInteractive', '-Command', script], {
355
+ stdio: ['ignore', 'ignore', 'ignore'],
356
+ timeout: 5000
357
+ });
358
+ return;
359
+ } catch (error) {
360
+ lastError = error;
361
+ }
362
+ }
363
+ throw lastError || new Error('No PowerShell executable available');
364
+ }
365
+
366
+ function setWindowsUserEnv(key, value, execSync) {
367
+ runWindowsEnvCommand(
368
+ `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, ${powershellQuote(value)}, 'User')`,
369
+ execSync
370
+ );
371
+ }
372
+
373
+ function removeWindowsUserEnv(key, execSync) {
374
+ runWindowsEnvCommand(
375
+ `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, $null, 'User')`,
376
+ execSync
377
+ );
378
+ }
379
+
380
+ function syncCodexUserEnvironment(envMap = {}, options = {}) {
381
+ const configDir = options.configDir || PATHS.config;
382
+ const homeDir = options.homeDir || HOME_DIR;
383
+ const envFilePath = options.envFilePath || defaultEnvFilePath(configDir);
384
+ const stateFilePath = options.stateFilePath || defaultStateFilePath(configDir);
385
+ const runtime = options.runtime || process.platform;
386
+ const shellEnv = options.shellEnv || process.env;
387
+ const execSync = options.execFileSync || execFileSync;
388
+ const previousState = readState(stateFilePath);
389
+ const nextValues = buildNextEnvValues(previousState.values, envMap, {
390
+ replace: options.replace !== false,
391
+ removeKeys: options.removeKeys || []
392
+ });
393
+
394
+ const syncOptions = {
395
+ runtime,
396
+ homeDir,
397
+ envFilePath,
398
+ stateFilePath,
399
+ shellEnv,
400
+ execSync
401
+ };
402
+
403
+ if (runtime === 'win32') {
404
+ return syncWindowsEnvironment(nextValues, previousState, syncOptions);
405
+ }
406
+
407
+ return syncPosixEnvironment(nextValues, previousState, syncOptions);
408
+ }
409
+
410
+ module.exports = {
411
+ syncCodexUserEnvironment,
412
+ _test: {
413
+ buildHomeRelativeShellPath,
414
+ buildNextEnvValues,
415
+ buildSourceSnippet,
416
+ getPosixProfileCandidates,
417
+ readState,
418
+ shellQuote,
419
+ stripManagedBlock,
420
+ syncCodexUserEnvironment,
421
+ upsertManagedBlock
422
+ }
423
+ };