@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 CHANGED
@@ -1,53 +1,287 @@
1
- # RelayPlane — Cost Intelligence for AI Agents
1
+ # @relayplane/proxy
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@relayplane/proxy)](https://www.npmjs.com/package/@relayplane/proxy)
4
4
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/RelayPlane/proxy/blob/main/LICENSE)
5
5
 
6
- **See where every dollar goes. Route to the cheapest model that works. Cut LLM costs ~80%.**
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
- RelayPlane is an open-source proxy that sits between your AI agents and LLM providers. It tracks every request, shows you exactly where the money goes, and uses smart routing to send tasks to the right model — all running locally on your machine.
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
- # Open http://localhost:4100 — your cost dashboard
14
+ # Dashboard at http://localhost:4100
17
15
  ```
18
16
 
19
- Three minutes to full cost visibility. Works with any agent framework no config changes needed.
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
- ## 🎯 How It Works
19
+ ## Supported Providers
22
20
 
23
- **Observe** Every LLM request is tracked: cost, model, tokens, latency. See it all in the local dashboard.
21
+ **Anthropic** · **OpenAI** · **Google Gemini** · **xAI/Grok** · **OpenRouter** · **DeepSeek** · **Groq** · **Mistral** · **Together** · **Fireworks** · **Perplexity**
24
22
 
25
- **Govern** — The policy engine classifies tasks and routes to the most cost-effective model. Simple file reads → Haiku. Complex architecture → Opus. You set the rules or use the defaults.
23
+ ## Configuration
26
24
 
27
- **Learn** *(coming soon)* Opt into the collective mesh. Share anonymized routing outcomes. As the mesh grows, routing gets smarter for everyone.
25
+ RelayPlane reads configuration from `~/.relayplane/config.json`. Override the path with the `RELAYPLANE_CONFIG_PATH` environment variable.
28
26
 
29
- ## 🔌 Supported Providers
27
+ ```bash
28
+ # Default location
29
+ ~/.relayplane/config.json
30
30
 
31
- All 11 major providers work out of the box:
31
+ # Override with env var
32
+ RELAYPLANE_CONFIG_PATH=/path/to/config.json relayplane start
33
+ ```
32
34
 
33
- **Anthropic** · **OpenAI** · **Google Gemini** · **xAI/Grok** · **OpenRouter** · **DeepSeek** · **Groq** · **Mistral** · **Together** · **Fireworks** · **Perplexity**
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
- ## 🛡️ Circuit Breaker Safety
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
- 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. Worst case: you pay what you would have paid anyway.
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
- ## 💰 Free Forever
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
- Everything that runs on your machine is free. No trials, no gates, no artificial limits. Full proxy, local dashboard, all 11 providers, smart routing, policy engine.
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
- ## 🔐 Your Keys Stay Yours
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
- ## 📄 License
280
+ ## License
48
281
 
49
- [MIT](https://github.com/RelayPlane/proxy/blob/main/LICENSE) — open source, free forever.
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
- status Show proxy status (circuit state, stats, process info)
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 handleStatusCommand();
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)