chcode 0.1.0__py3-none-any.whl
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.
- chcode/__init__.py +0 -0
- chcode/__main__.py +5 -0
- chcode/agent_setup.py +395 -0
- chcode/agents/__init__.py +0 -0
- chcode/agents/definitions.py +158 -0
- chcode/agents/loader.py +104 -0
- chcode/agents/runner.py +159 -0
- chcode/chat.py +1630 -0
- chcode/cli.py +142 -0
- chcode/config.py +571 -0
- chcode/display.py +325 -0
- chcode/prompts.py +640 -0
- chcode/session.py +149 -0
- chcode/skill_manager.py +165 -0
- chcode/utils/__init__.py +3 -0
- chcode/utils/enhanced_chat_openai.py +368 -0
- chcode/utils/git_checker.py +38 -0
- chcode/utils/git_manager.py +261 -0
- chcode/utils/modelscope_ratelimit.py +65 -0
- chcode/utils/multimodal.py +268 -0
- chcode/utils/shell/__init__.py +17 -0
- chcode/utils/shell/output.py +63 -0
- chcode/utils/shell/provider.py +128 -0
- chcode/utils/shell/result.py +14 -0
- chcode/utils/shell/semantics.py +55 -0
- chcode/utils/shell/session.py +159 -0
- chcode/utils/skill_loader.py +565 -0
- chcode/utils/text_utils.py +14 -0
- chcode/utils/tool_result_pipeline.py +244 -0
- chcode/utils/tools.py +1724 -0
- chcode/vision_config.py +371 -0
- chcode-0.1.0.dist-info/METADATA +275 -0
- chcode-0.1.0.dist-info/RECORD +36 -0
- chcode-0.1.0.dist-info/WHEEL +4 -0
- chcode-0.1.0.dist-info/entry_points.txt +2 -0
- chcode-0.1.0.dist-info/licenses/LICENSE +21 -0
chcode/vision_config.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
视觉模型配置管理 — 读取/保存 vision_model.json,配置视觉理解模型
|
|
3
|
+
|
|
4
|
+
视觉模型通过 ModelScope OpenAI 兼容 API 调用,
|
|
5
|
+
发送 base64 编码图片 + 文本 prompt,获取图像理解结果。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from chcode.prompts import select, confirm, password
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
CONFIG_DIR = Path.home() / ".chat"
|
|
21
|
+
VISION_JSON = CONFIG_DIR / "vision_model.json"
|
|
22
|
+
|
|
23
|
+
MODELSCOPE_BASE_URL = "https://api-inference.modelscope.cn/v1"
|
|
24
|
+
|
|
25
|
+
# 视觉模型预设(默认 + 备用)
|
|
26
|
+
VISION_MODEL_PRESETS = [
|
|
27
|
+
# 默认模型
|
|
28
|
+
{
|
|
29
|
+
"model": "moonshotai/Kimi-K2.5",
|
|
30
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
31
|
+
"temperature": 1.0,
|
|
32
|
+
"top_p": 0.95,
|
|
33
|
+
"stream_usage": True,
|
|
34
|
+
},
|
|
35
|
+
# 备用模型
|
|
36
|
+
{
|
|
37
|
+
"model": "Qwen/Qwen3-VL-235B-A22B-Instruct",
|
|
38
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
39
|
+
"temperature": 1.0,
|
|
40
|
+
"top_p": 0.95,
|
|
41
|
+
"stream_usage": True,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"model": "Qwen/Qwen3-VL-30B-A3B-Instruct",
|
|
45
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
46
|
+
"temperature": 1.0,
|
|
47
|
+
"top_p": 0.95,
|
|
48
|
+
"stream_usage": True,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"model": "Qwen/Qwen3-VL-8B-Instruct",
|
|
52
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
53
|
+
"temperature": 1.0,
|
|
54
|
+
"top_p": 0.95,
|
|
55
|
+
"stream_usage": True,
|
|
56
|
+
},
|
|
57
|
+
# 非视觉多模态模型(也支持图片输入)
|
|
58
|
+
{
|
|
59
|
+
"model": "Qwen/Qwen3.5-122B-A10B",
|
|
60
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
61
|
+
"temperature": 1.0,
|
|
62
|
+
"top_p": 0.95,
|
|
63
|
+
"stream_usage": True,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"model": "Qwen/Qwen3.5-397B-A17B",
|
|
67
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
68
|
+
"temperature": 1.0,
|
|
69
|
+
"top_p": 0.95,
|
|
70
|
+
"stream_usage": True,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"model": "Qwen/Qwen3.5-35B-A3B",
|
|
74
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
75
|
+
"temperature": 1.0,
|
|
76
|
+
"top_p": 0.95,
|
|
77
|
+
"stream_usage": True,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"model": "Qwen/Qwen3.5-27B",
|
|
81
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
82
|
+
"temperature": 1.0,
|
|
83
|
+
"top_p": 0.95,
|
|
84
|
+
"stream_usage": True,
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ensure_config_dir() -> Path:
|
|
90
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
91
|
+
return CONFIG_DIR
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
_vision_json_cache: tuple[float, dict] | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_vision_json() -> dict:
|
|
98
|
+
"""加载 vision_model.json,带 mtime 缓存"""
|
|
99
|
+
global _vision_json_cache
|
|
100
|
+
if not VISION_JSON.exists():
|
|
101
|
+
return {}
|
|
102
|
+
try:
|
|
103
|
+
mtime = VISION_JSON.stat().st_mtime
|
|
104
|
+
if _vision_json_cache and _vision_json_cache[0] == mtime:
|
|
105
|
+
return _vision_json_cache[1]
|
|
106
|
+
data = json.loads(VISION_JSON.read_text(encoding="utf-8"))
|
|
107
|
+
_vision_json_cache = (mtime, data)
|
|
108
|
+
return data
|
|
109
|
+
except Exception:
|
|
110
|
+
return {}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_vision_json(data: dict) -> None:
|
|
114
|
+
global _vision_json_cache
|
|
115
|
+
ensure_config_dir()
|
|
116
|
+
VISION_JSON.write_text(
|
|
117
|
+
json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8"
|
|
118
|
+
)
|
|
119
|
+
_vision_json_cache = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_vision_default_model() -> dict | None:
|
|
123
|
+
"""获取当前默认视觉模型配置"""
|
|
124
|
+
data = load_vision_json()
|
|
125
|
+
default = data.get("default")
|
|
126
|
+
if default and default.get("api_key"):
|
|
127
|
+
return default
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_vision_fallback_models() -> list[dict]:
|
|
132
|
+
"""获取备用视觉模型列表"""
|
|
133
|
+
data = load_vision_json()
|
|
134
|
+
fallback = data.get("fallback", {})
|
|
135
|
+
return [v for k, v in fallback.items() if v.get("api_key")]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _detect_modelscope_api_key() -> str | None:
|
|
139
|
+
"""检测 ModelScope API Key(环境变量 → model.json)"""
|
|
140
|
+
# 优先从环境变量
|
|
141
|
+
key = os.getenv("ModelScopeToken", "")
|
|
142
|
+
if key:
|
|
143
|
+
return key
|
|
144
|
+
|
|
145
|
+
# 从已配置的 model.json 中找 ModelScope 的 key
|
|
146
|
+
model_json_path = CONFIG_DIR / "model.json"
|
|
147
|
+
if model_json_path.exists():
|
|
148
|
+
try:
|
|
149
|
+
data = json.loads(model_json_path.read_text(encoding="utf-8"))
|
|
150
|
+
default = data.get("default", {})
|
|
151
|
+
if default.get("base_url") == MODELSCOPE_BASE_URL and default.get("api_key"):
|
|
152
|
+
return default["api_key"]
|
|
153
|
+
# 检查 fallback
|
|
154
|
+
for cfg in data.get("fallback", {}).values():
|
|
155
|
+
if cfg.get("base_url") == MODELSCOPE_BASE_URL and cfg.get("api_key"):
|
|
156
|
+
return cfg["api_key"]
|
|
157
|
+
except Exception: # pragma: no cover
|
|
158
|
+
pass # pragma: no cover
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _build_vision_config(api_key: str) -> dict:
|
|
163
|
+
"""用预设模型 + api_key 构建完整视觉配置"""
|
|
164
|
+
default_cfg = dict(VISION_MODEL_PRESETS[0])
|
|
165
|
+
default_cfg["api_key"] = api_key
|
|
166
|
+
|
|
167
|
+
fallback = {}
|
|
168
|
+
for preset in VISION_MODEL_PRESETS[1:]:
|
|
169
|
+
cfg = dict(preset)
|
|
170
|
+
cfg["api_key"] = api_key
|
|
171
|
+
fallback[cfg["model"]] = cfg
|
|
172
|
+
|
|
173
|
+
return {"default": default_cfg, "fallback": fallback}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def auto_configure_vision() -> dict | None:
|
|
177
|
+
"""自动配置视觉模型(静默模式,不需要用户交互)。
|
|
178
|
+
|
|
179
|
+
从环境变量或已配置的 ModelScope key 自动生成视觉配置。
|
|
180
|
+
与已有的视觉模型配置合并,不覆盖已有的默认模型。
|
|
181
|
+
返回默认模型配置,失败返回 None。
|
|
182
|
+
"""
|
|
183
|
+
api_key = _detect_modelscope_api_key()
|
|
184
|
+
if not api_key:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
data = load_vision_json()
|
|
188
|
+
existing_default = data.get("default", {})
|
|
189
|
+
existing_fallback = data.get("fallback", {})
|
|
190
|
+
|
|
191
|
+
# 已有相同 key 的 ModelScope 默认配置则跳过
|
|
192
|
+
if (
|
|
193
|
+
existing_default.get("base_url") == MODELSCOPE_BASE_URL
|
|
194
|
+
and existing_default.get("api_key") == api_key
|
|
195
|
+
):
|
|
196
|
+
return existing_default
|
|
197
|
+
|
|
198
|
+
# 已有其他提供商的默认视觉模型 → 只把 ModelScope 模型加入 fallback
|
|
199
|
+
if existing_default and existing_default.get("api_key"):
|
|
200
|
+
# 将 ModelScope 预设模型加入 fallback(去重)
|
|
201
|
+
for preset in VISION_MODEL_PRESETS:
|
|
202
|
+
cfg = dict(preset)
|
|
203
|
+
cfg["api_key"] = api_key
|
|
204
|
+
if cfg["model"] not in existing_fallback:
|
|
205
|
+
existing_fallback[cfg["model"]] = cfg
|
|
206
|
+
data["fallback"] = existing_fallback
|
|
207
|
+
save_vision_json(data)
|
|
208
|
+
return existing_default
|
|
209
|
+
|
|
210
|
+
# 没有默认视觉模型 → ModelScope 设为默认
|
|
211
|
+
new_default = dict(VISION_MODEL_PRESETS[0])
|
|
212
|
+
new_default["api_key"] = api_key
|
|
213
|
+
new_fallback = {}
|
|
214
|
+
for preset in VISION_MODEL_PRESETS[1:]:
|
|
215
|
+
cfg = dict(preset)
|
|
216
|
+
cfg["api_key"] = api_key
|
|
217
|
+
new_fallback[cfg["model"]] = cfg
|
|
218
|
+
|
|
219
|
+
data["default"] = new_default
|
|
220
|
+
data["fallback"] = {**existing_fallback, **new_fallback}
|
|
221
|
+
save_vision_json(data)
|
|
222
|
+
return data["default"]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def configure_vision_interactive() -> dict | None:
|
|
226
|
+
"""交互式配置视觉模型(/vision 命令调用)"""
|
|
227
|
+
ensure_config_dir()
|
|
228
|
+
|
|
229
|
+
current = load_vision_json()
|
|
230
|
+
current_default = current.get("default", {})
|
|
231
|
+
has_config = bool(current_default and current_default.get("api_key"))
|
|
232
|
+
|
|
233
|
+
if has_config:
|
|
234
|
+
action = await select(
|
|
235
|
+
"视觉模型配置:",
|
|
236
|
+
["查看当前配置", "重新配置", "切换模型", "返回"],
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
action = await select(
|
|
240
|
+
"视觉模型未配置,是否现在配置?",
|
|
241
|
+
["配置视觉模型", "返回"],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if action is None or action == "返回":
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
if action == "查看当前配置":
|
|
248
|
+
_display_vision_config(current)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
if action == "切换模型":
|
|
252
|
+
return await _switch_vision_model()
|
|
253
|
+
|
|
254
|
+
# 配置
|
|
255
|
+
return await _configure_vision_wizard()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _configure_vision_wizard() -> dict | None:
|
|
259
|
+
"""配置向导"""
|
|
260
|
+
# 选择 API Key 来源
|
|
261
|
+
env_key = os.getenv("ModelScopeToken", "")
|
|
262
|
+
choices = []
|
|
263
|
+
if env_key:
|
|
264
|
+
choices.append(f"使用环境变量 ModelScopeToken ({env_key[:6]}...{env_key[-4:]})")
|
|
265
|
+
choices.append("手动输入 API Key")
|
|
266
|
+
|
|
267
|
+
result = await select("选择 API Key 来源:", choices)
|
|
268
|
+
if result is None:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
if result.startswith("使用环境变量"):
|
|
272
|
+
api_key = env_key
|
|
273
|
+
else:
|
|
274
|
+
api_key = await password("输入 ModelScope API Key:")
|
|
275
|
+
if not api_key or not api_key.strip():
|
|
276
|
+
return None
|
|
277
|
+
api_key = api_key.strip()
|
|
278
|
+
|
|
279
|
+
# 选择默认模型
|
|
280
|
+
preset_names = [p["model"] for p in VISION_MODEL_PRESETS]
|
|
281
|
+
default_choice = await select("选择默认视觉模型:", preset_names, default=preset_names[0])
|
|
282
|
+
if default_choice is None:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
# 构建:用户选的模型作为 default,其余作为 fallback
|
|
286
|
+
all_presets = {p["model"]: p for p in VISION_MODEL_PRESETS}
|
|
287
|
+
default_preset = all_presets[default_choice]
|
|
288
|
+
default_cfg = dict(default_preset)
|
|
289
|
+
default_cfg["api_key"] = api_key
|
|
290
|
+
|
|
291
|
+
fallback = {}
|
|
292
|
+
for model_name, preset in all_presets.items():
|
|
293
|
+
if model_name == default_choice:
|
|
294
|
+
continue
|
|
295
|
+
cfg = dict(preset)
|
|
296
|
+
cfg["api_key"] = api_key
|
|
297
|
+
fallback[model_name] = cfg
|
|
298
|
+
|
|
299
|
+
config = {"default": default_cfg, "fallback": fallback}
|
|
300
|
+
save_vision_json(config)
|
|
301
|
+
|
|
302
|
+
console.print(f"[green]视觉模型配置完成: {default_choice} (默认)[/green]")
|
|
303
|
+
fallback_names = ", ".join(fallback.keys())
|
|
304
|
+
console.print(f"[dim]备用模型 ({len(fallback)} 个): {fallback_names}[/dim]")
|
|
305
|
+
|
|
306
|
+
return default_cfg
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def _switch_vision_model() -> dict | None:
|
|
310
|
+
"""切换视觉模型(从 fallback 列表选择)"""
|
|
311
|
+
data = load_vision_json()
|
|
312
|
+
default = data.get("default", {})
|
|
313
|
+
fallback = data.get("fallback", {})
|
|
314
|
+
|
|
315
|
+
if not default: # pragma: no cover
|
|
316
|
+
console.print("[yellow]请先配置默认视觉模型[/yellow]") # pragma: no cover
|
|
317
|
+
return await _configure_vision_wizard() # pragma: no cover
|
|
318
|
+
|
|
319
|
+
if not fallback: # pragma: no cover
|
|
320
|
+
console.print("[yellow]没有备用视觉模型可切换[/yellow]") # pragma: no cover
|
|
321
|
+
return None # pragma: no cover
|
|
322
|
+
|
|
323
|
+
current_name = default.get("model", "")
|
|
324
|
+
choices = []
|
|
325
|
+
for name in fallback:
|
|
326
|
+
tag = " (当前默认)" if name == current_name else ""
|
|
327
|
+
choices.append(f"{name}{tag}")
|
|
328
|
+
|
|
329
|
+
result = await select("选择要使用的视觉模型:", choices)
|
|
330
|
+
if result is None: # pragma: no cover
|
|
331
|
+
return None # pragma: no cover
|
|
332
|
+
|
|
333
|
+
selected_name = result.replace(" (当前默认)", "")
|
|
334
|
+
|
|
335
|
+
ok = await confirm(f"确定切换到 {selected_name}?当前默认将移至备用列表")
|
|
336
|
+
if not ok:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
selected_config = fallback.pop(selected_name)
|
|
340
|
+
if default:
|
|
341
|
+
fallback[current_name] = default
|
|
342
|
+
|
|
343
|
+
data["default"] = selected_config
|
|
344
|
+
data["fallback"] = fallback
|
|
345
|
+
save_vision_json(data)
|
|
346
|
+
console.print(f"[green]已切换到: {selected_name}[/green]")
|
|
347
|
+
return selected_config
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _display_vision_config(config: dict) -> None:
|
|
351
|
+
"""显示当前视觉模型配置"""
|
|
352
|
+
from rich.table import Table
|
|
353
|
+
|
|
354
|
+
default = config.get("default", {})
|
|
355
|
+
fallback = config.get("fallback", {})
|
|
356
|
+
|
|
357
|
+
if not default:
|
|
358
|
+
console.print("[yellow]未配置视觉模型[/yellow]")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
console.print(f"[bold]默认视觉模型:[/bold] {default.get('model', '未知')}")
|
|
362
|
+
|
|
363
|
+
if fallback:
|
|
364
|
+
table = Table(title="备用视觉模型")
|
|
365
|
+
table.add_column("模型", style="cyan")
|
|
366
|
+
table.add_column("状态", style="green")
|
|
367
|
+
for name in fallback:
|
|
368
|
+
table.add_row(name, "✓")
|
|
369
|
+
console.print(table)
|
|
370
|
+
else:
|
|
371
|
+
console.print("[dim]无备用模型[/dim]")
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chcode
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal-based AI coding agent with typer+rich
|
|
5
|
+
Project-URL: Homepage, https://github.com/ScarletMercy/chcode
|
|
6
|
+
Project-URL: Repository, https://github.com/ScarletMercy/chcode
|
|
7
|
+
Project-URL: Issues, https://github.com/ScarletMercy/chcode/issues
|
|
8
|
+
Author-email: Flymo Han <minimizeball@foxmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,cli,coding-agent,langchain,rich,typer
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
21
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
22
|
+
Requires-Dist: bs4>=0.0.2
|
|
23
|
+
Requires-Dist: charset-normalizer>=3.4.0
|
|
24
|
+
Requires-Dist: httpx>=0.28.0
|
|
25
|
+
Requires-Dist: langchain-openai>=1.1.10
|
|
26
|
+
Requires-Dist: langchain>=1.2.10
|
|
27
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=3.0.3
|
|
28
|
+
Requires-Dist: markdownify>=0.14.0
|
|
29
|
+
Requires-Dist: pillow>=12.2.0
|
|
30
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
31
|
+
Requires-Dist: psutil>=6.0.0
|
|
32
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
33
|
+
Requires-Dist: questionary>=2.1.0
|
|
34
|
+
Requires-Dist: rich>=13.9.0
|
|
35
|
+
Requires-Dist: tavily>=1.1.0
|
|
36
|
+
Requires-Dist: typer>=0.15.0
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'test'
|
|
39
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == 'test'
|
|
40
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == 'test'
|
|
41
|
+
Requires-Dist: pytest-timeout>=2.3.0; extra == 'test'
|
|
42
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# ChCode
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
███████╗ ██╗ ██╗ ███████╗ ██████╗ █████╗ ████████╗
|
|
49
|
+
██╔═════╝ ██║ ██║ ██╔═════╝ ██╔═══██╗ ██╔══██╗ ██╔═════╝
|
|
50
|
+
██║ ████████║ ██║ ██║ ██║ ██║ ██╗ ████████╗
|
|
51
|
+
██║ ██╔═══██║ ██║ ██║ ██║ ██║ ██╔╝ ██╔═════╝
|
|
52
|
+
████████╗ ██║ ██║ ████████╗ ╚██████╔╝ █████╔═╝ ████████╗
|
|
53
|
+
╚══════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚════╝ ╚══════╝
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Terminal-based AI coding agent, built with LangChain + Typer + Rich.
|
|
57
|
+
|
|
58
|
+
> **Why "ChCode"?** The original prototype was a tkinter + LangChain app called **chat-agent** (chagent). When it evolved into a CLI tool, the name became **ChCode** — chat-agent, meet code.
|
|
59
|
+
|
|
60
|
+
<details>
|
|
61
|
+
<summary>📸 chagent — the original tkinter prototype</summary>
|
|
62
|
+
<img src="https://raw.githubusercontent.com/ScarletMercy/chcode/main/assets/chagent.png" alt="chagent prototype" width="600"/>
|
|
63
|
+
</details>
|
|
64
|
+
|
|
65
|
+
> 6000+ lines of Python, 14 built-in tools, full session persistence, git-aware workflow.
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
[中文文档](README_zh.md)
|
|
71
|
+
|
|
72
|
+
<img src="https://raw.githubusercontent.com/ScarletMercy/chcode/main/assets/chcode.png" alt="ChCode main interface" width="800"/>
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
### Model Management
|
|
77
|
+
|
|
78
|
+
- Compatible with **all OpenAI-compatible APIs** (OpenAI, DeepSeek, Qwen, GLM, Claude via proxy, etc.)
|
|
79
|
+
- Built-in quick setup for **ModelScope**, **LongCat**, and major providers
|
|
80
|
+
- **ModelScope**: 2000 free model calls/day
|
|
81
|
+
- **LongCat**: 50M+ free tokens/day minimum
|
|
82
|
+
- First-run wizard with **env auto-detection** (scans `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `ZHIPU_API_KEY`, `ModelScopeToken`, etc.)
|
|
83
|
+
- Native **reasoning/thinking model** support — thinking tokens displayed in real time
|
|
84
|
+
- Create / edit / switch models at runtime
|
|
85
|
+
- Per-model hyperparameter tuning (temperature, top_p, top_k, max_completion_tokens, stop_sequences, etc.)
|
|
86
|
+
- Automatic **retry with exponential backoff** (3/10/30/60s) and fallback model switching on persistent failure
|
|
87
|
+
|
|
88
|
+
### Vision & Multimodal
|
|
89
|
+
|
|
90
|
+
- Dedicated vision model configuration via `/vision` command (independent from main model)
|
|
91
|
+
- Image analysis with **automatic media encoding** and base64 embedding
|
|
92
|
+
- **Video support** — send videos directly to vision models for analysis (MP4, MOV, AVI, MKV, WebM)
|
|
93
|
+
- Automatic image resizing for oversized inputs
|
|
94
|
+
- Supported image formats: PNG, JPG, JPEG, GIF, BMP, WebP, TIFF
|
|
95
|
+
|
|
96
|
+
### Session & History
|
|
97
|
+
|
|
98
|
+
- **Persistent sessions** with SQLite-backed checkpoints (LangGraph)
|
|
99
|
+
- Session list, switch, rename, delete
|
|
100
|
+
- **Context compression** — auto-summarize when approaching token limit
|
|
101
|
+
- Real-time **context usage display** in status bar
|
|
102
|
+
|
|
103
|
+
### Git Integration
|
|
104
|
+
|
|
105
|
+
- Working directory **rolls back with message edits**
|
|
106
|
+
- Create **branches from any message** (fork)
|
|
107
|
+
- Edit / fork / delete history messages via `/messages`
|
|
108
|
+
- Checkpoint counter in status bar
|
|
109
|
+
|
|
110
|
+
### Human-in-the-Loop
|
|
111
|
+
|
|
112
|
+
- **Common mode** — every tool call requires approval, with diff preview for edits
|
|
113
|
+
- **YOLO mode** — auto-approve everything
|
|
114
|
+
- Toggle with `Tab` key or `/mode` command
|
|
115
|
+
|
|
116
|
+
### Work Environment Isolation
|
|
117
|
+
|
|
118
|
+
- Per-project `.chat/` directory for sessions, skills, agents
|
|
119
|
+
- Global `~/.chat/` for shared skills and settings
|
|
120
|
+
- `/workdir` to switch project root
|
|
121
|
+
|
|
122
|
+
### Cross-Platform
|
|
123
|
+
|
|
124
|
+
- **Windows** — defaults to Git Bash, falls back to PowerShell
|
|
125
|
+
- **Linux / Mac** — native bash/zsh
|
|
126
|
+
- Persistent shell sessions with **automatic CWD tracking**
|
|
127
|
+
|
|
128
|
+
### Rich Terminal UI
|
|
129
|
+
|
|
130
|
+
- Real-time **status bar** — context usage %, git checkpoint count, ModelScope API quota
|
|
131
|
+
- **Streaming output** with token-by-token rendering
|
|
132
|
+
- Slash command auto-completion
|
|
133
|
+
- Color-coded tool approval UI with **inline diff preview** for file edits
|
|
134
|
+
|
|
135
|
+
### Observability
|
|
136
|
+
|
|
137
|
+
- **LangSmith tracing** — toggle on/off via `/langsmith` command
|
|
138
|
+
- Auto-disable tracing on 429 rate limit with user notification
|
|
139
|
+
|
|
140
|
+
### Sub-Agent System
|
|
141
|
+
|
|
142
|
+
- Three built-in agent types: **Explore** (codebase search, read-only), **Plan** (architecture design), **General** (full-capability coding)
|
|
143
|
+
- **Parallel execution** — launch multiple agents concurrently for independent tasks
|
|
144
|
+
- Sub-agents run with **isolated context**, protecting the main conversation from context pollution
|
|
145
|
+
- **Custom agents** — define your own agent types in `.chat/agents/` with dedicated tools and instructions
|
|
146
|
+
|
|
147
|
+
### Skill System
|
|
148
|
+
|
|
149
|
+
- Install / delete / manage skills via `/skill`
|
|
150
|
+
- Skills are injected into system prompt via LangChain middleware
|
|
151
|
+
- Supports project-level and global skill directories
|
|
152
|
+
|
|
153
|
+
### ModelScope Rate Limit
|
|
154
|
+
|
|
155
|
+
- Real-time **API quota display** in status bar (daily limit remaining, per-model remaining)
|
|
156
|
+
- Auto-enabled when using ModelScope models
|
|
157
|
+
|
|
158
|
+
## Built-in Tools (14)
|
|
159
|
+
|
|
160
|
+
| Tool | Description |
|
|
161
|
+
|------|-------------|
|
|
162
|
+
| `read` | Read file content with line numbers and offset |
|
|
163
|
+
| `write` | Create or overwrite files |
|
|
164
|
+
| `edit` | Surgical string replacement in existing files |
|
|
165
|
+
| `glob` | Find files by name pattern |
|
|
166
|
+
| `grep` | Search file contents with regex |
|
|
167
|
+
| `list_dir` | Browse directory structure |
|
|
168
|
+
| `bash` | Execute shell commands (Git Bash / PowerShell / bash) |
|
|
169
|
+
| `load_skill` | Dynamically load skill instructions via middleware |
|
|
170
|
+
| `web_fetch` | Fetch and convert URL content to markdown |
|
|
171
|
+
| `web_search` | Web search via [Tavily](https://tavily.com) |
|
|
172
|
+
| `ask_user` | Single-select, multi-select, batch questions for user interaction |
|
|
173
|
+
| `agent` | Launch sub-agents (explore, plan, general-purpose, custom), supports parallel execution |
|
|
174
|
+
| `todo_write` | Structured task tracking for complex multi-step work |
|
|
175
|
+
| `vision` | Analyze images and videos via ModelScope vision models |
|
|
176
|
+
|
|
177
|
+
## Quick Start
|
|
178
|
+
|
|
179
|
+
### Install
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Option 1: Install with pip
|
|
183
|
+
pip install chcode
|
|
184
|
+
|
|
185
|
+
# Option 2: Install with uv (recommended)
|
|
186
|
+
uv tool install chcode
|
|
187
|
+
|
|
188
|
+
# Option 3: Install with pipx
|
|
189
|
+
pipx install chcode
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Run
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Start interactive session
|
|
196
|
+
chcode
|
|
197
|
+
|
|
198
|
+
# Start in YOLO mode
|
|
199
|
+
chcode --yolo
|
|
200
|
+
|
|
201
|
+
# Model management
|
|
202
|
+
chcode config new # add new model
|
|
203
|
+
chcode config edit # edit current model
|
|
204
|
+
chcode config switch # switch model
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### First Run
|
|
208
|
+
|
|
209
|
+
On first launch, ChCode will:
|
|
210
|
+
|
|
211
|
+
1. Scan environment variables for known API keys
|
|
212
|
+
2. Guide you through model configuration
|
|
213
|
+
3. Optionally configure Tavily for web search
|
|
214
|
+
|
|
215
|
+
## Commands
|
|
216
|
+
|
|
217
|
+
| Command | Description |
|
|
218
|
+
|---------|-------------|
|
|
219
|
+
| `/new` | Start new session |
|
|
220
|
+
| `/history` | Browse and switch sessions |
|
|
221
|
+
| `/model` | Model management (new / edit / switch) |
|
|
222
|
+
| `/vision` | Visual model configuration |
|
|
223
|
+
| `/messages` | Edit / fork / delete history messages |
|
|
224
|
+
| `/compress` | Compress current session |
|
|
225
|
+
| `/skill` | Manage skills |
|
|
226
|
+
| `/search` | Configure Tavily API key |
|
|
227
|
+
| `/workdir` | Switch working directory |
|
|
228
|
+
| `/mode` | Toggle Common / YOLO mode |
|
|
229
|
+
| `/git` | Show git status |
|
|
230
|
+
| `/langsmith` | Toggle LangSmith tracing |
|
|
231
|
+
| `/tools` | List built-in tools |
|
|
232
|
+
| `/quit` | Exit |
|
|
233
|
+
|
|
234
|
+
## Keybindings
|
|
235
|
+
|
|
236
|
+
| Key | Action |
|
|
237
|
+
|-----|--------|
|
|
238
|
+
| `Enter` | Send message |
|
|
239
|
+
| `Ctrl+Enter` | New line |
|
|
240
|
+
| `Tab` | Toggle Common/YOLO mode (when input empty) |
|
|
241
|
+
| `Ctrl+C` | Interrupt generation |
|
|
242
|
+
|
|
243
|
+
## Why No MCP?
|
|
244
|
+
|
|
245
|
+
ChCode intentionally does not integrate MCP (Model Context Protocol). The combination of **Skills + CLI tools** covers 95%+ of real-world coding agent scenarios. Skills provide structured, reusable instructions injected via middleware — simpler, faster, and more portable than MCP servers.
|
|
246
|
+
|
|
247
|
+
## Architecture
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
chcode/
|
|
251
|
+
├── cli.py # Typer CLI entry
|
|
252
|
+
├── chat.py # REPL main loop, slash commands, HITL
|
|
253
|
+
├── agent_setup.py # Agent construction, middleware, model retry with fallback
|
|
254
|
+
├── config.py # Model config, Tavily, env detection
|
|
255
|
+
├── display.py # Rich rendering, streaming, status bar
|
|
256
|
+
├── prompts.py # Interactive prompts (select/confirm/text)
|
|
257
|
+
├── session.py # Session manager (SQLite)
|
|
258
|
+
├── skill_manager.py # Skill install/delete UI
|
|
259
|
+
├── agents/
|
|
260
|
+
│ ├── definitions.py # Agent types (explore, plan, general)
|
|
261
|
+
│ ├── loader.py # Load custom agents from .chat/agents/
|
|
262
|
+
│ └── runner.py # Sub-agent execution with middleware
|
|
263
|
+
└── utils/
|
|
264
|
+
├── tools.py # 14 built-in tools
|
|
265
|
+
├── shell/ # Shell abstraction (Bash/PowerShell providers)
|
|
266
|
+
├── enhanced_chat_openai.py # Extended ChatOpenAI with reasoning support
|
|
267
|
+
├── git_manager.py # Git checkpoint management
|
|
268
|
+
├── skill_loader.py # Skill discovery and loading
|
|
269
|
+
├── modelscope_ratelimit.py # ModelScope API rate limit monitor
|
|
270
|
+
└── tool_result_pipeline.py # Output truncation and budget enforcement
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
chcode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
chcode/__main__.py,sha256=Ruf2NjnBOyZwKKlwZYYbzu14axJuL_HHbeQEM3ZSFLg,95
|
|
3
|
+
chcode/agent_setup.py,sha256=5jBoK1boiZjMbRyvcnF-2xReJdjUzWbfXJV8Hdd5NWw,14275
|
|
4
|
+
chcode/chat.py,sha256=5o2J2-4Ga_JenLhwLd23IZ5Zv0q8USS0k3H6QAo-PQk,65830
|
|
5
|
+
chcode/cli.py,sha256=c-UpcoLDxTuJVLLwk4BYTYTuFTiIRFw6HJM0eCtGCSQ,3646
|
|
6
|
+
chcode/config.py,sha256=Eu1owyZkMKPc5wgix9RfXo_RuOelyepZoq9miS36kW8,18893
|
|
7
|
+
chcode/display.py,sha256=4aaNTsYzO0h9Hqlrl3jT7s9pfd32y8fKcvCoHqiEaQs,9309
|
|
8
|
+
chcode/prompts.py,sha256=Pc5lez4UBp_j_d2n_c92mtDm_LVQTOOJk2VTsy31sOM,19167
|
|
9
|
+
chcode/session.py,sha256=ROjLEug4abKJEzHc_bNQeKt9l3gPsKpxQfzatLY7MIg,5254
|
|
10
|
+
chcode/skill_manager.py,sha256=coxZPil_yk9YpAVO3wn1-3l9of5xpWRWFjqS_DtD0jo,4742
|
|
11
|
+
chcode/vision_config.py,sha256=La-kDmPwNAwDzLE48xetce4hLw1NOFpo6ZkLaO9AdrM,11510
|
|
12
|
+
chcode/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
chcode/agents/definitions.py,sha256=VD4sBChDF174bvK6_d5qgQY7DbPHJVbLRGoGdc0JNJY,7249
|
|
14
|
+
chcode/agents/loader.py,sha256=jAQfZ__6yO_BkNUCjrcQoJwvnoKAr5WdDcrpyfx0JyE,2693
|
|
15
|
+
chcode/agents/runner.py,sha256=i9QHGbqoc2xOF-1UZmXdo-K4VUCiUORmf_NcdUj_aNs,4939
|
|
16
|
+
chcode/utils/__init__.py,sha256=YrOe3vTZByoRkxMcff2sw3-XPcNtMsDwyZFyR_FkbIw,85
|
|
17
|
+
chcode/utils/enhanced_chat_openai.py,sha256=0eYqgt-UKGN8rhmH0UkLwAW04s2jDM2D6A6liR-tZdk,13965
|
|
18
|
+
chcode/utils/git_checker.py,sha256=nrH3nwTbPB6dMmtCu8zygi-7MvXtE983_tLGnrKWymE,1167
|
|
19
|
+
chcode/utils/git_manager.py,sha256=1EKn9ovUAmjMBxuKfFIFo64YvrgIV6hLbeUGrDXyPRg,9786
|
|
20
|
+
chcode/utils/modelscope_ratelimit.py,sha256=jVNsUD9JRsUr44yt-dfqOAE7Prh58wHwtNp9OAxwsm8,2218
|
|
21
|
+
chcode/utils/multimodal.py,sha256=2PYmUkheD2Dd6L6SGLFfHZdbK3sMmhNkRQBhvg8jY20,8962
|
|
22
|
+
chcode/utils/skill_loader.py,sha256=XL0njUDNSWkAax6FAR4Rhq-CZEQ8YyBNb_q_Fr2rq3I,17689
|
|
23
|
+
chcode/utils/text_utils.py,sha256=Qq4hfTBKKK9hN4VcNGtrx3eTOh6RAL47H-AW1PdN-9s,566
|
|
24
|
+
chcode/utils/tool_result_pipeline.py,sha256=NB5HpI9XUXRhq2Nxd17I3vIq1Vaihcjll7w_02yPowE,7222
|
|
25
|
+
chcode/utils/tools.py,sha256=WGQE-EXbfAgnYMboS6HxWoYgUxNNla-HQSfYLgn2n4Q,53844
|
|
26
|
+
chcode/utils/shell/__init__.py,sha256=sgJnNzMhnwYF3o-0UEnYF_foeyqNeVHBlqkkrMYJvug,564
|
|
27
|
+
chcode/utils/shell/output.py,sha256=G12tftYwM1ZZ0Wi6IwoeGUREuwwEskJNSxSy947V9cU,1807
|
|
28
|
+
chcode/utils/shell/provider.py,sha256=GBscA2IkKM64r6ZFWQWF7MBHyPR6_xq8GwZosYBgS7U,4011
|
|
29
|
+
chcode/utils/shell/result.py,sha256=Zwx_teqeyYrg8RDXIIPgWntzfyyVprGurhI8VWMLxqA,305
|
|
30
|
+
chcode/utils/shell/semantics.py,sha256=ewO-gC_sNuR0HtIPYnGlBo1Iz2oEKhezZ5eLIuk8tHg,1937
|
|
31
|
+
chcode/utils/shell/session.py,sha256=0wCr5gZ40_2C-UllbG-DNCI4NLkO7bhxsqLn9JjjMes,5083
|
|
32
|
+
chcode-0.1.0.dist-info/METADATA,sha256=YuJ7Yg0au6_CllMFasbv5K35grqA1Xz45pyo-Lvs9Sc,10998
|
|
33
|
+
chcode-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
34
|
+
chcode-0.1.0.dist-info/entry_points.txt,sha256=IfRbpalQYkqvoETi6MCWmx2s7lPWX21L7h_vqIYqCjI,42
|
|
35
|
+
chcode-0.1.0.dist-info/licenses/LICENSE,sha256=nHXAAMmgstqUKPHGO353VN94SKEhxr64CvZOVTV2PII,1066
|
|
36
|
+
chcode-0.1.0.dist-info/RECORD,,
|