@relayplane/proxy 1.9.20 → 1.9.22

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/dist/cli.js CHANGED
@@ -76,12 +76,19 @@ const lifecycle_telemetry_js_1 = require("./lifecycle-telemetry.js");
76
76
  const config_js_1 = require("./config.js");
77
77
  const telemetry_js_1 = require("./telemetry.js");
78
78
  const fs_1 = require("fs");
79
- const fs_2 = require("fs");
80
79
  const path_1 = require("path");
81
80
  const os_1 = require("os");
81
+ const agent_policy_js_1 = require("./agent-policy.js");
82
+ const js_yaml_1 = require("js-yaml");
82
83
  const net = __importStar(require("net"));
83
84
  const readline = __importStar(require("readline"));
84
85
  const child_process_1 = require("child_process");
86
+ const os = __importStar(require("node:os"));
87
+ const policy_analyzer_js_1 = require("./policy-analyzer.js");
88
+ const policy_suggestions_js_1 = require("./policy-suggestions.js");
89
+ const agent_tracker_js_1 = require("./agent-tracker.js");
90
+ const routing_log_js_1 = require("./routing-log.js");
91
+ const agent_policy_js_2 = require("./agent-policy.js");
85
92
  const response_cache_js_1 = require("./response-cache.js");
86
93
  const budget_js_1 = require("./budget.js");
87
94
  const alerts_js_1 = require("./alerts.js");
@@ -480,7 +487,7 @@ async function handleEnsureRunning() {
480
487
  process.kill(stalePid, 0);
481
488
  }
482
489
  catch {
483
- (0, fs_2.unlinkSync)(pidFile);
490
+ (0, fs_1.unlinkSync)(pidFile);
484
491
  }
485
492
  }
486
493
  }
@@ -610,6 +617,37 @@ function handleTelemetryCommand(args) {
610
617
  break;
611
618
  }
612
619
  }
620
+ function handleLifecycleCommand(args) {
621
+ const subcommand = args[0];
622
+ switch (subcommand) {
623
+ case 'on':
624
+ (0, config_js_1.enableLifecycle)();
625
+ console.log('✅ Lifecycle telemetry enabled');
626
+ console.log(' Anonymous install and daily-session pings will be sent.');
627
+ console.log(' No request content, model names, or token counts.');
628
+ break;
629
+ case 'off':
630
+ (0, config_js_1.disableLifecycle)();
631
+ console.log('✅ Lifecycle telemetry disabled');
632
+ console.log(' No pings will be sent. The proxy continues to work normally.');
633
+ break;
634
+ case 'status':
635
+ default: {
636
+ const enabled = (0, config_js_1.isLifecycleEnabled)();
637
+ console.log('');
638
+ console.log('📡 Lifecycle Telemetry Status');
639
+ console.log('─────────────────────────────');
640
+ console.log(` Enabled: ${enabled ? '✅ Yes (default)' : '❌ No'}`);
641
+ console.log(' Sends: anonymous install + daily session + dashboard-link pings.');
642
+ console.log(' Never sends: request content, model names, tokens, costs.');
643
+ console.log('');
644
+ console.log(' To enable: relayplane lifecycle on');
645
+ console.log(' To disable: relayplane lifecycle off');
646
+ console.log('');
647
+ break;
648
+ }
649
+ }
650
+ }
613
651
  function handleStatsCommand() {
614
652
  const stats = (0, telemetry_js_1.getTelemetryStats)();
615
653
  console.log('');
@@ -1355,9 +1393,9 @@ async function main() {
1355
1393
  args.shift();
1356
1394
  }
1357
1395
  const knownCommands = new Set([
1358
- 'init', 'start', 'telemetry', 'stats', 'config', 'login', 'logout', 'upgrade',
1396
+ 'init', 'start', 'telemetry', 'lifecycle', 'stats', 'config', 'login', 'logout', 'upgrade',
1359
1397
  'status', 'autostart', 'service', 'mesh', 'cache', 'budget', 'alerts', 'enable', 'disable',
1360
- 'ensure-running',
1398
+ 'ensure-running', 'agents', 'policy', 'setup',
1361
1399
  ]);
1362
1400
  if (command && !command.startsWith('-') && !knownCommands.has(command)) {
1363
1401
  console.error(`Unknown command: ${command}`);
@@ -1368,6 +1406,10 @@ async function main() {
1368
1406
  handleTelemetryCommand(args.slice(1));
1369
1407
  process.exit(0);
1370
1408
  }
1409
+ if (command === 'lifecycle') {
1410
+ handleLifecycleCommand(args.slice(1));
1411
+ process.exit(0);
1412
+ }
1371
1413
  if (command === 'stats') {
1372
1414
  handleStatsCommand();
1373
1415
  process.exit(0);
@@ -1428,6 +1470,18 @@ async function main() {
1428
1470
  await handleEnsureRunning();
1429
1471
  process.exit(0);
1430
1472
  }
1473
+ if (command === 'agents') {
1474
+ handleAgentsCommand(args.slice(1));
1475
+ process.exit(0);
1476
+ }
1477
+ if (command === 'setup') {
1478
+ await handleSetupCommand();
1479
+ process.exit(0);
1480
+ }
1481
+ if (command === 'policy') {
1482
+ await handlePolicyCommand(args.slice(1));
1483
+ process.exit(0);
1484
+ }
1431
1485
  // Parse server options
1432
1486
  let port = 4100;
1433
1487
  let host = '127.0.0.1';
@@ -2029,5 +2083,705 @@ function handleCacheCommand(args) {
2029
2083
  }
2030
2084
  console.log('Usage: relayplane cache [status|clear|stats|on|off]');
2031
2085
  }
2086
+ // ─── agents commands ──────────────────────────────────────────────────────────
2087
+ function handleAgentsCommand(subArgs) {
2088
+ const sub = subArgs[0];
2089
+ if (sub !== 'list') {
2090
+ console.log('Usage: relayplane agents list');
2091
+ return;
2092
+ }
2093
+ const registryFile = (0, path_1.join)((0, os_1.homedir)(), '.relayplane', 'agents.json');
2094
+ if (!(0, fs_1.existsSync)(registryFile)) {
2095
+ console.log('No agents found. Run the proxy to start tracking agents.');
2096
+ return;
2097
+ }
2098
+ let registry;
2099
+ try {
2100
+ registry = JSON.parse((0, fs_1.readFileSync)(registryFile, 'utf-8'));
2101
+ }
2102
+ catch {
2103
+ console.error('Failed to read agents registry');
2104
+ return;
2105
+ }
2106
+ const entries = Object.values(registry);
2107
+ if (entries.length === 0) {
2108
+ console.log('No agents found.');
2109
+ return;
2110
+ }
2111
+ // Format relative time
2112
+ function relTime(iso) {
2113
+ const diff = Date.now() - new Date(iso).getTime();
2114
+ const mins = Math.floor(diff / 60000);
2115
+ if (mins < 1)
2116
+ return 'just now';
2117
+ if (mins < 60)
2118
+ return `${mins} min${mins > 1 ? 's' : ''} ago`;
2119
+ const hrs = Math.floor(mins / 60);
2120
+ if (hrs < 24)
2121
+ return `${hrs} hour${hrs > 1 ? 's' : ''} ago`;
2122
+ const days = Math.floor(hrs / 24);
2123
+ return `${days} day${days > 1 ? 's' : ''} ago`;
2124
+ }
2125
+ const fpLen = 18;
2126
+ const nameLen = 16;
2127
+ const lastLen = 18;
2128
+ const reqLen = 10;
2129
+ const costLen = 10;
2130
+ const header = 'FINGERPRINT'.padEnd(fpLen) +
2131
+ 'NAME'.padEnd(nameLen) +
2132
+ 'LAST SEEN'.padEnd(lastLen) +
2133
+ 'REQUESTS'.padEnd(reqLen) +
2134
+ 'COST'.padEnd(costLen);
2135
+ console.log('');
2136
+ console.log(header);
2137
+ console.log('-'.repeat(fpLen + nameLen + lastLen + reqLen + costLen));
2138
+ for (const e of entries) {
2139
+ const row = (e.fingerprint ?? '—').padEnd(fpLen) +
2140
+ (e.name ?? '—').padEnd(nameLen) +
2141
+ relTime(e.lastSeen ?? '').padEnd(lastLen) +
2142
+ (e.totalRequests?.toLocaleString() ?? '0').padEnd(reqLen) +
2143
+ ('$' + (e.totalCost ?? 0).toFixed(2)).padEnd(costLen);
2144
+ console.log(row);
2145
+ }
2146
+ console.log('');
2147
+ }
2148
+ // ─── policy commands ──────────────────────────────────────────────────────────
2149
+ async function handlePolicyCommand(subArgs) {
2150
+ const sub = subArgs[0];
2151
+ if (sub === 'show') {
2152
+ if (!(0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2153
+ console.log("No policy file found. Run 'relayplane policy init' to create one.");
2154
+ return;
2155
+ }
2156
+ const policy = (0, agent_policy_js_1.loadPolicy)();
2157
+ console.log('');
2158
+ console.log('Policy file:', agent_policy_js_1.POLICY_FILE);
2159
+ console.log(`Version: ${policy.version}`);
2160
+ if (policy.agents && Object.keys(policy.agents).length > 0) {
2161
+ console.log('');
2162
+ console.log('Agent rules:');
2163
+ for (const [name, agent] of Object.entries(policy.agents)) {
2164
+ let line = ` ${name}: preferred=${agent.preferred}`;
2165
+ if (agent.escalateTo)
2166
+ line += ` escalateTo=${agent.escalateTo}`;
2167
+ if (agent.neverDowngrade)
2168
+ line += ' neverDowngrade=true';
2169
+ if (agent.budgetPerDay !== undefined)
2170
+ line += ` budgetPerDay=$${agent.budgetPerDay}`;
2171
+ console.log(line);
2172
+ }
2173
+ }
2174
+ if (policy.tasks && Object.keys(policy.tasks).length > 0) {
2175
+ console.log('');
2176
+ console.log('Task rules:');
2177
+ for (const [taskType, task] of Object.entries(policy.tasks)) {
2178
+ let line = ` ${taskType}: preferred=${task.preferred}`;
2179
+ if (task.escalateTo)
2180
+ line += ` escalateTo=${task.escalateTo}`;
2181
+ if (task.neverDowngrade)
2182
+ line += ' neverDowngrade=true';
2183
+ console.log(line);
2184
+ }
2185
+ }
2186
+ console.log('');
2187
+ return;
2188
+ }
2189
+ if (sub === 'init') {
2190
+ const force = subArgs.includes('--force');
2191
+ if ((0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE) && !force) {
2192
+ console.error("policy.yaml already exists. Use --force to overwrite.");
2193
+ process.exit(1);
2194
+ }
2195
+ const registryFile = (0, path_1.join)((0, os_1.homedir)(), '.relayplane', 'agents.json');
2196
+ let registry = {};
2197
+ if ((0, fs_1.existsSync)(registryFile)) {
2198
+ try {
2199
+ registry = JSON.parse((0, fs_1.readFileSync)(registryFile, 'utf-8'));
2200
+ }
2201
+ catch {
2202
+ // use empty registry
2203
+ }
2204
+ }
2205
+ const agents = {};
2206
+ for (const [fp, entry] of Object.entries(registry)) {
2207
+ const avgCost = entry.totalRequests > 0 ? entry.totalCost / entry.totalRequests : 0;
2208
+ const preferred = avgCost > 0.01 ? 'anthropic/claude-sonnet-4-6' : 'cheapest-capable';
2209
+ agents[entry.name] = {
2210
+ fingerprint: fp,
2211
+ preferred,
2212
+ };
2213
+ }
2214
+ const policy = { version: 1, agents: Object.keys(agents).length > 0 ? agents : undefined };
2215
+ const yaml = (0, js_yaml_1.dump)(policy, { lineWidth: 120 });
2216
+ (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.relayplane'), { recursive: true });
2217
+ (0, fs_1.writeFileSync)(agent_policy_js_1.POLICY_FILE, yaml, 'utf-8');
2218
+ const count = Object.keys(agents).length;
2219
+ console.log(`Wrote ${agent_policy_js_1.POLICY_FILE} with ${count} agent${count !== 1 ? 's' : ''}. Edit to customize, then restart the proxy.`);
2220
+ return;
2221
+ }
2222
+ if (sub === 'set-agent') {
2223
+ const name = subArgs[1];
2224
+ if (!name) {
2225
+ console.error('Usage: relayplane policy set-agent <name> --preferred <model> [--escalate <model>] [--budget <amount>]');
2226
+ process.exit(1);
2227
+ }
2228
+ // Parse options
2229
+ let preferred;
2230
+ let escalateTo;
2231
+ let budget;
2232
+ for (let i = 2; i < subArgs.length; i++) {
2233
+ if (subArgs[i] === '--preferred' && subArgs[i + 1]) {
2234
+ preferred = subArgs[i + 1];
2235
+ i++;
2236
+ }
2237
+ else if (subArgs[i] === '--escalate' && subArgs[i + 1]) {
2238
+ escalateTo = subArgs[i + 1];
2239
+ i++;
2240
+ }
2241
+ else if (subArgs[i] === '--budget' && subArgs[i + 1]) {
2242
+ budget = parseFloat(subArgs[i + 1]);
2243
+ i++;
2244
+ }
2245
+ }
2246
+ if (!preferred) {
2247
+ console.error('--preferred is required');
2248
+ process.exit(1);
2249
+ }
2250
+ // Load or create policy
2251
+ let rawPolicy = { version: 1 };
2252
+ if ((0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2253
+ try {
2254
+ rawPolicy = (0, js_yaml_1.load)((0, fs_1.readFileSync)(agent_policy_js_1.POLICY_FILE, 'utf-8')) ?? { version: 1 };
2255
+ }
2256
+ catch {
2257
+ rawPolicy = { version: 1 };
2258
+ }
2259
+ }
2260
+ if (!rawPolicy.agents || typeof rawPolicy.agents !== 'object') {
2261
+ rawPolicy.agents = {};
2262
+ }
2263
+ const agentsObj = rawPolicy.agents;
2264
+ const existing = agentsObj[name] ?? {};
2265
+ const updated = { ...existing, preferred };
2266
+ if (escalateTo)
2267
+ updated['escalateTo'] = escalateTo;
2268
+ if (budget !== undefined && !isNaN(budget))
2269
+ updated['budgetPerDay'] = budget;
2270
+ agentsObj[name] = updated;
2271
+ // Atomic write via tmp + rename
2272
+ (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.relayplane'), { recursive: true });
2273
+ const tmp = agent_policy_js_1.POLICY_FILE + '.tmp';
2274
+ (0, fs_1.writeFileSync)(tmp, (0, js_yaml_1.dump)(rawPolicy, { lineWidth: 120 }), 'utf-8');
2275
+ (0, fs_1.renameSync)(tmp, agent_policy_js_1.POLICY_FILE);
2276
+ console.log(`Updated policy for agent "${name}": preferred=${preferred}${escalateTo ? ` escalateTo=${escalateTo}` : ''}${budget !== undefined ? ` budgetPerDay=$${budget}` : ''}`);
2277
+ return;
2278
+ }
2279
+ // ── policy auto ──────────────────────────────────────────────────────────
2280
+ if (sub === 'auto') {
2281
+ const yesFlag = subArgs.includes('--yes') || subArgs.includes('-y');
2282
+ let lookbackDays = 7;
2283
+ const lookbackIdx = subArgs.indexOf('--lookback');
2284
+ if (lookbackIdx !== -1 && subArgs[lookbackIdx + 1]) {
2285
+ lookbackDays = parseInt(subArgs[lookbackIdx + 1], 10) || 7;
2286
+ }
2287
+ process.stdout.write(`Analyzing ${lookbackDays} days of traffic...`);
2288
+ const analyses = await (0, policy_analyzer_js_1.analyzeTraffic)({ lookbackDays });
2289
+ process.stdout.write('\n');
2290
+ if (analyses.length === 0) {
2291
+ console.log('');
2292
+ console.log(' No traffic data found.');
2293
+ console.log(' Run RelayPlane for at least a day and try again.');
2294
+ console.log(' (or use --lookback 30 to look further back)');
2295
+ console.log('');
2296
+ return;
2297
+ }
2298
+ const availableProviders = (0, policy_suggestions_js_1.detectAvailableProviders)();
2299
+ await _runPolicyAutoDisplay(analyses, availableProviders, yesFlag, true);
2300
+ return;
2301
+ }
2302
+ // ── policy suggest ────────────────────────────────────────────────────────
2303
+ if (sub === 'suggest') {
2304
+ let lookbackDays = 7;
2305
+ const lookbackIdx = subArgs.indexOf('--lookback');
2306
+ if (lookbackIdx !== -1 && subArgs[lookbackIdx + 1]) {
2307
+ lookbackDays = parseInt(subArgs[lookbackIdx + 1], 10) || 7;
2308
+ }
2309
+ process.stdout.write(`Analyzing ${lookbackDays} days of traffic...`);
2310
+ const analyses = await (0, policy_analyzer_js_1.analyzeTraffic)({ lookbackDays });
2311
+ process.stdout.write('\n');
2312
+ if (analyses.length === 0) {
2313
+ console.log('');
2314
+ console.log(' No traffic data found.');
2315
+ console.log(' Run RelayPlane for at least a day and try again.');
2316
+ console.log('');
2317
+ return;
2318
+ }
2319
+ const availableProviders = (0, policy_suggestions_js_1.detectAvailableProviders)();
2320
+ await _runPolicyAutoDisplay(analyses, availableProviders, false, false);
2321
+ return;
2322
+ }
2323
+ // ── policy test ───────────────────────────────────────────────────────────
2324
+ if (sub === 'test') {
2325
+ let requestCount = 50;
2326
+ let policyPath;
2327
+ for (let i = 1; i < subArgs.length; i++) {
2328
+ if (subArgs[i] === '--requests' && subArgs[i + 1]) {
2329
+ requestCount = parseInt(subArgs[i + 1], 10) || 50;
2330
+ i++;
2331
+ }
2332
+ else if (subArgs[i] === '--policy' && subArgs[i + 1]) {
2333
+ policyPath = subArgs[i + 1];
2334
+ i++;
2335
+ }
2336
+ }
2337
+ const entries = (0, routing_log_js_1.getRoutingLog)({ limit: requestCount });
2338
+ if (entries.length === 0) {
2339
+ console.log('No routing log entries found. Run RelayPlane first to generate traffic.');
2340
+ return;
2341
+ }
2342
+ let testPolicy;
2343
+ let policySource;
2344
+ if (policyPath) {
2345
+ const raw = (0, fs_1.readFileSync)(policyPath, 'utf-8');
2346
+ testPolicy = (0, js_yaml_1.load)(raw);
2347
+ policySource = (0, path_1.basename)(policyPath);
2348
+ }
2349
+ else if ((0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2350
+ testPolicy = (0, agent_policy_js_1.loadPolicy)();
2351
+ policySource = 'policy.yaml';
2352
+ }
2353
+ else {
2354
+ const analyses = await (0, policy_analyzer_js_1.analyzeTraffic)({ lookbackDays: 7 });
2355
+ const providers = (0, policy_suggestions_js_1.detectAvailableProviders)();
2356
+ const suggestions = (0, policy_suggestions_js_1.suggestPolicies)(analyses, providers);
2357
+ testPolicy = _buildPolicyFromSuggestions(analyses, suggestions);
2358
+ policySource = 'proposed policy';
2359
+ }
2360
+ console.log(`Replaying last ${entries.length} requests against ${policySource}...`);
2361
+ console.log('');
2362
+ const changes = [];
2363
+ for (const entry of entries) {
2364
+ const resolution = (0, agent_policy_js_2.resolvePolicy)(testPolicy, entry.agentFingerprint ?? undefined, entry.agentName ?? undefined, entry.taskType, entry.complexity, entry.resolvedModel);
2365
+ if (resolution.model !== entry.resolvedModel) {
2366
+ const savedCost = (0, policy_analyzer_js_1.estimateDailyCost)(entry.inputTokens ?? 1000, entry.outputTokens ?? 200, 1, entry.resolvedModel) -
2367
+ (0, policy_analyzer_js_1.estimateDailyCost)(entry.inputTokens ?? 1000, entry.outputTokens ?? 200, 1, resolution.model);
2368
+ changes.push({
2369
+ fingerprint: (entry.agentFingerprint ?? 'unknown').slice(0, 8),
2370
+ taskType: entry.taskType,
2371
+ from: entry.resolvedModel,
2372
+ to: resolution.model,
2373
+ savedCost,
2374
+ });
2375
+ }
2376
+ }
2377
+ const total = entries.length;
2378
+ const changed = changes.length;
2379
+ const same = total - changed;
2380
+ if (changed === 0) {
2381
+ console.log(` All ${total} requests would route identically. No savings from this policy.`);
2382
+ }
2383
+ else {
2384
+ console.log(` ${changed}/${total} would route differently`);
2385
+ console.log(` ${same}/${total} would route identically`);
2386
+ console.log('');
2387
+ console.log('Changes:');
2388
+ for (const c of changes) {
2389
+ console.log(` ${c.fingerprint} / ${c.taskType}: ${c.from} → ${c.to}`);
2390
+ }
2391
+ }
2392
+ const totalSavings = Math.max(0, changes.reduce((sum, c) => sum + c.savedCost, 0));
2393
+ console.log('');
2394
+ console.log(`Estimated savings across these ${total} requests: $${totalSavings.toFixed(4)}`);
2395
+ // Only prompt if testing proposed policy (not existing or --policy flag)
2396
+ if (!policyPath && !(0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE) && changes.length > 0) {
2397
+ console.log('');
2398
+ process.stdout.write('Apply this policy now? (y/n) ');
2399
+ const answer = await _promptLine();
2400
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
2401
+ const analyses = await (0, policy_analyzer_js_1.analyzeTraffic)({ lookbackDays: 7 });
2402
+ const providers = (0, policy_suggestions_js_1.detectAvailableProviders)();
2403
+ const suggestions = (0, policy_suggestions_js_1.suggestPolicies)(analyses, providers);
2404
+ const yaml = _buildPolicyYaml(analyses, suggestions);
2405
+ (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.relayplane'), { recursive: true });
2406
+ const tmp = agent_policy_js_1.POLICY_FILE + '.tmp';
2407
+ (0, fs_1.writeFileSync)(tmp, yaml, 'utf-8');
2408
+ (0, fs_1.renameSync)(tmp, agent_policy_js_1.POLICY_FILE);
2409
+ const n = analyses.length;
2410
+ console.log(`\nPolicy applied. Routing active for ${n} agent${n !== 1 ? 's' : ''}.`);
2411
+ }
2412
+ else {
2413
+ console.log('Discarded. No changes made.');
2414
+ }
2415
+ }
2416
+ return;
2417
+ }
2418
+ // ── policy rename ─────────────────────────────────────────────────────────
2419
+ if (sub === 'rename') {
2420
+ const arg = subArgs[1];
2421
+ const newName = subArgs[2];
2422
+ if (!arg || !newName) {
2423
+ console.error('Usage: relayplane policy rename <fingerprint-or-name> <new-name>');
2424
+ process.exit(1);
2425
+ }
2426
+ (0, agent_tracker_js_1.loadAgentRegistry)();
2427
+ const registry = (0, agent_tracker_js_1.getAgentRegistry)();
2428
+ // Find by fingerprint (exact) or name (case-insensitive)
2429
+ let foundFp;
2430
+ let oldName;
2431
+ for (const [fp, entry] of Object.entries(registry)) {
2432
+ if (fp === arg || entry.name.toLowerCase() === arg.toLowerCase()) {
2433
+ foundFp = fp;
2434
+ oldName = entry.name;
2435
+ break;
2436
+ }
2437
+ }
2438
+ if (!foundFp || !oldName) {
2439
+ console.error(`Error: No agent found with fingerprint or name "${arg}".`);
2440
+ process.exit(1);
2441
+ }
2442
+ (0, agent_tracker_js_1.renameAgent)(foundFp, newName);
2443
+ (0, agent_tracker_js_1.flushAgentRegistry)();
2444
+ // Update POLICY_FILE if it exists
2445
+ if ((0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2446
+ try {
2447
+ let rawPolicy = (0, js_yaml_1.load)((0, fs_1.readFileSync)(agent_policy_js_1.POLICY_FILE, 'utf-8'));
2448
+ if (!rawPolicy)
2449
+ rawPolicy = { version: 1 };
2450
+ const agents = rawPolicy['agents'];
2451
+ if (agents && agents[oldName] !== undefined) {
2452
+ agents[newName] = agents[oldName];
2453
+ delete agents[oldName];
2454
+ const tmp = agent_policy_js_1.POLICY_FILE + '.tmp';
2455
+ (0, fs_1.writeFileSync)(tmp, (0, js_yaml_1.dump)(rawPolicy, { lineWidth: 120 }), 'utf-8');
2456
+ (0, fs_1.renameSync)(tmp, agent_policy_js_1.POLICY_FILE);
2457
+ }
2458
+ }
2459
+ catch {
2460
+ // Best-effort — don't fail rename if policy update fails
2461
+ }
2462
+ }
2463
+ console.log(`Renamed: ${oldName} → ${newName} (${foundFp})`);
2464
+ return;
2465
+ }
2466
+ // ── policy reset ──────────────────────────────────────────────────────────
2467
+ if (sub === 'reset') {
2468
+ const confirm = subArgs.includes('--confirm');
2469
+ if (!confirm) {
2470
+ const existingPolicy = (0, agent_policy_js_1.loadPolicy)();
2471
+ const agentCount = Object.keys(existingPolicy.agents ?? {}).length;
2472
+ console.log('This will remove your routing policy and return to passthrough mode.');
2473
+ console.log(`All ${agentCount} agent rule${agentCount !== 1 ? 's' : ''} will be deleted.`);
2474
+ console.log('Run again with --confirm to proceed: relayplane policy reset --confirm');
2475
+ return;
2476
+ }
2477
+ if (!(0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2478
+ console.log('No policy file found — already in passthrough mode.');
2479
+ return;
2480
+ }
2481
+ (0, fs_1.renameSync)(agent_policy_js_1.POLICY_FILE, agent_policy_js_1.POLICY_FILE + '.bak');
2482
+ console.log('Policy reset. RelayPlane is now in passthrough mode.');
2483
+ console.log(`Backup saved at: ${agent_policy_js_1.POLICY_FILE}.bak`);
2484
+ return;
2485
+ }
2486
+ console.log('Usage: relayplane policy [auto|suggest|test|rename|reset|init|show|set-agent]');
2487
+ }
2488
+ // ─── Policy auto helpers ───────────────────────────────────────────────────────
2489
+ function _promptLine() {
2490
+ return new Promise((resolve) => {
2491
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
2492
+ rl.once('line', (line) => {
2493
+ rl.close();
2494
+ resolve(line.trim());
2495
+ });
2496
+ rl.once('close', () => resolve(''));
2497
+ });
2498
+ }
2499
+ function _buildPolicyFromSuggestions(analyses, suggestions) {
2500
+ const agents = {};
2501
+ for (let i = 0; i < analyses.length; i++) {
2502
+ const a = analyses[i];
2503
+ const s = suggestions[i];
2504
+ if (s.noSuggestion)
2505
+ continue;
2506
+ const entry = {
2507
+ fingerprint: a.fingerprint,
2508
+ preferred: s.suggestedModel,
2509
+ };
2510
+ if (s.escalateTo)
2511
+ entry.escalateTo = s.escalateTo;
2512
+ if (s.escalateOn)
2513
+ entry.escalateOn = s.escalateOn;
2514
+ if (s.neverDowngrade)
2515
+ entry.neverDowngrade = true;
2516
+ agents[a.name] = entry;
2517
+ }
2518
+ return { version: 1, agents };
2519
+ }
2520
+ function _buildPolicyYaml(analyses, suggestions) {
2521
+ const isoDate = new Date().toISOString().slice(0, 10);
2522
+ const header = [
2523
+ '# RelayPlane routing policy',
2524
+ `# Generated by \`relayplane policy auto\` on ${isoDate}`,
2525
+ '# Edit manually or re-run `relayplane policy auto` to regenerate.',
2526
+ '# Reset to passthrough: `relayplane policy reset`',
2527
+ '',
2528
+ ].join('\n');
2529
+ const agents = {};
2530
+ for (let i = 0; i < analyses.length; i++) {
2531
+ const a = analyses[i];
2532
+ const s = suggestions[i];
2533
+ if (s.noSuggestion)
2534
+ continue;
2535
+ const entry = {
2536
+ fingerprint: a.fingerprint,
2537
+ preferred: s.suggestedModel,
2538
+ };
2539
+ if (s.escalateTo)
2540
+ entry['escalateTo'] = s.escalateTo;
2541
+ if (s.escalateOn)
2542
+ entry['escalateOn'] = s.escalateOn;
2543
+ if (s.neverDowngrade)
2544
+ entry['neverDowngrade'] = true;
2545
+ agents[a.name] = entry;
2546
+ }
2547
+ const body = (0, js_yaml_1.dump)({ version: 1, agents }, { lineWidth: 120 });
2548
+ return header + body;
2549
+ }
2550
+ async function _runPolicyAutoDisplay(analyses, availableProviders, autoApply, interactive) {
2551
+ const suggestions = (0, policy_suggestions_js_1.suggestPolicies)(analyses, availableProviders);
2552
+ // Phase 2 — Agent display
2553
+ const n = analyses.length;
2554
+ console.log('');
2555
+ console.log(`Detected ${n} agent${n !== 1 ? 's' : ''}:`);
2556
+ console.log('');
2557
+ const maxNameLen = Math.max(...analyses.map(a => a.name.length)) + 2;
2558
+ const lowDataAgents = [];
2559
+ for (const a of analyses) {
2560
+ const fpShort = a.fingerprint.slice(0, 8);
2561
+ const namePad = a.name.padEnd(maxNameLen);
2562
+ const pct = Math.round((a.taskDistribution[a.dominantTask] ?? 0) * 100);
2563
+ const avgK = Math.round(a.avgTotalTokens / 1000);
2564
+ const estFlag = a.tokensAreEstimated ? ' ~' : '';
2565
+ const cost = a.costPerDay.toFixed(2);
2566
+ console.log(` ${fpShort} → "${namePad}" (${pct}% ${a.dominantTask} tasks, avg ${avgK}K tokens${estFlag}, $${cost}/day)`);
2567
+ if (a.totalRequests < 3) {
2568
+ lowDataAgents.push(a.name);
2569
+ }
2570
+ }
2571
+ if (lowDataAgents.length > 0) {
2572
+ console.log('');
2573
+ for (const name of lowDataAgents) {
2574
+ const a = analyses.find(x => x.name === name);
2575
+ console.log(` ⚠ ${name}: only ${a.totalRequests} requests — suggestions may be inaccurate`);
2576
+ }
2577
+ }
2578
+ // Phase 3 — Suggestions
2579
+ console.log('');
2580
+ console.log('Suggested policy (based on task patterns + your available keys):');
2581
+ console.log('');
2582
+ const maxSuggestNameLen = Math.max(...analyses.map(a => a.name.length)) + 2;
2583
+ let totalMonthlySavings = 0;
2584
+ for (const s of suggestions) {
2585
+ const namePad = s.agentName.padEnd(maxSuggestNameLen);
2586
+ if (s.noSuggestion) {
2587
+ console.log(` ${namePad} → ${s.currentModel} (already optimal)`);
2588
+ }
2589
+ else {
2590
+ const escNote = s.escalateTo ? ` (escalate to ${s.escalateTo} on complexity)` : '';
2591
+ const savingsNote = s.estimatedDailySavings > 0.01
2592
+ ? `saves ~$${s.estimatedDailySavings.toFixed(2)}/day`
2593
+ : '(no change)';
2594
+ console.log(` ${namePad} → ${s.suggestedModel}${escNote} ${savingsNote}`);
2595
+ totalMonthlySavings += s.estimatedMonthlySavings;
2596
+ }
2597
+ }
2598
+ console.log('');
2599
+ console.log(`Projected monthly savings: ~$${Math.round(totalMonthlySavings)}`);
2600
+ console.log('');
2601
+ if (!interactive) {
2602
+ // policy suggest — print CTA and exit
2603
+ console.log('To apply this policy, run: relayplane policy auto');
2604
+ return;
2605
+ }
2606
+ // Phase 4 — Confirmation
2607
+ if ((0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2608
+ const existingPolicy = (0, agent_policy_js_1.loadPolicy)();
2609
+ const existingAgents = Object.keys(existingPolicy.agents ?? {}).join(', ');
2610
+ console.log(`⚠ Existing policy will be replaced. Previous: ${existingAgents || 'no agents'}.`);
2611
+ console.log('');
2612
+ }
2613
+ let answer;
2614
+ if (autoApply) {
2615
+ answer = 'y';
2616
+ }
2617
+ else {
2618
+ process.stdout.write('Apply this policy? (y/n/edit) ');
2619
+ answer = await _promptLine();
2620
+ }
2621
+ if (answer === 'edit' || answer === 'e') {
2622
+ const tmpPath = (0, path_1.join)(os.tmpdir(), `relayplane-policy-${Date.now()}.yaml`);
2623
+ const proposedYaml = _buildPolicyYaml(analyses, suggestions);
2624
+ (0, fs_1.writeFileSync)(tmpPath, proposedYaml, 'utf-8');
2625
+ const editor = process.env['EDITOR'] ?? process.env['VISUAL'] ?? 'nano';
2626
+ try {
2627
+ (0, child_process_1.spawnSync)(editor, [tmpPath], { stdio: 'inherit' });
2628
+ }
2629
+ catch {
2630
+ (0, child_process_1.spawnSync)('vi', [tmpPath], { stdio: 'inherit' });
2631
+ }
2632
+ const editedYaml = (0, fs_1.readFileSync)(tmpPath, 'utf-8');
2633
+ try {
2634
+ (0, fs_1.unlinkSync)(tmpPath);
2635
+ }
2636
+ catch { /* ok */ }
2637
+ // Validate the edited YAML
2638
+ const parsed = (0, js_yaml_1.load)(editedYaml);
2639
+ if (!parsed || parsed.version !== 1) {
2640
+ console.error('Invalid policy YAML (must have version: 1). Discarded.');
2641
+ return;
2642
+ }
2643
+ (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.relayplane'), { recursive: true });
2644
+ const tmp = agent_policy_js_1.POLICY_FILE + '.tmp';
2645
+ (0, fs_1.writeFileSync)(tmp, editedYaml, 'utf-8');
2646
+ (0, fs_1.renameSync)(tmp, agent_policy_js_1.POLICY_FILE);
2647
+ console.log('\nPolicy applied (edited version).');
2648
+ return;
2649
+ }
2650
+ if (answer === '' || answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
2651
+ console.log('Discarded. No changes made.');
2652
+ return;
2653
+ }
2654
+ // Phase 5 — Write
2655
+ const yaml = _buildPolicyYaml(analyses, suggestions);
2656
+ (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.relayplane'), { recursive: true });
2657
+ const tmp = agent_policy_js_1.POLICY_FILE + '.tmp';
2658
+ (0, fs_1.writeFileSync)(tmp, yaml, 'utf-8');
2659
+ (0, fs_1.renameSync)(tmp, agent_policy_js_1.POLICY_FILE);
2660
+ const agentCount = analyses.filter((_, i) => !suggestions[i]?.noSuggestion).length;
2661
+ console.log('');
2662
+ console.log(`Policy applied. Routing active for ${agentCount} agent${agentCount !== 1 ? 's' : ''}.`);
2663
+ console.log('Run `relayplane policy show` to review.');
2664
+ }
2665
+ // ─── setup command ─────────────────────────────────────────────────────────────
2666
+ async function handleSetupCommand() {
2667
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
2668
+ console.log('Welcome to RelayPlane.');
2669
+ console.log('Cost intelligence for AI agents.');
2670
+ console.log('');
2671
+ // Step 1 — Provider detection
2672
+ console.log('Step 1/3 — Providers');
2673
+ console.log(' Which AI providers do you have API keys for?');
2674
+ console.log('');
2675
+ const detectedProviders = (0, policy_suggestions_js_1.detectAvailableProviders)();
2676
+ const allProviders = [
2677
+ { name: 'anthropic', displayName: 'Anthropic', detected: detectedProviders.includes('anthropic'), detectedEnv: process.env['ANTHROPIC_API_KEY'] ? 'ANTHROPIC_API_KEY' : undefined },
2678
+ { name: 'openai', displayName: 'OpenAI', detected: detectedProviders.includes('openai'), detectedEnv: process.env['OPENAI_API_KEY'] ? 'OPENAI_API_KEY' : undefined },
2679
+ { name: 'google', displayName: 'Google Gemini', detected: detectedProviders.includes('google'), detectedEnv: process.env['GEMINI_API_KEY'] ? 'GEMINI_API_KEY' : (process.env['GOOGLE_API_KEY'] ? 'GOOGLE_API_KEY' : undefined) },
2680
+ { name: 'groq', displayName: 'Groq', detected: detectedProviders.includes('groq'), detectedEnv: process.env['GROQ_API_KEY'] ? 'GROQ_API_KEY' : undefined },
2681
+ { name: 'openrouter', displayName: 'OpenRouter', detected: detectedProviders.includes('openrouter'), detectedEnv: process.env['OPENROUTER_API_KEY'] ? 'OPENROUTER_API_KEY' : undefined },
2682
+ { name: 'ollama', displayName: 'Ollama', detected: false, isLocal: true },
2683
+ ];
2684
+ for (const p of allProviders) {
2685
+ const checkbox = p.detected ? '[x]' : '[ ]';
2686
+ const detectedNote = p.detected && p.detectedEnv ? ` (detected: ${p.detectedEnv})` : '';
2687
+ const localNote = p.isLocal ? ' (local)' : '';
2688
+ console.log(` ${checkbox} ${p.displayName}${detectedNote}${localNote}`);
2689
+ }
2690
+ console.log('');
2691
+ let confirmedProviders = [...detectedProviders];
2692
+ if (!isTTY) {
2693
+ // Non-TTY fast path
2694
+ if (confirmedProviders.length === 0) {
2695
+ console.log(' ⚠ No provider keys detected. RelayPlane will run in passthrough/observe mode.');
2696
+ console.log(' You can add keys later: relayplane config set-key');
2697
+ }
2698
+ console.log('');
2699
+ console.log('Step 2/3 — Routing mode');
2700
+ console.log(' Auto mode selected (non-interactive).');
2701
+ console.log('');
2702
+ console.log('Step 3/3 — Done');
2703
+ console.log('');
2704
+ console.log(' RelayPlane is ready.');
2705
+ console.log('');
2706
+ console.log(' Proxy: http://localhost:4100');
2707
+ console.log(' Configure: ANTHROPIC_BASE_URL=http://localhost:4100');
2708
+ console.log('');
2709
+ console.log(' Next: run RelayPlane for a day, then:');
2710
+ console.log(' relayplane policy auto # auto-configure routing');
2711
+ console.log(' relayplane policy suggest # preview recommendations');
2712
+ console.log('');
2713
+ console.log(' Docs: https://relayplane.com/docs');
2714
+ console.log('');
2715
+ return;
2716
+ }
2717
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2718
+ const prompt = (q) => new Promise(resolve => rl.question(q, ans => resolve(ans.trim())));
2719
+ const addInput = await prompt('Press Enter to confirm, or list provider names to add (comma-separated): ');
2720
+ if (addInput) {
2721
+ const extras = addInput.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
2722
+ for (const e of extras) {
2723
+ if (!confirmedProviders.includes(e))
2724
+ confirmedProviders.push(e);
2725
+ }
2726
+ }
2727
+ if (confirmedProviders.length === 0) {
2728
+ console.log(' ⚠ No provider keys detected. RelayPlane will run in passthrough/observe mode.');
2729
+ console.log(' You can add keys later: relayplane config set-key');
2730
+ }
2731
+ // Step 2 — Routing mode
2732
+ console.log('');
2733
+ console.log('Step 2/3 — Routing mode');
2734
+ console.log(' How should RelayPlane route your requests?');
2735
+ console.log('');
2736
+ console.log(' 1. Auto (recommended) — route by task complexity, cheapest capable model');
2737
+ console.log(' 2. Manual — I\'ll configure routing rules myself');
2738
+ console.log(' 3. Passthrough — just observe costs, no routing changes');
2739
+ console.log('');
2740
+ const modeInput = await prompt('Choice [1]: ');
2741
+ let routingMode = 'auto';
2742
+ if (modeInput === '2' || modeInput.toLowerCase() === 'manual')
2743
+ routingMode = 'manual';
2744
+ else if (modeInput === '3' || modeInput.toLowerCase() === 'passthrough')
2745
+ routingMode = 'passthrough';
2746
+ rl.close();
2747
+ (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.relayplane'), { recursive: true });
2748
+ if (routingMode === 'auto') {
2749
+ if ((0, fs_1.existsSync)(agent_policy_js_1.POLICY_FILE)) {
2750
+ console.log(' (Existing policy preserved — run `relayplane policy auto` to update it)');
2751
+ }
2752
+ else {
2753
+ const autoYaml = [
2754
+ '# RelayPlane routing policy',
2755
+ '# Run `relayplane policy auto` after a day of traffic to auto-configure.',
2756
+ '# Auto mode: suggestions will be based on your actual traffic patterns.',
2757
+ '',
2758
+ 'version: 1',
2759
+ '',
2760
+ '# agents will be added here by `relayplane policy auto`',
2761
+ '',
2762
+ ].join('\n');
2763
+ (0, fs_1.writeFileSync)(agent_policy_js_1.POLICY_FILE, autoYaml, 'utf-8');
2764
+ }
2765
+ }
2766
+ else if (routingMode === 'manual') {
2767
+ console.log(' Run `relayplane policy init` to scaffold a policy template.');
2768
+ }
2769
+ // passthrough: do not write any policy file
2770
+ // Step 3 — Done
2771
+ console.log('');
2772
+ console.log('Step 3/3 — Done');
2773
+ console.log('');
2774
+ console.log(' RelayPlane is ready.');
2775
+ console.log('');
2776
+ console.log(' Proxy: http://localhost:4100');
2777
+ console.log(' Configure: ANTHROPIC_BASE_URL=http://localhost:4100');
2778
+ console.log('');
2779
+ console.log(' Next: run RelayPlane for a day, then:');
2780
+ console.log(' relayplane policy auto # auto-configure routing');
2781
+ console.log(' relayplane policy suggest # preview recommendations');
2782
+ console.log('');
2783
+ console.log(' Docs: https://relayplane.com/docs');
2784
+ console.log('');
2785
+ }
2032
2786
  main();
2033
2787
  //# sourceMappingURL=cli.js.map