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.
Files changed (2) hide show
  1. package/bin/setup.js +181 -28
  2. 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 chairArt = String.raw`
38
- __________________
39
- /_________________/|
40
- /_________________/ |
41
- | | |
42
- | BREAK | |
43
- | ROOM | /
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
- return CANDIDATE_FILES.map((parts) => {
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 requestJson(`${API_ORIGIN}/breakroom/license/lookup/${encodeURIComponent(email)}`);
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) Exit\n');
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
- 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');
623
+ banner();
477
624
 
478
625
  while (true) {
479
626
  showMenu();
480
- const choice = (await ask(' Enter choice (1-8): ')).trim();
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 !== '8' && choice !== '__EOF__') {
669
+ if (choice !== '0' && choice !== '__EOF__') {
517
670
  if (await ask('Press Enter to return to the menu...') === '__EOF__') break;
518
671
  }
519
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakroom",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Paid-license proxy routing for agents that get stuck in loops.",
5
5
  "bin": {
6
6
  "breakroom": "./bin/setup.js"