mosscode 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 (91) hide show
  1. mosscode/__init__.py +10 -0
  2. mosscode/__main__.py +3 -0
  3. mosscode/agent.py +486 -0
  4. mosscode/agents/__init__.py +249 -0
  5. mosscode/agents/builtin/explore.md +16 -0
  6. mosscode/agents/builtin/general.md +12 -0
  7. mosscode/agents/builtin/plan.md +16 -0
  8. mosscode/agents/runtime.py +685 -0
  9. mosscode/agents/tasks.py +193 -0
  10. mosscode/appearance.py +184 -0
  11. mosscode/builtin_commands.py +1363 -0
  12. mosscode/cli.py +547 -0
  13. mosscode/commands.py +254 -0
  14. mosscode/compaction.py +259 -0
  15. mosscode/config.py +337 -0
  16. mosscode/context.py +102 -0
  17. mosscode/conversation.py +1046 -0
  18. mosscode/coordinator.py +54 -0
  19. mosscode/doctor.py +303 -0
  20. mosscode/hooks/__init__.py +22 -0
  21. mosscode/hooks/actions.py +151 -0
  22. mosscode/hooks/conditions.py +170 -0
  23. mosscode/hooks/config.py +404 -0
  24. mosscode/hooks/engine.py +401 -0
  25. mosscode/hooks/events.py +48 -0
  26. mosscode/launcher.py +177 -0
  27. mosscode/mcp_client.py +460 -0
  28. mosscode/mcp_config.py +216 -0
  29. mosscode/member_ui.py +289 -0
  30. mosscode/memory.py +291 -0
  31. mosscode/memory_extract.py +245 -0
  32. mosscode/messages.py +216 -0
  33. mosscode/paths.py +31 -0
  34. mosscode/permissions.py +733 -0
  35. mosscode/prompts.py +483 -0
  36. mosscode/providers.py +587 -0
  37. mosscode/scrollback.py +106 -0
  38. mosscode/security.py +282 -0
  39. mosscode/serialization.py +121 -0
  40. mosscode/session.py +563 -0
  41. mosscode/setup.py +248 -0
  42. mosscode/skills/__init__.py +455 -0
  43. mosscode/skills/builtin/commit/SKILL.md +15 -0
  44. mosscode/skills/builtin/review/SKILL.md +15 -0
  45. mosscode/skills/builtin/test/SKILL.md +13 -0
  46. mosscode/skills/builtin/test/scripts/detect.py +96 -0
  47. mosscode/skills/builtin/test/tool.json +9 -0
  48. mosscode/teams/__init__.py +216 -0
  49. mosscode/teams/backends.py +259 -0
  50. mosscode/teams/board.py +230 -0
  51. mosscode/teams/lock.py +173 -0
  52. mosscode/teams/mailbox.py +172 -0
  53. mosscode/teams/member.py +427 -0
  54. mosscode/teams/merge.py +207 -0
  55. mosscode/teams/pane.py +20 -0
  56. mosscode/teams/pane_app.py +167 -0
  57. mosscode/teams/process.py +371 -0
  58. mosscode/teams/runtime.py +1355 -0
  59. mosscode/teams/view.py +182 -0
  60. mosscode/tool_budget.py +86 -0
  61. mosscode/tools/__init__.py +38 -0
  62. mosscode/tools/agent.py +127 -0
  63. mosscode/tools/base.py +384 -0
  64. mosscode/tools/diffing.py +16 -0
  65. mosscode/tools/execution.py +99 -0
  66. mosscode/tools/files.py +287 -0
  67. mosscode/tools/memory.py +65 -0
  68. mosscode/tools/plan.py +30 -0
  69. mosscode/tools/process.py +216 -0
  70. mosscode/tools/search.py +229 -0
  71. mosscode/tools/search_worker.py +243 -0
  72. mosscode/tools/shell.py +121 -0
  73. mosscode/tools/skill.py +118 -0
  74. mosscode/tools/team.py +447 -0
  75. mosscode/tools/windows_job.py +75 -0
  76. mosscode/tui.py +2325 -0
  77. mosscode/usage.py +68 -0
  78. mosscode/windows_inline.py +148 -0
  79. mosscode/worktree/__init__.py +29 -0
  80. mosscode/worktree/changes.py +59 -0
  81. mosscode/worktree/git.py +125 -0
  82. mosscode/worktree/manager.py +462 -0
  83. mosscode/worktree/meta.py +82 -0
  84. mosscode/worktree/names.py +52 -0
  85. mosscode/worktree/setup.py +119 -0
  86. mosscode-0.1.0.dist-info/METADATA +92 -0
  87. mosscode-0.1.0.dist-info/RECORD +91 -0
  88. mosscode-0.1.0.dist-info/WHEEL +5 -0
  89. mosscode-0.1.0.dist-info/entry_points.txt +3 -0
  90. mosscode-0.1.0.dist-info/licenses/LICENSE +21 -0
  91. mosscode-0.1.0.dist-info/top_level.txt +1 -0
mosscode/setup.py ADDED
@@ -0,0 +1,248 @@
1
+ """`moss setup`, and the first interactive run without a configuration.
2
+
3
+ Plain questions in the terminal (input and getpass), so it works in any console and before Textual loads.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import getpass
10
+ import ipaddress
11
+ import os
12
+ import re
13
+ import sys
14
+ from collections.abc import Callable
15
+ from pathlib import Path
16
+ from urllib.parse import urlsplit
17
+
18
+ PROTOCOLS = (
19
+ ("anthropic-messages", "Anthropic Messages(Claude,或兼容 Messages 的服务)"),
20
+ ("openai-responses", "OpenAI Responses"),
21
+ ("openai-chat-completions", "OpenAI Chat Completions(DeepSeek、Qwen 等兼容服务)"),
22
+ )
23
+ NAMES = {"anthropic-messages": "claude", "openai-responses": "openai", "openai-chat-completions": "compat"}
24
+ OFFICIAL = {
25
+ "anthropic-messages": "https://api.anthropic.com",
26
+ "openai-responses": "https://api.openai.com/v1",
27
+ "openai-chat-completions": "https://api.openai.com/v1",
28
+ } # Where the SDK goes without base_url.
29
+ KEYS = {
30
+ "anthropic-messages": "ANTHROPIC_API_KEY",
31
+ "openai-responses": "OPENAI_API_KEY",
32
+ "openai-chat-completions": "OPENAI_API_KEY",
33
+ }
34
+ ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
35
+ PLAINTEXT_WARNING = "密钥会以明文写入配置文件,能读取这个文件的程序和人都能看到它。"
36
+
37
+
38
+ class Cancelled(Exception):
39
+ pass
40
+
41
+
42
+ def user_config(home: Path | None = None) -> Path:
43
+ return (Path.home() if home is None else home) / ".mosscode" / "config.yaml"
44
+
45
+
46
+ def _host_name(base_url: str | None) -> str | None:
47
+ """api.deepseek.com -> deepseek; None for an address or a single label."""
48
+ host = urlsplit(base_url).hostname if base_url else None
49
+ if not host:
50
+ return None
51
+ try:
52
+ ipaddress.ip_address(host)
53
+ return None
54
+ except ValueError:
55
+ pass
56
+ labels = [label for label in host.split(".") if label]
57
+ return labels[-2].lower() if len(labels) >= 2 and re.fullmatch(r"[a-z0-9-]+", labels[-2].lower()) else None
58
+
59
+
60
+ def _url_problem(value: str) -> str | None:
61
+ parts = urlsplit(value)
62
+ if parts.scheme not in {"http", "https"} or not parts.hostname:
63
+ return "请填写 http:// 或 https:// 开头的地址。"
64
+ if parts.username or parts.password:
65
+ return "地址中不能包含账号或密码。"
66
+ return None
67
+
68
+
69
+ def _window(value: str) -> int | None:
70
+ """200000, 200k or 1m; None when it is not a positive count."""
71
+ match = re.fullmatch(r"(\d+(?:\.\d+)?)\s*([kKmM]?)", value.replace(",", "").replace("_", ""))
72
+ if match is None:
73
+ return None
74
+ number = float(match.group(1)) * {"": 1, "k": 1000, "m": 1000000}[match.group(2).lower()]
75
+ return int(number) if number >= 1 and number == int(number) else None
76
+
77
+
78
+ class Wizard:
79
+ def __init__(
80
+ self,
81
+ read: Callable[[str], str] = input,
82
+ secret: Callable[[str], str] = getpass.getpass,
83
+ write: Callable[[str], None] = print,
84
+ environ=None,
85
+ ):
86
+ self.read, self.secret, self.write = read, secret, write
87
+ self.environ = os.environ if environ is None else environ
88
+
89
+ def ask(self, question: str, default: str = "") -> str:
90
+ try:
91
+ answer = self.read(f"{question}{f' [{default}]' if default else ''}:")
92
+ except EOFError:
93
+ raise Cancelled from None
94
+ return answer.strip() or default
95
+
96
+ def choose(self, question: str, options: list[str], default: int = 1) -> int:
97
+ for index, option in enumerate(options, 1):
98
+ self.write(f" {index}. {option}")
99
+ while True:
100
+ answer = self.ask(question, str(default))
101
+ if answer.isdigit() and 1 <= int(answer) <= len(options):
102
+ return int(answer)
103
+ self.write(f" 请输入 1–{len(options)} 之间的数字。")
104
+
105
+ def collect(self) -> dict | None:
106
+ """The provider entry, or None when the user did not confirm. Raises Cancelled on Ctrl+D."""
107
+ self.write("")
108
+ protocol = PROTOCOLS[self.choose("1/5 协议", [label for _, label in PROTOCOLS]) - 1][0]
109
+ if protocol == "openai-chat-completions":
110
+ self.write(" DeepSeek、Qwen 等兼容服务请填写它的地址,例如 https://api.deepseek.com")
111
+ while True:
112
+ base_url = self.ask(f"2/5 服务地址 base_url(回车使用官方地址 {OFFICIAL[protocol]})")
113
+ problem = _url_problem(base_url) if base_url else None
114
+ if problem is None:
115
+ break
116
+ self.write(" " + problem)
117
+ while not (model := self.ask("3/5 模型名(以服务文档为准)")):
118
+ self.write(" 模型名不能为空。")
119
+ while True:
120
+ answer = self.ask("4/5 上下文窗口 Token 数,如 200k(回车按 128K 估算)")
121
+ window = _window(answer) if answer else None
122
+ if not answer or window is not None:
123
+ break
124
+ self.write(" 请输入正整数,例如 128000、200k 或 1m。")
125
+ name = _host_name(base_url) or NAMES[protocol]
126
+ provider: dict = {"name": name, "protocol": protocol, "model": model}
127
+ if base_url:
128
+ provider["base_url"] = base_url
129
+ self.write("5/5 凭据")
130
+ method = self.choose(
131
+ "保存方式", ["环境变量(推荐):配置文件只写变量名,密钥放在环境变量中", "直接输入密钥:写入配置文件"]
132
+ )
133
+ if method == 1:
134
+ default = f"{name.upper().replace('-', '_')}_API_KEY" if _host_name(base_url) else KEYS[protocol]
135
+ while not ENV_NAME.fullmatch(variable := self.ask("环境变量名", default)):
136
+ self.write(" 请填写变量名(字母、数字和下划线),不要填写密钥本身。")
137
+ provider["api_key_env"] = variable
138
+ else:
139
+ self.write(" " + PLAINTEXT_WARNING)
140
+ while True:
141
+ try:
142
+ key = self.secret("密钥(输入时不显示):").strip()
143
+ except EOFError:
144
+ raise Cancelled from None
145
+ if key:
146
+ break
147
+ self.write(" 密钥不能为空。")
148
+ provider["api_key"] = key
149
+ if window is not None:
150
+ provider["context_window"] = window
151
+ self.write("")
152
+ self.write("将写入:")
153
+ for line in summary(provider):
154
+ self.write(" " + line)
155
+ return provider if self.ask("确认写入?(Y/n)").lower() in {"", "y", "yes", "是"} else None
156
+
157
+
158
+ def summary(provider: dict) -> list[str]:
159
+ """What will be written, the key itself never shown."""
160
+ lines = [
161
+ f"名称 {provider['name']}(默认 Provider)",
162
+ f"协议 {provider['protocol']}",
163
+ f"模型 {provider['model']}",
164
+ f"服务地址 {provider.get('base_url') or '官方地址 ' + OFFICIAL[provider['protocol']]}",
165
+ f"上下文窗口 {provider.get('context_window') or '未填写,按 128K 估算'}",
166
+ ]
167
+ if "api_key_env" in provider:
168
+ lines.append(f"凭据 环境变量 {provider['api_key_env']}")
169
+ else:
170
+ lines.append("凭据 已设置(明文保存在配置文件中)")
171
+ return lines
172
+
173
+
174
+ def write_config(path: Path, provider: dict) -> None:
175
+ """Never replaces a file: the exclusive create fails if one appeared meanwhile."""
176
+ from .config import render_config
177
+
178
+ path.parent.mkdir(parents=True, exist_ok=True)
179
+ with path.open("x", encoding="utf-8", newline="\n") as stream:
180
+ stream.write(render_config(provider))
181
+ if "api_key" in provider and os.name != "nt":
182
+ path.chmod(0o600)
183
+
184
+
185
+ def setup(home: Path | None = None, wizard: Wizard | None = None, *, first_run: bool = True) -> str:
186
+ """One of ready, written (a credential is still to be set), exists, cancelled, failed; each is reported."""
187
+ from .config import ConfigError, load_settings
188
+
189
+ wizard = wizard or Wizard()
190
+ path = user_config(home)
191
+ if path.exists():
192
+ wizard.write(f"配置已存在:{path}。setup 不会覆盖它;追加 Provider 请直接编辑这个文件。")
193
+ return "exists"
194
+ if first_run:
195
+ wizard.write("MossCode 还没有配置。下面创建用户级配置,之后在任何目录都能使用。")
196
+ wizard.write(f"将写入 {path}。按 Ctrl+C 随时退出,不写入任何文件。")
197
+ try:
198
+ provider = wizard.collect()
199
+ except (Cancelled, KeyboardInterrupt):
200
+ wizard.write("\n已退出,没有写入文件。")
201
+ return "cancelled"
202
+ if provider is None:
203
+ wizard.write("没有写入文件。")
204
+ return "cancelled"
205
+ try:
206
+ write_config(path, provider)
207
+ except FileExistsError:
208
+ wizard.write(f"配置已存在:{path}。没有覆盖它。")
209
+ return "exists"
210
+ except OSError as exc:
211
+ wizard.write(f"无法写入 {path}:{exc.strerror or exc}。")
212
+ return "failed"
213
+ try:
214
+ load_settings(path)
215
+ except ConfigError as exc:
216
+ wizard.write(f"已写入 {path},但校验失败:{exc} 请修改文件后再运行 moss。")
217
+ return "failed"
218
+ wizard.write(f"已写入 {path}。")
219
+ variable = provider.get("api_key_env")
220
+ if variable and not wizard.environ.get(variable):
221
+ wizard.write(f"环境变量 {variable} 还没有设置。在 PowerShell 中设置(只对新打开的终端生效):")
222
+ wizard.write(f' [Environment]::SetEnvironmentVariable("{variable}", "你的密钥", "User")')
223
+ wizard.write("然后打开新的终端,运行 moss。")
224
+ return "written"
225
+ return "ready"
226
+
227
+
228
+ def run_setup(home: Path | None = None, wizard: Wizard | None = None) -> Path | None:
229
+ """The first interactive run: the configuration to start with, or None to stop there."""
230
+ return user_config(home) if setup(home, wizard) == "ready" else None
231
+
232
+
233
+ def main(argv: list[str] | None = None) -> int:
234
+ command = argparse.ArgumentParser(
235
+ prog="moss setup",
236
+ description="创建用户级配置 ~/.mosscode/config.yaml:逐项询问协议、地址、模型、上下文窗口和凭据。"
237
+ "已有配置时不修改它。",
238
+ )
239
+ command.parse_args(argv)
240
+ from .launcher import is_terminal
241
+
242
+ if not (is_terminal(sys.stdin) and is_terminal(sys.stdout)):
243
+ print("moss setup 需要在终端中运行。", file=sys.stderr)
244
+ return 2
245
+ result = setup(first_run=False)
246
+ if result == "ready":
247
+ print("运行 moss 开始使用。")
248
+ return {"ready": 0, "written": 0, "exists": 0, "cancelled": 130, "failed": 1}[result]