@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 +16 -20
- package/package.json +4 -2
- package/support/brand.js +1 -1
- package/support/brink.js +1 -1
- package/support/package.json +10 -0
- package/support/proxy.js +3 -1
- package/support/setup-ui.js +1 -2
- package/support/terminal-ui.js +6 -5
- package/support/transport.js +26 -18
- package/support/update-notice.js +40 -0
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Brink
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
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:
|
|
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."); }
|
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
|
|
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
|
});
|
package/support/setup-ui.js
CHANGED
|
@@ -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
|
|
package/support/terminal-ui.js
CHANGED
|
@@ -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 ?
|
|
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
|
|
402
|
+
if (footerRows > 1) {
|
|
402
403
|
frame.push(paint(" Arrows Logs | j/k Overview | v Preview", columns, "muted"));
|
|
403
|
-
frame.push(paint(` ${
|
|
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 -
|
|
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(` ${
|
|
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++) {
|
package/support/transport.js
CHANGED
|
@@ -141,8 +141,12 @@ function retryableFailure(status, value, text = "") {
|
|
|
141
141
|
return null;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function retryDelay(retry) {
|
|
145
|
-
|
|
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
|
-
//
|
|
155
|
-
function waitForRetry(delay, signal) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 };
|