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/cli.py ADDED
@@ -0,0 +1,547 @@
1
+ import argparse
2
+ import asyncio
3
+ import json
4
+ import os
5
+ import sys
6
+ from contextlib import aclosing
7
+ from pathlib import Path
8
+ from time import monotonic
9
+
10
+ from .agents import AgentCatalog
11
+ from .agents.tasks import Announcer
12
+ from .appearance import compact_notice
13
+ from .builtin_commands import BUILTIN, typed
14
+ from .commands import CommandRegistry, argument_problem, parse_input, unknown_note
15
+ from .config import ConfigError, find_config, load_settings
16
+ from .conversation import Conversation, Notice, Resumed
17
+ from .coordinator import decide as decide_coordinator
18
+ from .hooks import HookEngine
19
+ from .launcher import is_terminal, parse_arguments
20
+ from .mcp_client import MCPManager
21
+ from .mcp_config import MCPConfigError, load_mcp_settings
22
+ from .memory import MemoryStore
23
+ from .messages import (
24
+ CompactFailed,
25
+ CompactFinished,
26
+ CompactStarted,
27
+ Completed,
28
+ Delta,
29
+ Message,
30
+ ProviderError,
31
+ ResponseBoundary,
32
+ Retrying,
33
+ RunStopped,
34
+ ToolProgress,
35
+ UsageUpdate,
36
+ )
37
+ from .permissions import PermissionConfigError
38
+ from .providers import create_provider
39
+ from .session import Session, prune_all
40
+ from .skills import ForkRequest, SkillCatalog
41
+ from .tools import create_default_registry
42
+ from .tools.shell import ShellNotFound
43
+ from .tui import MossCodeApp, ProviderSelectApp, plan_text
44
+ from .usage import request_usage_text
45
+
46
+
47
+ def _headless_input(
48
+ text: str, commands: CommandRegistry = BUILTIN, notices: tuple[str, ...] = ()
49
+ ) -> tuple[str, str | None, str | None]:
50
+ """-p sends messages, prompt commands and skills; returns (prompt, command, skill) or exits with 2.
51
+
52
+ For a skill the prompt is its argument: the request is rendered once the tools are known.
53
+ """
54
+ parsed = parse_input(text, commands)
55
+ if parsed.kind in {"empty", "message"}:
56
+ return (parsed.text if parsed.kind == "message" else text), None, None
57
+ if parsed.kind == "unknown":
58
+ problem = unknown_note(parsed, commands)
59
+ elif parsed.command.kind == "local":
60
+ problem = f"/{parsed.command.name} 只能在交互界面中使用。"
61
+ else:
62
+ problem = argument_problem(parsed.command, parsed.argument)
63
+ if problem:
64
+ for notice in notices: # Say why a skill is missing before the run ends here.
65
+ print(notice, file=sys.stderr)
66
+ print(problem, file=sys.stderr)
67
+ raise SystemExit(2)
68
+ if parsed.command.kind == "skill":
69
+ return parsed.argument, typed(parsed.command, parsed.argument), parsed.command.skill
70
+ return parsed.command.expand(parsed.argument), typed(parsed.command, parsed.argument), None
71
+
72
+
73
+ def _skill_prompt(conversation: Conversation, skill: str | None, prompt: str, report):
74
+ """Check the skill's tools once MCP is up, then send it like the TUI does; returns (prompt, fork)."""
75
+ if skill is None:
76
+ return prompt, None
77
+ found, rendered, notices = conversation.prepare_skill(skill, prompt)
78
+ for notice in notices:
79
+ report("notice", notice)
80
+ if found.mode != "fork":
81
+ return rendered, None
82
+ report("notice", f"在独立上下文中运行 Skill {found.name}")
83
+ return rendered, ForkRequest(found, rendered)
84
+
85
+
86
+ def _arguments(arguments: str):
87
+ try:
88
+ return json.loads(arguments)
89
+ except ValueError:
90
+ return arguments
91
+
92
+
93
+ def _assistant(message: Message, usage, stop_reason: str | None) -> dict:
94
+ return {
95
+ "type": "assistant",
96
+ "text": message.text,
97
+ "tool_calls": [{"id": c.id, "name": c.name, "arguments": _arguments(c.arguments)} for c in message.tool_calls],
98
+ "stop_reason": stop_reason,
99
+ "usage": _usage_dict(usage),
100
+ }
101
+
102
+
103
+ def _usage_dict(usage) -> dict | None:
104
+ if usage is None:
105
+ return None
106
+ return {
107
+ "input_tokens": usage.input_tokens,
108
+ "output_tokens": usage.output_tokens,
109
+ "cached_input_tokens": usage.cached_input_tokens,
110
+ "cache_write_tokens": usage.cache_write_tokens,
111
+ }
112
+
113
+
114
+ def _emit(data: dict) -> None:
115
+ print(json.dumps(data, ensure_ascii=False), flush=True)
116
+
117
+
118
+ def _compact_event(event) -> dict:
119
+ if isinstance(event, CompactStarted):
120
+ return {"type": "compact", "subtype": "started", "manual": event.manual}
121
+ if isinstance(event, CompactFinished):
122
+ return {
123
+ "type": "compact",
124
+ "subtype": "finished",
125
+ "removed": event.removed,
126
+ "summary_tokens": event.summary_tokens,
127
+ "duration_ms": round(event.elapsed * 1000),
128
+ "record": event.record,
129
+ }
130
+ return {
131
+ "type": "compact",
132
+ "subtype": "failed",
133
+ "reason": event.reason,
134
+ "consecutive": event.consecutive,
135
+ "paused": event.paused,
136
+ }
137
+
138
+
139
+ async def _resume(conversation: Conversation, session_id: str | None, report) -> None:
140
+ """Restore before the task; report(kind, value) prints notices and compaction progress."""
141
+ if session_id is None:
142
+ return
143
+ async with aclosing(conversation.resume(session_id)) as stream:
144
+ async for event in stream:
145
+ if isinstance(event, Resumed):
146
+ restored = event.restored
147
+ report("notice", f"已恢复会话 {restored.id} · {restored.title} · {event.messages} 条消息")
148
+ for notice in event.notices:
149
+ report("notice", notice)
150
+ for text in restored.unanswered:
151
+ # Never resent automatically; shown so the user can decide.
152
+ report("unanswered", text)
153
+ elif isinstance(event, Notice):
154
+ report("notice", event.text)
155
+ else:
156
+ report("compact", event)
157
+
158
+
159
+ def _background_notices(conversation: Conversation, report) -> None:
160
+ """Hook failures, background ones included, go to stderr (stream-json: a notice event)."""
161
+ conversation.on_memory = lambda event: report("notice", event.text) if isinstance(event, Notice) else None
162
+
163
+
164
+ def _task_notices(conversation: Conversation, report) -> None:
165
+ """A line on stderr (stream-json: a notice event) when a background task starts or ends."""
166
+ conversation.tasks.on_change = Announcer(lambda text: report("notice", text))
167
+
168
+
169
+ async def _turns(conversation: Conversation, prompt: str, *, command: str | None, fork):
170
+ """The task's turn, then one turn per batch of notifications until no task is left running."""
171
+ async with aclosing(conversation.reply(prompt, command=command, fork=fork)) as stream:
172
+ async for event in stream:
173
+ yield event
174
+ tasks = conversation.tasks
175
+ while True:
176
+ if conversation.notifications:
177
+ async with aclosing(conversation.notify()) as stream:
178
+ async for event in stream:
179
+ yield event
180
+ continue
181
+ if not tasks.running():
182
+ return
183
+ tasks.finished.clear()
184
+ await tasks.finished.wait()
185
+
186
+
187
+ def _hook_engine(args, registry, *, interactive: bool):
188
+ """--no-hooks loads nothing; -p skips untrusted project files unless --trust-project-hooks."""
189
+ tool = registry.get("execute_command")
190
+ engine = HookEngine(
191
+ Path.cwd(),
192
+ shell=getattr(tool, "shell", None),
193
+ enabled=not args.no_hooks,
194
+ trust_project=args.trust_project_hooks,
195
+ )
196
+ notices = list(engine.problems)
197
+ if not interactive:
198
+ notices += [
199
+ f"项目 Hook 文件 {file.path} 未经确认,未启用;请在交互界面中启动一次并确认,或使用 --trust-project-hooks。"
200
+ for file in engine.untrusted
201
+ ]
202
+ return engine, tuple(notices)
203
+
204
+
205
+ async def _prompt(
206
+ conversation: Conversation,
207
+ prompt: str,
208
+ *,
209
+ show_usage: bool = False,
210
+ output_format: str = "text",
211
+ model: str = "",
212
+ resume: str | None = None,
213
+ command: str | None = None,
214
+ skill: str | None = None,
215
+ ) -> dict | None:
216
+ """Run one headless task; JSON formats return the result object instead of raising."""
217
+ if output_format == "text":
218
+ await _text_prompt(conversation, prompt, show_usage=show_usage, resume=resume, command=command, skill=skill)
219
+ return None
220
+ stream_json = output_format == "stream-json"
221
+ started = monotonic()
222
+ stopped: RunStopped | None = None
223
+ final = ""
224
+ error: str | None = None
225
+
226
+ def report(kind, value):
227
+ if kind == "compact":
228
+ if stream_json:
229
+ _emit(_compact_event(value))
230
+ elif stream_json:
231
+ _emit({"type": "notice", "text": value if kind == "notice" else "未得到回复的消息:" + value})
232
+ else:
233
+ print(value if kind == "notice" else "未得到回复的消息:" + value, file=sys.stderr, flush=True)
234
+
235
+ _background_notices(conversation, report)
236
+ _task_notices(conversation, report)
237
+ try:
238
+ try:
239
+ if stream_json:
240
+ _emit(
241
+ {
242
+ "type": "session",
243
+ "id": conversation.session_id if resume is None else resume,
244
+ "resumed": resume is not None,
245
+ }
246
+ )
247
+ await _resume(conversation, resume, report)
248
+ notices = await conversation.start()
249
+ for notice in notices:
250
+ if stream_json:
251
+ _emit({"type": "notice", "text": notice})
252
+ else:
253
+ print(notice, file=sys.stderr, flush=True)
254
+ prompt, fork = _skill_prompt(conversation, skill, prompt, report)
255
+ registry = conversation.registry
256
+ if stream_json:
257
+ _emit(
258
+ {
259
+ "type": "system",
260
+ "subtype": "init",
261
+ "cwd": str(Path.cwd()),
262
+ "model": model,
263
+ "permission_mode": registry.permissions.mode if registry else None,
264
+ "tools": [d.name for d in registry.definitions()] if registry else [],
265
+ }
266
+ )
267
+ usage = None
268
+ async with aclosing(_turns(conversation, prompt, command=command, fork=fork)) as stream:
269
+ async for event in stream:
270
+ if isinstance(event, UsageUpdate):
271
+ usage = event.usage
272
+ elif isinstance(event, RunStopped):
273
+ stopped = event
274
+ elif isinstance(event, Completed):
275
+ final = event.message.text
276
+ if not stream_json:
277
+ continue
278
+ if isinstance(event, ResponseBoundary):
279
+ _emit(_assistant(event.message, usage, "tool_use"))
280
+ elif isinstance(event, Completed):
281
+ _emit(_assistant(event.message, usage, event.stop_reason))
282
+ elif isinstance(event, ToolProgress) and event.phase == "finished":
283
+ result = event.result
284
+ _emit(
285
+ {
286
+ "type": "tool_result",
287
+ "id": event.call.id,
288
+ "name": event.call.name,
289
+ "is_error": result.is_error,
290
+ "code": result.code,
291
+ "output": result.output,
292
+ }
293
+ )
294
+ elif isinstance(event, Retrying):
295
+ _emit(
296
+ {
297
+ "type": "retry",
298
+ "attempt": event.attempt,
299
+ "max_attempts": event.max_attempts,
300
+ "delay": event.delay,
301
+ "reason": event.reason,
302
+ }
303
+ )
304
+ elif isinstance(event, (CompactStarted, CompactFinished, CompactFailed)):
305
+ _emit(_compact_event(event))
306
+ elif isinstance(event, Notice):
307
+ _emit({"type": "notice", "text": event.text})
308
+ except ProviderError as exc:
309
+ error = str(exc)
310
+ registry = conversation.registry
311
+ total = conversation.total_usage
312
+ subtype = "error" if error is not None else stopped.reason if stopped else "error"
313
+ result = {
314
+ "type": "result",
315
+ "subtype": subtype,
316
+ "is_error": subtype != "completed",
317
+ "result": final if subtype == "completed" else error or (stopped.notice if stopped else "没有完整回复。"),
318
+ "num_turns": stopped.iterations if stopped else 0,
319
+ "duration_ms": round((monotonic() - started) * 1000),
320
+ "usage": {
321
+ "input_tokens": total.input_tokens,
322
+ "output_tokens": total.output_tokens,
323
+ "cached_input_tokens": total.cached_input_tokens,
324
+ "cache_write_tokens": total.cache_write_tokens,
325
+ "requests": total.requests,
326
+ },
327
+ "compactions": conversation.compactions,
328
+ "session_id": conversation.session_id,
329
+ "model": model,
330
+ "permission_mode": registry.permissions.mode if registry else None,
331
+ }
332
+ _emit(result)
333
+ return result
334
+ finally:
335
+ await conversation.close()
336
+
337
+
338
+ async def _text_prompt(
339
+ conversation: Conversation,
340
+ prompt: str,
341
+ *,
342
+ show_usage: bool = False,
343
+ resume: str | None = None,
344
+ command: str | None = None,
345
+ skill: str | None = None,
346
+ ) -> None:
347
+ def report(kind, value):
348
+ if kind == "compact":
349
+ print(
350
+ "正在压缩上下文…" if isinstance(value, CompactStarted) else compact_notice(value),
351
+ file=sys.stderr,
352
+ flush=True,
353
+ )
354
+ else:
355
+ print(value if kind == "notice" else "未得到回复的消息:" + value, file=sys.stderr, flush=True)
356
+
357
+ _background_notices(conversation, report)
358
+ _task_notices(conversation, report)
359
+ try:
360
+ await _resume(conversation, resume, report)
361
+ for notice in await conversation.start():
362
+ print(notice, file=sys.stderr, flush=True)
363
+ prompt, fork = _skill_prompt(conversation, skill, prompt, report)
364
+ async with aclosing(_turns(conversation, prompt, command=command, fork=fork)) as stream:
365
+ async for event in stream:
366
+ if isinstance(event, Delta) and event.kind == "text":
367
+ print(event.text, end="", flush=True)
368
+ elif isinstance(event, ResponseBoundary):
369
+ print()
370
+ elif (
371
+ isinstance(event, ToolProgress) and event.phase == "running" and event.call.name == "exit_plan_mode"
372
+ ):
373
+ print(f"\n计划:\n{plan_text(event.call.arguments)}", flush=True)
374
+ elif isinstance(event, ToolProgress) and event.phase == "finished":
375
+ print(f"\n● {event.call.name} · {event.elapsed:.1f}s\n└ {event.result.output}", flush=True)
376
+ elif isinstance(event, RunStopped) and event.reason in {
377
+ "max_iterations",
378
+ "unknown_tools",
379
+ "max_tokens",
380
+ }:
381
+ print("\n" + event.notice, flush=True)
382
+ elif isinstance(event, Retrying):
383
+ print(
384
+ f"{event.reason},{event.delay:.1f} 秒后重试({event.attempt}/{event.max_attempts})",
385
+ file=sys.stderr,
386
+ flush=True,
387
+ )
388
+ elif isinstance(event, UsageUpdate) and show_usage:
389
+ print(request_usage_text(event.usage, event.iteration), file=sys.stderr, flush=True)
390
+ elif isinstance(event, CompactStarted):
391
+ print("正在压缩上下文…", file=sys.stderr, flush=True)
392
+ elif isinstance(event, (CompactFinished, CompactFailed)):
393
+ print(compact_notice(event), file=sys.stderr, flush=True)
394
+ elif isinstance(event, Notice):
395
+ print(event.text, file=sys.stderr, flush=True)
396
+ print()
397
+ finally:
398
+ await conversation.close()
399
+
400
+
401
+ def _memory_options(settings, *, interactive: bool) -> dict:
402
+ """Notes and the memory tool everywhere; background extraction only in the TUI."""
403
+ if not settings.memory:
404
+ return {}
405
+ options = {"memory": MemoryStore.for_project(Path.cwd()), "auto_memory": interactive and settings.auto_memory}
406
+ if options["auto_memory"] and settings.memory_provider:
407
+ options["memory_provider"] = create_provider(settings.select(settings.memory_provider))
408
+ return options
409
+
410
+
411
+ def _config_path(args) -> Path | None:
412
+ """No configuration in an interactive terminal starts the setup; None when it was left."""
413
+ if args.config is not None:
414
+ return Path(args.config)
415
+ try:
416
+ return find_config()
417
+ except ConfigError:
418
+ if args.prompt is not None or args.list_providers or not (is_terminal(sys.stdin) and is_terminal(sys.stdout)):
419
+ raise
420
+ from .setup import run_setup
421
+
422
+ return run_setup()
423
+
424
+
425
+ def main(argv: list[str] | None = None) -> None:
426
+ """The arguments as launcher.main parses them, without its terminal checks (tests call this)."""
427
+ run(parse_arguments(argv))
428
+
429
+
430
+ def run(args: argparse.Namespace) -> None:
431
+ if args.team_dir is not None:
432
+ from .teams.process import run_member
433
+
434
+ raise SystemExit(run_member(args.team_dir, args.team_member, args.config, args.provider))
435
+ # Skills are scanned first so that -p "/<name>" parses.
436
+ skills = SkillCatalog.load(Path.cwd(), reserved=BUILTIN.names())
437
+ commands, skipped = BUILTIN.with_skills(skills)
438
+ prompt, command, skill = (
439
+ _headless_input(args.prompt, commands, skills.problems + skipped)
440
+ if args.prompt is not None
441
+ else (None, None, None)
442
+ )
443
+ try:
444
+ path = _config_path(args)
445
+ if path is None:
446
+ return
447
+ settings = load_settings(path)
448
+ if args.list_providers:
449
+ for item in settings.providers:
450
+ print(f"{item.name}: {item.protocol} / {item.model}")
451
+ return
452
+ config = settings.select(args.provider)
453
+ if (
454
+ args.prompt is None
455
+ and args.provider is None
456
+ and settings.default_provider is None
457
+ and len(settings.providers) > 1
458
+ ):
459
+ config = ProviderSelectApp(settings).run(inline=True, mouse=False)
460
+ if config is None:
461
+ return
462
+ registry = create_default_registry(permission_mode=args.permission_mode, shell=settings.shell)
463
+ mcp_settings = load_mcp_settings(Path.cwd())
464
+ manager = MCPManager(Path.cwd(), mcp_settings) if mcp_settings.servers or mcp_settings.notices else None
465
+ prune_all()
466
+ budget = config.budget()
467
+ hooks, hook_notices = _hook_engine(args, registry, interactive=args.prompt is None)
468
+ notices = skills.problems + skipped + hook_notices
469
+ coordinator, coordinator_notice = decide_coordinator(
470
+ settings.coordinator_mode, os.environ, interactive=args.prompt is None
471
+ )
472
+ if coordinator_notice:
473
+ notices += (coordinator_notice,)
474
+ if not budget.configured:
475
+ notices += (f"{config.name} 未配置 context_window,按 128K 估算上下文。",)
476
+ conversation = Conversation(
477
+ create_provider(config),
478
+ settings.system_prompt,
479
+ registry,
480
+ mcp_manager=manager,
481
+ max_iterations=settings.max_iterations,
482
+ context_root=Path.cwd(),
483
+ budget=budget,
484
+ session=Session.new(Path.cwd()),
485
+ auto_compact=settings.auto_compact,
486
+ notices=notices,
487
+ persist=settings.persist_sessions,
488
+ skills=skills,
489
+ provider_configs=settings.providers,
490
+ provider_factory=create_provider,
491
+ provider_info=(config.name, config.protocol, config.model),
492
+ hooks=hooks,
493
+ agents=AgentCatalog.load(Path.cwd()),
494
+ agent_auto_background=settings.agent_auto_background,
495
+ agent_max_background=settings.agent_max_background,
496
+ worktree_links=settings.worktree_links,
497
+ worktree_stale_hours=settings.worktree_stale_hours,
498
+ teams=args.prompt is None,
499
+ team_max_members=settings.team_max_members,
500
+ teammate_mode=settings.teammate_mode,
501
+ team_launch={"config": settings.source, "provider": config.name},
502
+ coordinator=coordinator,
503
+ **_memory_options(settings, interactive=args.prompt is None),
504
+ )
505
+ resume = args.resume
506
+ if args.continue_session or resume:
507
+ resume = conversation.resolve_session(resume or None)
508
+ if args.prompt is not None:
509
+ if args.output_format != "text" and hasattr(sys.stdout, "reconfigure"):
510
+ # Machine-readable output is always UTF-8, whatever the console code page.
511
+ sys.stdout.reconfigure(encoding="utf-8")
512
+ result = asyncio.run(
513
+ _prompt(
514
+ conversation,
515
+ prompt,
516
+ show_usage=args.show_usage,
517
+ output_format=args.output_format,
518
+ model=config.model,
519
+ resume=resume,
520
+ command=command,
521
+ skill=skill,
522
+ )
523
+ )
524
+ if result is not None and result["is_error"]:
525
+ raise SystemExit(1)
526
+ else:
527
+ if resume == "" and conversation.unavailable():
528
+ raise ProviderError(conversation.unavailable())
529
+ MossCodeApp(
530
+ conversation,
531
+ config.name,
532
+ settings=settings,
533
+ model=config.model,
534
+ resume=resume,
535
+ commands=commands,
536
+ initial_prompt=args.initial,
537
+ ).run(
538
+ inline=True,
539
+ inline_no_clear=True,
540
+ mouse=False,
541
+ )
542
+ except (ConfigError, ProviderError, PermissionConfigError, MCPConfigError, ShellNotFound) as exc:
543
+ print(f"MossCode: {exc}", file=sys.stderr)
544
+ raise SystemExit(1) from None
545
+ except KeyboardInterrupt:
546
+ print("\n已取消。", file=sys.stderr)
547
+ raise SystemExit(130) from None