@hileeon/mcc 0.1.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.
Files changed (138) hide show
  1. package/.claude/CLAUDE.md +204 -0
  2. package/.claude/agents/.gitkeep +0 -0
  3. package/.claude/settings.json +9 -0
  4. package/.claude/skills/.gitkeep +0 -0
  5. package/README.md +127 -0
  6. package/dist/accounts/instance-manager.d.ts +11 -0
  7. package/dist/accounts/instance-manager.d.ts.map +1 -0
  8. package/dist/accounts/instance-manager.js +89 -0
  9. package/dist/accounts/instance-manager.js.map +1 -0
  10. package/dist/accounts/shared-manager.d.ts +25 -0
  11. package/dist/accounts/shared-manager.d.ts.map +1 -0
  12. package/dist/accounts/shared-manager.js +186 -0
  13. package/dist/accounts/shared-manager.js.map +1 -0
  14. package/dist/accounts/store.d.ts +30 -0
  15. package/dist/accounts/store.d.ts.map +1 -0
  16. package/dist/accounts/store.js +128 -0
  17. package/dist/accounts/store.js.map +1 -0
  18. package/dist/core/model-router.d.ts +30 -0
  19. package/dist/core/model-router.d.ts.map +1 -0
  20. package/dist/core/model-router.js +64 -0
  21. package/dist/core/model-router.js.map +1 -0
  22. package/dist/dashboard-server.d.ts +5 -0
  23. package/dist/dashboard-server.d.ts.map +1 -0
  24. package/dist/dashboard-server.js +387 -0
  25. package/dist/dashboard-server.js.map +1 -0
  26. package/dist/mcc.d.ts +8 -0
  27. package/dist/mcc.d.ts.map +1 -0
  28. package/dist/mcc.js +474 -0
  29. package/dist/mcc.js.map +1 -0
  30. package/dist/mcp/external-registry.d.ts +24 -0
  31. package/dist/mcp/external-registry.d.ts.map +1 -0
  32. package/dist/mcp/external-registry.js +99 -0
  33. package/dist/mcp/external-registry.js.map +1 -0
  34. package/dist/mcp/installer.d.ts +31 -0
  35. package/dist/mcp/installer.d.ts.map +1 -0
  36. package/dist/mcp/installer.js +273 -0
  37. package/dist/mcp/installer.js.map +1 -0
  38. package/dist/mcp/mcp-config.d.ts +86 -0
  39. package/dist/mcp/mcp-config.d.ts.map +1 -0
  40. package/dist/mcp/mcp-config.js +178 -0
  41. package/dist/mcp/mcp-config.js.map +1 -0
  42. package/dist/mcp/registry.d.ts +23 -0
  43. package/dist/mcp/registry.d.ts.map +1 -0
  44. package/dist/mcp/registry.js +100 -0
  45. package/dist/mcp/registry.js.map +1 -0
  46. package/dist/proxy/proxy-daemon.d.ts +27 -0
  47. package/dist/proxy/proxy-daemon.d.ts.map +1 -0
  48. package/dist/proxy/proxy-daemon.js +192 -0
  49. package/dist/proxy/proxy-daemon.js.map +1 -0
  50. package/dist/proxy/proxy-entry.d.ts +11 -0
  51. package/dist/proxy/proxy-entry.d.ts.map +1 -0
  52. package/dist/proxy/proxy-entry.js +74 -0
  53. package/dist/proxy/proxy-entry.js.map +1 -0
  54. package/dist/proxy/proxy-paths.d.ts +27 -0
  55. package/dist/proxy/proxy-paths.d.ts.map +1 -0
  56. package/dist/proxy/proxy-paths.js +125 -0
  57. package/dist/proxy/proxy-paths.js.map +1 -0
  58. package/dist/proxy/proxy-server.d.ts +20 -0
  59. package/dist/proxy/proxy-server.d.ts.map +1 -0
  60. package/dist/proxy/proxy-server.js +280 -0
  61. package/dist/proxy/proxy-server.js.map +1 -0
  62. package/dist/proxy/upstream-url.d.ts +7 -0
  63. package/dist/proxy/upstream-url.d.ts.map +1 -0
  64. package/dist/proxy/upstream-url.js +38 -0
  65. package/dist/proxy/upstream-url.js.map +1 -0
  66. package/dist/shared/logger.d.ts +23 -0
  67. package/dist/shared/logger.d.ts.map +1 -0
  68. package/dist/shared/logger.js +184 -0
  69. package/dist/shared/logger.js.map +1 -0
  70. package/dist/shared/provider-preset-catalog.d.ts +41 -0
  71. package/dist/shared/provider-preset-catalog.d.ts.map +1 -0
  72. package/dist/shared/provider-preset-catalog.js +299 -0
  73. package/dist/shared/provider-preset-catalog.js.map +1 -0
  74. package/docs/decisions.md +33 -0
  75. package/docs/lessons.md +8 -0
  76. package/docs/product.md +37 -0
  77. package/lib/mcp/mcc-image-analysis-server.cjs +454 -0
  78. package/lib/mcp/mcc-websearch-server.cjs +339 -0
  79. package/lib/mcp-hooks/image-analysis-runtime.cjs +510 -0
  80. package/lib/mcp-hooks/image-analyzer-transformer.cjs +526 -0
  81. package/lib/mcp-hooks/websearch-transformer.cjs +1421 -0
  82. package/lib/proxy/config/config-loader-facade.js +24 -0
  83. package/lib/proxy/glmt/delta-accumulator.js +363 -0
  84. package/lib/proxy/glmt/glmt-transformer.js +204 -0
  85. package/lib/proxy/glmt/index.js +41 -0
  86. package/lib/proxy/glmt/locale-enforcer.js +69 -0
  87. package/lib/proxy/glmt/pipeline/content-transformer.js +162 -0
  88. package/lib/proxy/glmt/pipeline/index.js +20 -0
  89. package/lib/proxy/glmt/pipeline/request-transformer.js +116 -0
  90. package/lib/proxy/glmt/pipeline/response-builder.js +205 -0
  91. package/lib/proxy/glmt/pipeline/stream-parser.js +234 -0
  92. package/lib/proxy/glmt/pipeline/tool-call-handler.js +78 -0
  93. package/lib/proxy/glmt/pipeline/types.js +6 -0
  94. package/lib/proxy/glmt/reasoning-enforcer.js +151 -0
  95. package/lib/proxy/glmt/sse-parser.js +102 -0
  96. package/lib/proxy/services/logging.js +13 -0
  97. package/lib/proxy/transformers/request-transformer.js +452 -0
  98. package/lib/proxy/transformers/sse-stream-transformer.js +199 -0
  99. package/lib/shared/logger.cjs +138 -0
  100. package/package.json +35 -0
  101. package/src/accounts/instance-manager.ts +58 -0
  102. package/src/accounts/shared-manager.ts +154 -0
  103. package/src/accounts/store.ts +111 -0
  104. package/src/core/model-router.ts +82 -0
  105. package/src/dashboard-server.ts +407 -0
  106. package/src/mcc.ts +474 -0
  107. package/src/mcp/external-registry.ts +73 -0
  108. package/src/mcp/installer.ts +258 -0
  109. package/src/mcp/mcp-config.ts +168 -0
  110. package/src/mcp/registry.ts +89 -0
  111. package/src/proxy/proxy-daemon.ts +184 -0
  112. package/src/proxy/proxy-entry.ts +63 -0
  113. package/src/proxy/proxy-paths.ts +97 -0
  114. package/src/proxy/proxy-server.ts +278 -0
  115. package/src/proxy/upstream-url.ts +38 -0
  116. package/src/shared/logger.ts +140 -0
  117. package/src/shared/provider-preset-catalog.ts +340 -0
  118. package/tsconfig.json +33 -0
  119. package/ui/.prettierrc +9 -0
  120. package/ui/index.html +12 -0
  121. package/ui/package.json +33 -0
  122. package/ui/postcss.config.js +6 -0
  123. package/ui/src/App.tsx +753 -0
  124. package/ui/src/components/ui/button.tsx +48 -0
  125. package/ui/src/components/ui/card.tsx +50 -0
  126. package/ui/src/components/ui/input.tsx +21 -0
  127. package/ui/src/components/ui/label.tsx +20 -0
  128. package/ui/src/components/ui/select.tsx +80 -0
  129. package/ui/src/components/ui/switch.tsx +26 -0
  130. package/ui/src/components/ui/tabs.tsx +52 -0
  131. package/ui/src/index.css +33 -0
  132. package/ui/src/lib/api.ts +185 -0
  133. package/ui/src/lib/utils.ts +6 -0
  134. package/ui/src/main.tsx +10 -0
  135. package/ui/src/vite-env.d.ts +1 -0
  136. package/ui/tailwind.config.js +49 -0
  137. package/ui/tsconfig.json +25 -0
  138. package/ui/vite.config.ts +20 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * MCC Logger — 单文件,零依赖
3
+ *
4
+ * 设计原则:
5
+ * - session 隔离:每次 mcc <profile> 生成独立 session,所有子进程写同一文件
6
+ * - 确定性路径:MCC_LOG_DIR / MCC_LOG_SESSION_ID 两个 env 确定路径
7
+ * - 最小 API:init(sessionId, logDir) + logger(component).error/info/debug(msg)
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
15
+
16
+ function env(key, fallback) {
17
+ return process.env[key] ?? fallback;
18
+ }
19
+
20
+ function levelValue(level) {
21
+ return LEVELS[level?.toLowerCase()] ?? LEVELS.info;
22
+ }
23
+
24
+ let _sessionId = '';
25
+ let _logDir = '';
26
+ let _currentFile = '';
27
+ let _currentSize = 0;
28
+ let _maxSize = 5 * 1024 * 1024;
29
+ let _maxFiles = 3;
30
+ let _minLevel = LEVELS.info;
31
+
32
+ function getLogDir() {
33
+ if (_logDir) return _logDir;
34
+ const logDirEnv = env('MCC_LOG_DIR', '');
35
+ if (logDirEnv) return logDirEnv;
36
+ const mccHome = env('MCC_HOME', path.join(os.homedir(), '.mcc'));
37
+ const profile = env('MCC_CURRENT_PROFILE', 'default');
38
+ return path.join(mccHome, 'logs', profile, _sessionId || 'nosession');
39
+ }
40
+
41
+ function getLogPath() {
42
+ return path.join(getLogDir(), 'mcc.log');
43
+ }
44
+
45
+ function rotate() {
46
+ const logPath = getLogPath();
47
+ if (!fs.existsSync(logPath)) return;
48
+ try { fs.unlinkSync(`${logPath}.${_maxFiles}`); } catch {}
49
+ for (let i = _maxFiles - 1; i >= 1; i--) {
50
+ const from = `${logPath}.${i}`;
51
+ const to = `${logPath}.${i + 1}`;
52
+ try { if (fs.existsSync(from)) fs.renameSync(from, to); } catch {}
53
+ }
54
+ try { fs.renameSync(logPath, `${logPath}.1`); } catch {}
55
+ _currentSize = 0;
56
+ _currentFile = '';
57
+ }
58
+
59
+ function ensureDir() {
60
+ const dir = getLogDir();
61
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
62
+ return dir;
63
+ }
64
+
65
+ function writeFile(line) {
66
+ if (!_sessionId) return;
67
+ ensureDir();
68
+ const logPath = getLogPath();
69
+ const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
70
+ if (_currentFile !== logPath || _currentSize + lineBytes > _maxSize) {
71
+ rotate();
72
+ }
73
+ try {
74
+ fs.appendFileSync(logPath, line + '\n', { mode: 0o644 });
75
+ _currentFile = logPath;
76
+ _currentSize += lineBytes;
77
+ } catch (err) {
78
+ process.stderr.write(`[logger] write failed: ${err.message}\n`);
79
+ }
80
+ }
81
+
82
+ function timestamp() {
83
+ const now = new Date();
84
+ const p = (n, l = 2) => String(n).padStart(l, '0');
85
+ return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())} ` +
86
+ `${p(now.getHours())}:${p(now.getMinutes())}:${p(now.getSeconds())}.${p(now.getMilliseconds(), 3)}`;
87
+ }
88
+
89
+ function format(level, message, component) {
90
+ const sid = _sessionId ? `[${_sessionId}]` : '';
91
+ const comp = component ? `[${component}]` : '';
92
+ return `${timestamp()} ${(level || 'info').toUpperCase().padEnd(5)} ${sid}${comp} ${message}`;
93
+ }
94
+
95
+ function write(level, message, component) {
96
+ if (levelValue(level) > _minLevel) return;
97
+ const line = format(level, message, component);
98
+ process.stderr.write(line + '\n');
99
+ writeFile(line);
100
+ }
101
+
102
+ // --- 导出---
103
+
104
+ function init(sessionId, logDir) {
105
+ _sessionId = sessionId || '';
106
+ _logDir = logDir || '';
107
+ _currentFile = '';
108
+ _currentSize = 0;
109
+ _minLevel = levelValue(env('MCC_LOG_LEVEL', 'info'));
110
+ _maxSize = parseInt(env('MCC_LOG_MAX_SIZE', ''), 10) || 5 * 1024 * 1024;
111
+ _maxFiles = parseInt(env('MCC_LOG_MAX_FILES', ''), 10) || 3;
112
+ const dir = ensureDir();
113
+ try { fs.writeFileSync(path.join(dir, '.session'), `${sessionId}\n${Date.now()}\n`, { mode: 0o644 }); } catch {}
114
+ return dir;
115
+ }
116
+
117
+ function initFromEnv() {
118
+ const sid = env('MCC_LOG_SESSION_ID', '');
119
+ const dir = env('MCC_LOG_DIR', '');
120
+ if (sid) init(sid, dir);
121
+ }
122
+
123
+ // 统一 API:log.info(component, message) — 两参数风格
124
+ const log = {
125
+ error: (c, m) => write('error', m, c),
126
+ warn: (c, m) => write('warn', m, c),
127
+ info: (c, m) => write('info', m, c),
128
+ debug: (c, m) => write('debug', m, c),
129
+ init,
130
+ initFromEnv,
131
+ getSessionId: () => _sessionId,
132
+ getLogDir,
133
+ };
134
+
135
+ // --- 自动从 env 初始化 ---
136
+ initFromEnv();
137
+
138
+ module.exports = log;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@hileeon/mcc",
3
+ "version": "0.1.0",
4
+ "description": "My Cloud Code - Multi-provider account and model switching for Claude Code",
5
+ "license": "MIT",
6
+ "author": "Zhang Chicheng",
7
+ "repository": "https://github.com/leeon917/mcc",
8
+ "main": "dist/mcc.js",
9
+ "types": "dist/mcc.d.ts",
10
+ "bin": {
11
+ "mcc": "dist/mcc.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "build:watch": "tsc --watch",
19
+ "typecheck": "tsc --noEmit",
20
+ "dev": "node dist/mcc.js",
21
+ "build:ui": "cd ui && npm install && npm run build",
22
+ "dashboard": "npm run build && npm run build:ui && node dist/dashboard-server.js"
23
+ },
24
+ "dependencies": {
25
+ "cors": "^2.8.5",
26
+ "express": "^4.18.2",
27
+ "picocolors": "^1.1.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/cors": "^2.8.17",
31
+ "@types/express": "^4.17.21",
32
+ "@types/node": "^20.0.0",
33
+ "typescript": "^5.3.0"
34
+ }
35
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Instance Manager - Account isolation via CLAUDE_CONFIG_DIR
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { SharedManager } from './shared-manager';
8
+
9
+ function getMccHome(): string {
10
+ return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
11
+ }
12
+
13
+ function getInstancesDir(): string {
14
+ return path.join(getMccHome(), 'instances');
15
+ }
16
+
17
+ const sharedManager = new SharedManager();
18
+
19
+ export class MCCInstanceManager {
20
+ async ensureInstance(accountName: string): Promise<string> {
21
+ const instancePath = this.getInstancePath(accountName);
22
+ if (!fs.existsSync(instancePath)) {
23
+ fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
24
+ const subdirs = ['session-env', 'todos', 'logs', 'file-history', 'shell-snapshots', 'debug', '.anthropic'];
25
+ for (const dir of subdirs) {
26
+ fs.mkdirSync(path.join(instancePath, dir), { recursive: true, mode: 0o700 });
27
+ }
28
+ }
29
+ // Always re-link shared directories (skills, commands, agents, plugins, settings)
30
+ sharedManager.linkSharedDirectories(instancePath);
31
+ return instancePath;
32
+ }
33
+
34
+ getInstancePath(accountName: string): string {
35
+ const safeName = accountName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
36
+ return path.join(getInstancesDir(), safeName);
37
+ }
38
+
39
+ async deleteInstance(accountName: string): Promise<void> {
40
+ const instancePath = this.getInstancePath(accountName);
41
+ if (fs.existsSync(instancePath)) {
42
+ fs.rmSync(instancePath, { recursive: true, force: true });
43
+ }
44
+ }
45
+
46
+ listInstances(): string[] {
47
+ const instancesDir = getInstancesDir();
48
+ if (!fs.existsSync(instancesDir)) return [];
49
+ return fs.readdirSync(instancesDir).filter((name) => {
50
+ if (name.startsWith('.')) return false;
51
+ return fs.statSync(path.join(instancesDir, name)).isDirectory();
52
+ });
53
+ }
54
+
55
+ hasInstance(accountName: string): boolean {
56
+ return fs.existsSync(this.getInstancePath(accountName));
57
+ }
58
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * SharedManager - Symlink management for shared directories
3
+ * Ported from CCS management/shared-manager.js (simplified)
4
+ *
5
+ * Creates symlinks: ~/.mcc/instances/<name>/<item> -> ~/.claude/<item>
6
+ * so that skills, commands, agents, plugins, and settings are shared across instances.
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import pc from 'picocolors';
12
+
13
+ function getHome(): string {
14
+ return process.env.HOME ?? process.env.USERPROFILE ?? '~';
15
+ }
16
+
17
+ interface SharedItem {
18
+ name: string;
19
+ type: 'directory' | 'file';
20
+ }
21
+
22
+ const SHARED_ITEMS: readonly SharedItem[] = [
23
+ { name: 'commands', type: 'directory' },
24
+ { name: 'skills', type: 'directory' },
25
+ { name: 'agents', type: 'directory' },
26
+ { name: 'plugins', type: 'directory' },
27
+ { name: 'settings.json', type: 'file' },
28
+ ];
29
+
30
+ export class SharedManager {
31
+ private claudeDir: string;
32
+
33
+ constructor() {
34
+ this.claudeDir = path.join(getHome(), '.claude');
35
+ }
36
+
37
+ /**
38
+ * Link shared directories to an instance.
39
+ * Creates symlinks from instance/<item> -> ~/.claude/<item>
40
+ */
41
+ linkSharedDirectories(instancePath: string): void {
42
+ const linked: string[] = [];
43
+ for (const item of SHARED_ITEMS) {
44
+ const claudePath = path.join(this.claudeDir, item.name);
45
+ const linkPath = path.join(instancePath, item.name);
46
+
47
+ // Ensure source exists in ~/.claude/
48
+ if (!this.pathExists(claudePath)) {
49
+ if (item.type === 'directory') {
50
+ fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
51
+ } else {
52
+ fs.writeFileSync(claudePath, '{}', 'utf8');
53
+ }
54
+ }
55
+
56
+ // Skip if already a symlink pointing to the correct target
57
+ if (this.isCorrectSymlink(linkPath, claudePath)) {
58
+ continue;
59
+ }
60
+
61
+ // Remove existing path (file, dir, or stale symlink)
62
+ this.removeExisting(linkPath, item.type);
63
+
64
+ // Create symlink
65
+ try {
66
+ const symlinkType = item.type === 'directory' ? 'dir' : 'file';
67
+ fs.symlinkSync(claudePath, linkPath, symlinkType);
68
+ linked.push(item.name);
69
+ } catch (_err) {
70
+ // Windows fallback: copy if symlink fails (Developer Mode not enabled)
71
+ if (process.platform === 'win32') {
72
+ if (item.type === 'directory') {
73
+ this.copyDirectoryFallback(claudePath, linkPath);
74
+ } else {
75
+ fs.copyFileSync(claudePath, linkPath);
76
+ }
77
+ console.warn(`[!] Symlink failed for ${item.name}, copied instead (enable Developer Mode for symlinks)`);
78
+ linked.push(`${item.name} (copied)`);
79
+ } else {
80
+ throw _err;
81
+ }
82
+ }
83
+ }
84
+ if (linked.length > 0) {
85
+ console.log(` ${pc.dim('shared')} ${pc.dim(linked.join(', '))}`);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Remove symlinks from an instance (cleanup).
91
+ */
92
+ unlinkSharedDirectories(instancePath: string): void {
93
+ for (const item of SHARED_ITEMS) {
94
+ const linkPath = path.join(instancePath, item.name);
95
+ if (!this.pathExists(linkPath)) continue;
96
+
97
+ try {
98
+ const stats = fs.lstatSync(linkPath);
99
+ if (stats.isSymbolicLink()) {
100
+ fs.unlinkSync(linkPath);
101
+ }
102
+ } catch {
103
+ // Best-effort
104
+ }
105
+ }
106
+ }
107
+
108
+ private pathExists(p: string): boolean {
109
+ try {
110
+ fs.lstatSync(p);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ private isCorrectSymlink(linkPath: string, expectedTarget: string): boolean {
118
+ try {
119
+ const stats = fs.lstatSync(linkPath);
120
+ if (!stats.isSymbolicLink()) return false;
121
+ const currentTarget = fs.readlinkSync(linkPath);
122
+ const resolved = path.resolve(path.dirname(linkPath), currentTarget);
123
+ return resolved === expectedTarget;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ private removeExisting(p: string, type: 'directory' | 'file'): void {
130
+ if (!this.pathExists(p)) return;
131
+ if (type === 'directory') {
132
+ fs.rmSync(p, { recursive: true, force: true });
133
+ } else {
134
+ try {
135
+ fs.unlinkSync(p);
136
+ } catch {
137
+ fs.rmSync(p, { force: true });
138
+ }
139
+ }
140
+ }
141
+
142
+ private copyDirectoryFallback(src: string, dest: string): void {
143
+ fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
144
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
145
+ const srcPath = path.join(src, entry.name);
146
+ const destPath = path.join(dest, entry.name);
147
+ if (entry.isDirectory()) {
148
+ this.copyDirectoryFallback(srcPath, destPath);
149
+ } else {
150
+ fs.copyFileSync(srcPath, destPath);
151
+ }
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Profile Store - File-based profile management
3
+ *
4
+ * Storage structure:
5
+ * ~/.mcc/
6
+ * ├── profiles.json # Profile metadata + tiered model config
7
+ * └── profiles/
8
+ * └── <profile-name>/
9
+ * └── .key # API key (plain text)
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+
15
+ function getMccHome(): string {
16
+ return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
17
+ }
18
+
19
+ function getProfilesDir(): string {
20
+ return path.join(getMccHome(), 'profiles');
21
+ }
22
+
23
+ function getProfilesJsonPath(): string {
24
+ return path.join(getMccHome(), 'profiles.json');
25
+ }
26
+
27
+ export interface Profile {
28
+ name: string;
29
+ baseUrl: string;
30
+ model: string; // Default model
31
+ opusModel?: string; // Tier 1 (Opus)
32
+ sonnetModel?: string; // Tier 2 (Sonnet)
33
+ haikuModel?: string; // Tier 3 (Haiku / small-fast)
34
+ protocol?: 'anthropic' | 'openai'; // 'anthropic' = direct, 'openai' = needs translation proxy
35
+ createdAt: string;
36
+ lastUsedAt?: string;
37
+ }
38
+
39
+ interface ProfilesJson {
40
+ version: number;
41
+ profiles: Record<string, Profile>;
42
+ defaultProfile?: string;
43
+ }
44
+
45
+ function readProfilesJson(): ProfilesJson {
46
+ const jsonPath = getProfilesJsonPath();
47
+ if (!fs.existsSync(jsonPath)) {
48
+ return { version: 1, profiles: {} };
49
+ }
50
+ try {
51
+ return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
52
+ } catch {
53
+ return { version: 1, profiles: {} };
54
+ }
55
+ }
56
+
57
+ function writeProfilesJson(data: ProfilesJson): void {
58
+ const jsonPath = getProfilesJsonPath();
59
+ fs.mkdirSync(path.dirname(jsonPath), { recursive: true, mode: 0o700 });
60
+ fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
61
+ }
62
+
63
+ export function listProfiles(): Profile[] {
64
+ return Object.values(readProfilesJson().profiles);
65
+ }
66
+
67
+ export function getProfile(name: string): Profile | undefined {
68
+ return readProfilesJson().profiles[name];
69
+ }
70
+
71
+ export function getDefaultProfile(): string | undefined {
72
+ return readProfilesJson().defaultProfile;
73
+ }
74
+
75
+ export function saveProfile(profile: Profile, apiKey: string): void {
76
+ const data = readProfilesJson();
77
+ const profileDir = path.join(getProfilesDir(), profile.name);
78
+ fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
79
+ fs.writeFileSync(path.join(profileDir, '.key'), apiKey, { encoding: 'utf8', mode: 0o600 });
80
+ data.profiles[profile.name] = { ...profile, lastUsedAt: new Date().toISOString() };
81
+ writeProfilesJson(data);
82
+ }
83
+
84
+ export function getProfileApiKey(name: string): string | undefined {
85
+ const keyPath = path.join(getProfilesDir(), name, '.key');
86
+ if (!fs.existsSync(keyPath)) return undefined;
87
+ return fs.readFileSync(keyPath, 'utf8').trim();
88
+ }
89
+
90
+ export function deleteProfile(name: string): void {
91
+ const data = readProfilesJson();
92
+ if (!data.profiles[name]) return;
93
+ delete data.profiles[name];
94
+ if (data.defaultProfile === name) {
95
+ data.defaultProfile = Object.keys(data.profiles)[0];
96
+ }
97
+ writeProfilesJson(data);
98
+ const profileDir = path.join(getProfilesDir(), name);
99
+ if (fs.existsSync(profileDir)) fs.rmSync(profileDir, { recursive: true, force: true });
100
+ }
101
+
102
+ export function setDefaultProfile(name: string): void {
103
+ const data = readProfilesJson();
104
+ if (!data.profiles[name]) throw new Error(`Profile not found: ${name}`);
105
+ data.defaultProfile = name;
106
+ writeProfilesJson(data);
107
+ }
108
+
109
+ export function hasProfile(name: string): boolean {
110
+ return name in readProfilesJson().profiles;
111
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Model Router - Builds Claude Code env vars from a Profile
3
+ *
4
+ * Mirrors CCS's tiered model mapping:
5
+ * ANTHROPIC_MODEL = Default model
6
+ * ANTHROPIC_DEFAULT_OPUS_MODEL = Opus tier
7
+ * ANTHROPIC_DEFAULT_SONNET_MODEL = Sonnet tier
8
+ * ANTHROPIC_DEFAULT_HAIKU_MODEL = Haiku tier
9
+ * ANTHROPIC_SMALL_FAST_MODEL = Alias for Haiku tier
10
+ */
11
+
12
+ import type { Profile } from '../accounts/store';
13
+ import { readMcpConfig, getActiveImageAnalysisProvider, getEnabledWebSearchProviders } from '../mcp/mcp-config';
14
+
15
+ export interface ProfileEnv {
16
+ ANTHROPIC_BASE_URL: string;
17
+ ANTHROPIC_AUTH_TOKEN: string;
18
+ ANTHROPIC_MODEL: string;
19
+ ANTHROPIC_DEFAULT_OPUS_MODEL: string;
20
+ ANTHROPIC_DEFAULT_SONNET_MODEL: string;
21
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: string;
22
+ ANTHROPIC_SMALL_FAST_MODEL: string;
23
+ DISABLE_TELEMETRY: '1';
24
+ DISABLE_COST_WARNINGS: '1';
25
+ CLAUDE_CONFIG_DIR: string;
26
+ [key: string]: string; // Allow MCP env vars
27
+ }
28
+
29
+ /**
30
+ * Build env vars for launching Claude Code with a profile.
31
+ * Falls back to profile.model for any missing tier.
32
+ */
33
+ export function buildProfileEnv(profile: Profile, apiKey: string, claudeConfigDir: string): ProfileEnv {
34
+ const model = profile.model;
35
+
36
+ // Read MCP config for provider settings
37
+ const mcpConfig = readMcpConfig();
38
+ const wsProviders = getEnabledWebSearchProviders(mcpConfig);
39
+ const iaProvider = getActiveImageAnalysisProvider(mcpConfig);
40
+
41
+ const env: Record<string, string> = {
42
+ ANTHROPIC_BASE_URL: profile.baseUrl,
43
+ ANTHROPIC_AUTH_TOKEN: apiKey,
44
+ ANTHROPIC_MODEL: model,
45
+ ANTHROPIC_DEFAULT_OPUS_MODEL: profile.opusModel ?? model,
46
+ ANTHROPIC_DEFAULT_SONNET_MODEL: profile.sonnetModel ?? model,
47
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: profile.haikuModel ?? model,
48
+ ANTHROPIC_SMALL_FAST_MODEL: profile.haikuModel ?? model,
49
+ DISABLE_TELEMETRY: '1',
50
+ DISABLE_COST_WARNINGS: '1',
51
+ CLAUDE_CONFIG_DIR: claudeConfigDir,
52
+ };
53
+
54
+ // MCP: WebSearch
55
+ env.MCC_WEBSEARCH_ENABLED = mcpConfig.websearch.enabled ? '1' : '0';
56
+ for (const p of wsProviders) {
57
+ env[`MCC_WEBSEARCH_${p.toUpperCase()}`] = '1';
58
+ // Set API key env vars for providers that need them
59
+ const providerConfig = mcpConfig.websearch.providers[p];
60
+ if (providerConfig?.apiKey) {
61
+ const keyEnvMap: Record<string, string> = {
62
+ exa: 'MCC_WEBSEARCH_EXA_API_KEY',
63
+ tavily: 'MCC_WEBSEARCH_TAVILY_API_KEY',
64
+ brave: 'MCC_WEBSEARCH_BRAVE_API_KEY',
65
+ };
66
+ if (keyEnvMap[p]) {
67
+ env[keyEnvMap[p]] = providerConfig.apiKey;
68
+ }
69
+ }
70
+ }
71
+
72
+ // MCP: Image Analysis
73
+ if (mcpConfig.imageAnalysis.enabled && iaProvider) {
74
+ env.MCC_IMAGE_ANALYSIS_ENABLED = '1';
75
+ env.MCC_IMAGE_ANALYSIS_RUNTIME_BASE_URL = iaProvider.baseUrl;
76
+ env.MCC_IMAGE_ANALYSIS_RUNTIME_API_KEY = iaProvider.apiKey;
77
+ env.MCC_IMAGE_ANALYSIS_MODEL = iaProvider.model;
78
+ env.MCC_IMAGE_ANALYSIS_FORMAT = iaProvider.format;
79
+ }
80
+
81
+ return env as unknown as ProfileEnv;
82
+ }