ccoding 0.1.0__tar.gz
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.
- ccoding-0.1.0/.github/workflows/publish.yml +24 -0
- ccoding-0.1.0/.gitignore +33 -0
- ccoding-0.1.0/CLAUDE.md +42 -0
- ccoding-0.1.0/LICENSE +21 -0
- ccoding-0.1.0/PKG-INFO +144 -0
- ccoding-0.1.0/README.md +104 -0
- ccoding-0.1.0/pyproject.toml +34 -0
- ccoding-0.1.0/screenshot.png +0 -0
- ccoding-0.1.0/src/ccode/__init__.py +7 -0
- ccoding-0.1.0/src/ccode/__main__.py +3 -0
- ccoding-0.1.0/src/ccode/api.py +140 -0
- ccoding-0.1.0/src/ccode/cli.py +12 -0
- ccoding-0.1.0/src/ccode/config.py +145 -0
- ccoding-0.1.0/src/ccode/env_schema.json +979 -0
- ccoding-0.1.0/src/ccode/logo.py +177 -0
- ccoding-0.1.0/src/ccode/ui.py +727 -0
- ccoding-0.1.0/uv.lock +31 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # 用于 OIDC 免密发布
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: Build
|
|
21
|
+
run: uv build
|
|
22
|
+
|
|
23
|
+
- name: Publish
|
|
24
|
+
run: uv publish
|
ccoding-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyd
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.eggs/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# Test/typing/cache
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
coverage.xml
|
|
22
|
+
|
|
23
|
+
# Editor/OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
|
|
28
|
+
# Tooling
|
|
29
|
+
.serena/
|
|
30
|
+
|
|
31
|
+
# Env files
|
|
32
|
+
.env
|
|
33
|
+
.claude/
|
ccoding-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
**ccoding** — 基于 curses 的 Claude Code 启动器 TUI,用于配置 LiteLLM proxy / CLI Proxy API 的模型网关参数,并直接启动 `claude` 命令。PyPI 包名 `ccoding`,Python import 包名 `ccode`,命令行入口 `ccode`。
|
|
8
|
+
|
|
9
|
+
## 常用命令
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync # 安装/同步依赖
|
|
13
|
+
uv run ccode # 运行
|
|
14
|
+
uv run python -m ccode # 模块方式运行(调试)
|
|
15
|
+
uv build # 构建 wheel / sdist
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
无测试框架,无 lint 配置。
|
|
19
|
+
|
|
20
|
+
## 架构
|
|
21
|
+
|
|
22
|
+
`src layout`,包在 `src/ccode/`,模块职责如下:
|
|
23
|
+
|
|
24
|
+
- **`config.py`** — 所有持久化逻辑。配置文件路径 `~/.ccode/config.json`。`toggles` 字段存储任意环境变量键值对(`dict[str, str]`)。提供 `load_env_schema()` / `env_schema_keys()` / `env_schema_description()` / `env_schema_enum()` 四个函数,从 `env_schema.json` 读取已知变量的元数据。
|
|
25
|
+
- **`api.py`** — 无状态函数层:HTTP 拉取 `/v1/models`、构建启动环境变量 `build_env()`、调用 `subprocess.run(["claude", ...])` 启动 claude。
|
|
26
|
+
- **`logo.py`** — LOGOS 数据(3 种 ASCII 字体)及 4 种渲染风格函数(wave / pulse / glitch / rain),接收独立参数,不依赖 `CursesApp`。
|
|
27
|
+
- **`ui.py`** — `CursesApp` 类,持有全部 UI 状态。配置界面的 field 列表由 `_config_fields()` 动态构建(`base_url`、`api_key`、每个 toggle key、`__add__` 新增行),支持运行时增删 toggle。新增 toggle 时 key 输入框实时展示候选列表,Tab 键补全,候选来源为 `env_schema.json` 的 217 个已知变量。
|
|
28
|
+
- **`cli.py`** — 入口,`main(argv)` 构造 `CursesApp` 并调用 `curses.wrapper`。
|
|
29
|
+
- **`env_schema.json`** — 从官方文档生成的 Claude Code 环境变量 JSON Schema,含 `description` 和 `enum` 字段,供 UI 补全和提示使用。更新 claude code 版本后如需同步新变量,手动编辑此文件的 `properties` 节。
|
|
30
|
+
|
|
31
|
+
## 关键数据流
|
|
32
|
+
|
|
33
|
+
1. `load_config()` 读取 `~/.ccode/config.json`,toggles 值统一为字符串。
|
|
34
|
+
2. 主界面 → `fetch_models()` 拉取可用模型 → 用户选择 opus/sonnet/haiku 的 owned_by 和 model_id。
|
|
35
|
+
3. 启动时 `build_env()` 将 `base_url`、`api_key`、model id、toggles 全部注入环境变量,执行 `claude`。
|
|
36
|
+
4. 配置界面 `ESC` 自动保存后刷新模型列表。
|
|
37
|
+
|
|
38
|
+
## 注意事项
|
|
39
|
+
|
|
40
|
+
- `TOGGLE_LABELS` 常量已移除,配置界面 toggles 完全动态,从 `config["toggles"]` 渲染。
|
|
41
|
+
- toggles 旧格式(`int` 值 0/1)在 `load_config()` 中自动迁移为字符串。
|
|
42
|
+
- Windows 上 curses 依赖 `windows-curses`,在 `pyproject.toml` 中已通过 platform marker 声明。
|
ccoding-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 8DE4732A
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ccoding-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ccoding
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Curses-based Claude Code launcher
|
|
5
|
+
Project-URL: Homepage, https://github.com/8DE4732A/ccode
|
|
6
|
+
Project-URL: Repository, https://github.com/8DE4732A/ccode
|
|
7
|
+
Project-URL: Issues, https://github.com/8DE4732A/ccode/issues
|
|
8
|
+
Author: 8de4732a
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2025 8DE4732A
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: claude,claude-code,curses,launcher,tui
|
|
32
|
+
Classifier: Environment :: Console :: Curses
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: POSIX
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Requires-Dist: windows-curses; sys_platform == 'win32'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# CCode Launcher (Curses UI)
|
|
42
|
+
|
|
43
|
+
这是一个基于 curses 的 Claude Code 启动器,提供键盘全操作的终端界面,用于配置 Base URL、API Key、模型选择和常用开关,并直接启动 `claude` 命令。
|
|
44
|
+
|
|
45
|
+
工具的目的是方便启动claude code时快速选择想要使用的模型。模型网关可以使用litellm proxy 或者 CLI Proxy API
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
## 功能特点
|
|
49
|
+
|
|
50
|
+
- 终端 curses UI,键盘全操作
|
|
51
|
+
- 主界面选择 OPUS / SONNET / HAIKU 的 owned_by 与 model_id
|
|
52
|
+
- 配置界面编辑 Base URL / API Key 与常用开关
|
|
53
|
+
- 自动保存配置到 `~/.ccode/config.json`
|
|
54
|
+
- 直接启动 `claude` 并传递额外参数
|
|
55
|
+
- 动画 Logo(颜色渐变 / 流光 / 轻微波浪)
|
|
56
|
+
|
|
57
|
+
## 安装
|
|
58
|
+
|
|
59
|
+
推荐使用 [uv](https://github.com/astral-sh/uv) 安装为全局工具:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv tool install ccoding
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
或使用 [pipx](https://pipx.pypa.io):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pipx install ccoding
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
安装后直接运行:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ccode
|
|
75
|
+
ccode --chrome # 传递额外参数给 claude
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 开发者运行方式
|
|
79
|
+
|
|
80
|
+
需要 Python >=3.10,仅依赖标准库(Windows 额外需要 `windows-curses`,安装时自动处理)。
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/8DE4732A/ccode
|
|
84
|
+
cd ccode
|
|
85
|
+
uv sync
|
|
86
|
+
uv run ccode
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 快捷键(主界面)
|
|
90
|
+
|
|
91
|
+
- `↑ / ↓`:切换 OPUS / SONNET / HAIKU 行
|
|
92
|
+
- `← / →`:切换 owned_by / model_id 字段
|
|
93
|
+
- `a / d`:循环切换当前字段的可选项
|
|
94
|
+
- `b`:刷新模型列表
|
|
95
|
+
- `c`:进入配置界面
|
|
96
|
+
- `Enter`:校验并启动 `claude`
|
|
97
|
+
- `q`:退出
|
|
98
|
+
|
|
99
|
+
> 提示:首次使用需要先在配置界面设置 Base URL 与 API Key,然后返回主界面按 `b` 获取模型列表。
|
|
100
|
+
|
|
101
|
+
## 快捷键(配置界面)
|
|
102
|
+
|
|
103
|
+
- `↑ / ↓`:移动字段焦点
|
|
104
|
+
- `Enter / Space`:切换开关项
|
|
105
|
+
- 文本输入:编辑 BASE_URL / API_KEY
|
|
106
|
+
- `ESC`:自动保存并返回主界面
|
|
107
|
+
|
|
108
|
+
## 配置文件
|
|
109
|
+
|
|
110
|
+
配置自动保存在:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
~/.ccode/config.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
示例结构:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"base_url": "http://127.0.0.1:8317",
|
|
121
|
+
"api_key": "",
|
|
122
|
+
"models": {
|
|
123
|
+
"opus": {"owned_by": null, "id": null},
|
|
124
|
+
"sonnet": {"owned_by": null, "id": null},
|
|
125
|
+
"haiku": {"owned_by": null, "id": null}
|
|
126
|
+
},
|
|
127
|
+
"toggles": {
|
|
128
|
+
"CLAUDE_CODE_ENABLE_TELEMETRY": "0",
|
|
129
|
+
"DISABLE_COST_WARNINGS": "1",
|
|
130
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 工作原理
|
|
136
|
+
|
|
137
|
+
1. 从 `~/.ccode/config.json` 读取配置(首次运行自动生成)。
|
|
138
|
+
2. 根据 Base URL + API Key 拉取模型列表。
|
|
139
|
+
3. 选择 OPUS / SONNET / HAIKU 的 owned_by / model_id。
|
|
140
|
+
4. 启动 `claude`,并注入相关环境变量。
|
|
141
|
+
|
|
142
|
+
## 许可证
|
|
143
|
+
|
|
144
|
+
MIT
|
ccoding-0.1.0/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# CCode Launcher (Curses UI)
|
|
2
|
+
|
|
3
|
+
这是一个基于 curses 的 Claude Code 启动器,提供键盘全操作的终端界面,用于配置 Base URL、API Key、模型选择和常用开关,并直接启动 `claude` 命令。
|
|
4
|
+
|
|
5
|
+
工具的目的是方便启动claude code时快速选择想要使用的模型。模型网关可以使用litellm proxy 或者 CLI Proxy API
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
## 功能特点
|
|
9
|
+
|
|
10
|
+
- 终端 curses UI,键盘全操作
|
|
11
|
+
- 主界面选择 OPUS / SONNET / HAIKU 的 owned_by 与 model_id
|
|
12
|
+
- 配置界面编辑 Base URL / API Key 与常用开关
|
|
13
|
+
- 自动保存配置到 `~/.ccode/config.json`
|
|
14
|
+
- 直接启动 `claude` 并传递额外参数
|
|
15
|
+
- 动画 Logo(颜色渐变 / 流光 / 轻微波浪)
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
推荐使用 [uv](https://github.com/astral-sh/uv) 安装为全局工具:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install ccoding
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
或使用 [pipx](https://pipx.pypa.io):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pipx install ccoding
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
安装后直接运行:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ccode
|
|
35
|
+
ccode --chrome # 传递额外参数给 claude
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 开发者运行方式
|
|
39
|
+
|
|
40
|
+
需要 Python >=3.10,仅依赖标准库(Windows 额外需要 `windows-curses`,安装时自动处理)。
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/8DE4732A/ccode
|
|
44
|
+
cd ccode
|
|
45
|
+
uv sync
|
|
46
|
+
uv run ccode
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 快捷键(主界面)
|
|
50
|
+
|
|
51
|
+
- `↑ / ↓`:切换 OPUS / SONNET / HAIKU 行
|
|
52
|
+
- `← / →`:切换 owned_by / model_id 字段
|
|
53
|
+
- `a / d`:循环切换当前字段的可选项
|
|
54
|
+
- `b`:刷新模型列表
|
|
55
|
+
- `c`:进入配置界面
|
|
56
|
+
- `Enter`:校验并启动 `claude`
|
|
57
|
+
- `q`:退出
|
|
58
|
+
|
|
59
|
+
> 提示:首次使用需要先在配置界面设置 Base URL 与 API Key,然后返回主界面按 `b` 获取模型列表。
|
|
60
|
+
|
|
61
|
+
## 快捷键(配置界面)
|
|
62
|
+
|
|
63
|
+
- `↑ / ↓`:移动字段焦点
|
|
64
|
+
- `Enter / Space`:切换开关项
|
|
65
|
+
- 文本输入:编辑 BASE_URL / API_KEY
|
|
66
|
+
- `ESC`:自动保存并返回主界面
|
|
67
|
+
|
|
68
|
+
## 配置文件
|
|
69
|
+
|
|
70
|
+
配置自动保存在:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
~/.ccode/config.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
示例结构:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"base_url": "http://127.0.0.1:8317",
|
|
81
|
+
"api_key": "",
|
|
82
|
+
"models": {
|
|
83
|
+
"opus": {"owned_by": null, "id": null},
|
|
84
|
+
"sonnet": {"owned_by": null, "id": null},
|
|
85
|
+
"haiku": {"owned_by": null, "id": null}
|
|
86
|
+
},
|
|
87
|
+
"toggles": {
|
|
88
|
+
"CLAUDE_CODE_ENABLE_TELEMETRY": "0",
|
|
89
|
+
"DISABLE_COST_WARNINGS": "1",
|
|
90
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 工作原理
|
|
96
|
+
|
|
97
|
+
1. 从 `~/.ccode/config.json` 读取配置(首次运行自动生成)。
|
|
98
|
+
2. 根据 Base URL + API Key 拉取模型列表。
|
|
99
|
+
3. 选择 OPUS / SONNET / HAIKU 的 owned_by / model_id。
|
|
100
|
+
4. 启动 `claude`,并注入相关环境变量。
|
|
101
|
+
|
|
102
|
+
## 许可证
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ccoding"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Curses-based Claude Code launcher"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{ name = "8de4732a" }]
|
|
9
|
+
keywords = ["claude", "claude-code", "launcher", "curses", "tui"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Environment :: Console :: Curses",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: POSIX",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"windows-curses; sys_platform == 'win32'",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/8DE4732A/ccode"
|
|
23
|
+
Repository = "https://github.com/8DE4732A/ccode"
|
|
24
|
+
Issues = "https://github.com/8DE4732A/ccode/issues"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
ccode = "ccode.cli:main"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/ccode"]
|
|
Binary file
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .config import MODEL_KEYS, mask_secret
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _truncate_body(text: str, max_len: int = 200) -> str:
|
|
14
|
+
return f"{text[:max_len]}..." if len(text) > max_len else text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch_models(base_url: str, api_key: str) -> list[dict[str, str]]:
|
|
18
|
+
url = f"{base_url.rstrip('/')}/v1/models"
|
|
19
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
20
|
+
request = urllib.request.Request(url, headers=headers)
|
|
21
|
+
try:
|
|
22
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
23
|
+
status = response.getcode()
|
|
24
|
+
body_bytes = response.read()
|
|
25
|
+
except urllib.error.HTTPError as exc:
|
|
26
|
+
body = ""
|
|
27
|
+
try:
|
|
28
|
+
if exc.fp is not None:
|
|
29
|
+
body = exc.fp.read().decode("utf-8", "replace").strip()
|
|
30
|
+
except Exception:
|
|
31
|
+
body = ""
|
|
32
|
+
message = _truncate_body(body) or "No response body"
|
|
33
|
+
raise RuntimeError(f"HTTP {exc.code}: {message}") from exc
|
|
34
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
35
|
+
raise RuntimeError(f"Request failed: {exc}") from exc
|
|
36
|
+
|
|
37
|
+
if status != 200:
|
|
38
|
+
body = body_bytes.decode("utf-8", "replace").strip()
|
|
39
|
+
message = _truncate_body(body) or "No response body"
|
|
40
|
+
raise RuntimeError(f"HTTP {status}: {message}")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
payload = json.loads(body_bytes.decode("utf-8", "replace"))
|
|
44
|
+
except ValueError as exc:
|
|
45
|
+
raise RuntimeError("Invalid JSON response") from exc
|
|
46
|
+
|
|
47
|
+
data = payload.get("data")
|
|
48
|
+
if not isinstance(data, list):
|
|
49
|
+
raise RuntimeError("Response JSON missing data array")
|
|
50
|
+
|
|
51
|
+
models: list[dict[str, str]] = []
|
|
52
|
+
for item in data:
|
|
53
|
+
if not isinstance(item, dict):
|
|
54
|
+
continue
|
|
55
|
+
model_id = item.get("id")
|
|
56
|
+
owned_by = item.get("owned_by")
|
|
57
|
+
if isinstance(model_id, str) and isinstance(owned_by, str):
|
|
58
|
+
models.append({"id": model_id, "owned_by": owned_by})
|
|
59
|
+
|
|
60
|
+
return models
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_models(config: dict[str, Any], models: list[dict[str, str]]) -> bool:
|
|
64
|
+
valid = {(item["owned_by"], item["id"]) for item in models}
|
|
65
|
+
changed = False
|
|
66
|
+
for key in MODEL_KEYS:
|
|
67
|
+
entry = config["models"].get(key, {})
|
|
68
|
+
owned_by = entry.get("owned_by")
|
|
69
|
+
model_id = entry.get("id")
|
|
70
|
+
if not owned_by or not model_id or (owned_by, model_id) not in valid:
|
|
71
|
+
if entry.get("owned_by") is not None or entry.get("id") is not None:
|
|
72
|
+
config["models"][key] = {"owned_by": None, "id": None}
|
|
73
|
+
changed = True
|
|
74
|
+
return changed
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_models_by_owner(
|
|
78
|
+
models_data: list[dict[str, str]] | None,
|
|
79
|
+
) -> dict[str, list[str]]:
|
|
80
|
+
by_owner: dict[str, list[str]] = {}
|
|
81
|
+
for item in models_data or []:
|
|
82
|
+
owned_by = item.get("owned_by")
|
|
83
|
+
model_id = item.get("id")
|
|
84
|
+
if not isinstance(owned_by, str) or not isinstance(model_id, str):
|
|
85
|
+
continue
|
|
86
|
+
by_owner.setdefault(owned_by, []).append(model_id)
|
|
87
|
+
for owner in by_owner:
|
|
88
|
+
by_owner[owner] = sorted(by_owner[owner])
|
|
89
|
+
return by_owner
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def owner_options(models_by_owner: dict[str, list[str]]) -> list[str]:
|
|
93
|
+
return sorted(models_by_owner.keys())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def model_options(models_by_owner: dict[str, list[str]], owner: str) -> list[str]:
|
|
97
|
+
return models_by_owner.get(owner, [])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_env(config: dict[str, Any], masked: bool = False) -> dict[str, str]:
|
|
101
|
+
env = os.environ.copy()
|
|
102
|
+
base_url = config.get("base_url", "")
|
|
103
|
+
api_key = config.get("api_key", "")
|
|
104
|
+
env["ANTHROPIC_BASE_URL"] = base_url
|
|
105
|
+
env["ANTHROPIC_AUTH_TOKEN"] = mask_secret(api_key) if masked else api_key
|
|
106
|
+
for key in MODEL_KEYS:
|
|
107
|
+
entry = config["models"].get(key, {})
|
|
108
|
+
model_id = entry.get("id")
|
|
109
|
+
if model_id:
|
|
110
|
+
env[f"ANTHROPIC_DEFAULT_{key.upper()}_MODEL"] = model_id
|
|
111
|
+
for key, value in config.get("toggles", {}).items():
|
|
112
|
+
env[key] = str(value)
|
|
113
|
+
return env
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def validate_launch_requirements(config: dict[str, Any]) -> str | None:
|
|
117
|
+
base_url = config.get("base_url", "").strip()
|
|
118
|
+
api_key = config.get("api_key", "").strip()
|
|
119
|
+
if not base_url or not api_key:
|
|
120
|
+
return "Base URL and API key are required."
|
|
121
|
+
for key in MODEL_KEYS:
|
|
122
|
+
entry = config["models"].get(key, {})
|
|
123
|
+
if not entry.get("owned_by") or not entry.get("id"):
|
|
124
|
+
return "OPUS, SONNET, and HAIKU selections are required."
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def launch_claude(config: dict[str, Any], args: list[str]) -> str | None:
|
|
129
|
+
base_url = config.get("base_url", "").strip()
|
|
130
|
+
api_key = config.get("api_key", "").strip()
|
|
131
|
+
if not base_url or not api_key:
|
|
132
|
+
return "Missing base URL or API key."
|
|
133
|
+
env = build_env(config, masked=False)
|
|
134
|
+
try:
|
|
135
|
+
subprocess.run(["claude", *args], check=True, env=env)
|
|
136
|
+
except FileNotFoundError:
|
|
137
|
+
return "Could not find 'claude' on PATH."
|
|
138
|
+
except subprocess.CalledProcessError as exc:
|
|
139
|
+
return f"claude exited with code {exc.returncode}."
|
|
140
|
+
return None
|