@mrrlin-dev/external-agents 0.3.1 → 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 (2) hide show
  1. package/package.json +1 -1
  2. package/ui.js +40 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/external-agents",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",
package/ui.js CHANGED
@@ -313,20 +313,28 @@ function renderUnlock(agents) {
313
313
  // Ollama-cloud uses its own CLI ('ollama' — no API key input needed),
314
314
  // so its row skips the input + Save and just shows the signup/download link.
315
315
  const hasEnvInput = !!m.env && !m.env.startsWith("(");
316
+ // Shared button metrics so Save + Get-free-key line up pixel-perfect.
317
+ // 32px total height (7px vertical padding + 14px font + border), 13px font,
318
+ // min-width so short-text buttons ("Save") do not feel dwarfed next to
319
+ // long-text ones ("Get free key ↗").
320
+ const btnStyle = 'display:inline-flex;align-items:center;justify-content:center;height:32px;box-sizing:border-box;padding:0 14px;min-width:110px;font-size:13px;font-weight:600;border-radius:4px;text-decoration:none;white-space:nowrap;border:1px solid transparent;cursor:pointer;';
321
+ const saveStyle = btnStyle + 'background:#4a90e2;color:#fff;border-color:#3878c0;';
322
+ const signupStyle = btnStyle + 'background:#4a8;color:#fff;';
323
+ const inputStyle = 'flex:1;min-width:180px;height:32px;box-sizing:border-box;padding:0 10px;border:1px solid #d4b96b;border-radius:4px;font:inherit;';
324
+
316
325
  const keyInput = hasEnvInput
317
326
  ? '<input id="k-' + m.env + '" type="password" placeholder="paste ' + m.env + ' here" ' +
318
- 'style="flex:1;min-width:180px;padding:5px 8px;border:1px solid #d4b96b;border-radius:4px;font:inherit;" ' +
319
- 'onkeydown="if(event.key===\\'Enter\\')saveKey(\\'' + m.env + '\\')">' +
320
- '<button class="primary" onclick="saveKey(\\'' + m.env + '\\')">Save</button>' +
327
+ 'style="' + inputStyle + '" onkeydown="if(event.key===\\'Enter\\')saveKey(\\'' + m.env + '\\')">' +
328
+ '<button onclick="saveKey(\\'' + m.env + '\\')" style="' + saveStyle + '">Save</button>' +
321
329
  '<span id="s-' + m.env + '" class="status" style="font-size:12px;"></span>'
322
330
  : '<span style="color:#8a7532;font-size:12px;">' + m.env + '</span>';
323
331
  return '<div class="row" style="display:grid;grid-template-columns:180px 1fr auto;gap:10px 16px;align-items:start;padding:12px 0;border-top:1px solid #e6d78e;">' +
324
332
  '<div><div class="prov">' + m.label + '</div>' +
325
333
  '<div style="font-size:11px;color:#8a7532">+' + count + ' model' + (count>1?"s":"") + ' waiting</div></div>' +
326
334
  '<div><div class="desc" style="margin-bottom:6px;">' + m.pitch + '</div>' +
327
- '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">' + keyInput + '</div></div>' +
335
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">' + keyInput + '</div></div>' +
328
336
  '<a class="signup" href="' + m.signup + '" target="_blank" rel="noopener" ' +
329
- 'style="align-self:center;padding:6px 12px;background:#4a8;color:#fff;text-decoration:none;border-radius:4px;font-size:13px;white-space:nowrap;">Get free key ↗</a>' +
337
+ 'style="align-self:center;' + signupStyle + '">Get free key ↗</a>' +
330
338
  '</div>';
331
339
  }).join("");
332
340
  box.innerHTML =
@@ -350,9 +358,14 @@ async function saveKey(envName) {
350
358
  });
351
359
  const j = await r.json();
352
360
  if (r.ok) {
353
- stat.textContent = "✓ persisted to " + j.persisted_to + ". Restart your MCP client to pick it up.";
361
+ const nProbed = (j.reprobed || []).length;
362
+ stat.textContent = "✓ persisted to keys.env" + (nProbed ? " · " + nProbed + " entries re-probed" : "");
354
363
  stat.className = "status";
355
364
  inp.value = "";
365
+ // Immediately refresh the table + banner so freshly-unlocked entries
366
+ // drop from the banner without a manual page reload. State has already
367
+ // been server-side re-probed by /api/set_credential.
368
+ await refresh();
356
369
  } else {
357
370
  stat.textContent = "error: " + (j.error || r.statusText);
358
371
  stat.className = "status err";
@@ -415,11 +428,31 @@ const server = http.createServer(async (req, res) => {
415
428
  // restart to see it. We tell them so in the response.
416
429
  process.env[env_name] = value;
417
430
  console.error(`external-agents ui: credential persisted for ${env_name} (${value.length} chars)`);
431
+ // Re-probe every registry entry that references this env var — either
432
+ // via its `auth: "env:XYZ"` field or its `transports.generate_new.env`.
433
+ // Without this, state.json keeps its stale `needs_auth` marker until
434
+ // the operator restarts the process, and the banner keeps counting
435
+ // just-unlocked entries as still-locked. State + banner reconcile now.
436
+ const affected = REGISTRY.agents.filter((a) => {
437
+ const authVar = typeof a.auth === "string" && a.auth.startsWith("env:")
438
+ ? a.auth.slice("env:".length).split(/\s+/)[0]
439
+ : null;
440
+ const genVar = a.transports?.generate_new?.env || null;
441
+ return authVar === env_name || genVar === env_name;
442
+ });
443
+ const patch = {};
444
+ for (const a of affected) {
445
+ const r = probeInstalled(a);
446
+ patch[a.id] = { ...r, checked: Math.floor(Date.now() / 1000) };
447
+ }
448
+ if (Object.keys(patch).length > 0) writeState(patch);
449
+ console.error(`external-agents ui: re-probed ${affected.length} entries after set_credential(${env_name}): ${affected.map((a) => a.id).join(", ")}`);
418
450
  return json(res, 200, {
419
451
  ok: true,
420
452
  env_name,
421
453
  persisted_to: KEYS_FILE,
422
- restart_required: "Restart your MCP client (Claude Code / Codex) to pick up the new key.",
454
+ reprobed: affected.map((a) => a.id),
455
+ restart_required: "Restart your MCP client (Claude Code / Codex) so IT reads keys.env too.",
423
456
  });
424
457
  } catch (e) {
425
458
  return json(res, 400, { error: "invalid json: " + e.message });