zrcoder 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.
zrcoder-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zr
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 WARRANTIES 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.
zrcoder-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: zrcoder
3
+ Version: 0.1.0
4
+ Summary: Terminal coding agent chat — OpenAI-compatible APIs, tools, and slash commands
5
+ Keywords: cli,agent,coding,terminal,openai
6
+ Author: Zr
7
+ Author-email: Zr <547471919@qq.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development
18
+ Requires-Dist: prompt-toolkit>=3.0.53
19
+ Requires-Dist: pydantic-ai>=2.40.0
20
+ Requires-Dist: python-dotenv>=1.2.3
21
+ Requires-Dist: rich>=15.0.0
22
+ Requires-Python: >=3.12
23
+ Project-URL: Homepage, https://github.com/ZRMYDYCG/ClaudeCode
24
+ Project-URL: Repository, https://github.com/ZRMYDYCG/ClaudeCode
25
+ Project-URL: Issues, https://github.com/ZRMYDYCG/ClaudeCode/issues
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Zrcoder
29
+
30
+ 终端里的编程助手:连 OpenAI 兼容 API,可读写文件、执行命令,并支持 `/help` 等斜杠命令。
31
+
32
+ ## 安装
33
+
34
+ 需要 Python **3.12+**。推荐用 [uv](https://docs.astral.sh/uv/):
35
+
36
+ ```bash
37
+ uv tool install zrcoder
38
+ ```
39
+
40
+ 也可用 pipx / pip:
41
+
42
+ ```bash
43
+ pipx install zrcoder
44
+ # 或
45
+ pip install zrcoder
46
+ ```
47
+
48
+ ## 配置
49
+
50
+ 在环境变量或项目目录的 `.env` 中设置(可参考 `.env.example`):
51
+
52
+ ```bash
53
+ export API_KEY=your_api_key
54
+ export BASE_URL=https://api.example.com/v1
55
+ # 可选,默认 deepseek-v4-flash
56
+ # export MODEL_NAME=deepseek-v4-flash
57
+ ```
58
+
59
+ ## 使用
60
+
61
+ ```bash
62
+ zrcoder
63
+ ```
64
+
65
+ 常用命令:`/help`、`/status`、`/new`、`/api-detail`、`/exit`。
66
+
67
+ ## 从源码开发
68
+
69
+ ```bash
70
+ git clone https://github.com/ZRMYDYCG/ClaudeCode.git
71
+ cd ClaudeCode
72
+ uv sync --group dev
73
+
74
+ uv run zrcoder
75
+ # 或
76
+ uv run python main.py
77
+
78
+ uv run ruff format .
79
+ uv run ruff check --fix .
80
+ uv run pytest
81
+ uv run mypy core
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,59 @@
1
+ # Zrcoder
2
+
3
+ 终端里的编程助手:连 OpenAI 兼容 API,可读写文件、执行命令,并支持 `/help` 等斜杠命令。
4
+
5
+ ## 安装
6
+
7
+ 需要 Python **3.12+**。推荐用 [uv](https://docs.astral.sh/uv/):
8
+
9
+ ```bash
10
+ uv tool install zrcoder
11
+ ```
12
+
13
+ 也可用 pipx / pip:
14
+
15
+ ```bash
16
+ pipx install zrcoder
17
+ # 或
18
+ pip install zrcoder
19
+ ```
20
+
21
+ ## 配置
22
+
23
+ 在环境变量或项目目录的 `.env` 中设置(可参考 `.env.example`):
24
+
25
+ ```bash
26
+ export API_KEY=your_api_key
27
+ export BASE_URL=https://api.example.com/v1
28
+ # 可选,默认 deepseek-v4-flash
29
+ # export MODEL_NAME=deepseek-v4-flash
30
+ ```
31
+
32
+ ## 使用
33
+
34
+ ```bash
35
+ zrcoder
36
+ ```
37
+
38
+ 常用命令:`/help`、`/status`、`/new`、`/api-detail`、`/exit`。
39
+
40
+ ## 从源码开发
41
+
42
+ ```bash
43
+ git clone https://github.com/ZRMYDYCG/ClaudeCode.git
44
+ cd ClaudeCode
45
+ uv sync --group dev
46
+
47
+ uv run zrcoder
48
+ # 或
49
+ uv run python main.py
50
+
51
+ uv run ruff format .
52
+ uv run ruff check --fix .
53
+ uv run pytest
54
+ uv run mypy core
55
+ ```
56
+
57
+ ## License
58
+
59
+ MIT
@@ -0,0 +1 @@
1
+ """Core package: agent runtime and terminal UI."""
@@ -0,0 +1,15 @@
1
+ """Coding Agent 完整封装包。
2
+
3
+ 外部代码只关心:
4
+ - agent: 配置好工具和 hooks 的 Agent 实例
5
+ - MODEL_NAME: 当前使用的模型名(/status 命令要用)
6
+ - api_call_log: 主循环跑 Agent 时收集的 API 调用元数据(/api-detail 命令要用)
7
+ - ApiCall: 一次 API 调用的数据结构
8
+
9
+ 子模块(tools / hooks / core)是实现细节,不需要直接 import。
10
+ """
11
+
12
+ from .core import MODEL_NAME, agent
13
+ from .hooks import ApiCall, api_call_log
14
+
15
+ __all__ = ["agent", "MODEL_NAME", "api_call_log", "ApiCall"]
@@ -0,0 +1,58 @@
1
+ """
2
+ Agent 实例化:把 model / instructions / tools / hooks 拼起来。
3
+ """
4
+
5
+ import os
6
+ from typing import Any, cast
7
+ from urllib.parse import urlparse, urlunparse
8
+
9
+ from dotenv import load_dotenv
10
+ from pydantic_ai import Agent
11
+ from pydantic_ai.models.openai import OpenAIChatModel
12
+ from pydantic_ai.providers.openai import OpenAIProvider
13
+
14
+ from .hooks import hooks
15
+ from .tools import TOOLS
16
+
17
+ load_dotenv()
18
+
19
+ # 从环境变量读取 API Key / Base URL(OpenAI 兼容接口)
20
+ API_KEY = os.environ.get("API_KEY")
21
+ if not API_KEY:
22
+ raise RuntimeError("请先设置环境变量 API_KEY")
23
+
24
+ BASE_URL = os.environ.get("BASE_URL")
25
+ if not BASE_URL:
26
+ raise RuntimeError("请先设置环境变量 BASE_URL")
27
+
28
+
29
+ def _normalize_openai_base_url(url: str) -> str:
30
+ """
31
+ OpenAI SDK 会自己拼 /chat/completions;若 .env 里写了完整 path,去掉末尾这段。
32
+ """
33
+ parsed = urlparse(url.strip())
34
+ path = parsed.path.rstrip("/")
35
+ for suffix in ("/chat/completions", "/completions"):
36
+ if path.endswith(suffix):
37
+ path = path[: -len(suffix)] or "/"
38
+ break
39
+ return urlunparse((parsed.scheme, parsed.netloc, path, "", "", ""))
40
+
41
+
42
+ MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-v4-flash")
43
+
44
+ model = OpenAIChatModel(
45
+ MODEL_NAME,
46
+ provider=OpenAIProvider(base_url=_normalize_openai_base_url(BASE_URL), api_key=API_KEY),
47
+ )
48
+
49
+ agent = Agent(
50
+ model,
51
+ instructions=(
52
+ "你是一个编程助手。你可以读写文件和执行命令来帮用户完成编程任务。\n"
53
+ "工作流程:先理解需求,写代码,然后运行验证。"
54
+ "如果有错误就修复并重新运行,直到确认正确。"
55
+ ),
56
+ tools=cast(Any, TOOLS),
57
+ capabilities=[hooks],
58
+ )
@@ -0,0 +1,73 @@
1
+ """
2
+ 挂在 Agent 上的 hooks,用来抓每次 model API 调用的元数据。
3
+
4
+ 主循环在每轮 run_sync 之前清空 api_call_log,跑完后快照到 SessionState 里,
5
+ /api-detail 命令再把这一轮的所有调用展示给用户。
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from pydantic_ai.capabilities import Hooks
12
+
13
+
14
+ @dataclass
15
+ class ApiCall:
16
+ """
17
+ 一次 model API 调用的元数据。before_model_request 创建并填充上半部分,
18
+ after_model_request 填充下半部分。
19
+ """
20
+
21
+ # request 侧
22
+ model: str
23
+ messages_count: int
24
+ # 这次发送给模型的 messages 中最后一条消息的最后一个 part
25
+ last_part: Any
26
+ tools: list[str]
27
+ # response 侧(after hook 填充)
28
+ finish_reason: str = ""
29
+ parts_kinds: list[str] = field(default_factory=list)
30
+ input_tokens: int = 0
31
+ output_tokens: int = 0
32
+
33
+
34
+ # 主循环在每轮 run_sync 之前清空它
35
+ api_call_log: list[ApiCall] = []
36
+
37
+ hooks = Hooks()
38
+
39
+
40
+ @hooks.on.before_model_request
41
+ async def _record_request(ctx: Any, request_context: Any) -> Any:
42
+ """
43
+ 每次发起 model 调用之前,创建一条 ApiCall 记录。
44
+ """
45
+ msgs = list(request_context.messages)
46
+ last_part = msgs[-1].parts[-1] if msgs and msgs[-1].parts else None
47
+ try:
48
+ tool_names = [t.name for t in request_context.model_request_parameters.function_tools]
49
+ except AttributeError:
50
+ tool_names = []
51
+ api_call_log.append(
52
+ ApiCall(
53
+ model=request_context.model.model_name,
54
+ messages_count=len(msgs),
55
+ last_part=last_part,
56
+ tools=tool_names,
57
+ )
58
+ )
59
+ return request_context
60
+
61
+
62
+ @hooks.on.after_model_request
63
+ async def _record_response(ctx: Any, request_context: Any, response: Any) -> Any:
64
+ """
65
+ 每次 model 调用返回后,填充上面这条 ApiCall 的 response 字段。
66
+ """
67
+ if api_call_log:
68
+ call = api_call_log[-1]
69
+ call.finish_reason = str(response.finish_reason) if response.finish_reason else "unknown"
70
+ call.parts_kinds = [p.part_kind for p in response.parts]
71
+ call.input_tokens = response.usage.input_tokens
72
+ call.output_tokens = response.usage.output_tokens
73
+ return response
@@ -0,0 +1,45 @@
1
+ """
2
+ Coding Agent 用到的三个工具:读文件、写文件、跑 shell 命令。
3
+ """
4
+
5
+ import subprocess
6
+
7
+
8
+ def read_file(path: str) -> str:
9
+ """
10
+ 读取指定文件的内容。
11
+ """
12
+ try:
13
+ with open(path, encoding="utf-8") as f:
14
+ return f.read()
15
+ except FileNotFoundError:
16
+ return f"错误:文件 {path} 不存在"
17
+
18
+
19
+ def write_file(path: str, content: str) -> str:
20
+ """
21
+ 将内容写入指定文件。
22
+ """
23
+ with open(path, "w", encoding="utf-8") as f:
24
+ f.write(content)
25
+ return f"已写入 {path}"
26
+
27
+
28
+ def run_command(command: str) -> str:
29
+ """
30
+ 执行一条 shell 命令并返回输出。
31
+ """
32
+ try:
33
+ result = subprocess.run(
34
+ command, shell=True, capture_output=True, text=True, errors="replace", timeout=10
35
+ )
36
+ output = result.stdout
37
+ if result.returncode != 0:
38
+ output += f"\n[错误] {result.stderr}"
39
+ return output or "(无输出)"
40
+ except subprocess.TimeoutExpired:
41
+ return "[错误] 命令执行超时(10秒)"
42
+
43
+
44
+ # Pydantic AI 支持 tools=[plain_function],从函数签名 + docstring 自动生成 JSON Schema
45
+ TOOLS = [read_file, write_file, run_command]
@@ -0,0 +1,90 @@
1
+ """
2
+ 终端主循环:读输入、分发斜杠命令、驱动 Agent。
3
+ """
4
+
5
+ from typing import Any, Literal
6
+
7
+ from prompt_toolkit import PromptSession
8
+
9
+ from core.agent import MODEL_NAME, agent, api_call_log
10
+ from core.ui.commands import COMMANDS, SessionState, print_agent_steps, print_divider
11
+ from core.ui.render import console, print_welcome_banner
12
+
13
+ # PromptSession 比内置 input() 好用:支持左右移光标编辑,并记住本次输入历史
14
+ prompt_session: PromptSession[str] = PromptSession()
15
+
16
+ CommandAction = Literal["pass", "continue", "break"]
17
+
18
+
19
+ def read_user_input() -> str | None:
20
+ """
21
+ 打印上横线并读一行用户输入;回车后再补一条下横线。
22
+ 返回 None 表示用户希望退出(Ctrl-C / Ctrl-D)。
23
+ """
24
+ print_divider()
25
+ try:
26
+ user_input = prompt_session.prompt("❯ ").strip()
27
+ except (EOFError, KeyboardInterrupt):
28
+ print()
29
+ return None
30
+ print_divider()
31
+ return user_input
32
+
33
+
34
+ def handle_command(user_input: str, state: SessionState) -> CommandAction:
35
+ """
36
+ 处理以 / 开头的命令。
37
+ 返回 'pass':不是命令,主循环继续往下走交给 Agent;
38
+ 返回 'continue':命令已处理,主循环跳到下一轮;
39
+ 返回 'break':命令要求退出主循环。
40
+ """
41
+ if not user_input.startswith("/"):
42
+ return "pass"
43
+ cmd_name = user_input[1:].split()[0]
44
+ command = COMMANDS.get(cmd_name)
45
+ if command is None:
46
+ console.print(f"未知命令:/{cmd_name},输入 /help 查看可用命令\n")
47
+ return "continue"
48
+ return "continue" if command.handler(state) else "break"
49
+
50
+
51
+ def apply_result(state: SessionState, result: Any) -> None:
52
+ """
53
+ 跑完一轮 Agent 后,把结果同步到 SessionState 并显示新增的中间过程。
54
+ """
55
+ state.history = result.all_messages()
56
+ usage = result.usage
57
+ state.input_tokens += usage.input_tokens
58
+ state.output_tokens += usage.output_tokens
59
+ state.last_api_calls = list(api_call_log)
60
+ # result.new_messages() 直接拿到这一轮新增的 message,不需要手动算偏移
61
+ print_agent_steps(result.new_messages())
62
+
63
+
64
+ def main() -> None:
65
+ state = SessionState(model_name=MODEL_NAME)
66
+ print_welcome_banner("Zrcoder")
67
+
68
+ while True:
69
+ # 读用户输入
70
+ user_input = read_user_input()
71
+ if user_input is None:
72
+ break
73
+ if not user_input:
74
+ continue
75
+
76
+ # 处理 / 开头的命令
77
+ action = handle_command(user_input, state)
78
+ if action == "break":
79
+ break
80
+ if action == "continue":
81
+ continue
82
+
83
+ # 核心 Agent 循环:清空收集 buffer,跑一轮,把结果应用到 state
84
+ api_call_log.clear()
85
+ result = agent.run_sync(user_input, message_history=state.history)
86
+ apply_result(state, result)
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
@@ -0,0 +1,6 @@
1
+ """
2
+ 终端 UI 层:渲染原语(render)、斜杠命令(commands)。
3
+
4
+ 直接从子模块导入(core.ui.render / core.ui.commands)。这个 __init__ 故意不做任何重导出:
5
+ render 是底层原语,在这里重导出 commands 可能形成循环导入。
6
+ """
@@ -0,0 +1,212 @@
1
+ from collections.abc import Callable, Iterable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ from rich.console import Console, ConsoleOptions, RenderResult
6
+ from rich.markdown import Heading, Markdown
7
+ from rich.markup import escape
8
+ from rich.padding import Padding
9
+ from rich.rule import Rule
10
+
11
+ from .render import console, print_step
12
+
13
+
14
+ class LeftAlignedHeading(Heading):
15
+ """
16
+ rich 默认把 Markdown 标题渲染成居中对齐,宽终端里看着像错位,覆盖成左对齐。
17
+ """
18
+
19
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
20
+ text = self.text
21
+ text.justify = "left"
22
+ yield text
23
+
24
+
25
+ # 全局替换 Markdown 的标题渲染元素
26
+ Markdown.elements["heading_open"] = LeftAlignedHeading
27
+
28
+
29
+ @dataclass
30
+ class SessionState:
31
+ """
32
+ 跨命令共享的会话状态,主循环把它传给每个命令处理函数。
33
+ """
34
+
35
+ history: list[Any] = field(default_factory=list)
36
+ input_tokens: int = 0
37
+ output_tokens: int = 0
38
+ model_name: str = ""
39
+ # 最近一轮 user input 触发的所有 model API 调用记录
40
+ last_api_calls: list[Any] = field(default_factory=list)
41
+
42
+
43
+ @dataclass
44
+ class Command:
45
+ name: str
46
+ description: str
47
+ # handler 返回 False 表示主循环应当退出
48
+ handler: Callable[[SessionState], bool]
49
+
50
+
51
+ def print_divider() -> None:
52
+ """
53
+ 每轮交互之前打印一条分割线,区分输入区域。Rule 会自适应终端宽度。
54
+ """
55
+ console.print(Rule(style="grey50"))
56
+
57
+
58
+ def _truncate(text: Any, limit: int = 120) -> str:
59
+ """
60
+ 截断并 escape,用于 tool 参数 / 返回值 / 用户输入这类可能过长的内容。
61
+ """
62
+ text = str(text).strip()
63
+ text = text if len(text) <= limit else text[:limit] + "..."
64
+ return escape(text)
65
+
66
+
67
+ def _full(text: Any) -> str:
68
+ """
69
+ 完整显示,只做 escape 不截断,用于 thinking 和 assistant text 这种用户关心的内容。
70
+ """
71
+ return escape(str(text).strip())
72
+
73
+
74
+ def _format_part_line(part: Any) -> str | None:
75
+ """
76
+ 把一条消息里的单个 part 格式化为带 Rich markup 的字符串。
77
+ 版式:图标 + role 标签独占一行,内容换行到下一行,不用「|」分隔。
78
+ """
79
+ # 内容行统一缩进 2 格,和图标(占 2 格:图标 + 空格)后的 role 名对齐
80
+ kind = part.part_kind
81
+ if kind == "user-prompt":
82
+ return f"[cyan]❯ user[/]\n {_truncate(part.content)}"
83
+ if kind == "thinking":
84
+ # thinking 整块 dim,弱化视觉权重;不截断,完整保留思考过程
85
+ return f"[dim]✻ thinking[/]\n [dim]{_full(part.content)}[/]"
86
+ if kind == "text":
87
+ content = (part.content or "").strip()
88
+ if not content:
89
+ return None
90
+ # assistant 是用户最关心的最终回答,完整显示
91
+ return f"[green]● assistant[/]\n {_full(content)}"
92
+ if kind == "tool-call":
93
+ # 命令、路径动辄上百字符,参数放宽到 500 字符再截断
94
+ args = _truncate(part.args, 500)
95
+ return f"[yellow]⏺ tool_call[/]\n [yellow dim]{part.tool_name}({args})[/]"
96
+ if kind == "tool-return":
97
+ content = _truncate(part.content)
98
+ return f"[magenta]✔ tool_return[/]\n [magenta dim]{part.tool_name} -> {content}[/]"
99
+ if kind == "retry-prompt":
100
+ # 工具抛 ModelRetry 后,SDK 生成 retry-prompt 把错误反馈给模型
101
+ content = _truncate(part.content)
102
+ return f"[yellow]✘ tool_retry[/]\n [yellow dim]{part.tool_name} -> {content}[/]"
103
+ return None
104
+
105
+
106
+ def print_assistant_markdown(content: str) -> None:
107
+ """
108
+ 模型的回复天然是 Markdown 格式,整块渲染出来,而不是打印原始文本。
109
+ """
110
+ console.print("[green]● assistant[/]")
111
+ # Markdown 是块级渲染对象,没法跟在行内前缀后面,所以另起一行渲染;左缩进 2 格和 role 名对齐
112
+ console.print(Padding(Markdown(content), (0, 0, 0, 2)))
113
+
114
+
115
+ def print_part(part: Any) -> None:
116
+ """
117
+ 渲染单个消息 part:assistant 文本走 Markdown 块渲染,其余 part 是单行文本。
118
+ """
119
+ if part.part_kind == "text":
120
+ content = (part.content or "").strip()
121
+ if content:
122
+ print_assistant_markdown(content)
123
+ # 每个 role block 末尾留一个空行,块与块之间不那么挤
124
+ console.print()
125
+ return
126
+ line = _format_part_line(part)
127
+ if line:
128
+ label, _, body = line.partition("\n")
129
+ # body 形如 " [markup]…":去掉字面前导 2 空格,交给 print_step 用 Padding 缩进
130
+ print_step(label, body[2:])
131
+
132
+
133
+ def print_agent_steps(new_messages: Iterable[Any]) -> None:
134
+ """
135
+ 主循环里调用:显示这一轮 Agent 新增的中间过程(thinking、文本、工具调用、工具返回)。
136
+ """
137
+ for msg in new_messages:
138
+ for part in msg.parts:
139
+ # 主循环里不重复显示用户刚刚输入的内容
140
+ if part.part_kind == "user-prompt":
141
+ continue
142
+ print_part(part)
143
+
144
+
145
+ def cmd_exit(state: SessionState) -> bool:
146
+ console.print("再见 👋")
147
+ return False
148
+
149
+
150
+ def cmd_help(state: SessionState) -> bool:
151
+ console.print("可用命令:")
152
+ for cmd in COMMANDS.values():
153
+ console.print(f" /{cmd.name:<10} {cmd.description}")
154
+ console.print()
155
+ return True
156
+
157
+
158
+ def cmd_new(state: SessionState) -> bool:
159
+ """
160
+ 开启新会话:清空历史、token 计数、API 调用记录。
161
+ """
162
+ state.history.clear()
163
+ state.input_tokens = 0
164
+ state.output_tokens = 0
165
+ state.last_api_calls.clear()
166
+ console.print("已开启新会话\n")
167
+ return True
168
+
169
+
170
+ def cmd_status(state: SessionState) -> bool:
171
+ console.print(f"模型: {state.model_name}")
172
+ console.print(f"历史消息条数: {len(state.history)}")
173
+ console.print(f"累计输入 tokens:{state.input_tokens}")
174
+ console.print(f"累计输出 tokens:{state.output_tokens}\n")
175
+ return True
176
+
177
+
178
+ def cmd_api_detail(state: SessionState) -> bool:
179
+ """
180
+ 显示最近一轮 user input 触发的所有 model API 调用元数据。
181
+ """
182
+ if not state.last_api_calls:
183
+ console.print("(还没有任何模型调用记录,先发一条消息再来看)\n")
184
+ return True
185
+
186
+ console.print(f"最近一轮共发起 {len(state.last_api_calls)} 次 model API 调用\n")
187
+
188
+ for i, call in enumerate(state.last_api_calls, 1):
189
+ console.print(f"[bold]Call #{i}[/]")
190
+ console.print(" Request:")
191
+ console.print(f" model: {call.model}")
192
+ console.print(f" messages: {call.messages_count} 条")
193
+ if call.last_part is not None:
194
+ preview = _format_part_line(call.last_part)
195
+ if preview:
196
+ console.print(f" last_message: {preview}")
197
+ console.print(f" tools: {', '.join(call.tools)}")
198
+ console.print(" Response:")
199
+ console.print(f" finish_reason: {call.finish_reason}")
200
+ console.print(f" parts: {', '.join(call.parts_kinds)}")
201
+ console.print(f" usage: input={call.input_tokens}, output={call.output_tokens}")
202
+ console.print()
203
+ return True
204
+
205
+
206
+ COMMANDS = {
207
+ "new": Command("new", "开启新会话", cmd_new),
208
+ "status": Command("status", "显示当前会话状态", cmd_status),
209
+ "api-detail": Command("api-detail", "显示最近一轮 model API 调用详情", cmd_api_detail),
210
+ "help": Command("help", "显示可用命令", cmd_help),
211
+ "exit": Command("exit", "退出程序", cmd_exit),
212
+ }
@@ -0,0 +1,38 @@
1
+ """
2
+ 共享的终端渲染原语,供 commands / permissions / hooks 复用,只依赖 rich。
3
+ 抽到这里是为了打破 commands 与 permissions/hooks 的循环依赖,调用方就能在文件顶部直接 import。
4
+ """
5
+
6
+ from rich.console import Console
7
+ from rich.padding import Padding
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ # 共享的 Console 实例,跨平台自动处理 ANSI 颜色(Windows 老终端也能正常显示)。
12
+ # highlight=False 关闭 Rich 自带的数字/字符串自动高亮,只让我们手动加的 markup 生效。
13
+ console = Console(highlight=False)
14
+
15
+
16
+ def print_step(label: str, content: str = "") -> None:
17
+ """
18
+ 打印一个中间过程 block:标签独占一行,内容用 Padding 左缩进 2 格
19
+ (这样自动折行的续行也能保持缩进),末尾留一个空行。
20
+ """
21
+ console.print(label)
22
+ if content:
23
+ console.print(Padding(content, (0, 0, 0, 2)))
24
+ console.print()
25
+
26
+
27
+ def print_welcome_banner(title: str) -> None:
28
+ """
29
+ 打印启动欢迎横幅:标题装在带色彩的 rich.Panel 里,附 /help 提示。
30
+ """
31
+ body = Text.assemble(
32
+ ("✻ ", "bright_magenta"),
33
+ (f"欢迎使用 {title}\n\n", "bold"),
34
+ (" /help ", "cyan"),
35
+ ("查看可用命令", "dim"),
36
+ )
37
+ console.print(Panel(body, border_style="bright_blue", padding=(0, 1), width=48))
38
+ console.print()
@@ -0,0 +1,85 @@
1
+ [project]
2
+ name = "zrcoder"
3
+ version = "0.1.0"
4
+ description = "Terminal coding agent chat — OpenAI-compatible APIs, tools, and slash commands"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "prompt-toolkit>=3.0.53",
10
+ "pydantic-ai>=2.40.0",
11
+ "python-dotenv>=1.2.3",
12
+ "rich>=15.0.0",
13
+ ]
14
+ keywords = [
15
+ "cli",
16
+ "agent",
17
+ "coding",
18
+ "terminal",
19
+ "openai",
20
+ ]
21
+ license-files = ["LICENSE"]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Environment :: Console",
25
+ "Intended Audience :: Developers",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Software Development",
31
+ ]
32
+
33
+ [[project.authors]]
34
+ name = "Zr"
35
+ email = "547471919@qq.com"
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/ZRMYDYCG/ClaudeCode"
39
+ Repository = "https://github.com/ZRMYDYCG/ClaudeCode"
40
+ Issues = "https://github.com/ZRMYDYCG/ClaudeCode/issues"
41
+
42
+ [project.scripts]
43
+ zrcoder = "core.cli:main"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "ruff>=0.12.0",
48
+ "pytest>=8.0.0",
49
+ "mypy>=1.17.0",
50
+ ]
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.12.10,<0.13.0"]
54
+ build-backend = "uv_build"
55
+
56
+ [tool.uv.build-backend]
57
+ module-root = ""
58
+ module-name = "core"
59
+
60
+ [tool.ruff]
61
+ target-version = "py312"
62
+ line-length = 100
63
+
64
+ [tool.ruff.format]
65
+ quote-style = "double"
66
+ indent-style = "space"
67
+ line-ending = "lf"
68
+
69
+ [tool.ruff.lint]
70
+ select = [
71
+ "E",
72
+ "F",
73
+ "I",
74
+ "UP",
75
+ "B",
76
+ ]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ pythonpath = ["."]
81
+
82
+ [tool.mypy]
83
+ python_version = "3.12"
84
+ strict = true
85
+ packages = ["core"]
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "zrcoder"
3
+ version = "0.1.0"
4
+ description = "Terminal coding agent chat — OpenAI-compatible APIs, tools, and slash commands"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Zr", email = "547471919@qq.com" }
9
+ ]
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "prompt-toolkit>=3.0.53",
13
+ "pydantic-ai>=2.40.0",
14
+ "python-dotenv>=1.2.3",
15
+ "rich>=15.0.0",
16
+ ]
17
+ keywords = ["cli", "agent", "coding", "terminal", "openai"]
18
+ license-files = ["LICENSE"]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/ZRMYDYCG/ClaudeCode"
32
+ Repository = "https://github.com/ZRMYDYCG/ClaudeCode"
33
+ Issues = "https://github.com/ZRMYDYCG/ClaudeCode/issues"
34
+
35
+ [project.scripts]
36
+ zrcoder = "core.cli:main"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "ruff>=0.12.0",
41
+ "pytest>=8.0.0",
42
+ "mypy>=1.17.0",
43
+ ]
44
+
45
+ [build-system]
46
+ requires = ["uv_build>=0.12.10,<0.13.0"]
47
+ build-backend = "uv_build"
48
+
49
+ [tool.uv.build-backend]
50
+ module-root = ""
51
+ module-name = "core"
52
+
53
+ [tool.ruff]
54
+ target-version = "py312"
55
+ line-length = 100
56
+
57
+ [tool.ruff.format]
58
+ quote-style = "double"
59
+ indent-style = "space"
60
+ line-ending = "lf"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "I", "UP", "B"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ pythonpath = ["."]
68
+
69
+ [tool.mypy]
70
+ python_version = "3.12"
71
+ strict = true
72
+ packages = ["core"]