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.
- nexforge/__init__.py +7 -0
- nexforge/__main__.py +6 -0
- nexforge/app.py +38 -0
- nexforge/assets/icons/agent.svg +13 -0
- nexforge/assets/icons/cost.svg +5 -0
- nexforge/assets/icons/edit.svg +4 -0
- nexforge/assets/icons/err.svg +5 -0
- nexforge/assets/icons/file.svg +5 -0
- nexforge/assets/icons/model.svg +7 -0
- nexforge/assets/icons/ok.svg +4 -0
- nexforge/assets/icons/read.svg +8 -0
- nexforge/assets/icons/running.svg +5 -0
- nexforge/assets/icons/search.svg +5 -0
- nexforge/assets/icons/shell.svg +5 -0
- nexforge/assets/icons/thinking.svg +5 -0
- nexforge/assets/icons/web.svg +6 -0
- nexforge/assets/icons/write.svg +4 -0
- nexforge/cli.py +159 -0
- nexforge/config.py +192 -0
- nexforge/core/__init__.py +1 -0
- nexforge/core/agent.py +244 -0
- nexforge/core/jobs.py +97 -0
- nexforge/core/permissions.py +73 -0
- nexforge/core/router.py +62 -0
- nexforge/core/session.py +55 -0
- nexforge/core/types.py +103 -0
- nexforge/icons.py +173 -0
- nexforge/mcp/__init__.py +168 -0
- nexforge/provider/__init__.py +1 -0
- nexforge/provider/balance.py +37 -0
- nexforge/provider/deepseek.py +204 -0
- nexforge/skills/loader.py +102 -0
- nexforge/store/__init__.py +1 -0
- nexforge/store/plan_store.py +112 -0
- nexforge/store/session_store.py +206 -0
- nexforge/theme.tcss +128 -0
- nexforge/tools/__init__.py +6 -0
- nexforge/tools/_paths.py +22 -0
- nexforge/tools/background.py +111 -0
- nexforge/tools/base.py +56 -0
- nexforge/tools/edit_file.py +73 -0
- nexforge/tools/fs_ops.py +125 -0
- nexforge/tools/git.py +89 -0
- nexforge/tools/list_dir.py +49 -0
- nexforge/tools/multi_edit.py +84 -0
- nexforge/tools/read_file.py +51 -0
- nexforge/tools/registry.py +94 -0
- nexforge/tools/search_content.py +90 -0
- nexforge/tools/search_files.py +48 -0
- nexforge/tools/shell.py +60 -0
- nexforge/tools/web.py +40 -0
- nexforge/tools/write_file.py +40 -0
- nexforge/ui/__init__.py +1 -0
- nexforge/ui/screens/__init__.py +1 -0
- nexforge/ui/screens/chat.py +825 -0
- nexforge/ui/widgets/__init__.py +1 -0
- nexforge/ui/widgets/apikey_modal.py +65 -0
- nexforge/ui/widgets/banner.py +18 -0
- nexforge/ui/widgets/confirm_modal.py +58 -0
- nexforge/ui/widgets/diff_modal.py +67 -0
- nexforge/ui/widgets/messages.py +132 -0
- nexforge/ui/widgets/mode_warning.py +57 -0
- nexforge/ui/widgets/multiline_modal.py +59 -0
- nexforge/ui/widgets/multiline_prompt.py +25 -0
- nexforge/ui/widgets/plan_sidebar.py +89 -0
- nexforge/ui/widgets/sidebar.py +99 -0
- nexforge/ui/widgets/statusbar.py +70 -0
- nexforge/web/__init__.py +0 -0
- nexforge/web/server.py +177 -0
- nexforge/web/static/index.html +123 -0
- nexforge_cli-0.1.0.dist-info/METADATA +168 -0
- nexforge_cli-0.1.0.dist-info/RECORD +74 -0
- nexforge_cli-0.1.0.dist-info/WHEEL +4 -0
- 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)
|
nexforge/web/__init__.py
ADDED
|
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,,
|