@relayplane/proxy 1.9.18 → 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/agent-policy.d.ts +54 -0
- package/dist/agent-policy.d.ts.map +1 -0
- package/dist/agent-policy.js +183 -0
- package/dist/agent-policy.js.map +1 -0
- package/dist/budget.d.ts +81 -0
- package/dist/budget.d.ts.map +1 -1
- package/dist/budget.js +224 -1
- package/dist/budget.js.map +1 -1
- package/dist/cli.js +773 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/policy-analyzer.d.ts +41 -0
- package/dist/policy-analyzer.d.ts.map +1 -0
- package/dist/policy-analyzer.js +268 -0
- package/dist/policy-analyzer.js.map +1 -0
- package/dist/policy-suggestions.d.ts +31 -0
- package/dist/policy-suggestions.d.ts.map +1 -0
- package/dist/policy-suggestions.js +173 -0
- package/dist/policy-suggestions.js.map +1 -0
- package/dist/routing-log.d.ts +47 -0
- package/dist/routing-log.d.ts.map +1 -0
- package/dist/routing-log.js +141 -0
- package/dist/routing-log.js.map +1 -0
- package/dist/standalone-proxy.d.ts.map +1 -1
- package/dist/standalone-proxy.js +139 -0
- package/dist/standalone-proxy.js.map +1 -1
- package/dist/telemetryPinger.d.ts +2 -0
- package/dist/telemetryPinger.d.ts.map +1 -0
- package/dist/telemetryPinger.js +80 -0
- package/dist/telemetryPinger.js.map +1 -0
- package/package.json +4 -2
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,
|
|
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';
|
|
@@ -1830,6 +1849,7 @@ async function handleInitWizard() {
|
|
|
1830
1849
|
const budget = rawConfig['budget'] ?? {};
|
|
1831
1850
|
budget['enabled'] = budgetEnabled;
|
|
1832
1851
|
budget['dailyUsd'] = dailyCapUsd;
|
|
1852
|
+
budget['dailyCapUSD'] = dailyCapUsd;
|
|
1833
1853
|
budget['onBreach'] = onBreach;
|
|
1834
1854
|
rawConfig['budget'] = budget;
|
|
1835
1855
|
// Atomic write
|
|
@@ -1866,9 +1886,24 @@ function handleBudgetCommand(args) {
|
|
|
1866
1886
|
catch { /* ok */ }
|
|
1867
1887
|
const status = budget.getStatus();
|
|
1868
1888
|
const config = budget.getConfig();
|
|
1889
|
+
const tracker = (0, budget_js_1.getBudgetTracker)();
|
|
1890
|
+
try {
|
|
1891
|
+
tracker.init();
|
|
1892
|
+
}
|
|
1893
|
+
catch { /* ok */ }
|
|
1894
|
+
const cap = tracker.getCap();
|
|
1895
|
+
const todaySpend = tracker.getDailySpend();
|
|
1869
1896
|
console.log('');
|
|
1870
1897
|
console.log('💰 Budget Status');
|
|
1871
1898
|
console.log(` Enabled: ${config.enabled ? '✅' : '❌'}`);
|
|
1899
|
+
if (cap !== null) {
|
|
1900
|
+
const pct = cap > 0 ? (todaySpend / cap) * 100 : 0;
|
|
1901
|
+
const bar = pct >= 100 ? '🚫 BLOCKED' : pct >= 80 ? '⚠️ warning' : '✅ ok';
|
|
1902
|
+
console.log(` Daily cap: $${todaySpend.toFixed(4)} / $${cap.toFixed(2)} (${pct.toFixed(1)}%) ${bar}`);
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
console.log(` Daily cap: $${todaySpend.toFixed(4)} / unlimited`);
|
|
1906
|
+
}
|
|
1872
1907
|
console.log(` Daily: $${status.dailySpend.toFixed(4)} / $${status.dailyLimit} (${status.dailyPercent.toFixed(1)}%)`);
|
|
1873
1908
|
console.log(` Hourly: $${status.hourlySpend.toFixed(4)} / $${status.hourlyLimit} (${status.hourlyPercent.toFixed(1)}%)`);
|
|
1874
1909
|
console.log(` Per-request: max $${config.perRequestUsd}`);
|
|
@@ -1877,6 +1912,7 @@ function handleBudgetCommand(args) {
|
|
|
1877
1912
|
console.log(` ⚠️ BREACHED: ${status.breachType}`);
|
|
1878
1913
|
}
|
|
1879
1914
|
console.log('');
|
|
1915
|
+
tracker.close();
|
|
1880
1916
|
budget.close();
|
|
1881
1917
|
return;
|
|
1882
1918
|
}
|
|
@@ -1904,8 +1940,41 @@ function handleBudgetCommand(args) {
|
|
|
1904
1940
|
budget.close();
|
|
1905
1941
|
return;
|
|
1906
1942
|
}
|
|
1907
|
-
|
|
1943
|
+
if (sub === 'history') {
|
|
1944
|
+
const daysArg = parseInt(args[1] ?? '7', 10);
|
|
1945
|
+
const days = isNaN(daysArg) ? 7 : daysArg;
|
|
1946
|
+
const tracker = (0, budget_js_1.getBudgetTracker)();
|
|
1947
|
+
try {
|
|
1948
|
+
tracker.init();
|
|
1949
|
+
}
|
|
1950
|
+
catch { /* ok */ }
|
|
1951
|
+
const history = tracker.getHistory(days);
|
|
1952
|
+
const cap = tracker.getCap();
|
|
1953
|
+
console.log('');
|
|
1954
|
+
console.log(`📅 Budget History (last ${days} days)`);
|
|
1955
|
+
if (cap !== null) {
|
|
1956
|
+
console.log(` Daily cap: $${cap.toFixed(2)}`);
|
|
1957
|
+
}
|
|
1958
|
+
if (history.length === 0) {
|
|
1959
|
+
console.log(' No spend recorded.');
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
for (const day of history) {
|
|
1963
|
+
const pct = cap !== null && cap > 0 ? ` (${((day.totalSpend / cap) * 100).toFixed(1)}%)` : '';
|
|
1964
|
+
console.log(` ${day.date} $${day.totalSpend.toFixed(4)}${pct}`);
|
|
1965
|
+
for (const [model, amt] of Object.entries(day.byModel)) {
|
|
1966
|
+
console.log(` ↳ ${model}: $${amt.toFixed(4)}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
console.log('');
|
|
1971
|
+
tracker.close();
|
|
1972
|
+
budget.close();
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
console.log('Usage: relayplane budget [status|set|reset|history]');
|
|
1908
1976
|
console.log(' set --daily <usd> --hourly <usd> --per-request <usd>');
|
|
1977
|
+
console.log(' history [days] Show daily spend history (default: 7 days)');
|
|
1909
1978
|
budget.close();
|
|
1910
1979
|
}
|
|
1911
1980
|
function handleCacheCommand(args) {
|
|
@@ -1979,5 +2048,705 @@ function handleCacheCommand(args) {
|
|
|
1979
2048
|
}
|
|
1980
2049
|
console.log('Usage: relayplane cache [status|clear|stats|on|off]');
|
|
1981
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
|
+
}
|
|
1982
2751
|
main();
|
|
1983
2752
|
//# sourceMappingURL=cli.js.map
|