@ar-ai/brink-cli 1.0.6 → 1.0.9

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/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Brink
2
2
 
3
- **Made by rexy and brink.** Brink is a local AgentRouter proxy for Claude Code and Codex. It provides guided client setup, protocol bridges, bounded retries for transient provider failures, and a terminal dashboard.
3
+ made by rexy and brink
4
+ compiled by cr33p1ngp4ck3t/creepingpacket
5
+
6
+ Brink is a local AgentRouter proxy for Claude Code and Codex. It provides guided client setup, protocol bridges, bounded retries for transient provider failures, and a terminal dashboard.
4
7
 
5
8
  ## Install and run
6
9
 
@@ -23,6 +26,18 @@ npx @ar-ai/brink-cli
23
26
 
24
27
  For a project-local installation, use `npm install @ar-ai/brink-cli`, then `npx brink`. The runtime supports Windows, macOS, and Linux; selected client installers require a supported 64-bit platform.
25
28
 
29
+ ## Retry waits
30
+
31
+ Brink retries temporary rate limits, server errors, and connection resets up to five times, using base waits of **5, 15, 30, 60, and 120 seconds** before each retry. A valid `Retry-After` header (seconds or an HTTP date) takes precedence when it requests a longer wait. Each cooldown also adds up to 10% random extra time, capped at five seconds, so users do not all retry together. Without longer server cooldowns, the waits total about four minutes, plus time spent on upstream attempts.
32
+
33
+ The client must stay connected while Brink waits. Cancellation stops pending retries. Requests are retried only before output begins; authentication, billing, and quota errors are returned immediately. Clients that explicitly own retries bypass this schedule. The dashboard reports each retry and its wait.
34
+
35
+ ## Update notices
36
+
37
+ When the proxy starts, Brink checks npm for a newer stable release in the background. If one is available, the activity log (or plain terminal output) shows **New version available** and the command `npm install --global @ar-ai/brink-cli@latest`. Stop Brink before updating, then restart it.
38
+
39
+ The check sends only a public package-metadata request to `registry.npmjs.org`; no AgentRouter key, prompts, or client settings are included. It has a three-second timeout. Offline or failed checks stay quiet, and Brink never updates itself automatically.
40
+
26
41
  ## Commands
27
42
 
28
43
  | Command | Purpose |
@@ -38,22 +53,3 @@ For a project-local installation, use `npm install @ar-ai/brink-cli`, then `npx
38
53
  `brink update` changes credentials. To update the software, stop Brink and run `npm install --global @ar-ai/brink-cli@latest`.
39
54
 
40
55
  Settings are saved in `~/.config/brink/config.json` (or `BRINK_CONFIG_HOME`). Setup also writes selected clients' user settings. Credentials are stored locally without encryption; Windows file access follows your profile permissions. Model access, quota, and billing remain controlled by AgentRouter.
41
-
42
- ## Develop and verify
43
-
44
- Run these commands from the repository root:
45
-
46
- ```sh
47
- npm ci
48
- node support/brink.js --help
49
- npm pack
50
- ```
51
-
52
- The help command checks CLI startup without provider calls or client settings changes. npm needs registry access or cached runtime dependencies.
53
-
54
- Before publishing, you can distribute and install the tarball locally:
55
-
56
- ```sh
57
- npm install --global ./ar-ai-brink-cli-1.0.4.tgz
58
- brink --help
59
- ```
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@ar-ai/brink-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "description": "Local AgentRouter proxy and guided setup for Claude Code and Codex.",
5
5
  "author": "rexy and brink",
6
+ "contributors": ["cr33p1ngp4ck3t/creepingpacket"],
6
7
  "license": "UNLICENSED",
7
8
  "type": "commonjs",
8
9
  "bin": {
9
10
  "brink": "support/brink.js"
10
11
  },
11
12
  "files": [
12
- "support/*.js"
13
+ "support/*.js",
14
+ "support/package.json"
13
15
  ],
14
16
  "engines": {
15
17
  "node": ">=24"
package/support/brand.js CHANGED
@@ -107,4 +107,4 @@ function mascotLines(width, maxRows) {
107
107
  return [];
108
108
  }
109
109
 
110
- module.exports = { duck, cat, mascotLines, credit: "made by rexy and brink" };
110
+ module.exports = { duck, cat, mascotLines, credit: "made by rexy and brink\ncompiled by cr33p1ngp4ck3t/creepingpacket" };
package/support/brink.js CHANGED
@@ -136,7 +136,7 @@ async function setup() {
136
136
  "Run brink and keep that terminal open.",
137
137
  ...ready.map((client) => `In a second terminal, run ${results[client].executable || client}.`),
138
138
  "In either client, use /model to choose a model.",
139
- "Rate-limit, server-error and connection-reset retry waits: 1, 2, 3, 4, 5 seconds. No model substitution.",
139
+ "Rate-limit, server-error and connection-reset base retry waits: 5, 15, 30, 60, 120 seconds; Retry-After and small random delays can extend them. No model substitution.",
140
140
  ...(failures.length ? ["Some clients were not changed. Run brink setup again to finish them."] : []),
141
141
  ]);
142
142
  } else { ui.step("No client was configured", "Fix the issue above, then run brink setup again."); }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@ar-ai/brink-cli",
3
+ "version": "1.0.9",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "engines": { "node": ">=24" },
7
+ "dependencies": {
8
+ "toml-eslint-parser": "1.0.3"
9
+ }
10
+ }
package/support/proxy.js CHANGED
@@ -6,6 +6,7 @@ const { bridgeMessages } = require("./messages-bridge");
6
6
  const { adaptModelCatalog } = require("./model-catalog");
7
7
  const { createMonitor } = require("./monitor");
8
8
  const { startTerminalUI } = require("./terminal-ui");
9
+ const { checkForUpdate } = require("./update-notice");
9
10
  const { createUsageObserver } = require("./usage");
10
11
  const {
11
12
  cleanHeaders,
@@ -475,6 +476,7 @@ server.listen(LISTEN_PORT, LISTEN_HOST, () => {
475
476
  );
476
477
  monitor.log(
477
478
  "info",
478
- "AgentRouter upstream: https://agentrouter.org/v1; transient-failure retry waits 1/2/3/4/5 seconds before output (client-owned retries bypassed); no model or credential substitution",
479
+ "AgentRouter upstream: https://agentrouter.org/v1; transient-failure retry base waits 5/15/30/60/120 seconds, respecting Retry-After plus jitter before output (client-owned retries bypassed); no model or credential substitution",
479
480
  );
481
+ void checkForUpdate((message) => monitor.log("info", message));
480
482
  });
@@ -51,7 +51,6 @@ function createSetupUI({ input = process.stdin, output = process.stdout } = {})
51
51
  { text: detail, tone: "dim" },
52
52
  { text: "", tone: "dim" },
53
53
  { text: "Start with brink", tone: "amber" },
54
- { text: credit, tone: "dim" },
55
54
  ];
56
55
  const room = width();
57
56
  const rows = output.rows || 24;
@@ -72,8 +71,8 @@ function createSetupUI({ input = process.stdin, output = process.stdout } = {})
72
71
  line("BRINK", "section");
73
72
  line(title, "bold");
74
73
  line(detail, "dim");
75
- line(credit, "dim");
76
74
  }
75
+ for (const text of credit.split("\n")) line(text, "dim");
77
76
  line("─".repeat(room), "dim");
78
77
  }
79
78
 
@@ -371,13 +371,14 @@ function startTerminalUI(monitor, { clients = [] } = {}) {
371
371
  const rows = Math.max(1, output.rows || 24);
372
372
  const resized = columns !== previousColumns || rows !== previousRows;
373
373
  const frame = masthead(columns, rows);
374
+ const creditLines = credit.split("\n");
374
375
  if (columns < 48 || rows < 12) {
375
376
  const contentWidth = Math.max(1, columns - 2);
376
377
  const traffic = trafficLines(contentWidth);
377
378
  const logs = logLines(contentWidth);
378
379
  lastTrafficLines = traffic.length;
379
380
  lastLogLines = logs.lines.length;
380
- const footerRows = rows >= 12 ? 3 : 1;
381
+ const footerRows = rows >= 12 ? 2 + creditLines.length : 1;
381
382
  const availableRows = Math.max(0, rows - frame.length - footerRows);
382
383
  bodyHeight = Math.max(0, Math.floor((availableRows - 2) / 2));
383
384
  logBodyHeight = Math.max(0, availableRows - bodyHeight - 2);
@@ -398,9 +399,9 @@ function startTerminalUI(monitor, { clients = [] } = {}) {
398
399
  while (frame.length < rows - footerRows) frame.push(paint("", columns));
399
400
  frame.length = Math.min(frame.length, Math.max(0, rows - footerRows));
400
401
  frame.push(paint(searching ? ` Search: ${searchDraft}_` : " q Quit / Search p Pause c Clear f Level", columns, "text"));
401
- if (footerRows === 3) {
402
+ if (footerRows > 1) {
402
403
  frame.push(paint(" Arrows Logs | j/k Overview | v Preview", columns, "muted"));
403
- frame.push(paint(` ${credit}`, columns, "muted"));
404
+ for (const text of creditLines) frame.push(paint(` ${text}`, columns, "muted"));
404
405
  }
405
406
  } else {
406
407
  const leftWidth = Math.floor((columns - 1) / 2);
@@ -417,7 +418,7 @@ function startTerminalUI(monitor, { clients = [] } = {}) {
417
418
  paint("│", 1, "border") +
418
419
  paint(rightHeader, rightWidth, "section", true),
419
420
  );
420
- bodyHeight = Math.max(1, rows - frame.length - 4);
421
+ bodyHeight = Math.max(1, rows - frame.length - 3 - creditLines.length);
421
422
  logBodyHeight = bodyHeight;
422
423
  const traffic = trafficLines(leftContent);
423
424
  const logs = logLines(rightContent);
@@ -467,7 +468,7 @@ function startTerminalUI(monitor, { clients = [] } = {}) {
467
468
  ? " q Quit p Pause c Clear f Level / Search | Up/Down Logs | j/k Traffic v Preview"
468
469
  : " q Quit p Pause c Clear f Level / Search v Preview";
469
470
  frame.push(paint(help, columns, "text"));
470
- frame.push(paint(` ${credit}`, columns, "muted"));
471
+ for (const text of creditLines) frame.push(paint(` ${text}`, columns, "muted"));
471
472
  }
472
473
  let buffer = resized ? `${ESC}2J` : "";
473
474
  for (let i = 0; i < frame.length; i++) {
@@ -141,8 +141,12 @@ function retryableFailure(status, value, text = "") {
141
141
  return null;
142
142
  }
143
143
 
144
- function retryDelay(retry) {
145
- return retry * 1000;
144
+ function retryDelay(retry, retryAfter) {
145
+ const value = typeof retryAfter === "string" ? retryAfter.trim() : "";
146
+ const serverDelay = /^\d+$/.test(value) ? Number(value) * 1000 :
147
+ /^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(value) ? Date.parse(value) - Date.now() : 0;
148
+ const delay = Math.max([5_000, 15_000, 30_000, 60_000, 120_000][retry - 1], Number.isFinite(serverDelay) ? serverDelay : 0);
149
+ return delay + Math.floor(Math.random() * Math.min(delay * 0.1, 5000));
146
150
  }
147
151
 
148
152
  function cancelledError() {
@@ -151,21 +155,25 @@ function cancelledError() {
151
155
  });
152
156
  }
153
157
 
154
- // The fixed 1–5 second retry waits remain cancellable on client disconnect.
155
- function waitForRetry(delay, signal) {
156
- return new Promise((resolve, reject) => {
157
- if (signal?.aborted) return reject(cancelledError());
158
- const abort = () => {
159
- clearTimeout(timer);
160
- signal.removeEventListener("abort", abort);
161
- reject(cancelledError());
162
- };
163
- const timer = setTimeout(() => {
164
- signal?.removeEventListener("abort", abort);
165
- resolve();
166
- }, delay);
167
- signal?.addEventListener("abort", abort, { once: true });
168
- });
158
+ // Long server cooldowns use bounded timers and remain cancellable.
159
+ async function waitForRetry(delay, signal) {
160
+ while (delay > 0) {
161
+ const interval = Math.min(delay, 2_147_483_647);
162
+ await new Promise((resolve, reject) => {
163
+ if (signal?.aborted) return reject(cancelledError());
164
+ const abort = () => {
165
+ clearTimeout(timer);
166
+ signal.removeEventListener("abort", abort);
167
+ reject(cancelledError());
168
+ };
169
+ const timer = setTimeout(() => {
170
+ signal?.removeEventListener("abort", abort);
171
+ resolve();
172
+ }, interval);
173
+ signal?.addEventListener("abort", abort, { once: true });
174
+ });
175
+ delay -= interval;
176
+ }
169
177
  }
170
178
 
171
179
  function harmlessPrelude(value, event) {
@@ -396,7 +404,7 @@ async function requestWithRetries(target, options, body, {
396
404
  if (signal?.aborted) throw cancelledError();
397
405
  if (retryReason && !clientOwnsRetries && attempt < MAX_RETRIES) {
398
406
  const retry = attempt + 1;
399
- const delay = retryDelay(retry);
407
+ const delay = retryDelay(retry, response?.headers["retry-after"]);
400
408
  response?.destroy();
401
409
  request?.destroy();
402
410
  tracker?.note("warn", `${upstreamFailure(null, { code: retryReason })}; retry ${retry}/${MAX_RETRIES} in ${delay}ms`);
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ const { name, version } = require("./package.json");
4
+
5
+ function versionParts(value) {
6
+ if (typeof value !== "string" || !/^\d+\.\d+\.\d+$/.test(value)) return null;
7
+ const parts = value.split(".").map(Number);
8
+ return parts.every(Number.isSafeInteger) ? parts : null;
9
+ }
10
+
11
+ async function checkForUpdate(notify) {
12
+ try {
13
+ const response = await fetch('https://registry.npmjs.org/' + encodeURIComponent(name) + '/latest', {
14
+ headers: { accept: "application/json" },
15
+ signal: AbortSignal.timeout(3000),
16
+ redirect: "error",
17
+ });
18
+ if (!response.ok) { await response.body?.cancel(); return; }
19
+ const reader = response.body.getReader();
20
+ const chunks = [];
21
+ let bytes = 0;
22
+ for (;;) {
23
+ const { done, value } = await reader.read();
24
+ if (done) break;
25
+ bytes += value.byteLength;
26
+ if (bytes > 16 * 1024) { await reader.cancel(); return; }
27
+ chunks.push(Buffer.from(value));
28
+ }
29
+ const latest = JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"))?.version;
30
+ const candidate = versionParts(latest);
31
+ const current = versionParts(version);
32
+ if (!candidate || !current) return;
33
+ const differing = candidate.findIndex((part, index) => part !== current[index]);
34
+ if (differing >= 0 && candidate[differing] > current[differing]) {
35
+ notify('New version available: ' + version + ' -> ' + latest + '. Update: npm install --global ' + name + '@latest');
36
+ }
37
+ } catch {} // Registry outages must not interrupt the proxy.
38
+ }
39
+
40
+ module.exports = { checkForUpdate };