@relayplane/proxy 1.5.3 → 1.5.5
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 +255 -21
- package/dist/cli.js +257 -2
- package/dist/cli.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/standalone-proxy.d.ts.map +1 -1
- package/dist/standalone-proxy.js +86 -31
- package/dist/standalone-proxy.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +11 -0
- package/dist/telemetry.js.map +1 -1
- package/package.json +2 -2
- package/dist/__tests__/model-suggestions.test.d.ts +0 -2
- package/dist/__tests__/model-suggestions.test.d.ts.map +0 -1
- package/dist/__tests__/model-suggestions.test.js +0 -67
- package/dist/__tests__/model-suggestions.test.js.map +0 -1
- package/dist/__tests__/routing-aliases.test.d.ts +0 -2
- package/dist/__tests__/routing-aliases.test.d.ts.map +0 -1
- package/dist/__tests__/routing-aliases.test.js +0 -81
- package/dist/__tests__/routing-aliases.test.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,53 +1,287 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @relayplane/proxy
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@relayplane/proxy)
|
|
4
4
|
[](https://github.com/RelayPlane/proxy/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
An open-source LLM proxy that sits between your AI agents and providers. Tracks every request, shows where the money goes, and routes tasks to the right model — all running locally.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## ⚡ Quick Start
|
|
8
|
+
## Quick Start
|
|
11
9
|
|
|
12
10
|
```bash
|
|
13
11
|
npm install -g @relayplane/proxy
|
|
14
12
|
relayplane init
|
|
15
13
|
relayplane start
|
|
16
|
-
#
|
|
14
|
+
# Dashboard at http://localhost:4100
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
Works with any agent framework that talks to OpenAI or Anthropic APIs. Point your client at `http://localhost:4100` and the proxy handles the rest.
|
|
20
18
|
|
|
21
|
-
##
|
|
19
|
+
## Supported Providers
|
|
22
20
|
|
|
23
|
-
**
|
|
21
|
+
**Anthropic** · **OpenAI** · **Google Gemini** · **xAI/Grok** · **OpenRouter** · **DeepSeek** · **Groq** · **Mistral** · **Together** · **Fireworks** · **Perplexity**
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
## Configuration
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
RelayPlane reads configuration from `~/.relayplane/config.json`. Override the path with the `RELAYPLANE_CONFIG_PATH` environment variable.
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
```bash
|
|
28
|
+
# Default location
|
|
29
|
+
~/.relayplane/config.json
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Override with env var
|
|
32
|
+
RELAYPLANE_CONFIG_PATH=/path/to/config.json relayplane start
|
|
33
|
+
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
A minimal config file:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"enabled": true,
|
|
40
|
+
"modelOverrides": {},
|
|
41
|
+
"routing": {
|
|
42
|
+
"mode": "cascade",
|
|
43
|
+
"cascade": { "enabled": true },
|
|
44
|
+
"complexity": { "enabled": true }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
All configuration is optional — sensible defaults are applied for every field. The proxy merges your config with its defaults via deep merge, so you only need to specify what you want to change.
|
|
50
|
+
|
|
51
|
+
## Complexity-Based Routing
|
|
52
|
+
|
|
53
|
+
The proxy classifies incoming requests by complexity (simple, moderate, complex) based on prompt length, token patterns, and the presence of tools. Each tier maps to a different model.
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"routing": {
|
|
58
|
+
"complexity": {
|
|
59
|
+
"enabled": true,
|
|
60
|
+
"simple": "claude-3-5-haiku-latest",
|
|
61
|
+
"moderate": "claude-sonnet-4-20250514",
|
|
62
|
+
"complex": "claude-opus-4-20250514"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**How classification works:**
|
|
69
|
+
|
|
70
|
+
- **Simple** — Short prompts, straightforward Q&A, basic code tasks
|
|
71
|
+
- **Moderate** — Multi-step reasoning, code review, analysis with context
|
|
72
|
+
- **Complex** — Architecture decisions, large codebases, tasks with many tools, long prompts with evaluation/comparison language
|
|
73
|
+
|
|
74
|
+
The classifier scores requests based on message count, total token length, tool usage, and content patterns (e.g., words like "analyze", "compare", "evaluate" increase the score). This happens locally — no prompt content is sent anywhere.
|
|
75
|
+
|
|
76
|
+
## Model Overrides
|
|
36
77
|
|
|
37
|
-
|
|
78
|
+
Map any model name to a different one. Useful for silently redirecting expensive models to cheaper alternatives without changing your agent configuration:
|
|
38
79
|
|
|
39
|
-
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"modelOverrides": {
|
|
83
|
+
"claude-opus-4-5": "claude-3-5-haiku",
|
|
84
|
+
"gpt-4o": "gpt-4o-mini"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Overrides are applied before any other routing logic. The original requested model is logged for tracking.
|
|
90
|
+
|
|
91
|
+
## Cascade Mode
|
|
40
92
|
|
|
41
|
-
|
|
93
|
+
Start with the cheapest model and escalate only when the response shows uncertainty or refusal. This gives you the cost savings of a cheap model with a safety net.
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"routing": {
|
|
98
|
+
"mode": "cascade",
|
|
99
|
+
"cascade": {
|
|
100
|
+
"enabled": true,
|
|
101
|
+
"models": [
|
|
102
|
+
"claude-3-5-haiku-latest",
|
|
103
|
+
"claude-sonnet-4-20250514",
|
|
104
|
+
"claude-opus-4-20250514"
|
|
105
|
+
],
|
|
106
|
+
"escalateOn": "uncertainty",
|
|
107
|
+
"maxEscalations": 2
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
42
112
|
|
|
43
|
-
|
|
113
|
+
**`escalateOn` options:**
|
|
114
|
+
|
|
115
|
+
| Value | Triggers escalation when... |
|
|
116
|
+
|-------|----------------------------|
|
|
117
|
+
| `uncertainty` | Response contains hedging language ("I'm not sure", "it's hard to say", "this is just a guess") |
|
|
118
|
+
| `refusal` | Model refuses to help ("I can't assist with that", "as an AI") |
|
|
119
|
+
| `error` | The request fails outright |
|
|
120
|
+
|
|
121
|
+
**`maxEscalations`** caps how many times the proxy will retry with a more expensive model. Default: `1`.
|
|
122
|
+
|
|
123
|
+
The cascade walks through the `models` array in order, starting from the first. Each escalation moves to the next model in the list.
|
|
124
|
+
|
|
125
|
+
## Smart Aliases
|
|
126
|
+
|
|
127
|
+
Use semantic model names instead of provider-specific IDs:
|
|
128
|
+
|
|
129
|
+
| Alias | Resolves to |
|
|
130
|
+
|-------|------------|
|
|
131
|
+
| `rp:best` | `anthropic/claude-sonnet-4-20250514` |
|
|
132
|
+
| `rp:fast` | `anthropic/claude-3-5-haiku-20241022` |
|
|
133
|
+
| `rp:cheap` | `openai/gpt-4o-mini` |
|
|
134
|
+
| `rp:balanced` | `anthropic/claude-3-5-haiku-20241022` |
|
|
135
|
+
| `relayplane:auto` | Same as `rp:balanced` |
|
|
136
|
+
| `rp:auto` | Same as `rp:balanced` |
|
|
137
|
+
|
|
138
|
+
Use these as the `model` field in your API requests:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"model": "rp:fast",
|
|
143
|
+
"messages": [{"role": "user", "content": "Hello"}]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Routing Suffixes
|
|
148
|
+
|
|
149
|
+
Append `:cost`, `:fast`, or `:quality` to any model name to hint at routing preference:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"model": "claude-sonnet-4:cost",
|
|
154
|
+
"messages": [{"role": "user", "content": "Summarize this"}]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Suffix | Behavior |
|
|
159
|
+
|--------|----------|
|
|
160
|
+
| `:cost` | Optimize for lowest cost |
|
|
161
|
+
| `:fast` | Optimize for lowest latency |
|
|
162
|
+
| `:quality` | Optimize for best output quality |
|
|
163
|
+
|
|
164
|
+
The suffix is stripped before provider lookup — the base model must still be valid. Suffixes influence routing decisions when the proxy has multiple options.
|
|
165
|
+
|
|
166
|
+
## Provider Cooldowns / Reliability
|
|
167
|
+
|
|
168
|
+
When a provider starts failing, the proxy automatically cools it down to avoid hammering a broken endpoint:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"reliability": {
|
|
173
|
+
"cooldowns": {
|
|
174
|
+
"enabled": true,
|
|
175
|
+
"allowedFails": 3,
|
|
176
|
+
"windowSeconds": 60,
|
|
177
|
+
"cooldownSeconds": 120
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Field | Default | Description |
|
|
184
|
+
|-------|---------|-------------|
|
|
185
|
+
| `enabled` | `true` | Enable/disable cooldown tracking |
|
|
186
|
+
| `allowedFails` | `3` | Failures within the window before cooldown triggers |
|
|
187
|
+
| `windowSeconds` | `60` | Rolling window for counting failures |
|
|
188
|
+
| `cooldownSeconds` | `120` | How long to avoid the provider after cooldown triggers |
|
|
189
|
+
|
|
190
|
+
After cooldown expires, the provider is automatically retried. Successful requests clear the failure counter.
|
|
191
|
+
|
|
192
|
+
## Hybrid Auth
|
|
193
|
+
|
|
194
|
+
Use your Anthropic MAX subscription token for expensive models (Opus) while using standard API keys for cheaper models (Haiku, Sonnet). This lets you leverage MAX plan pricing where it matters most.
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"auth": {
|
|
199
|
+
"anthropicMaxToken": "sk-ant-oat-...",
|
|
200
|
+
"useMaxForModels": ["opus", "claude-opus"]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**How it works:**
|
|
206
|
+
|
|
207
|
+
- When a request targets a model matching any pattern in `useMaxForModels`, the proxy uses `anthropicMaxToken` with `Authorization: Bearer` header (OAuth-style)
|
|
208
|
+
- All other Anthropic requests use the standard `ANTHROPIC_API_KEY` env var with `x-api-key` header
|
|
209
|
+
- Pattern matching is case-insensitive substring match — `"opus"` matches `claude-opus-4-20250514`
|
|
210
|
+
|
|
211
|
+
Set your standard key in the environment as usual:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
export ANTHROPIC_API_KEY="sk-ant-api03-..."
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Telemetry
|
|
218
|
+
|
|
219
|
+
The proxy collects anonymized telemetry to improve routing decisions. Here's exactly what's collected:
|
|
220
|
+
|
|
221
|
+
- **device_id** — Random anonymous ID (no PII)
|
|
222
|
+
- **task_type** — Inferred from token patterns, NOT from prompt content
|
|
223
|
+
- **model** — Which model was used
|
|
224
|
+
- **tokens_in/out** — Token counts
|
|
225
|
+
- **latency_ms** — Response time
|
|
226
|
+
- **cost_usd** — Estimated cost
|
|
227
|
+
|
|
228
|
+
**Never collected:** prompts, responses, file paths, or anything that could identify you or your project.
|
|
229
|
+
|
|
230
|
+
### Disabling telemetry
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Via environment variable
|
|
234
|
+
RELAYPLANE_TELEMETRY=0 relayplane start
|
|
235
|
+
|
|
236
|
+
# Or in config — telemetry can be disabled programmatically via the config module
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Audit mode
|
|
240
|
+
|
|
241
|
+
Audit mode buffers telemetry events in memory so you can inspect exactly what would be sent before it goes anywhere. Useful for compliance review.
|
|
242
|
+
|
|
243
|
+
### Offline mode
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
relayplane start --offline
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Disables all network calls except the actual LLM requests. No telemetry transmission, no cloud features. The proxy still tracks everything locally for your dashboard.
|
|
250
|
+
|
|
251
|
+
## Dashboard
|
|
252
|
+
|
|
253
|
+
The built-in dashboard runs at [http://localhost:4100](http://localhost:4100) (or `/dashboard`). It shows:
|
|
254
|
+
|
|
255
|
+
- Total requests, success rate, average latency
|
|
256
|
+
- Cost breakdown by model and provider
|
|
257
|
+
- Recent request history with routing decisions
|
|
258
|
+
- Savings from routing optimizations
|
|
259
|
+
- Provider health status
|
|
260
|
+
|
|
261
|
+
### API Endpoints
|
|
262
|
+
|
|
263
|
+
The dashboard is powered by JSON endpoints you can use directly:
|
|
264
|
+
|
|
265
|
+
| Endpoint | Description |
|
|
266
|
+
|----------|-------------|
|
|
267
|
+
| `GET /v1/telemetry/stats` | Aggregate statistics (total requests, costs, model counts) |
|
|
268
|
+
| `GET /v1/telemetry/runs?limit=N` | Recent request history |
|
|
269
|
+
| `GET /v1/telemetry/savings` | Cost savings from smart routing |
|
|
270
|
+
| `GET /v1/telemetry/health` | Provider health and cooldown status |
|
|
271
|
+
|
|
272
|
+
## Circuit Breaker
|
|
273
|
+
|
|
274
|
+
If the proxy ever fails, all traffic automatically bypasses it — your agent talks directly to the provider. When RelayPlane recovers, traffic resumes. No manual intervention needed.
|
|
275
|
+
|
|
276
|
+
## Your Keys Stay Yours
|
|
44
277
|
|
|
45
278
|
RelayPlane requires your own provider API keys. We never see your prompts, keys, or data. All execution is local.
|
|
46
279
|
|
|
47
|
-
##
|
|
280
|
+
## License
|
|
48
281
|
|
|
49
|
-
[MIT](https://github.com/RelayPlane/proxy/blob/main/LICENSE)
|
|
282
|
+
[MIT](https://github.com/RelayPlane/proxy/blob/main/LICENSE)
|
|
50
283
|
|
|
51
284
|
---
|
|
52
285
|
|
|
53
286
|
[relayplane.com](https://relayplane.com) · [GitHub](https://github.com/RelayPlane/proxy)
|
|
287
|
+
|
package/dist/cli.js
CHANGED
|
@@ -87,6 +87,238 @@ async function checkForUpdate() {
|
|
|
87
87
|
return null; // Network error, offline, etc. — silently skip
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
const CREDENTIALS_PATH = (0, path_1.join)((0, os_1.homedir)(), '.relayplane', 'credentials.json');
|
|
91
|
+
function loadCredentials() {
|
|
92
|
+
try {
|
|
93
|
+
if ((0, fs_1.existsSync)(CREDENTIALS_PATH)) {
|
|
94
|
+
return JSON.parse((0, fs_1.readFileSync)(CREDENTIALS_PATH, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function saveCredentials(creds) {
|
|
101
|
+
const dir = (0, path_1.dirname)(CREDENTIALS_PATH);
|
|
102
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
103
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
104
|
+
(0, fs_1.writeFileSync)(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + '\n');
|
|
105
|
+
}
|
|
106
|
+
function clearCredentials() {
|
|
107
|
+
try {
|
|
108
|
+
if ((0, fs_1.existsSync)(CREDENTIALS_PATH)) {
|
|
109
|
+
(0, fs_1.writeFileSync)(CREDENTIALS_PATH, '{}');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
}
|
|
114
|
+
const API_URL = process.env.RELAYPLANE_API_URL || 'https://api.relayplane.com';
|
|
115
|
+
// ============================================
|
|
116
|
+
// LOGIN COMMAND (Device OAuth Flow)
|
|
117
|
+
// ============================================
|
|
118
|
+
async function handleLoginCommand() {
|
|
119
|
+
const existing = loadCredentials();
|
|
120
|
+
if (existing?.apiKey) {
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(' ✅ Already logged in');
|
|
123
|
+
if (existing.email)
|
|
124
|
+
console.log(` Account: ${existing.email}`);
|
|
125
|
+
if (existing.plan)
|
|
126
|
+
console.log(` Plan: ${existing.plan}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(' Run `relayplane logout` first to switch accounts.');
|
|
129
|
+
console.log('');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(' 🔐 Logging in to RelayPlane...');
|
|
134
|
+
console.log('');
|
|
135
|
+
try {
|
|
136
|
+
// Start device auth flow
|
|
137
|
+
const startRes = await fetch(`${API_URL}/v1/cli/device/start`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ client: 'relayplane-proxy', version: VERSION }),
|
|
141
|
+
});
|
|
142
|
+
if (!startRes.ok) {
|
|
143
|
+
console.error(' ❌ Failed to start login flow. Is the API reachable?');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const { deviceCode, userCode, verificationUrl, pollIntervalSec, expiresIn } = await startRes.json();
|
|
147
|
+
console.log(` Open this URL in your browser:`);
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(` ${verificationUrl}`);
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(` And enter this code:`);
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(` 📋 ${userCode}`);
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(` Waiting for approval (expires in ${Math.floor(expiresIn / 60)} minutes)...`);
|
|
156
|
+
// Try to open browser automatically
|
|
157
|
+
try {
|
|
158
|
+
const { exec: execCmd } = await import('child_process');
|
|
159
|
+
const openCmd = process.platform === 'darwin' ? 'open'
|
|
160
|
+
: process.platform === 'win32' ? 'start'
|
|
161
|
+
: 'xdg-open';
|
|
162
|
+
execCmd(`${openCmd} "${verificationUrl}"`);
|
|
163
|
+
}
|
|
164
|
+
catch { }
|
|
165
|
+
// Poll for approval
|
|
166
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
167
|
+
while (Date.now() < deadline) {
|
|
168
|
+
await new Promise(r => setTimeout(r, (pollIntervalSec || 5) * 1000));
|
|
169
|
+
const pollRes = await fetch(`${API_URL}/v1/cli/device/poll`, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ deviceCode }),
|
|
173
|
+
});
|
|
174
|
+
if (!pollRes.ok)
|
|
175
|
+
continue;
|
|
176
|
+
const pollData = await pollRes.json();
|
|
177
|
+
if (pollData.status === 'approved') {
|
|
178
|
+
saveCredentials({
|
|
179
|
+
apiKey: pollData.accessToken,
|
|
180
|
+
plan: pollData.plan || 'free',
|
|
181
|
+
teamId: pollData.teamId,
|
|
182
|
+
teamName: pollData.teamName,
|
|
183
|
+
loggedInAt: new Date().toISOString(),
|
|
184
|
+
});
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(' ✅ Login successful!');
|
|
187
|
+
if (pollData.teamName)
|
|
188
|
+
console.log(` Team: ${pollData.teamName}`);
|
|
189
|
+
console.log(` Plan: ${pollData.plan || 'free'}`);
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log(' ☁️ Cloud sync will activate on next proxy start.');
|
|
192
|
+
console.log('');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (pollData.status === 'denied') {
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log(' ❌ Login denied.');
|
|
198
|
+
console.log('');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
if (pollData.status === 'expired') {
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(' ⏰ Login expired. Please try again.');
|
|
204
|
+
console.log('');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
// Still pending, continue polling
|
|
208
|
+
process.stdout.write('.');
|
|
209
|
+
}
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(' ⏰ Login timed out. Please try again.');
|
|
212
|
+
console.log('');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
console.error(' ❌ Login failed:', err instanceof Error ? err.message : err);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ============================================
|
|
221
|
+
// LOGOUT COMMAND
|
|
222
|
+
// ============================================
|
|
223
|
+
function handleLogoutCommand() {
|
|
224
|
+
const creds = loadCredentials();
|
|
225
|
+
clearCredentials();
|
|
226
|
+
console.log('');
|
|
227
|
+
if (creds?.apiKey) {
|
|
228
|
+
console.log(' ✅ Logged out successfully.');
|
|
229
|
+
console.log(' Cloud sync will stop on next proxy restart.');
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.log(' ℹ️ Not logged in.');
|
|
233
|
+
}
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
// ============================================
|
|
237
|
+
// UPGRADE COMMAND
|
|
238
|
+
// ============================================
|
|
239
|
+
function handleUpgradeCommand() {
|
|
240
|
+
const url = 'https://relayplane.com/pricing';
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(' 🚀 Opening pricing page...');
|
|
243
|
+
console.log(` ${url}`);
|
|
244
|
+
console.log('');
|
|
245
|
+
try {
|
|
246
|
+
const { exec: execCmd } = require('child_process');
|
|
247
|
+
const openCmd = process.platform === 'darwin' ? 'open'
|
|
248
|
+
: process.platform === 'win32' ? 'start'
|
|
249
|
+
: 'xdg-open';
|
|
250
|
+
execCmd(`${openCmd} "${url}"`);
|
|
251
|
+
}
|
|
252
|
+
catch { }
|
|
253
|
+
}
|
|
254
|
+
// ============================================
|
|
255
|
+
// ENHANCED STATUS COMMAND
|
|
256
|
+
// ============================================
|
|
257
|
+
async function handleCloudStatusCommand() {
|
|
258
|
+
const creds = loadCredentials();
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log(' 📊 RelayPlane Status');
|
|
261
|
+
console.log(' ════════════════════');
|
|
262
|
+
console.log('');
|
|
263
|
+
// Proxy status
|
|
264
|
+
let proxyReachable = false;
|
|
265
|
+
try {
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
268
|
+
const res = await fetch('http://127.0.0.1:4100/health', { signal: controller.signal });
|
|
269
|
+
clearTimeout(timeout);
|
|
270
|
+
proxyReachable = res.ok;
|
|
271
|
+
}
|
|
272
|
+
catch { }
|
|
273
|
+
console.log(` Proxy: ${proxyReachable ? '🟢 Running' : '🔴 Stopped'}`);
|
|
274
|
+
// Auth status
|
|
275
|
+
if (creds?.apiKey) {
|
|
276
|
+
console.log(` Account: ✅ Logged in${creds.email ? ` (${creds.email})` : ''}`);
|
|
277
|
+
console.log(` Plan: ${creds.plan || 'free'}`);
|
|
278
|
+
console.log(` API Key: ••••${creds.apiKey.slice(-4)}`);
|
|
279
|
+
// Check cloud sync
|
|
280
|
+
if (proxyReachable) {
|
|
281
|
+
console.log(` Cloud sync: ☁️ Active`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.log(` Cloud sync: ⏸️ Proxy not running`);
|
|
285
|
+
}
|
|
286
|
+
// Try to get fresh plan info from API
|
|
287
|
+
try {
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
290
|
+
const res = await fetch(`${API_URL}/v1/cli/teams/current`, {
|
|
291
|
+
signal: controller.signal,
|
|
292
|
+
headers: { 'Authorization': `Bearer ${creds.apiKey}` },
|
|
293
|
+
});
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
if (res.ok) {
|
|
296
|
+
const data = await res.json();
|
|
297
|
+
if (data.plan && data.plan !== creds.plan) {
|
|
298
|
+
creds.plan = data.plan;
|
|
299
|
+
saveCredentials(creds);
|
|
300
|
+
console.log(` Plan (live): ${data.plan}`);
|
|
301
|
+
}
|
|
302
|
+
if (data.teamName)
|
|
303
|
+
console.log(` Team: ${data.teamName}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch { }
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log(` Account: ❌ Not logged in`);
|
|
310
|
+
console.log(` Plan: free (local only)`);
|
|
311
|
+
console.log(` Cloud sync: ❌ Disabled`);
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
if (!creds?.apiKey) {
|
|
315
|
+
console.log(' Run `relayplane login` to enable cloud features.');
|
|
316
|
+
}
|
|
317
|
+
else if (creds.plan === 'free') {
|
|
318
|
+
console.log(' Run `relayplane upgrade` to unlock cloud dashboard.');
|
|
319
|
+
}
|
|
320
|
+
console.log('');
|
|
321
|
+
}
|
|
90
322
|
function printHelp() {
|
|
91
323
|
console.log(`
|
|
92
324
|
RelayPlane Proxy - Intelligent AI Model Routing
|
|
@@ -97,7 +329,10 @@ Usage:
|
|
|
97
329
|
|
|
98
330
|
Commands:
|
|
99
331
|
(default) Start the proxy server
|
|
100
|
-
|
|
332
|
+
login Log in to RelayPlane (opens browser)
|
|
333
|
+
logout Clear stored credentials
|
|
334
|
+
status Show proxy status, plan, and cloud sync
|
|
335
|
+
upgrade Open pricing page in browser
|
|
101
336
|
enable Enable RelayPlane in openclaw.json
|
|
102
337
|
disable Disable RelayPlane in openclaw.json
|
|
103
338
|
telemetry [on|off|status] Manage telemetry settings
|
|
@@ -469,8 +704,20 @@ async function main() {
|
|
|
469
704
|
handleConfigCommand(args.slice(1));
|
|
470
705
|
process.exit(0);
|
|
471
706
|
}
|
|
707
|
+
if (command === 'login') {
|
|
708
|
+
await handleLoginCommand();
|
|
709
|
+
process.exit(0);
|
|
710
|
+
}
|
|
711
|
+
if (command === 'logout') {
|
|
712
|
+
handleLogoutCommand();
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
if (command === 'upgrade') {
|
|
716
|
+
handleUpgradeCommand();
|
|
717
|
+
process.exit(0);
|
|
718
|
+
}
|
|
472
719
|
if (command === 'status') {
|
|
473
|
-
await
|
|
720
|
+
await handleCloudStatusCommand();
|
|
474
721
|
process.exit(0);
|
|
475
722
|
}
|
|
476
723
|
if (command === 'mesh') {
|
|
@@ -547,6 +794,7 @@ async function main() {
|
|
|
547
794
|
console.log('');
|
|
548
795
|
// Show modes
|
|
549
796
|
const telemetryEnabled = (0, config_js_1.isTelemetryEnabled)();
|
|
797
|
+
const creds = loadCredentials();
|
|
550
798
|
console.log(' Mode:');
|
|
551
799
|
if (offline) {
|
|
552
800
|
console.log(' 🔒 Offline (no telemetry transmission)');
|
|
@@ -560,6 +808,13 @@ async function main() {
|
|
|
560
808
|
else {
|
|
561
809
|
console.log(' 📴 Telemetry disabled');
|
|
562
810
|
}
|
|
811
|
+
// Cloud sync status
|
|
812
|
+
if (creds?.apiKey && !offline) {
|
|
813
|
+
console.log(` ☁️ Cloud sync: active (plan: ${creds.plan || 'free'})`);
|
|
814
|
+
}
|
|
815
|
+
else if (!creds?.apiKey) {
|
|
816
|
+
console.log(' 💻 Local only (run `relayplane login` for cloud sync)');
|
|
817
|
+
}
|
|
563
818
|
console.log('');
|
|
564
819
|
console.log(' Providers:');
|
|
565
820
|
if (hasAnthropicKey)
|