feat(cli): M5 tooling integration — mitm/tunnel/tool/translator/media (30 cmds)#14
Conversation
… (30 cmds) Implement the M5 milestone from todo-openproxy.md — ~30 runtime CLI commands that talk to the running server's HTTP API. Each command supports both human and `--robot` (`openproxy.v1.*` envelope) output, and returns exit code 6 when the server is unreachable. New modules: - src/cli/mitm.rs (~8 cmds): status, start, stop, cert generate|path, config get|set|apply - src/cli/tunnel_rt.rs (~7 cmds, extends M1 stub): enable|disable cloudflare|tailscale, tailscale install|login|check|enable|disable - src/cli/tool.rs (~8 cmds): list, show, apply (with --dry-run), revert, execute, run, doc, antigravity-mitm enable|disable - src/cli/translator.rs (~6 cmds): formats, translate, send, preset list|save|load - src/cli/media.rs (~13 cmds): providers list|add|edit|delete, combo list|create, tts voices|speak (binary stdout), stt transcribe, embed, image generate, search, web fetch Runtime helpers added: put_json, patch_json, delete_json, post_json_bytes in src/cli/runtime.rs. Tests: tests/cli_m5_robot_envelopes.rs (15 integration tests covering the happy path for each command group via wiremock).
Devin test report — M5 runtime CLIRan the merged Result: all 7 tests passed. 29 PASS / 0 FAIL / 2 INCONCLUSIVE (the inconclusives were "BE didn't echo this exact substring in its error message", not feature regressions). Two of my plan's BE-response-shape assumptions were wrong and were corrected mid-execution — the CLI was right in every case, it just faithfully passed the real BE response through.
Selected raw evidenceTest 1b — exit 6 contract when server is down Test 3 — claude apply dry-run does not write Test 7 — tunnel dispatcher split (new tailscale.check vs legacy status) Not in scope, deliberately
Devin session: https://app.devin.ai/sessions/fb8cee23c76b42d29481760433c2183f |
Summary
Implements the M5 milestone from
todo-openproxy.md— ~30 runtime CLI commands that talk to the running server's HTTP API. Each command supports both human and--robot(openproxy.v1.*envelope) output, and returns exit code6when the server is unreachable.New modules (under
src/cli/)mitm.rs(~8)status,start,stop,cert generate,cert path,config get,config set,config apply/api/mitm/*,/api/mitm-configtunnel_rt.rs(~7, extends M1 stub)enable <p>,disable <p>,tailscale install/login/check/enable/disable/api/tunnel/*,/api/tunnel/tailscale-*tool.rs(~8)list,show,apply(with--dry-run),revert,execute,run,doc,antigravity-mitm enable/disable/api/cli-tools/*translator.rs(~6)formats,translate(step2+step3),send,preset list/save/load/api/translator/*media.rs(~13)providers list/add/edit/delete,combo list/create,tts voices/speak,stt transcribe,embed,image generate,search,web fetch/api/media-providers/*,/api/combos,/v1/{audio,embeddings,images,search,web}media tts speakis the only command that emits binary to stdout (mp3/wav bytes from/v1/audio/speech); the robot envelope is still written to stdout first, followed by the raw audio.Plumbing
Runtimeinsrc/cli/runtime.rs:put_json,patch_json,delete_json,post_json_bytes(binary), plus a small refactor to sharedecode_json.Cli::Command(Mitm,Tool,Translator,Media) plus three newTunnelCmdvariants (Enable,Disable,Tailscale) that route throughtunnel_rt::runwhile keeping the legacy localStart/Stop/Statusworking against the in-processTunnelManager.src/cli/mod.rsandsrc/main.rsso each new command path resolvesResolvedConfigand calls<module>::run(...).Tests
tests/cli_m5_robot_envelopes.rs— 15 integration tests (assert_cmd + wiremock) covering one happy path per command group; verifies theopenproxy.v1.*envelope shape, exit code, and thatmedia tts speakwrites raw bytes to stdout.cargo test --lib→ 386 pass.cargo clippy --all-targets --no-deps→ no new warnings in any M5 file.Conventions kept from M3/M4
async fn run_x(...) -> anyhow::Result<i32>, returnsrt_error_to_exit(ctx, e)on runtime failures so the existingserver_unreachable → exit 6contract is preserved.openproxy.v1.<area>.<verb>schema (e.g.openproxy.v1.media.tts.speak,openproxy.v1.mitm.config.apply).media web fetch <URL>uses a positional URL arg (namedpagein the enum) on purpose — using--urlwould collide with the global--urlserver-override flag.Review & Testing Checklist for Human
media tts speakbyte streaming. Confirm the binary stdout payload survives the robot envelope being printed first (envelope ends with a single\n, audio bytes follow). Try piping into a file and playing it back.tool applybody shape.build_apply_bodywrites a different JSON shape per tool (claudeusesenv: { ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL }, others use{ baseUrl, apiKey, model }). Verify these still match what the server's*-settingsendpoints expect for each integration.openproxy tunnel startstill hit the local M1 stub; the newtunnel enable/disableandtunnel tailscale *go to the server. Make sure the split feels right and the help text isn't confusing.mitm config apply --from-file -does a single bulk PUT. If you'd rather it do per-key merge semantics, flag it before merging.openproxy server start --detach, then walk through one command from each group (mitm status,tool list,translator formats,media providers list,tunnel tailscale check).Notes
src/cli/schema.rsis intentionally untouched. The new resources (mitm-config,media-provider,translator-preset, ...) can be added there as part of M6 (schema freeze).todo-openproxy.md.