cc-hub 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 (41) hide show
  1. package/README.md +92 -0
  2. package/README.zh-CN.md +92 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +6 -0
  5. package/dist/components/ui/modal.d.ts +10 -0
  6. package/dist/components/ui/modal.js +11 -0
  7. package/dist/components/ui/status-bar.d.ts +15 -0
  8. package/dist/components/ui/status-bar.js +6 -0
  9. package/dist/components/ui/tab-bar.d.ts +14 -0
  10. package/dist/components/ui/tab-bar.js +10 -0
  11. package/dist/components/ui/table.d.ts +36 -0
  12. package/dist/components/ui/table.js +79 -0
  13. package/dist/config/presets.d.ts +2 -0
  14. package/dist/config/presets.js +57 -0
  15. package/dist/store/claude-config.d.ts +5 -0
  16. package/dist/store/claude-config.js +79 -0
  17. package/dist/store/config-store.d.ts +15 -0
  18. package/dist/store/config-store.js +138 -0
  19. package/dist/store/model-store.d.ts +8 -0
  20. package/dist/store/model-store.js +76 -0
  21. package/dist/test-escape-compiled.d.ts +1 -0
  22. package/dist/test-escape-compiled.js +17 -0
  23. package/dist/tui/add-model.d.ts +7 -0
  24. package/dist/tui/add-model.js +102 -0
  25. package/dist/tui/add-provider.d.ts +13 -0
  26. package/dist/tui/add-provider.js +185 -0
  27. package/dist/tui/app.d.ts +1 -0
  28. package/dist/tui/app.js +163 -0
  29. package/dist/tui/confirm.d.ts +7 -0
  30. package/dist/tui/confirm.js +5 -0
  31. package/dist/tui/dashboard.d.ts +10 -0
  32. package/dist/tui/dashboard.js +45 -0
  33. package/dist/tui/edit-model.d.ts +8 -0
  34. package/dist/tui/edit-model.js +54 -0
  35. package/dist/tui/edit-provider.d.ts +12 -0
  36. package/dist/tui/edit-provider.js +140 -0
  37. package/dist/tui/scenario-config.d.ts +8 -0
  38. package/dist/tui/scenario-config.js +87 -0
  39. package/dist/types.d.ts +19 -0
  40. package/dist/types.js +2 -0
  41. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # cc-hub
2
+
3
+ A terminal TUI tool for managing Claude Code model configurations. Switch between LLM providers and models without manually editing config files.
4
+
5
+ ## Features
6
+
7
+ - Switch active model with one key press
8
+ - Manage multiple providers and models
9
+ - Map Claude Code role aliases (opus/sonnet/haiku/subagent) to any model
10
+ - Atomic config writes to prevent corruption
11
+ - JSON5 config file with comment support
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm install
17
+ pnpm run build
18
+ ```
19
+
20
+ For global install:
21
+
22
+ ```bash
23
+ pnpm add -g .
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ Run the TUI:
29
+
30
+ ```bash
31
+ cc-hub
32
+ ```
33
+
34
+ ### Keyboard Shortcuts
35
+
36
+ | Key | Action |
37
+ |-----|--------|
38
+ | `↑` `↓` | Navigate models |
39
+ | `Enter` | Switch to selected model |
40
+ | `d` | Delete selected model |
41
+ | `s` | Scenario alias mapping |
42
+ | `q` | Quit |
43
+
44
+ ### Scenario Mapping
45
+
46
+ Press `s` to map Claude Code's built-in aliases to your models:
47
+
48
+ | Alias | Env Var |
49
+ |-------|---------|
50
+ | Opus | `ANTHROPIC_DEFAULT_OPUS_MODEL` |
51
+ | Sonnet | `ANTHROPIC_DEFAULT_SONNET_MODEL` |
52
+ | Haiku | `ANTHROPIC_DEFAULT_HAIKU_MODEL` |
53
+ | Subagent | `CLAUDE_CODE_SUBAGENT_MODEL` |
54
+
55
+ ## Configuration
56
+
57
+ Edit `~/.cc-hub/config.json` to add providers:
58
+
59
+ ```jsonc
60
+ {
61
+ "providers": [
62
+ {
63
+ "id": "dashscope",
64
+ "name": "DashScope",
65
+ "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
66
+ "apiKey": "sk-your-api-key",
67
+ "models": ["qwen3.6-plus", "qwen-max"]
68
+ },
69
+ {
70
+ "id": "deepseek",
71
+ "name": "DeepSeek",
72
+ "baseUrl": "https://api.deepseek.com/v1",
73
+ "apiKey": "sk-your-api-key",
74
+ "models": ["deepseek-chat", "deepseek-reasoner"]
75
+ }
76
+ ],
77
+ "scenarioModels": {}
78
+ }
79
+ ```
80
+
81
+ When you select a model, cc-hub writes the provider's credentials and model ID into `~/.claude/settings.json`. **Restart Claude Code for the change to take effect.**
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ pnpm run dev # Build and run
87
+ pnpm run dev:watch # Auto-rebuild on file changes
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,92 @@
1
+ # cc-hub
2
+
3
+ 一个终端 TUI 工具,用于管理 Claude Code 的模型配置。无需手动编辑配置文件,即可在不同 LLM 提供商和模型之间切换。
4
+
5
+ ## 功能
6
+
7
+ - 一键切换当前使用的模型
8
+ - 管理多个提供商和模型
9
+ - 将 Claude Code 的角色别名(opus/sonnet/haiku/subagent)映射到任意模型
10
+ - 原子写入配置,防止文件损坏
11
+ - 配置文件支持 JSON5(可写注释)
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ pnpm install
17
+ pnpm run build
18
+ ```
19
+
20
+ 全局安装:
21
+
22
+ ```bash
23
+ pnpm add -g .
24
+ ```
25
+
26
+ ## 使用
27
+
28
+ 启动 TUI:
29
+
30
+ ```bash
31
+ cc-hub
32
+ ```
33
+
34
+ ### 快捷键
35
+
36
+ | 按键 | 操作 |
37
+ |------|------|
38
+ | `↑` `↓` | 导航模型列表 |
39
+ | `Enter` | 切换到选中的模型 |
40
+ | `d` | 删除选中的模型 |
41
+ | `s` | 场景别名映射 |
42
+ | `q` | 退出 |
43
+
44
+ ### 场景映射
45
+
46
+ 按 `s` 将 Claude Code 内置别名映射到你的模型:
47
+
48
+ | 别名 | 环境变量 |
49
+ |------|---------|
50
+ | Opus | `ANTHROPIC_DEFAULT_OPUS_MODEL` |
51
+ | Sonnet | `ANTHROPIC_DEFAULT_SONNET_MODEL` |
52
+ | Haiku | `ANTHROPIC_DEFAULT_HAIKU_MODEL` |
53
+ | Subagent | `CLAUDE_CODE_SUBAGENT_MODEL` |
54
+
55
+ ## 配置
56
+
57
+ 编辑 `~/.cc-hub/config.json` 添加提供商:
58
+
59
+ ```jsonc
60
+ {
61
+ "providers": [
62
+ {
63
+ "id": "dashscope",
64
+ "name": "DashScope",
65
+ "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
66
+ "apiKey": "sk-your-api-key",
67
+ "models": ["qwen3.6-plus", "qwen-max"]
68
+ },
69
+ {
70
+ "id": "deepseek",
71
+ "name": "DeepSeek",
72
+ "baseUrl": "https://api.deepseek.com/v1",
73
+ "apiKey": "sk-your-api-key",
74
+ "models": ["deepseek-chat", "deepseek-reasoner"]
75
+ }
76
+ ],
77
+ "scenarioModels": {}
78
+ }
79
+ ```
80
+
81
+ 选中模型后,cc-hub 会将提供商的凭证和模型 ID 写入 `~/.claude/settings.json`。**需要重启 Claude Code 才能生效。**
82
+
83
+ ## 开发
84
+
85
+ ```bash
86
+ pnpm run dev # 构建并运行
87
+ pnpm run dev:watch # 监听文件变更自动重建
88
+ ```
89
+
90
+ ## 许可证
91
+
92
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ // src/cli.ts
4
+ import { render } from "ink";
5
+ import { App } from "./tui/app.js";
6
+ render(_jsx(App, {}));
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface ModalProps {
3
+ children: ReactNode;
4
+ title?: string;
5
+ borderColor?: string;
6
+ borderStyle?: 'single' | 'double' | 'round' | 'bold' | 'singleDouble' | 'doubleSingle' | 'classic' | 'arrow';
7
+ onClose?: () => void;
8
+ }
9
+ export declare function Modal({ children, title, borderColor, borderStyle, onClose, }: ModalProps): import("react/jsx-runtime").JSX.Element;
10
+ export default Modal;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ export function Modal({ children, title, borderColor = 'blue', borderStyle = 'round', onClose, }) {
4
+ useInput((_input, key) => {
5
+ if (key.escape && onClose) {
6
+ onClose();
7
+ }
8
+ });
9
+ return (_jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: borderStyle, borderColor: borderColor, children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: borderColor, children: title }) })), children] }) }));
10
+ }
11
+ export default Modal;
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface StatusBarItem {
3
+ /** The key or key combination (e.g., "Tab", "←→", "q") */
4
+ key: string;
5
+ /** Description of the action (e.g., "switch focus", "quit") */
6
+ label: string;
7
+ }
8
+ export interface StatusBarProps {
9
+ /** Keybinding hints to display */
10
+ items: StatusBarItem[];
11
+ /** Optional extra content to display before the keybinding hints */
12
+ extra?: ReactNode;
13
+ }
14
+ export declare function StatusBar({ items, extra, }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
15
+ export default StatusBar;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function StatusBar({ items, extra, }) {
4
+ return (_jsxs(Box, { gap: 2, children: [extra, extra && _jsx(Text, { dimColor: true, children: "\u2502" }), _jsx(Box, { gap: 1, children: items.map((item) => (_jsxs(Box, { gap: 0, children: [_jsxs(Text, { inverse: true, bold: true, children: [" ", item.key, " "] }), _jsxs(Text, { dimColor: true, children: [" ", item.label] })] }, item.key + item.label))) })] }));
5
+ }
6
+ export default StatusBar;
@@ -0,0 +1,14 @@
1
+ export interface TabBarProps {
2
+ /** Label shown before the options (e.g., "View", "Mode") */
3
+ label?: string;
4
+ /** The options to display */
5
+ options: string[];
6
+ /** Index of the currently selected option */
7
+ selectedIndex: number;
8
+ /** Whether this tab bar is currently focused (affects visual styling) */
9
+ focused?: boolean;
10
+ /** Color for the selected tab when focused */
11
+ activeColor?: string;
12
+ }
13
+ export declare function TabBar({ label, options, selectedIndex, focused, activeColor, }: TabBarProps): import("react/jsx-runtime").JSX.Element;
14
+ export default TabBar;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ export function TabBar({ label, options, selectedIndex, focused = true, activeColor = 'cyan', }) {
5
+ return (_jsxs(Box, { children: [label && (_jsx(Text, { dimColor: !focused, bold: focused, children: label })), _jsx(Text, { children: " " }), options.map((opt, i) => {
6
+ const selected = i === selectedIndex;
7
+ return (_jsx(React.Fragment, { children: selected ? (_jsxs(Text, { inverse: true, bold: true, color: focused ? activeColor : undefined, dimColor: !focused, children: [' ', opt, ' '] })) : (_jsxs(Text, { dimColor: !focused, children: [' ', opt, ' '] })) }, opt));
8
+ })] }));
9
+ }
10
+ export default TabBar;
@@ -0,0 +1,36 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface Column {
3
+ header: string;
4
+ width?: number;
5
+ minWidth?: number;
6
+ align?: 'left' | 'right';
7
+ headerColor?: string;
8
+ }
9
+ export interface Cell {
10
+ text: string;
11
+ color?: string;
12
+ bold?: boolean;
13
+ dimColor?: boolean;
14
+ /** Custom node for rendering. `text` is still required for width calculation. */
15
+ node?: ReactNode;
16
+ }
17
+ type TableBaseProps = {
18
+ padding?: number;
19
+ /** Max header width before truncating with … */
20
+ maxHeaderWidth?: number;
21
+ };
22
+ type SimpleTableProps<T> = TableBaseProps & {
23
+ data: T[];
24
+ columns?: (keyof T)[];
25
+ rows?: never;
26
+ footerRows?: never;
27
+ };
28
+ type AdvancedTableProps = TableBaseProps & {
29
+ columns: Column[];
30
+ rows: Cell[][];
31
+ footerRows?: Cell[][];
32
+ data?: never;
33
+ };
34
+ export type TableProps<T extends Record<string, unknown> = Record<string, unknown>> = SimpleTableProps<T> | AdvancedTableProps;
35
+ export declare function Table<T extends Record<string, unknown>>(props: TableProps<T>): import("react/jsx-runtime").JSX.Element;
36
+ export default Table;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ // ── Helpers ─
5
+ function buildBorder(type, widths) {
6
+ const c = {
7
+ top: { l: '╭', r: '╮', m: '┬', h: '─' },
8
+ mid: { l: '├', r: '┤', m: '┼', h: '─' },
9
+ bot: { l: '╰', r: '╯', m: '┴', h: '─' },
10
+ }[type];
11
+ return c.l + widths.map(w => c.h.repeat(w + 2)).join(c.m) + c.r;
12
+ }
13
+ function truncate(s, max) {
14
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
15
+ }
16
+ function computeWidths(columns, rows, footerRows, maxHeader) {
17
+ return columns.map((col, i) => {
18
+ let w = Math.min(col.header.length, maxHeader);
19
+ if (col.minWidth)
20
+ w = Math.max(w, col.minWidth);
21
+ for (const row of rows) {
22
+ const cell = row[i];
23
+ if (cell)
24
+ w = Math.max(w, cell.text.length);
25
+ }
26
+ for (const row of footerRows) {
27
+ const cell = row[i];
28
+ if (cell)
29
+ w = Math.max(w, cell.text.length);
30
+ }
31
+ return col.width ?? w;
32
+ });
33
+ }
34
+ function pad(s, w, align) {
35
+ return align === 'right' ? s.padStart(w) : s.padEnd(w);
36
+ }
37
+ function V() {
38
+ return _jsx(Text, { dimColor: true, children: "\u2502" });
39
+ }
40
+ function renderRow(cells, widths, columns) {
41
+ return (_jsxs(Box, { children: [cells.map((cell, i) => {
42
+ const w = widths[i];
43
+ const align = columns[i]?.align ?? 'left';
44
+ return (_jsxs(React.Fragment, { children: [_jsx(V, {}), cell.node ? (_jsxs(Box, { width: w + 2, justifyContent: align === 'right' ? 'flex-end' : undefined, children: [_jsx(Text, { children: " " }), cell.node, align === 'right' && _jsx(Text, { children: " " })] })) : (_jsxs(Text, { color: cell.color, bold: cell.bold, dimColor: cell.dimColor, children: [' ', pad(cell.text, w, align), ' '] }))] }, i));
45
+ }), _jsx(V, {})] }));
46
+ }
47
+ // ── Simple → Advanced conversion ──
48
+ function toAdvanced(data, columnKeys) {
49
+ const keys = columnKeys ?? Array.from(data.reduce((set, row) => {
50
+ for (const k in row)
51
+ set.add(k);
52
+ return set;
53
+ }, new Set()));
54
+ const columns = keys.map(k => ({ header: String(k) }));
55
+ const rows = data.map(row => keys.map(k => ({ text: row[k] == null ? '' : String(row[k]) })));
56
+ return { columns, rows };
57
+ }
58
+ // ── Component ──
59
+ export function Table(props) {
60
+ let columns;
61
+ let rows;
62
+ let footerRows = [];
63
+ let maxHeaderWidth;
64
+ if ('data' in props && props.data !== undefined) {
65
+ const converted = toAdvanced(props.data, props.columns);
66
+ columns = converted.columns;
67
+ rows = converted.rows;
68
+ maxHeaderWidth = props.maxHeaderWidth ?? Infinity;
69
+ }
70
+ else {
71
+ columns = props.columns;
72
+ rows = props.rows;
73
+ footerRows = props.footerRows ?? [];
74
+ maxHeaderWidth = props.maxHeaderWidth ?? 8;
75
+ }
76
+ const widths = computeWidths(columns, rows, footerRows, maxHeaderWidth);
77
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: buildBorder('top', widths) }), _jsxs(Box, { children: [columns.map((col, i) => (_jsxs(React.Fragment, { children: [_jsx(V, {}), _jsxs(Text, { bold: true, color: col.headerColor, children: [' ', pad(truncate(col.header, maxHeaderWidth), widths[i], col.align ?? 'left'), ' '] })] }, i))), _jsx(V, {})] }), _jsx(Text, { dimColor: true, children: buildBorder('mid', widths) }), rows.map((row, i) => (_jsx(React.Fragment, { children: renderRow(row, widths, columns) }, i))), footerRows.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: buildBorder('mid', widths) }), footerRows.map((row, i) => (_jsx(React.Fragment, { children: renderRow(row, widths, columns) }, i)))] })), _jsx(Text, { dimColor: true, children: buildBorder('bot', widths) })] }));
78
+ }
79
+ export default Table;
@@ -0,0 +1,2 @@
1
+ import { PresetProvider } from "../types.js";
2
+ export declare const PRESETS: PresetProvider[];
@@ -0,0 +1,57 @@
1
+ // src/config/presets.ts
2
+ export const PRESETS = [
3
+ {
4
+ id: "dashscope",
5
+ name: "DashScope (通义千问)",
6
+ baseUrl: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
7
+ models: [
8
+ { id: "qwen-plus", name: "Qwen 3.6 Plus", modelId: "qwen3.6-plus" },
9
+ { id: "qwen-max", name: "Qwen Max", modelId: "qwen-max" },
10
+ ],
11
+ },
12
+ {
13
+ id: "moonshot",
14
+ name: "Moonshot (Kimi)",
15
+ baseUrl: "https://api.moonshot.cn/v1",
16
+ models: [{ id: "kimi-k2", name: "Kimi K2.5", modelId: "kimi-k2-0905" }],
17
+ },
18
+ {
19
+ id: "minimax",
20
+ name: "MiniMax",
21
+ baseUrl: "https://api.minimax.chat/v1",
22
+ models: [
23
+ { id: "minimax-m2", name: "MiniMax M2.7", modelId: "MiniMax-M2.7" },
24
+ ],
25
+ },
26
+ {
27
+ id: "zhipu",
28
+ name: "Zhipu (智谱)",
29
+ baseUrl: "https://open.bigmodel.cn/api/paas/v4",
30
+ models: [{ id: "glm-5", name: "GLM 5.1", modelId: "glm-5" }],
31
+ },
32
+ {
33
+ id: "deepseek",
34
+ name: "DeepSeek",
35
+ baseUrl: "https://api.deepseek.com/v1",
36
+ models: [
37
+ { id: "deepseek-chat", name: "DeepSeek V3", modelId: "deepseek-chat" },
38
+ {
39
+ id: "deepseek-reasoner",
40
+ name: "DeepSeek R1",
41
+ modelId: "deepseek-reasoner",
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ id: "siliconflow",
47
+ name: "SiliconFlow",
48
+ baseUrl: "https://api.siliconflow.cn/v1",
49
+ models: [
50
+ {
51
+ id: "siliconflow-qwen",
52
+ name: "SiliconFlow Qwen",
53
+ modelId: "Qwen/Qwen2.5-72B-Instruct",
54
+ },
55
+ ],
56
+ },
57
+ ];
@@ -0,0 +1,5 @@
1
+ import { Provider, ScenarioModels } from "../types.js";
2
+ export declare function activateModel(store: {
3
+ providers: Provider[];
4
+ }, modelId: string, scenarioModels: ScenarioModels): void;
5
+ export declare function getActiveModelName(): string | null;
@@ -0,0 +1,79 @@
1
+ // src/store/claude-config.ts
2
+ import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { findModel } from "./config-store.js";
6
+ const CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
7
+ export function activateModel(store, modelId, scenarioModels) {
8
+ const dir = join(homedir(), ".claude");
9
+ if (!existsSync(dir)) {
10
+ return;
11
+ }
12
+ const resolved = findModel(store, modelId);
13
+ if (!resolved)
14
+ return;
15
+ const { provider, modelId: resolvedModelId } = resolved;
16
+ let settings = {};
17
+ if (existsSync(CLAUDE_SETTINGS)) {
18
+ try {
19
+ settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, "utf8"));
20
+ }
21
+ catch {
22
+ settings = {};
23
+ }
24
+ }
25
+ // Determine which auth env var to use based on existing settings
26
+ // Priority: ANTHROPIC_AUTH_TOKEN > ANTHROPIC_API_KEY
27
+ const existingEnv = settings.env || {};
28
+ const hasAuthToken = "ANTHROPIC_AUTH_TOKEN" in existingEnv;
29
+ const hasApiKey = "ANTHROPIC_API_KEY" in existingEnv;
30
+ const env = { ...existingEnv };
31
+ if (hasAuthToken) {
32
+ // Prefer AUTH_TOKEN, remove API_KEY if both exist
33
+ env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
34
+ delete env.ANTHROPIC_API_KEY;
35
+ }
36
+ else if (hasApiKey) {
37
+ env.ANTHROPIC_API_KEY = provider.apiKey;
38
+ }
39
+ else {
40
+ // Default to AUTH_TOKEN for new setups
41
+ env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
42
+ }
43
+ env.ANTHROPIC_BASE_URL = provider.baseUrl;
44
+ env.ANTHROPIC_MODEL = resolvedModelId;
45
+ // Set scenario model IDs: use configured mapping, or fall back to current model
46
+ const scenarioEntries = [
47
+ ["opusModelId", "ANTHROPIC_DEFAULT_OPUS_MODEL"],
48
+ ["sonnetModelId", "ANTHROPIC_DEFAULT_SONNET_MODEL"],
49
+ ["haikuModelId", "ANTHROPIC_DEFAULT_HAIKU_MODEL"],
50
+ ["subagentModelId", "CLAUDE_CODE_SUBAGENT_MODEL"],
51
+ ];
52
+ for (const [scenarioKey, envVar] of scenarioEntries) {
53
+ const configuredModelId = scenarioModels[scenarioKey];
54
+ if (configuredModelId) {
55
+ const r = findModel(store, configuredModelId);
56
+ if (r)
57
+ env[envVar] = r.modelId;
58
+ }
59
+ else {
60
+ // Not configured — default to current model
61
+ env[envVar] = resolvedModelId;
62
+ }
63
+ }
64
+ settings.env = env;
65
+ const tmpPath = CLAUDE_SETTINGS + ".tmp";
66
+ writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
67
+ renameSync(tmpPath, CLAUDE_SETTINGS);
68
+ }
69
+ export function getActiveModelName() {
70
+ if (!existsSync(CLAUDE_SETTINGS))
71
+ return null;
72
+ try {
73
+ const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, "utf8"));
74
+ return settings.env?.ANTHROPIC_BASE_URL || null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
@@ -0,0 +1,15 @@
1
+ import { ConfigStore, Provider, ScenarioModels } from "../types.js";
2
+ export declare function getConfigError(): string | null;
3
+ export declare function clearConfigError(): void;
4
+ export declare function loadConfig(): ConfigStore;
5
+ export declare function saveConfig(store: ConfigStore): void;
6
+ export declare function findModel(store: {
7
+ providers: Provider[];
8
+ }, modelId: string): {
9
+ provider: Provider;
10
+ modelId: string;
11
+ } | undefined;
12
+ export declare function getAllModels(store: ConfigStore): string[];
13
+ export declare function setActiveModel(store: ConfigStore, modelId: string): ConfigStore;
14
+ export declare function updateScenarioModels(store: ConfigStore, updates: Partial<ScenarioModels>): ConfigStore;
15
+ export declare function removeModelFromProvider(store: ConfigStore, providerId: string, modelId: string): ConfigStore;