@tkpdx01/ccc 1.3.7 → 1.6.0

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.
package/src/launch.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import { spawn } from 'child_process';
4
- import {
5
- getProfiles,
6
- getDefaultProfile,
7
- profileExists,
8
- getProfilePath
4
+ import {
5
+ getAllProfiles,
6
+ getDefaultProfile,
7
+ profileExists,
8
+ codexProfileExists,
9
+ getProfilePath,
10
+ getCodexProfileDir,
11
+ getCodexProfileCredentials
9
12
  } from './profiles.js';
10
13
 
11
14
  // 启动 claude
@@ -37,22 +40,73 @@ export function launchClaude(profileName, dangerouslySkipPermissions = false) {
37
40
  });
38
41
  }
39
42
 
43
+ // 启动 codex
44
+ export function launchCodex(profileName, dangerouslySkipPermissions = false) {
45
+ const codexHome = getCodexProfileDir(profileName);
46
+
47
+ if (!codexProfileExists(profileName)) {
48
+ console.log(chalk.red(`Profile "${profileName}" 不存在`));
49
+ console.log(chalk.yellow(`使用 "ccc list" 查看可用的 profiles`));
50
+ process.exit(1);
51
+ }
52
+
53
+ const { baseUrl } = getCodexProfileCredentials(profileName);
54
+
55
+ const args = [];
56
+ if (dangerouslySkipPermissions) {
57
+ args.push('--full-auto');
58
+ }
59
+
60
+ // 构建进程环境变量
61
+ const env = { ...process.env, CODEX_HOME: codexHome };
62
+ if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
63
+ env.OPENAI_BASE_URL = baseUrl;
64
+ }
65
+
66
+ console.log(chalk.green(`启动 Codex,使用配置: ${profileName}`));
67
+ console.log(chalk.gray(`CODEX_HOME=${codexHome} codex ${args.join(' ')}`));
68
+
69
+ const child = spawn('codex', args, {
70
+ stdio: 'inherit',
71
+ shell: true,
72
+ env
73
+ });
74
+
75
+ child.on('error', (err) => {
76
+ console.log(chalk.red(`启动失败: ${err.message}`));
77
+ process.exit(1);
78
+ });
79
+ }
80
+
81
+ // 根据 profile 类型自动选择启动方式
82
+ export function launchProfile(profileName, type, dangerouslySkipPermissions = false) {
83
+ if (type === 'codex') {
84
+ launchCodex(profileName, dangerouslySkipPermissions);
85
+ } else {
86
+ launchClaude(profileName, dangerouslySkipPermissions);
87
+ }
88
+ }
89
+
40
90
  // 交互式选择 profile
41
91
  export async function selectProfile(dangerouslySkipPermissions = false) {
42
- const profiles = getProfiles();
92
+ const allProfiles = getAllProfiles();
43
93
 
44
- if (profiles.length === 0) {
94
+ if (allProfiles.length === 0) {
45
95
  console.log(chalk.yellow('没有可用的 profiles'));
46
- console.log(chalk.gray('使用 "ccc import" 导入配置'));
96
+ console.log(chalk.gray('使用 "ccc new" 创建配置'));
47
97
  process.exit(0);
48
98
  }
49
99
 
50
100
  const defaultProfile = getDefaultProfile();
51
101
 
52
- const choices = profiles.map((p, index) => ({
53
- name: p === defaultProfile ? `${index + 1}. ${p} ${chalk.green('(默认)')}` : `${index + 1}. ${p}`,
54
- value: p
55
- }));
102
+ const choices = allProfiles.map((p, index) => {
103
+ const typeTag = p.type === 'codex' ? chalk.blue('[Codex]') : chalk.magenta('[Claude]');
104
+ const isDefault = p.name === defaultProfile;
105
+ const label = isDefault
106
+ ? `${index + 1}. ${typeTag} ${p.name} ${chalk.green('(默认)')}`
107
+ : `${index + 1}. ${typeTag} ${p.name}`;
108
+ return { name: label, value: p };
109
+ });
56
110
 
57
111
  const { profile } = await inquirer.prompt([
58
112
  {
@@ -60,10 +114,9 @@ export async function selectProfile(dangerouslySkipPermissions = false) {
60
114
  name: 'profile',
61
115
  message: '选择要使用的配置:',
62
116
  choices,
63
- default: defaultProfile
117
+ default: allProfiles.findIndex(p => p.name === defaultProfile)
64
118
  }
65
119
  ]);
66
120
 
67
- launchClaude(profile, dangerouslySkipPermissions);
121
+ launchProfile(profile.name, profile.type, dangerouslySkipPermissions);
68
122
  }
69
-
package/src/profiles.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
- import { CONFIG_DIR, PROFILES_DIR, DEFAULT_FILE, CLAUDE_SETTINGS_PATH } from './config.js';
4
+ import { CONFIG_DIR, PROFILES_DIR, CODEX_PROFILES_DIR, DEFAULT_FILE, CLAUDE_SETTINGS_PATH, CODEX_HOME_PATH } from './config.js';
5
5
 
6
6
  function stringifyClaudeSettings(settings) {
7
7
  // Claude Code 默认 settings.json 使用 2 空格缩进,并以换行结尾(便于 diff/兼容各平台编辑器)
@@ -16,6 +16,9 @@ export function ensureDirs() {
16
16
  if (!fs.existsSync(PROFILES_DIR)) {
17
17
  fs.mkdirSync(PROFILES_DIR, { recursive: true });
18
18
  }
19
+ if (!fs.existsSync(CODEX_PROFILES_DIR)) {
20
+ fs.mkdirSync(CODEX_PROFILES_DIR, { recursive: true });
21
+ }
19
22
  }
20
23
 
21
24
  // 获取所有 profiles(按 a-z 排序)
@@ -135,7 +138,8 @@ export function ensureRequiredClaudeEnvSettings() {
135
138
  return ensureClaudeEnvSettings({
136
139
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
137
140
  CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
138
- DISABLE_INSTALLATION_CHECKS: '1'
141
+ DISABLE_INSTALLATION_CHECKS: '1',
142
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1'
139
143
  });
140
144
  }
141
145
 
@@ -157,6 +161,12 @@ export function applyClaudeSettingsExtras(target) {
157
161
 
158
162
  let changed = false;
159
163
 
164
+ // 确保 hasCompletedOnboarding 为 true
165
+ if (target.hasCompletedOnboarding !== true) {
166
+ target.hasCompletedOnboarding = true;
167
+ changed = true;
168
+ }
169
+
160
170
  // 确保 attribution 禁用(commit/pr 为空字符串)
161
171
  if (!target.attribution || typeof target.attribution !== 'object' || Array.isArray(target.attribution)) {
162
172
  target.attribution = { commit: '', pr: '' };
@@ -246,6 +256,7 @@ export function createProfileFromTemplate(name, apiUrl, apiKey) {
246
256
  template.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
247
257
  template.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0';
248
258
  template.env.DISABLE_INSTALLATION_CHECKS = '1';
259
+ template.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
249
260
 
250
261
  // 只设置 API 凭证到 env
251
262
  template.env.ANTHROPIC_AUTH_TOKEN = apiKey;
@@ -284,6 +295,7 @@ export function syncProfileWithTemplate(name) {
284
295
  newProfile.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
285
296
  newProfile.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0';
286
297
  newProfile.env.DISABLE_INSTALLATION_CHECKS = '1';
298
+ newProfile.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
287
299
  applyClaudeSettingsExtras(newProfile);
288
300
 
289
301
  saveProfile(name, newProfile);
@@ -319,3 +331,221 @@ export function clearDefaultProfile() {
319
331
  fs.unlinkSync(DEFAULT_FILE);
320
332
  }
321
333
  }
334
+
335
+ // ============================================================
336
+ // Codex Profile 管理
337
+ // ============================================================
338
+
339
+ // 获取 Codex profile 目录路径
340
+ export function getCodexProfileDir(name) {
341
+ return path.join(CODEX_PROFILES_DIR, name);
342
+ }
343
+
344
+ // 检查 Codex profile 是否存在
345
+ export function codexProfileExists(name) {
346
+ const dir = getCodexProfileDir(name);
347
+ return fs.existsSync(path.join(dir, 'auth.json'));
348
+ }
349
+
350
+ // 获取所有 Codex profiles(按 a-z 排序)
351
+ export function getCodexProfiles() {
352
+ ensureDirs();
353
+ if (!fs.existsSync(CODEX_PROFILES_DIR)) return [];
354
+ return fs.readdirSync(CODEX_PROFILES_DIR, { withFileTypes: true })
355
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(CODEX_PROFILES_DIR, d.name, 'auth.json')))
356
+ .map(d => d.name)
357
+ .sort((a, b) => a.localeCompare(b, 'zh-CN', { sensitivity: 'base' }));
358
+ }
359
+
360
+ // 读取 Codex profile
361
+ export function readCodexProfile(name) {
362
+ const dir = getCodexProfileDir(name);
363
+ const authPath = path.join(dir, 'auth.json');
364
+ const configPath = path.join(dir, 'config.toml');
365
+
366
+ if (!fs.existsSync(authPath)) return null;
367
+
368
+ try {
369
+ const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
370
+ const configToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
371
+ return { auth, configToml };
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+
377
+ // 保存 Codex profile
378
+ export function saveCodexProfile(name, auth, configToml) {
379
+ ensureDirs();
380
+ const dir = getCodexProfileDir(name);
381
+ if (!fs.existsSync(dir)) {
382
+ fs.mkdirSync(dir, { recursive: true });
383
+ }
384
+ fs.writeFileSync(path.join(dir, 'auth.json'), JSON.stringify(auth, null, 2) + '\n');
385
+ fs.writeFileSync(path.join(dir, 'config.toml'), configToml);
386
+ }
387
+
388
+ // 生成 Codex config.toml 内容
389
+ export function generateCodexConfigToml(baseUrl, model) {
390
+ let lines = ['# Codex profile managed by ccc'];
391
+
392
+ if (model) {
393
+ lines.push(`model = "${model}"`);
394
+ }
395
+
396
+ if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
397
+ lines.push('');
398
+ lines.push('[model_providers.openai]');
399
+ lines.push(`name = "OpenAI"`);
400
+ lines.push(`base_url = "${baseUrl}"`);
401
+ }
402
+
403
+ lines.push('');
404
+ return lines.join('\n');
405
+ }
406
+
407
+ // 创建 Codex profile
408
+ export function createCodexProfile(name, apiKey, baseUrl, model) {
409
+ const auth = {
410
+ auth_mode: 'apikey',
411
+ OPENAI_API_KEY: apiKey
412
+ };
413
+ const configToml = generateCodexConfigToml(baseUrl, model);
414
+ saveCodexProfile(name, auth, configToml);
415
+ return { auth, configToml };
416
+ }
417
+
418
+ // 获取 Codex profile 的凭证
419
+ export function getCodexProfileCredentials(name) {
420
+ const profile = readCodexProfile(name);
421
+ if (!profile) return { apiKey: '', baseUrl: '', model: '' };
422
+
423
+ const apiKey = profile.auth?.OPENAI_API_KEY || '';
424
+
425
+ // 从 config.toml 解析 base_url 和 model
426
+ let baseUrl = '';
427
+ let model = '';
428
+ if (profile.configToml) {
429
+ const baseUrlMatch = profile.configToml.match(/base_url\s*=\s*"([^"]+)"/);
430
+ if (baseUrlMatch) baseUrl = baseUrlMatch[1];
431
+ const modelMatch = profile.configToml.match(/^model\s*=\s*"([^"]+)"/m);
432
+ if (modelMatch) model = modelMatch[1];
433
+ }
434
+
435
+ return { apiKey, baseUrl: baseUrl || 'https://api.openai.com/v1', model: model || '' };
436
+ }
437
+
438
+ // 删除 Codex profile
439
+ export function deleteCodexProfile(name) {
440
+ const dir = getCodexProfileDir(name);
441
+ if (fs.existsSync(dir)) {
442
+ fs.rmSync(dir, { recursive: true });
443
+ }
444
+ }
445
+
446
+ // 同步 Codex profile(从 ~/.codex/ 模板同步,保留 API 凭证)
447
+ export function syncCodexProfileWithTemplate(name) {
448
+ const templateConfigPath = path.join(CODEX_HOME_PATH, 'config.toml');
449
+ if (!fs.existsSync(templateConfigPath)) return null;
450
+
451
+ const current = readCodexProfile(name);
452
+ if (!current) return null;
453
+
454
+ // 读取模板 config.toml
455
+ let templateConfig = fs.readFileSync(templateConfigPath, 'utf-8');
456
+
457
+ // 保留当前 profile 的 base_url 和 model
458
+ const { baseUrl, model } = getCodexProfileCredentials(name);
459
+
460
+ // 在模板基础上覆盖 base_url 和 model
461
+ // 如果当前 profile 有自定义 base_url,追加到模板
462
+ if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
463
+ // 检查模板是否已有 [model_providers.openai] 节
464
+ if (templateConfig.includes('[model_providers.openai]')) {
465
+ templateConfig = templateConfig.replace(
466
+ /(\[model_providers\.openai\][^\[]*?)base_url\s*=\s*"[^"]*"/,
467
+ `$1base_url = "${baseUrl}"`
468
+ );
469
+ } else {
470
+ templateConfig += `\n[model_providers.openai]\nbase_url = "${baseUrl}"\n`;
471
+ }
472
+ }
473
+
474
+ if (model) {
475
+ if (templateConfig.match(/^model\s*=/m)) {
476
+ templateConfig = templateConfig.replace(/^model\s*=\s*"[^"]*"/m, `model = "${model}"`);
477
+ } else {
478
+ templateConfig = `model = "${model}"\n` + templateConfig;
479
+ }
480
+ }
481
+
482
+ saveCodexProfile(name, current.auth, templateConfig);
483
+ return { auth: current.auth, configToml: templateConfig };
484
+ }
485
+
486
+ // 应用 Claude profile 到 ~/.claude/settings.json
487
+ export function applyClaudeProfile(name) {
488
+ const profile = readProfile(name);
489
+ if (!profile) return false;
490
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, stringifyClaudeSettings(profile));
491
+ return true;
492
+ }
493
+
494
+ // 应用 Codex profile 到 ~/.codex/
495
+ export function applyCodexProfile(name) {
496
+ const profile = readCodexProfile(name);
497
+ if (!profile) return false;
498
+
499
+ if (!fs.existsSync(CODEX_HOME_PATH)) {
500
+ fs.mkdirSync(CODEX_HOME_PATH, { recursive: true });
501
+ }
502
+
503
+ // 写入 auth.json
504
+ fs.writeFileSync(
505
+ path.join(CODEX_HOME_PATH, 'auth.json'),
506
+ JSON.stringify(profile.auth, null, 2) + '\n'
507
+ );
508
+
509
+ // 写入 config.toml(如果有内容)
510
+ if (profile.configToml && profile.configToml.trim()) {
511
+ fs.writeFileSync(path.join(CODEX_HOME_PATH, 'config.toml'), profile.configToml);
512
+ }
513
+
514
+ return true;
515
+ }
516
+
517
+ // ============================================================
518
+ // 统一 Profile 管理(Claude + Codex 混合)
519
+ // ============================================================
520
+
521
+ // 获取所有 profiles(混合 Claude 和 Codex),按名称排序
522
+ export function getAllProfiles() {
523
+ const claudeProfiles = getProfiles().map(name => ({ name, type: 'claude' }));
524
+ const codexProfiles = getCodexProfiles().map(name => ({ name, type: 'codex' }));
525
+ return [...claudeProfiles, ...codexProfiles]
526
+ .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'base' }));
527
+ }
528
+
529
+ // 根据序号或名称解析 profile(统一)
530
+ export function resolveAnyProfile(input) {
531
+ const all = getAllProfiles();
532
+ const map = {};
533
+ all.forEach((p, i) => { map[i + 1] = p; });
534
+
535
+ // 尝试作为数字序号
536
+ const num = parseInt(input, 10);
537
+ if (!isNaN(num) && map[num]) {
538
+ return map[num];
539
+ }
540
+
541
+ // 作为名称查找
542
+ const found = all.find(p => p.name === input);
543
+ return found || null;
544
+ }
545
+
546
+ // 检查任意类型 profile 是否存在
547
+ export function anyProfileExists(name) {
548
+ if (profileExists(name)) return { exists: true, type: 'claude' };
549
+ if (codexProfileExists(name)) return { exists: true, type: 'codex' };
550
+ return { exists: false, type: null };
551
+ }
package/src/utils.js CHANGED
@@ -1,56 +1,3 @@
1
- // 从文本中提取 URL 和 token
2
- export function extractFromText(text) {
3
- // 提取 URL
4
- const urlRegex = /https?:\/\/[^\s"'<>]+/gi;
5
- const urls = text.match(urlRegex) || [];
6
-
7
- // 提取 sk token
8
- const tokenRegex = /sk-[a-zA-Z0-9_-]+/g;
9
- const tokens = text.match(tokenRegex) || [];
10
-
11
- return { urls, tokens };
12
- }
13
-
14
- // 从 URL 获取域名作为名称(去掉协议和www子域名)
15
- export function getDomainName(url) {
16
- try {
17
- const urlObj = new URL(url);
18
- // 获取完整域名
19
- let hostname = urlObj.hostname;
20
- // 移除 www. 前缀
21
- hostname = hostname.replace(/^www\./, '');
22
- return hostname;
23
- } catch {
24
- return null;
25
- }
26
- }
27
-
28
- // 生成安全的 profile 名称
29
- export function sanitizeProfileName(name) {
30
- return name
31
- .replace(/[<>:"/\\|?*]/g, '_') // 替换 Windows 非法字符
32
- .replace(/\s+/g, '_') // 替换空格
33
- .replace(/_+/g, '_') // 合并多个下划线
34
- .replace(/^_|_$/g, '') // 去除首尾下划线
35
- .substring(0, 50); // 限制长度
36
- }
37
-
38
- // 将导入的配置转换为影子配置格式(只包含 API 凭证)
39
- export function convertToClaudeSettings(provider, template) {
40
- const config = provider.settingsConfig || {};
41
-
42
- // 从 env 中提取 API 信息
43
- const env = config.env || {};
44
- const apiKey = env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || config.apiKey || '';
45
- const apiUrl = env.ANTHROPIC_BASE_URL || config.apiUrl || provider.websiteUrl || '';
46
-
47
- // 影子配置只存储 API 凭证,不用 env 包裹
48
- return {
49
- ANTHROPIC_AUTH_TOKEN: apiKey,
50
- ANTHROPIC_BASE_URL: apiUrl
51
- };
52
- }
53
-
54
1
  // 格式化显示配置值
55
2
  export function formatValue(key, value) {
56
3
  if ((key === 'apiKey' || key === 'ANTHROPIC_AUTH_TOKEN') && value) {