champollion 0.3.3

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 (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. package/shared/schemas/source-snapshot.schema.json +96 -0
@@ -0,0 +1,430 @@
1
+ """
2
+ Method Bridge — ships with the champollion CLI npm package.
3
+
4
+ Loads Python translation method modules and exposes them via
5
+ stdin/stdout JSON-lines protocol. Self-contained — no Arena dependency.
6
+
7
+ The CLI's ExternalMethod class spawns this as a subprocess:
8
+ python cli/lib/bridge/method_bridge.py --method path/to/method_plugin
9
+
10
+ The bridge also supports standalone discovery mode:
11
+ python cli/lib/bridge/method_bridge.py --discover
12
+
13
+ Protocol (JSON-lines over stdin/stdout):
14
+ stdin: one JSON object per line with an "action" field
15
+ stdout: one JSON response per line with an "ok" field
16
+ stderr: human-readable logging (not parsed by CLI)
17
+
18
+ Actions:
19
+ discover → list installed method modules (via pip entry points)
20
+ translate → translate key-value pairs through the loaded method
21
+ method_card → return method metadata
22
+ shutdown → clean exit
23
+
24
+ This module has ZERO imports beyond Python 3.10+ stdlib (except for
25
+ the method modules themselves, which bring their own dependencies).
26
+
27
+ Why self-contained:
28
+ The bridge inlines the method_loader logic (~60 lines) so the user
29
+ does NOT need to pip install the Arena. Only the method module
30
+ (e.g., pip install crk-translate) is required.
31
+
32
+ Maintained alongside the Arena's method_loader.py — if the manifest
33
+ format changes there, it must be updated here. Both live in the same
34
+ monorepo so this is easy to track.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import asyncio
40
+ import importlib.metadata
41
+ import importlib.util
42
+ import json
43
+ import logging
44
+ import sys
45
+ from pathlib import Path
46
+
47
+ logger = logging.getLogger("champollion.bridge")
48
+
49
+
50
+ # ── Inline method loader ─────────────────────────────────────────────
51
+ #
52
+ # Self-contained reimplementation of arena/mt_eval_harness/method_loader.py.
53
+ # Reads method.json, parses the entry_point, loads the Python module,
54
+ # and instantiates the TranslationMethod class.
55
+ #
56
+ # Kept in sync manually. If method.json schema changes in the Arena's
57
+ # method_loader.py, update this section to match.
58
+
59
+ _REQUIRED_MANIFEST_FIELDS = {"name", "entry_point"}
60
+
61
+
62
+ class MethodLoadError(Exception):
63
+ """Raised when a method plugin cannot be loaded."""
64
+ pass
65
+
66
+
67
+ def _load_method(method_dir: Path) -> tuple:
68
+ """Load a TranslationMethod from a plugin directory.
69
+
70
+ Returns:
71
+ (instance, manifest) tuple — the method object and its parsed
72
+ method.json for metadata access.
73
+
74
+ Raises:
75
+ MethodLoadError with human-readable messages explaining what
76
+ went wrong and what the user should do about it.
77
+ """
78
+ method_dir = Path(method_dir).resolve()
79
+
80
+ if not method_dir.is_dir():
81
+ raise MethodLoadError(
82
+ f"Not a directory: {method_dir}\n"
83
+ f" Expected a method plugin directory containing method.json.\n"
84
+ f" If you installed a method module via pip, try:\n"
85
+ f" python {__file__} --discover"
86
+ )
87
+
88
+ manifest_path = method_dir / "method.json"
89
+ if not manifest_path.exists():
90
+ raise MethodLoadError(
91
+ f"No method.json found in {method_dir}\n"
92
+ f" A method plugin directory must contain a method.json manifest.\n"
93
+ f" See: https://champollion.dev/docs/guides/serving-a-method"
94
+ )
95
+
96
+ try:
97
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
98
+ except json.JSONDecodeError as e:
99
+ raise MethodLoadError(
100
+ f"Invalid JSON in {manifest_path}: {e}"
101
+ ) from e
102
+
103
+ missing = _REQUIRED_MANIFEST_FIELDS - set(manifest.keys())
104
+ if missing:
105
+ raise MethodLoadError(
106
+ f"method.json is missing required fields: {sorted(missing)}.\n"
107
+ f" Required: {sorted(_REQUIRED_MANIFEST_FIELDS)}"
108
+ )
109
+
110
+ entry_point = manifest["entry_point"]
111
+ if ":" not in entry_point:
112
+ raise MethodLoadError(
113
+ f"Invalid entry_point: '{entry_point}'.\n"
114
+ f" Expected format: 'module_name:ClassName'\n"
115
+ f" Example: 'pipeline:CrkPipelineMethod'"
116
+ )
117
+
118
+ module_name, class_name = entry_point.split(":", 1)
119
+ module_file = method_dir / f"{module_name}.py"
120
+ if not module_file.exists():
121
+ raise MethodLoadError(
122
+ f"Entry point module not found: {module_file}\n"
123
+ f" The entry_point '{entry_point}' expects {module_name}.py\n"
124
+ f" in {method_dir}"
125
+ )
126
+
127
+ # Add method dir + parent to sys.path so the plugin can import siblings
128
+ # (e.g., a method package imported from its method_plugin/ directory)
129
+ for p in [str(method_dir), str(method_dir.parent)]:
130
+ if p not in sys.path:
131
+ sys.path.insert(0, p)
132
+
133
+ spec = importlib.util.spec_from_file_location(
134
+ f"method_plugin.{module_name}", module_file
135
+ )
136
+ if spec is None or spec.loader is None:
137
+ raise MethodLoadError(
138
+ f"Python could not create a module spec for {module_file}"
139
+ )
140
+
141
+ try:
142
+ module = importlib.util.module_from_spec(spec)
143
+ spec.loader.exec_module(module)
144
+ except Exception as e:
145
+ raise MethodLoadError(
146
+ f"Failed to load {module_file}: {type(e).__name__}: {e}\n"
147
+ f" Check that the method module's dependencies are installed.\n"
148
+ f" Try: pip install -e {method_dir.parent}"
149
+ ) from e
150
+
151
+ if not hasattr(module, class_name):
152
+ available = [n for n in dir(module) if not n.startswith("_")]
153
+ raise MethodLoadError(
154
+ f"{module_name}.py does not export '{class_name}'.\n"
155
+ f" Available names: {available}"
156
+ )
157
+
158
+ method_class = getattr(module, class_name)
159
+
160
+ # Instantiate — try with manifest/method_dir, fall back to no-arg
161
+ try:
162
+ instance = method_class(manifest=manifest, method_dir=method_dir)
163
+ except TypeError:
164
+ try:
165
+ instance = method_class()
166
+ except Exception as e:
167
+ raise MethodLoadError(
168
+ f"Could not instantiate {class_name}: {e}"
169
+ ) from e
170
+
171
+ if not hasattr(instance, "translate") or not callable(instance.translate):
172
+ raise MethodLoadError(
173
+ f"{class_name} must have an async translate(entries, config) method.\n"
174
+ f" See: https://champollion.dev/docs/guides/serving-a-method"
175
+ )
176
+
177
+ return instance, manifest
178
+
179
+
180
+ # ── Entry-point discovery ─────────────────────────────────────────────
181
+
182
+
183
+ def _discover_installed_methods() -> list[dict]:
184
+ """Find all pip-installed method modules via entry points.
185
+
186
+ Method modules register in their pyproject.toml:
187
+
188
+ [project.entry-points."champollion.methods"]
189
+ crk-decomp-recomp = "method_plugin.pipeline:CrkPipelineMethod"
190
+
191
+ Returns:
192
+ List of dicts with name, method_id, entry_point, supported_pairs,
193
+ and plugin_path for each discovered module.
194
+ """
195
+ methods = []
196
+
197
+ try:
198
+ eps = importlib.metadata.entry_points(group="champollion.methods")
199
+ except Exception:
200
+ # Python < 3.12 or no entry points registered
201
+ return methods
202
+
203
+ for ep in eps:
204
+ try:
205
+ # Resolve the entry point's module to find its file path
206
+ module_ref = ep.value.split(":")[0]
207
+ module_spec = importlib.util.find_spec(module_ref)
208
+
209
+ if module_spec and module_spec.origin:
210
+ method_dir = Path(module_spec.origin).parent
211
+ manifest_path = method_dir / "method.json"
212
+
213
+ if manifest_path.exists():
214
+ manifest = json.loads(
215
+ manifest_path.read_text(encoding="utf-8")
216
+ )
217
+ methods.append({
218
+ "name": manifest.get("name", ep.name),
219
+ "method_id": manifest.get("method_id", ep.name),
220
+ "supported_pairs": manifest.get("supported_pairs", []),
221
+ "entry_point": ep.value,
222
+ "plugin_path": str(method_dir),
223
+ })
224
+ else:
225
+ logger.debug(
226
+ "Entry point %s has no method.json at %s",
227
+ ep.name, method_dir,
228
+ )
229
+ except Exception as e:
230
+ logger.debug("Skipping entry point %s: %s", ep.name, e)
231
+
232
+ return methods
233
+
234
+
235
+ # ── Protocol ──────────────────────────────────────────────────────────
236
+
237
+
238
+ def _send(obj: dict) -> None:
239
+ """Write a JSON response line to stdout and flush immediately."""
240
+ sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
241
+ sys.stdout.flush()
242
+
243
+
244
+ async def _handle_translate(method, request: dict) -> dict:
245
+ """Convert CLI key-value request → Arena entries → translate → CLI map.
246
+
247
+ The CLI sends:
248
+ { keys: { "greeting": "Hello", "farewell": "Goodbye" },
249
+ source_locale: "en", target_locale: "crk",
250
+ config: { model: "anthropic/claude-sonnet-4.6" } }
251
+
252
+ The bridge converts to Arena format:
253
+ [{ id: 0, source: "Hello" }, { id: 1, source: "Goodbye" }]
254
+
255
+ Calls method.translate(entries, config), then converts results back:
256
+ { translations: { "greeting": "tânisi", "farewell": "êkwa" } }
257
+ """
258
+ keys_payload = request.get("keys", {})
259
+ config_overrides = request.get("config", {})
260
+
261
+ # Build Arena-shaped entries from CLI key-value pairs
262
+ entries = []
263
+ key_order = [] # preserves insertion order for response mapping
264
+ for idx, (key, source_text) in enumerate(keys_payload.items()):
265
+ entries.append({"id": idx, "source": source_text})
266
+ key_order.append(key)
267
+
268
+ if not entries:
269
+ return {"ok": True, "translations": {}, "meta": {}}
270
+
271
+ # Build a minimal config namespace matching what method plugins read.
272
+ # The bridge passes through whatever config the CLI sends — the method
273
+ # plugin decides what it uses. At minimum, model and source/target lang.
274
+ class BridgeConfig:
275
+ """Minimal config namespace for method plugin compatibility."""
276
+ pass
277
+
278
+ cfg = BridgeConfig()
279
+ cfg.model = config_overrides.get("model")
280
+ cfg.model_id = config_overrides.get("model") # CrkPipelineMethod reads this
281
+ cfg.source_lang = request.get("source_locale", "en")
282
+ cfg.target_lang = request.get("target_locale", "")
283
+ cfg.temperature = config_overrides.get("temperature", 0.3)
284
+
285
+ # Call the method's translate — same interface the Arena uses
286
+ results = await method.translate(entries, cfg)
287
+
288
+ # Convert Arena results back to CLI key-value map
289
+ translations = {}
290
+ meta = {"total_tokens": 0, "total_latency_s": 0.0, "errors": []}
291
+
292
+ for result in results:
293
+ idx = result.get("id", 0)
294
+ if idx < len(key_order):
295
+ key = key_order[idx]
296
+ if result.get("error"):
297
+ meta["errors"].append({"key": key, "error": result["error"]})
298
+ else:
299
+ predicted = result.get("predicted", "")
300
+ if predicted: # skip empty predictions
301
+ translations[key] = predicted
302
+
303
+ usage = result.get("usage", {})
304
+ meta["total_tokens"] += usage.get("total_tokens", 0)
305
+ meta["total_latency_s"] += result.get("latency_s", 0)
306
+
307
+ return {"ok": True, "translations": translations, "meta": meta}
308
+
309
+
310
+ async def _run_bridge(method_path: str | None = None) -> None:
311
+ """Main loop: read JSON-lines from stdin, dispatch, respond.
312
+
313
+ If method_path is given, load the method on startup.
314
+ If not, run in discovery-only mode (responds to 'discover' only).
315
+ """
316
+ method = None
317
+ manifest = None
318
+
319
+ if method_path:
320
+ try:
321
+ method, manifest = _load_method(Path(method_path))
322
+ except MethodLoadError as e:
323
+ _send({"ok": False, "error": str(e)})
324
+ return
325
+
326
+ method_name = getattr(method, "name", manifest.get("name", "Unknown"))
327
+ _send({"ok": True, "ready": True, "name": method_name})
328
+ else:
329
+ # Discovery-only mode — no method loaded
330
+ _send({"ok": True, "ready": True, "name": "champollion-bridge"})
331
+
332
+ for line in sys.stdin:
333
+ line = line.strip()
334
+ if not line:
335
+ continue
336
+
337
+ try:
338
+ request = json.loads(line)
339
+ except json.JSONDecodeError as e:
340
+ _send({"ok": False, "error": f"Invalid JSON: {e}"})
341
+ continue
342
+
343
+ action = request.get("action", "")
344
+
345
+ if action == "discover":
346
+ methods = _discover_installed_methods()
347
+ _send({"ok": True, "methods": methods})
348
+
349
+ elif action == "translate":
350
+ if not method:
351
+ _send({
352
+ "ok": False,
353
+ "error": (
354
+ "No method loaded. Start the bridge with --method.\n"
355
+ " Example: python method_bridge.py "
356
+ "--method ./my-method/method_plugin"
357
+ ),
358
+ })
359
+ continue
360
+ try:
361
+ response = await _handle_translate(method, request)
362
+ _send(response)
363
+ except Exception as e:
364
+ logger.exception("Translation error")
365
+ _send({
366
+ "ok": False,
367
+ "error": f"{type(e).__name__}: {e}",
368
+ })
369
+
370
+ elif action == "method_card":
371
+ card = manifest or {}
372
+ if method and hasattr(method, "method_card"):
373
+ try:
374
+ card = method.method_card()
375
+ except Exception:
376
+ pass # fall back to manifest
377
+ _send({"ok": True, "card": card})
378
+
379
+ elif action == "shutdown":
380
+ _send({"ok": True})
381
+ break
382
+
383
+ else:
384
+ _send({
385
+ "ok": False,
386
+ "error": f"Unknown action: '{action}'",
387
+ })
388
+
389
+
390
+ def main():
391
+ """CLI entry point for the method bridge."""
392
+ import argparse
393
+
394
+ parser = argparse.ArgumentParser(
395
+ description=(
396
+ "Champollion method bridge — connects the CLI to Python "
397
+ "translation method modules via JSON-lines protocol."
398
+ ),
399
+ )
400
+ parser.add_argument(
401
+ "--method",
402
+ help=(
403
+ "Path to method plugin directory (must contain method.json). "
404
+ "Example: ./my-method/method_plugin"
405
+ ),
406
+ )
407
+ parser.add_argument(
408
+ "--discover",
409
+ action="store_true",
410
+ help="List all pip-installed method modules and exit.",
411
+ )
412
+ args = parser.parse_args()
413
+
414
+ # Logging goes to stderr — stdout is reserved for the JSON protocol
415
+ logging.basicConfig(
416
+ level=logging.INFO,
417
+ format="%(name)s: %(message)s",
418
+ stream=sys.stderr,
419
+ )
420
+
421
+ if args.discover:
422
+ methods = _discover_installed_methods()
423
+ _send({"ok": True, "methods": methods})
424
+ return
425
+
426
+ asyncio.run(_run_bridge(args.method))
427
+
428
+
429
+ if __name__ == "__main__":
430
+ main()