breakroom 2.0.1 → 2.1.0
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 +181 -28
- package/package.json +1 -1
package/bin/setup.js
CHANGED
|
@@ -34,45 +34,45 @@ const rl = readline.createInterface({
|
|
|
34
34
|
output: process.stdout,
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const wordmark = String.raw`
|
|
50
|
-
____ ____ _____ _ _ ______ ___ ___ __ __
|
|
51
|
-
| __ )| _ \| ____| / \ | |/ / _ \ / _ \ / _ \| \/ |
|
|
52
|
-
| _ \| |_) | _| / _ \ | ' /| |_) | | | | | | | |\/| |
|
|
53
|
-
| |_) | _ <| |___ / ___ \| . \| _ <| |_| | |_| | | | |
|
|
54
|
-
|____/|_| \_\_____/_/ \_\_|\_\_| \_\\___/ \___/|_| |_|
|
|
55
|
-
`;
|
|
37
|
+
const banner = () => {
|
|
38
|
+
console.clear();
|
|
39
|
+
console.log(`
|
|
40
|
+
\x1b[36m\u26a1\x1b[0m \x1b[1m\x1b[37mBREAK ROOM\x1b[0m
|
|
41
|
+
\x1b[90mClinical cognitive routing for autonomous agents.\x1b[0m
|
|
42
|
+
|
|
43
|
+
\x1b[90m\u250c\u2500\x1b[0m \x1b[37mScope:\x1b[0m Paid-license proxy routing for agent loops
|
|
44
|
+
\x1b[90m\u251c\u2500\x1b[0m \x1b[37mSafety:\x1b[0m Only proxy URLs are modified. Code remains local.
|
|
45
|
+
\x1b[90m\u2514\u2500\x1b[0m \x1b[37mStatus:\x1b[0m \x1b[32mAwaiting License Key...\x1b[0m
|
|
46
|
+
`);
|
|
47
|
+
};
|
|
56
48
|
|
|
57
49
|
const CANDIDATE_FILES = [
|
|
58
50
|
['.env'],
|
|
59
51
|
['.env.local'],
|
|
60
52
|
['.cursor', 'mcp.json'],
|
|
61
53
|
['.cursor', 'settings.json'],
|
|
54
|
+
['.vscode', 'settings.json'],
|
|
55
|
+
['.windsurf', 'settings.json'],
|
|
62
56
|
['litellm.yaml'],
|
|
63
57
|
['litellm.yml'],
|
|
64
58
|
[os.homedir(), '.hermes', 'config.yaml'],
|
|
65
59
|
[os.homedir(), '.litellm', 'config.yaml'],
|
|
60
|
+
[os.homedir(), '.config', 'breakroom', 'config.yaml'],
|
|
66
61
|
];
|
|
67
62
|
|
|
68
63
|
function candidateFiles() {
|
|
69
64
|
const cwd = process.cwd();
|
|
70
65
|
const home = os.homedir();
|
|
71
|
-
|
|
66
|
+
const files = CANDIDATE_FILES.map((parts) => {
|
|
72
67
|
const base = parts[0] === '~' ? home : cwd;
|
|
73
68
|
const rest = parts[0] === '~' ? parts.slice(1) : parts;
|
|
74
69
|
return path.join(base, ...rest);
|
|
75
70
|
});
|
|
71
|
+
// Add Docker files if they exist
|
|
72
|
+
['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.dockerignore'].forEach((f) => {
|
|
73
|
+
files.push(path.join(cwd, f));
|
|
74
|
+
});
|
|
75
|
+
return files;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function unique(arr) {
|
|
@@ -110,6 +110,33 @@ function requestJson(url) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function postJson(url, data) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const body = JSON.stringify(data);
|
|
116
|
+
const u = new URL(url);
|
|
117
|
+
const opts = {
|
|
118
|
+
hostname: u.hostname, port: u.port, path: u.pathname,
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
|
121
|
+
};
|
|
122
|
+
const req = https.request(opts, (res) => {
|
|
123
|
+
let respBody = '';
|
|
124
|
+
res.setEncoding('utf8');
|
|
125
|
+
res.on('data', (chunk) => { respBody += chunk; });
|
|
126
|
+
res.on('end', () => {
|
|
127
|
+
try {
|
|
128
|
+
resolve({ status: res.statusCode || 0, json: JSON.parse(respBody) });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
reject(new Error(`Invalid response from Break Room (${res.statusCode}): ${respBody.slice(0, 160)}`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
req.on('error', reject);
|
|
135
|
+
req.write(body);
|
|
136
|
+
req.end();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
113
140
|
async function verifyLicense(licenseKey) {
|
|
114
141
|
const encoded = encodeURIComponent(licenseKey);
|
|
115
142
|
const response = await requestJson(`${API_ORIGIN}/breakroom/${encoded}/v1`);
|
|
@@ -421,7 +448,7 @@ async function actionRecover() {
|
|
|
421
448
|
const email = (await ask('Enter the email you used for purchase: ')).trim().toLowerCase();
|
|
422
449
|
if (!email) { console.log('Canceled.\n'); return; }
|
|
423
450
|
try {
|
|
424
|
-
const resp = await
|
|
451
|
+
const resp = await postJson(`${API_ORIGIN}/breakroom/license/lookup`, { email });
|
|
425
452
|
if (resp.status !== 200) {
|
|
426
453
|
console.log(`\x1b[31mNo licenses found for ${email}.\x1b[0m`);
|
|
427
454
|
console.log('Make sure you used the same email at checkout.\n');
|
|
@@ -455,6 +482,127 @@ async function actionCheck() {
|
|
|
455
482
|
console.log();
|
|
456
483
|
}
|
|
457
484
|
|
|
485
|
+
async function actionScan() {
|
|
486
|
+
console.log();
|
|
487
|
+
|
|
488
|
+
const cwd = process.cwd();
|
|
489
|
+
const files = candidateFiles();
|
|
490
|
+
|
|
491
|
+
console.log('Scanning for IDE, config, and Docker files...\n');
|
|
492
|
+
|
|
493
|
+
const found = files.filter((f) => fs.existsSync(f));
|
|
494
|
+
if (!found.length) {
|
|
495
|
+
console.log('No recognized config files found.');
|
|
496
|
+
} else {
|
|
497
|
+
console.log(`Found ${found.length} file(s):\n`);
|
|
498
|
+
found.forEach((f) => {
|
|
499
|
+
const rel = f.startsWith(cwd) ? '.' + f.slice(cwd.length) : f.replace(os.homedir(), '~');
|
|
500
|
+
const stat = fs.statSync(f);
|
|
501
|
+
const size = stat.size;
|
|
502
|
+
const modified = new Date(stat.mtime).toLocaleDateString();
|
|
503
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
504
|
+
const hasProxy = content.includes('zahuierik.com') || content.includes('breakroom');
|
|
505
|
+
const marker = hasProxy ? ' \x1b[32m[break room]\x1b[0m' : '';
|
|
506
|
+
console.log(` ${rel} (${size}B, ${modified})${marker}`);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Check for Docker
|
|
511
|
+
console.log();
|
|
512
|
+
const dockerFiles = ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.dockerignore']
|
|
513
|
+
.map((f) => path.join(cwd, f))
|
|
514
|
+
.filter((f) => fs.existsSync(f));
|
|
515
|
+
if (dockerFiles.length) {
|
|
516
|
+
console.log(`Docker assets: ${dockerFiles.map((f) => path.basename(f)).join(', ')}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check for running containers (if docker available)
|
|
520
|
+
try {
|
|
521
|
+
const { execSync } = require('child_process');
|
|
522
|
+
const out = execSync('docker ps --format "{{.Names}}" 2>/dev/null', { timeout: 3000, encoding: 'utf8' }).trim();
|
|
523
|
+
if (out) {
|
|
524
|
+
const containers = out.split('\n').filter(Boolean);
|
|
525
|
+
console.log(`Running containers: ${containers.join(', ')}`);
|
|
526
|
+
}
|
|
527
|
+
} catch (e) { /* docker not available or no containers */ }
|
|
528
|
+
|
|
529
|
+
console.log();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function actionRotate() {
|
|
533
|
+
console.log();
|
|
534
|
+
|
|
535
|
+
const key = (await ask('Enter current license key to rotate: ')).trim();
|
|
536
|
+
if (!key) { console.log('Canceled.\n'); return; }
|
|
537
|
+
|
|
538
|
+
// Verify key is active
|
|
539
|
+
console.log('\nVerifying license...');
|
|
540
|
+
let licenseInfo;
|
|
541
|
+
try {
|
|
542
|
+
licenseInfo = await verifyLicense(key);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.log(`\x1b[31m${err.message}\x1b[0m\n`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
console.log(`\x1b[32mOK\x1b[0m — license for ${licenseInfo.email || 'unknown'} is active.`);
|
|
548
|
+
|
|
549
|
+
// Request rotation code
|
|
550
|
+
console.log('\nRequesting verification code...');
|
|
551
|
+
let resp;
|
|
552
|
+
try {
|
|
553
|
+
resp = await postJson(`${API_ORIGIN}/breakroom/${encodeURIComponent(key)}/request-rotate`, {});
|
|
554
|
+
if (resp.status !== 200) {
|
|
555
|
+
console.log(`\x1b[31m${resp.json?.error || 'Failed to send code'}\x1b[0m\n`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.log(`\x1b[31m${err.message}\x1b[0m\n`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
console.log('A verification code has been sent to the email on file.');
|
|
563
|
+
|
|
564
|
+
// Prompt for code
|
|
565
|
+
const code = (await ask('\nEnter the code from your email: ')).trim();
|
|
566
|
+
if (!code) { console.log('Canceled.\n'); return; }
|
|
567
|
+
|
|
568
|
+
// Confirm rotation
|
|
569
|
+
console.log('\nVerifying code and rotating license...');
|
|
570
|
+
let rotateResp;
|
|
571
|
+
try {
|
|
572
|
+
rotateResp = await postJson(`${API_ORIGIN}/breakroom/${encodeURIComponent(key)}/confirm-rotate`, { code });
|
|
573
|
+
if (rotateResp.status !== 200) {
|
|
574
|
+
console.log(`\x1b[31m${rotateResp.json?.error || 'Rotation failed'}\x1b[0m\n`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
} catch (err) {
|
|
578
|
+
console.log(`\x1b[31m${err.message}\x1b[0m\n`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const newKey = rotateResp.json.new_key;
|
|
583
|
+
console.log(`\n\x1b[32mLicense rotated successfully!\x1b[0m`);
|
|
584
|
+
console.log(`\n New license key: \x1b[36m${newKey}\x1b[0m`);
|
|
585
|
+
console.log(` Old key (\x1b[31m${key}\x1b[0m) has been invalidated.\n`);
|
|
586
|
+
|
|
587
|
+
// Offer to update config patches
|
|
588
|
+
const existing = scanExistingConfig();
|
|
589
|
+
if (existing.length) {
|
|
590
|
+
const update = (await ask('Update existing config files with the new key? (y/N): ')).trim().toLowerCase();
|
|
591
|
+
if (update === 'y' || update === 'yes') {
|
|
592
|
+
const proxyUrl = `${BREAKROOM_ORIGIN}/breakroom/${encodeURIComponent(newKey)}/v1`;
|
|
593
|
+
const patches = discoverPatches(proxyUrl);
|
|
594
|
+
if (patches.length) {
|
|
595
|
+
applyPatches(patches);
|
|
596
|
+
console.log('\x1b[32mConfig files updated.\x1b[0m');
|
|
597
|
+
} else {
|
|
598
|
+
console.log('Config files already up to date.');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log();
|
|
604
|
+
}
|
|
605
|
+
|
|
458
606
|
// --- Menu ---
|
|
459
607
|
|
|
460
608
|
function showMenu() {
|
|
@@ -466,18 +614,17 @@ function showMenu() {
|
|
|
466
614
|
console.log(' 5) Revert patches (restore backups)');
|
|
467
615
|
console.log(' 6) Check configuration status');
|
|
468
616
|
console.log(' 7) Recover lost license');
|
|
469
|
-
console.log(' 8)
|
|
617
|
+
console.log(' 8) Scan for IDE/config/Docker files');
|
|
618
|
+
console.log(' 9) Rotate license key (2FA)');
|
|
619
|
+
console.log(' 0) Exit\n');
|
|
470
620
|
}
|
|
471
621
|
|
|
472
622
|
async function main() {
|
|
473
|
-
|
|
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');
|
|
623
|
+
banner();
|
|
477
624
|
|
|
478
625
|
while (true) {
|
|
479
626
|
showMenu();
|
|
480
|
-
const choice = (await ask(' Enter choice (
|
|
627
|
+
const choice = (await ask(' Enter choice (0-9): ')).trim();
|
|
481
628
|
if (choice === '__EOF__') break;
|
|
482
629
|
|
|
483
630
|
try {
|
|
@@ -504,6 +651,12 @@ async function main() {
|
|
|
504
651
|
await actionRecover();
|
|
505
652
|
break;
|
|
506
653
|
case '8':
|
|
654
|
+
await actionScan();
|
|
655
|
+
break;
|
|
656
|
+
case '9':
|
|
657
|
+
await actionRotate();
|
|
658
|
+
break;
|
|
659
|
+
case '0':
|
|
507
660
|
console.log('\nGoodbye.\n');
|
|
508
661
|
return;
|
|
509
662
|
default:
|
|
@@ -513,7 +666,7 @@ async function main() {
|
|
|
513
666
|
console.error(`\n\x1b[31mError:\x1b[0m ${err.message}\n`);
|
|
514
667
|
}
|
|
515
668
|
|
|
516
|
-
if (choice !== '
|
|
669
|
+
if (choice !== '0' && choice !== '__EOF__') {
|
|
517
670
|
if (await ask('Press Enter to return to the menu...') === '__EOF__') break;
|
|
518
671
|
}
|
|
519
672
|
}
|