fr-cli 2.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 (64) hide show
  1. fr_cli/README.md +148 -0
  2. fr_cli/WEAPON.MD +186 -0
  3. fr_cli/__init__.py +4 -0
  4. fr_cli/addon/plugin.py +69 -0
  5. fr_cli/agent/__init__.py +9 -0
  6. fr_cli/agent/builtins/__init__.py +4 -0
  7. fr_cli/agent/builtins/_utils.py +48 -0
  8. fr_cli/agent/builtins/db.py +269 -0
  9. fr_cli/agent/builtins/local.py +105 -0
  10. fr_cli/agent/builtins/rag.py +652 -0
  11. fr_cli/agent/builtins/rag_watcher_daemon.py +156 -0
  12. fr_cli/agent/builtins/remote.py +214 -0
  13. fr_cli/agent/builtins/spider.py +247 -0
  14. fr_cli/agent/client.py +164 -0
  15. fr_cli/agent/executor.py +86 -0
  16. fr_cli/agent/generator.py +104 -0
  17. fr_cli/agent/manager.py +193 -0
  18. fr_cli/agent/master.py +604 -0
  19. fr_cli/agent/master_prompt.py +118 -0
  20. fr_cli/agent/remote.py +70 -0
  21. fr_cli/agent/server.py +279 -0
  22. fr_cli/agent/workflow.py +164 -0
  23. fr_cli/breakthrough/update.py +154 -0
  24. fr_cli/command/__init__.py +4 -0
  25. fr_cli/command/executor.py +276 -0
  26. fr_cli/command/registry.py +1034 -0
  27. fr_cli/command/security.py +30 -0
  28. fr_cli/conf/config.py +126 -0
  29. fr_cli/conf/wizard.py +172 -0
  30. fr_cli/core/chat.py +280 -0
  31. fr_cli/core/core.py +111 -0
  32. fr_cli/core/intent.py +129 -0
  33. fr_cli/core/recommender.py +71 -0
  34. fr_cli/core/stream.py +83 -0
  35. fr_cli/core/sysmon.py +117 -0
  36. fr_cli/core/thinking.py +215 -0
  37. fr_cli/gatekeeper/__init__.py +7 -0
  38. fr_cli/gatekeeper/daemon.py +216 -0
  39. fr_cli/gatekeeper/manager.py +218 -0
  40. fr_cli/lang/i18n.py +827 -0
  41. fr_cli/main.py +329 -0
  42. fr_cli/memory/context.py +119 -0
  43. fr_cli/memory/history.py +96 -0
  44. fr_cli/memory/session.py +134 -0
  45. fr_cli/repl/__init__.py +0 -0
  46. fr_cli/repl/commands.py +1098 -0
  47. fr_cli/security/security.py +46 -0
  48. fr_cli/ui/ui.py +116 -0
  49. fr_cli/weapon/cron.py +217 -0
  50. fr_cli/weapon/dataframe.py +97 -0
  51. fr_cli/weapon/disk.py +141 -0
  52. fr_cli/weapon/fs.py +206 -0
  53. fr_cli/weapon/launcher.py +249 -0
  54. fr_cli/weapon/loader.py +98 -0
  55. fr_cli/weapon/mail.py +227 -0
  56. fr_cli/weapon/mcp.py +204 -0
  57. fr_cli/weapon/vision.py +74 -0
  58. fr_cli/weapon/web.py +88 -0
  59. fr_cli-2.1.0.dist-info/METADATA +227 -0
  60. fr_cli-2.1.0.dist-info/RECORD +64 -0
  61. fr_cli-2.1.0.dist-info/WHEEL +5 -0
  62. fr_cli-2.1.0.dist-info/entry_points.txt +2 -0
  63. fr_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
  64. fr_cli-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,154 @@
1
+ """
2
+ 自动更新模块 update.py
3
+ 接口地址: https://up.seeknew.cn/v1/cliupdate
4
+ """
5
+ import os, sys, json, platform, subprocess, shutil, tempfile, time, zipfile, hashlib
6
+ from pathlib import Path
7
+ from typing import Callable, Optional, Tuple
8
+
9
+ UPDATE_API_URL = "https://up.seeknew.cn/v1/cliupdate"
10
+ REQUEST_TIMEOUT = 15
11
+ PROJECT_ROOT = Path(__file__).resolve().parent
12
+ VERSION_FILE = PROJECT_ROOT / "__version__.txt"
13
+
14
+ def _read_local_version() -> str:
15
+ try:
16
+ if VERSION_FILE.is_file():
17
+ txt = VERSION_FILE.read_text(encoding="utf-8").strip()
18
+ if txt:
19
+ first_line = txt.splitlines()[0].strip().lower().lstrip('v')
20
+ return first_line
21
+ except Exception: pass
22
+ # Fallback to package __version__
23
+ try:
24
+ from fr_cli import __version__
25
+ return str(__version__).strip().lower().lstrip('v')
26
+ except Exception:
27
+ return "0.0.0"
28
+
29
+ def _save_local_version(version: str) -> None:
30
+ try: VERSION_FILE.write_text(str(version).strip() + "\n", encoding="utf-8")
31
+ except Exception: pass
32
+
33
+ def _parse_version_tuple(v: str) -> Tuple[int, ...]:
34
+ parts = v.replace("-", ".").split(".")
35
+ nums = []
36
+ for p in parts:
37
+ p = p.strip(); num = 0
38
+ if p and p[0].isdigit():
39
+ i = 0
40
+ while i < len(p) and p[i].isdigit(): i += 1
41
+ num = int(p[:i])
42
+ nums.append(num)
43
+ while len(nums) < 3: nums.append(0)
44
+ return tuple(nums[:3])
45
+
46
+ def _is_newer(remote: str, local: str) -> bool:
47
+ return _parse_version_tuple(remote) > _parse_version_tuple(local)
48
+
49
+ def _fetch_info() -> Tuple[Optional[dict], Optional[str]]:
50
+ try:
51
+ import urllib.request, ssl
52
+ ctx = ssl.create_default_context()
53
+ req = urllib.request.Request(UPDATE_API_URL, method="GET", headers={
54
+ "Accept": "application/json", "User-Agent": f"cli-update/{platform.system()}"
55
+ })
56
+ with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT, context=ctx) as resp:
57
+ data = json.loads(resp.read().decode("utf-8", errors="ignore"))
58
+ if isinstance(data, dict) and "version" in data and "download_url" in data: return data, None
59
+ if isinstance(data, dict) and "data" in data and isinstance(data["data"], dict):
60
+ inner = data["data"]
61
+ if "version" in inner and "download_url" in inner: return inner, None
62
+ return None, "Unrecognized schema."
63
+ except Exception as e: return None, str(e)
64
+
65
+ def _download(url: str) -> Tuple[Optional[bytes], Optional[str]]:
66
+ try:
67
+ import urllib.request, ssl
68
+ req = urllib.request.Request(url, headers={"User-Agent": "cli-update/1.0"})
69
+ with urllib.request.urlopen(req, timeout=120, context=ssl.create_default_context()) as resp:
70
+ data = resp.read()
71
+ return (data, None) if data else (None, "Empty payload.")
72
+ except Exception as e: return None, str(e)
73
+
74
+ def _apply_source_zip(zip_bytes: bytes, root: Path) -> Tuple[bool, str]:
75
+ try:
76
+ with tempfile.TemporaryDirectory() as tmp:
77
+ zpath = Path(tmp) / "u.zip"; zpath.write_bytes(zip_bytes)
78
+ ext = Path(tmp) / "ext"
79
+ with zipfile.ZipFile(zpath, 'r') as zf: zf.extractall(ext)
80
+
81
+ safe_exts = {".py",".sh",".bat",".md",".txt",".toml",".yaml",".yml",".html",".css",".js",".png",".jpg",".gif"}
82
+ skip = {"__version__.txt", "__config__.json"}
83
+ skip_dirs = {"data", "logs", "__pycache__", ".git", ".idea"}
84
+
85
+ for r, dirs, files in os.walk(ext):
86
+ rp = Path(r).relative_to(ext)
87
+ if any(p in skip_dirs for p in rp.parts): continue
88
+ for n in files:
89
+ s = Path(r) / n; d = root / rp / n
90
+ if n in skip or n.startswith(".") or d.suffix.lower() not in safe_exts: continue
91
+ try:
92
+ d.parent.mkdir(parents=True, exist_ok=True)
93
+ if d.exists():
94
+ shutil.copy2(str(d), str(d.with_suffix(d.suffix+".bak")))
95
+ shutil.copy2(str(s), str(d))
96
+ except Exception: pass
97
+ return True, "OK"
98
+ except Exception as e: return False, str(e)
99
+
100
+ def update_check(local_version: Optional[str] = None, verbose: bool = False) -> Tuple[bool, Optional[dict], Optional[str]]:
101
+ lv = local_version or _read_local_version()
102
+ if verbose: print(f"[Update] Local: {lv}")
103
+ info, err = _fetch_info()
104
+ if err: return False, None, err
105
+ rv = info.get("version", "0.0.0")
106
+ if verbose: print(f"[Update] Remote: {rv}")
107
+ return _is_newer(rv, lv), info, None
108
+
109
+ def update_and_restart(local_version: Optional[str] = None, verbose: bool = False, allow_restart: bool = True, on_before_restart: Optional[Callable] = None) -> Tuple[bool, str]:
110
+ has, info, err = update_check(local_version, verbose)
111
+ if err: return False, err
112
+ if not has or not info: return False, "Already up to date."
113
+
114
+ data, err = _download(info.get("download_url", ""))
115
+ if err or not data: return False, err or "Download failed."
116
+
117
+ sha_exp = info.get("sha256")
118
+ if sha_exp and hashlib.sha256(data).hexdigest().lower() != sha_exp.lower():
119
+ return False, "SHA256 mismatch."
120
+
121
+ ftype = info.get("file_type", "source_zip")
122
+ nver = info.get("version", "")
123
+
124
+ if ftype == "source_zip":
125
+ ok, msg = _apply_source_zip(data, PROJECT_ROOT)
126
+ if not ok: return False, msg
127
+ _save_local_version(nver)
128
+ if allow_restart:
129
+ if on_before_restart: on_before_restart()
130
+ subprocess.Popen([sys.executable, str(PROJECT_ROOT / "main.py")], cwd=str(PROJECT_ROOT))
131
+ time.sleep(0.5)
132
+ sys.stdout.flush()
133
+ sys.stderr.flush()
134
+ sys.exit(0)
135
+ return True, f"Updated to {nver}. Please restart."
136
+ else:
137
+ fname = os.path.basename(info.get("download_url", "update.bin"))
138
+ (PROJECT_ROOT / fname).write_bytes(data)
139
+ return True, f"Saved {fname}. Please update manually."
140
+
141
+ def cli_entry(args=None):
142
+ args = args or sys.argv[1:]
143
+ if not args or args[0].lower() == "check":
144
+ ok, info, err = update_check(verbose=True)
145
+ if err: print(f"Check failed: {err}"); return 1
146
+ if not ok: print("Already up to date."); return 0
147
+ print(f"New version: {info.get('version')}\nNote: {info.get('release_note', '')}"); return 0
148
+ elif args[0].lower() == "run":
149
+ ok, msg = update_and_restart(verbose=True)
150
+ print(f"{'Success' if ok else 'Failed'}: {msg}"); return 0 if ok else 1
151
+ else:
152
+ print("Usage: python update.py check | run"); return 2
153
+
154
+ if __name__ == "__main__": sys.exit(cli_entry())
@@ -0,0 +1,4 @@
1
+ """
2
+ 命令执行子系统
3
+ 包含安全确认管理器与命令执行引擎
4
+ """
@@ -0,0 +1,276 @@
1
+ """
2
+ 命令执行引擎
3
+ 负责解析 AI 响应中的调用标记,并调度到统一注册表执行。
4
+ """
5
+ import re
6
+ import json
7
+ import ast
8
+ from types import SimpleNamespace
9
+ from fr_cli.command.registry import get_registry
10
+ from fr_cli.addon.plugin import exec_plugin
11
+
12
+
13
+ def _build_deps(state):
14
+ """根据 AppState 动态构建依赖命名空间(每次调用实时反射,避免快照过时)"""
15
+ return SimpleNamespace(
16
+ vfs=state.vfs,
17
+ mail_c=state.mail_c,
18
+ web_c=state.web_c,
19
+ disk_c=state.disk_c,
20
+ plugins=state.plugins,
21
+ lang=state.lang,
22
+ security=state.security,
23
+ cfg=state.cfg,
24
+ client=state.client,
25
+ model_name=state.model_name,
26
+ mcp=getattr(state, "mcp", None),
27
+ )
28
+
29
+
30
+ class CommandExecutor:
31
+ """
32
+ 命令执行器:解析 AI 回复中的调用标记,并通过注册表统一调度执行。
33
+ 直接持有 AppState,每次调用时动态构建依赖快照,彻底消除状态过时问题。
34
+
35
+ 公共接口(保持向后兼容):
36
+ - invoke_tool(tool_name, kwargs, msgs=None): 结构化工具调用
37
+ - execute(cmd_str, msgs=None): 命令字符串调用
38
+ - process_ai_commands(ai_response, msgs=None): 解析并执行 AI 回复中的命令标记
39
+ """
40
+
41
+ def __init__(self, state):
42
+ self.state = state
43
+ self._reg = get_registry()
44
+
45
+ # ------------------------------------------------------------------
46
+ # 第一层:结构化工具调用
47
+ # ------------------------------------------------------------------
48
+ def invoke_tool(self, tool_name, kwargs, msgs=None):
49
+ """根据工具名和结构化参数,通过注册表调度执行。返回 (result, error)"""
50
+ return self._reg.dispatch(_build_deps(self.state), tool_name, msgs=msgs, **kwargs)
51
+
52
+ # ------------------------------------------------------------------
53
+ # 第二层:传统命令解析(用户输入 / 插件调用)
54
+ # ------------------------------------------------------------------
55
+ def execute(self, cmd_str, msgs=None):
56
+ """执行单个命令并返回结果 (result, error)
57
+ 已分词检查插件后,直接通过注册表内部接口调度,避免重复 split。"""
58
+ parts = cmd_str.strip().split()
59
+ if not parts:
60
+ return None, "Empty command"
61
+ cmd = parts[0].lstrip("/")
62
+ # 插件命令优先直接处理(保持 mock 路径兼容)
63
+ if cmd in self.state.plugins:
64
+ p_args = ' '.join(parts[1:]) if len(parts) > 1 else ""
65
+ exec_plugin(cmd, self.state.plugins[cmd], p_args, self.state.lang)
66
+ return f"Plugin {cmd} executed", None
67
+ # 其余命令通过注册表内部接口直接调度,避免 dispatch_cmd 再次 split
68
+ return self._reg._dispatch_cmd_parts(_build_deps(self.state), parts, msgs=msgs)
69
+
70
+ # ------------------------------------------------------------------
71
+ # 第三层:AI 回复解析
72
+ # ------------------------------------------------------------------
73
+ def _loose_parse_kwargs(self, arg_str):
74
+ """宽松解析 JSON 参数字符串(回退方案)"""
75
+ key_pattern = r'"(\w+)"\s*:\s*'
76
+ keys = list(re.finditer(key_pattern, arg_str))
77
+ if not keys:
78
+ return None
79
+ result = {}
80
+ for i, m in enumerate(keys):
81
+ key = m.group(1)
82
+ start = m.end()
83
+ if i + 1 < len(keys):
84
+ end = keys[i + 1].start()
85
+ else:
86
+ end = len(arg_str)
87
+ while end > 0 and arg_str[end - 1] in ' \t\n\r}':
88
+ end -= 1
89
+ val_str = arg_str[start:end].strip().rstrip(',').strip()
90
+
91
+ # 布尔值
92
+ if val_str == 'true':
93
+ result[key] = True
94
+ continue
95
+ if val_str == 'false':
96
+ result[key] = False
97
+ continue
98
+ if val_str == 'null':
99
+ result[key] = None
100
+ continue
101
+ # 数字
102
+ try:
103
+ if '.' in val_str:
104
+ result[key] = float(val_str)
105
+ else:
106
+ result[key] = int(val_str)
107
+ continue
108
+ except ValueError:
109
+ pass
110
+ # 字符串(去掉两端引号)
111
+ if val_str.startswith('"') and val_str.endswith('"'):
112
+ val_str = val_str[1:-1]
113
+ # 还原转义序列
114
+ QUOTE_PH = '\x00Q\x00'
115
+ val_str = val_str.replace('\\"', QUOTE_PH)
116
+ val_str = val_str.replace('\\\\', '\\')
117
+ val_str = val_str.replace('\\n', '\n')
118
+ val_str = val_str.replace('\\t', '\t')
119
+ val_str = val_str.replace('\\r', '\r')
120
+ val_str = val_str.replace(QUOTE_PH, '"')
121
+ result[key] = val_str
122
+ return result if result is not None else None
123
+
124
+ def _parse_tool_kwargs(self, arg_str):
125
+ """安全解析工具参数字符串(JSON 或 Python dict)"""
126
+ arg_str = arg_str.strip()
127
+ if not arg_str:
128
+ return {}
129
+
130
+ # 预处理:将 JSON 字符串值内的原始换行替换为 \n 转义序列
131
+ fixed = ""
132
+ in_string = False
133
+ escape = False
134
+ for ch in arg_str:
135
+ if escape:
136
+ fixed += ch
137
+ escape = False
138
+ continue
139
+ if ch == '\\':
140
+ fixed += ch
141
+ escape = True
142
+ continue
143
+ if ch == '"':
144
+ in_string = not in_string
145
+ fixed += ch
146
+ continue
147
+ if ch in '\n\r' and in_string:
148
+ fixed += '\\n'
149
+ continue
150
+ fixed += ch
151
+
152
+ try:
153
+ return json.loads(fixed)
154
+ except json.JSONDecodeError:
155
+ try:
156
+ return ast.literal_eval(arg_str)
157
+ except (ValueError, SyntaxError):
158
+ return self._loose_parse_kwargs(arg_str)
159
+
160
+ def _extract_tool_calls(self, text):
161
+ """从文本中提取所有【调用:tool_name({...})】标记(支持嵌套括号,忽略字符串内的括号)"""
162
+ calls = []
163
+ i = 0
164
+ while True:
165
+ start = text.find('【调用:', i)
166
+ if start == -1:
167
+ break
168
+ paren = text.find('(', start)
169
+ if paren == -1:
170
+ break
171
+ tool_name = text[start + 4:paren].strip()
172
+ depth = 1
173
+ j = paren + 1
174
+ in_string = False
175
+ escape = False
176
+ while j < len(text) and depth > 0:
177
+ ch = text[j]
178
+ if escape:
179
+ escape = False
180
+ elif ch == '\\':
181
+ escape = True
182
+ elif ch == '"':
183
+ in_string = not in_string
184
+ elif not in_string:
185
+ if ch == '(':
186
+ depth += 1
187
+ elif ch == ')':
188
+ depth -= 1
189
+ j += 1
190
+ if depth != 0:
191
+ i = paren + 1
192
+ continue
193
+ arg_str = text[paren + 1:j - 1]
194
+ end = text.find('】', j - 1)
195
+ if end == -1:
196
+ break
197
+ marker = text[start:end + 1]
198
+ calls.append((tool_name, arg_str, marker))
199
+ i = end + 1
200
+ return calls
201
+
202
+ def process_ai_commands(self, ai_response, msgs=None):
203
+ """
204
+ 解析AI响应中的调用标记并自动执行
205
+ 支持三种格式:
206
+ 1. 【调用:tool_name({"参数": "值"})】(结构化调用)
207
+ 2. 【命令:/command args】(插件 / 兼容命令)
208
+ 3. file_operations/xxx(兼容旧模型输出)
209
+ 返回 (clean_response, cmd_results)
210
+ """
211
+ results = []
212
+ markers_to_remove = []
213
+
214
+ # ===== 格式1:【调用:...】 =====
215
+ for tool_name, arg_str, marker in self._extract_tool_calls(ai_response):
216
+ kwargs = self._parse_tool_kwargs(arg_str)
217
+ if kwargs is None:
218
+ results.append(f"❌ 参数解析失败: {tool_name}\n 原始参数: {arg_str}")
219
+ markers_to_remove.append(marker)
220
+ continue
221
+ result, error = self.invoke_tool(tool_name, kwargs, msgs)
222
+ if error:
223
+ results.append(f"❌ 工具调用失败: {tool_name}\n {error}")
224
+ else:
225
+ results.append(f"✅ 工具调用成功: {tool_name}\n 结果: {result}")
226
+ markers_to_remove.append(marker)
227
+
228
+ # ===== 格式2:【命令:...】 =====
229
+ pattern_cmd = r'【命令:(.*?)】'
230
+ for m in re.finditer(pattern_cmd, ai_response):
231
+ cmd_str = m.group(1).strip()
232
+ marker = m.group(0)
233
+ result, error = self.execute(cmd_str, msgs)
234
+ if error:
235
+ results.append(f"❌ 命令执行失败: {cmd_str}\n {error}")
236
+ else:
237
+ results.append(f"✅ 命令执行成功: {cmd_str}\n 结果: {result}")
238
+ markers_to_remove.append(marker)
239
+
240
+ # ===== 格式3:file_operations/xxx(兼容) =====
241
+ pattern2_quoted = r'file_operations\s*/(\w+)\s+(\S+)\s+"([\s\S]*?)"'
242
+ pattern2_plain = r'file_operations\s*/(\w+)\s+(.+)$'
243
+ for m in re.finditer(pattern2_quoted, ai_response):
244
+ action = m.group(1)
245
+ path = m.group(2)
246
+ content = m.group(3)
247
+ cmd_str = f"/{action} {path} {content}"
248
+ result, error = self.execute(cmd_str, msgs)
249
+ if error:
250
+ results.append(f"❌ 命令执行失败: {cmd_str}\n {error}")
251
+ else:
252
+ results.append(f"✅ 命令执行成功: {cmd_str}\n 结果: {result}")
253
+ markers_to_remove.append(m.group(0))
254
+ for m in re.finditer(pattern2_plain, ai_response, re.MULTILINE):
255
+ action = m.group(1)
256
+ args = m.group(2).strip()
257
+ if args.startswith('"') and args.endswith('"'):
258
+ args = args[1:-1]
259
+ already = any(m.group(0) in mk for mk in markers_to_remove)
260
+ if already:
261
+ continue
262
+ cmd_str = f"/{action} {args}"
263
+ result, error = self.execute(cmd_str, msgs)
264
+ if error:
265
+ results.append(f"❌ 命令执行失败: {cmd_str}\n {error}")
266
+ else:
267
+ results.append(f"✅ 命令执行成功: {cmd_str}\n 结果: {result}")
268
+ markers_to_remove.append(m.group(0))
269
+
270
+ # 清理回复文本:移除命令标记后,仅压缩因移除标记产生的连续多余空行,并去除首尾空白
271
+ clean_response = ai_response
272
+ for marker in markers_to_remove:
273
+ clean_response = clean_response.replace(marker, "")
274
+ clean_response = re.sub(r'\n\s*\n\s*\n+', '\n\n', clean_response).strip()
275
+
276
+ return clean_response, results