@ar-ai/brink-cli 0.0.0-stage → 1.0.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.
- package/README.md +58 -2
- package/package.json +18 -4
- package/support/activity.js +102 -0
- package/support/brand.js +110 -0
- package/support/bridge-stream.js +109 -0
- package/support/brink.js +275 -0
- package/support/chat-bridge.js +1093 -0
- package/support/client-setup.js +620 -0
- package/support/messages-bridge.js +317 -0
- package/support/messages-request.js +442 -0
- package/support/model-catalog.js +200 -0
- package/support/monitor.js +443 -0
- package/support/proxy.js +480 -0
- package/support/setup-ui.js +333 -0
- package/support/terminal-ui.js +630 -0
- package/support/transport.js +485 -0
- package/support/usage.js +144 -0
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Brink
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install and run
|
|
6
|
+
|
|
7
|
+
Install [Node.js 24 or newer](https://nodejs.org/en/download) with npm, then install Brink:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install --global @ar-ai/brink-cli
|
|
11
|
+
brink
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
On first run, Brink opens guided setup and starts the proxy after setup succeeds. Choose Claude Code, Codex, or both; enter your own AgentRouter API key at the hidden prompt and select a model. Setup can install a missing selected client. npm installation itself only installs Brink; it does not ask for credentials or change client settings.
|
|
15
|
+
|
|
16
|
+
Keep Brink open. In a second terminal, open your project and run `claude` or `codex`. Use `/model` inside the client. The proxy listens on `127.0.0.1:8787` by default. Stop it with Ctrl+C.
|
|
17
|
+
|
|
18
|
+
You can also run Brink without a global installation:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx @ar-ai/brink-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
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
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| Command | Purpose |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `brink` | Start the proxy, opening setup on first run. |
|
|
31
|
+
| `brink setup` | Install/connect clients or change models. |
|
|
32
|
+
| `brink start --plain` | Start the proxy with ordinary line logs. |
|
|
33
|
+
| `brink check` | Repair Brink-managed client settings. |
|
|
34
|
+
| `brink update` | Replace the saved API key through hidden input. |
|
|
35
|
+
| `brink art` | Display the supplied artwork. |
|
|
36
|
+
| `brink --help` | Display help. |
|
|
37
|
+
|
|
38
|
+
`brink update` changes credentials. To update the software, stop Brink and run `npm install --global @ar-ai/brink-cli@latest`.
|
|
39
|
+
|
|
40
|
+
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,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ar-ai/brink-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Local AgentRouter proxy and guided setup for Claude Code and Codex.",
|
|
5
|
+
"author": "rexy and brink",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"brink": "support/brink.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"support/*.js"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"toml-eslint-parser": "1.0.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// UI activity from public Responses output only. Never inspect prompts,
|
|
4
|
+
// arguments, tool-result bodies, encrypted_content, or raw reasoning content.
|
|
5
|
+
function observeActivity(value, tracker, { summaries = true } = {}) {
|
|
6
|
+
if (!tracker?.activity || !value || typeof value !== "object") return;
|
|
7
|
+
const type = value.type;
|
|
8
|
+
const id = String(value.item_id ?? value.output_index ?? "message");
|
|
9
|
+
if (
|
|
10
|
+
type === "response.output_text.delta" ||
|
|
11
|
+
type === "response.refusal.delta"
|
|
12
|
+
) {
|
|
13
|
+
if (typeof value.delta === "string")
|
|
14
|
+
tracker.activity({ kind: "assistant", id, text: value.delta });
|
|
15
|
+
} else if (
|
|
16
|
+
type === "response.output_text.done" ||
|
|
17
|
+
type === "response.refusal.done"
|
|
18
|
+
) {
|
|
19
|
+
const text = value.text ?? value.refusal;
|
|
20
|
+
if (typeof text === "string")
|
|
21
|
+
tracker.activity({ kind: "assistant", id, text, replace: true });
|
|
22
|
+
} else if (summaries && type === "response.reasoning_summary_text.delta") {
|
|
23
|
+
if (typeof value.delta === "string")
|
|
24
|
+
tracker.activity({ kind: "summary", id, text: value.delta });
|
|
25
|
+
} else if (summaries && type === "response.reasoning_summary_text.done") {
|
|
26
|
+
if (typeof value.text === "string")
|
|
27
|
+
tracker.activity({
|
|
28
|
+
kind: "summary",
|
|
29
|
+
id,
|
|
30
|
+
text: value.text,
|
|
31
|
+
replace: true,
|
|
32
|
+
});
|
|
33
|
+
} else if (
|
|
34
|
+
type === "response.output_item.added" ||
|
|
35
|
+
type === "response.output_item.done"
|
|
36
|
+
) {
|
|
37
|
+
item(value.item, tracker, type.endsWith(".done"), summaries);
|
|
38
|
+
}
|
|
39
|
+
const response =
|
|
40
|
+
value.response ?? (value.object === "response" ? value : null);
|
|
41
|
+
if (Array.isArray(response?.output)) {
|
|
42
|
+
for (const output of response.output)
|
|
43
|
+
item(output, tracker, response.status !== "in_progress", summaries);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function item(output, tracker, done, summaries) {
|
|
48
|
+
if (!output || typeof output !== "object") return;
|
|
49
|
+
const id = String(output.id ?? output.call_id ?? "message");
|
|
50
|
+
if (["function_call", "custom_tool_call"].includes(output.type)) {
|
|
51
|
+
const name = typeof output.name === "string" ? output.name : "tool";
|
|
52
|
+
tracker.activity({
|
|
53
|
+
kind: "tool",
|
|
54
|
+
id,
|
|
55
|
+
name: output.namespace ? `${output.namespace}.${name}` : name,
|
|
56
|
+
status: done ? "requested" : "preparing",
|
|
57
|
+
});
|
|
58
|
+
} else if (output.type === "message" && output.role === "assistant") {
|
|
59
|
+
const text = Array.isArray(output.content)
|
|
60
|
+
? output.content
|
|
61
|
+
.map((part) =>
|
|
62
|
+
part.type === "output_text"
|
|
63
|
+
? part.text
|
|
64
|
+
: part.type === "refusal"
|
|
65
|
+
? part.refusal
|
|
66
|
+
: "",
|
|
67
|
+
)
|
|
68
|
+
.filter((part) => typeof part === "string")
|
|
69
|
+
.join("")
|
|
70
|
+
: "";
|
|
71
|
+
if (text) tracker.activity({ kind: "assistant", id, text, replace: true });
|
|
72
|
+
} else if (output.type === "reasoning") {
|
|
73
|
+
tracker.activity({ kind: "reasoning" });
|
|
74
|
+
if (summaries && Array.isArray(output.summary)) {
|
|
75
|
+
const text = output.summary
|
|
76
|
+
.filter(
|
|
77
|
+
(part) =>
|
|
78
|
+
part.type === "summary_text" && typeof part.text === "string",
|
|
79
|
+
)
|
|
80
|
+
.map((part) => part.text)
|
|
81
|
+
.join("");
|
|
82
|
+
if (text) tracker.activity({ kind: "summary", id, text, replace: true });
|
|
83
|
+
}
|
|
84
|
+
} else if (
|
|
85
|
+
typeof output.type === "string" &&
|
|
86
|
+
[
|
|
87
|
+
"web_search_call",
|
|
88
|
+
"file_search_call",
|
|
89
|
+
"computer_call",
|
|
90
|
+
"code_interpreter_call",
|
|
91
|
+
].includes(output.type)
|
|
92
|
+
) {
|
|
93
|
+
tracker.activity({
|
|
94
|
+
kind: "tool",
|
|
95
|
+
id,
|
|
96
|
+
name: output.type,
|
|
97
|
+
status: done ? "requested" : "preparing",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { observeActivity };
|
package/support/brand.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const duck = `⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡶⠿⠿⠷⣶⣄⠀⠀⠀⠀⠀
|
|
4
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡿⠁⠀⠀⢀⣀⡀⠙⣷⡀⠀⠀⠀
|
|
5
|
+
⠀⠀⠀⡀⠀⠀⠀⠀⠀⢠⣿⠁⠀⠀⠀⠘⠿⠃⠀⢸⣿⣿⣿⣿
|
|
6
|
+
⠀⣠⡿⠛⢷⣦⡀⠀⠀⠈⣿⡄⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⠟
|
|
7
|
+
⢰⡿⠁⠀⠀⠙⢿⣦⣤⣤⣼⣿⣄⠀⠀⠀⠀⠀⢴⡟⠛⠋⠁⠀
|
|
8
|
+
⣿⠇⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠈⣿⡀⠀⠀⠀
|
|
9
|
+
⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡇⠀⠀⠀
|
|
10
|
+
⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⡇⠀⠀⠀
|
|
11
|
+
⠸⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡿⠀⠀⠀⠀
|
|
12
|
+
⠀⠹⣷⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣰⡿⠁⠀⠀⠀⠀
|
|
13
|
+
⠀⠀⠀⠉⠙⠛⠿⠶⣶⣶⣶⣶⣶⠶⠿⠟⠛⠉⠀⠀⠀⠀⠀⠀`;
|
|
14
|
+
const cat = `⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
15
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡿⠇⠀⠀⠀⠀⢻⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
16
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⡸⣞⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
17
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠀⠀⢀⣧⢿⣽⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
18
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢴⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⣼⣞⡿⣞⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
19
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠀⠀⠀⣰⣟⢾⣽⢫⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
20
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣠⢤⣶⡻⣞⣿⣺⢯⣽⣳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
21
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⢠⣄⡀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⣀⣠⣤⣿⣽⣻⢾⣽⣷⣾⣽⣻⣞⣷⣳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
22
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣿⣶⣄⡀⠀⠀⠀⣉⣲⣴⢶⣞⡿⣽⣞⡷⣯⢿⡽⣞⣿⠟⠋⠁⠉⠈⠳⣟⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
23
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⢶⣾⣿⡽⣯⣟⡾⣽⡷⣯⣟⡽⡾⣽⡯⠁⠀⠀⠀⠀⠀⠀⢮⣭⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
24
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢞⣿⣿⢯⡿⣿⣯⣟⣷⣯⢿⣳⣟⡷⣽⣼⣻⣽⠀⠀⠀⠀⠀⠀⠀⢀⣼⡯⡗⠋⠤⠀⠀⠀⠀⠀⠀⠀⠀
|
|
25
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⣿⣿⣯⣽⣾⣿⣾⣗⡿⣯⡷⣯⣟⡷⣞⣼⣿⣀⠀⠀⠀⠀⢀⣠⡿⣏⡗⠈⠐⠈⠅⠀⠀⠀⠀⠀⠀⠀
|
|
26
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⠛⠏⠉⠉⠽⢟⢿⣿⣿⣿⣿⣷⣻⢾⡽⣞⡷⠄⡹⣶⢿⣻⢿⣻⡽⢯⣼⢦⠶⠁⠈⠀⠀⠀⠀⠀⠀⠀
|
|
27
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣯⠇⠀⠀⠀⠀⠀⠁⣽⣿⣿⣿⣷⣯⣿⣽⣛⡦⠀⠀⢩⣿⣹⢯⣷⢻⣟⠺⢣⡖⣘⠤⠓⠀⠀⠀⠀⠀⠀
|
|
28
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⣿⡃⠁⠀⠀⠀⢀⣤⣾⣟⢿⣻⣿⣿⣟⡾⣽⡳⠄⠎⢳⣯⢯⣟⡾⢯⣞⣯⣓⠉⢀⠀⠀⡄⢢⡀⠀⠀⠀
|
|
29
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣷⣷⣶⣳⣶⣺⣿⣿⣳⢯⣟⣿⣿⣳⢯⠛⠅⠃⠀⠀⣴⣿⡿⣬⢶⠾⠙⣊⣥⠾⡒⠊⢁⢠⠣⣌⠀⠀⠀
|
|
30
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢺⡽⣾⡽⣯⣟⣿⡿⣯⣿⣿⣾⢿⣿⠳⢏⣈⢠⠀⠀⣰⢿⡿⣽⣉⡶⠌⠋⠉⣀⡀⠁⠀⠀⠀⣘⡐⣂⠀⠀
|
|
31
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣽⣳⣟⣳⣟⣾⣽⣿⣿⣿⣿⣿⣦⣜⡻⡽⠆⠧⣴⡟⣯⢟⡳⣭⠲⠄⠐⠀⠀⠀⠈⠁⠉⠑⢊⡕⢃⠄⠀
|
|
32
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⣾⣿⣯⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣾⢧⠀⠹⠾⡵⡞⡽⢢⣃⠐⠀⠀⠄⡐⠀⠀⠀⡘⢦⠘⣌⠀⠀
|
|
33
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠹⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢯⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠒⡈⠀⡀⠄⡑⠢⣉⠴⣈⣆
|
|
34
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢯⣏⡴⣶⣵⣢⢤⢠⡀⡄⢠⠐⡰⢌⡱⠀⡁⡀⠆⡥⠆⡥⣛⡽⣾
|
|
35
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠔⠉⠀⠀⢽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣼⣻⢷⣯⡽⣞⣷⣻⡼⣡⢋⡔⠣⠜⡐⢐⠠⡓⣤⣙⣲⣽⣻⢷
|
|
36
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡿⣽⣞⣷⣻⡴⣣⢜⡱⣊⡕⣊⠠⡙⡰⣭⢷⣯⣿⢿`;
|
|
37
|
+
|
|
38
|
+
// Keep the supplied originals above intact. Compact marks are reduced from
|
|
39
|
+
// their braille dots, not substituted with a different duck or cat drawing.
|
|
40
|
+
const BRAILLE_DOTS = [[1, 8], [2, 16], [4, 32], [64, 128]];
|
|
41
|
+
const mascotCache = new Map();
|
|
42
|
+
|
|
43
|
+
function compactArt(value, maxRows) {
|
|
44
|
+
const source = value.split("\n");
|
|
45
|
+
const blank = /^[ \u2800]*$/;
|
|
46
|
+
const sourceWidth = Math.max(...source.map((row) => row.length));
|
|
47
|
+
let left = sourceWidth;
|
|
48
|
+
let right = 0;
|
|
49
|
+
for (const row of source) {
|
|
50
|
+
for (let x = 0; x < row.length; x++) {
|
|
51
|
+
if (!blank.test(row[x])) {
|
|
52
|
+
left = Math.min(left, x);
|
|
53
|
+
right = Math.max(right, x + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const sourceRows = source.map((row) => row.slice(left, right).padEnd(right - left, "\u2800"));
|
|
58
|
+
if (sourceRows.length <= maxRows) return sourceRows;
|
|
59
|
+
const scale = maxRows / sourceRows.length;
|
|
60
|
+
const columns = Math.max(1, Math.round((right - left) * scale));
|
|
61
|
+
const pixelWidth = (right - left) * 2;
|
|
62
|
+
const pixelHeight = sourceRows.length * 4;
|
|
63
|
+
const rows = [];
|
|
64
|
+
for (let y = 0; y < maxRows; y++) {
|
|
65
|
+
let row = "";
|
|
66
|
+
for (let x = 0; x < columns; x++) {
|
|
67
|
+
let dots = 0;
|
|
68
|
+
for (let dy = 0; dy < 4; dy++) {
|
|
69
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
70
|
+
const x0 = Math.floor((x * 2 + dx) * pixelWidth / (columns * 2));
|
|
71
|
+
const x1 = Math.max(x0 + 1, Math.floor((x * 2 + dx + 1) * pixelWidth / (columns * 2)));
|
|
72
|
+
const y0 = Math.floor((y * 4 + dy) * pixelHeight / (maxRows * 4));
|
|
73
|
+
const y1 = Math.max(y0 + 1, Math.floor((y * 4 + dy + 1) * pixelHeight / (maxRows * 4)));
|
|
74
|
+
let ink = 0;
|
|
75
|
+
for (let sy = y0; sy < y1; sy++) {
|
|
76
|
+
for (let sx = x0; sx < x1; sx++) {
|
|
77
|
+
const cell = sourceRows[Math.floor(sy / 4)].charCodeAt(Math.floor(sx / 2)) - 0x2800;
|
|
78
|
+
if (cell & BRAILLE_DOTS[sy % 4][sx % 2]) ink++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (ink / ((x1 - x0) * (y1 - y0)) >= 0.25) dots |= BRAILLE_DOTS[dy][dx];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
row += String.fromCharCode(0x2800 + dots);
|
|
85
|
+
}
|
|
86
|
+
rows.push(row);
|
|
87
|
+
}
|
|
88
|
+
return rows;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mascotLines(width, maxRows) {
|
|
92
|
+
for (const height of [23, 11, 6, 3]) {
|
|
93
|
+
if (height > maxRows) continue;
|
|
94
|
+
let pair = mascotCache.get(height);
|
|
95
|
+
if (!pair) {
|
|
96
|
+
const duckRows = compactArt(duck, height);
|
|
97
|
+
const catRows = compactArt(cat, height);
|
|
98
|
+
const duckWidth = duckRows[0].length;
|
|
99
|
+
const rows = Math.max(duckRows.length, catRows.length);
|
|
100
|
+
pair = Array.from({ length: rows }, (_, i) =>
|
|
101
|
+
`${duckRows[i - rows + duckRows.length] || " ".repeat(duckWidth)} ${catRows[i - rows + catRows.length] || ""}`,
|
|
102
|
+
);
|
|
103
|
+
mascotCache.set(height, pair);
|
|
104
|
+
}
|
|
105
|
+
if (pair[0].length <= width) return pair;
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { duck, cat, mascotLines, credit: "made by rexy and brink" };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { StringDecoder } = require("string_decoder");
|
|
4
|
+
const zlib = require("zlib");
|
|
5
|
+
function failure(message, status = 502, code = "bridge_error") {
|
|
6
|
+
return Object.assign(new Error(message), { status, code });
|
|
7
|
+
}
|
|
8
|
+
async function writeChunk(res, chunk) {
|
|
9
|
+
if (res.destroyed || res.writableEnded)
|
|
10
|
+
throw failure("Client disconnected", 499, "client_disconnected");
|
|
11
|
+
if (res.write(chunk)) return;
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
const cleanup = () => {
|
|
14
|
+
res.off("drain", drained);
|
|
15
|
+
res.off("close", closed);
|
|
16
|
+
res.off("error", errored);
|
|
17
|
+
};
|
|
18
|
+
const drained = () => {
|
|
19
|
+
cleanup();
|
|
20
|
+
resolve();
|
|
21
|
+
};
|
|
22
|
+
const closed = () => {
|
|
23
|
+
cleanup();
|
|
24
|
+
reject(failure("Client disconnected", 499, "client_disconnected"));
|
|
25
|
+
};
|
|
26
|
+
const errored = (error) => {
|
|
27
|
+
cleanup();
|
|
28
|
+
reject(error);
|
|
29
|
+
};
|
|
30
|
+
res.once("drain", drained);
|
|
31
|
+
res.once("close", closed);
|
|
32
|
+
res.once("error", errored);
|
|
33
|
+
if (res.destroyed) closed();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function* sseEvents(stream) {
|
|
37
|
+
const decoder = new StringDecoder("utf8");
|
|
38
|
+
let line = "";
|
|
39
|
+
let skipLF = false;
|
|
40
|
+
let first = true;
|
|
41
|
+
let data = [];
|
|
42
|
+
let event = "";
|
|
43
|
+
function consumeLine() {
|
|
44
|
+
if (!line) {
|
|
45
|
+
const result = data.length ? { data: data.join("\n"), event } : null;
|
|
46
|
+
data = [];
|
|
47
|
+
event = "";
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
const colon = line.indexOf(":");
|
|
51
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
52
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
53
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
54
|
+
if (field === "data") data.push(value);
|
|
55
|
+
else if (field === "event") event = value;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
for await (const chunk of stream) {
|
|
59
|
+
let text = decoder.write(chunk);
|
|
60
|
+
if (first && text.length) {
|
|
61
|
+
first = false;
|
|
62
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
63
|
+
}
|
|
64
|
+
let start = 0;
|
|
65
|
+
for (let i = 0; i < text.length; i++) {
|
|
66
|
+
const char = text[i];
|
|
67
|
+
if (skipLF) {
|
|
68
|
+
skipLF = false;
|
|
69
|
+
if (char === "\n") {
|
|
70
|
+
start = i + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (char === "\r" || char === "\n") {
|
|
75
|
+
line += text.slice(start, i);
|
|
76
|
+
const parsed = consumeLine();
|
|
77
|
+
line = "";
|
|
78
|
+
start = i + 1;
|
|
79
|
+
skipLF = char === "\r";
|
|
80
|
+
if (parsed) yield parsed;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
line += text.slice(start);
|
|
84
|
+
}
|
|
85
|
+
line += decoder.end();
|
|
86
|
+
// SSE requires an empty line to dispatch an event. Do not accept an
|
|
87
|
+
// unterminated final JSON event as a successful stream termination.
|
|
88
|
+
if (line.trim() || data.length)
|
|
89
|
+
throw failure("Truncated upstream SSE event", 502, "incomplete_stream");
|
|
90
|
+
}
|
|
91
|
+
function decodedStream(upstream, source = upstream) {
|
|
92
|
+
const encoding = String(
|
|
93
|
+
upstream.headers["content-encoding"] || "identity",
|
|
94
|
+
).toLowerCase();
|
|
95
|
+
let decoder;
|
|
96
|
+
if (encoding === "gzip") decoder = zlib.createGunzip();
|
|
97
|
+
else if (encoding === "deflate") decoder = zlib.createInflate();
|
|
98
|
+
else if (encoding === "br") decoder = zlib.createBrotliDecompress();
|
|
99
|
+
else if (encoding !== "identity")
|
|
100
|
+
throw failure(`Unsupported upstream content encoding: ${encoding}`);
|
|
101
|
+
if (!decoder) return source;
|
|
102
|
+
source.on("error", (error) => decoder.destroy(error));
|
|
103
|
+
source.on("aborted", () =>
|
|
104
|
+
decoder.destroy(failure("Upstream response aborted")),
|
|
105
|
+
);
|
|
106
|
+
return source.pipe(decoder);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { failure, writeChunk, sseEvents, decodedStream };
|