bench2agent 0.3.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.
@@ -0,0 +1,3 @@
1
+ """Bench2Agent: conversational benchmark usage and adoption research."""
2
+
3
+ __version__ = "0.3.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
bench2agent/cli.py ADDED
@@ -0,0 +1,515 @@
1
+ """Bench2Agent terminal conversations and a local MCP service."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import getpass
6
+ import json
7
+ import os
8
+ import re
9
+ import shlex
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from . import __version__, providers
16
+ from .session import Session, conversations, write_json
17
+
18
+ HELP = "/new /chats /resume ID /sources /status /help /exit"
19
+ TOOL_LABELS = {"benchmark_scope": "Checking data coverage", "benchmark_usage": "Counting benchmark use",
20
+ "benchmark_trend": "Comparing editions", "new_benchmarks": "Finding introductions",
21
+ "benchmark_adoption": "Counting adoption", "benchmark_evidence": "Checking paper evidence"}
22
+
23
+
24
+ def safe_text(text) -> str:
25
+ # Model text, paper titles and saved questions can contain terminal controls.
26
+ # Keep newlines/tabs and printable Unicode, remove escape/control sequences.
27
+ text = re.sub(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))", "", str(text))
28
+ return "".join(c for c in text if c in "\n\t" or (c.isprintable() and c != "\x7f"))
29
+
30
+
31
+ def render(turn: dict, *, sources: bool = False) -> str:
32
+ pieces, evidence = [], []
33
+ for seg in turn.get("segs", []):
34
+ if seg.get("t") == "p":
35
+ pieces.append(seg.get("s", ""))
36
+ elif seg.get("t") == "n":
37
+ mark = " [unverified figure]" if seg.get("v") in {"no", "na"} else ""
38
+ pieces.append(seg.get("s", "") + mark)
39
+ elif seg.get("t") == "c":
40
+ evidence.append(seg)
41
+ pieces.append(f"[{len(evidence)}]" + (" [unverified quote]" if not seg.get("v") else ""))
42
+ if sources:
43
+ for i, seg in enumerate(evidence, 1):
44
+ verdict = "✓" if seg.get("v") else "✗"
45
+ pieces.append(f"\n[{i}] {verdict} {seg.get('title', '')}\n {seg.get('url') or seg.get('paper_id') or seg.get('gid')}\n {seg.get('q', '')}\n")
46
+ else:
47
+ for i, seg in enumerate(evidence, 1):
48
+ pieces.append(f"\n[{i}] {seg.get('title', '')} · {seg.get('url') or seg.get('paper_id') or seg.get('gid')}")
49
+ checked = turn.get("verified") or {}
50
+ if checked.get("checked") or checked.get("fchecked"):
51
+ pieces.append(f"\n\nQuotes {checked.get('passed', 0)}/{checked.get('checked', 0)} · "
52
+ f"Figures {checked.get('fpassed', 0)}/{checked.get('fchecked', 0)}")
53
+ return safe_text("".join(pieces))
54
+
55
+
56
+ def settings_path(root: Path) -> Path:
57
+ return root / "data/bench2agent/settings.json"
58
+
59
+
60
+ def settings(root: Path) -> dict:
61
+ try:
62
+ with settings_path(root).open(encoding="utf-8") as source:
63
+ value = json.load(source)
64
+ if not isinstance(value, dict):
65
+ raise ValueError("Invalid settings. Run bench2agent init.")
66
+ return value
67
+ except FileNotFoundError:
68
+ return {}
69
+
70
+
71
+ def configuration(args, root: Path, *, interactive: bool = False) -> dict:
72
+ saved = settings(root)
73
+ provider = args.provider or saved.get("provider")
74
+ if not provider:
75
+ provider = "anthropic" if os.environ.get("ANTHROPIC_API_KEY") and not os.environ.get("OPENAI_API_KEY") else "openai"
76
+ if interactive and not any(os.environ.get(name) for name in providers.KEY_NAMES.values()):
77
+ choice = input("Model provider [openai / anthropic]: ").strip().lower()
78
+ provider = choice or provider
79
+ if provider not in providers.KEY_NAMES:
80
+ raise ValueError("Choose --provider openai or anthropic.")
81
+ model = args.model or (saved.get("model") if saved.get("provider") == provider else None) or providers.DEFAULT_MODELS[provider]
82
+ language = args.language or saved.get("language", "auto")
83
+ return {"provider": provider, "model": model, "language": language}
84
+
85
+
86
+ def ensure_key(config: dict, *, interactive: bool):
87
+ name = providers.KEY_NAMES[config["provider"]]
88
+ if not os.environ.get(name):
89
+ if not interactive:
90
+ raise ValueError(f"Set {name} or start bench2agent interactively to enter a key for this session.")
91
+ key = getpass.getpass(name + " (this session only): ").strip()
92
+ if not key:
93
+ raise ValueError("API key is required.")
94
+ os.environ[name] = key
95
+
96
+
97
+ def cmd_init(args, root: Path) -> int:
98
+ config = configuration(args, root, interactive=sys.stdin.isatty())
99
+ if sys.stdin.isatty() and not args.model:
100
+ config["model"] = input(f"Model [{config['model']}]: ").strip() or config["model"]
101
+ write_json(settings_path(root), config)
102
+ print(f"Saved {config['provider']} / {config['model']} · data: {root}")
103
+ print(f"Set {providers.KEY_NAMES[config['provider']]} in your environment, or enter it when bench2agent starts.")
104
+ print("Next: bench2agent")
105
+ return 0
106
+
107
+
108
+ CLIENT_LABELS = {"claude": "Claude Code", "codex": "Codex"}
109
+
110
+
111
+ def client_status(run=subprocess.run) -> dict:
112
+ """Is Bench2Agent reachable through Claude Code and Codex?
113
+
114
+ Asked of the clients' own CLIs (`<client> mcp get bench2agent`), never read
115
+ from their config or credential files. `signed_in` comes from the same
116
+ login probes the browser settings use; None means the probe gave no answer.
117
+ """
118
+ from bench2agent.core.chat import _agent_status
119
+ out = {}
120
+ for name in ("claude", "codex"):
121
+ info = _agent_status(name)
122
+ entry = {"installed": info["installed"], "signed_in": info["authenticated"],
123
+ "mcp_registered": None, "mcp_scope": None, "mcp_connected": None}
124
+ if info["installed"]:
125
+ try:
126
+ done = run([name, "mcp", "get", "bench2agent"], capture_output=True, text=True,
127
+ timeout=20, stdin=subprocess.DEVNULL)
128
+ entry["mcp_registered"] = done.returncode == 0
129
+ if name == "claude" and done.returncode == 0:
130
+ scope = re.search(r"Scope:\s*(\w+)", done.stdout or "")
131
+ state = re.search(r"Status:\s*\S*\s*(\w+)", done.stdout or "")
132
+ entry["mcp_scope"] = scope.group(1).lower() if scope else None
133
+ entry["mcp_connected"] = (state.group(1).lower() == "connected") if state else None
134
+ except (OSError, subprocess.TimeoutExpired):
135
+ pass
136
+ out[name] = entry
137
+ return out
138
+
139
+
140
+ def _client_line(name: str, c: dict) -> str:
141
+ label = CLIENT_LABELS[name]
142
+ if not c["installed"]:
143
+ return f"{label}: not installed"
144
+ parts = ["installed",
145
+ {True: "signed in", False: "not signed in", None: "sign-in unknown"}[c["signed_in"]]]
146
+ if c["mcp_registered"]:
147
+ where = f" ({c['mcp_scope']} scope)" if c["mcp_scope"] else ""
148
+ parts.append("bench2agent MCP server registered" + where)
149
+ if c["mcp_connected"] is True:
150
+ parts.append("connected")
151
+ elif c["mcp_connected"] is False:
152
+ parts.append("NOT connecting — check `bench2agent data status` and the registered command")
153
+ elif c["mcp_registered"] is False:
154
+ parts.append(f"bench2agent MCP server not registered → run: bench2agent mcp --connect {name}")
155
+ else:
156
+ parts.append("MCP registration unknown")
157
+ return f"{label}: " + " · ".join(parts)
158
+
159
+
160
+ def cmd_status(args, root: Path) -> int:
161
+ from .data import status
162
+ result = {"version": __version__, "home": str(root), "data": status(root),
163
+ "settings": settings(root), "api_keys_present": {
164
+ provider: bool(os.environ.get(name)) for provider, name in providers.KEY_NAMES.items()},
165
+ "clients": client_status()}
166
+ if args.json:
167
+ print(json.dumps(result, ensure_ascii=False))
168
+ else:
169
+ print(f"Bench2Agent {__version__} · {root}")
170
+ print(json.dumps(result["data"], ensure_ascii=False, indent=2))
171
+ keys = ", ".join(f"{p}: {'set' if v else 'not set'}" for p, v in result["api_keys_present"].items())
172
+ print(f"Standalone conversation (`bench2agent`): needs an API key — {keys}")
173
+ print("Through an AI client (no API key; uses the client's own login):")
174
+ for name, c in result["clients"].items():
175
+ print(" " + _client_line(name, c))
176
+ return 0
177
+
178
+
179
+ def cmd_data(args, root: Path) -> int:
180
+ from . import data
181
+ if args.action == "status":
182
+ result = data.status(root)
183
+ elif args.action == "install":
184
+ result = data.install(root, file=args.file, url=args.url, sha256=args.sha256)
185
+ else:
186
+ result = data.bundle(root, Path(args.out))
187
+ print(json.dumps(result, ensure_ascii=False, indent=2))
188
+ return 0
189
+
190
+
191
+ def cmd_migrate(args, root: Path) -> int:
192
+ from .migration import migrate
193
+ print(json.dumps(migrate(root, Path(args.source)), ensure_ascii=False, indent=2))
194
+ return 0
195
+
196
+
197
+ def print_chats(root: Path):
198
+ rows = conversations(root)
199
+ for row in rows:
200
+ print(safe_text(f"{row['id']} {row['provider']} {row['title']}"))
201
+ if not rows:
202
+ print("No saved conversations.")
203
+
204
+
205
+ def load_session(args, root: Path, *, interactive: bool) -> Session:
206
+ cid = args.resume
207
+ if cid == "latest":
208
+ rows = conversations(root)
209
+ if not rows:
210
+ raise ValueError("No saved conversations to resume.")
211
+ cid = rows[0]["id"]
212
+ config = configuration(args, root, interactive=interactive and not cid)
213
+ if interactive and not cid and not settings(root):
214
+ if not args.model:
215
+ config["model"] = input(f"Model [{config['model']}]: ").strip() or config["model"]
216
+ write_json(settings_path(root), config)
217
+ session = Session(root, **config, cid=cid)
218
+ if cid and ((args.provider and args.provider != session.doc["provider"]) or
219
+ (args.model and args.model != session.doc["model"])):
220
+ raise ValueError("A resumed conversation keeps its provider and model. Start a new conversation to change them.")
221
+ return session
222
+
223
+
224
+ def require_data(root: Path, *, interactive: bool):
225
+ if (root / "data/processed/benchmark_snapshot.json").is_file():
226
+ return
227
+ if interactive:
228
+ from .data import install
229
+ print("Downloading benchmark data (16 MB); checking its checksum…", file=sys.stderr, flush=True)
230
+ install(root)
231
+ return
232
+ raise ValueError("Benchmark data is missing. Run bench2agent data install.")
233
+
234
+
235
+ def cmd_chat(args, root: Path) -> int:
236
+ interactive = sys.stdin.isatty() and not getattr(args, "question", None)
237
+ require_data(root, interactive=interactive)
238
+ session = load_session(args, root, interactive=interactive)
239
+ ensure_key(session.doc, interactive=interactive)
240
+ last = session.doc["turns"][-1] if session.doc["turns"] else None
241
+
242
+ def emit(event):
243
+ if event["t"] == "tool":
244
+ print(safe_text(" " + TOOL_LABELS.get(event["name"], event["name"])), file=sys.stderr, flush=True)
245
+
246
+ def answer(question):
247
+ nonlocal last
248
+ last = session.ask(question, emit)
249
+ if args.json:
250
+ print(json.dumps({"chat": session.doc["id"], **last}, ensure_ascii=False))
251
+ else:
252
+ print("\n" + render(last) + "\n", flush=True)
253
+
254
+ if getattr(args, "question", None):
255
+ answer(args.question)
256
+ return 0
257
+ if not args.json:
258
+ from bench2agent.core.mcp import B
259
+ snapshot = B.data()
260
+ editions = ", ".join(sorted(snapshot["editions"]))
261
+ print(f"Bench2Agent · {session.doc['provider']} / {session.doc['model']} · {session.doc['id']}")
262
+ print(safe_text("Data: " + editions))
263
+ print(HELP + "\n")
264
+ if last:
265
+ print(render(last) + "\n")
266
+ if interactive:
267
+ try:
268
+ import readline # optional on Windows; do not save questions outside our chat store
269
+ except ImportError:
270
+ pass
271
+ failed = False
272
+ while True:
273
+ try:
274
+ question = input("> " if interactive else "").strip()
275
+ except EOFError:
276
+ break
277
+ except KeyboardInterrupt:
278
+ if interactive:
279
+ print("\n" + HELP)
280
+ continue
281
+ return 130
282
+ if not question:
283
+ continue
284
+ if question in {"/exit", "/quit"}:
285
+ break
286
+ if question == "/help":
287
+ print(HELP)
288
+ continue
289
+ if question == "/chats":
290
+ print_chats(root)
291
+ continue
292
+ if question == "/status":
293
+ cmd_status(args, root)
294
+ continue
295
+ if question == "/sources":
296
+ print(render(last, sources=True) if last else "No answer yet.")
297
+ continue
298
+ if question == "/new":
299
+ session = Session(root, session.doc["provider"], session.doc["model"], language=session.doc.get("language", "auto"))
300
+ last = None
301
+ print("New conversation · " + session.doc["id"])
302
+ continue
303
+ try:
304
+ if question.startswith("/resume "):
305
+ cid = question.split(maxsplit=1)[1]
306
+ candidate = Session(root, session.doc["provider"], session.doc["model"], cid=cid)
307
+ ensure_key(candidate.doc, interactive=interactive)
308
+ session = candidate
309
+ last = session.doc["turns"][-1] if session.doc["turns"] else None
310
+ print("Resumed · " + session.doc["id"])
311
+ if last:
312
+ print(render(last))
313
+ elif question.startswith("/"):
314
+ print(HELP)
315
+ else:
316
+ answer(question)
317
+ except KeyboardInterrupt:
318
+ print("\nCancelled; the unfinished turn was not saved.", file=sys.stderr)
319
+ if not interactive:
320
+ return 130
321
+ except (ValueError, OSError, providers.ProviderError) as exc:
322
+ failed = True
323
+ print(safe_text(str(exc)), file=sys.stderr)
324
+ if not interactive:
325
+ break
326
+ if not args.json and session.doc["turns"]:
327
+ print("Continue: bench2agent --resume " + session.doc["id"])
328
+ return 1 if failed else 0
329
+
330
+
331
+ def mcp_command(root: Path) -> list[str]:
332
+ # An absolute Python/module command survives venv installations and does
333
+ # not depend on bench2agent being discoverable in the MCP host's PATH.
334
+ return [sys.executable, "-m", "bench2agent", "--home", str(root), "mcp"]
335
+
336
+
337
+ def client_command(client: str, root: Path, *, model: str | None = None) -> list[str]:
338
+ executable = shutil.which(client)
339
+ if not executable:
340
+ raise ValueError(f"{CLIENT_LABELS[client]} is not installed or is not on PATH. Install it first, then run bench2agent {client}.")
341
+ from bench2agent.core.paths import agent_environment
342
+ env = {**agent_environment(), "BENCH2AGENT_HOME": str(root)}
343
+ command = mcp_command(root)
344
+ if client == "claude":
345
+ config = {"mcpServers": {"bench2agent": {"command": command[0], "args": command[1:], "env": env}}}
346
+ native = [executable, "--mcp-config", json.dumps(config)]
347
+ if model:
348
+ native += ["--model", model]
349
+ return native
350
+ # Codex's -c values are TOML; JSON-quoted strings and arrays also fit TOML.
351
+ settings = {"command": json.dumps(command[0]), "args": json.dumps(command[1:]),
352
+ "env": "{" + ", ".join(json.dumps(k) + "=" + json.dumps(v) for k, v in env.items()) + "}"}
353
+ native = [executable]
354
+ for key, value in settings.items():
355
+ native += ["-c", f"mcp_servers.bench2agent.{key}={value}"]
356
+ native += ["--model", model or providers.DEFAULT_MODELS["openai"]]
357
+ return native
358
+
359
+
360
+ def cmd_client(args, root: Path) -> int:
361
+ command = client_command(args.command, root, model=getattr(args, "model", None))
362
+ if args.dry_run:
363
+ print(shlex.join(command))
364
+ return 0
365
+ require_data(root, interactive=True)
366
+ print(f"Opening {CLIENT_LABELS[args.command]} with Bench2Agent…", file=sys.stderr, flush=True)
367
+ return subprocess.run(command, check=False).returncode
368
+
369
+
370
+ def cmd_mcp(args, root: Path) -> int:
371
+ command = mcp_command(root)
372
+ if args.config:
373
+ from bench2agent.core.paths import agent_environment
374
+ print(json.dumps({"mcpServers": {"bench2agent": {"command": command[0], "args": command[1:],
375
+ "env": agent_environment()}}}, indent=2))
376
+ return 0
377
+ if args.connect:
378
+ client = shutil.which(args.connect)
379
+ if not client:
380
+ raise ValueError(f"{args.connect} is not installed or is not on PATH.")
381
+ from bench2agent.core.paths import agent_environment
382
+ env = agent_environment()
383
+ if args.connect == "codex":
384
+ native = [client, "mcp", "add", "bench2agent"]
385
+ for key, value in env.items():
386
+ native += ["--env", key + "=" + value]
387
+ else:
388
+ native = [client, "mcp", "add"]
389
+ for key, value in env.items():
390
+ native += ["--env", key + "=" + value]
391
+ # Claude's --env is variadic; another option must terminate it
392
+ # before the positional server name.
393
+ native += ["--transport", "stdio", "--scope", "user", "bench2agent"]
394
+ native += ["--", *command]
395
+ if args.dry_run:
396
+ print(shlex.join(native))
397
+ return 0
398
+ result = subprocess.run(native, check=False)
399
+ if not result.returncode:
400
+ print(f"Connected Bench2Agent to {args.connect}. Start a new {args.connect} session to use it.")
401
+ return result.returncode
402
+ if args.dry_run:
403
+ raise ValueError("--dry-run requires --connect codex or --connect claude.")
404
+ from bench2agent.core.mcp import _benchmark_tools, serve_stdio
405
+ return serve_stdio(tools=_benchmark_tools(), name="bench2agent")
406
+
407
+
408
+ def common_options(parser, *, child=False):
409
+ default = argparse.SUPPRESS if child else None
410
+ parser.add_argument("--home", default=default, help="runtime data directory (or BENCH2AGENT_HOME)")
411
+ parser.add_argument("--provider", choices=list(providers.KEY_NAMES), default=default)
412
+ parser.add_argument("--model", default=default, help="model ID supported by your account")
413
+ parser.add_argument("--language", choices=["auto", "ko", "en"], default=default)
414
+ parser.add_argument("--resume", nargs="?", const="latest", default=default, metavar="ID")
415
+ parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS if child else False)
416
+
417
+
418
+ def cmd_service(args, root: Path) -> int:
419
+ """Use the same service implementation from the public terminal command."""
420
+ from .core import cli as service
421
+ handlers = {"serve": service.cmd_serve, "setup": service.cmd_setup,
422
+ "bundle": service.cmd_bundle, "fetch-data": service.cmd_fetch,
423
+ "datasets": service.cmd_datasets, "benchmarks": service.cmd_benchmarks}
424
+ return handlers[args.command](args)
425
+
426
+
427
+ def main(argv=None) -> int:
428
+ parser = argparse.ArgumentParser(prog="bench2agent", description=__doc__)
429
+ parser.add_argument("--version", action="version", version="Bench2Agent " + __version__)
430
+ common_options(parser)
431
+ commands = parser.add_subparsers(dest="command")
432
+ chat = commands.add_parser("chat", help="start a conversation (also the default)")
433
+ common_options(chat, child=True)
434
+ chat.set_defaults(fn=cmd_chat)
435
+ ask = commands.add_parser("ask", help="answer one question")
436
+ ask.add_argument("question")
437
+ common_options(ask, child=True)
438
+ ask.set_defaults(fn=cmd_chat)
439
+ init = commands.add_parser("init", help="choose model provider and model")
440
+ common_options(init, child=True)
441
+ init.set_defaults(fn=cmd_init)
442
+ status = commands.add_parser("status", help="data and model connection status")
443
+ common_options(status, child=True)
444
+ status.set_defaults(fn=cmd_status)
445
+ chats = commands.add_parser("chats", help="list saved conversations")
446
+ common_options(chats, child=True)
447
+ chats.set_defaults(fn=lambda args, root: print_chats(root) or 0)
448
+ migration = commands.add_parser("migrate", help="copy data and saved chats from a previous installation")
449
+ common_options(migration, child=True)
450
+ migration.add_argument("--from", dest="source", required=True, metavar="DIR", help="previous runtime data directory")
451
+ migration.set_defaults(fn=cmd_migrate)
452
+ data = commands.add_parser("data", help="install, inspect or bundle benchmark data")
453
+ common_options(data, child=True)
454
+ actions = data.add_subparsers(dest="action", required=True)
455
+ data_status = actions.add_parser("status")
456
+ common_options(data_status, child=True)
457
+ install = actions.add_parser("install")
458
+ common_options(install, child=True)
459
+ source = install.add_mutually_exclusive_group()
460
+ source.add_argument("--file")
461
+ source.add_argument("--url")
462
+ install.add_argument("--sha256", help="expected SHA-256 of the downloaded file")
463
+ bundle = actions.add_parser("bundle")
464
+ common_options(bundle, child=True)
465
+ bundle.add_argument("--out", required=True)
466
+ data.set_defaults(fn=cmd_data)
467
+ mcp = commands.add_parser("mcp", help="serve read-only tools or connect an existing AI client")
468
+ common_options(mcp, child=True)
469
+ mode = mcp.add_mutually_exclusive_group()
470
+ mode.add_argument("--config", action="store_true", help="print MCP configuration for other clients")
471
+ mode.add_argument("--connect", choices=["codex", "claude"], help="register via the client's own CLI")
472
+ mcp.add_argument("--dry-run", action="store_true", help="show registration command without changing settings")
473
+ mcp.set_defaults(fn=cmd_mcp)
474
+ for name in ("claude", "codex"):
475
+ client = commands.add_parser(name, help=f"open {CLIENT_LABELS[name]} with Bench2Agent tools")
476
+ common_options(client, child=True)
477
+ client.add_argument("--dry-run", action="store_true", help="show the launch command without downloading data or starting the client")
478
+ client.set_defaults(fn=cmd_client)
479
+ serve = commands.add_parser("serve", help="start the local web interface")
480
+ common_options(serve, child=True)
481
+ serve.add_argument("--port", type=int, default=8001)
482
+ serve.add_argument("--no-meili", action="store_true", help="do not start the optional search engine")
483
+ serve.set_defaults(fn=cmd_service)
484
+ for name, description in (("setup", "install the optional browser search engine"),
485
+ ("bundle", "bundle the full browser data on the build machine")):
486
+ service = commands.add_parser(name, help=description)
487
+ common_options(service, child=True)
488
+ service.set_defaults(fn=cmd_service)
489
+ fetch = commands.add_parser("fetch-data", help="install a full browser data bundle")
490
+ common_options(fetch, child=True)
491
+ source = fetch.add_mutually_exclusive_group()
492
+ source.add_argument("--file")
493
+ source.add_argument("--url")
494
+ source.add_argument("--release", metavar="TAG")
495
+ fetch.set_defaults(fn=cmd_service)
496
+ datasets = commands.add_parser("datasets", help="export paper and dataset source inventories")
497
+ common_options(datasets, child=True)
498
+ datasets.add_argument("--out", default=".")
499
+ datasets.add_argument("--gid", type=int)
500
+ datasets.set_defaults(fn=cmd_service)
501
+ benchmarks = commands.add_parser("benchmarks", help="build the benchmark snapshot from reviewed extraction")
502
+ common_options(benchmarks, child=True)
503
+ benchmarks.add_argument("--out")
504
+ benchmarks.set_defaults(fn=cmd_service)
505
+ args = parser.parse_args(argv)
506
+ if args.home:
507
+ os.environ["BENCH2AGENT_HOME"] = args.home
508
+ from bench2agent.core.paths import ROOT
509
+ try:
510
+ return getattr(args, "fn", cmd_chat)(args, ROOT)
511
+ except KeyboardInterrupt:
512
+ return 130
513
+ except (ValueError, OSError, providers.ProviderError) as exc:
514
+ print(safe_text(str(exc)), file=sys.stderr)
515
+ return 1
@@ -0,0 +1 @@
1
+ """Shared benchmark evidence, deterministic tools and the local web service."""
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())