@mrrlin-dev/external-agents 0.2.0 → 0.2.1

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 (4) hide show
  1. package/README.md +43 -19
  2. package/cli.js +19 -1
  3. package/package.json +1 -1
  4. package/ui.js +7 -3
package/README.md CHANGED
@@ -1,28 +1,50 @@
1
1
  # @mrrlin-dev/external-agents
2
2
 
3
- **One MCP server for every LLM you talk to.**
3
+ **Cut your LLM bill by 10-100x by fanning work across free tiers of a dozen providers.**
4
4
 
5
- `external-agents` is a small, opinionated MCP server + CLI that lets your primary coding agent (Claude Code, Codex, Cursor) route work to a pool of secondary LLMs — Gemini, DeepSeek, Grok, OpenRouter, local Ollama, and any CLI-agentic reviewer (cursor-agent, opencode) through one clean surface, with per-provider auth, cooldowns, round-robin, escalation, and a local statistics dashboard.
5
+ `external-agents` is an MCP server + CLI that routes work from your primary coding agent (Claude Code, Codex, Cursor) to a **pool of 20+ free-tier LLMs** — Gemini, Groq, Cerebras, OpenRouter :free, Z.ai, Ollama Cloud, and paid tiers of DeepSeekvia one clean surface: round-robin, cooldown-aware, auto-fallback on 429, with a local dashboard for setting keys and tracking usage.
6
6
 
7
- > **Part of [mrrlin.com](https://mrrlin.com)** the AI orchestration platform for developers. `external-agents` is the open-source layer we use internally to power multi-model consensus and cost-efficient atomic execution. Ships MIT so anyone can adopt it standalone.
7
+ **The core value is economic.** You have separate free-tier quotas at Google + Groq + Cerebras + OpenRouter + Z.ai. `external-agents` treats them as **one pool of tokens** and dispatches to the next healthy bucket. What used to be one 30-req/min Gemini limit is now ~150 req/min across five providers, all at $0 — enough to run entire agentic loops (implementations, reviews, refactors) that would burn $10-100/day on a single paid model.
8
+
9
+ **It's also the substrate for LLM-consensus** (the Karpathy-style [multi-model panel](https://x.com/karpathy/status/1770467322202042745): ask N different models the same question, adjudicate). `pick_agents` gives you N distinct-provider picks in one call, so a "fleet of subagents deliberating in parallel" is one primitive away. **[Mrrlin](https://mrrlin.com) uses exactly this** — its `/consensus` gate resolves two dynamic terminal reviewers from this pool every round, so every design and every diff gets stress-tested by different model families before it merges.
10
+
11
+ Beyond savings and consensus, you also get the zoo-manager niceties: per-provider auth, cooldowns keyed to the *provider's* reset time (not a made-up default), quota tracking in a local JSONL, and a config-free `status` view of who is healthy right now.
12
+
13
+ > **Part of [mrrlin.com](https://mrrlin.com)** — the AI orchestration platform for developers. `external-agents` is the open-source layer we use internally for cost-efficient atomic execution and multi-model consensus. Ships MIT so anyone can adopt it standalone.
8
14
 
9
15
  ---
10
16
 
11
- ## Why this exists
17
+ ## Why this exists — the money argument first
18
+
19
+ If you're paying for a single frontier model to do everything (planning, atomic edits, reviews, unit-test scaffolding, docstrings), you're leaving a **lot** of money on the table:
20
+
21
+ - **Free-tier quotas stack.** Google gives you generous Gemini quotas. Groq gives you 30 rpm Llama 3.3 70B at 500-800 tok/s. Cerebras gives you 30 rpm at ~2000 tok/s. OpenRouter gives you 20 rpd of `:free`-tagged frontier models with no card. Z.ai gives you GLM-4.7-flash. Ollama Cloud gives you gpt-oss:120b. Each of these has a *separate* bucket — `external-agents` treats them as one pool, so effective throughput is Σ(free-tier limits).
22
+ - **Round-robin, not "always pick the smartest".** Weak-tier atomic tasks (rename, refactor, write test, fix lint) don't need frontier reasoning; they need a competent model that's currently under quota. `pick --tier weak` finds one; escalation to strong-tier only fires when a task genuinely fails twice.
23
+ - **Fallback is automatic.** A 429 on Groq flips you to Cerebras without a retry loop in your code. The exhausted provider is marked with the reset time the provider itself reports in headers (not a 1-hour fallback), so you don't waste calls probing it.
24
+ - **Consensus is cheap.** Fan `pick_agents --n 4 --min-distinct-providers 4` and dispatch in parallel — four independent verdicts across four provider families, all on free-tier buckets, in the wall-time of the slowest one.
25
+
26
+ The net effect for us has been **10-100x reduction** in per-task cost for the atomic-executor workload that used to run on a single paid model. Your mileage varies with task mix, but the direction is the same for anyone who fans work out.
27
+
28
+ ### What else you get (the zoo-management layer)
12
29
 
13
- If you use more than one LLM in your workflow (and by now most people do), you've probably done at least one of these:
30
+ - **Unified dispatch.** `dispatch(agent_id, prompt)` runs a specific agent; `pick_agents` picks N healthy candidates by round-robin with cross-provider diversity guaranteed.
31
+ - **State that heals itself.** Quota exhaustion detected from live responses + rate-limit headers; cooldown honors the *provider's* reset time; healthy calls auto-clear stale cooldowns.
32
+ - **Auth surfaces you actually have.** Subscription CLI (Codex, Claude), env-var API keys via [`aider`](https://aider.chat) → direct-to-provider through LiteLLM (100+ providers, no gateway proxy), direct CLI (cursor-agent, opencode, ollama).
33
+ - **Statistics that answer your questions.** How many dispatches went to Gemini this week? What did they cost? Which provider is failing the most? Local JSONL log + dashboard, no cloud required.
14
34
 
15
- - Hand-rolled a shell script that alternates DeepSeek and Gemini for cheap tasks.
16
- - Copy-pasted the same "review this diff" prompt into three CLIs to compare answers.
17
- - Written a wrapper to detect a 429 and retry on a different provider.
18
- - Kept a mental note of which providers are quota-exhausted today.
35
+ ### LLM-consensus, the Karpathy pattern, made trivial
19
36
 
20
- `external-agents` collapses all of that into **one tool with one config**:
37
+ Andrej Karpathy's much-shared observation: [ask several distinct LLMs the same thing and pick the majority answer — an ensemble of frontier models routinely beats any single one](https://x.com/karpathy/status/1770467322202042745). That works only if you can (a) reach N different providers cheaply and (b) fan out in parallel. `external-agents` gives you both:
21
38
 
22
- - **Unified dispatch.** `dispatch(agent_id, prompt)` with default transport `generate_new` picks the next healthy provider by round-robin; N parallel `pick_agents` + `dispatch` calls (consumer-composed) fans out to N providers in parallel with cross-model diversity guaranteed.
23
- - **State that heals itself.** Quota exhaustion is detected from live responses and rate-limit headers, cooldown lasts until the *provider's* reset time (not a made-up default), and healthy calls automatically clear stale cooldowns.
24
- - **Auth surfaces you actually have.** Subscription CLI (Codex, Claude), env-var API keys via [`aider`](https://aider.chat) direct-to-provider through LiteLLM (Gemini, DeepSeek, Grok, OpenRouter, and 100+ more), direct CLI (cursor-agent, opencode, ollama) — pick your credential path per entry, no forced OAuth, no gateway proxy.
25
- - **Statistics that answer your questions.** How many dispatches went to Gemini this week? What did they cost? Which provider is failing the most? All in a local dashboard, no cloud required.
39
+ ```
40
+ ids = pick_agents({ n: 3, min_distinct_providers: 3, exclude_ids: [primary] })
41
+ outs = Promise.all(ids.map(id => dispatch({ agent_id: id, prompt })))
42
+ // three distinct-provider verdicts, ~$0, in one wall-clock round.
43
+ ```
44
+
45
+ **Mrrlin's `/consensus` gate does exactly this.** Every design/spec and every PR diff goes through a 4-reviewer panel — GPT + Gemini (over MCP) + two dynamic terminal reviewers resolved from this pool (`external-agents pick --n 2 --min-distinct-providers 2 --exclude-providers openai,google`). The primary coding agent commits a blind verdict first, then adjudicates the panel. Free-tier terminals mean the gate is essentially free to run on every substantial change.
46
+
47
+ You don't need Mrrlin's gate to use the pattern — the primitives are unopinionated. Build your own reviewer panel, self-consistency check, jury-of-N verifier, whatever fits.
26
48
 
27
49
  ---
28
50
 
@@ -64,13 +86,15 @@ Open the local dashboard with `external-agents ui` and you'll get a page like:
64
86
 
65
87
  Once installed, add one block to your MCP client config.
66
88
 
89
+ The package ships a dedicated `external-agents-mcp` binary — the MCP server entry — so client configs are plain command lines with no args.
90
+
67
91
  **Claude Code** (`~/.claude.json`):
68
92
  ```json
69
93
  {
70
94
  "mcpServers": {
71
95
  "external-agents": {
72
- "command": "external-agents",
73
- "args": ["mcp", "serve"]
96
+ "command": "external-agents-mcp",
97
+ "args": []
74
98
  }
75
99
  }
76
100
  }
@@ -79,11 +103,11 @@ Once installed, add one block to your MCP client config.
79
103
  **Codex Code** (`~/.codex/config.toml`):
80
104
  ```toml
81
105
  [mcp_servers.external-agents]
82
- command = "external-agents"
83
- args = ["mcp", "serve"]
106
+ command = "external-agents-mcp"
107
+ args = []
84
108
  ```
85
109
 
86
- **Cursor** — Settings → MCP → Add server → same command.
110
+ **Cursor** — Settings → MCP → Add server → command `external-agents-mcp`, no args.
87
111
 
88
112
  Your primary agent now has these low-level tools (build your own exec/review flows on top):
89
113
 
package/cli.js CHANGED
@@ -21,6 +21,7 @@ import { pickAgents } from "./lib/pick.js";
21
21
  import fs from "node:fs";
22
22
  import os from "node:os";
23
23
  import path from "node:path";
24
+ import { spawn } from "node:child_process";
24
25
 
25
26
  // bootEnv duplicated from server.js so CLI + server load the same env context.
26
27
  function bootEnv() {
@@ -184,6 +185,21 @@ function cmdProbe(args) {
184
185
  console.log(JSON.stringify({ id: agentId, ...result, checked }));
185
186
  }
186
187
 
188
+ // `external-agents ui` — spawn the loopback dashboard (ui.js) inline so the CLI
189
+ // stays the single entry point. ui.js runs its server at top level and blocks;
190
+ // we spawn it as a child so cli.js does not need to import server-lifecycle code
191
+ // and so Ctrl-C from the terminal terminates the child cleanly.
192
+ function cmdUi(flags) {
193
+ const uiPath = path.join(path.dirname(new URL(import.meta.url).pathname), "ui.js");
194
+ const env = { ...process.env };
195
+ if (flags.port) env.EXTERNAL_AGENTS_UI_PORT = String(flags.port);
196
+ if (flags.host) env.EXTERNAL_AGENTS_UI_HOST = String(flags.host);
197
+ const child = spawn(process.execPath, [uiPath], { stdio: "inherit", env });
198
+ child.on("exit", (code) => process.exit(code ?? 0));
199
+ process.on("SIGINT", () => child.kill("SIGINT"));
200
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
201
+ }
202
+
187
203
  // --- entrypoint ---------------------------------------------------
188
204
  const [, , subcmd, ...rest] = process.argv;
189
205
  const { args, flags } = parseArgs(rest);
@@ -194,6 +210,7 @@ switch (subcmd) {
194
210
  case "status": cmdStatus(flags); break;
195
211
  case "probe": cmdProbe(args); break;
196
212
  case "stats": cmdStats(flags); break;
213
+ case "ui": cmdUi(flags); break;
197
214
  case "help":
198
215
  case "--help":
199
216
  case undefined:
@@ -202,7 +219,8 @@ switch (subcmd) {
202
219
  dispatch <agent-id> [--pro] [--transport generate_new|edit_exists] "<prompt>"
203
220
  status [--json]
204
221
  probe <agent-id>
205
- stats [--since ISO] [--json]`);
222
+ stats [--since ISO] [--json]
223
+ ui [--port N] [--host H] # local dashboard for setting keys + inspecting state (default http://127.0.0.1:4711)`);
206
224
  process.exit(subcmd ? 0 : 2);
207
225
  default: die(`unknown subcommand: ${subcmd}`, 2);
208
226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/external-agents",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "One MCP server for every LLM you talk to — direct-API dispatcher across Gemini, DeepSeek, Groq, OpenRouter, Cerebras, and more. Part of mrrlin.com.",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/ui.js CHANGED
@@ -34,9 +34,13 @@ function saveKeysFile(kv) {
34
34
  fs.renameSync(tmp, KEYS_FILE);
35
35
  }
36
36
 
37
- const REGISTRY = loadRegistry("./agents.yaml");
38
- const HOST = "127.0.0.1";
39
- const PORT = 4711;
37
+ // Resolve agents.yaml relative to this file so `external-agents ui` works from
38
+ // any cwd (previously the "./agents.yaml" relative path only worked from the
39
+ // package root).
40
+ const __ui_dir = path.dirname(new URL(import.meta.url).pathname);
41
+ const REGISTRY = loadRegistry(path.join(__ui_dir, "agents.yaml"));
42
+ const HOST = process.env.EXTERNAL_AGENTS_UI_HOST || "127.0.0.1";
43
+ const PORT = Number(process.env.EXTERNAL_AGENTS_UI_PORT) || 4711;
40
44
 
41
45
  function stateRows() {
42
46
  const state = readState();