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,48 @@
1
+ """search_files 工具:按文件名匹配(子串或 glob)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nexforge.tools._paths import resolve, truncate
6
+ from nexforge.tools.base import BaseTool, ToolResult
7
+
8
+ _SKIP_DIRS = {".git", "__pycache__", ".venv", "node_modules", ".mypy_cache",
9
+ ".pytest_cache", "dist", "build", ".nexforge"}
10
+
11
+
12
+ class SearchFilesTool(BaseTool):
13
+ name = "search_files"
14
+ description = "按文件名查找(不区分大小写的子串匹配)。返回匹配的相对路径列表。"
15
+ parameters = {
16
+ "type": "object",
17
+ "properties": {
18
+ "pattern": {"type": "string", "description": "文件名子串"},
19
+ "path": {"type": "string", "description": "起始目录,默认当前目录"},
20
+ },
21
+ "required": ["pattern"],
22
+ }
23
+
24
+ def preview(self, args: dict) -> str:
25
+ return f"search_files({args.get('pattern', '')!r})"
26
+
27
+ def run(self, pattern: str, path: str = ".") -> ToolResult:
28
+ base = resolve(path)
29
+ if not base.is_dir():
30
+ return ToolResult(ok=False, error=f"目录不存在:{path}")
31
+ needle = pattern.lower()
32
+ matches: list[str] = []
33
+ for fp in base.rglob("*"):
34
+ if not fp.is_file():
35
+ continue
36
+ if any(part in _SKIP_DIRS for part in fp.parts):
37
+ continue
38
+ if needle in fp.name.lower():
39
+ matches.append(fp.relative_to(base).as_posix())
40
+ if len(matches) >= 500:
41
+ break
42
+ if not matches:
43
+ return ToolResult(ok=True, output="(无匹配)", summary=f"{pattern} — 0")
44
+ return ToolResult(
45
+ ok=True,
46
+ output=truncate("\n".join(sorted(matches))),
47
+ summary=f"{pattern} — {len(matches)} 个",
48
+ )
@@ -0,0 +1,60 @@
1
+ """shell 工具:执行终端命令(带超时与输出截断)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ from nexforge.tools._paths import truncate
8
+ from nexforge.tools.base import BaseTool, ToolResult
9
+
10
+ # 简单危险命令拦截(M2 会做更完善的沙箱)
11
+ _DANGER = ("rm -rf /", "rm -rf /*", ":(){", "mkfs", "dd if=", "> /dev/sda")
12
+
13
+
14
+ class ShellTool(BaseTool):
15
+ name = "run_shell"
16
+ description = "在当前工作目录执行一条 shell 命令,返回 stdout+stderr。默认超时 30s。"
17
+ parameters = {
18
+ "type": "object",
19
+ "properties": {
20
+ "command": {"type": "string", "description": "要执行的命令"},
21
+ "timeout": {"type": "integer", "description": "超时秒数,默认 30"},
22
+ },
23
+ "required": ["command"],
24
+ }
25
+ requires_approval = True
26
+ parallel_safe = False
27
+
28
+ def preview(self, args: dict) -> str:
29
+ cmd = args.get("command", "")
30
+ return f"run_shell($ {cmd[:60]})"
31
+
32
+ def run(self, command: str, timeout: int = 30) -> ToolResult:
33
+ low = command.strip().lower()
34
+ if any(d in low for d in _DANGER):
35
+ return ToolResult(ok=False, error="命令被安全策略拦截(高危操作)")
36
+ try:
37
+ proc = subprocess.run(
38
+ command,
39
+ shell=True,
40
+ stdin=subprocess.DEVNULL,
41
+ capture_output=True,
42
+ text=True,
43
+ timeout=timeout,
44
+ encoding="utf-8",
45
+ errors="replace",
46
+ )
47
+ except subprocess.TimeoutExpired:
48
+ return ToolResult(ok=False, error=f"命令超时({timeout}s)")
49
+ except Exception as exc:
50
+ return ToolResult(ok=False, error=f"执行失败:{exc}")
51
+
52
+ out = (proc.stdout or "") + (proc.stderr or "")
53
+ out = truncate(out.strip()) or "(无输出)"
54
+ body = f"[exit {proc.returncode}]\n{out}"
55
+ return ToolResult(
56
+ ok=proc.returncode == 0,
57
+ output=body,
58
+ error="" if proc.returncode == 0 else f"退出码 {proc.returncode}\n{out}",
59
+ summary=f"$ {command[:50]} → exit {proc.returncode}",
60
+ )
nexforge/tools/web.py ADDED
@@ -0,0 +1,40 @@
1
+ """web_fetch 工具:HTTP GET(用 httpx,截断)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nexforge.tools._paths import truncate
6
+ from nexforge.tools.base import BaseTool, ToolResult
7
+
8
+
9
+ class WebFetchTool(BaseTool):
10
+ name = "web_fetch"
11
+ description = "HTTP GET 抓取一个 URL,返回响应正文(截断)。用于读文档/API。"
12
+ parameters = {
13
+ "type": "object",
14
+ "properties": {
15
+ "url": {"type": "string", "description": "http(s) URL"},
16
+ "timeout": {"type": "integer", "description": "超时秒数,默认 20"},
17
+ },
18
+ "required": ["url"],
19
+ }
20
+
21
+ def preview(self, args: dict) -> str:
22
+ return f"web_fetch({args.get('url', '')[:50]})"
23
+
24
+ def run(self, url: str, timeout: int = 20) -> ToolResult:
25
+ if not url.startswith(("http://", "https://")):
26
+ return ToolResult(ok=False, error="仅支持 http(s) URL")
27
+ try:
28
+ import httpx
29
+
30
+ resp = httpx.get(url, timeout=timeout, follow_redirects=True)
31
+ except Exception as exc:
32
+ return ToolResult(ok=False, error=f"请求失败:{exc}")
33
+ ctype = resp.headers.get("content-type", "")
34
+ body = resp.text
35
+ return ToolResult(
36
+ ok=resp.status_code < 400,
37
+ output=truncate(f"[{resp.status_code} {ctype}]\n{body}"),
38
+ error="" if resp.status_code < 400 else f"HTTP {resp.status_code}",
39
+ summary=f"GET {url[:40]} → {resp.status_code}",
40
+ )
@@ -0,0 +1,40 @@
1
+ """write_file 工具:整文件覆写 / 新建。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nexforge.tools._paths import resolve
6
+ from nexforge.tools.base import BaseTool, ToolResult
7
+
8
+
9
+ class WriteFileTool(BaseTool):
10
+ name = "write_file"
11
+ description = "把内容整体写入文件(覆盖已有内容或新建)。谨慎使用,会覆盖原文件。"
12
+ parameters = {
13
+ "type": "object",
14
+ "properties": {
15
+ "path": {"type": "string", "description": "目标文件路径"},
16
+ "content": {"type": "string", "description": "文件完整内容"},
17
+ },
18
+ "required": ["path", "content"],
19
+ }
20
+ requires_approval = True
21
+ parallel_safe = False
22
+
23
+ def preview(self, args: dict) -> str:
24
+ return f"write_file({args.get('path', '')})"
25
+
26
+ def run(self, path: str, content: str) -> ToolResult:
27
+ fp = resolve(path)
28
+ existed = fp.exists()
29
+ try:
30
+ fp.parent.mkdir(parents=True, exist_ok=True)
31
+ fp.write_text(content, encoding="utf-8")
32
+ except Exception as exc:
33
+ return ToolResult(ok=False, error=f"写入失败:{exc}")
34
+ verb = "覆写" if existed else "新建"
35
+ lines = content.count("\n") + 1
36
+ return ToolResult(
37
+ ok=True,
38
+ output=f"已{verb} {path}({lines} 行)",
39
+ summary=f"{verb} {path}",
40
+ )
@@ -0,0 +1 @@
1
+ """表现层:Textual 组件与屏幕。"""
@@ -0,0 +1 @@
1
+ """Textual 屏幕。"""