nexforge-cli 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.
Files changed (74) hide show
  1. nexforge/__init__.py +7 -0
  2. nexforge/__main__.py +6 -0
  3. nexforge/app.py +38 -0
  4. nexforge/assets/icons/agent.svg +13 -0
  5. nexforge/assets/icons/cost.svg +5 -0
  6. nexforge/assets/icons/edit.svg +4 -0
  7. nexforge/assets/icons/err.svg +5 -0
  8. nexforge/assets/icons/file.svg +5 -0
  9. nexforge/assets/icons/model.svg +7 -0
  10. nexforge/assets/icons/ok.svg +4 -0
  11. nexforge/assets/icons/read.svg +8 -0
  12. nexforge/assets/icons/running.svg +5 -0
  13. nexforge/assets/icons/search.svg +5 -0
  14. nexforge/assets/icons/shell.svg +5 -0
  15. nexforge/assets/icons/thinking.svg +5 -0
  16. nexforge/assets/icons/web.svg +6 -0
  17. nexforge/assets/icons/write.svg +4 -0
  18. nexforge/cli.py +159 -0
  19. nexforge/config.py +192 -0
  20. nexforge/core/__init__.py +1 -0
  21. nexforge/core/agent.py +244 -0
  22. nexforge/core/jobs.py +97 -0
  23. nexforge/core/permissions.py +73 -0
  24. nexforge/core/router.py +62 -0
  25. nexforge/core/session.py +55 -0
  26. nexforge/core/types.py +103 -0
  27. nexforge/icons.py +173 -0
  28. nexforge/mcp/__init__.py +168 -0
  29. nexforge/provider/__init__.py +1 -0
  30. nexforge/provider/balance.py +37 -0
  31. nexforge/provider/deepseek.py +204 -0
  32. nexforge/skills/loader.py +102 -0
  33. nexforge/store/__init__.py +1 -0
  34. nexforge/store/plan_store.py +112 -0
  35. nexforge/store/session_store.py +206 -0
  36. nexforge/theme.tcss +128 -0
  37. nexforge/tools/__init__.py +6 -0
  38. nexforge/tools/_paths.py +22 -0
  39. nexforge/tools/background.py +111 -0
  40. nexforge/tools/base.py +56 -0
  41. nexforge/tools/edit_file.py +73 -0
  42. nexforge/tools/fs_ops.py +125 -0
  43. nexforge/tools/git.py +89 -0
  44. nexforge/tools/list_dir.py +49 -0
  45. nexforge/tools/multi_edit.py +84 -0
  46. nexforge/tools/read_file.py +51 -0
  47. nexforge/tools/registry.py +94 -0
  48. nexforge/tools/search_content.py +90 -0
  49. nexforge/tools/search_files.py +48 -0
  50. nexforge/tools/shell.py +60 -0
  51. nexforge/tools/web.py +40 -0
  52. nexforge/tools/write_file.py +40 -0
  53. nexforge/ui/__init__.py +1 -0
  54. nexforge/ui/screens/__init__.py +1 -0
  55. nexforge/ui/screens/chat.py +825 -0
  56. nexforge/ui/widgets/__init__.py +1 -0
  57. nexforge/ui/widgets/apikey_modal.py +65 -0
  58. nexforge/ui/widgets/banner.py +18 -0
  59. nexforge/ui/widgets/confirm_modal.py +58 -0
  60. nexforge/ui/widgets/diff_modal.py +67 -0
  61. nexforge/ui/widgets/messages.py +132 -0
  62. nexforge/ui/widgets/mode_warning.py +57 -0
  63. nexforge/ui/widgets/multiline_modal.py +59 -0
  64. nexforge/ui/widgets/multiline_prompt.py +25 -0
  65. nexforge/ui/widgets/plan_sidebar.py +89 -0
  66. nexforge/ui/widgets/sidebar.py +99 -0
  67. nexforge/ui/widgets/statusbar.py +70 -0
  68. nexforge/web/__init__.py +0 -0
  69. nexforge/web/server.py +177 -0
  70. nexforge/web/static/index.html +123 -0
  71. nexforge_cli-0.1.0.dist-info/METADATA +168 -0
  72. nexforge_cli-0.1.0.dist-info/RECORD +74 -0
  73. nexforge_cli-0.1.0.dist-info/WHEEL +4 -0
  74. nexforge_cli-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,70 @@
1
+ """底部状态栏:权限/计划/余额/模型/思考/上下文/费用(中文)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.text import Text
6
+ from textual.widgets import Static
7
+
8
+ from nexforge.icons import icon
9
+
10
+
11
+ class StatusBar(Static):
12
+ def on_mount(self) -> None:
13
+ self.render_stats(tier="auto", thinking=True, ctx_tokens=0, cost=0.0)
14
+
15
+ def render_stats(
16
+ self,
17
+ *,
18
+ tier: str,
19
+ thinking: bool,
20
+ ctx_tokens: int,
21
+ cost: float,
22
+ plan_mode: bool = False,
23
+ perm: str = "supervised",
24
+ balance: str = "",
25
+ working: bool = False,
26
+ ) -> None:
27
+ t = Text()
28
+
29
+ # 工作中
30
+ if working:
31
+ t.append("⏳ 工作中… ", style="#22D3EE bold")
32
+
33
+ # 权限(中文)
34
+ perm_styles = {"safe": "green", "supervised": "#22D3EE", "auto": "red"}
35
+ perm_labels = {"safe": "🛡 审核", "supervised": "⚡ 半自动", "auto": "🔥 全自动"}
36
+ t.append(perm_labels.get(perm, "? 未知"), style=perm_styles.get(perm, ""))
37
+
38
+ # 计划
39
+ if plan_mode:
40
+ t.append(" 📋 计划中", style="#22D3EE bold")
41
+
42
+ # 余额(人民币)
43
+ if balance:
44
+ t.append(f" 💰 {balance}", style="green")
45
+
46
+ t.append(" │ ", style="dim")
47
+
48
+ # 模型
49
+ model_labels = {"flash": "闪速", "pro": "专业", "auto": "自动"}
50
+ t.append(f"{icon('model')} {model_labels.get(tier, tier)}", style="bold #6D5EF5")
51
+
52
+ # 思考
53
+ t.append(f" 💭 思考:" + ("开" if thinking else "关"), style="dim")
54
+
55
+ t.append(" │ ", style="dim")
56
+
57
+ # 上下文
58
+ if ctx_tokens >= 1000:
59
+ ctx_str = f"{ctx_tokens / 1000:.0f}k"
60
+ else:
61
+ ctx_str = str(ctx_tokens)
62
+ t.append(f"上下文 {ctx_str} token", style="dim")
63
+
64
+ t.append(" │ ", style="dim")
65
+
66
+ # 费用(人民币)
67
+ cny = cost * 7.2 # 汇率估算
68
+ t.append(f"¥{cny:.4f}", style="#22D3EE")
69
+
70
+ self.update(t)
File without changes
nexforge/web/server.py ADDED
@@ -0,0 +1,177 @@
1
+ """NexForgeCLI Web 管理面板 —— 轻量 HTTP 服务器(无外部依赖)。
2
+
3
+ 启动:/web start · 停止:/web stop · 默认端口 8899。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ import threading
11
+ from http.server import HTTPServer, SimpleHTTPRequestHandler
12
+ from pathlib import Path
13
+ from urllib.parse import parse_qs, urlparse
14
+
15
+ _WEB_SERVER: HTTPServer | None = None
16
+ _WEB_PORT = 8899
17
+ _WEB_DIR = Path(__file__).parent / "static"
18
+
19
+ # 运行时引用(CLI 注入)
20
+ _chat_screen = None
21
+
22
+
23
+ def _set_screen(screen) -> None:
24
+ global _chat_screen
25
+ _chat_screen = screen
26
+
27
+
28
+ class _Handler(SimpleHTTPRequestHandler):
29
+ def __init__(self, *args, **kwargs):
30
+ super().__init__(*args, directory=str(_WEB_DIR), **kwargs)
31
+
32
+ def do_GET(self):
33
+ path = urlparse(self.path).path
34
+ if path == "/api/status":
35
+ self._json({"running": True, "agent": _chat_screen._agent_running if _chat_screen else False})
36
+ elif path == "/api/config":
37
+ self._json(_get_config())
38
+ elif path == "/api/sessions":
39
+ self._json(_get_sessions())
40
+ elif path == "/api/balance":
41
+ self._json({"balance": getattr(_chat_screen, "_balance", "") if _chat_screen else ""})
42
+ elif path == "/api/plans":
43
+ self._json(_get_plans())
44
+ elif path == "/api/messages":
45
+ self._json(_get_messages())
46
+ elif path == "/api/last_response":
47
+ self._json(_get_last_response())
48
+ else:
49
+ super().do_GET()
50
+
51
+ def do_POST(self):
52
+ path = urlparse(self.path).path
53
+ length = int(self.headers.get("Content-Length", 0))
54
+ body = self.rfile.read(length) if length else b"{}"
55
+ try:
56
+ data = json.loads(body)
57
+ except json.JSONDecodeError:
58
+ data = {}
59
+
60
+ if path == "/api/chat":
61
+ self._handle_chat(data)
62
+ elif path == "/api/config":
63
+ _set_config(data)
64
+ self._json({"ok": True})
65
+ elif path == "/api/chat/stop":
66
+ if _chat_screen:
67
+ _chat_screen.workers.cancel_group(_chat_screen, "agent")
68
+ self._json({"ok": True})
69
+ else:
70
+ self._json({"error": "unknown"}, status=404)
71
+
72
+ def _handle_chat(self, data: dict):
73
+ text = data.get("text", "").strip()
74
+ if not text or _chat_screen is None:
75
+ self._json({"error": "no message"}, status=400)
76
+ return
77
+ # 注入到 CLI 输入框,触发 MultilinePrompt.PromptSubmitted
78
+ try:
79
+ from nexforge.ui.widgets.multiline_prompt import MultilinePrompt
80
+ prompt = _chat_screen.query_one("#prompt", MultilinePrompt)
81
+ prompt.text = text
82
+ prompt.post_message(MultilinePrompt.PromptSubmitted(text))
83
+ self._json({"ok": True, "text": text[:100]})
84
+ except Exception as exc:
85
+ self._json({"error": str(exc)}, status=500)
86
+
87
+ def _json(self, data: dict, status: int = 200):
88
+ self.send_response(status)
89
+ self.send_header("Content-Type", "application/json; charset=utf-8")
90
+ self.end_headers()
91
+ self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
92
+
93
+ def log_message(self, format, *args):
94
+ pass # 静默,不污染终端
95
+
96
+
97
+ def _get_config() -> dict:
98
+ if _chat_screen is None:
99
+ return {}
100
+ cfg = _chat_screen.config
101
+ return {
102
+ "tier": cfg.models.default_tier,
103
+ "thinking": cfg.models.thinking,
104
+ "mode": cfg.ui.mode,
105
+ "plan_mode": cfg.ui.plan_mode,
106
+ "perm": cfg.permissions.profile,
107
+ }
108
+
109
+
110
+ def _set_config(data: dict) -> None:
111
+ if _chat_screen is None:
112
+ return
113
+ cfg = _chat_screen.config
114
+ if "tier" in data:
115
+ cfg.models.default_tier = data["tier"]
116
+ if "thinking" in data:
117
+ cfg.models.thinking = bool(data["thinking"])
118
+ if "perm" in data:
119
+ cfg.permissions.profile = data["perm"]
120
+ if "plan_mode" in data:
121
+ cfg.ui.plan_mode = bool(data["plan_mode"])
122
+
123
+
124
+ def _get_sessions() -> list:
125
+ from nexforge.store.session_store import list_sessions
126
+ return [{"id": s.id, "name": s.name, "count": s.message_count, "created": s.created} for s in list_sessions()]
127
+
128
+
129
+ def _get_last_response() -> dict:
130
+ if _chat_screen is None:
131
+ return {}
132
+ for m in reversed(_chat_screen.session.messages):
133
+ if m.get("role") == "assistant":
134
+ content = str(m.get("content", ""))
135
+ if len(content) > 500:
136
+ content = content[:500] + "…"
137
+ return {"content": content}
138
+ return {}
139
+
140
+ def _get_messages() -> list:
141
+ if _chat_screen is None:
142
+ return []
143
+ msgs = []
144
+ for m in _chat_screen.session.messages[-20:]:
145
+ role = m.get("role", "")
146
+ if role in ("system", "tool"):
147
+ continue
148
+ content = str(m.get("content", ""))
149
+ if len(content) > 300:
150
+ content = content[:300] + "…"
151
+ msgs.append({"role": role, "content": content})
152
+ return msgs
153
+
154
+ def _get_plans() -> list:
155
+ from nexforge.store.plan_store import list_plans
156
+ return [{"id": p.id, "title": p.title, "done": p.done, "total": p.total} for p in list_plans()]
157
+
158
+
159
+ def start_web(screen, port: int = 8899) -> str:
160
+ global _WEB_SERVER, _WEB_PORT
161
+ if _WEB_SERVER:
162
+ return f"Web 已在 http://localhost:{_WEB_PORT} 运行"
163
+ _set_screen(screen)
164
+ _WEB_PORT = port
165
+ _WEB_SERVER = HTTPServer(("0.0.0.0", port), _Handler)
166
+ t = threading.Thread(target=_WEB_SERVER.serve_forever, daemon=True)
167
+ t.start()
168
+ return f"Web 面板已启动: http://localhost:{port}"
169
+
170
+
171
+ def stop_web() -> str:
172
+ global _WEB_SERVER
173
+ if _WEB_SERVER is None:
174
+ return "Web 未运行"
175
+ _WEB_SERVER.shutdown()
176
+ _WEB_SERVER = None
177
+ return "Web 面板已停止"
@@ -0,0 +1,123 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>NexForgeCLI</title>
7
+ <style>
8
+ :root{--bg:#101014;--surface:#1a1a24;--accent:#6D5EF5;--cyan:#22D3EE;--text:#e0e0e0;--dim:#888;--green:#3CE88A}
9
+ *{box-sizing:border-box;margin:0;padding:0}
10
+ body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex}
11
+ #sidebar{width:260px;background:var(--surface);padding:16px;display:flex;flex-direction:column;gap:12px;overflow-y:auto;border-right:1px solid #222}
12
+ #sidebar h2{color:var(--accent);font-size:16px;margin-bottom:8px}
13
+ #sessions,#plans{list-style:none}
14
+ #sessions li,#plans li{padding:8px 12px;cursor:pointer;border-radius:8px;margin-bottom:4px;font-size:13px;transition:background .15s}
15
+ #sessions li:hover,#plans li:hover{background:var(--accent)33}
16
+ .srow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;font-size:13px}
17
+ .srow select,.srow button{padding:4px 8px;border-radius:6px;border:1px solid #333;background:var(--bg);color:var(--text);font-size:12px;cursor:pointer}
18
+ #main{flex:1;display:flex;flex-direction:column}
19
+ #chat{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:12px}
20
+ .msg{padding:12px 16px;border-radius:10px;max-width:80%;line-height:1.6;font-size:14px;white-space:pre-wrap}
21
+ .msg.user{align-self:flex-end;background:var(--accent);color:#fff}
22
+ .msg.assistant{align-self:flex-start;background:var(--surface);border:1px solid #333}
23
+ #input-row{display:flex;padding:12px 20px;gap:8px;border-top:1px solid #222;background:var(--bg)}
24
+ #input-row textarea{flex:1;padding:10px 14px;border-radius:10px;border:1px solid #444;background:#1a1a24;color:var(--text);font-size:14px;resize:none;min-height:44px;max-height:120px;font-family:inherit}
25
+ #input-row textarea:focus{outline:none;border-color:var(--accent)}
26
+ #input-row button{padding:10px 20px;border-radius:10px;border:none;background:var(--accent);color:#fff;font-size:14px;cursor:pointer;font-weight:600}
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <aside id="sidebar">
31
+ <h2>⚡ NexForgeCLI</h2>
32
+ <div class="srow"><span>模型</span><select id="cfg-tier"><option value="auto">自动</option><option value="flash">闪速</option><option value="pro">专业</option></select></div>
33
+ <div class="srow"><span>思考</span><select id="cfg-thinking"><option value="1">开</option><option value="0">关</option></select></div>
34
+ <div class="srow"><span>权限</span><select id="cfg-perm"><option value="supervised">半自动</option><option value="safe">审核</option><option value="auto">全自动</option></select></div>
35
+ <div class="srow"><span>计划模式</span><select id="cfg-plan"><option value="0">关</option><option value="1">开</option></select></div>
36
+ <div class="srow"><span>余额</span><span id="balance">-</span></div>
37
+ <h2>📋 会话</h2><ul id="sessions"></ul>
38
+ <h2>📝 计划</h2><ul id="plans"></ul>
39
+ </aside>
40
+ <main id="main">
41
+ <div id="chat"></div>
42
+ <div id="input-row">
43
+ <textarea id="input" placeholder="输入消息… (Enter 发送)" rows="1"></textarea>
44
+ <button id="btn-send">发送</button>
45
+ </div>
46
+ </main>
47
+ <script>
48
+ document.addEventListener('DOMContentLoaded',()=>{
49
+ const chat=document.getElementById('chat');
50
+ const input=document.getElementById('input');
51
+ const btnSend=document.getElementById('btn-send');
52
+ const balance=document.getElementById('balance');
53
+ const sessions=document.getElementById('sessions');
54
+ const plans=document.getElementById('plans');
55
+
56
+ async function api(url,opts={}){
57
+ const r=await fetch(url,{headers:{'Content-Type':'application/json'},...opts});
58
+ return r.json();
59
+ }
60
+
61
+ async function load(){
62
+ try{
63
+ const cfg=await api('/api/config');
64
+ document.getElementById('cfg-tier').value=cfg.tier||'auto';
65
+ document.getElementById('cfg-thinking').value=cfg.thinking?'1':'0';
66
+ document.getElementById('cfg-perm').value=cfg.perm||'supervised';
67
+ document.getElementById('cfg-plan').value=cfg.plan_mode?'1':'0';
68
+ const bal=await api('/api/balance');
69
+ balance.textContent=bal.balance||'-';
70
+ const ss=await api('/api/sessions');
71
+ sessions.innerHTML=ss.map(s=>`<li>${s.name} (${s.count})</li>`).join('');
72
+ const ps=await api('/api/plans');
73
+ plans.innerHTML=ps.map(p=>`<li>${p.title} [${p.done}/${p.total}]</li>`).join('');
74
+ }catch(e){console.error(e);}
75
+ }
76
+ load();
77
+
78
+ ['tier','thinking','perm','plan'].forEach(k=>{
79
+ const el=document.getElementById('cfg-'+k);
80
+ if(!el)return;
81
+ el.onchange=()=>api('/api/config',{method:'POST',body:JSON.stringify({[k]:el.value})});
82
+ });
83
+
84
+ input.onkeydown=e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMsg();}};
85
+ btnSend.onclick=sendMsg;
86
+
87
+ async function sendMsg(){
88
+ const text=input.value.trim();if(!text)return;
89
+ input.value='';
90
+ addMsg(text,'user');
91
+ try{
92
+ const r=await api('/api/chat',{method:'POST',body:JSON.stringify({text})});
93
+ if(!r.ok){addMsg('❌ '+r.error,'assistant');return;}
94
+ // 轮询最新消息
95
+ let lastContent='';
96
+ const poll=setInterval(async()=>{
97
+ const resp=await api('/api/last_response');
98
+ if(resp.content&&resp.content!==lastContent){
99
+ lastContent=resp.content;
100
+ // 替换或追加最后一条 assistant 消息
101
+ const last=chat.lastElementChild;
102
+ if(last&&last.classList.contains('assistant')){
103
+ last.textContent=resp.content;
104
+ }else{
105
+ addMsg(resp.content,'assistant');
106
+ }
107
+ }
108
+ const st=await api('/api/status');
109
+ if(!st.agent&&lastContent){clearInterval(poll);}
110
+ },1000);
111
+ setTimeout(()=>clearInterval(poll),120000); // 2min 超时
112
+ }catch(e){addMsg('❌ 连接错误:'+e.message,'assistant');}
113
+ }
114
+
115
+ function addMsg(text,role){
116
+ const div=document.createElement('div');
117
+ div.className='msg '+role;div.textContent=text;
118
+ chat.appendChild(div);chat.scrollTop=chat.scrollHeight;
119
+ }
120
+ });
121
+ </script>
122
+ </body>
123
+ </html>
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexforge-cli
3
+ Version: 0.1.0
4
+ Summary: NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)
5
+ Project-URL: Homepage, https://gitee.com/Lighthorn/nexforgecli
6
+ Author: NexForge
7
+ License: MIT
8
+ Keywords: agent,cli,deepseek,textual,tui
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Classifier: Topic :: Terminals
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: openai>=1.40
24
+ Requires-Dist: pydantic>=2.6
25
+ Requires-Dist: rich>=13.7
26
+ Requires-Dist: textual>=0.60
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: twine>=5.0; extra == 'dev'
32
+ Provides-Extra: images
33
+ Requires-Dist: cairosvg>=2.7; extra == 'images'
34
+ Requires-Dist: pillow>=10.0; extra == 'images'
35
+ Requires-Dist: term-image>=0.7; extra == 'images'
36
+ Provides-Extra: mcp
37
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
38
+ Description-Content-Type: text/markdown
39
+
40
+ <div align="center">
41
+
42
+ # ◆ NexForgeCLI
43
+
44
+ **面向 DeepSeek V4 的终端智能体 · TUI + CLI 双模式**
45
+
46
+ </div>
47
+
48
+ NexForgeCLI 是一个运行在终端里的智能体助手,专为 **DeepSeek V4**(`deepseek-v4-flash` / `deepseek-v4-pro`)打造。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
49
+
50
+ > 状态:**M1 走通骨架**(真实 API 接入 + Textual TUI + 核心工具循环)。M2+ 见下方路线图。
51
+
52
+ ---
53
+
54
+ ## ✨ 特性
55
+
56
+ - **DeepSeek V4 原生接入** —— flash / pro 双模型,思考模式(`reasoning_content` 独立面板)、上下文缓存计费、真实用量与费用统计。
57
+ - **三档模型选择** —— `flash`(快)、`pro`(强)、`auto`(客户端智能路由:默认 flash,命中信号自动升 pro + 思考)。
58
+ - **智能体循环** —— 函数调用(function calling),自动读写文件、执行命令,直到任务完成。
59
+ - **美化 TUI** —— Textual 驱动:品牌横幅、流式 Markdown、可折叠思考面板、工具调用卡片、状态栏(档位/上下文/费用)。
60
+ - **SVG 图标系统** —— 以 SVG 为唯一源,按终端能力分级渲染(Kitty/iTerm2/Sixel 真图 → Nerd/Unicode 字形 → ASCII 兜底)。
61
+ - **双运行模式** —— 全屏交互 or 管道式 one-shot,同一套核心。
62
+
63
+ ---
64
+
65
+ ## 📦 安装
66
+
67
+ ```bash
68
+ # 一键安装(推荐)
69
+ pip install git+https://gitee.com/Lighthorn/nexforgecli.git
70
+
71
+ # 或本地开发安装
72
+ git clone https://gitee.com/Lighthorn/nexforgecli.git
73
+ cd NexForgeCLI && pip install -e .
74
+ ```
75
+
76
+ 设置 API Key:
77
+
78
+ ```bash
79
+ export DEEPSEEK_API_KEY="sk-..." # Windows: setx DEEPSEEK_API_KEY sk-...
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🚀 使用
85
+
86
+ ```bash
87
+ # 全屏 TUI(默认)
88
+ nexforge
89
+
90
+ # one-shot:回答后退出
91
+ nexforge -p "这个项目是做什么的?"
92
+
93
+ # 管道输入
94
+ git diff | nexforge -p "帮我写一条 commit message"
95
+
96
+ # 指定档位 / 关思考
97
+ nexforge --model pro
98
+ nexforge --no-think -p "列出当前目录"
99
+
100
+ # 查看当前配置
101
+ nexforge --config
102
+ ```
103
+
104
+ ### TUI 内斜杠命令
105
+
106
+ | 命令 | 作用 |
107
+ |---|---|
108
+ | `/help` | 帮助 |
109
+ | `/model flash\|pro\|auto` | 切换模型档位 |
110
+ | `/think on\|off` | 开关思考模式 |
111
+ | `/cost` | 累计费用 |
112
+ | `/clear` | 清空会话 |
113
+ | `/quit` | 退出 |
114
+
115
+ 快捷键:`Ctrl+C` 退出 · `Ctrl+L` 清屏
116
+
117
+ ---
118
+
119
+ ## ⚙️ 配置
120
+
121
+ 默认值内置(真实 V4 参数),可在 `~/.nexforge/config.toml` 覆盖。参考 [`config.example.toml`](config.example.toml)。
122
+
123
+ | 档位 | 模型 | 说明 |
124
+ |---|---|---|
125
+ | `flash` | deepseek-v4-flash | 低延迟低成本 |
126
+ | `pro` | deepseek-v4-pro | 复杂推理/大重构 |
127
+ | `auto` | 客户端路由 | 默认 flash,命中关键词/大上下文/多次失败 → 升 pro |
128
+
129
+ ---
130
+
131
+ ## 🧱 架构
132
+
133
+ ```
134
+ cli.py ─┬─ app.py (Textual TUI) ── ui/ (screens, widgets)
135
+ └─ core/agent.py (AgentLoop 事件流)
136
+ ├─ provider/deepseek.py (V4 流式/思考/工具/缓存)
137
+ ├─ core/router.py (auto 路由)
138
+ ├─ core/session.py (消息 + reasoning 回传规则)
139
+ └─ tools/ (read/list/edit/shell + Registry)
140
+ icons.py + assets/icons/*.svg (图标能力分级)
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 🗺️ 路线图
146
+
147
+ - [x] **M1 骨架** —— 真 V4 接入、TUI、核心工具循环、双模式
148
+ - [ ] **M2 工具齐全 + 通用 skill** —— 完整工具集、审批门 diff、MCP 客户端、SKILL.md 技能加载
149
+ - [ ] **M3 美化** —— 图标真图渲染、diff 视图、主题切换
150
+ - [ ] **M4 实用化** —— 会话持久化/恢复、上下文压缩、费用总账、`nexforge login`
151
+ - [ ] **M5 发布** —— PyInstaller 单文件 + pipx + brew/scoop、PyPI
152
+
153
+ ---
154
+
155
+ ## 🧪 开发
156
+
157
+ ```bash
158
+ pip install -e ".[dev]"
159
+ python -m pytest -q # 单元测试(不触网)
160
+ python scripts/smoke_provider.py flash think # 真实 API 冒烟
161
+ python scripts/smoke_tui.py # TUI 端到端冒烟
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 📄 License
167
+
168
+ MIT
@@ -0,0 +1,74 @@
1
+ nexforge/__init__.py,sha256=OFPrqhtkY1CJUj0VQb0llaCKiU8GiQP3wd6i6enY5o0,168
2
+ nexforge/__main__.py,sha256=X24uh1BkF0W1N3KHnstBKZhLQ4Dup0TLRbUAmXSQLSs,125
3
+ nexforge/app.py,sha256=m4lR2aNgiFCgehlCGWyXa35hpkZNXGb1ddRg9lR5aII,999
4
+ nexforge/cli.py,sha256=SU4PS7CjMUycpYqXW0_ilLJ775Jbj4Thtk6AszTLtnI,4701
5
+ nexforge/config.py,sha256=kz4NBJhj3ug47cicy7o8NKKCqRsQyl1g6eFp9RhzRkU,6897
6
+ nexforge/icons.py,sha256=5EE9AYKsDxfn7PMTqta54IBdR6ib8oa3xZ6cvwpR_eE,5131
7
+ nexforge/theme.tcss,sha256=iGRJ99WKOxo9mX7pbOXMQp0WVygFxlPpsrQJjVazN24,2439
8
+ nexforge/core/__init__.py,sha256=jMpZZWShVrdl8ncKs5Yyxwwltce6chlZ591JXG59pdY,73
9
+ nexforge/core/agent.py,sha256=UEAzem1bT4sRSy-ZzlM-_a7UOTBr4DdFv8W_zPokOvA,10013
10
+ nexforge/core/jobs.py,sha256=_1eVvt_uDnQxgAvFVOunVisBtEvtFNKvwUwn9jzpmY4,2509
11
+ nexforge/core/permissions.py,sha256=rQN4wMOuhZBveD_E0zJoEUwzJ85R75fJkpHJta6A570,1869
12
+ nexforge/core/router.py,sha256=2507aj8FbZZ97Xpx9AmvJPVfqrdGE-PqK7hMl5E74m4,2070
13
+ nexforge/core/session.py,sha256=bF_3mFpsZIsoLw1OdPB-MRCsEmR2G14CFGGOUO0nrkE,2277
14
+ nexforge/core/types.py,sha256=xRzC-yOvJ8lToXtUxnCJ3mL1rPvIuSmKyLMdO_zTUc8,3373
15
+ nexforge/mcp/__init__.py,sha256=Wvxkdc9_MA9gAozLbyJ1mOrFqDB_TxIoFvG_1Pfcok0,6160
16
+ nexforge/provider/__init__.py,sha256=QmcSGuoBsV5cPgzt62gvT663DWM2D9MfgpGkEhupg_E,37
17
+ nexforge/provider/balance.py,sha256=XF8rWNPd7HMeZ3VFEmCNzm-V8n_upoKMDct6qTLcCxQ,1267
18
+ nexforge/provider/deepseek.py,sha256=Zq1aFsHRAbwQfHW5mh6q_M0b-y_jQqTEBijG6SMibCk,7689
19
+ nexforge/skills/loader.py,sha256=-QXgzVO0121plMrPrf9jBCuLkOufWL_kCYozWVsC5tU,2798
20
+ nexforge/store/__init__.py,sha256=H5ECdoS4nBwIIuxxrQ9qEMwvPDjFgc4z4025pL9Gy8k,28
21
+ nexforge/store/plan_store.py,sha256=jcnKyzWPluPLwv5guiVCVXRttDMZO09sf_6J1qyY3bc,3266
22
+ nexforge/store/session_store.py,sha256=CIYVCxyDLVrvemgp3JiANaO9l7zLSTWL-TK-SBcuy6c,6330
23
+ nexforge/tools/__init__.py,sha256=IGFFNWqfXIkMX-7L2HKrnmDIYufk4CZZ0M-kLY5KAAs,250
24
+ nexforge/tools/_paths.py,sha256=C1ak62889QB9LApMtP0bKCjBE50B6Gh8yUF_rTBXzhc,556
25
+ nexforge/tools/background.py,sha256=vBSruP4oLm1PctSa4_0AYmYyxQQZWA4isEXWHuKqbFk,4150
26
+ nexforge/tools/base.py,sha256=XqTWlvtyuxbUwHpPzP6xJMis9LYrL_QZEQKeeqLzIAY,1694
27
+ nexforge/tools/edit_file.py,sha256=PbKKhXd20EA6nLypYsHG6LUz8fr9NtzDdkNjQ9dwPGo,2812
28
+ nexforge/tools/fs_ops.py,sha256=DfnOFGIPe8-i50K0tuWKkDerucF-mZbjms37zNneWpo,4269
29
+ nexforge/tools/git.py,sha256=Y_NvDWswIgMm5qU6H64NjOLRlctWd-vjCRDNlXsGoEk,3163
30
+ nexforge/tools/list_dir.py,sha256=_GbZ49Y1W2kAXr1C_0bx3pL0_vKTYHmeeYOWxBZ4Hkg,1778
31
+ nexforge/tools/multi_edit.py,sha256=SQS_OheGR7zVVx5KKOfJFmZpiFrWWDLftRT-VmAF86g,3140
32
+ nexforge/tools/read_file.py,sha256=ZQA4h5uTpMBccQXlL334_0SGKv7mL7qOuwpNlFbsvpg,1966
33
+ nexforge/tools/registry.py,sha256=10NmWN9Sr6_xMRXXzAOXsgDlpwdkOXGVSJ-jGb-CkBk,3111
34
+ nexforge/tools/search_content.py,sha256=yHxPju4Y0rXmwyzhUJKeAc5MujQCIBIZxddN5xHV69Y,3375
35
+ nexforge/tools/search_files.py,sha256=T_RJh9pYAiJB43aMMxrtHWkx8D-MBxqIwk3YbuAWUpk,1806
36
+ nexforge/tools/shell.py,sha256=hV8UUKmZ_VVKeJ1TqaqL3uyECl54o4iOyQwFwrv7AEQ,2172
37
+ nexforge/tools/web.py,sha256=GWMviEvymEf9Gx2DKq2Ie5lYCqRccTVYny0j7bA9e0M,1478
38
+ nexforge/tools/write_file.py,sha256=zoyJ-a5K3WkC990GtndIwjwWRj6S6qyo5WYW5UGDafU,1369
39
+ nexforge/ui/__init__.py,sha256=5ZKdwOa__1JajHuOOY9IpgU8jWNUBuqkYOjm0tdSeX0,43
40
+ nexforge/ui/screens/__init__.py,sha256=XuM5jei80V9Uv6Hm6DdO94J8uSW0AADWW_BF2lyQtGs,24
41
+ nexforge/ui/screens/chat.py,sha256=zmonlYfhdhNPRj2R5OVKAMiWqQoQKTMjjDOSNFiT7po,34951
42
+ nexforge/ui/widgets/__init__.py,sha256=hiur7wW89PHJoMuS7-K0HMv66n94KRsQwMyMD3s2okc,33
43
+ nexforge/ui/widgets/apikey_modal.py,sha256=dfth52ktBHUBZOfCbFknIsW4oWumTFKwyPsdCvuGjKI,2149
44
+ nexforge/ui/widgets/banner.py,sha256=7T5xBWJmL5xwk_zQR061U5M6Gte6f7q0aLSXG05IVUw,452
45
+ nexforge/ui/widgets/confirm_modal.py,sha256=QU-XlyQ5w3JLtwexcggtMabOchNBzWCMa6-qvvWtxEY,1718
46
+ nexforge/ui/widgets/diff_modal.py,sha256=d2MdNCgk4A6s3fttf4kO2aDvEq1EoU3ojpeCxDFx2Ec,2000
47
+ nexforge/ui/widgets/messages.py,sha256=Lztg5AyIq-dXwzIwP7ngyVJE_ooChisy7RnOD57-lpY,4785
48
+ nexforge/ui/widgets/mode_warning.py,sha256=SHfEU209lsKV2A-JSbSZGH3QUBZnG9IZJj5U-f_RTIQ,1752
49
+ nexforge/ui/widgets/multiline_modal.py,sha256=LSwQR25bUWxsefPqea0omuqVzwAqhmIH897lIVrwj0E,1745
50
+ nexforge/ui/widgets/multiline_prompt.py,sha256=WleE47sMDuIN9JQP2-bavM_pmkURy4Rq7o-qIAcBR44,735
51
+ nexforge/ui/widgets/plan_sidebar.py,sha256=qRgYI6B1jD8O2ttgPJAsIUstDh8CYVqa4ucNmBiq4m4,2613
52
+ nexforge/ui/widgets/sidebar.py,sha256=Q9LEmNEZcgFA5KjJvqr4QE2oL2emXlrhPB9lBxQh5M4,3015
53
+ nexforge/ui/widgets/statusbar.py,sha256=bMErwrNbiRIoxx8fhPFWo2MgkZMv6UCOyNm1-WrkFrU,2034
54
+ nexforge/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
+ nexforge/web/server.py,sha256=75BwSxkvbUJUNvMAJx7X6UVaFxQYalEh2STnwV0HUk0,5785
56
+ nexforge/web/static/index.html,sha256=ew-Gz57SvUrs2LwzHV8j4OmGeiJw4pCjIUxRIXjhsU8,6016
57
+ nexforge/assets/icons/agent.svg,sha256=0TcDDGD5Ps56AnSKpMIbEAtYd9LiAq2Yn1PrxeLYnu4,621
58
+ nexforge/assets/icons/cost.svg,sha256=0OMb0AaMV9xHBWqORQB9ammQxdrzSLS4AIbDOiWmg5U,256
59
+ nexforge/assets/icons/edit.svg,sha256=Zsz3ougxVnfei8t44woSPmOKqWyFIBDi5afxXjWWvxI,269
60
+ nexforge/assets/icons/err.svg,sha256=eSHt6YUZiF_a9YC5-IqYAZP8R8v_CDZ0X9k5C_q_EPg,238
61
+ nexforge/assets/icons/file.svg,sha256=VWB_fh2jOS0GYJB2qim7wP3Z6pK3QtnHUyeSANTlHS8,307
62
+ nexforge/assets/icons/model.svg,sha256=s495BoNTFiT4NzZhW4VUnmD1ffLt3oXnCZAE8IeQC8M,477
63
+ nexforge/assets/icons/ok.svg,sha256=TIz_YUnMa81XdBbfJxG9rqpiBECvEdf6qwpJ8Lum-2M,222
64
+ nexforge/assets/icons/read.svg,sha256=V45GsOpel2oaUnAUIw8cXNBjKkA9k-REEU9hJ-B2NMw,422
65
+ nexforge/assets/icons/running.svg,sha256=sEgf5irWwODusDzpIUixbsOHSo7RQJerOKrLd07qwR4,290
66
+ nexforge/assets/icons/search.svg,sha256=VT3TQbjj_XPu-jl6zjxiSBH6ryXDe4ok_sPDvd8VLVY,280
67
+ nexforge/assets/icons/shell.svg,sha256=RGI3O5CZNi1rhoBJ5fykzRGKBf3DC3_J51xyI8dP61E,274
68
+ nexforge/assets/icons/thinking.svg,sha256=YU7nKjwRqIH7tPwer3o0XqxjbBjuzTBLBKKONm6xiMU,321
69
+ nexforge/assets/icons/web.svg,sha256=tW5QupGKXUM9Nw2utOio7yBySolVcfHvkq2ePl5_MAQ,365
70
+ nexforge/assets/icons/write.svg,sha256=YbxaVlJrq4YpB6YUe3vCf4WSI9izu0ik_5BkPR4bv3c,257
71
+ nexforge_cli-0.1.0.dist-info/METADATA,sha256=4PZ5rj5dtEh-suxI2eUw3hBJ1jbt-ixiRMr4EmhN1fc,5377
72
+ nexforge_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
73
+ nexforge_cli-0.1.0.dist-info/entry_points.txt,sha256=ZfReQ0Cbn8__MmHehn9SnItlOG49Kby1XRq7OcVWvhw,70
74
+ nexforge_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ nexforge = nexforge.cli:main
3
+ nf = nexforge.cli:main