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
@@ -0,0 +1,193 @@
1
+ """The sub-agents of one session, foreground or background."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from collections.abc import Awaitable, Callable
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from ..messages import UsageTotals
13
+
14
+ STATUS_LABELS = {"running": "运行中", "completed": "已完成", "failed": "失败", "cancelled": "已取消"}
15
+ ORIGIN_LABELS = {"agent": "agent 工具", "skill": "Skill fork", "hook": "Hook"}
16
+ CANCEL_WAIT = 5.0
17
+
18
+
19
+ @dataclass
20
+ class Task:
21
+ id: str
22
+ origin: Literal["agent", "skill", "hook"]
23
+ agent: Literal["fork", "subagent"]
24
+ agent_type: str
25
+ description: str
26
+ prompt: str
27
+ call_id: str = ""
28
+ status: Literal["running", "completed", "failed", "cancelled"] = "running"
29
+ background: bool = False
30
+ started: float = field(default_factory=time.monotonic)
31
+ ended: float | None = None
32
+ usage: UsageTotals = field(default_factory=UsageTotals)
33
+ tool_calls: int = 0
34
+ last_tool: str = ""
35
+ result: str | None = None
36
+ transcript: Path | None = None
37
+ notified: bool = False
38
+ stopped: bool = False # Stopped by /tasks stop or task_stop: never notified.
39
+ job: asyncio.Task | None = field(default=None, repr=False)
40
+ detach: asyncio.Event = field(default_factory=asyncio.Event, repr=False)
41
+ policy: object = field(default=None, repr=False) # The PolicyView, switched to non-interactive on detach.
42
+ isolated: bool = False # Runs in a worktree (asked for and not skipped by plan mode).
43
+ root: Path | None = None # The worktree directory it works in; None for the project.
44
+ # {"name", "path", "branch", "base", "outcome"}; outcome is 使用中 / 已保留 / 已删除 / 删除未完成.
45
+ worktree: dict | None = None
46
+
47
+ @property
48
+ def elapsed(self) -> float:
49
+ return (self.ended if self.ended is not None else time.monotonic()) - self.started
50
+
51
+ @property
52
+ def tokens(self) -> int:
53
+ return self.usage.input_tokens + self.usage.output_tokens
54
+
55
+ @property
56
+ def label(self) -> str:
57
+ return f"{self.agent_type} · {self.description}"
58
+
59
+ def to_background(self) -> None:
60
+ self.background = True
61
+ if self.policy is not None:
62
+ self.policy.to_background()
63
+
64
+
65
+ def task_line(task: Task) -> str:
66
+ """The line shown when a background task starts or ends."""
67
+ state = "已启动" if task.status == "running" else STATUS_LABELS[task.status]
68
+ return f"后台任务 {task.id}({task.label}){state}"
69
+
70
+
71
+ class Announcer:
72
+ """One line when a background task starts and one when it ends, however often on_change fires between.
73
+
74
+ Hook tasks are left out: the hook engine reports them.
75
+ """
76
+
77
+ def __init__(self, say: Callable[[str], None]):
78
+ self.say = say
79
+ self.started: set[str] = set()
80
+ self.ended: set[str] = set()
81
+
82
+ def __call__(self, task: Task) -> None:
83
+ if not task.background or task.origin == "hook":
84
+ return
85
+ if task.id not in self.started:
86
+ self.started.add(task.id)
87
+ if task.status == "running":
88
+ self.say(task_line(task))
89
+ if task.status != "running" and task.id not in self.ended:
90
+ self.ended.add(task.id)
91
+ self.say(task_line(task))
92
+
93
+
94
+ class TaskManager:
95
+ def __init__(self, *, max_background: int = 4):
96
+ self.tasks: dict[str, Task] = {}
97
+ self.max_background = max_background
98
+ self._next = 0
99
+ self.on_finish: Callable[[Task], None] | None = None
100
+ self.on_change: Callable[[Task], None] | None = None # The UI refreshes a tool row or the status bar.
101
+ self.finished = asyncio.Event() # Set whenever a task ends; -p waits on it.
102
+
103
+ def new(self, **fields) -> Task:
104
+ self._next += 1
105
+ task = Task(f"t{self._next}", **fields)
106
+ self.tasks[task.id] = task
107
+ return task
108
+
109
+ def get(self, task_id: str) -> Task | None:
110
+ return self.tasks.get(task_id)
111
+
112
+ def running(self) -> list[Task]:
113
+ return [task for task in self.tasks.values() if task.status == "running"]
114
+
115
+ def background(self) -> list[Task]:
116
+ return [task for task in self.running() if task.background]
117
+
118
+ def full(self) -> bool:
119
+ return len(self.background()) >= self.max_background
120
+
121
+ def changed(self, task: Task) -> None:
122
+ if self.on_change is not None:
123
+ try:
124
+ self.on_change(task)
125
+ except Exception:
126
+ pass
127
+
128
+ def launch(self, task: Task, run: Callable[[], Awaitable[str]]) -> asyncio.Task:
129
+ task.job = asyncio.get_running_loop().create_task(self._run(task, run))
130
+ self.changed(task)
131
+ return task.job
132
+
133
+ async def _run(self, task: Task, run) -> str:
134
+ try:
135
+ text = await run()
136
+ except asyncio.CancelledError:
137
+ text = task.result or "[子 Agent 未完成:已取消]"
138
+ task.status = "cancelled"
139
+ except Exception as exc: # run() turns provider errors into text; anything else is a bug.
140
+ text = f"[子 Agent 未完成:内部错误 {type(exc).__name__}]"
141
+ task.status = "failed"
142
+ self.settle(task, text)
143
+ return text
144
+
145
+ def settle(self, task: Task, text: str) -> None:
146
+ """The end of a task, also of one run outside launch (a Skill fork)."""
147
+ task.result = text
148
+ task.ended = time.monotonic()
149
+ if task.status == "running":
150
+ task.status = "completed"
151
+ self.finished.set()
152
+ self.changed(task)
153
+ if self.on_finish is not None:
154
+ try:
155
+ self.on_finish(task)
156
+ except Exception:
157
+ pass
158
+
159
+ def detach_foreground(self) -> int:
160
+ """ctrl+b moves every running foreground sub-agent of the agent tool to the background."""
161
+ count = 0
162
+ for task in self.running():
163
+ if not task.background and task.origin == "agent" and not task.detach.is_set():
164
+ task.detach.set()
165
+ count += 1
166
+ return count
167
+
168
+ async def stop(self, task_id: str) -> bool:
169
+ """True when it was running; its notification is never sent."""
170
+ task = self.tasks.get(task_id)
171
+ if task is None or task.status != "running" or task.job is None:
172
+ return False
173
+ task.stopped = task.notified = True
174
+ task.job.cancel()
175
+ await asyncio.gather(task.job, return_exceptions=True)
176
+ return True
177
+
178
+ async def cancel_all(self, wait: float = CANCEL_WAIT) -> int:
179
+ """On exit and when the user confirms: cancel, wait a little, never notify."""
180
+ jobs = []
181
+ for task in self.running():
182
+ task.stopped = task.notified = True
183
+ if task.job is not None:
184
+ task.job.cancel()
185
+ jobs.append(task.job)
186
+ if jobs:
187
+ await asyncio.wait(jobs, timeout=wait)
188
+ return len(jobs)
189
+
190
+ async def reset(self) -> None:
191
+ """/clear and resume start with an empty list."""
192
+ await self.cancel_all()
193
+ self.tasks.clear()
mosscode/appearance.py ADDED
@@ -0,0 +1,184 @@
1
+ """Terminal colors and the mascot of the startup banner."""
2
+
3
+ from importlib.metadata import version
4
+ from pathlib import Path
5
+
6
+ from rich.style import Style
7
+ from rich.table import Table
8
+ from rich.text import Text
9
+ from textual.theme import Theme
10
+
11
+ TEXT_COLOR = "ansi_default"
12
+ TOOL_SUCCESS_COLOR = "#8bd5a6"
13
+ PARAMETER_COLOR = "#6fa8ff"
14
+ OPERATION_COLOR = "#e3b95f"
15
+ ERROR_COLOR = "#e87983"
16
+ COMMAND_HIGHLIGHT_COLOR = "#3673ff"
17
+ # User messages sit on a grey bar, as in Claude Code: a mid grey with near-white
18
+ # text stands out on both dark and light terminal backgrounds.
19
+ USER_MESSAGE_BACKGROUND = "#4a4a4a"
20
+ USER_MESSAGE_COLOR = "#f2f2f2"
21
+
22
+ # The mascot, a little ghost, as pixels: two pixel rows make one line of half blocks, so each pixel is
23
+ # about square. "." is the terminal's own background; the other letters are MASCOT_COLORS.
24
+ MASCOT_PIXELS = (
25
+ "....wwwwww....",
26
+ "..wwwwwwwwww..",
27
+ ".wwwwwwwwwwww.",
28
+ ".wwkhwwwwkhww.",
29
+ ".wwkkwwwwkkww.",
30
+ ".wrrwwkkwwrrw.",
31
+ ".wwwwwwwwwwww.",
32
+ ".wwwwwwwwwwww.",
33
+ ".ww.www.www.w.",
34
+ ".w...w...w....",
35
+ )
36
+ MASCOT_COLORS = {"w": "#eef1ff", "k": "#26212e", "h": "#ffffff", "r": "#ff9fb2"}
37
+
38
+
39
+ # Transparent TCSS inherits the parent's painted background. ANSI default means
40
+ # the terminal's own background, and must be preserved by ansi_color=True.
41
+ TERMINAL_CSS = """
42
+ App, Screen, * {
43
+ color: ansi_default !important;
44
+ text-opacity: 100% !important;
45
+ opacity: 1 !important;
46
+ background: ansi_default !important;
47
+ background-tint: transparent !important;
48
+ tint: transparent !important;
49
+ scrollbar-background: ansi_default;
50
+ scrollbar-background-hover: ansi_default;
51
+ scrollbar-background-active: ansi_default;
52
+ scrollbar-corner-color: ansi_default;
53
+ }
54
+ Input > .input--placeholder, Input > .input--suggestion {
55
+ color: ansi_default !important;
56
+ text-style: none !important;
57
+ }
58
+ Markdown MarkdownBlock > .code_inline {
59
+ background: ansi_default !important;
60
+ }
61
+ Input > .input--cursor {
62
+ color: $foreground;
63
+ background: ansi_default !important;
64
+ text-style: reverse;
65
+ }
66
+ Input > .input--selection {
67
+ color: $primary;
68
+ background: ansi_default !important;
69
+ text-style: bold underline;
70
+ }
71
+ OptionList:focus > .option-list--option-highlighted {
72
+ color: $primary;
73
+ background: ansi_default !important;
74
+ text-style: bold;
75
+ }
76
+ """
77
+
78
+
79
+ def moss_themes() -> tuple[Theme, ...]:
80
+ # Use the terminal's full-strength foreground; never dim supporting text.
81
+ variables = {
82
+ name: TEXT_COLOR
83
+ for name in (
84
+ "text",
85
+ "text-muted",
86
+ "text-disabled",
87
+ "foreground-muted",
88
+ "input-cursor-foreground",
89
+ "link-color",
90
+ "link-color-hover",
91
+ )
92
+ }
93
+ variables["input-cursor-background"] = "ansi_default"
94
+ return (
95
+ Theme(
96
+ name="moss-dark",
97
+ primary=TEXT_COLOR,
98
+ secondary=TEXT_COLOR,
99
+ accent=TEXT_COLOR,
100
+ foreground=TEXT_COLOR,
101
+ background="#0c0c0c",
102
+ surface="#0c0c0c",
103
+ panel="#0c0c0c",
104
+ success=TEXT_COLOR,
105
+ warning=TEXT_COLOR,
106
+ error=TEXT_COLOR,
107
+ dark=True,
108
+ variables=variables,
109
+ ),
110
+ )
111
+
112
+
113
+ def pixel_art(pixels: tuple[str, ...], colors: dict[str, str]) -> Text:
114
+ """Each pair of pixel rows as one line: ▀ in the upper color over the lower one as background."""
115
+ art = Text(no_wrap=True)
116
+ for index, (upper, lower) in enumerate(zip(pixels[::2], pixels[1::2])):
117
+ if index:
118
+ art.append("\n")
119
+ for top, bottom in zip(upper, lower):
120
+ top, bottom = colors.get(top), colors.get(bottom)
121
+ if top and top == bottom:
122
+ art.append("█", Style(color=top))
123
+ elif top:
124
+ art.append("▀", Style(color=top, bgcolor=bottom))
125
+ elif bottom:
126
+ art.append("▄", Style(color=bottom))
127
+ else:
128
+ art.append(" ")
129
+ return art
130
+
131
+
132
+ def startup_banner(
133
+ provider: str,
134
+ ) -> Table:
135
+ logo = pixel_art(MASCOT_PIXELS, MASCOT_COLORS)
136
+ info = Text(style="default", no_wrap=True, overflow="ellipsis")
137
+ info.append("MossCode", style=Style(bold=True))
138
+ info.append(f" v{version('mosscode')}\n")
139
+ info.append(provider)
140
+ info.append("\n" + str(Path.cwd()))
141
+ banner = Table.grid(padding=(0, 2), expand=True)
142
+ banner.add_column(width=len(MASCOT_PIXELS[0]), no_wrap=True)
143
+ banner.add_column(ratio=1, no_wrap=True, overflow="ellipsis", vertical="middle")
144
+ banner.add_row(logo, info)
145
+ return banner
146
+
147
+
148
+ def compact_notice(event) -> str:
149
+ """One line for a finished or failed compaction, shared by the TUI and headless text output."""
150
+ if hasattr(event, "removed"):
151
+ return (
152
+ f"✓ 已压缩上下文 · {event.removed} 条消息 → 摘要 · 约 {event.summary_tokens} Token"
153
+ f" · 用时 {event.elapsed:.1f}s"
154
+ )
155
+ if event.code == "nothing":
156
+ return "└ " + event.reason
157
+ if event.code == "restore":
158
+ return "! " + event.reason
159
+ if event.manual:
160
+ return "! 压缩上下文失败:" + event.reason
161
+ notice = f"! 自动压缩上下文失败(连续 {event.consecutive} 次):{event.reason}"
162
+ if event.paused:
163
+ notice += "\n 自动压缩已暂停,可以 /compact 手动重试或 /clear"
164
+ return notice
165
+
166
+
167
+ # The key half of /help; the command half comes from the registry.
168
+ KEY_HELP = """按键
169
+ Enter 发送消息或执行命令
170
+ Shift+Enter / Ctrl+J 换行(也可在行尾输入 \\ 再按 Enter)
171
+ ↑ / ↓ 在命令提示中选择;首行/末行时浏览本次输入历史
172
+ Tab 补全命令或参数
173
+ Shift+Tab 依次切换 default → acceptEdits → plan
174
+ 运行中 Enter 排队消息,本轮工具执行后发送给模型
175
+ Esc 关闭命令提示(保留输入)
176
+ Esc / Ctrl+C 取消正在生成的回复;排队消息放回输入框
177
+ 授权面板 ↑ / ↓ 选择,Enter 确认,Esc 拒绝本次,Ctrl+C 取消任务
178
+ Ctrl+B 把前台运行的子 Agent 转入后台
179
+ Ctrl+T 展开团队成员面板:Enter 查看对话,x 停止成员
180
+ 成员视图 PgUp / PgDn 滚动,← / → 切换成员,Esc 返回
181
+ Ctrl+Q 退出(有后台任务时先确认)
182
+ 鼠标滚轮 滚动对话记录
183
+ 鼠标拖选 使用终端原生文字选择
184
+ Ctrl+V 粘贴(多行内容保持原样)"""