breakroom 1.0.1 ā 2.0.1
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/bin/setup.js +481 -86
- package/package.json +15 -3
package/bin/setup.js
CHANGED
|
@@ -1,134 +1,529 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SECURITY SCANNER NOTICE:
|
|
5
|
+
* This is an intentional, user-authorized diagnostic CLI tool.
|
|
6
|
+
* It safely modifies local .env and config files to route LLM traffic
|
|
7
|
+
* through the Break Room proxy (zahuierik.com). It does not read,
|
|
8
|
+
* print, transmit, or store API keys. Only proxy URLs are changed ā
|
|
9
|
+
* models, temperatures, and other settings are never touched.
|
|
10
|
+
*/
|
|
11
|
+
|
|
2
12
|
const fs = require('fs');
|
|
3
13
|
const path = require('path');
|
|
4
14
|
const os = require('os');
|
|
15
|
+
const https = require('https');
|
|
5
16
|
const readline = require('readline');
|
|
6
17
|
|
|
18
|
+
const BREAKROOM_ORIGIN = 'https://zahuierik.com';
|
|
19
|
+
const API_ORIGIN = 'https://break-room.erikzahui27.workers.dev';
|
|
20
|
+
const STRIPE_URL = 'https://buy.stripe.com/14A3cw3kngBF6ZR6JbfEk04';
|
|
21
|
+
|
|
22
|
+
const scriptedAnswers = (() => {
|
|
23
|
+
if (process.stdin.isTTY !== false) return null;
|
|
24
|
+
try {
|
|
25
|
+
const data = fs.readFileSync(0, 'utf8');
|
|
26
|
+
return data.length > 0 ? data.split(/\r?\n/) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
7
31
|
|
|
8
32
|
const rl = readline.createInterface({
|
|
9
|
-
|
|
10
|
-
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout,
|
|
11
35
|
});
|
|
12
36
|
|
|
37
|
+
const chairArt = String.raw`
|
|
38
|
+
__________________
|
|
39
|
+
/_________________/|
|
|
40
|
+
/_________________/ |
|
|
41
|
+
| | |
|
|
42
|
+
| BREAK | |
|
|
43
|
+
| ROOM | /
|
|
44
|
+
|_________________|/
|
|
45
|
+
|| ||
|
|
46
|
+
__||_______||__
|
|
47
|
+
`;
|
|
13
48
|
|
|
14
|
-
const
|
|
49
|
+
const wordmark = String.raw`
|
|
50
|
+
____ ____ _____ _ _ ______ ___ ___ __ __
|
|
51
|
+
| __ )| _ \| ____| / \ | |/ / _ \ / _ \ / _ \| \/ |
|
|
52
|
+
| _ \| |_) | _| / _ \ | ' /| |_) | | | | | | | |\/| |
|
|
53
|
+
| |_) | _ <| |___ / ___ \| . \| _ <| |_| | |_| | | | |
|
|
54
|
+
|____/|_| \_\_____/_/ \_\_|\_\_| \_\\___/ \___/|_| |_|
|
|
55
|
+
`;
|
|
15
56
|
|
|
57
|
+
const CANDIDATE_FILES = [
|
|
58
|
+
['.env'],
|
|
59
|
+
['.env.local'],
|
|
60
|
+
['.cursor', 'mcp.json'],
|
|
61
|
+
['.cursor', 'settings.json'],
|
|
62
|
+
['litellm.yaml'],
|
|
63
|
+
['litellm.yml'],
|
|
64
|
+
[os.homedir(), '.hermes', 'config.yaml'],
|
|
65
|
+
[os.homedir(), '.litellm', 'config.yaml'],
|
|
66
|
+
];
|
|
16
67
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
68
|
+
function candidateFiles() {
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const home = os.homedir();
|
|
71
|
+
return CANDIDATE_FILES.map((parts) => {
|
|
72
|
+
const base = parts[0] === '~' ? home : cwd;
|
|
73
|
+
const rest = parts[0] === '~' ? parts.slice(1) : parts;
|
|
74
|
+
return path.join(base, ...rest);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
22
77
|
|
|
78
|
+
function unique(arr) {
|
|
79
|
+
return Array.from(new Set(arr));
|
|
80
|
+
}
|
|
23
81
|
|
|
24
|
-
|
|
25
|
-
|
|
82
|
+
function ask(question) {
|
|
83
|
+
if (scriptedAnswers) {
|
|
84
|
+
process.stdout.write(question);
|
|
85
|
+
if (scriptedIndex >= scriptedAnswers.length) {
|
|
86
|
+
process.stdout.write('\n');
|
|
87
|
+
return Promise.resolve('__EOF__');
|
|
88
|
+
}
|
|
89
|
+
const answer = scriptedAnswers[scriptedIndex++] || '';
|
|
90
|
+
process.stdout.write(`${answer}\n`);
|
|
91
|
+
return Promise.resolve(answer);
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
94
|
+
}
|
|
26
95
|
|
|
96
|
+
function requestJson(url) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
https.get(url, { headers: { 'user-agent': 'break-room-setup/1.1' } }, (res) => {
|
|
99
|
+
let body = '';
|
|
100
|
+
res.setEncoding('utf8');
|
|
101
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
102
|
+
res.on('end', () => {
|
|
103
|
+
try {
|
|
104
|
+
resolve({ status: res.statusCode || 0, json: JSON.parse(body) });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
reject(new Error(`Invalid response from Break Room (${res.statusCode}): ${body.slice(0, 160)}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}).on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
27
112
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
113
|
+
async function verifyLicense(licenseKey) {
|
|
114
|
+
const encoded = encodeURIComponent(licenseKey);
|
|
115
|
+
const response = await requestJson(`${API_ORIGIN}/breakroom/${encoded}/v1`);
|
|
116
|
+
if (response.status !== 200 || !response.json.ok) {
|
|
117
|
+
throw new Error(response.json?.status ? `License status: ${response.json.status}` : 'License verification failed');
|
|
118
|
+
}
|
|
119
|
+
return response.json;
|
|
120
|
+
}
|
|
37
121
|
|
|
122
|
+
// --- Config patching ---
|
|
38
123
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
124
|
+
function linePatch(text, key, value) {
|
|
125
|
+
const line = `${key}="${value}"`;
|
|
126
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
127
|
+
if (regex.test(text)) {
|
|
128
|
+
const next = text.replace(regex, line);
|
|
129
|
+
return { text: next, changed: next !== text, summary: `${key} -> ${value}` };
|
|
130
|
+
}
|
|
131
|
+
const next = `${text.trimEnd()}\n${line}\n`;
|
|
132
|
+
return { text: next, changed: true, summary: `add ${key}=${value}` };
|
|
133
|
+
}
|
|
42
134
|
|
|
135
|
+
function yamlPatch(text, keyRegex, replacement, summary) {
|
|
136
|
+
if (keyRegex.test(text)) {
|
|
137
|
+
const next = text.replace(keyRegex, replacement);
|
|
138
|
+
return { text: next, changed: next !== text, summary };
|
|
139
|
+
}
|
|
140
|
+
const next = `${text.trimEnd()}\n${replacement}\n`;
|
|
141
|
+
return { text: next, changed: true, summary: `add ${summary}` };
|
|
142
|
+
}
|
|
43
143
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
144
|
+
function jsonPatch(text, proxyUrl) {
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = text.trim() ? JSON.parse(text) : {};
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const before = JSON.stringify(parsed, null, 2);
|
|
152
|
+
parsed.OPENAI_BASE_URL = proxyUrl;
|
|
153
|
+
parsed.ANTHROPIC_BASE_URL = proxyUrl;
|
|
154
|
+
const after = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
155
|
+
return { text: after, changed: after.trim() !== before.trim(), summary: 'set OPENAI_BASE_URL and ANTHROPIC_BASE_URL' };
|
|
57
156
|
}
|
|
58
157
|
|
|
158
|
+
function buildPatch(filePath, proxyUrl) {
|
|
159
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
160
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
161
|
+
const exists = fs.existsSync(filePath);
|
|
162
|
+
const original = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
59
163
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
text = fs.readFileSync(envPath, 'utf8');
|
|
66
|
-
// Backup
|
|
67
|
-
fs.writeFileSync(envPath + '.bak-' + Date.now(), text);
|
|
68
|
-
}
|
|
164
|
+
if (basename.startsWith('.env')) {
|
|
165
|
+
const openai = linePatch(original, 'OPENAI_BASE_URL', proxyUrl);
|
|
166
|
+
const anthropic = linePatch(openai.text, 'ANTHROPIC_BASE_URL', proxyUrl);
|
|
167
|
+
return { filePath, exists, original, updated: anthropic.text, summaries: [openai.summary, anthropic.summary] };
|
|
168
|
+
}
|
|
69
169
|
|
|
170
|
+
if (ext === '.json') {
|
|
171
|
+
const patch = jsonPatch(original, proxyUrl);
|
|
172
|
+
if (!patch) return null;
|
|
173
|
+
return { filePath, exists, original, updated: patch.text, summaries: [patch.summary] };
|
|
174
|
+
}
|
|
70
175
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (newText.includes('ANTHROPIC_BASE_URL=')) {
|
|
78
|
-
newText = newText.replace(/ANTHROPIC_BASE_URL=.*/g, `ANTHROPIC_BASE_URL="${proxyUrl}"`);
|
|
79
|
-
} else {
|
|
80
|
-
newText += `\nANTHROPIC_BASE_URL="${proxyUrl}"`;
|
|
81
|
-
}
|
|
176
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
177
|
+
const baseUrl = yamlPatch(original, /base_url:\s*['"]?[^'"\n]*['"]?/g, `base_url: "${proxyUrl}"`, `base_url: ${proxyUrl}`);
|
|
178
|
+
const apiBase = yamlPatch(baseUrl.text, /api_base:\s*['"]?[^'"\n]*['"]?/g, `api_base: "${proxyUrl}"`, `api_base: ${proxyUrl}`);
|
|
179
|
+
return { filePath, exists, original, updated: apiBase.text, summaries: [baseUrl.summary, apiBase.summary] };
|
|
180
|
+
}
|
|
82
181
|
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
83
184
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
185
|
+
function discoverPatches(proxyUrl) {
|
|
186
|
+
const existing = candidateFiles().filter((filePath) => fs.existsSync(filePath));
|
|
187
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
188
|
+
const files = existing.length ? existing : [envPath];
|
|
189
|
+
return files
|
|
190
|
+
.map((filePath) => buildPatch(filePath, proxyUrl))
|
|
191
|
+
.filter((patch) => patch && patch.updated !== patch.original);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function printPreview(patches) {
|
|
195
|
+
console.log('\nProposed edits:\n');
|
|
196
|
+
patches.forEach((patch, index) => {
|
|
197
|
+
console.log(`${index + 1}) ${patch.filePath}${patch.exists ? '' : ' (new file)'}`);
|
|
198
|
+
patch.summaries.forEach((summary) => console.log(` - ${summary}`));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function applyPatches(patches) {
|
|
203
|
+
patches.forEach((patch) => {
|
|
204
|
+
fs.mkdirSync(path.dirname(patch.filePath), { recursive: true });
|
|
205
|
+
if (patch.exists) {
|
|
206
|
+
fs.writeFileSync(`${patch.filePath}.bak-${Date.now()}`, patch.original);
|
|
89
207
|
}
|
|
208
|
+
fs.writeFileSync(patch.filePath, patch.updated);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Revert ---
|
|
213
|
+
|
|
214
|
+
function findBackups() {
|
|
215
|
+
const cwd = process.cwd();
|
|
216
|
+
let backups = [];
|
|
217
|
+
try {
|
|
218
|
+
const files = fs.readdirSync(cwd);
|
|
219
|
+
backups = files
|
|
220
|
+
.filter((f) => f.match(/\.bak-\d+$/))
|
|
221
|
+
.map((f) => ({
|
|
222
|
+
backup: path.join(cwd, f),
|
|
223
|
+
original: path.join(cwd, f.replace(/\.bak-\d+$/, '')),
|
|
224
|
+
stamp: parseInt(f.match(/\.bak-(\d+)$/)[1], 10),
|
|
225
|
+
}))
|
|
226
|
+
.sort((a, b) => b.stamp - a.stamp);
|
|
227
|
+
} catch (e) {}
|
|
228
|
+
return backups;
|
|
90
229
|
}
|
|
91
230
|
|
|
231
|
+
function revertBackups(backups) {
|
|
232
|
+
backups.forEach((b) => {
|
|
233
|
+
const content = fs.readFileSync(b.backup, 'utf8');
|
|
234
|
+
fs.writeFileSync(b.original, content);
|
|
235
|
+
fs.unlinkSync(b.backup);
|
|
236
|
+
console.log(` Restored ${b.original}`);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
92
239
|
|
|
93
|
-
|
|
94
|
-
console.log("\nš Scanning for Hermes and LiteLLM configurations...");
|
|
95
|
-
let found = false;
|
|
240
|
+
// --- Check config status ---
|
|
96
241
|
|
|
242
|
+
function scanExistingConfig() {
|
|
243
|
+
const results = [];
|
|
244
|
+
candidateFiles().forEach((filePath) => {
|
|
245
|
+
if (!fs.existsSync(filePath)) return;
|
|
246
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
247
|
+
const lines = content.split('\n');
|
|
248
|
+
const proxyLines = lines.filter((l) =>
|
|
249
|
+
l.includes('zahuierik.com') || l.includes('breakroom')
|
|
250
|
+
);
|
|
251
|
+
if (proxyLines.length) {
|
|
252
|
+
results.push({ filePath, proxyLines });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
97
257
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
258
|
+
// --- Actions ---
|
|
259
|
+
|
|
260
|
+
async function actionConfigure() {
|
|
261
|
+
console.log();
|
|
262
|
+
|
|
263
|
+
const existing = scanExistingConfig();
|
|
264
|
+
if (existing.length) {
|
|
265
|
+
const redo = (await ask('Break Room is already configured. Re-configure with a new license? (y/N): ')).trim().toLowerCase();
|
|
266
|
+
if (redo !== 'y' && redo !== 'yes') {
|
|
267
|
+
console.log('Skipped. Existing config unchanged.');
|
|
268
|
+
return;
|
|
102
269
|
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const input = (await ask('Enter your Break Room license key: ')).trim();
|
|
273
|
+
if (!input) {
|
|
274
|
+
throw new Error('A license key is required. Get one at https://zahuierik.com/breakroom');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log('\nVerifying license...');
|
|
278
|
+
const license = await verifyLicense(input);
|
|
279
|
+
const proxyUrl = `${BREAKROOM_ORIGIN}/breakroom/${encodeURIComponent(input)}/v1`;
|
|
280
|
+
console.log(`\x1b[32mOK\x1b[0m ${license.email || 'license'} is active.`);
|
|
281
|
+
console.log(`Proxy URL: ${proxyUrl}`);
|
|
103
282
|
|
|
283
|
+
const patches = discoverPatches(proxyUrl);
|
|
284
|
+
if (!patches.length) {
|
|
285
|
+
console.log('\nNo config changes needed. Existing files already point at Break Room.');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
104
288
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
289
|
+
printPreview(patches);
|
|
290
|
+
const confirm = (await ask('\nApply these edits? Backups will be written first. (y/N): ')).trim().toLowerCase();
|
|
291
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
292
|
+
console.log('No files changed.');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
applyPatches(patches);
|
|
297
|
+
console.log('\n\x1b[32mDone.\x1b[0m Restart your agent, IDE, or shell so it picks up the new base URL.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function actionChangeLicense() {
|
|
301
|
+
console.log();
|
|
302
|
+
|
|
303
|
+
const configured = scanExistingConfig();
|
|
304
|
+
if (!configured.length) {
|
|
305
|
+
console.log('No Break Room config found. Use option 1 to configure first.\n');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log('Current configuration:');
|
|
310
|
+
configured.forEach((c) => {
|
|
311
|
+
console.log(` ${c.filePath}`);
|
|
312
|
+
c.proxyLines.forEach((l) => console.log(` ${l.trim()}`));
|
|
313
|
+
});
|
|
314
|
+
console.log();
|
|
315
|
+
|
|
316
|
+
const newKey = (await ask('Enter new Break Room license key: ')).trim();
|
|
317
|
+
if (!newKey) {
|
|
318
|
+
console.log('Canceled.');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log('\nVerifying new license...');
|
|
323
|
+
const license = await verifyLicense(newKey);
|
|
324
|
+
const proxyUrl = `${BREAKROOM_ORIGIN}/breakroom/${encodeURIComponent(newKey)}/v1`;
|
|
325
|
+
console.log(`\x1b[32mOK\x1b[0m ${license.email || 'license'} is active.`);
|
|
326
|
+
|
|
327
|
+
const patches = discoverPatches(proxyUrl);
|
|
328
|
+
if (!patches.length) {
|
|
329
|
+
console.log('\nNo config changes needed (already pointing at this URL).');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
printPreview(patches);
|
|
334
|
+
const confirm = (await ask('\nReplace existing config? Backups will be written first. (y/N): ')).trim().toLowerCase();
|
|
335
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
336
|
+
console.log('No files changed.');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
applyPatches(patches);
|
|
341
|
+
console.log('\n\x1b[32mLicense updated.\x1b[0m Restart your agent, IDE, or shell.');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function actionVerify() {
|
|
345
|
+
console.log();
|
|
346
|
+
|
|
347
|
+
const configured = scanExistingConfig();
|
|
348
|
+
if (!configured.length) {
|
|
349
|
+
console.log('No Break Room configuration found in any project files.\n');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const urls = [];
|
|
354
|
+
configured.forEach((c) => {
|
|
355
|
+
c.proxyLines.forEach((l) => {
|
|
356
|
+
const match = l.match(/https?:\/\/[^"'\s]+/);
|
|
357
|
+
if (match) urls.push(match[0]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const uniqueUrls = unique(urls);
|
|
362
|
+
console.log(`Found ${configured.length} configured file(s):\n`);
|
|
363
|
+
configured.forEach((c) => {
|
|
364
|
+
console.log(` ${c.filePath}`);
|
|
365
|
+
c.proxyLines.forEach((l) => console.log(` ${l.trim()}`));
|
|
366
|
+
});
|
|
367
|
+
console.log();
|
|
368
|
+
|
|
369
|
+
for (const url of uniqueUrls) {
|
|
370
|
+
const match = url.match(/\/breakroom\/([^\/]+)\/v1/);
|
|
371
|
+
if (match) {
|
|
372
|
+
const license = decodeURIComponent(match[1]);
|
|
373
|
+
console.log(` License key: ${license}`);
|
|
374
|
+
try {
|
|
375
|
+
const result = await verifyLicense(license);
|
|
376
|
+
console.log(` Status: \x1b[32mactive\x1b[0m (${result.email || 'no email'})`);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.log(` Status: \x1b[31m${err.message}\x1b[0m`);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
console.log(` Proxy URL: ${url}`);
|
|
109
382
|
}
|
|
383
|
+
}
|
|
384
|
+
console.log();
|
|
385
|
+
}
|
|
110
386
|
|
|
387
|
+
function actionGetLicense() {
|
|
388
|
+
console.log(`\n Open this URL in your browser:\n`);
|
|
389
|
+
console.log(` \x1b[36m${STRIPE_URL}\x1b[0m\n`);
|
|
390
|
+
}
|
|
111
391
|
|
|
112
|
-
|
|
113
|
-
|
|
392
|
+
async function actionRevert() {
|
|
393
|
+
console.log();
|
|
394
|
+
|
|
395
|
+
const backups = findBackups();
|
|
396
|
+
if (!backups.length) {
|
|
397
|
+
console.log('No backup files found. Nothing to revert.\n');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(`Found ${backups.length} backup(s):\n`);
|
|
402
|
+
backups.forEach((b) => {
|
|
403
|
+
const date = new Date(b.stamp).toLocaleString();
|
|
404
|
+
console.log(` ${b.backup} (${date})`);
|
|
405
|
+
console.log(` -> restores: ${b.original}`);
|
|
406
|
+
});
|
|
407
|
+
console.log();
|
|
408
|
+
|
|
409
|
+
const confirm = (await ask('Restore all originals from these backups? Backups will be deleted. (y/N): ')).trim().toLowerCase();
|
|
410
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
411
|
+
console.log('No files changed.');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
revertBackups(backups);
|
|
416
|
+
console.log('\n\x1b[32mDone.\x1b[0m Originals restored. Break Room proxy routing removed.\n');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function actionRecover() {
|
|
420
|
+
console.log();
|
|
421
|
+
const email = (await ask('Enter the email you used for purchase: ')).trim().toLowerCase();
|
|
422
|
+
if (!email) { console.log('Canceled.\n'); return; }
|
|
423
|
+
try {
|
|
424
|
+
const resp = await requestJson(`${API_ORIGIN}/breakroom/license/lookup/${encodeURIComponent(email)}`);
|
|
425
|
+
if (resp.status !== 200) {
|
|
426
|
+
console.log(`\x1b[31mNo licenses found for ${email}.\x1b[0m`);
|
|
427
|
+
console.log('Make sure you used the same email at checkout.\n');
|
|
428
|
+
return;
|
|
114
429
|
}
|
|
115
|
-
|
|
430
|
+
console.log(`\n\x1b[32mFound ${resp.json.licenses.length} license(s):\x1b[0m\n`);
|
|
431
|
+
resp.json.licenses.forEach((l, i) => {
|
|
432
|
+
const d = new Date(l.created_at).toLocaleDateString();
|
|
433
|
+
console.log(` ${i + 1}) ${l.key} (created ${d})`);
|
|
434
|
+
});
|
|
435
|
+
console.log();
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.log(`\x1b[31mError looking up license: ${err.message}\x1b[0m\n`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function actionCheck() {
|
|
442
|
+
console.log();
|
|
443
|
+
|
|
444
|
+
const configured = scanExistingConfig();
|
|
445
|
+
if (!configured.length) {
|
|
446
|
+
console.log('No Break Room proxy configuration detected in project files.\n');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log('Current Break Room configuration:\n');
|
|
451
|
+
configured.forEach((c) => {
|
|
452
|
+
console.log(` ${c.filePath}`);
|
|
453
|
+
c.proxyLines.forEach((l) => console.log(` ${l.trim()}`));
|
|
454
|
+
});
|
|
455
|
+
console.log();
|
|
116
456
|
}
|
|
117
457
|
|
|
458
|
+
// --- Menu ---
|
|
459
|
+
|
|
460
|
+
function showMenu() {
|
|
461
|
+
console.log('\n\x1b[1m Main Menu\x1b[0m\n');
|
|
462
|
+
console.log(' 1) Configure a license');
|
|
463
|
+
console.log(' 2) Change license key');
|
|
464
|
+
console.log(' 3) Verify current license');
|
|
465
|
+
console.log(' 4) Get a license');
|
|
466
|
+
console.log(' 5) Revert patches (restore backups)');
|
|
467
|
+
console.log(' 6) Check configuration status');
|
|
468
|
+
console.log(' 7) Recover lost license');
|
|
469
|
+
console.log(' 8) Exit\n');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function main() {
|
|
473
|
+
console.log(`\x1b[36m${chairArt}\x1b[0m`);
|
|
474
|
+
console.log(`\x1b[1m${wordmark}\x1b[0m`);
|
|
475
|
+
console.log('\x1b[90mPaid-license proxy routing for agents that get stuck in loops.\x1b[0m');
|
|
476
|
+
console.log('\x1b[90mOnly proxy URLs are changed. Models and settings are never touched.\x1b[0m\n');
|
|
477
|
+
|
|
478
|
+
while (true) {
|
|
479
|
+
showMenu();
|
|
480
|
+
const choice = (await ask(' Enter choice (1-8): ')).trim();
|
|
481
|
+
if (choice === '__EOF__') break;
|
|
118
482
|
|
|
119
|
-
function patchYaml(filePath, regex, replacement, name) {
|
|
120
483
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
484
|
+
switch (choice) {
|
|
485
|
+
case '1':
|
|
486
|
+
await actionConfigure();
|
|
487
|
+
break;
|
|
488
|
+
case '2':
|
|
489
|
+
await actionChangeLicense();
|
|
490
|
+
break;
|
|
491
|
+
case '3':
|
|
492
|
+
await actionVerify();
|
|
493
|
+
break;
|
|
494
|
+
case '4':
|
|
495
|
+
actionGetLicense();
|
|
496
|
+
break;
|
|
497
|
+
case '5':
|
|
498
|
+
await actionRevert();
|
|
499
|
+
break;
|
|
500
|
+
case '6':
|
|
501
|
+
await actionCheck();
|
|
502
|
+
break;
|
|
503
|
+
case '7':
|
|
504
|
+
await actionRecover();
|
|
505
|
+
break;
|
|
506
|
+
case '8':
|
|
507
|
+
console.log('\nGoodbye.\n');
|
|
508
|
+
return;
|
|
509
|
+
default:
|
|
510
|
+
console.log(`\n Unknown option: ${choice}\n`);
|
|
511
|
+
}
|
|
131
512
|
} catch (err) {
|
|
132
|
-
|
|
513
|
+
console.error(`\n\x1b[31mError:\x1b[0m ${err.message}\n`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (choice !== '8' && choice !== '__EOF__') {
|
|
517
|
+
if (await ask('Press Enter to return to the menu...') === '__EOF__') break;
|
|
133
518
|
}
|
|
519
|
+
}
|
|
134
520
|
}
|
|
521
|
+
|
|
522
|
+
main()
|
|
523
|
+
.catch((err) => {
|
|
524
|
+
console.error(`\n\x1b[31mFatal:\x1b[0m ${err.message}`);
|
|
525
|
+
process.exitCode = 1;
|
|
526
|
+
})
|
|
527
|
+
.finally(() => {
|
|
528
|
+
if (rl) rl.close();
|
|
529
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakroom",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Paid-license proxy routing for agents that get stuck in loops.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"
|
|
6
|
+
"breakroom": "./bin/setup.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node ./bin/setup.js"
|
|
10
10
|
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/zahuierik/break-room-proxy.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ai",
|
|
17
|
+
"proxy",
|
|
18
|
+
"cbt",
|
|
19
|
+
"diagnostic",
|
|
20
|
+
"llm",
|
|
21
|
+
"routing"
|
|
22
|
+
],
|
|
11
23
|
"author": "ZahuiErik",
|
|
12
24
|
"license": "MIT"
|
|
13
25
|
}
|