comate-cli 0.1.0__tar.gz → 0.1.3__tar.gz

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 (76) hide show
  1. {comate_cli-0.1.0 → comate_cli-0.1.3}/PKG-INFO +2 -2
  2. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/main.py +48 -8
  3. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/app.py +131 -15
  4. comate_cli-0.1.3/comate_cli/terminal_agent/preflight.py +1054 -0
  5. comate_cli-0.1.3/comate_cli/terminal_agent/resume_selector.py +288 -0
  6. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/rpc_stdio.py +133 -53
  7. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/selection_menu.py +87 -4
  8. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui.py +76 -10
  9. {comate_cli-0.1.0 → comate_cli-0.1.3}/pyproject.toml +2 -2
  10. comate_cli-0.1.3/tests/conftest.py +14 -0
  11. comate_cli-0.1.3/tests/test_app_preflight_gate.py +32 -0
  12. comate_cli-0.1.3/tests/test_app_shutdown.py +86 -0
  13. comate_cli-0.1.3/tests/test_app_usage_line.py +60 -0
  14. comate_cli-0.1.3/tests/test_cli_project_root.py +31 -0
  15. comate_cli-0.1.3/tests/test_compact_command_semantics.py +92 -0
  16. comate_cli-0.1.3/tests/test_completion_status_panel.py +209 -0
  17. comate_cli-0.1.3/tests/test_context_command.py +94 -0
  18. comate_cli-0.1.3/tests/test_event_renderer.py +401 -0
  19. comate_cli-0.1.3/tests/test_history_sync.py +156 -0
  20. comate_cli-0.1.3/tests/test_input_behavior.py +55 -0
  21. comate_cli-0.1.3/tests/test_layout_coordinator.py +76 -0
  22. comate_cli-0.1.3/tests/test_main_args.py +46 -0
  23. comate_cli-0.1.3/tests/test_mention_completer.py +69 -0
  24. comate_cli-0.1.3/tests/test_preflight.py +412 -0
  25. comate_cli-0.1.3/tests/test_question_view.py +423 -0
  26. comate_cli-0.1.3/tests/test_resume_selector.py +167 -0
  27. comate_cli-0.1.3/tests/test_rewind_command_semantics.py +129 -0
  28. comate_cli-0.1.3/tests/test_rewind_store.py +195 -0
  29. comate_cli-0.1.3/tests/test_rpc_protocol.py +52 -0
  30. comate_cli-0.1.3/tests/test_rpc_stdio_bridge.py +123 -0
  31. comate_cli-0.1.3/tests/test_selection_menu.py +103 -0
  32. comate_cli-0.1.3/tests/test_slash_completer.py +59 -0
  33. comate_cli-0.1.3/tests/test_slash_registry.py +49 -0
  34. comate_cli-0.1.3/tests/test_status_bar.py +87 -0
  35. comate_cli-0.1.3/tests/test_tool_view.py +64 -0
  36. comate_cli-0.1.3/tests/test_tui_mcp_init_gate.py +88 -0
  37. comate_cli-0.1.3/tests/test_tui_paste_placeholder.py +364 -0
  38. comate_cli-0.1.3/tests/test_tui_split_invariance.py +68 -0
  39. comate_cli-0.1.3/uv.lock +2208 -0
  40. {comate_cli-0.1.0 → comate_cli-0.1.3}/.gitignore +0 -0
  41. {comate_cli-0.1.0 → comate_cli-0.1.3}/README.md +0 -0
  42. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/__init__.py +0 -0
  43. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/__main__.py +0 -0
  44. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/__init__.py +0 -0
  45. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/animations.py +0 -0
  46. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/assistant_render.py +0 -0
  47. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/env_utils.py +0 -0
  48. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/error_display.py +0 -0
  49. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/event_renderer.py +0 -0
  50. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  51. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/history_printer.py +0 -0
  52. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/input_geometry.py +0 -0
  53. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  54. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  55. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/logo.py +0 -0
  56. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/markdown_render.py +0 -0
  57. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/mention_completer.py +0 -0
  58. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/message_style.py +0 -0
  59. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/models.py +0 -0
  60. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/question_view.py +0 -0
  61. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/rewind_store.py +0 -0
  62. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  63. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/session_view.py +0 -0
  64. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/slash_commands.py +0 -0
  65. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/startup.py +0 -0
  66. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/status_bar.py +0 -0
  67. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/text_effects.py +0 -0
  68. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tool_view.py +0 -0
  69. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  70. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  71. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  72. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  73. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  74. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  75. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  76. {comate_cli-0.1.0 → comate_cli-0.1.3}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.1.0
3
+ Version: 0.1.3
4
4
  Summary: Comate terminal CLI built on comate-agent-sdk
5
5
  Project-URL: Homepage, https://github.com/AndyLee1024/agent-sdk
6
6
  Project-URL: Repository, https://github.com/AndyLee1024/agent-sdk
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Python: >=3.11
17
- Requires-Dist: comate-agent-sdk<1.0.0,>=0.0.1
17
+ Requires-Dist: comate-agent-sdk<1.0.0,>=0.0.2
18
18
  Requires-Dist: prompt-toolkit>=3.0
19
19
  Requires-Dist: rich>=14.0
20
20
  Description-Content-Type: text/markdown
@@ -12,6 +12,10 @@ import threading
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
+ class _ArgumentError(ValueError):
16
+ """Raised when CLI arguments are invalid."""
17
+
18
+
15
19
  class _TerminalStateGuard:
16
20
  """Best-effort tty restore guard for abnormal shutdown paths."""
17
21
 
@@ -88,18 +92,43 @@ class _ShutdownNoiseGuard:
88
92
  self._orig_unraisablehook(unraisable)
89
93
 
90
94
 
91
- def _parse_args(argv: list[str]) -> tuple[bool, str | None]:
95
+ def _usage_text() -> str:
96
+ return (
97
+ "Usage:\n"
98
+ " comate [--rpc-stdio]\n"
99
+ " comate resume [<session_id>] [--rpc-stdio]"
100
+ )
101
+
102
+
103
+ def _parse_args(argv: list[str]) -> tuple[bool, str | None, bool]:
92
104
  rpc_stdio = False
93
- session_id: str | None = None
105
+ positionals: list[str] = []
94
106
  for arg in argv:
95
107
  if arg == "--rpc-stdio":
96
108
  rpc_stdio = True
97
109
  continue
98
110
  if arg.startswith("-"):
99
- continue
100
- if session_id is None:
101
- session_id = arg
102
- return rpc_stdio, session_id
111
+ raise _ArgumentError(f"Unknown option: {arg}")
112
+ positionals.append(arg)
113
+
114
+ if not positionals:
115
+ return rpc_stdio, None, False
116
+
117
+ command = positionals[0]
118
+ if command != "resume":
119
+ raise _ArgumentError(f"Unknown command: {command}")
120
+
121
+ if len(positionals) == 1:
122
+ if rpc_stdio:
123
+ raise _ArgumentError(
124
+ "resume without <session_id> does not support --rpc-stdio"
125
+ )
126
+ return rpc_stdio, None, True
127
+
128
+ if len(positionals) == 2:
129
+ return rpc_stdio, positionals[1], False
130
+
131
+ raise _ArgumentError("resume accepts at most one <session_id>")
103
132
 
104
133
 
105
134
  def main(argv: list[str] | None = None) -> None:
@@ -114,9 +143,20 @@ def main(argv: list[str] | None = None) -> None:
114
143
 
115
144
  from comate_cli.terminal_agent.app import run
116
145
 
117
- rpc_stdio, session_id = _parse_args(run_argv)
118
146
  try:
119
- asyncio.run(run(rpc_stdio=rpc_stdio, session_id=session_id))
147
+ rpc_stdio, resume_session_id, resume_select = _parse_args(run_argv)
148
+ except _ArgumentError as exc:
149
+ sys.stderr.write(f"{exc}\n{_usage_text()}\n")
150
+ raise SystemExit(2) from exc
151
+
152
+ try:
153
+ asyncio.run(
154
+ run(
155
+ rpc_stdio=rpc_stdio,
156
+ resume_session_id=resume_session_id,
157
+ resume_select=resume_select,
158
+ )
159
+ )
120
160
  except KeyboardInterrupt:
121
161
  noise_guard.begin_shutdown()
122
162
  finally:
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import importlib.metadata
5
+ import locale
4
6
  import logging
5
7
  import os
6
8
  import signal
7
- import sys
8
9
  import threading
9
10
  import time
10
11
  from collections.abc import Iterator
@@ -20,6 +21,8 @@ from comate_agent_sdk.tools import tool
20
21
 
21
22
  from comate_cli.terminal_agent.event_renderer import EventRenderer
22
23
  from comate_cli.terminal_agent.logo import print_logo
24
+ from comate_cli.terminal_agent.preflight import run_preflight_if_needed
25
+ from comate_cli.terminal_agent.resume_selector import select_resume_session_id
23
26
  from comate_cli.terminal_agent.rpc_stdio import StdioRPCBridge
24
27
  from comate_cli.terminal_agent.status_bar import StatusBar
25
28
  from comate_cli.terminal_agent.tui import TerminalAgentTUI
@@ -28,6 +31,72 @@ console = Console()
28
31
  logger = logging.getLogger(__name__)
29
32
 
30
33
 
34
+ def _resolve_cli_project_root() -> Path:
35
+ """Resolve project root: always the current working directory."""
36
+ return Path.cwd().expanduser().resolve()
37
+
38
+
39
+ def _is_chinese_locale() -> bool:
40
+ """判断当前系统 locale 是否为中文环境。"""
41
+ # zh_tw(台湾)和 zh_hk(香港)不走清华镜像源
42
+ _CN_PREFIXES = ("zh_cn", "zh_sg", "zh_mo", "zh_hans")
43
+ for env_var in ("LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"):
44
+ val = os.environ.get(env_var, "").lower()
45
+ if any(val.startswith(p) for p in _CN_PREFIXES):
46
+ return True
47
+ try:
48
+ lang, _ = locale.getlocale()
49
+ if lang:
50
+ lang_lower = lang.lower()
51
+ if any(lang_lower.startswith(p) for p in _CN_PREFIXES):
52
+ return True
53
+ except Exception:
54
+ pass
55
+ return False
56
+
57
+
58
+ async def _check_update() -> str | None:
59
+ """异步检查 comate-cli 是否有新版本,返回提示字符串或 None。
60
+
61
+ 中文 locale 自动使用清华镜像源,其余使用官方 PyPI。
62
+ """
63
+ package = "comate-cli"
64
+ try:
65
+ current = importlib.metadata.version(package)
66
+ except importlib.metadata.PackageNotFoundError:
67
+ return None
68
+
69
+ if _is_chinese_locale():
70
+ url = f"https://pypi.tuna.tsinghua.edu.cn/pypi/{package}/json"
71
+ source_label = "tuna"
72
+ else:
73
+ url = f"https://pypi.org/pypi/{package}/json"
74
+ source_label = "pypi"
75
+
76
+ try:
77
+ import httpx
78
+ async with httpx.AsyncClient(timeout=3.0) as client:
79
+ resp = await client.get(url)
80
+ resp.raise_for_status()
81
+ latest = resp.json()["info"]["version"]
82
+ except Exception:
83
+ logger.debug(f"update check failed (source={source_label})", exc_info=True)
84
+ return None
85
+
86
+ try:
87
+ from importlib.metadata import version as _v
88
+ from packaging.version import Version
89
+ if Version(latest) > Version(current):
90
+ return (
91
+ f"[dim]💡 New version available: [bold cyan]{latest}[/] "
92
+ f"(current: {current}) "
93
+ f"Run [bold]uv tool upgrade {package}[/] to upgrade.[/]"
94
+ )
95
+ except Exception:
96
+ pass
97
+ return None
98
+
99
+
31
100
  def _flush_langfuse_if_configured() -> None:
32
101
  """Flush Langfuse pending events synchronously to prevent atexit thread-join errors on Ctrl+C."""
33
102
  try:
@@ -108,9 +177,10 @@ async def add(a: int, b: int) -> int:
108
177
  return a + b
109
178
 
110
179
 
111
- def _build_agent() -> Agent:
180
+ def _build_agent(*, project_root: Path | None = None) -> Agent:
112
181
  context7_api_key = os.getenv("CONTEXT7_API_KEY")
113
182
  exa_api_key = os.getenv("EXA_API_KEY")
183
+ resolved_project_root = project_root or _resolve_cli_project_root()
114
184
 
115
185
  exa_tools = (
116
186
  "web_search_exa,"
@@ -127,6 +197,7 @@ def _build_agent() -> Agent:
127
197
  return Agent(
128
198
  config=AgentConfig(
129
199
  role="software_engineering",
200
+ project_root=resolved_project_root,
130
201
  env_options=EnvOptions(system_env=True, git_env=True),
131
202
  use_streaming_task=True, # 启用流式 Task(实时显示嵌套工具调用)
132
203
  mcp_servers={
@@ -150,10 +221,12 @@ def _build_agent() -> Agent:
150
221
  )
151
222
 
152
223
 
153
- def _resolve_session(agent: Agent, session_id: str | None) -> tuple[ChatSession, str]:
154
- if session_id:
155
- return ChatSession.resume(agent, session_id=session_id), "resume"
156
- return ChatSession(agent), "new"
224
+ def _resolve_session(
225
+ agent: Agent, resume_session_id: str | None, *, cwd: Path | None = None
226
+ ) -> tuple[ChatSession, str]:
227
+ if resume_session_id:
228
+ return ChatSession.resume(agent, session_id=resume_session_id, cwd=cwd), "resume"
229
+ return ChatSession(agent, cwd=cwd), "new"
157
230
 
158
231
 
159
232
  def _format_exit_usage_line(usage: object) -> str:
@@ -175,6 +248,15 @@ def _format_exit_usage_line(usage: object) -> str:
175
248
  )
176
249
 
177
250
 
251
+ def _format_resume_hint(session_id: str | None) -> str | None:
252
+ if not session_id:
253
+ return None
254
+ return (
255
+ f"[dim]To continue this session, run [bold cyan]comate resume "
256
+ f"{session_id}[/][/]"
257
+ )
258
+
259
+
178
260
  async def _preload_mcp_in_tui(session: ChatSession, renderer: EventRenderer) -> None:
179
261
  """在 TUI 内部异步加载 MCP,通过 renderer 输出状态消息."""
180
262
  if not session._agent.options.mcp_servers:
@@ -196,11 +278,29 @@ async def _preload_mcp_in_tui(session: ChatSession, renderer: EventRenderer) ->
196
278
  renderer.append_system_message(f"MCP loaded: {', '.join(aliases)} ({count} tools)")
197
279
 
198
280
 
199
- async def run(*, rpc_stdio: bool = False, session_id: str | None = None) -> None:
200
- agent = _build_agent()
201
- session, mode = _resolve_session(agent, session_id)
281
+ async def run(
282
+ *,
283
+ rpc_stdio: bool = False,
284
+ resume_session_id: str | None = None,
285
+ resume_select: bool = False,
286
+ ) -> None:
287
+ project_root = _resolve_cli_project_root()
288
+ preflight_result = await run_preflight_if_needed(
289
+ console=console,
290
+ project_root=project_root,
291
+ interactive=not rpc_stdio,
292
+ )
293
+ if preflight_result.should_abort_launch:
294
+ return
295
+
296
+ agent = _build_agent(project_root=project_root)
297
+
298
+ if rpc_stdio and resume_select and not resume_session_id:
299
+ console.print("[red]`comate resume` does not support `--rpc-stdio` without <session_id>[/]")
300
+ return
202
301
 
203
302
  if rpc_stdio:
303
+ session, _mode = _resolve_session(agent, resume_session_id, cwd=project_root)
204
304
  bridge = StdioRPCBridge(session)
205
305
  try:
206
306
  await bridge.run()
@@ -209,6 +309,23 @@ async def run(*, rpc_stdio: bool = False, session_id: str | None = None) -> None
209
309
  return
210
310
 
211
311
  print_logo(console)
312
+
313
+ if resume_select and not resume_session_id:
314
+ selected_session_id = await select_resume_session_id(console, cwd=project_root)
315
+ if not selected_session_id:
316
+ return
317
+ resume_session_id = selected_session_id
318
+
319
+ session, mode = _resolve_session(agent, resume_session_id, cwd=project_root)
320
+
321
+ # 版本检查:带超时,不阻塞启动
322
+ try:
323
+ update_hint = await asyncio.wait_for(_check_update(), timeout=2.0)
324
+ if update_hint:
325
+ console.print(update_hint)
326
+ except (asyncio.TimeoutError, Exception):
327
+ pass
328
+
212
329
  # 不再阻塞等待 MCP,改为 TUI 内部异步加载
213
330
  status_bar = StatusBar(session)
214
331
  status_bar.set_mode(session.get_mode())
@@ -250,12 +367,11 @@ async def run(*, rpc_stdio: bool = False, session_id: str | None = None) -> None
250
367
  if usage_line:
251
368
  console.print(f"[dim]{usage_line}[/]")
252
369
 
253
- console.print(
254
- f"[dim]To continue this session, run [bold cyan]comate resume "
255
- f"{active_session.session_id}[/][/]"
256
- )
370
+ initialized_session_id = tui.initialized_session_id
371
+ resume_hint = _format_resume_hint(initialized_session_id)
372
+ if resume_hint is not None:
373
+ console.print(resume_hint)
257
374
 
258
375
 
259
376
  if __name__ == "__main__":
260
- argv_session_id = sys.argv[1] if len(sys.argv) > 1 else None
261
- asyncio.run(run(session_id=argv_session_id))
377
+ asyncio.run(run())