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.
- package/README.md +92 -0
- package/README.zh-CN.md +92 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +6 -0
- package/dist/components/ui/modal.d.ts +10 -0
- package/dist/components/ui/modal.js +11 -0
- package/dist/components/ui/status-bar.d.ts +15 -0
- package/dist/components/ui/status-bar.js +6 -0
- package/dist/components/ui/tab-bar.d.ts +14 -0
- package/dist/components/ui/tab-bar.js +10 -0
- package/dist/components/ui/table.d.ts +36 -0
- package/dist/components/ui/table.js +79 -0
- package/dist/config/presets.d.ts +2 -0
- package/dist/config/presets.js +57 -0
- package/dist/store/claude-config.d.ts +5 -0
- package/dist/store/claude-config.js +79 -0
- package/dist/store/config-store.d.ts +15 -0
- package/dist/store/config-store.js +138 -0
- package/dist/store/model-store.d.ts +8 -0
- package/dist/store/model-store.js +76 -0
- package/dist/test-escape-compiled.d.ts +1 -0
- package/dist/test-escape-compiled.js +17 -0
- package/dist/tui/add-model.d.ts +7 -0
- package/dist/tui/add-model.js +102 -0
- package/dist/tui/add-provider.d.ts +13 -0
- package/dist/tui/add-provider.js +185 -0
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +163 -0
- package/dist/tui/confirm.d.ts +7 -0
- package/dist/tui/confirm.js +5 -0
- package/dist/tui/dashboard.d.ts +10 -0
- package/dist/tui/dashboard.js +45 -0
- package/dist/tui/edit-model.d.ts +8 -0
- package/dist/tui/edit-model.js +54 -0
- package/dist/tui/edit-provider.d.ts +12 -0
- package/dist/tui/edit-provider.js +140 -0
- package/dist/tui/scenario-config.d.ts +8 -0
- package/dist/tui/scenario-config.js +87 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +2 -0
- 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
|
package/README.zh-CN.md
ADDED
|
@@ -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,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,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,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;
|