mindcode 0.2.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 (116) hide show
  1. mindcode/__init__.py +5 -0
  2. mindcode/__main__.py +4 -0
  3. mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
  4. mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
  5. mindcode/__pycache__/_version.cpython-313.pyc +0 -0
  6. mindcode/__pycache__/config.cpython-313.pyc +0 -0
  7. mindcode/__pycache__/policy.cpython-313.pyc +0 -0
  8. mindcode/__pycache__/remote.cpython-313.pyc +0 -0
  9. mindcode/__pycache__/render.cpython-313.pyc +0 -0
  10. mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
  11. mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
  12. mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
  13. mindcode/_version.py +11 -0
  14. mindcode/approval.py +105 -0
  15. mindcode/bench/__init__.py +19 -0
  16. mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
  17. mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
  18. mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
  19. mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
  20. mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
  21. mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
  22. mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
  23. mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
  24. mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
  25. mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
  26. mindcode/bench/adapters/__init__.py +22 -0
  27. mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  28. mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
  29. mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
  30. mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
  31. mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
  32. mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
  33. mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  34. mindcode/bench/adapters/base.py +45 -0
  35. mindcode/bench/adapters/mini_bfcl.py +18 -0
  36. mindcode/bench/adapters/mini_gaia.py +18 -0
  37. mindcode/bench/adapters/mini_terminal.py +18 -0
  38. mindcode/bench/adapters/swe_bench.py +86 -0
  39. mindcode/bench/adapters/terminal_bench.py +107 -0
  40. mindcode/bench/fake_tools.py +73 -0
  41. mindcode/bench/jsonutil.py +20 -0
  42. mindcode/bench/paths.py +10 -0
  43. mindcode/bench/predictions.py +65 -0
  44. mindcode/bench/report.py +48 -0
  45. mindcode/bench/runner.py +250 -0
  46. mindcode/bench/schema.py +78 -0
  47. mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
  48. mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
  49. mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
  50. mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
  51. mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
  52. mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
  53. mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  54. mindcode/bench/scorers/base.py +22 -0
  55. mindcode/bench/scorers/composite.py +57 -0
  56. mindcode/bench/scorers/exact.py +30 -0
  57. mindcode/bench/scorers/json_call.py +44 -0
  58. mindcode/bench/scorers/swe.py +29 -0
  59. mindcode/bench/scorers/terminal.py +43 -0
  60. mindcode/bench/scorers/terminal_bench.py +25 -0
  61. mindcode/bench/trace.py +34 -0
  62. mindcode/bench/workspace.py +127 -0
  63. mindcode/cli/__init__.py +112 -0
  64. mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  65. mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
  66. mindcode/cli/_shared.py +43 -0
  67. mindcode/cli/commands/__init__.py +1 -0
  68. mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  69. mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
  70. mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
  71. mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
  72. mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
  73. mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
  74. mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
  75. mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
  76. mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
  77. mindcode/cli/commands/bench.py +365 -0
  78. mindcode/cli/commands/chat.py +86 -0
  79. mindcode/cli/commands/config_cmd.py +197 -0
  80. mindcode/cli/commands/remote.py +146 -0
  81. mindcode/cli/commands/shell.py +108 -0
  82. mindcode/cli/commands/status.py +37 -0
  83. mindcode/cli/commands/task.py +417 -0
  84. mindcode/cli/commands/terminal.py +124 -0
  85. mindcode/cli/shell/__init__.py +5 -0
  86. mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
  87. mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
  88. mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
  89. mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
  90. mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
  91. mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
  92. mindcode/cli/shell/completion.py +72 -0
  93. mindcode/cli/shell/menu.py +88 -0
  94. mindcode/cli/shell/repl.py +597 -0
  95. mindcode/cli/shell/slash.py +480 -0
  96. mindcode/cli/shell/startup.py +55 -0
  97. mindcode/cli/shell/tui.py +897 -0
  98. mindcode/config.py +173 -0
  99. mindcode/mcp.py +205 -0
  100. mindcode/policy.py +173 -0
  101. mindcode/remote.py +216 -0
  102. mindcode/render.py +458 -0
  103. mindcode/runtime.py +541 -0
  104. mindcode/skills.py +154 -0
  105. mindcode/subagents.py +43 -0
  106. mindcode/tasking.py +281 -0
  107. mindcode/terminal/__init__.py +1 -0
  108. mindcode/terminal/__main__.py +17 -0
  109. mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
  110. mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
  111. mindcode/terminal_core.py +264 -0
  112. mindcode-0.2.0.dist-info/METADATA +244 -0
  113. mindcode-0.2.0.dist-info/RECORD +116 -0
  114. mindcode-0.2.0.dist-info/WHEEL +5 -0
  115. mindcode-0.2.0.dist-info/entry_points.txt +2 -0
  116. mindcode-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,365 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from rich.table import Table
13
+
14
+ from mindagent.core.artifact import (
15
+ ArtifactPathError,
16
+ ensure_artifact_directory,
17
+ validate_artifact_path,
18
+ )
19
+
20
+ from ...bench.paths import (
21
+ DEFAULT_BENCHMARK_ROOT,
22
+ DEFAULT_RUNS_ROOT,
23
+ DEFAULT_TERMINAL_BENCH_ROOT,
24
+ APP_ROOT,
25
+ )
26
+ from ...bench.report import build_summary
27
+ from ...bench.runner import (
28
+ MindcodeBenchmarkRunner,
29
+ _case_trace_path,
30
+ _run_output_dir,
31
+ run_benchmark,
32
+ )
33
+ from ...bench.schema import BenchmarkRunConfig
34
+ from ...config import load_config
35
+ from ...policy import ApprovalMode
36
+ from .._shared import console
37
+
38
+
39
+ app = typer.Typer(help="运行 mindcode benchmark")
40
+
41
+
42
+ @app.command("list")
43
+ def list_suites(
44
+ benchmark_root: str = typer.Option(
45
+ str(DEFAULT_BENCHMARK_ROOT),
46
+ "--benchmark-root",
47
+ help="benchmark case 根目录",
48
+ ),
49
+ ) -> None:
50
+ runner = MindcodeBenchmarkRunner(Path(benchmark_root).expanduser().resolve())
51
+ table = Table("suite", "cases")
52
+ for suite, count in runner.list_suites():
53
+ table.add_row(suite, str(count))
54
+ console.print(table)
55
+
56
+
57
+ @app.command("run")
58
+ def run(
59
+ suite: str = typer.Option(..., "--suite", help="要运行的 suite"),
60
+ limit: int | None = typer.Option(None, "--limit", help="限制 case 数"),
61
+ case_ids: list[str] | None = typer.Option(
62
+ None,
63
+ "--case",
64
+ help="指定 case id,可重复",
65
+ ),
66
+ provider: str | None = typer.Option(
67
+ None,
68
+ "-p",
69
+ "--provider",
70
+ help="覆盖 default_provider",
71
+ ),
72
+ model: str | None = typer.Option(None, "--model", help="覆盖模型"),
73
+ approve_mode: str = typer.Option(
74
+ ApprovalMode.FULL_ACCEPT.value,
75
+ "--approve-mode",
76
+ case_sensitive=False,
77
+ help="权限模式:ask_user 逐项询问,full_accept 全部接受",
78
+ ),
79
+ output: str = typer.Option(
80
+ str(DEFAULT_RUNS_ROOT),
81
+ "--output",
82
+ help="run 输出目录",
83
+ ),
84
+ benchmark_root: str = typer.Option(
85
+ str(DEFAULT_BENCHMARK_ROOT),
86
+ "--benchmark-root",
87
+ help="benchmark case 根目录",
88
+ ),
89
+ max_steps: int = typer.Option(50, "--max-steps", help="每 case 最大 step"),
90
+ total_timeout: float | None = typer.Option(
91
+ 600.0,
92
+ "--total-timeout",
93
+ help="每 case 总超时秒数",
94
+ ),
95
+ ) -> None:
96
+ config = load_config()
97
+ provider_name = provider or config.default_provider
98
+ run_id = f"{suite}-{time.strftime('%Y%m%d-%H%M%S')}"
99
+ try:
100
+ output_root = Path(output).expanduser()
101
+ run_output_dir = _run_output_dir(output_root, run_id)
102
+ run_config = BenchmarkRunConfig(
103
+ suite=suite,
104
+ run_id=run_id,
105
+ provider=provider_name,
106
+ model=model,
107
+ approve_mode=ApprovalMode(approve_mode).value,
108
+ workspace_root=run_output_dir / "workspaces",
109
+ output_root=output_root,
110
+ limit=limit,
111
+ case_ids=tuple(case_ids or ()),
112
+ max_steps=max_steps,
113
+ total_timeout_s=total_timeout,
114
+ )
115
+ results = run_benchmark(
116
+ run_config,
117
+ Path(benchmark_root).expanduser().resolve(),
118
+ )
119
+ except ValueError as exc:
120
+ console.print(f"[red]benchmark 配置错误:[/red] {exc}")
121
+ raise typer.Exit(2)
122
+
123
+ summary = build_summary(results)
124
+ console.print(
125
+ f"run_id={run_id} passed={summary['passed']}/{summary['total']} "
126
+ f"pass_rate={summary['pass_rate']:.1%}"
127
+ )
128
+ console.print(f"output={run_output_dir}")
129
+
130
+
131
+ @app.command("report")
132
+ def report(
133
+ run_id: str,
134
+ output: str = typer.Option(
135
+ str(DEFAULT_RUNS_ROOT),
136
+ "--output",
137
+ help="run 输出目录",
138
+ ),
139
+ ) -> None:
140
+ output_root = Path(output).expanduser()
141
+ try:
142
+ summary_path = validate_artifact_path(
143
+ output_root,
144
+ _run_output_dir(output_root, run_id) / "summary.json",
145
+ )
146
+ except ArtifactPathError as exc:
147
+ console.print(f"[red]artifact 路径非法:[/red] {exc}")
148
+ raise typer.Exit(2)
149
+ if not summary_path.exists():
150
+ console.print(f"[red]summary 不存在:[/red] {summary_path}")
151
+ raise typer.Exit(2)
152
+ summary = json.loads(summary_path.read_text(encoding="utf-8"))
153
+ console.print(
154
+ f"passed={summary['passed']}/{summary['total']} "
155
+ f"pass_rate={summary['pass_rate']:.1%} "
156
+ f"avg_latency_s={summary['avg_latency_s']:.2f}"
157
+ )
158
+ if summary["failed_cases"]:
159
+ table = Table("case", "score", "phase", "reason")
160
+ for item in summary["failed_cases"]:
161
+ table.add_row(
162
+ item["case_id"],
163
+ str(item["score"]),
164
+ str(item.get("failure_phase")),
165
+ str(item.get("failure_reason")),
166
+ )
167
+ console.print(table)
168
+
169
+
170
+ @app.command("inspect")
171
+ def inspect(
172
+ run_id: str,
173
+ case_id: str,
174
+ output: str = typer.Option(
175
+ str(DEFAULT_RUNS_ROOT),
176
+ "--output",
177
+ help="run 输出目录",
178
+ ),
179
+ ) -> None:
180
+ output_root = Path(output).expanduser()
181
+ try:
182
+ trace_path = _case_trace_path(
183
+ _run_output_dir(output_root, run_id) / "traces",
184
+ case_id,
185
+ )
186
+ validate_artifact_path(output_root, trace_path)
187
+ except ArtifactPathError as exc:
188
+ console.print(f"[red]artifact 路径非法:[/red] {exc}")
189
+ raise typer.Exit(2)
190
+ if not trace_path.exists():
191
+ console.print(f"[red]trace 不存在:[/red] {trace_path}")
192
+ raise typer.Exit(2)
193
+ trace = json.loads(trace_path.read_text(encoding="utf-8"))
194
+ result = trace["result"]
195
+ console.print(f"case={case_id} passed={result['passed']} score={result['score']}")
196
+ console.print(f"final_answer={result.get('final_answer')}")
197
+ console.print(f"expected={trace['case'].get('expected')}")
198
+ console.print(f"tool_calls={len(trace.get('tool_calls', []))}")
199
+ if trace.get("failure_analysis"):
200
+ console.print(f"failure={trace['failure_analysis']}")
201
+
202
+
203
+ @app.command("swe-eval")
204
+ def swe_eval(
205
+ predictions_path: str = typer.Option(
206
+ ...,
207
+ "--predictions-path",
208
+ help="SWE-bench predictions.jsonl 路径",
209
+ ),
210
+ dataset_name: str = typer.Option(
211
+ "SWE-bench/SWE-bench_Lite",
212
+ "--dataset-name",
213
+ help="SWE-bench dataset 名称或本地 json/jsonl/parquet 路径",
214
+ ),
215
+ split: str = typer.Option("test", "--split", help="dataset split"),
216
+ run_id: str = typer.Option(
217
+ None,
218
+ "--run-id",
219
+ help="SWE-bench evaluation run id",
220
+ ),
221
+ max_workers: int = typer.Option(1, "--max-workers", help="并发 worker 数"),
222
+ timeout: int | None = typer.Option(
223
+ None,
224
+ "--timeout",
225
+ help="单实例评测超时秒数",
226
+ ),
227
+ swebench_path: str = typer.Option(
228
+ str(APP_ROOT.parent / "lib" / "SWE-bench"),
229
+ "--swebench-path",
230
+ help="本地 SWE-bench 仓库路径",
231
+ ),
232
+ ) -> None:
233
+ predictions = Path(predictions_path).expanduser().resolve()
234
+ if not predictions.exists():
235
+ console.print(f"[red]predictions 不存在:[/red] {predictions}")
236
+ raise typer.Exit(2)
237
+ swebench = Path(swebench_path).expanduser().resolve()
238
+ if not (swebench / "swebench").is_dir():
239
+ console.print(f"[red]SWE-bench 路径无效:[/red] {swebench}")
240
+ raise typer.Exit(2)
241
+
242
+ eval_run_id = run_id or f"mindcode-{time.strftime('%Y%m%d-%H%M%S')}"
243
+ cmd = [
244
+ sys.executable,
245
+ "-m",
246
+ "swebench.harness.run_evaluation",
247
+ "--dataset_name",
248
+ dataset_name,
249
+ "--split",
250
+ split,
251
+ "--predictions_path",
252
+ str(predictions),
253
+ "--max_workers",
254
+ str(max_workers),
255
+ "--run_id",
256
+ eval_run_id,
257
+ ]
258
+ if timeout is not None:
259
+ cmd.extend(["--timeout", str(timeout)])
260
+
261
+ env = os.environ.copy()
262
+ existing = env.get("PYTHONPATH")
263
+ env["PYTHONPATH"] = (
264
+ str(swebench) if not existing else str(swebench) + os.pathsep + existing
265
+ )
266
+ console.print(" ".join(cmd))
267
+ result = subprocess.run(cmd, cwd=swebench, env=env)
268
+ raise typer.Exit(result.returncode)
269
+
270
+
271
+ @app.command("terminal-eval")
272
+ def terminal_eval(
273
+ dataset_path: str = typer.Option(
274
+ str(DEFAULT_TERMINAL_BENCH_ROOT / "original-tasks"),
275
+ "--dataset-path",
276
+ help="Terminal-Bench dataset/task 目录",
277
+ ),
278
+ task_ids: list[str] | None = typer.Option(
279
+ None,
280
+ "--task-id",
281
+ help="指定 task id,可重复",
282
+ ),
283
+ agent: str = typer.Option(
284
+ "oracle",
285
+ "--agent",
286
+ "-a",
287
+ help="Terminal-Bench agent 名称",
288
+ ),
289
+ model: str | None = typer.Option(
290
+ None,
291
+ "--model",
292
+ "-m",
293
+ help="Terminal-Bench agent 使用的模型名",
294
+ ),
295
+ output: str = typer.Option(
296
+ str(DEFAULT_TERMINAL_BENCH_ROOT / "runs"),
297
+ "--output",
298
+ help="Terminal-Bench run 输出目录",
299
+ ),
300
+ run_id: str | None = typer.Option(
301
+ None,
302
+ "--run-id",
303
+ help="Terminal-Bench run id",
304
+ ),
305
+ n_concurrent: int = typer.Option(
306
+ 1,
307
+ "--n-concurrent",
308
+ help="并发 trial 数",
309
+ ),
310
+ n_attempts: int = typer.Option(
311
+ 1,
312
+ "--n-attempts",
313
+ help="每个 task 尝试次数",
314
+ ),
315
+ no_rebuild: bool = typer.Option(
316
+ False,
317
+ "--no-rebuild",
318
+ help="跳过重建容器",
319
+ ),
320
+ ) -> None:
321
+ tb_cmd = shutil.which("tb") or shutil.which("terminal-bench")
322
+ if tb_cmd is None:
323
+ console.print("[red]未找到 Terminal-Bench CLI:[/red] 请先安装 tb")
324
+ raise typer.Exit(2)
325
+ dataset = Path(dataset_path).expanduser().resolve()
326
+ if not dataset.exists():
327
+ console.print(f"[red]dataset_path 不存在:[/red] {dataset}")
328
+ raise typer.Exit(2)
329
+
330
+ output_path = Path(output).expanduser()
331
+ try:
332
+ ensure_artifact_directory(output_path)
333
+ except ArtifactPathError as exc:
334
+ console.print(f"[red]artifact 输出路径非法:[/red] {exc}")
335
+ raise typer.Exit(2)
336
+
337
+ cmd = [
338
+ tb_cmd,
339
+ "run",
340
+ "--dataset-path",
341
+ str(dataset),
342
+ "--agent",
343
+ agent,
344
+ "--output-path",
345
+ str(output_path),
346
+ "--n-concurrent",
347
+ str(n_concurrent),
348
+ "--n-attempts",
349
+ str(n_attempts),
350
+ ]
351
+ if model:
352
+ cmd.extend(["--model", model])
353
+ if run_id:
354
+ cmd.extend(["--run-id", run_id])
355
+ if no_rebuild:
356
+ cmd.append("--no-rebuild")
357
+ for task_id in task_ids or ():
358
+ cmd.extend(["--task-id", task_id])
359
+
360
+ console.print(
361
+ f"Terminal-Bench run: agent={agent} "
362
+ f"tasks={','.join(task_ids or ['all'])} output={output_path}"
363
+ )
364
+ result = subprocess.run(cmd)
365
+ raise typer.Exit(result.returncode)
@@ -0,0 +1,86 @@
1
+ """`mindcode chat` 子命令 —— 一次性 prompt 模式。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from ...policy import ApprovalMode
10
+ from .._shared import console
11
+ from ..shell import run_shell
12
+
13
+
14
+ def chat(
15
+ message: str = typer.Option(
16
+ None, "--message", "-m", help="要发送的消息"
17
+ ),
18
+ workspace: str = typer.Option(
19
+ ".", "-w", "--workspace", help="workspace 根目录"
20
+ ),
21
+ approve_mode: str = typer.Option(
22
+ ApprovalMode.FULL_ACCEPT.value,
23
+ "-a",
24
+ "--approve-mode",
25
+ case_sensitive=False,
26
+ help="权限模式:ask_user 逐项询问,full_accept 全部接受",
27
+ ),
28
+ provider: str = typer.Option(
29
+ None, "-p", "--provider", help="覆盖 default_provider"
30
+ ),
31
+ model: str = typer.Option(
32
+ None, "--model", help="覆盖 default_model",
33
+ ),
34
+ session: str = typer.Option(
35
+ None, "-s", "--session", help="指定 session_id"
36
+ ),
37
+ resume: str = typer.Option(
38
+ None, "-r", "--resume", help="恢复 session_id 后追加"
39
+ ),
40
+ no_stream: bool = typer.Option(
41
+ False, "--no-stream", help="关闭流式输出"
42
+ ),
43
+ max_steps: int | None = typer.Option(
44
+ None, "--max-steps", help="最大 step 数(默认 50)"
45
+ ),
46
+ step_timeout: float | None = typer.Option(
47
+ None, "--step-timeout", help="单步超时秒数,0 表示禁用(默认 120)"
48
+ ),
49
+ total_timeout: float | None = typer.Option(
50
+ None, "--total-timeout", help="总超时秒数,0 表示禁用(默认 600)"
51
+ ),
52
+ ) -> None:
53
+ """发送单条消息,不进入交互模式。"""
54
+ if not message:
55
+ console.print("[red]Error:[/red] --message is required")
56
+ raise typer.Exit(1)
57
+
58
+ ws = Path(workspace).expanduser().resolve()
59
+ if not ws.is_dir():
60
+ console.print(
61
+ f"[red]workspace 不存在或不是目录:[/red] {ws}"
62
+ )
63
+ raise typer.Exit(2)
64
+
65
+ try:
66
+ rc = run_shell(
67
+ ws,
68
+ approve_mode=ApprovalMode(approve_mode),
69
+ provider_name=provider,
70
+ model=model,
71
+ session_id=session,
72
+ resume=resume,
73
+ stream=not no_stream,
74
+ one_shot_prompt=message,
75
+ max_steps=max_steps if max_steps is not None else 50,
76
+ step_timeout_s=None
77
+ if step_timeout == 0
78
+ else (step_timeout if step_timeout is not None else 300.0),
79
+ total_timeout_s=None
80
+ if total_timeout == 0
81
+ else (total_timeout if total_timeout is not None else 1800.0),
82
+ )
83
+ except ValueError as exc:
84
+ console.print(f"[red]配置错误:[/red] {exc}")
85
+ raise typer.Exit(2)
86
+ raise typer.Exit(rc)
@@ -0,0 +1,197 @@
1
+ """`mindcode config` 子命令 —— 配置管理(typer app)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.markup import escape
7
+ from rich.table import Table
8
+
9
+ from ...config import (
10
+ CONFIG_FILE,
11
+ load_config,
12
+ save_config,
13
+ )
14
+ from .._shared import console, mask_key
15
+
16
+ app = typer.Typer(help="配置管理(providers / models / api-key)")
17
+
18
+
19
+ @app.command("providers")
20
+ def config_providers() -> None:
21
+ """列出所有 provider。"""
22
+ config = load_config()
23
+ table = Table(
24
+ title=f"Providers ({CONFIG_FILE})",
25
+ show_header=True,
26
+ header_style="bold cyan",
27
+ )
28
+ table.add_column("", width=3)
29
+ table.add_column("Provider", style="cyan")
30
+ table.add_column("Protocol")
31
+ table.add_column("Key")
32
+ table.add_column("Base URL", style="dim")
33
+ table.add_column("Default Model", style="green")
34
+
35
+ for name, p in config.providers.items():
36
+ mark = "[green]*[/green]" if name == config.default_provider else " "
37
+ table.add_row(
38
+ mark,
39
+ name,
40
+ p.protocol,
41
+ mask_key(p.api_key),
42
+ p.base_url,
43
+ p.default_model,
44
+ )
45
+
46
+ console.print(table)
47
+
48
+
49
+ @app.command("models")
50
+ def config_models(
51
+ provider: str = typer.Argument(..., help="provider 名称"),
52
+ ) -> None:
53
+ """列出指定 provider 的所有 model。"""
54
+ config = load_config()
55
+ if provider not in config.providers:
56
+ console.print(
57
+ f"[red]未知 provider:[/red] {provider} "
58
+ f"[dim]可用: {', '.join(config.providers)}[/dim]"
59
+ )
60
+ raise typer.Exit(1)
61
+ p = config.providers[provider]
62
+ console.print(
63
+ f"[bold cyan]{provider}[/bold cyan] "
64
+ f"[dim]{p.protocol} {p.base_url}[/dim]"
65
+ )
66
+ for m in p.models:
67
+ mark = (
68
+ "[green]*[/green]" if m == p.default_model else " "
69
+ )
70
+ console.print(f" {mark} {m}")
71
+
72
+
73
+ @app.command("api-key")
74
+ def config_api_key(
75
+ provider: str = typer.Argument(..., help="provider 名称"),
76
+ key: str = typer.Argument(..., help="api_key"),
77
+ ) -> None:
78
+ """设置 provider 的 api_key(持久化)。"""
79
+ config = load_config()
80
+ if provider not in config.providers:
81
+ console.print(
82
+ f"[red]未知 provider:[/red] {provider} "
83
+ f"[dim]可用: {', '.join(config.providers)}[/dim]"
84
+ )
85
+ raise typer.Exit(1)
86
+ config.providers[provider].api_key = key.strip()
87
+ save_config(config)
88
+ console.print(
89
+ f"[green]✓[/green] 已保存 [cyan]{provider}[/cyan] 的 api_key "
90
+ f"({mask_key(key.strip())})"
91
+ )
92
+
93
+
94
+ @app.command("get")
95
+ def config_get(
96
+ provider: str = typer.Argument(..., help="provider 名称"),
97
+ key: str = typer.Argument(
98
+ ..., help="字段名:protocol / api_key / base_url / default_model / models"
99
+ ),
100
+ ) -> None:
101
+ """读取 provider 的某个字段。"""
102
+ config = load_config()
103
+ if provider not in config.providers:
104
+ console.print(f"[red]未知 provider:[/red] {provider}")
105
+ raise typer.Exit(1)
106
+ p = config.providers[provider]
107
+ valid_keys = {"protocol", "api_key", "base_url", "default_model", "models"}
108
+ if key not in valid_keys:
109
+ console.print(
110
+ f"[red]未知字段:[/red] {key} [dim]可用: {', '.join(valid_keys)}[/dim]"
111
+ )
112
+ raise typer.Exit(1)
113
+ value = getattr(p, key)
114
+ if key == "api_key":
115
+ value = mask_key(p.api_key)
116
+ console.print(f"[green]{provider}.{key}[/green] = {value}")
117
+
118
+
119
+ @app.command("set")
120
+ def config_set(
121
+ provider: str = typer.Argument(..., help="provider 名称"),
122
+ key: str = typer.Argument(
123
+ ..., help="字段名:protocol / base_url / default_model / api_key"
124
+ ),
125
+ value: str = typer.Argument(..., help="字段值"),
126
+ ) -> None:
127
+ """修改 provider 的某个字段(持久化)。"""
128
+ config = load_config()
129
+ if provider not in config.providers:
130
+ console.print(f"[red]未知 provider:[/red] {provider}")
131
+ raise typer.Exit(1)
132
+ p = config.providers[provider]
133
+ valid_keys = {"protocol", "api_key", "base_url", "default_model"}
134
+ if key not in valid_keys:
135
+ console.print(
136
+ f"[red]未知字段:[/red] {key} "
137
+ f"[dim]可 set: {', '.join(valid_keys)}[/dim]"
138
+ )
139
+ raise typer.Exit(1)
140
+ setattr(p, key, value)
141
+ if key == "protocol" and value not in {"openai", "anthropic"}:
142
+ console.print(f"[red]配置错误:[/red] 不支持的 protocol: {value}")
143
+ raise typer.Exit(2)
144
+ save_config(config)
145
+ console.print(
146
+ f"[green]✓[/green] {provider}.{key} = "
147
+ f"{(mask_key(value) if key == 'api_key' else value)}"
148
+ )
149
+
150
+
151
+ @app.command("models-set")
152
+ def config_models_set(
153
+ provider: str = typer.Argument(..., help="provider 名称"),
154
+ models: list[str] = typer.Argument(..., help="模型列表"),
155
+ ) -> None:
156
+ """设置 provider 的 model_list;第一个模型会成为 default_model。"""
157
+ config = load_config()
158
+ if provider not in config.providers:
159
+ console.print(f"[red]未知 provider:[/red] {provider}")
160
+ raise typer.Exit(1)
161
+ cleaned = [model.strip() for model in models if model.strip()]
162
+ if not cleaned:
163
+ console.print("[red]models 不能为空[/red]")
164
+ raise typer.Exit(2)
165
+ config.providers[provider].models = cleaned
166
+ config.providers[provider].default_model = cleaned[0]
167
+ save_config(config)
168
+ console.print(
169
+ f"[green]✓[/green] {provider}.models = {', '.join(cleaned)}"
170
+ )
171
+
172
+
173
+ @app.command("list")
174
+ def config_list() -> None:
175
+ """显示完整配置(脱敏)。"""
176
+ config = load_config()
177
+ console.print(f"[dim]# {CONFIG_FILE}[/dim]")
178
+ console.print(f'default_provider = "{config.default_provider}"\n')
179
+ for name, p in config.providers.items():
180
+ console.print(f"[bold]{escape(f'[providers.{name}]')}[/bold]")
181
+ console.print(f" protocol = [cyan]{p.protocol}[/cyan]")
182
+ console.print(f" api_key = {mask_key(p.api_key)}")
183
+ console.print(f" base_url = [dim]{p.base_url}[/dim]")
184
+ console.print(f" default_model = [green]{p.default_model}[/green]")
185
+ if p.models:
186
+ console.print(f" models = {', '.join(p.models)}")
187
+ console.print()
188
+
189
+
190
+ @app.command("path")
191
+ def config_path() -> None:
192
+ """显示配置文件路径。"""
193
+ console.print(str(CONFIG_FILE))
194
+
195
+
196
+ if __name__ == "__main__":
197
+ app()