agent-from-zero 0.39.0 → 0.39.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.
package/dist/agent.js CHANGED
@@ -532,6 +532,9 @@ async function main() {
532
532
  }
533
533
  apply(models[choice]); // picker is session-only too; "/model save <name>" to persist
534
534
  };
535
+ // The login link for /mcp auth. This REPL waits on the login, so a browser on
536
+ // another machine (SSH) gets its redirect here through curl, run on this one.
537
+ const showAuthUrl = (url) => console.log(chalk.dim(`authenticate in your browser: ${url}\n browser on another machine (SSH)? after authorizing, run here in another terminal: curl '<the address it lands on>'`));
535
538
  // /mcp — list configured MCP servers + status, then manage one (like Claude Code).
536
539
  const mcpStatusText = (status) => status === "connected" ? chalk.green("connected") : status === "needs-auth" ? chalk.yellow("needs auth") : status === "failed" ? chalk.red("failed") : status === "pending" ? chalk.dim("connecting…") : chalk.dim("disabled");
537
540
  const handleMcpCommand = async (arg) => {
@@ -546,7 +549,7 @@ async function main() {
546
549
  const name = parts.slice(1).join(" ");
547
550
  const action = MCP_SUBCOMMANDS[parts[0]];
548
551
  try {
549
- console.log(chalk.dim(await runMcpAction(name, action, (url) => console.log(chalk.dim(`authenticate in your browser: ${url}`)))));
552
+ console.log(chalk.dim(await runMcpAction(name, action, showAuthUrl)));
550
553
  }
551
554
  catch (err) {
552
555
  console.log(chalk.yellow(err.message));
@@ -578,7 +581,7 @@ async function main() {
578
581
  return;
579
582
  }
580
583
  try {
581
- console.log(chalk.dim(await runMcpAction(server.name, actions[actionChoice].action, (url) => console.log(chalk.dim(`authenticate in your browser: ${url}`)))));
584
+ console.log(chalk.dim(await runMcpAction(server.name, actions[actionChoice].action, showAuthUrl)));
582
585
  }
583
586
  catch (err) {
584
587
  console.log(chalk.yellow(err.message));
package/dist/ink/app.js CHANGED
@@ -44,7 +44,7 @@ import { detailPage, summarizeActivity } from "./activity.js";
44
44
  import { useTerminalSize } from "./viewport.js";
45
45
  import { makeInkSink } from "./sink.js"; // turns the loop's output into React state
46
46
  import { runInfoCommand, mcpStatusText } from "./commands.js"; // the non-interactive slash commands + /mcp status
47
- import { listMcpServers, MCP_SUBCOMMANDS, mcpActionsFor, mcpProblems, mcpServerDetails, reloadMcpServers, runMcpAction } from "../mcp.js"; // /mcp: list + per-server actions
47
+ import { listMcpServers, MCP_SUBCOMMANDS, mcpActionsFor, mcpProblems, mcpServerDetails, reloadMcpServers, runMcpAction, oauthLoginPending, cancelOAuthLogins, deliverOAuthRedirect } from "../mcp.js"; // /mcp: list + per-server actions, and the login that waits for its browser
48
48
  // Injected when plan mode turns on, so the model knows the rules it now lives in.
49
49
  const PLAN_MODE_NOTICE = `[plan mode ON] Investigate this request using only read-only tools — read_file, search, and safe read-only shell (ls, cat, git status). Do NOT write files, edit, or run mutating commands; the permission gate will block them. When you have a concrete, ordered plan, call the exit_plan_mode tool with that plan. The user reviews and approves it before you make any change.`;
50
50
  // No ✳ (U+2733): it has an emoji presentation, and terminals that honour it draw
@@ -508,6 +508,16 @@ export function App({ session, runTurn, clipboard }) {
508
508
  const submitLine = async (draft, attachments = [], immediate = false) => {
509
509
  peerTurns.current = 0; // a human is here: peers may start turns again
510
510
  peerHoldNoted.current = false;
511
+ // An MCP login is waiting for its browser redirect: what you submit is the
512
+ // address the browser landed on, or nothing for the model yet.
513
+ if (oauthLoginPending()) {
514
+ const got = deliverOAuthRedirect(pastedTexts.expand(draft));
515
+ if (got === "delivered")
516
+ return note(chalk.dim("(got the authorization redirect — finishing the login…)"));
517
+ return note(chalk.yellow(got === "stale"
518
+ ? "That address belongs to another login attempt — paste the one from this login, or Esc to cancel."
519
+ : "An MCP login is waiting: paste the address the browser landed on (http://localhost:…/callback?code=…), or Esc to cancel."));
520
+ }
511
521
  // Talk mode: plain text goes to the chosen session, even while this agent works.
512
522
  if (talkTo && !pending && !draft.startsWith("/")) {
513
523
  const text = pastedTexts.expand(draft);
@@ -617,6 +627,25 @@ export function App({ session, runTurn, clipboard }) {
617
627
  current.controller.abort();
618
628
  note(chalk.dim("(interrupting the current step to process your follow-up)"));
619
629
  };
630
+ // Run one /mcp action. A login waits for its browser redirect (up to 5
631
+ // minutes): say so on the spinner line, let Esc cancel it, and accept the
632
+ // redirected address pasted into the input box — the way in when the browser
633
+ // runs on another machine (SSH), where localhost is not this one.
634
+ const runMcpUi = async (name, action) => {
635
+ setBusy(true);
636
+ if (action === "authenticate")
637
+ setStatus(`Waiting for ${name} to authorize in the browser · paste the address it lands on here, or Esc to cancel`);
638
+ try {
639
+ note(chalk.dim(await runMcpAction(name, action, (url) => note(chalk.dim(`authenticate in your browser: ${url}\n browser on another machine (SSH)? after authorizing, paste the address it lands on (http://localhost:…/callback?code=…) here and press Enter`)))));
640
+ }
641
+ catch (err) {
642
+ note(chalk.yellow(err.message));
643
+ }
644
+ finally {
645
+ setBusy(false);
646
+ setStatus(null);
647
+ }
648
+ };
620
649
  // Handle a /slash command. Returns true if the line was a command (handled).
621
650
  // /mcp — list configured servers + status, then pick one to manage it.
622
651
  const handleMcpCommand = async (arg) => {
@@ -635,19 +664,7 @@ export function App({ session, runTurn, clipboard }) {
635
664
  }
636
665
  // Direct subcommands (scriptable, matching Claude Code): /mcp reconnect|auth|enable|disable <name>
637
666
  if (parts.length >= 2 && MCP_SUBCOMMANDS[parts[0]]) {
638
- const name = parts.slice(1).join(" ");
639
- setBusy(true);
640
- try {
641
- const action = MCP_SUBCOMMANDS[parts[0]];
642
- const msg = await runMcpAction(name, action, (url) => note(chalk.dim(`authenticate in your browser: ${url}`)));
643
- note(chalk.dim(msg));
644
- }
645
- catch (err) {
646
- note(chalk.yellow(err.message));
647
- }
648
- finally {
649
- setBusy(false);
650
- }
667
+ await runMcpUi(parts.slice(1).join(" "), MCP_SUBCOMMANDS[parts[0]]);
651
668
  return;
652
669
  }
653
670
  const servers = listMcpServers();
@@ -664,11 +681,7 @@ export function App({ session, runTurn, clipboard }) {
664
681
  openSelect(`${server.name} MCP Server`, actions.map((a) => a.label), (j) => {
665
682
  if (j < 0)
666
683
  return note(chalk.dim("(cancelled)"));
667
- setBusy(true);
668
- runMcpAction(server.name, actions[j].action, (url) => note(chalk.dim(`authenticate in your browser: ${url}`)))
669
- .then((msg) => note(chalk.dim(msg)))
670
- .catch((err) => note(chalk.yellow(err.message)))
671
- .finally(() => setBusy(false));
684
+ void runMcpUi(server.name, actions[j].action);
672
685
  }, mcpServerDetails(server)); // Claude Code's server screen: status, auth, protocol, URL, config, capabilities, tools
673
686
  });
674
687
  };
@@ -992,6 +1005,10 @@ export function App({ session, runTurn, clipboard }) {
992
1005
  statusRequest.current.abort();
993
1006
  return;
994
1007
  }
1008
+ if (key.escape && oauthLoginPending()) {
1009
+ cancelOAuthLogins();
1010
+ return;
1011
+ } // a /mcp login waiting for its browser
995
1012
  if (key.escape && talkTo && !pending && !input) {
996
1013
  setTalkTo(null);
997
1014
  note(chalk.dim(`(back to this agent — no longer talking to "${talkTo.name}")`));
@@ -1136,6 +1153,16 @@ export function App({ session, runTurn, clipboard }) {
1136
1153
  }
1137
1154
  // Ink 5 strips the leading ESC from CSI-u / modifyOtherKeys sequences.
1138
1155
  if (key.return || modifiedReturn) {
1156
+ // A /mcp login waits for its browser redirect: the UI is busy, but what
1157
+ // you submit is the address the browser landed on (submitLine takes it).
1158
+ if (oauthLoginPending()) {
1159
+ const text = input.trim();
1160
+ setInput("");
1161
+ setCursor(0);
1162
+ if (text)
1163
+ void submitLine(text);
1164
+ return;
1165
+ }
1139
1166
  if (submitting.current || (busy && !turn.current))
1140
1167
  return;
1141
1168
  const immediate = key.ctrl || modifiedReturn;
@@ -1348,5 +1375,5 @@ export function App({ session, runTurn, clipboard }) {
1348
1375
  }
1349
1376
  return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: paint(line, lineStarts[i] ?? 0, "l") })] }, i));
1350
1377
  });
1351
- })() })), completion.view, rows >= 20 && busy && !details && !selectedAgent && !pending && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Enter queue follow-up \u00B7 Ctrl+Enter interrupt and send now" }), rows >= 20 && !details && !selectedAgent && !pending && !completion.visible && _jsx(AgentList, { agents: agents, focus: subAgentFocus, viewing: subAgentDetail, mainBusy: busy, maxRows: Math.max(2, Math.min(3, rows - 24)) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, children: _jsx(StatusBar, { model: CONFIG.model, dir: dir, branch: branch, status: getStatus() }) }), _jsx(McpBadge, {})] }), (planMode || autoEnabled || CONFIG.bypassPermissions) && (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: planMode ? "magenta" : CONFIG.bypassPermissions ? "red" : "yellow", children: planMode ? "⏸ plan mode on" : CONFIG.bypassPermissions ? "▶▶ bypass permissions on" : "▶▶ auto mode on" }), _jsx(Text, { dimColor: true, children: planMode ? " (/plan to toggle)" : CONFIG.bypassPermissions ? " (this run)" : " (/auto to toggle)" })] }))] })] }));
1378
+ })() })), completion.view, rows >= 20 && busy && !details && !selectedAgent && !pending && !oauthLoginPending() && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Enter queue follow-up \u00B7 Ctrl+Enter interrupt and send now" }), rows >= 20 && !details && !selectedAgent && !pending && !completion.visible && _jsx(AgentList, { agents: agents, focus: subAgentFocus, viewing: subAgentDetail, mainBusy: busy, maxRows: Math.max(2, Math.min(3, rows - 24)) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, children: _jsx(StatusBar, { model: CONFIG.model, dir: dir, branch: branch, status: getStatus() }) }), _jsx(McpBadge, {})] }), (planMode || autoEnabled || CONFIG.bypassPermissions) && (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: planMode ? "magenta" : CONFIG.bypassPermissions ? "red" : "yellow", children: planMode ? "⏸ plan mode on" : CONFIG.bypassPermissions ? "▶▶ bypass permissions on" : "▶▶ auto mode on" }), _jsx(Text, { dimColor: true, children: planMode ? " (/plan to toggle)" : CONFIG.bypassPermissions ? " (this run)" : " (/auto to toggle)" })] }))] })] }));
1352
1379
  }
package/dist/mcp.js CHANGED
@@ -37,6 +37,10 @@ import { emit } from "./telemetry.js"; // observability
37
37
  const INIT_TIMEOUT_MS = 15_000; // a server that doesn't initialize in 15s is skipped
38
38
  const CALL_TIMEOUT_MS = 60_000; // a single tool call may take this long
39
39
  const OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60_000; // how long the local callback waits for the browser
40
+ // A startup connect that dies on the network (a dropped TLS handshake through a
41
+ // proxy, a timeout, a blip) is retried after these pauses before the server is
42
+ // marked failed; it shows as "connecting…" meanwhile. Mutable for the tests.
43
+ export const MCP_CONNECT_RETRY = { delaysMs: [2_000, 5_000, 10_000] };
40
44
  const PROTOCOL_VERSION = "2024-11-05"; // the MCP revision we speak in the handshake
41
45
  const CLIENT_INFO = { name: "mini-agent", version: "0" }; // who we say we are in the handshake
42
46
  // Thrown when an HTTP MCP server answers 401 — the caller marks the server as
@@ -59,6 +63,15 @@ export class NeedsAuthError extends Error {
59
63
  }
60
64
  }
61
65
  }
66
+ // The request never reached the server or its answer never came back: no
67
+ // connection, a dropped TLS handshake, a timeout. Unlike an error the server
68
+ // returns, trying again later can succeed.
69
+ export class McpNetworkError extends Error {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = "McpNetworkError";
73
+ }
74
+ }
62
75
  // ---- stdio transport: one JSON object per line, to a subprocess --------------
63
76
  class StdioTransport {
64
77
  child;
@@ -268,7 +281,11 @@ class HttpTransport {
268
281
  catch (err) {
269
282
  if (err instanceof NeedsAuthError)
270
283
  throw err; // propagate so the registry can mark needs-auth
271
- return { id: payload.id, error: { message: `MCP HTTP request '${payload.method}' failed: ${err.message}` } };
284
+ // fetch says only "fetch failed"; the reason (UND_ERR_CONNECT_TIMEOUT,
285
+ // ECONNRESET, ENOTFOUND…) is on its cause, and it is what you need to know.
286
+ const cause = err.cause;
287
+ const why = cause?.code ?? cause?.message;
288
+ return { id: payload.id, error: { message: `MCP HTTP request '${payload.method}' failed: ${err.message}${why ? ` (${why})` : ""}`, network: true } };
272
289
  }
273
290
  }
274
291
  notify(method, params) {
@@ -312,7 +329,7 @@ class McpClient {
312
329
  async start() {
313
330
  const init = await this.request("initialize", { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: CLIENT_INFO }, INIT_TIMEOUT_MS);
314
331
  if (init.error)
315
- throw new Error(init.error.message); // server refused to initialize
332
+ throw init.error.network ? new McpNetworkError(init.error.message) : new Error(init.error.message); // unreachable, or the server refused to initialize
316
333
  this.protocolVersion = init.result?.protocolVersion;
317
334
  this.capabilities = Object.keys(init.result?.capabilities ?? {});
318
335
  this.transport.notify("notifications/initialized", {}); // tell the server we're ready
@@ -325,7 +342,7 @@ class McpClient {
325
342
  async listTools() {
326
343
  const list = await this.request("tools/list", {}, INIT_TIMEOUT_MS);
327
344
  if (list.error)
328
- throw new Error(list.error.message);
345
+ throw list.error.network ? new McpNetworkError(list.error.message) : new Error(list.error.message);
329
346
  return list.result?.tools ?? [];
330
347
  }
331
348
  // Invoke one tool and flatten its content into plain text for the model.
@@ -355,9 +372,17 @@ const registry = new Map();
355
372
  // scratch path so a fake server's tokens never land in the real store).
356
373
  const OAUTH_STORE_PATH = process.env.MINI_AGENT_OAUTH_STORE || path.join(os.homedir(), ".config", "mini-agent", "mcp-oauth.json");
357
374
  const expiryFrom = (t) => (typeof t.expires_in === "number" ? Date.now() + t.expires_in * 1000 : undefined);
375
+ // The file holds bearer tokens, so only its owner may read it (0600): set on
376
+ // every write, and tightened on read for a store created before this rule or
377
+ // copied over from another machine.
358
378
  function readOAuthStore() {
359
379
  try {
360
- return JSON.parse(fs.readFileSync(OAUTH_STORE_PATH, "utf8"));
380
+ const store = JSON.parse(fs.readFileSync(OAUTH_STORE_PATH, "utf8"));
381
+ try {
382
+ fs.chmodSync(OAUTH_STORE_PATH, 0o600);
383
+ }
384
+ catch { /* not ours to fix (read-only mount) — still usable */ }
385
+ return store;
361
386
  }
362
387
  catch {
363
388
  return {};
@@ -365,7 +390,8 @@ function readOAuthStore() {
365
390
  }
366
391
  function writeOAuthStore(store) {
367
392
  fs.mkdirSync(path.dirname(OAUTH_STORE_PATH), { recursive: true });
368
- fs.writeFileSync(OAUTH_STORE_PATH, JSON.stringify(store, null, 2) + "\n");
393
+ fs.writeFileSync(OAUTH_STORE_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
394
+ fs.chmodSync(OAUTH_STORE_PATH, 0o600); // mode applies only when the file is created
369
395
  }
370
396
  const oauthStore = readOAuthStore();
371
397
  function base64url(input) {
@@ -475,8 +501,48 @@ export async function discoverOAuth(serverUrl, challenge = {}) {
475
501
  // DCR case, where we register whatever we got); a fixed port is what a
476
502
  // pre-registered client demands. The code promise is armed on creation, BEFORE
477
503
  // the browser opens, so a fast callback can't race us.
504
+ // Logins waiting for their browser redirect, by OAuth state. The loopback
505
+ // listener is one way in; a pasted redirect address is the other, for a browser
506
+ // on another machine (SSH, a remote box) whose "localhost" is not this one.
507
+ const waitingLogins = new Map();
508
+ export const oauthLoginPending = () => waitingLogins.size > 0;
509
+ // Esc while a login waits: give up on it (the flow rejects, its listener closes).
510
+ export function cancelOAuthLogins() {
511
+ for (const login of [...waitingLogins.values()])
512
+ login.cancel();
513
+ }
514
+ // The user pasted the address the browser landed on — on its own, or inside a
515
+ // sentence ("curl this: http://localhost:37831/callback?code=…"). Hand it to the
516
+ // login waiting for its state, exactly as if the browser had reached us.
517
+ // "stale": a callback address no waiting login owns (an older attempt, a typo);
518
+ // "none": no callback address in the text at all.
519
+ export function deliverOAuthRedirect(text) {
520
+ const found = /(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\/callback\?[^\s"'<>]+/.exec(text);
521
+ if (!found)
522
+ return "none";
523
+ let url;
524
+ try {
525
+ url = new URL(found[0].replace(/[)\]}。,、;,;!!??]+$/, "").replace(/^(?!https?:\/\/)/, "http://")); // trailing sentence punctuation is not part of it
526
+ }
527
+ catch {
528
+ return "none";
529
+ }
530
+ const login = waitingLogins.get(url.searchParams.get("state") ?? "");
531
+ if (!login)
532
+ return "stale";
533
+ login.accept(url);
534
+ return "delivered";
535
+ }
478
536
  async function listenForCallback(port, state) {
479
537
  let settle;
538
+ // The redirect's verdict, however it arrives (the browser, or a pasted address).
539
+ const accept = (url) => {
540
+ const code = url.searchParams.get("code");
541
+ if (code)
542
+ settle?.resolve(code);
543
+ else
544
+ settle?.reject(new Error(`OAuth callback did not include a code: ${url.searchParams.get("error_description") ?? url.searchParams.get("error") ?? "unknown error"}`));
545
+ };
480
546
  const handler = (req, res) => {
481
547
  const url = new URL(req.url ?? "/", "http://localhost");
482
548
  res.setHeader("connection", "close"); // one-shot listener: a kept-alive socket would deliver a LATER flow's callback (same fixed port) to this dead handler
@@ -486,13 +552,7 @@ async function listenForCallback(port, state) {
486
552
  res.end("<h1>Connected</h1><p>You can close this window and return to the terminal.</p>");
487
553
  // Settle only once the page has been flushed: settling tears the servers
488
554
  // down, and tearing down before the write lands would blank the browser tab.
489
- res.once("finish", () => {
490
- const code = url.searchParams.get("code");
491
- if (code)
492
- settle?.resolve(code);
493
- else
494
- settle?.reject(new Error(`OAuth callback did not include a code: ${url.searchParams.get("error_description") ?? url.searchParams.get("error") ?? "unknown error"}`));
495
- });
555
+ res.once("finish", () => accept(url));
496
556
  };
497
557
  const v4 = createServer(handler);
498
558
  const bound = await new Promise((resolve, reject) => {
@@ -508,6 +568,7 @@ async function listenForCallback(port, state) {
508
568
  v6.listen(bound, "::1", () => resolve());
509
569
  });
510
570
  const close = () => {
571
+ waitingLogins.delete(state);
511
572
  // Release the port for real: close() alone leaves open sockets alive, and a
512
573
  // pre-registered client's next flow needs the SAME port back immediately.
513
574
  for (const s of [v4, v6]) {
@@ -525,6 +586,7 @@ async function listenForCallback(port, state) {
525
586
  reject: (e) => (clearTimeout(timer), close(), reject(e)),
526
587
  };
527
588
  });
589
+ waitingLogins.set(state, { accept, cancel: () => settle?.reject(new Error("Authorization cancelled")) });
528
590
  return { port: bound, close, code };
529
591
  }
530
592
  // Run the OAuth authorization-code flow for an HTTP server: PKCE always;
@@ -688,8 +750,10 @@ function markNeedsAuth(name, challenge) {
688
750
  }
689
751
  // Connect a single configured server and register its tools. Updates the
690
752
  // registry with the live status. Used by connectMcpServers() at startup and by
691
- // /mcp reconnect / enable / auth.
692
- async function connectOne(name, retried = false, quiet = false) {
753
+ // /mcp reconnect / enable / auth. `retryDelays`: pauses before retrying a
754
+ // network failure (background startup only — an explicit /mcp action answers
755
+ // at once, and its user can simply run it again).
756
+ async function connectOne(name, retried = false, quiet = false, retryDelays = []) {
693
757
  const entry = entryFor(name);
694
758
  const attempt = {}; // this connect's identity (see the guard after the handshake)
695
759
  entry.attempt = attempt;
@@ -735,6 +799,17 @@ async function connectOne(name, retried = false, quiet = false) {
735
799
  return connectOne(name, true, quiet);
736
800
  markNeedsAuth(name, err);
737
801
  }
802
+ else if (err instanceof McpNetworkError && retryDelays.length) {
803
+ // Unreachable this time: stay "connecting" and try again shortly, unless
804
+ // the user disabled or reconnected it meanwhile (a newer attempt).
805
+ entry.status = "pending";
806
+ entry.error = err.message;
807
+ await new Promise((resolve) => setTimeout(resolve, retryDelays[0]).unref());
808
+ const now = registry.get(name); // re-read: /mcp may have changed it while we waited
809
+ if (now !== entry || now.attempt !== attempt || now.status === "disabled")
810
+ return;
811
+ return connectOne(name, retried, quiet, retryDelays.slice(1));
812
+ }
738
813
  else {
739
814
  entry.status = "failed";
740
815
  entry.error = err.message;
@@ -800,7 +875,7 @@ export async function connectMcpServers(options = {}) {
800
875
  for (const name of entries)
801
876
  entryFor(name).status = "pending";
802
877
  for (const name of entries)
803
- void connectOne(name, false, true); // fire and forget; connectOne never throws
878
+ void connectOne(name, false, true, MCP_CONNECT_RETRY.delaysMs); // fire and forget; connectOne never throws
804
879
  }
805
880
  else {
806
881
  for (const name of entries)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-from-zero",
3
- "version": "0.39.0",
3
+ "version": "0.39.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mini-agent": "dist/agent.js"