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.
- mosscode/__init__.py +10 -0
- mosscode/__main__.py +3 -0
- mosscode/agent.py +486 -0
- mosscode/agents/__init__.py +249 -0
- mosscode/agents/builtin/explore.md +16 -0
- mosscode/agents/builtin/general.md +12 -0
- mosscode/agents/builtin/plan.md +16 -0
- mosscode/agents/runtime.py +685 -0
- mosscode/agents/tasks.py +193 -0
- mosscode/appearance.py +184 -0
- mosscode/builtin_commands.py +1363 -0
- mosscode/cli.py +547 -0
- mosscode/commands.py +254 -0
- mosscode/compaction.py +259 -0
- mosscode/config.py +337 -0
- mosscode/context.py +102 -0
- mosscode/conversation.py +1046 -0
- mosscode/coordinator.py +54 -0
- mosscode/doctor.py +303 -0
- mosscode/hooks/__init__.py +22 -0
- mosscode/hooks/actions.py +151 -0
- mosscode/hooks/conditions.py +170 -0
- mosscode/hooks/config.py +404 -0
- mosscode/hooks/engine.py +401 -0
- mosscode/hooks/events.py +48 -0
- mosscode/launcher.py +177 -0
- mosscode/mcp_client.py +460 -0
- mosscode/mcp_config.py +216 -0
- mosscode/member_ui.py +289 -0
- mosscode/memory.py +291 -0
- mosscode/memory_extract.py +245 -0
- mosscode/messages.py +216 -0
- mosscode/paths.py +31 -0
- mosscode/permissions.py +733 -0
- mosscode/prompts.py +483 -0
- mosscode/providers.py +587 -0
- mosscode/scrollback.py +106 -0
- mosscode/security.py +282 -0
- mosscode/serialization.py +121 -0
- mosscode/session.py +563 -0
- mosscode/setup.py +248 -0
- mosscode/skills/__init__.py +455 -0
- mosscode/skills/builtin/commit/SKILL.md +15 -0
- mosscode/skills/builtin/review/SKILL.md +15 -0
- mosscode/skills/builtin/test/SKILL.md +13 -0
- mosscode/skills/builtin/test/scripts/detect.py +96 -0
- mosscode/skills/builtin/test/tool.json +9 -0
- mosscode/teams/__init__.py +216 -0
- mosscode/teams/backends.py +259 -0
- mosscode/teams/board.py +230 -0
- mosscode/teams/lock.py +173 -0
- mosscode/teams/mailbox.py +172 -0
- mosscode/teams/member.py +427 -0
- mosscode/teams/merge.py +207 -0
- mosscode/teams/pane.py +20 -0
- mosscode/teams/pane_app.py +167 -0
- mosscode/teams/process.py +371 -0
- mosscode/teams/runtime.py +1355 -0
- mosscode/teams/view.py +182 -0
- mosscode/tool_budget.py +86 -0
- mosscode/tools/__init__.py +38 -0
- mosscode/tools/agent.py +127 -0
- mosscode/tools/base.py +384 -0
- mosscode/tools/diffing.py +16 -0
- mosscode/tools/execution.py +99 -0
- mosscode/tools/files.py +287 -0
- mosscode/tools/memory.py +65 -0
- mosscode/tools/plan.py +30 -0
- mosscode/tools/process.py +216 -0
- mosscode/tools/search.py +229 -0
- mosscode/tools/search_worker.py +243 -0
- mosscode/tools/shell.py +121 -0
- mosscode/tools/skill.py +118 -0
- mosscode/tools/team.py +447 -0
- mosscode/tools/windows_job.py +75 -0
- mosscode/tui.py +2325 -0
- mosscode/usage.py +68 -0
- mosscode/windows_inline.py +148 -0
- mosscode/worktree/__init__.py +29 -0
- mosscode/worktree/changes.py +59 -0
- mosscode/worktree/git.py +125 -0
- mosscode/worktree/manager.py +462 -0
- mosscode/worktree/meta.py +82 -0
- mosscode/worktree/names.py +52 -0
- mosscode/worktree/setup.py +119 -0
- mosscode-0.1.0.dist-info/METADATA +92 -0
- mosscode-0.1.0.dist-info/RECORD +91 -0
- mosscode-0.1.0.dist-info/WHEEL +5 -0
- mosscode-0.1.0.dist-info/entry_points.txt +3 -0
- mosscode-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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]
|