@mrrlin-dev/external-agents 0.3.4 → 0.3.6

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 (3) hide show
  1. package/cli.js +13 -3
  2. package/lib/state.js +63 -32
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -227,10 +227,20 @@ function cmdInit(flags) {
227
227
  const child = spawn(process.execPath, [uiPath], { stdio: "inherit", env });
228
228
  // Give the UI ~600ms to bind before opening the browser (loopback listen is
229
229
  // usually instantaneous but we do not want the browser to open on a not-yet-
230
- // bound port).
230
+ // bound port). Browser-open is best-effort — swallow BOTH sync spawn errors
231
+ // AND async 'error' events (ENOENT is emitted async, not thrown; without a
232
+ // listener it crashes the process — this is what breaks curl|bash on a
233
+ // headless Linux box that has no xdg-open installed). The UI keeps running.
231
234
  setTimeout(() => {
232
- try { spawn(opener, openerArgs, { stdio: "ignore", detached: true }).unref(); }
233
- catch { /* browser open is best-effort; UI is still up on ${url} */ }
235
+ try {
236
+ const opener_proc = spawn(opener, openerArgs, { stdio: "ignore", detached: true });
237
+ opener_proc.on("error", (err) => {
238
+ console.error(`external-agents init: could not launch browser (${err.code || err.message}) — open ${url} manually.`);
239
+ });
240
+ opener_proc.unref();
241
+ } catch (err) {
242
+ console.error(`external-agents init: could not launch browser (${err.message}) — open ${url} manually.`);
243
+ }
234
244
  }, 600);
235
245
  child.on("exit", (code) => process.exit(code ?? 0));
236
246
  process.on("SIGINT", () => child.kill("SIGINT"));
package/lib/state.js CHANGED
@@ -57,25 +57,28 @@ export function writeState(patch) {
57
57
  }
58
58
  }
59
59
 
60
+ // Probe an agent's usability. Transport-aware — if EITHER transport is usable,
61
+ // the entry is healthy. This is important because most economy-flow entries
62
+ // (Groq, Cerebras, OpenRouter, Gemini, DeepSeek, etc.) work via generate_new
63
+ // (native fetch, no binary needed) and do NOT require aider. Previously the
64
+ // probe short-circuited on aider-missing and marked everything not_installed
65
+ // even when the operator only wanted native fetch — that broke the "just
66
+ // paste API keys and go" flow on any host without aider installed.
67
+ //
68
+ // Precedence:
69
+ // 1. generate_new usable (URL configured + env var set OR Ollama sentinel)
70
+ // → healthy. Aider is not required.
71
+ // 2. edit_exists usable (binary on PATH + auth satisfied) → healthy.
72
+ // 3. Neither → the more informative failure wins (needs_auth > not_installed).
60
73
  export function probeInstalled(agentEntry) {
61
- const cliCmd = agentEntry?.transports?.edit_exists;
62
- if (!cliCmd || typeof cliCmd !== "string") {
63
- return { state: "errored_transient", note: "no cli transport" };
64
- }
65
- const bin = cliCmd.trim().split(/\s+/)[0];
66
- const r = spawnSync("command", ["-v", bin], { shell: "/bin/bash" });
67
- if (r.status !== 0) {
68
- return { state: "not_installed", note: `binary missing: ${bin}` };
69
- }
70
-
71
- // Binary present. Check auth prerequisites:
72
- // - per-entry `env` override (with optional @file: refs) OR
73
- // - `auth: "env:XXX"` — the var must be in process.env
74
- // If a per-entry env override is present, it takes precedence: we verify
75
- // every @file: reference resolves. Otherwise fall back to auth-field.
76
74
  const auth = agentEntry.auth || "";
75
+ const gen = agentEntry.transports?.generate_new;
76
+ const cli = agentEntry.transports?.edit_exists;
77
+ if (!gen && !cli) return { state: "errored_transient", note: "no transport declared" };
77
78
 
78
- if (agentEntry.env && typeof agentEntry.env === "object") {
79
+ // Resolve per-entry env overrides once (used by both transport checks).
80
+ const envOverrideReady = (() => {
81
+ if (!agentEntry.env || typeof agentEntry.env !== "object") return { ok: true, note: null };
79
82
  for (const [k, v] of Object.entries(agentEntry.env)) {
80
83
  if (typeof v === "string" && v.startsWith("@file:")) {
81
84
  let p = v.slice("@file:".length);
@@ -84,30 +87,58 @@ export function probeInstalled(agentEntry) {
84
87
  const s = fs.statSync(p);
85
88
  if (!s.isFile()) throw new Error("not a regular file");
86
89
  } catch (e) {
87
- return { state: "needs_auth", note: `env override ${k}: cannot read ${p}` };
90
+ return { ok: false, note: `env override ${k}: cannot read ${p}` };
88
91
  }
89
92
  }
90
93
  }
91
- return { state: "healthy", note: `binary present: ${bin}; per-entry env resolved` };
92
- }
94
+ return { ok: true, note: null };
95
+ })();
93
96
 
94
- if (auth.startsWith("env:")) {
95
- const varName = auth.slice("env:".length).split(/\s+/)[0];
96
- if (!process.env[varName]) {
97
- return { state: "needs_auth", note: `env var ${varName} not set (add to shell or ADR-0016 store)` };
97
+ // ---- Attempt (1): generate_new transport is usable ---------------------
98
+ if (gen && gen.url) {
99
+ const genEnv = gen.env;
100
+ const isOllama = genEnv === "OLLAMA_UNUSED_KEY";
101
+ const envSatisfied = isOllama
102
+ || !genEnv // no env → no auth wall
103
+ || (agentEntry.env && agentEntry.env[genEnv]) // per-entry override
104
+ || !!process.env[genEnv]; // process env
105
+ if (envOverrideReady.ok && envSatisfied) {
106
+ return {
107
+ state: "healthy",
108
+ note: isOllama || !genEnv
109
+ ? `generate_new ready (no api key required)`
110
+ : `generate_new ready (${genEnv} set)`,
111
+ };
98
112
  }
99
113
  }
100
114
 
101
- // For entries with only generate transport (no cli), the "binary" is aider-agnostic
102
- // (native fetch). Also handle the Ollama sentinel — no real env var needed.
103
- const genEnv = agentEntry.transports?.generate_new?.env;
104
- if (genEnv && genEnv !== "OLLAMA_UNUSED_KEY" && !process.env[genEnv] && !(agentEntry.env && agentEntry.env[genEnv])) {
105
- // If we already passed the cli auth check above, this is a "generate needs its own env" case.
106
- // Skip when auth check already flagged the same var.
107
- if (!auth.startsWith("env:") || !auth.includes(genEnv)) {
108
- return { state: "needs_auth", note: `generate env var ${genEnv} not set` };
115
+ // ---- Attempt (2): edit_exists transport is usable ----------------------
116
+ if (cli && typeof cli === "string") {
117
+ const bin = cli.trim().split(/\s+/)[0];
118
+ const r = spawnSync("command", ["-v", bin], { shell: "/bin/bash" });
119
+ if (r.status === 0) {
120
+ if (!envOverrideReady.ok) return { state: "needs_auth", note: envOverrideReady.note };
121
+ // Auth wall from the top-level `auth:` field (subscription CLI vs env var).
122
+ if (auth.startsWith("env:")) {
123
+ const varName = auth.slice("env:".length).split(/\s+/)[0];
124
+ if (!process.env[varName]) {
125
+ return { state: "needs_auth", note: `env var ${varName} not set (paste via UI or run: external-agents set-credential ${varName})` };
126
+ }
127
+ }
128
+ return { state: "healthy", note: `binary present: ${bin}` };
109
129
  }
110
130
  }
111
131
 
112
- return { state: "healthy", note: `binary present: ${bin}` };
132
+ // ---- Both attempts failed pick the most informative failure ---------
133
+ if (gen && gen.url && gen.env && gen.env !== "OLLAMA_UNUSED_KEY") {
134
+ return {
135
+ state: "needs_auth",
136
+ note: `env var ${gen.env} not set (paste via UI or run: external-agents set-credential ${gen.env})`,
137
+ };
138
+ }
139
+ if (cli) {
140
+ const bin = cli.trim().split(/\s+/)[0];
141
+ return { state: "not_installed", note: `binary missing: ${bin}` };
142
+ }
143
+ return { state: "errored_transient", note: "no usable transport" };
113
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/external-agents",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "One MCP server for every LLM you talk to \u2014 direct-API dispatcher across Gemini, DeepSeek, Groq, OpenRouter, Cerebras, and more. Part of mrrlin.com.",
5
5
  "type": "module",
6
6
  "main": "server.js",