@thelioo/opencode-balancer 0.1.0
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/INSTALL.txt +58 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/balancer/accounts.d.ts +9 -0
- package/dist/balancer/accounts.js +102 -0
- package/dist/balancer/accounts.js.map +1 -0
- package/dist/balancer/auth-watcher.d.ts +1 -0
- package/dist/balancer/auth-watcher.js +30 -0
- package/dist/balancer/auth-watcher.js.map +1 -0
- package/dist/balancer/commands.d.ts +1 -0
- package/dist/balancer/commands.js +94 -0
- package/dist/balancer/commands.js.map +1 -0
- package/dist/balancer/core.d.ts +6 -0
- package/dist/balancer/core.js +7 -0
- package/dist/balancer/core.js.map +1 -0
- package/dist/balancer/fetch-patch.d.ts +1 -0
- package/dist/balancer/fetch-patch.js +110 -0
- package/dist/balancer/fetch-patch.js.map +1 -0
- package/dist/balancer/http.d.ts +40 -0
- package/dist/balancer/http.js +199 -0
- package/dist/balancer/http.js.map +1 -0
- package/dist/balancer/native.d.ts +3 -0
- package/dist/balancer/native.js +16 -0
- package/dist/balancer/native.js.map +1 -0
- package/dist/balancer/state.d.ts +14 -0
- package/dist/balancer/state.js +16 -0
- package/dist/balancer/state.js.map +1 -0
- package/dist/balancer/storage.d.ts +8 -0
- package/dist/balancer/storage.js +92 -0
- package/dist/balancer/storage.js.map +1 -0
- package/dist/balancer/types.d.ts +44 -0
- package/dist/balancer/types.js +2 -0
- package/dist/balancer/types.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/INSTALL.txt
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
opencode-balancer installation guide for AI agents
|
|
2
|
+
|
|
3
|
+
Goal
|
|
4
|
+
Install @thelioo/opencode-balancer as an opencode npm plugin without removing or rewriting the user's existing opencode configuration.
|
|
5
|
+
|
|
6
|
+
What this plugin does
|
|
7
|
+
opencode-balancer lets opencode users save multiple authenticated accounts per provider, switch active accounts with /balancer commands, and automatically retry with another saved account when the active account is rate-limited.
|
|
8
|
+
|
|
9
|
+
Important rules
|
|
10
|
+
- Do not run npm install for this plugin. opencode installs npm plugins automatically with Bun at startup.
|
|
11
|
+
- Preserve existing opencode config, plugins, comments, formatting, providers, agents, MCP servers, and permissions whenever possible.
|
|
12
|
+
- Use the singular key "plugin", not "plugins".
|
|
13
|
+
- Add the plugin only if it is not already present.
|
|
14
|
+
- Prefer @thelioo/opencode-balancer@latest for user installs.
|
|
15
|
+
|
|
16
|
+
Steps
|
|
17
|
+
1. Locate the user's opencode config.
|
|
18
|
+
- Prefer ~/.config/opencode/opencode.jsonc if it exists.
|
|
19
|
+
- Otherwise use ~/.config/opencode/opencode.json if it exists.
|
|
20
|
+
- If neither exists, create ~/.config/opencode/opencode.json with a minimal JSON object.
|
|
21
|
+
|
|
22
|
+
2. Ensure the config has a top-level plugin array.
|
|
23
|
+
- If plugin already exists and is an array, keep all existing entries.
|
|
24
|
+
- If plugin is missing, create it.
|
|
25
|
+
- If the config is JSONC, keep JSONC syntax valid.
|
|
26
|
+
- If the config is JSON, keep JSON syntax valid.
|
|
27
|
+
|
|
28
|
+
3. Add this plugin entry:
|
|
29
|
+
@thelioo/opencode-balancer@latest
|
|
30
|
+
|
|
31
|
+
4. Save the config.
|
|
32
|
+
|
|
33
|
+
5. Show the user the final plugin array.
|
|
34
|
+
|
|
35
|
+
6. Tell the user to restart opencode.
|
|
36
|
+
|
|
37
|
+
Expected config shape
|
|
38
|
+
{
|
|
39
|
+
"$schema": "https://opencode.ai/config.json",
|
|
40
|
+
"plugin": ["@thelioo/opencode-balancer@latest"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
After restart
|
|
44
|
+
The user can connect and save accounts with:
|
|
45
|
+
/connect anthropic
|
|
46
|
+
/balancer alias work
|
|
47
|
+
|
|
48
|
+
Useful commands
|
|
49
|
+
/balancer help
|
|
50
|
+
/balancer list
|
|
51
|
+
/balancer status
|
|
52
|
+
/balancer use <provider> <alias>
|
|
53
|
+
/balancer remove <provider> <alias>
|
|
54
|
+
|
|
55
|
+
Verification
|
|
56
|
+
- Confirm the opencode config contains @thelioo/opencode-balancer@latest in the plugin array.
|
|
57
|
+
- Confirm no existing plugin entries were removed.
|
|
58
|
+
- Remind the user that opencode must be restarted before /balancer becomes available.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thelioo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# opencode-balancer
|
|
4
|
+
|
|
5
|
+
_Use multiple accounts per opencode provider and switch automatically when one hits a rate limit._
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@thelioo/opencode-balancer)
|
|
8
|
+
[](https://www.typescriptlang.org)
|
|
9
|
+
[](https://opencode.ai/docs/plugins)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
[Features](#features) | [Installation](#installation) | [Usage](#usage) | [Commands](#commands) | [Troubleshooting](#troubleshooting)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
`opencode-balancer` is an [opencode](https://opencode.ai/) plugin that lets you save multiple authenticated accounts for the same provider, give each one a friendly alias, and keep working when the active account becomes rate-limited.
|
|
17
|
+
|
|
18
|
+
It works with opencode's existing auth flow: connect a provider, save the detected credentials as an alias, then let the plugin inject the active account credentials into future model requests.
|
|
19
|
+
|
|
20
|
+
> [!NOTE]
|
|
21
|
+
> This plugin manages credentials already configured through opencode. It does not create accounts, bypass provider limits, or modify provider-side quotas.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Multiple accounts per provider**: Save aliases like `work`, `personal`, or `backup` for the same provider.
|
|
26
|
+
- **Automatic failover**: Switches to another saved account on retryable rate-limit/server responses.
|
|
27
|
+
- **Native opencode commands**: Adds `/balancer` directly inside opencode.
|
|
28
|
+
- **OAuth-friendly setup**: Adds an optional alias prompt to provider OAuth flows.
|
|
29
|
+
- **Agent tool support**: Exposes a `balancer_command` tool so opencode agents can manage accounts when asked.
|
|
30
|
+
- **Local credential store**: Saves account metadata under your opencode config directory.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### Option A: Let an AI Agent Install It
|
|
35
|
+
|
|
36
|
+
Paste this into opencode or another coding agent running on your machine:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
Install and configure @thelioo/opencode-balancer by following this guide:
|
|
40
|
+
https://raw.githubusercontent.com/thelioo/opencode-balancer/refs/heads/main/INSTALL.txt
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or read the local guide: [INSTALL.txt](INSTALL.txt).
|
|
44
|
+
|
|
45
|
+
### Option B: Manual Setup
|
|
46
|
+
|
|
47
|
+
Add the plugin to your opencode config:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"$schema": "https://opencode.ai/config.json",
|
|
52
|
+
"plugin": ["@thelioo/opencode-balancer@latest"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then restart opencode.
|
|
57
|
+
|
|
58
|
+
> [!TIP]
|
|
59
|
+
> No manual `npm install` is required. opencode automatically installs npm plugins with Bun at startup and caches them locally.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Save Your First Account
|
|
64
|
+
|
|
65
|
+
Connect a provider as usual:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
/connect anthropic
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After the connection is detected, save it with an alias:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
/balancer alias work
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Add Another Account
|
|
78
|
+
|
|
79
|
+
Connect the same provider with a different account, then save it:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
/connect anthropic
|
|
83
|
+
/balancer alias personal
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Switch Accounts Manually
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
/balancer use anthropic work
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### List Saved Accounts
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
/balancer list
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Example output:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
anthropic:
|
|
102
|
+
* work (oauth, healthy)
|
|
103
|
+
personal (oauth, healthy)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
When the active account receives a retryable response such as `429`, `500`, `502`, `503`, `504`, or `529`, the plugin marks it as temporarily rate-limited and retries with another available account for the same provider.
|
|
107
|
+
|
|
108
|
+
## Commands
|
|
109
|
+
|
|
110
|
+
| Command | Description |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `/balancer help` | Show available commands. |
|
|
113
|
+
| `/balancer alias <name>` | Save the last detected connection with an alias. |
|
|
114
|
+
| `/balancer save <provider> <name>` | Save the provider's current native credentials. |
|
|
115
|
+
| `/balancer use <provider> <name>` | Switch the active account for a provider. |
|
|
116
|
+
| `/balancer list` | List saved accounts. |
|
|
117
|
+
| `/balancer status` | Show saved accounts, last capture, and storage path. |
|
|
118
|
+
| `/balancer remove <provider> <name>` | Remove a saved account. |
|
|
119
|
+
|
|
120
|
+
Aliases are normalized to lowercase and may contain letters, numbers, dots, hyphens, and underscores.
|
|
121
|
+
|
|
122
|
+
## How It Works
|
|
123
|
+
|
|
124
|
+
`opencode-balancer` hooks into opencode's plugin lifecycle and request flow:
|
|
125
|
+
|
|
126
|
+
1. It watches opencode auth changes and records the last detected provider credentials.
|
|
127
|
+
2. `/balancer alias <name>` stores those credentials under a provider-specific alias.
|
|
128
|
+
3. Before model requests, the plugin selects the active account for the request provider.
|
|
129
|
+
4. If the provider returns a retryable rate-limit or server response, the plugin marks the account as temporarily unavailable and retries with another saved account.
|
|
130
|
+
|
|
131
|
+
Saved account data is written to:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
~/.config/opencode/balancer-accounts.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If `OPENCODE_CONFIG_DIR` is set, the plugin uses that directory instead.
|
|
138
|
+
|
|
139
|
+
> [!CAUTION]
|
|
140
|
+
> The account store contains credentials. Keep it private and do not commit it to a repository.
|
|
141
|
+
|
|
142
|
+
## Local Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm install
|
|
146
|
+
npm run check
|
|
147
|
+
npm run build
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
To test a local checkout with opencode, point your config to the package directory:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"plugin": ["file:///absolute/path/to/opencode-balancer"]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Troubleshooting
|
|
159
|
+
|
|
160
|
+
| Problem | What to try |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| Plugin does not load | Confirm `plugin` is singular, restart opencode, and check that the package name is `@thelioo/opencode-balancer@latest`. |
|
|
163
|
+
| `/balancer` is unavailable | Restart opencode after editing the config. |
|
|
164
|
+
| No connection detected | Run `/connect <provider>` first, then `/balancer alias <name>`. |
|
|
165
|
+
| Account is not switching | Run `/balancer list` and confirm there is another healthy account for the same provider. |
|
|
166
|
+
| Need to inspect storage | Run `/balancer status` to see the account store path. |
|
|
167
|
+
|
|
168
|
+
## Resources
|
|
169
|
+
|
|
170
|
+
- [opencode plugins documentation](https://opencode.ai/docs/plugins)
|
|
171
|
+
- [opencode configuration](https://opencode.ai/docs/config)
|
|
172
|
+
- [npm package](https://www.npmjs.com/package/@thelioo/opencode-balancer)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Account, AuthInfo, ProviderState } from "./types";
|
|
2
|
+
export declare function normalizeAlias(alias: string): string;
|
|
3
|
+
export declare function saveAccount(providerID: string, aliasInput: string, auth: AuthInfo): Promise<Account>;
|
|
4
|
+
export declare function getProviderState(providerID: string): Promise<ProviderState | undefined>;
|
|
5
|
+
export declare function getActiveAccount(providerID: string): Promise<Account | undefined>;
|
|
6
|
+
export declare function listAccountsText(): Promise<string>;
|
|
7
|
+
export declare function nextAvailableAccount(providerID: string, currentAlias?: string): Promise<Account | undefined>;
|
|
8
|
+
export declare function markRateLimited(providerID: string, alias: string, retryAfterMs?: number): Promise<void>;
|
|
9
|
+
export declare function markUsed(providerID: string, alias: string): Promise<void>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { now } from "./state";
|
|
2
|
+
import { readStore, writeStore } from "./storage";
|
|
3
|
+
export function normalizeAlias(alias) {
|
|
4
|
+
return alias
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
8
|
+
.replace(/^-+|-+$/g, "");
|
|
9
|
+
}
|
|
10
|
+
export async function saveAccount(providerID, aliasInput, auth) {
|
|
11
|
+
const alias = normalizeAlias(aliasInput);
|
|
12
|
+
if (!alias)
|
|
13
|
+
throw new Error("Invalid alias. Use letters, numbers, dots, hyphens, or underscores.");
|
|
14
|
+
const store = await readStore();
|
|
15
|
+
const provider = store.providers[providerID] ?? { accounts: {} };
|
|
16
|
+
const existing = provider.accounts[alias];
|
|
17
|
+
const account = {
|
|
18
|
+
alias,
|
|
19
|
+
providerID,
|
|
20
|
+
auth,
|
|
21
|
+
createdAt: existing?.createdAt ?? now(),
|
|
22
|
+
updatedAt: now(),
|
|
23
|
+
lastUsedAt: existing?.lastUsedAt,
|
|
24
|
+
failures: existing?.failures,
|
|
25
|
+
rateLimitedUntil: existing?.rateLimitedUntil,
|
|
26
|
+
};
|
|
27
|
+
provider.accounts[alias] = account;
|
|
28
|
+
provider.active = alias;
|
|
29
|
+
store.providers[providerID] = provider;
|
|
30
|
+
store.lastCaptured = {
|
|
31
|
+
providerID,
|
|
32
|
+
auth,
|
|
33
|
+
capturedAt: now(),
|
|
34
|
+
source: store.lastCaptured?.source ?? "auth-file",
|
|
35
|
+
};
|
|
36
|
+
await writeStore(store);
|
|
37
|
+
return account;
|
|
38
|
+
}
|
|
39
|
+
export async function getProviderState(providerID) {
|
|
40
|
+
return (await readStore()).providers[providerID];
|
|
41
|
+
}
|
|
42
|
+
export async function getActiveAccount(providerID) {
|
|
43
|
+
const provider = await getProviderState(providerID);
|
|
44
|
+
if (!provider?.active)
|
|
45
|
+
return undefined;
|
|
46
|
+
return provider.accounts[provider.active];
|
|
47
|
+
}
|
|
48
|
+
export async function listAccountsText() {
|
|
49
|
+
const store = await readStore();
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const [providerID, provider] of Object.entries(store.providers).sort(([a], [b]) => a.localeCompare(b))) {
|
|
52
|
+
lines.push(`${providerID}:`);
|
|
53
|
+
const accounts = Object.values(provider.accounts).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
54
|
+
if (accounts.length === 0) {
|
|
55
|
+
lines.push(" (no accounts)");
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const account of accounts) {
|
|
59
|
+
const marker = provider.active === account.alias ? "*" : " ";
|
|
60
|
+
const limited = account.rateLimitedUntil && account.rateLimitedUntil > now()
|
|
61
|
+
? ` rate-limited ${Math.ceil((account.rateLimitedUntil - now()) / 1000)}s`
|
|
62
|
+
: "healthy";
|
|
63
|
+
lines.push(` ${marker} ${account.alias} (${account.auth.type}, ${limited})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (lines.length === 0)
|
|
67
|
+
return "No accounts saved. Use `/connect <provider>` and then `/balancer alias <name>`.";
|
|
68
|
+
const last = store.lastCaptured;
|
|
69
|
+
if (last)
|
|
70
|
+
lines.push("", `Last detected connection: ${last.providerID} (${last.auth.type})`);
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
export async function nextAvailableAccount(providerID, currentAlias) {
|
|
74
|
+
const provider = await getProviderState(providerID);
|
|
75
|
+
if (!provider)
|
|
76
|
+
return undefined;
|
|
77
|
+
const accounts = Object.values(provider.accounts);
|
|
78
|
+
return accounts.find((account) => account.alias !== currentAlias &&
|
|
79
|
+
(!account.rateLimitedUntil || account.rateLimitedUntil <= now()));
|
|
80
|
+
}
|
|
81
|
+
export async function markRateLimited(providerID, alias, retryAfterMs = 60_000) {
|
|
82
|
+
const store = await readStore();
|
|
83
|
+
const account = store.providers[providerID]?.accounts[alias];
|
|
84
|
+
if (!account)
|
|
85
|
+
return;
|
|
86
|
+
account.failures = (account.failures ?? 0) + 1;
|
|
87
|
+
account.rateLimitedUntil = now() + retryAfterMs;
|
|
88
|
+
account.updatedAt = now();
|
|
89
|
+
await writeStore(store);
|
|
90
|
+
}
|
|
91
|
+
export async function markUsed(providerID, alias) {
|
|
92
|
+
const store = await readStore();
|
|
93
|
+
const account = store.providers[providerID]?.accounts[alias];
|
|
94
|
+
if (!account)
|
|
95
|
+
return;
|
|
96
|
+
account.lastUsedAt = now();
|
|
97
|
+
account.updatedAt = now();
|
|
98
|
+
if (account.rateLimitedUntil && account.rateLimitedUntil <= now())
|
|
99
|
+
delete account.rateLimitedUntil;
|
|
100
|
+
await writeStore(store);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=accounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/balancer/accounts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startAuthWatcher(client: any): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { authPath, captureAuth, isAuthInfo, now, readNativeAuth, showToast, state, } from "./core";
|
|
2
|
+
export function startAuthWatcher(client) {
|
|
3
|
+
if (state.watchStarted)
|
|
4
|
+
return;
|
|
5
|
+
state.watchStarted = true;
|
|
6
|
+
void readNativeAuth().then((auth) => {
|
|
7
|
+
state.lastAuthSnapshot = auth;
|
|
8
|
+
});
|
|
9
|
+
const path = authPath();
|
|
10
|
+
const handleChange = async () => {
|
|
11
|
+
const next = await readNativeAuth();
|
|
12
|
+
for (const [providerID, auth] of Object.entries(next)) {
|
|
13
|
+
if (!isAuthInfo(auth))
|
|
14
|
+
continue;
|
|
15
|
+
const suppressedUntil = state.suppressAuthCaptureUntil.get(providerID) ?? 0;
|
|
16
|
+
if (suppressedUntil > now())
|
|
17
|
+
continue;
|
|
18
|
+
const previous = JSON.stringify(state.lastAuthSnapshot[providerID] ?? null);
|
|
19
|
+
const current = JSON.stringify(auth);
|
|
20
|
+
if (previous === current)
|
|
21
|
+
continue;
|
|
22
|
+
await captureAuth(providerID, auth, "auth-file");
|
|
23
|
+
await showToast(client, `Balancer: new ${providerID} connection detected. Use /balancer alias <name>.`, "info");
|
|
24
|
+
}
|
|
25
|
+
state.lastAuthSnapshot = next;
|
|
26
|
+
};
|
|
27
|
+
const timer = setInterval(() => void handleChange(), 1_000);
|
|
28
|
+
timer.unref?.();
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=auth-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-watcher.js","sourceRoot":"","sources":["../../src/balancer/auth-watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,QAAQ,EACR,WAAW,EACX,UAAU,EACV,GAAG,EACH,cAAc,EACd,SAAS,EACT,KAAK,GACR,MAAM,QAAQ,CAAC;AAEhB,MAAM,UAAU,gBAAgB,CAAC,MAAW;IACxC,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO;IAC/B,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC1B,KAAK,cAAc,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QAChC,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAClC,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,MAAM,YAAY,GAAG,KAAK,IAAI,EAAE;QAC5B,MAAM,IAAI,GAAG,MAAM,cAAc,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAChC,MAAM,eAAe,GACjB,KAAK,CAAC,wBAAwB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACxD,IAAI,eAAe,GAAG,GAAG,EAAE;gBAAE,SAAS;YACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAC3B,KAAK,CAAC,gBAAgB,CAAC,UAAU,CAAC,IAAI,IAAI,CAC7C,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,QAAQ,KAAK,OAAO;gBAAE,SAAS;YACnC,MAAM,WAAW,CAAC,UAAU,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;YACjD,MAAM,SAAS,CACX,MAAM,EACN,iBAAiB,UAAU,mDAAmD,EAC9E,MAAM,CACT,CAAC;QACN,CAAC;QACD,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAClC,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,YAAY,EAAE,EAAE,KAAK,CAAC,CAAC;IAC3D,KAAgC,CAAC,KAAK,EAAE,EAAE,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runBalancerCommand(client: any, raw: string): Promise<string>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isAuthInfo, listAccountsText, normalizeAlias, readNativeAuth, readStore, saveAccount, setNativeAuth, storePath, writeStore, } from "./core";
|
|
2
|
+
function parseArgs(input) {
|
|
3
|
+
return (input
|
|
4
|
+
.match(/(?:[^\s"]+|"[^"]*")+/g)
|
|
5
|
+
?.map((x) => x.replace(/^"|"$/g, "")) ?? []);
|
|
6
|
+
}
|
|
7
|
+
export async function runBalancerCommand(client, raw) {
|
|
8
|
+
const [action = "help", ...args] = parseArgs(raw);
|
|
9
|
+
const store = await readStore();
|
|
10
|
+
if (["help", "--help", "-h"].includes(action)) {
|
|
11
|
+
return [
|
|
12
|
+
"Balancer commands:",
|
|
13
|
+
"",
|
|
14
|
+
" /balancer alias <name>",
|
|
15
|
+
" saves the last detected connection",
|
|
16
|
+
" with an alias",
|
|
17
|
+
"",
|
|
18
|
+
" /balancer save <provider> <name>",
|
|
19
|
+
" saves the provider's current credentials",
|
|
20
|
+
"",
|
|
21
|
+
" /balancer use <provider> <name>",
|
|
22
|
+
" switches the active account",
|
|
23
|
+
"",
|
|
24
|
+
" /balancer list",
|
|
25
|
+
" lists accounts",
|
|
26
|
+
"",
|
|
27
|
+
" /balancer status",
|
|
28
|
+
" shows current state",
|
|
29
|
+
"",
|
|
30
|
+
" /balancer remove <provider> <name>",
|
|
31
|
+
" removes an account",
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
if (action === "alias") {
|
|
35
|
+
const alias = args[0];
|
|
36
|
+
if (!alias)
|
|
37
|
+
return "Usage: /balancer alias <name>";
|
|
38
|
+
const captured = store.lastCaptured;
|
|
39
|
+
if (!captured)
|
|
40
|
+
return "No connection detected yet. Run /connect <provider> first.";
|
|
41
|
+
const account = await saveAccount(captured.providerID, alias, captured.auth);
|
|
42
|
+
await setNativeAuth(client, captured.providerID, captured.auth);
|
|
43
|
+
return `Account saved and activated: ${captured.providerID}/${account.alias}`;
|
|
44
|
+
}
|
|
45
|
+
if (action === "save") {
|
|
46
|
+
const [providerID, alias] = args;
|
|
47
|
+
if (!providerID || !alias)
|
|
48
|
+
return "Usage: /balancer save <provider> <name>";
|
|
49
|
+
const auth = (await readNativeAuth())[providerID];
|
|
50
|
+
if (!isAuthInfo(auth))
|
|
51
|
+
return `No native credentials found for ${providerID}. Run /connect ${providerID} first.`;
|
|
52
|
+
const account = await saveAccount(providerID, alias, auth);
|
|
53
|
+
return `Account saved and activated: ${providerID}/${account.alias}`;
|
|
54
|
+
}
|
|
55
|
+
if (action === "use") {
|
|
56
|
+
const [providerID, aliasInput] = args;
|
|
57
|
+
const alias = normalizeAlias(aliasInput ?? "");
|
|
58
|
+
if (!providerID || !alias)
|
|
59
|
+
return "Usage: /balancer use <provider> <name>";
|
|
60
|
+
const account = store.providers[providerID]?.accounts[alias];
|
|
61
|
+
if (!account)
|
|
62
|
+
return `Account not found: ${providerID}/${alias}`;
|
|
63
|
+
store.providers[providerID].active = alias;
|
|
64
|
+
await writeStore(store);
|
|
65
|
+
await setNativeAuth(client, providerID, account.auth);
|
|
66
|
+
return `Active account changed to ${providerID}/${alias}`;
|
|
67
|
+
}
|
|
68
|
+
if (action === "list" || action === "accounts")
|
|
69
|
+
return listAccountsText();
|
|
70
|
+
if (action === "status") {
|
|
71
|
+
const last = store.lastCaptured;
|
|
72
|
+
const lines = [await listAccountsText()];
|
|
73
|
+
if (last)
|
|
74
|
+
lines.push("", `Last capture: ${last.providerID} via ${last.source} at ${new Date(last.capturedAt).toLocaleString()}`);
|
|
75
|
+
lines.push("", `Storage: ${storePath()}`);
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
if (action === "remove") {
|
|
79
|
+
const [providerID, aliasInput] = args;
|
|
80
|
+
const alias = normalizeAlias(aliasInput ?? "");
|
|
81
|
+
if (!providerID || !alias)
|
|
82
|
+
return "Usage: /balancer remove <provider> <name>";
|
|
83
|
+
const provider = store.providers[providerID];
|
|
84
|
+
if (!provider?.accounts[alias])
|
|
85
|
+
return `Account not found: ${providerID}/${alias}`;
|
|
86
|
+
delete provider.accounts[alias];
|
|
87
|
+
if (provider.active === alias)
|
|
88
|
+
provider.active = Object.keys(provider.accounts)[0];
|
|
89
|
+
await writeStore(store);
|
|
90
|
+
return `Account removed: ${providerID}/${alias}`;
|
|
91
|
+
}
|
|
92
|
+
return `Unknown command: ${action}. Use /balancer help.`;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/balancer/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/balancer/core.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function installFetchPatch(client: any): Promise<void>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { BALANCER_ALIAS_INPUT, applyAuthToHeaders, captureAuth, cloneRequestInput, fakeCommandResponse, headersFrom, INTERNAL_REQUEST_HEADER, isAuthInfo, isInternalUrl, markRateLimited, markUsed, maybeInjectAliasPrompt, nextAvailableAccount, normalizeAlias, now, parseAuthSetProvider, parseJsonBody, parseProviderAuthorize, parseProviderCallback, parseSessionCommand, parseUrl, readNativeAuth, readStore, RETRYABLE_STATUS, retryAfterMs, saveAccount, setNativeAuth, showToast, state, writeStore, } from "./core";
|
|
2
|
+
import { runBalancerCommand } from "./commands";
|
|
3
|
+
export async function installFetchPatch(client) {
|
|
4
|
+
if (state.fetchPatched)
|
|
5
|
+
return;
|
|
6
|
+
state.fetchPatched = true;
|
|
7
|
+
const originalFetch = globalThis.fetch.bind(globalThis);
|
|
8
|
+
globalThis.fetch = (async (input, init) => {
|
|
9
|
+
const url = parseUrl(input);
|
|
10
|
+
const commandSessionID = parseSessionCommand(url);
|
|
11
|
+
if (commandSessionID &&
|
|
12
|
+
(init?.method ?? "GET").toUpperCase() === "POST") {
|
|
13
|
+
const body = parseJsonBody(init);
|
|
14
|
+
if (body?.command === "balancer") {
|
|
15
|
+
const result = await runBalancerCommand(client, typeof body.arguments === "string" ? body.arguments : "");
|
|
16
|
+
await showToast(client, result.split("\n")[0] ?? result, "info");
|
|
17
|
+
return new Response(JSON.stringify(fakeCommandResponse(commandSessionID, result)), {
|
|
18
|
+
status: 200,
|
|
19
|
+
headers: { "content-type": "application/json" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (isInternalUrl(url) && url?.pathname === "/provider/auth") {
|
|
24
|
+
return maybeInjectAliasPrompt(await originalFetch(input, init));
|
|
25
|
+
}
|
|
26
|
+
const authorizeProvider = parseProviderAuthorize(url);
|
|
27
|
+
if (authorizeProvider &&
|
|
28
|
+
(init?.method ?? "GET").toUpperCase() === "POST") {
|
|
29
|
+
const body = parseJsonBody(init);
|
|
30
|
+
const alias = typeof body?.inputs?.[BALANCER_ALIAS_INPUT] === "string"
|
|
31
|
+
? normalizeAlias(body.inputs[BALANCER_ALIAS_INPUT])
|
|
32
|
+
: "";
|
|
33
|
+
if (alias)
|
|
34
|
+
state.pendingOauthAlias.set(authorizeProvider, alias);
|
|
35
|
+
if (body?.inputs && BALANCER_ALIAS_INPUT in body.inputs) {
|
|
36
|
+
delete body.inputs[BALANCER_ALIAS_INPUT];
|
|
37
|
+
init = { ...init, body: JSON.stringify(body) };
|
|
38
|
+
}
|
|
39
|
+
return originalFetch(input, init);
|
|
40
|
+
}
|
|
41
|
+
const callbackProvider = parseProviderCallback(url);
|
|
42
|
+
if (callbackProvider &&
|
|
43
|
+
(init?.method ?? "GET").toUpperCase() === "POST") {
|
|
44
|
+
const response = await originalFetch(input, init);
|
|
45
|
+
const alias = state.pendingOauthAlias.get(callbackProvider);
|
|
46
|
+
if (response.ok && alias) {
|
|
47
|
+
setTimeout(async () => {
|
|
48
|
+
const auth = (await readNativeAuth())[callbackProvider];
|
|
49
|
+
if (isAuthInfo(auth)) {
|
|
50
|
+
await saveAccount(callbackProvider, alias, auth);
|
|
51
|
+
await showToast(client, `Balancer: account ${callbackProvider}/${alias} saved.`, "success");
|
|
52
|
+
}
|
|
53
|
+
state.pendingOauthAlias.delete(callbackProvider);
|
|
54
|
+
}, 250);
|
|
55
|
+
}
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
const authSetProvider = parseAuthSetProvider(url);
|
|
59
|
+
if (authSetProvider &&
|
|
60
|
+
(init?.method ?? "GET").toUpperCase() === "PUT") {
|
|
61
|
+
const body = parseJsonBody(init);
|
|
62
|
+
const response = await originalFetch(input, init);
|
|
63
|
+
if (response.ok && isAuthInfo(body)) {
|
|
64
|
+
const suppressedUntil = state.suppressAuthCaptureUntil.get(authSetProvider) ?? 0;
|
|
65
|
+
if (suppressedUntil <= now()) {
|
|
66
|
+
await captureAuth(authSetProvider, body, "http");
|
|
67
|
+
await showToast(client, `Balancer: ${authSetProvider} connection detected. Use /balancer alias <name>.`, "info");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
const headers = headersFrom(input, init);
|
|
73
|
+
const requestID = headers.get(INTERNAL_REQUEST_HEADER);
|
|
74
|
+
if (!requestID)
|
|
75
|
+
return originalFetch(input, init);
|
|
76
|
+
const pending = state.pendingRequests.get(requestID);
|
|
77
|
+
headers.delete(INTERNAL_REQUEST_HEADER);
|
|
78
|
+
if (!pending?.account) {
|
|
79
|
+
const [nextInput, nextInit] = cloneRequestInput(input, init, headers);
|
|
80
|
+
return originalFetch(nextInput, nextInit);
|
|
81
|
+
}
|
|
82
|
+
let account = pending.account;
|
|
83
|
+
let attempt = 0;
|
|
84
|
+
while (true) {
|
|
85
|
+
attempt++;
|
|
86
|
+
const attemptHeaders = new Headers(headers);
|
|
87
|
+
applyAuthToHeaders(attemptHeaders, account);
|
|
88
|
+
const [nextInput, nextInit] = cloneRequestInput(input, init, attemptHeaders);
|
|
89
|
+
const response = await originalFetch(nextInput, nextInit);
|
|
90
|
+
if (!RETRYABLE_STATUS.has(response.status)) {
|
|
91
|
+
await markUsed(account.providerID, account.alias);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
await markRateLimited(account.providerID, account.alias, retryAfterMs(response));
|
|
95
|
+
const next = await nextAvailableAccount(account.providerID, account.alias);
|
|
96
|
+
if (!next || attempt >= 3)
|
|
97
|
+
return response;
|
|
98
|
+
account = next;
|
|
99
|
+
const store = await readStore();
|
|
100
|
+
const provider = store.providers[account.providerID];
|
|
101
|
+
if (provider) {
|
|
102
|
+
provider.active = account.alias;
|
|
103
|
+
await writeStore(store);
|
|
104
|
+
}
|
|
105
|
+
await setNativeAuth(client, account.providerID, account.auth);
|
|
106
|
+
await showToast(client, `Balancer: ${pending.providerID}/${pending.account.alias} is rate limited. Switching to ${account.alias}.`, "warning");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=fetch-patch.js.map
|