@relayplane/proxy 1.9.20 → 1.9.21

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