rote-cli 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.
- rote/__init__.py +3 -0
- rote/adapters/__init__.py +70 -0
- rote/adapters/_common.py +149 -0
- rote/adapters/cloudflare.py +978 -0
- rote/adapters/dbos.py +1235 -0
- rote/adapters/temporal.py +568 -0
- rote/cli.py +520 -0
- rote/graduator/__init__.py +259 -0
- rote/graduator/drivers/__init__.py +226 -0
- rote/graduator/drivers/anthropic_api.py +423 -0
- rote/graduator/drivers/claude.py +312 -0
- rote/graduator/drivers/codex.py +83 -0
- rote/ir.py +495 -0
- rote/serve/__init__.py +14 -0
- rote/serve/backends.py +162 -0
- rote/serve/registry.py +201 -0
- rote/serve/server.py +249 -0
- rote/skills/rote-graduate/SKILL.md +230 -0
- rote/skills/rote-graduate/references/crystallization-heuristics.md +260 -0
- rote/skills/rote-graduate/references/ir-schema.md +420 -0
- rote/skills/rote-graduate/references/llm-judge-extraction.md +341 -0
- rote/skills/rote-graduate/references/node-kinds.md +223 -0
- rote_cli-0.1.0.dist-info/METADATA +546 -0
- rote_cli-0.1.0.dist-info/RECORD +27 -0
- rote_cli-0.1.0.dist-info/WHEEL +4 -0
- rote_cli-0.1.0.dist-info/entry_points.txt +2 -0
- rote_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
rote/cli.py
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""rote CLI — entry point.
|
|
2
|
+
|
|
3
|
+
The north star for this CLI:
|
|
4
|
+
|
|
5
|
+
rote graduate ./path/to/skill --runtime temporal --out ./graduated/
|
|
6
|
+
|
|
7
|
+
One command, skill in, runnable workflow out. Every other command is a
|
|
8
|
+
building block for that flow:
|
|
9
|
+
|
|
10
|
+
rote emit <pipeline.yaml> --runtime temporal # IR → code only
|
|
11
|
+
rote analyze <skill-path> # graduator dry run
|
|
12
|
+
rote eval <skill-path> # regression against expected/
|
|
13
|
+
|
|
14
|
+
Internally ``rote graduate`` runs the ``rote-graduate`` skill in an agent
|
|
15
|
+
loop, which reads the source skill, produces a ``pipeline.yaml``, stubs
|
|
16
|
+
the extracted/signature modules, then invokes the chosen adapter to emit
|
|
17
|
+
the runtime code.
|
|
18
|
+
|
|
19
|
+
This module is deliberately thin — subcommands delegate to real
|
|
20
|
+
implementations in sibling modules so the CLI itself stays boring and
|
|
21
|
+
testable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import asyncio
|
|
28
|
+
import sys
|
|
29
|
+
from collections.abc import Sequence
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from rote import __version__
|
|
33
|
+
from rote.adapters import ADAPTERS, get_adapter
|
|
34
|
+
from rote.graduator import Graduator, GraduatorError
|
|
35
|
+
from rote.ir import load_pipeline
|
|
36
|
+
|
|
37
|
+
# ───────── Subcommand: emit ─────────
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_emit(args: argparse.Namespace) -> int:
|
|
41
|
+
"""Render a runtime adapter from a pipeline.yaml.
|
|
42
|
+
|
|
43
|
+
Pure IR → code. No agent involved. This is the lowest-level
|
|
44
|
+
subcommand and the simplest to reason about — it should be used
|
|
45
|
+
when the user already has a hand-written or previously-graduated
|
|
46
|
+
``pipeline.yaml`` and just wants to re-render the runtime code.
|
|
47
|
+
"""
|
|
48
|
+
pipeline_path = Path(args.pipeline_yaml)
|
|
49
|
+
if not pipeline_path.exists():
|
|
50
|
+
print(f"error: pipeline file not found: {pipeline_path}", file=sys.stderr)
|
|
51
|
+
return 2
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
pipeline = load_pipeline(pipeline_path)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"error: failed to load pipeline: {e}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
adapter = get_adapter(args.runtime)
|
|
61
|
+
except KeyError as e:
|
|
62
|
+
print(f"error: {e.args[0]}", file=sys.stderr)
|
|
63
|
+
return 2
|
|
64
|
+
|
|
65
|
+
out_dir = Path(args.out)
|
|
66
|
+
written = adapter.emit(pipeline, out_dir)
|
|
67
|
+
|
|
68
|
+
print(f"rote: emitted {pipeline.name} v{pipeline.version} → {out_dir}")
|
|
69
|
+
for label, path in written.items():
|
|
70
|
+
print(f" {label}: {path}")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ───────── Subcommand stubs (graduate / analyze / eval) ─────────
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _cmd_graduate(args: argparse.Namespace) -> int:
|
|
78
|
+
"""Run the full one-shot graduation flow.
|
|
79
|
+
|
|
80
|
+
Steps:
|
|
81
|
+
|
|
82
|
+
1. Resolve the source skill directory; reject if it's not a skill bundle.
|
|
83
|
+
2. Run the graduator agent (via ``Graduator.graduate``) to produce
|
|
84
|
+
``<out>/graduated/pipeline.yaml`` plus extracted modules and stubs.
|
|
85
|
+
3. Hand the IR to the chosen runtime adapter to emit
|
|
86
|
+
``<out>/runtime/<target>/`` with workflow.py + activities.py.
|
|
87
|
+
4. Print a one-screen summary of what was produced.
|
|
88
|
+
|
|
89
|
+
Failure modes are reported with EX_USAGE (2) for user errors and
|
|
90
|
+
EX_SOFTWARE (70) for everything else.
|
|
91
|
+
"""
|
|
92
|
+
skill_path = Path(args.skill_path)
|
|
93
|
+
if not skill_path.is_dir():
|
|
94
|
+
print(
|
|
95
|
+
f"error: skill path is not a directory: {skill_path}",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
return 2
|
|
99
|
+
|
|
100
|
+
out_dir = Path(args.out)
|
|
101
|
+
graduated_dir = out_dir / "graduated"
|
|
102
|
+
runtime_dir = out_dir / "runtime" / args.runtime
|
|
103
|
+
|
|
104
|
+
graduator = Graduator(agent=args.agent, model=args.model)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = asyncio.run(graduator.graduate(skill_path, graduated_dir))
|
|
108
|
+
except GraduatorError as e:
|
|
109
|
+
print(f"rote graduate: {e}", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
except KeyboardInterrupt:
|
|
112
|
+
print("rote graduate: interrupted", file=sys.stderr)
|
|
113
|
+
return 130
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
adapter = get_adapter(args.runtime)
|
|
117
|
+
except KeyError as e:
|
|
118
|
+
print(f"error: {e.args[0]}", file=sys.stderr)
|
|
119
|
+
return 2
|
|
120
|
+
|
|
121
|
+
written = adapter.emit(result.pipeline, runtime_dir)
|
|
122
|
+
|
|
123
|
+
# ── Summary ──
|
|
124
|
+
print(f"rote graduate: ✓ {result.pipeline.name} v{result.pipeline.version}")
|
|
125
|
+
print(f" driver: {result.driver_name}")
|
|
126
|
+
if result.driver_metadata:
|
|
127
|
+
meta_str = ", ".join(f"{k}={v}" for k, v in result.driver_metadata.items())
|
|
128
|
+
print(f" metadata: {meta_str}")
|
|
129
|
+
print(f" graduated artifacts: {graduated_dir}")
|
|
130
|
+
print(f" emitted runtime ({args.runtime}): {runtime_dir}")
|
|
131
|
+
for label, path in written.items():
|
|
132
|
+
print(f" {label}: {path}")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ───────── Subcommand: register ─────────
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _resolve_pipeline_yaml(path: Path) -> Path | None:
|
|
140
|
+
"""Find the pipeline.yaml behind a user-supplied path.
|
|
141
|
+
|
|
142
|
+
Accepts the file itself, a directory containing one, or a
|
|
143
|
+
``rote graduate --out`` directory (which nests it under graduated/).
|
|
144
|
+
"""
|
|
145
|
+
if path.is_file():
|
|
146
|
+
return path
|
|
147
|
+
for candidate in (path / "pipeline.yaml", path / "graduated" / "pipeline.yaml"):
|
|
148
|
+
if candidate.is_file():
|
|
149
|
+
return candidate
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cmd_register(args: argparse.Namespace) -> int:
|
|
154
|
+
"""Add or update a registry entry for a graduated pipeline.
|
|
155
|
+
|
|
156
|
+
Reads the pipeline.yaml from a graduate run, derives the MCP tool
|
|
157
|
+
name / description / inputSchema from it, attaches the runtime
|
|
158
|
+
trigger config from the flags, and upserts ``~/.rote/registry.json``
|
|
159
|
+
(or ``--registry``). ``rote serve`` picks the change up live.
|
|
160
|
+
"""
|
|
161
|
+
from rote.serve.registry import (
|
|
162
|
+
CloudflareTrigger,
|
|
163
|
+
Registry,
|
|
164
|
+
TemporalTrigger,
|
|
165
|
+
default_registry_path,
|
|
166
|
+
entry_from_pipeline,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
pipeline_yaml = _resolve_pipeline_yaml(Path(args.graduated_dir))
|
|
170
|
+
if pipeline_yaml is None:
|
|
171
|
+
print(
|
|
172
|
+
f"error: no pipeline.yaml found at or under: {args.graduated_dir}",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
return 2
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
pipeline = load_pipeline(pipeline_yaml)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"error: failed to load pipeline: {e}", file=sys.stderr)
|
|
181
|
+
return 1
|
|
182
|
+
|
|
183
|
+
trigger: TemporalTrigger | CloudflareTrigger
|
|
184
|
+
if args.runtime == "temporal":
|
|
185
|
+
workflow_name = args.workflow_name
|
|
186
|
+
if workflow_name is None:
|
|
187
|
+
# Must match the versioned @workflow.defn name the Temporal
|
|
188
|
+
# adapter emits (PascalCase + pipeline hash).
|
|
189
|
+
from rote.adapters._common import _pipeline_hash, _to_pascal_case
|
|
190
|
+
|
|
191
|
+
workflow_name = f"{_to_pascal_case(pipeline.name)}_{_pipeline_hash(pipeline)}"
|
|
192
|
+
trigger = TemporalTrigger(
|
|
193
|
+
address=args.temporal_address,
|
|
194
|
+
namespace=args.temporal_namespace,
|
|
195
|
+
task_queue=args.task_queue or pipeline.name,
|
|
196
|
+
workflow_name=workflow_name,
|
|
197
|
+
)
|
|
198
|
+
else: # cloudflare
|
|
199
|
+
if not args.url:
|
|
200
|
+
print(
|
|
201
|
+
"error: --url is required for --runtime cloudflare "
|
|
202
|
+
"(the deployed worker's trigger endpoint)",
|
|
203
|
+
file=sys.stderr,
|
|
204
|
+
)
|
|
205
|
+
return 2
|
|
206
|
+
trigger = CloudflareTrigger(url=args.url, status_url=args.status_url)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
entry = entry_from_pipeline(pipeline, pipeline_yaml, trigger, name=args.name)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(f"error: {e}", file=sys.stderr)
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
registry_path = Path(args.registry) if args.registry else default_registry_path()
|
|
215
|
+
registry = Registry.load(registry_path)
|
|
216
|
+
replaced = registry.upsert(entry)
|
|
217
|
+
registry.save(registry_path)
|
|
218
|
+
|
|
219
|
+
verb = "updated" if replaced else "registered"
|
|
220
|
+
print(f"rote register: {verb} tool '{entry.name}' ({args.runtime}) → {registry_path}")
|
|
221
|
+
print(f" pipeline: {entry.pipeline_yaml}")
|
|
222
|
+
print(f" serve it: rote serve --registry {registry_path}")
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ───────── Subcommand: serve ─────────
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
230
|
+
"""Launch the MCP server exposing every registered pipeline as a tool.
|
|
231
|
+
|
|
232
|
+
stdio by default (what ``claude mcp add`` expects); ``--http`` for
|
|
233
|
+
Streamable HTTP. Nothing may be printed to stdout in stdio mode —
|
|
234
|
+
stdout is the MCP transport.
|
|
235
|
+
"""
|
|
236
|
+
from rote.serve.registry import default_registry_path
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
from rote.serve.server import build_server
|
|
240
|
+
except ImportError as e:
|
|
241
|
+
print(
|
|
242
|
+
f"error: fastmcp is not installed ({e}). "
|
|
243
|
+
"Install the serve extra: pip install 'rote[serve]'",
|
|
244
|
+
file=sys.stderr,
|
|
245
|
+
)
|
|
246
|
+
return 2
|
|
247
|
+
|
|
248
|
+
registry_path = Path(args.registry) if args.registry else default_registry_path()
|
|
249
|
+
if not registry_path.exists():
|
|
250
|
+
print(
|
|
251
|
+
f"rote serve: registry {registry_path} does not exist yet — serving an "
|
|
252
|
+
f"empty tool list. Register a pipeline with `rote register`.",
|
|
253
|
+
file=sys.stderr,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
server = build_server(registry_path)
|
|
257
|
+
if args.http:
|
|
258
|
+
server.run(transport="http", host=args.host, port=args.port, show_banner=False)
|
|
259
|
+
else:
|
|
260
|
+
server.run(show_banner=False) # stdio
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _cmd_analyze(args: argparse.Namespace) -> int:
|
|
265
|
+
print("rote analyze: not yet implemented.", file=sys.stderr)
|
|
266
|
+
return 70
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _cmd_eval(args: argparse.Namespace) -> int:
|
|
270
|
+
print("rote eval: not yet implemented.", file=sys.stderr)
|
|
271
|
+
return 70
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ───────── Argument parsing ─────────
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
278
|
+
parser = argparse.ArgumentParser(
|
|
279
|
+
prog="rote",
|
|
280
|
+
description=(
|
|
281
|
+
"Graduate fuzzy AI skills into deterministic, reliable workflows. "
|
|
282
|
+
"The north-star command is `rote graduate`; `rote emit` exposes "
|
|
283
|
+
"the underlying IR → code step for users who already have a "
|
|
284
|
+
"pipeline.yaml."
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"-v",
|
|
289
|
+
"--version",
|
|
290
|
+
action="version",
|
|
291
|
+
version=f"rote {__version__}",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
subparsers = parser.add_subparsers(
|
|
295
|
+
dest="command",
|
|
296
|
+
metavar="<command>",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
available_runtimes = sorted(ADAPTERS)
|
|
300
|
+
|
|
301
|
+
# rote emit
|
|
302
|
+
emit = subparsers.add_parser(
|
|
303
|
+
"emit",
|
|
304
|
+
help="Render runtime code from a pipeline.yaml (IR → code only)",
|
|
305
|
+
description=(
|
|
306
|
+
"Render runtime code from a pipeline.yaml. This is the pure "
|
|
307
|
+
"IR → adapter step — no graduator agent is invoked."
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
emit.add_argument(
|
|
311
|
+
"pipeline_yaml",
|
|
312
|
+
help="Path to a pipeline.yaml (IR file)",
|
|
313
|
+
)
|
|
314
|
+
emit.add_argument(
|
|
315
|
+
"--runtime",
|
|
316
|
+
default="temporal",
|
|
317
|
+
choices=available_runtimes,
|
|
318
|
+
help=(
|
|
319
|
+
f"Target workflow runtime (default: temporal). "
|
|
320
|
+
f"Available: {', '.join(available_runtimes)}"
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
emit.add_argument(
|
|
324
|
+
"--out",
|
|
325
|
+
required=True,
|
|
326
|
+
help="Output directory (created if missing)",
|
|
327
|
+
)
|
|
328
|
+
emit.set_defaults(func=_cmd_emit)
|
|
329
|
+
|
|
330
|
+
# rote graduate (stub)
|
|
331
|
+
graduate = subparsers.add_parser(
|
|
332
|
+
"graduate",
|
|
333
|
+
help="Graduate a skill into a runnable pipeline (north-star command)",
|
|
334
|
+
description=(
|
|
335
|
+
"Read a skill bundle, run the rote-graduate agent to produce "
|
|
336
|
+
"a pipeline IR + extracted modules + signature stubs, then "
|
|
337
|
+
"emit runtime code for the chosen adapter. Output layout: "
|
|
338
|
+
"<out>/graduated/ (the IR + stubs) and <out>/runtime/<target>/ "
|
|
339
|
+
"(the emitted workflow code)."
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
graduate.add_argument("skill_path", help="Path to the source skill directory")
|
|
343
|
+
graduate.add_argument(
|
|
344
|
+
"--runtime",
|
|
345
|
+
default="temporal",
|
|
346
|
+
choices=available_runtimes,
|
|
347
|
+
)
|
|
348
|
+
graduate.add_argument(
|
|
349
|
+
"--out",
|
|
350
|
+
required=True,
|
|
351
|
+
help="Output directory for the graduated artifacts",
|
|
352
|
+
)
|
|
353
|
+
graduate.add_argument(
|
|
354
|
+
"--agent",
|
|
355
|
+
choices=["claude", "codex", "api"],
|
|
356
|
+
default=None,
|
|
357
|
+
help=(
|
|
358
|
+
"Agent runtime to use for the graduator. Defaults to auto-detect "
|
|
359
|
+
"(claude CLI first, then codex, then anthropic API). Users with "
|
|
360
|
+
"a Claude Max/Pro or ChatGPT subscription should prefer claude/codex "
|
|
361
|
+
"to avoid per-token API charges."
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
graduate.add_argument(
|
|
365
|
+
"--model",
|
|
366
|
+
default=None,
|
|
367
|
+
help=(
|
|
368
|
+
"Override the LLM model used by the graduator agent "
|
|
369
|
+
"(e.g. 'claude-opus-4-6' for higher-quality / higher-cost "
|
|
370
|
+
"runs on complex skills). Defaults to the driver's default, "
|
|
371
|
+
"which is Sonnet 4.6 for the subscription path."
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
graduate.set_defaults(func=_cmd_graduate)
|
|
375
|
+
|
|
376
|
+
# rote register
|
|
377
|
+
register = subparsers.add_parser(
|
|
378
|
+
"register",
|
|
379
|
+
help="Register a graduated pipeline as an MCP tool for `rote serve`",
|
|
380
|
+
description=(
|
|
381
|
+
"Append or update an entry in the serve registry "
|
|
382
|
+
"(~/.rote/registry.json by default) from a graduate run's "
|
|
383
|
+
"pipeline.yaml. Tool name comes from pipeline.name, description "
|
|
384
|
+
"from pipeline.description, inputSchema from the pipeline input "
|
|
385
|
+
"contract. A running `rote serve` picks the change up live."
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
register.add_argument(
|
|
389
|
+
"graduated_dir",
|
|
390
|
+
help=(
|
|
391
|
+
"A graduate --out directory, a directory containing pipeline.yaml, "
|
|
392
|
+
"or the pipeline.yaml itself"
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
register.add_argument(
|
|
396
|
+
"--registry",
|
|
397
|
+
default=None,
|
|
398
|
+
help="Registry file (default: ~/.rote/registry.json)",
|
|
399
|
+
)
|
|
400
|
+
register.add_argument(
|
|
401
|
+
"--runtime",
|
|
402
|
+
default="temporal",
|
|
403
|
+
choices=["temporal", "cloudflare"],
|
|
404
|
+
help="Runtime the graduated pipeline is deployed on (default: temporal)",
|
|
405
|
+
)
|
|
406
|
+
register.add_argument(
|
|
407
|
+
"--name",
|
|
408
|
+
default=None,
|
|
409
|
+
help="Override the MCP tool name (default: pipeline.name)",
|
|
410
|
+
)
|
|
411
|
+
register.add_argument(
|
|
412
|
+
"--temporal-address",
|
|
413
|
+
default="localhost:7233",
|
|
414
|
+
help="Temporal frontend address (default: localhost:7233)",
|
|
415
|
+
)
|
|
416
|
+
register.add_argument(
|
|
417
|
+
"--temporal-namespace",
|
|
418
|
+
default="default",
|
|
419
|
+
help="Temporal namespace (default: default)",
|
|
420
|
+
)
|
|
421
|
+
register.add_argument(
|
|
422
|
+
"--task-queue",
|
|
423
|
+
default=None,
|
|
424
|
+
help="Temporal task queue the graduated worker polls (default: pipeline.name)",
|
|
425
|
+
)
|
|
426
|
+
register.add_argument(
|
|
427
|
+
"--workflow-name",
|
|
428
|
+
default=None,
|
|
429
|
+
help=(
|
|
430
|
+
"Temporal workflow type name (default: the versioned name the "
|
|
431
|
+
"Temporal adapter emits for this pipeline)"
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
register.add_argument(
|
|
435
|
+
"--url",
|
|
436
|
+
default=None,
|
|
437
|
+
help="Cloudflare: the deployed worker's trigger endpoint (required for cloudflare)",
|
|
438
|
+
)
|
|
439
|
+
register.add_argument(
|
|
440
|
+
"--status-url",
|
|
441
|
+
dest="status_url",
|
|
442
|
+
default=None,
|
|
443
|
+
help=(
|
|
444
|
+
"Cloudflare: optional status endpoint template containing "
|
|
445
|
+
"'{workflow_id}' if the worker exposes a status route"
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
register.set_defaults(func=_cmd_register)
|
|
449
|
+
|
|
450
|
+
# rote serve
|
|
451
|
+
serve = subparsers.add_parser(
|
|
452
|
+
"serve",
|
|
453
|
+
help="Serve every registered pipeline as an MCP tool (stdio or HTTP)",
|
|
454
|
+
description=(
|
|
455
|
+
"Launch a single MCP server exposing one tool per registry entry "
|
|
456
|
+
"(plus a <tool>_status companion for polling long-running "
|
|
457
|
+
"workflows). stdio by default — add it to Claude Code with "
|
|
458
|
+
"`claude mcp add rote -- rote serve`. Use --http for Streamable "
|
|
459
|
+
"HTTP."
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
serve.add_argument(
|
|
463
|
+
"--registry",
|
|
464
|
+
default=None,
|
|
465
|
+
help="Registry file (default: ~/.rote/registry.json)",
|
|
466
|
+
)
|
|
467
|
+
serve.add_argument(
|
|
468
|
+
"--http",
|
|
469
|
+
action="store_true",
|
|
470
|
+
help="Serve Streamable HTTP instead of stdio",
|
|
471
|
+
)
|
|
472
|
+
serve.add_argument(
|
|
473
|
+
"--host",
|
|
474
|
+
default="127.0.0.1",
|
|
475
|
+
help="HTTP bind host (default: 127.0.0.1; only with --http)",
|
|
476
|
+
)
|
|
477
|
+
serve.add_argument(
|
|
478
|
+
"--port",
|
|
479
|
+
type=int,
|
|
480
|
+
default=8734,
|
|
481
|
+
help="HTTP port (default: 8734; only with --http)",
|
|
482
|
+
)
|
|
483
|
+
serve.set_defaults(func=_cmd_serve)
|
|
484
|
+
|
|
485
|
+
# rote analyze (stub)
|
|
486
|
+
analyze = subparsers.add_parser(
|
|
487
|
+
"analyze",
|
|
488
|
+
help="Run the graduator against a skill and print a report only",
|
|
489
|
+
)
|
|
490
|
+
analyze.add_argument("skill_path")
|
|
491
|
+
analyze.set_defaults(func=_cmd_analyze)
|
|
492
|
+
|
|
493
|
+
# rote eval (stub)
|
|
494
|
+
eval_cmd = subparsers.add_parser(
|
|
495
|
+
"eval",
|
|
496
|
+
help="Compare graduator output against an expected/ baseline",
|
|
497
|
+
)
|
|
498
|
+
eval_cmd.add_argument("skill_path")
|
|
499
|
+
eval_cmd.set_defaults(func=_cmd_eval)
|
|
500
|
+
|
|
501
|
+
return parser
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
505
|
+
parser = _build_parser()
|
|
506
|
+
args = parser.parse_args(argv)
|
|
507
|
+
|
|
508
|
+
if not getattr(args, "command", None):
|
|
509
|
+
# No subcommand — print a helpful banner and usage.
|
|
510
|
+
print(f"rote {__version__} — graduate fuzzy AI skills into deterministic workflows")
|
|
511
|
+
print()
|
|
512
|
+
parser.print_help()
|
|
513
|
+
return 0
|
|
514
|
+
|
|
515
|
+
func = args.func
|
|
516
|
+
return int(func(args))
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == "__main__":
|
|
520
|
+
raise SystemExit(main())
|