@closedrouter/cli 1.1.7 → 1.1.9

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/index.js CHANGED
@@ -5137,6 +5137,13 @@ var require_websocket_server = __commonJS(function(exports, module) {
5137
5137
  }
5138
5138
  });
5139
5139
 
5140
+ // ../node_modules/.bun/emoji-regex@10.6.0/node_modules/emoji-regex/index.js
5141
+ var require_emoji_regex = __commonJS(function(exports, module) {
5142
+ module.exports = () => {
5143
+ return /[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E-\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])))?))?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3C-\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC2\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF]|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g;
5144
+ };
5145
+ });
5146
+
5140
5147
  // ../node_modules/.bun/commander@14.0.3/node_modules/commander/index.js
5141
5148
  var exports_commander = {};
5142
5149
  __exportCjs(exports_commander, {
@@ -5197,7 +5204,7 @@ var {
5197
5204
  } = exports_commander;
5198
5205
 
5199
5206
  // src/index.ts
5200
- import { readFileSync as readFileSync9 } from "node:fs";
5207
+ import { readFileSync as readFileSync13 } from "node:fs";
5201
5208
 
5202
5209
  // ../node_modules/.bun/@clack+prompts@0.11.0/node_modules/@clack/prompts/dist/index.mjs
5203
5210
  import { stripVTControlCharacters as S2 } from "node:util";
@@ -6261,6 +6268,10 @@ class ClosedRouterClient {
6261
6268
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6262
6269
  import { homedir } from "node:os";
6263
6270
  import { join } from "node:path";
6271
+ var MODEL_GROUPINGS = ["model-provider", "account-provider", "none"];
6272
+ function isModelGrouping(value) {
6273
+ return MODEL_GROUPINGS.some((grouping) => grouping === value);
6274
+ }
6264
6275
  var DEFAULT_SERVER_URL = "https://closedrouter.213.183.59.186.sslip.io";
6265
6276
  function getConfigDir() {
6266
6277
  return process.env.CLOSEDROUTER_CONFIG_DIR || join(homedir(), ".closedrouter");
@@ -6281,7 +6292,9 @@ function loadConfig() {
6281
6292
  sessionToken: parsed.sessionToken,
6282
6293
  apiKey: parsed.apiKey,
6283
6294
  bridgeApiKey: parsed.bridgeApiKey,
6284
- user: parsed.user
6295
+ user: parsed.user,
6296
+ modelGrouping: isModelGrouping(parsed.modelGrouping) ? parsed.modelGrouping : undefined,
6297
+ favoriteModels: Array.isArray(parsed.favoriteModels) ? [...new Set(parsed.favoriteModels.filter((id) => typeof id === "string" && id.length > 0))] : undefined
6285
6298
  };
6286
6299
  } catch {
6287
6300
  return { serverUrl: DEFAULT_SERVER_URL };
@@ -8643,18 +8656,770 @@ to scan and import local subscriptions!`, "Pool Empty");
8643
8656
 
8644
8657
  // src/commands/start.ts
8645
8658
  var import_picocolors12 = __toESM(require_picocolors(), 1);
8646
- import { spawn as spawn3 } from "node:child_process";
8647
- import { execFileSync } from "node:child_process";
8648
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
8649
- import { homedir as homedir9 } from "node:os";
8650
- import { dirname, join as join9 } from "node:path";
8659
+ import { randomUUID as randomUUID5 } from "node:crypto";
8660
+ import { spawn as spawn5 } from "node:child_process";
8661
+ import { execFileSync as execFileSync2 } from "node:child_process";
8662
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "node:fs";
8663
+ import { homedir as homedir13 } from "node:os";
8664
+ import { dirname as dirname3, join as join14 } from "node:path";
8665
+
8666
+ // src/model-picker.ts
8667
+ import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
8668
+
8669
+ // ../node_modules/.bun/ansi-regex@6.4.0/node_modules/ansi-regex/index.js
8670
+ function ansiRegex({ onlyFirst = false } = {}) {
8671
+ const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
8672
+ const osc = `(?:(?:\\u001B\\]|\\u009D)[^\\u0007\\u001B\\u009C\\u009D]*${ST})`;
8673
+ const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
8674
+ const pattern = `${osc}|${csi}`;
8675
+ return new RegExp(pattern, onlyFirst ? undefined : "g");
8676
+ }
8677
+
8678
+ // ../node_modules/.bun/strip-ansi@7.2.0/node_modules/strip-ansi/index.js
8679
+ var regex = ansiRegex();
8680
+ function stripAnsi(string) {
8681
+ if (typeof string !== "string") {
8682
+ throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
8683
+ }
8684
+ if (!string.includes("\x1B") && !string.includes("›")) {
8685
+ return string;
8686
+ }
8687
+ return string.replace(regex, "");
8688
+ }
8689
+
8690
+ // ../node_modules/.bun/get-east-asian-width@1.7.0/node_modules/get-east-asian-width/lookup-data.js
8691
+ var ambiguousMinimalCodePoint = 161;
8692
+ var ambiguousMaximumCodePoint = 1114109;
8693
+ var ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
8694
+ var fullwidthMinimalCodePoint = 12288;
8695
+ var fullwidthMaximumCodePoint = 65510;
8696
+ var fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
8697
+ var wideMinimalCodePoint = 4352;
8698
+ var wideMaximumCodePoint = 262141;
8699
+ var wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101594, 101631, 101664, 101760, 101874, 101888, 102801, 102816, 102866, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110888, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110952, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127406, 127406, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128729, 128732, 128735, 128747, 128748, 128756, 128764, 128986, 128986, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129734, 129736, 129736, 129740, 129757, 129759, 129771, 129775, 129786, 131072, 196605, 196608, 262141];
8700
+
8701
+ // ../node_modules/.bun/get-east-asian-width@1.7.0/node_modules/get-east-asian-width/utilities.js
8702
+ var isInRange = (ranges, codePoint) => {
8703
+ let low = 0;
8704
+ let high = Math.floor(ranges.length / 2) - 1;
8705
+ while (low <= high) {
8706
+ const mid = Math.floor((low + high) / 2);
8707
+ const i = mid * 2;
8708
+ if (codePoint < ranges[i]) {
8709
+ high = mid - 1;
8710
+ } else if (codePoint > ranges[i + 1]) {
8711
+ low = mid + 1;
8712
+ } else {
8713
+ return true;
8714
+ }
8715
+ }
8716
+ return false;
8717
+ };
8718
+
8719
+ // ../node_modules/.bun/get-east-asian-width@1.7.0/node_modules/get-east-asian-width/lookup.js
8720
+ var commonCjkCodePoint = 19968;
8721
+ var [wideFastPathStart, wideFastPathEnd] = /* @__PURE__ */ findWideFastPathRange(wideRanges);
8722
+ function findWideFastPathRange(ranges) {
8723
+ let fastPathStart = ranges[0];
8724
+ let fastPathEnd = ranges[1];
8725
+ for (let index = 0;index < ranges.length; index += 2) {
8726
+ const start = ranges[index];
8727
+ const end = ranges[index + 1];
8728
+ if (commonCjkCodePoint >= start && commonCjkCodePoint <= end) {
8729
+ return [start, end];
8730
+ }
8731
+ if (end - start > fastPathEnd - fastPathStart) {
8732
+ fastPathStart = start;
8733
+ fastPathEnd = end;
8734
+ }
8735
+ }
8736
+ return [fastPathStart, fastPathEnd];
8737
+ }
8738
+ var isAmbiguous = (codePoint) => {
8739
+ if (codePoint < ambiguousMinimalCodePoint || codePoint > ambiguousMaximumCodePoint) {
8740
+ return false;
8741
+ }
8742
+ return isInRange(ambiguousRanges, codePoint);
8743
+ };
8744
+ var isFullwidth = (codePoint) => {
8745
+ if (codePoint < fullwidthMinimalCodePoint || codePoint > fullwidthMaximumCodePoint) {
8746
+ return false;
8747
+ }
8748
+ return isInRange(fullwidthRanges, codePoint);
8749
+ };
8750
+ var isWide = (codePoint) => {
8751
+ if (codePoint >= wideFastPathStart && codePoint <= wideFastPathEnd) {
8752
+ return true;
8753
+ }
8754
+ if (codePoint < wideMinimalCodePoint || codePoint > wideMaximumCodePoint) {
8755
+ return false;
8756
+ }
8757
+ return isInRange(wideRanges, codePoint);
8758
+ };
8759
+
8760
+ // ../node_modules/.bun/get-east-asian-width@1.7.0/node_modules/get-east-asian-width/index.js
8761
+ function validate(codePoint) {
8762
+ if (!Number.isSafeInteger(codePoint)) {
8763
+ throw new TypeError(`Expected a code point, got \`${typeof codePoint}\`.`);
8764
+ }
8765
+ }
8766
+ function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
8767
+ validate(codePoint);
8768
+ if (isFullwidth(codePoint) || isWide(codePoint) || ambiguousAsWide && isAmbiguous(codePoint)) {
8769
+ return 2;
8770
+ }
8771
+ return 1;
8772
+ }
8773
+
8774
+ // ../node_modules/.bun/string-width@7.2.0/node_modules/string-width/index.js
8775
+ var import_emoji_regex = __toESM(require_emoji_regex(), 1);
8776
+ var segmenter = new Intl.Segmenter;
8777
+ var defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
8778
+ function stringWidth(string, options = {}) {
8779
+ if (typeof string !== "string" || string.length === 0) {
8780
+ return 0;
8781
+ }
8782
+ const {
8783
+ ambiguousIsNarrow = true,
8784
+ countAnsiEscapeCodes = false
8785
+ } = options;
8786
+ if (!countAnsiEscapeCodes) {
8787
+ string = stripAnsi(string);
8788
+ }
8789
+ if (string.length === 0) {
8790
+ return 0;
8791
+ }
8792
+ let width = 0;
8793
+ const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
8794
+ for (const { segment: character } of segmenter.segment(string)) {
8795
+ const codePoint = character.codePointAt(0);
8796
+ if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) {
8797
+ continue;
8798
+ }
8799
+ if (codePoint >= 8203 && codePoint <= 8207 || codePoint === 65279) {
8800
+ continue;
8801
+ }
8802
+ if (codePoint >= 768 && codePoint <= 879 || codePoint >= 6832 && codePoint <= 6911 || codePoint >= 7616 && codePoint <= 7679 || codePoint >= 8400 && codePoint <= 8447 || codePoint >= 65056 && codePoint <= 65071) {
8803
+ continue;
8804
+ }
8805
+ if (codePoint >= 55296 && codePoint <= 57343) {
8806
+ continue;
8807
+ }
8808
+ if (codePoint >= 65024 && codePoint <= 65039) {
8809
+ continue;
8810
+ }
8811
+ if (defaultIgnorableCodePointRegex.test(character)) {
8812
+ continue;
8813
+ }
8814
+ if (import_emoji_regex.default().test(character)) {
8815
+ width += 2;
8816
+ continue;
8817
+ }
8818
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
8819
+ }
8820
+ return width;
8821
+ }
8822
+
8823
+ // src/terminal-ui.ts
8824
+ var PALETTE = {
8825
+ text: { rgb: [252, 252, 254], indexed: 255, ansi: 97 },
8826
+ muted: { rgb: [160, 161, 163], indexed: 247, ansi: 37 },
8827
+ border: { rgb: [83, 98, 107], indexed: 241, ansi: 90 },
8828
+ accent: { rgb: [212, 254, 81], indexed: 191, ansi: 92 },
8829
+ gold: { rgb: [255, 213, 128], indexed: 222, ansi: 93 },
8830
+ teal: { rgb: [108, 216, 213], indexed: 80, ansi: 96 },
8831
+ blue: { rgb: [120, 185, 255], indexed: 111, ansi: 94 },
8832
+ warm: { rgb: [244, 179, 147], indexed: 216, ansi: 93 },
8833
+ violet: { rgb: [200, 164, 244], indexed: 183, ansi: 95 },
8834
+ key: { rgb: [252, 252, 254], indexed: 255, ansi: 97 }
8835
+ };
8836
+ var CANVAS = [4, 8, 10];
8837
+ var SELECTION = [31, 44, 21];
8838
+ var KEY_SURFACE = [38, 52, 59];
8839
+ var segmenter2 = new Intl.Segmenter(undefined, { granularity: "grapheme" });
8840
+ function terminalText(text) {
8841
+ return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, "");
8842
+ }
8843
+ function span(text, tone = "text", bold = false) {
8844
+ return { text: terminalText(text), tone, bold };
8845
+ }
8846
+ function append(line, text, source) {
8847
+ const last = line.at(-1);
8848
+ if (last?.tone === source.tone && last.bold === source.bold)
8849
+ last.text += text;
8850
+ else
8851
+ line.push({ ...source, text });
8852
+ }
8853
+ function lineWidth(line) {
8854
+ return line.reduce((width, part) => width + stringWidth(part.text), 0);
8855
+ }
8856
+ function fitLine(line, width, ellipsis = true) {
8857
+ if (width < 1)
8858
+ return [];
8859
+ const result = [];
8860
+ const clipped = lineWidth(line) > width;
8861
+ const limit = clipped && ellipsis ? width - 1 : width;
8862
+ let used = 0;
8863
+ outer:
8864
+ for (const part of line) {
8865
+ for (const { segment } of segmenter2.segment(part.text)) {
8866
+ const size = stringWidth(segment);
8867
+ if (used + size > limit)
8868
+ break outer;
8869
+ append(result, segment, part);
8870
+ used += size;
8871
+ }
8872
+ }
8873
+ if (clipped && ellipsis) {
8874
+ result.push(span("…", "muted"));
8875
+ used++;
8876
+ }
8877
+ if (used < width)
8878
+ result.push(span(" ".repeat(width - used)));
8879
+ return result;
8880
+ }
8881
+ function wrapLine(line, width, continuation = 0) {
8882
+ width = Math.max(1, width);
8883
+ const lines = [];
8884
+ let current = [];
8885
+ let used = 0;
8886
+ for (const part of line) {
8887
+ for (const { segment } of segmenter2.segment(part.text)) {
8888
+ const size = stringWidth(segment);
8889
+ if (used + size > width && used > 0) {
8890
+ lines.push(current);
8891
+ const indent = Math.min(continuation, Math.max(0, width - size));
8892
+ current = indent ? [span(" ".repeat(indent))] : [];
8893
+ used = indent;
8894
+ }
8895
+ const visible = size > width ? "?" : segment;
8896
+ append(current, visible, part);
8897
+ used += size > width ? 1 : size;
8898
+ }
8899
+ }
8900
+ lines.push(current);
8901
+ return lines;
8902
+ }
8903
+ function terminalColorMode(env = process.env) {
8904
+ if (env.NO_COLOR !== undefined || env.FORCE_COLOR === "0" || env.TERM === "dumb")
8905
+ return "none";
8906
+ if (env.FORCE_COLOR === "3")
8907
+ return "truecolor";
8908
+ if (env.FORCE_COLOR === "2")
8909
+ return "256";
8910
+ if (env.FORCE_COLOR !== undefined)
8911
+ return "ansi";
8912
+ if (!process.stdout.isTTY)
8913
+ return "none";
8914
+ const depth = process.stdout.getColorDepth?.(env) || 4;
8915
+ return depth >= 24 ? "truecolor" : depth >= 8 ? "256" : depth >= 4 ? "ansi" : "none";
8916
+ }
8917
+
8918
+ class TerminalTheme {
8919
+ mode;
8920
+ constructor(mode = terminalColorMode()) {
8921
+ this.mode = mode;
8922
+ }
8923
+ color(tone, selected) {
8924
+ const swatch = PALETTE[tone];
8925
+ const rgb = selected && tone === "muted" ? [202, 213, 191] : swatch.rgb;
8926
+ if (this.mode === "truecolor")
8927
+ return `38;2;${rgb.join(";")}`;
8928
+ if (this.mode === "256")
8929
+ return `38;5;${swatch.indexed}`;
8930
+ return String(swatch.ansi);
8931
+ }
8932
+ paint(line, width, selected = false) {
8933
+ return fitLine(line, width).map((part) => {
8934
+ if (this.mode === "none")
8935
+ return part.text;
8936
+ const rgb = selected ? SELECTION : part.tone === "key" ? KEY_SURFACE : CANVAS;
8937
+ const bg = this.mode === "truecolor" ? `48;2;${rgb.join(";")}` : this.mode === "256" ? `48;5;${selected ? 22 : part.tone === "key" ? 236 : 232}` : String(selected || part.tone === "key" ? 100 : 40);
8938
+ return `\x1B[${this.color(part.tone, selected)};${bg}${part.bold ? ";1" : ""}m${part.text}\x1B[0m`;
8939
+ }).join("");
8940
+ }
8941
+ }
8942
+ function providerTone(provider) {
8943
+ const name = provider.toLowerCase();
8944
+ if (/anthropic|claude/.test(name))
8945
+ return "warm";
8946
+ if (/openai|codex/.test(name))
8947
+ return "teal";
8948
+ if (/google|gemini|antigravity/.test(name))
8949
+ return "blue";
8950
+ if (/cursor/.test(name))
8951
+ return "violet";
8952
+ return "text";
8953
+ }
8954
+
8955
+ // src/model-picker.ts
8956
+ var MODEL_GROUPING_LABELS = {
8957
+ "model-provider": "Model provider",
8958
+ "account-provider": "Account provider",
8959
+ none: "No grouping"
8960
+ };
8961
+ var PROVIDER_LABELS = {
8962
+ anthropic: "Anthropic",
8963
+ openai: "OpenAI",
8964
+ google: "Google",
8965
+ xai: "xAI",
8966
+ "x-ai": "xAI",
8967
+ cursor: "Cursor",
8968
+ custom: "Other models",
8969
+ antigravity_oauth: "Antigravity",
8970
+ claude_oauth: "Claude Code",
8971
+ codex_oauth: "Codex",
8972
+ cursor_oauth: "Cursor",
8973
+ cursor_sdk: "Cursor SDK",
8974
+ grok_oauth: "Grok",
8975
+ openai_api: "OpenAI API",
8976
+ anthropic_api: "Anthropic API",
8977
+ claude_api: "Claude API",
8978
+ google_api: "Google API",
8979
+ gemini_api: "Gemini API",
8980
+ xai_api: "xAI API"
8981
+ };
8982
+ function providerLabel(provider) {
8983
+ return PROVIDER_LABELS[provider] || provider;
8984
+ }
8985
+ function gatewayModelId(model) {
8986
+ return model.id || model.name || "";
8987
+ }
8988
+ function preparePickerModels(models) {
8989
+ const unique = new Map;
8990
+ for (const model of models) {
8991
+ const id = gatewayModelId(model);
8992
+ if (!id)
8993
+ continue;
8994
+ const existing = unique.get(id);
8995
+ unique.set(id, {
8996
+ id,
8997
+ name: model.name || id,
8998
+ owner: model.owned_by || existing?.owner || "Unknown model provider",
8999
+ providers: [...new Set([...existing?.providers || [], ...model.provider_types || []])].sort()
9000
+ });
9001
+ }
9002
+ return [...unique.values()].sort((a, b) => a.id.localeCompare(b.id));
9003
+ }
9004
+ function modelProviderLabel(model) {
9005
+ const providers = model.providers.map(providerLabel);
9006
+ return terminalText(`${model.id} [${providers.length ? providers.join(", ") : "provider info unavailable"}]`);
9007
+ }
9008
+
9009
+ class ModelPickerState {
9010
+ models;
9011
+ query = "";
9012
+ grouping;
9013
+ favorites;
9014
+ entries = [];
9015
+ cursor = 0;
9016
+ constructor(models, grouping = "none", favorites = []) {
9017
+ this.models = models;
9018
+ this.grouping = grouping;
9019
+ this.favorites = new Set(favorites);
9020
+ this.refresh();
9021
+ }
9022
+ get selected() {
9023
+ return this.entries[this.cursor];
9024
+ }
9025
+ refresh() {
9026
+ const selectedKey = this.selected?.key;
9027
+ const terms = this.query.toLowerCase().trim().split(/\s+/).filter(Boolean);
9028
+ const groups = new Map;
9029
+ for (const model of this.models) {
9030
+ const searchable = [
9031
+ model.id,
9032
+ model.name,
9033
+ model.owner,
9034
+ providerLabel(model.owner),
9035
+ ...model.providers,
9036
+ ...model.providers.map(providerLabel)
9037
+ ].join(" ").toLowerCase();
9038
+ if (!terms.every((term) => searchable.includes(term)))
9039
+ continue;
9040
+ const groupNames = this.grouping === "model-provider" ? [providerLabel(model.owner)] : this.grouping === "account-provider" ? model.providers.map(providerLabel) : [""];
9041
+ for (const group of new Set(groupNames.length ? groupNames : ["Unknown account provider"])) {
9042
+ const members = groups.get(group) || [];
9043
+ members.push(model);
9044
+ groups.set(group, members);
9045
+ }
9046
+ }
9047
+ this.entries = [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).flatMap(([group, models]) => models.sort((a, b) => Number(this.favorites.has(b.id)) - Number(this.favorites.has(a.id)) || a.id.localeCompare(b.id)).map((model) => ({ key: `${group}\x00${model.id}`, group, model })));
9048
+ this.cursor = Math.max(0, this.entries.findIndex((entry) => entry.key === selectedKey));
9049
+ }
9050
+ handleKey(text, key, pageSize = 8) {
9051
+ if (key.ctrl && (key.name === "c" || key.name === "d"))
9052
+ return { type: "cancel" };
9053
+ if (key.name === "escape") {
9054
+ if (!this.query)
9055
+ return { type: "cancel" };
9056
+ this.query = "";
9057
+ } else if (key.name === "return" || key.name === "enter") {
9058
+ return this.selected ? { type: "select", model: this.selected.model.id } : undefined;
9059
+ } else if (["up", "down", "pageup", "pagedown", "home", "end"].includes(key.name || "")) {
9060
+ const count = this.entries.length;
9061
+ if (!count)
9062
+ return;
9063
+ if (key.name === "home")
9064
+ this.cursor = 0;
9065
+ else if (key.name === "end")
9066
+ this.cursor = count - 1;
9067
+ else if (key.name === "pageup")
9068
+ this.cursor = Math.max(0, this.cursor - pageSize);
9069
+ else if (key.name === "pagedown")
9070
+ this.cursor = Math.min(count - 1, this.cursor + pageSize);
9071
+ else
9072
+ this.cursor = (this.cursor + (key.name === "up" ? -1 : 1) + count) % count;
9073
+ return;
9074
+ } else if (key.name === "tab") {
9075
+ const selectedId = this.selected?.model.id;
9076
+ this.grouping = MODEL_GROUPINGS[(MODEL_GROUPINGS.indexOf(this.grouping) + 1) % MODEL_GROUPINGS.length];
9077
+ this.refresh();
9078
+ this.cursor = Math.max(0, this.entries.findIndex((entry) => entry.model.id === selectedId));
9079
+ return { type: "grouping" };
9080
+ } else if (text === "*" && !key.ctrl && !key.meta) {
9081
+ if (!this.selected)
9082
+ return;
9083
+ const id = this.selected.model.id;
9084
+ if (this.favorites.has(id))
9085
+ this.favorites.delete(id);
9086
+ else
9087
+ this.favorites.add(id);
9088
+ this.refresh();
9089
+ return { type: "favorites" };
9090
+ } else if (key.name === "backspace") {
9091
+ this.query = Array.from(this.query).slice(0, -1).join("");
9092
+ } else if (key.ctrl && key.name === "u") {
9093
+ this.query = "";
9094
+ } else if (text && !key.ctrl && !key.meta && !/[\x00-\x1f\x7f-\x9f]/.test(text)) {
9095
+ this.query += text;
9096
+ } else {
9097
+ return;
9098
+ }
9099
+ this.refresh();
9100
+ }
9101
+ }
9102
+ function matchingText(text, tone, query, bold = false) {
9103
+ const terms = query.trim().split(/\s+/).filter(Boolean).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
9104
+ if (!terms.length)
9105
+ return [span(text, tone, bold)];
9106
+ const pattern = new RegExp(terms.join("|"), "gi");
9107
+ const result = [];
9108
+ let offset = 0;
9109
+ for (const match of text.matchAll(pattern)) {
9110
+ const index = match.index;
9111
+ if (index > offset)
9112
+ result.push(span(text.slice(offset, index), tone, bold));
9113
+ result.push(span(match[0], "accent", true));
9114
+ offset = index + match[0].length;
9115
+ }
9116
+ if (offset < text.length)
9117
+ result.push(span(text.slice(offset), tone, bold));
9118
+ return result;
9119
+ }
9120
+ function modelRows(state, width) {
9121
+ const result = [];
9122
+ let lastGroup;
9123
+ const groupCounts = new Map;
9124
+ for (const entry of state.entries)
9125
+ groupCounts.set(entry.group, (groupCounts.get(entry.group) || 0) + 1);
9126
+ for (const [index, entry] of state.entries.entries()) {
9127
+ if (entry.group && entry.group !== lastGroup) {
9128
+ if (result.length)
9129
+ result.push({ line: [], active: false });
9130
+ result.push({ line: [span(` ${entry.group}`, providerTone(entry.group), true), span(` ${groupCounts.get(entry.group)}`, "muted")], active: false });
9131
+ lastGroup = entry.group;
9132
+ }
9133
+ const active = index === state.cursor;
9134
+ const label = [
9135
+ span(active ? "> " : " ", "accent", true),
9136
+ span(state.favorites.has(entry.model.id) ? "★" : " ", "gold"),
9137
+ span(" "),
9138
+ ...matchingText(entry.model.id, "text", state.query, active),
9139
+ span(" [", "border")
9140
+ ];
9141
+ if (entry.model.providers.length) {
9142
+ entry.model.providers.forEach((provider, i) => {
9143
+ if (i)
9144
+ label.push(span(", ", "muted"));
9145
+ label.push(...matchingText(providerLabel(provider), providerTone(provider), state.query));
9146
+ });
9147
+ } else
9148
+ label.push(span("provider info unavailable", "muted"));
9149
+ label.push(span("]", "border"));
9150
+ result.push(...wrapLine(label, width, 4).map((line) => ({ line, active })));
9151
+ }
9152
+ return result;
9153
+ }
9154
+ function viewport(body, height) {
9155
+ const first = Math.max(0, body.findIndex((row) => row.active));
9156
+ let last = first;
9157
+ while (body[last + 1]?.active)
9158
+ last++;
9159
+ const preferred = last - first + 1 > height ? first : Math.max(last - height + 1, first - Math.floor(height / 2));
9160
+ const start = Math.max(0, Math.min(Math.max(0, body.length - height), preferred));
9161
+ return { rows: body.slice(start, start + height), start };
9162
+ }
9163
+ function balanced(left, right, width) {
9164
+ const rightSize = Math.min(lineWidth(right), Math.floor(width / 2));
9165
+ return [...fitLine(left, width - rightSize - 1), span(" "), ...fitLine(right, rightSize)];
9166
+ }
9167
+ function keyHint(key, label) {
9168
+ return [span(`[${key}]`, "key", true), span(` ${label} `, "muted")];
9169
+ }
9170
+ function hintRows(hints, width) {
9171
+ const rows = [];
9172
+ let current = [];
9173
+ for (const hint of hints) {
9174
+ if (current.length && lineWidth(current) + lineWidth(hint) > width) {
9175
+ rows.push(current);
9176
+ current = [];
9177
+ }
9178
+ if (lineWidth(hint) > width) {
9179
+ rows.push(...wrapLine(hint, width));
9180
+ } else
9181
+ current.push(...hint);
9182
+ }
9183
+ if (current.length)
9184
+ rows.push(current);
9185
+ return rows;
9186
+ }
9187
+ function panelRule(width, label = "", bottom = false) {
9188
+ const title = label ? fitLine([span(` ${label} `, "muted")], Math.min(width - 4, lineWidth([span(` ${label} `)]))) : [];
9189
+ return [
9190
+ span(bottom ? "╰" : "╭", "border"),
9191
+ span("─", "border"),
9192
+ ...title,
9193
+ span("─".repeat(Math.max(0, width - lineWidth(title) - 3)), "border"),
9194
+ span(bottom ? "╯" : "╮", "border")
9195
+ ];
9196
+ }
9197
+ function detailRows(state, width) {
9198
+ const model = state.selected?.model;
9199
+ if (!model)
9200
+ return [[span("No model selected", "muted")], [], [span("Clear the search to browse", "muted")]];
9201
+ const rows = [
9202
+ ...wrapLine([span(model.id, "text", true)], width),
9203
+ [],
9204
+ [span("Model provider", "muted")],
9205
+ [span(providerLabel(model.owner), providerTone(model.owner), true)],
9206
+ [],
9207
+ [span("Account providers", "muted")]
9208
+ ];
9209
+ if (model.providers.length) {
9210
+ for (const provider of model.providers)
9211
+ rows.push(...wrapLine([span("• ", "border"), span(providerLabel(provider), providerTone(provider))], width, 2));
9212
+ } else
9213
+ rows.push(...wrapLine([span("Provider info unavailable", "muted")], width));
9214
+ rows.push([], [span(state.favorites.has(model.id) ? "★ Saved to favorites" : "☆ Add to favorites with *", "gold")], [], [span("Automatic pool routing", "muted")]);
9215
+ return rows;
9216
+ }
9217
+ function renderModelPicker(state, columns = 80, rows = 24, options = {}) {
9218
+ const theme = options.theme || new TerminalTheme;
9219
+ const width = Math.max(1, Math.min(120, columns - 1));
9220
+ const height = Math.max(1, rows - 1);
9221
+ const count = new Set(state.entries.map((entry) => entry.model.id)).size;
9222
+ const favorites = state.models.filter((model) => state.favorites.has(model.id)).length;
9223
+ const status = [
9224
+ span(`${count} of ${state.models.length} models`, "muted"),
9225
+ span(` · ★ ${favorites}`, "gold"),
9226
+ span(state.entries.length ? ` · ${state.cursor + 1}/${state.entries.length}` : "", "muted")
9227
+ ];
9228
+ const search = [span("Search: ", "muted"), span(state.query || "Type to filter models or providers", state.query ? "text" : "muted"), span("▏", "accent")];
9229
+ const compact = width < 72 || height < 18;
9230
+ const brand = [span("CLOSED", "text", true), span("ROUTER", "accent", true)];
9231
+ const header = [];
9232
+ if (width < 24 || height < 7) {
9233
+ const minimal = modelRows(state, width);
9234
+ const top = height > 2 ? [theme.paint(search, width)] : [];
9235
+ const visible = viewport(minimal, Math.max(1, height - top.length - (height > 3 ? 1 : 0)));
9236
+ const body = visible.rows.length ? visible.rows.map((row) => theme.paint(row.line, width, row.active)) : [theme.paint([span("No matching models", "warm")], width)];
9237
+ return [...top, ...body, ...height > 3 ? [theme.paint(status, width)] : []].slice(0, height).join(`
9238
+ `);
9239
+ }
9240
+ if (compact) {
9241
+ header.push(balanced(brand, [span("Model pool", "muted")], width), search, [span("Grouping: ", "muted"), span(MODEL_GROUPING_LABELS[state.grouping], "accent", true)]);
9242
+ } else {
9243
+ header.push(balanced([...brand, span(" / ", "border"), span("Model pool", "muted")], [span(options.toolLabel || "Choose a model", "text")], width), [], panelRule(width), [span("│", "border"), span(" / ", "accent"), ...fitLine(search, width - 5), span("│", "border")], panelRule(width, "", true));
9244
+ const tabs = [span("Grouping: ", "muted")];
9245
+ for (const grouping of MODEL_GROUPINGS) {
9246
+ const active = grouping === state.grouping;
9247
+ tabs.push(span(active ? `[ ${MODEL_GROUPING_LABELS[grouping]} ]` : ` ${MODEL_GROUPING_LABELS[grouping]} `, active ? "accent" : "muted", active), span(" "));
9248
+ }
9249
+ header.push(tabs, []);
9250
+ }
9251
+ const keys = [keyHint("Enter", "start"), keyHint("↑/↓", "move"), keyHint("*", "favorite"), keyHint("Tab", "group")];
9252
+ const secondary = [keyHint("Esc", "clear/cancel"), keyHint("Ctrl+U", "clear")];
9253
+ if (!compact)
9254
+ secondary.push(keyHint("Backspace", "edit"));
9255
+ const footer = [status, ...hintRows(keys, width), ...hintRows(secondary, width)];
9256
+ const footerBudget = Math.max(1, height - header.length - 3);
9257
+ footer.splice(footerBudget);
9258
+ const panelHeight = Math.max(1, height - header.length - footer.length);
9259
+ const framed = panelHeight >= 5 && width >= 36;
9260
+ const bodyHeight = Math.max(1, panelHeight - (framed ? 2 : 0));
9261
+ const detailWidth = !compact && width >= 100 && bodyHeight >= 10 ? 30 : 0;
9262
+ const listWidth = width - (detailWidth ? detailWidth + 2 : 0);
9263
+ const contentWidth = listWidth - (framed ? 5 : 0);
9264
+ let body = modelRows(state, contentWidth);
9265
+ if (!body.length) {
9266
+ body = [
9267
+ [],
9268
+ [span("No matching models", "warm", true)],
9269
+ [],
9270
+ ...wrapLine([span("Try a shorter model or provider name.", "muted")], contentWidth),
9271
+ [],
9272
+ [...keyHint("Ctrl+U", "clear search")]
9273
+ ].map((line) => ({ line, active: false }));
9274
+ }
9275
+ const visible = viewport(body, bodyHeight);
9276
+ const details = detailWidth ? detailRows(state, detailWidth - 4) : [];
9277
+ const frame = header.map((line) => theme.paint(line, width));
9278
+ const joinPanels = (left, right) => left + (detailWidth ? theme.paint([span(" ")], 2) + right : "");
9279
+ if (framed)
9280
+ frame.push(joinPanels(theme.paint(panelRule(listWidth, state.selected?.group ? `Models · ${state.selected.group}` : "Models"), listWidth), detailWidth ? theme.paint(panelRule(detailWidth, "Selected model"), detailWidth) : ""));
9281
+ const scrolling = body.length > bodyHeight;
9282
+ const thumbSize = Math.max(1, Math.floor(bodyHeight * bodyHeight / body.length));
9283
+ const thumbStart = scrolling ? Math.round(visible.start / (body.length - bodyHeight) * (bodyHeight - thumbSize)) : 0;
9284
+ for (let i = 0;i < bodyHeight; i++) {
9285
+ const row = visible.rows[i] || { line: [], active: false };
9286
+ const thumb = scrolling && i >= thumbStart && i < thumbStart + thumbSize;
9287
+ const bar = span(scrolling ? thumb ? "┃" : "│" : " ", thumb ? "accent" : "border");
9288
+ const left = framed ? theme.paint([span("│", "border"), span(" ")], 2) + theme.paint(row.line, contentWidth, row.active) + theme.paint([bar, span(" "), span("│", "border")], 3) : theme.paint(row.line, listWidth, row.active);
9289
+ const right = detailWidth ? theme.paint([span("│", "border"), span(" "), ...fitLine(details[i] || [], detailWidth - 4), span(" "), span("│", "border")], detailWidth) : "";
9290
+ frame.push(joinPanels(left, right));
9291
+ }
9292
+ if (framed)
9293
+ frame.push(joinPanels(theme.paint(panelRule(listWidth, "", true), listWidth), detailWidth ? theme.paint(panelRule(detailWidth, "", true), detailWidth) : ""));
9294
+ frame.push(...footer.map((line) => theme.paint(line, width)));
9295
+ return frame.slice(0, height).join(`
9296
+ `);
9297
+ }
9298
+ async function selectPoolModel(models, options = {}) {
9299
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
9300
+ throw new Error("Model selection requires an interactive terminal. Pass --model <id>.");
9301
+ const state = new ModelPickerState(preparePickerModels(models), options.grouping, options.favorites);
9302
+ if (!state.models.length)
9303
+ throw new Error("No models are available to select.");
9304
+ const input = process.stdin;
9305
+ const output = process.stdout;
9306
+ const wasRaw = input.isRaw;
9307
+ const wasFlowing = input.readableFlowing === true;
9308
+ let cleanupPicker;
9309
+ let chosen;
9310
+ let frameLines = 0;
9311
+ let previousFrame = "";
9312
+ const alternateScreen = process.env.TERM !== "dumb";
9313
+ const theme = new TerminalTheme;
9314
+ const draw = () => {
9315
+ const frame = renderModelPicker(state, output.columns || 80, output.rows || 24, { theme, toolLabel: options.toolLabel });
9316
+ if (frame === previousFrame)
9317
+ return;
9318
+ if (alternateScreen)
9319
+ output.write("\x1B[H\x1B[0J");
9320
+ else if (frameLines)
9321
+ output.write(`\r${frameLines > 1 ? `\x1B[${frameLines - 1}A` : ""}\x1B[0J`);
9322
+ output.write(frame);
9323
+ frameLines = frame.split(`
9324
+ `).length;
9325
+ previousFrame = frame;
9326
+ };
9327
+ emitKeypressEvents2(input);
9328
+ try {
9329
+ return await new Promise((resolve, reject) => {
9330
+ const cancel = () => reject(new Error("Start cancelled."));
9331
+ const onKey = (text, key) => {
9332
+ try {
9333
+ const action = state.handleKey(text, key, Math.max(1, (output.rows || 24) - 8));
9334
+ if (action?.type === "select") {
9335
+ chosen = state.selected?.model;
9336
+ resolve(action.model);
9337
+ } else if (action?.type === "cancel")
9338
+ cancel();
9339
+ else {
9340
+ if (action?.type === "favorites")
9341
+ options.onFavoritesChange?.([...state.favorites].sort());
9342
+ if (action?.type === "grouping")
9343
+ options.onGroupingChange?.(state.grouping);
9344
+ draw();
9345
+ }
9346
+ } catch (error) {
9347
+ reject(error);
9348
+ }
9349
+ };
9350
+ const onError = (error) => reject(error);
9351
+ const cleanup = () => {
9352
+ input.off("keypress", onKey);
9353
+ input.off("end", cancel);
9354
+ input.off("error", onError);
9355
+ output.off("resize", draw);
9356
+ process.off("SIGINT", cancel);
9357
+ process.off("SIGTERM", cancel);
9358
+ };
9359
+ cleanupPicker = cleanup;
9360
+ input.on("keypress", onKey);
9361
+ input.once("end", cancel);
9362
+ input.once("error", onError);
9363
+ output.on("resize", draw);
9364
+ process.once("SIGINT", cancel);
9365
+ process.once("SIGTERM", cancel);
9366
+ input.setRawMode(true);
9367
+ input.resume();
9368
+ output.write(`${alternateScreen ? "\x1B[?1049h" : ""}\x1B[?25l`);
9369
+ draw();
9370
+ });
9371
+ } finally {
9372
+ cleanupPicker?.();
9373
+ input.setRawMode(wasRaw);
9374
+ if (!wasFlowing)
9375
+ input.pause();
9376
+ if (alternateScreen)
9377
+ output.write("\x1B[?1049l");
9378
+ else if (frameLines)
9379
+ output.write(`\r${frameLines > 1 ? `\x1B[${frameLines - 1}A` : ""}\x1B[0J`);
9380
+ output.write(`\x1B[?25h${chosen ? `Selected ${modelProviderLabel(chosen)}` : "Model selection cancelled."}
9381
+ `);
9382
+ }
9383
+ }
9384
+
9385
+ // src/commands/settings.ts
9386
+ function parseModelGrouping(value) {
9387
+ if (!isModelGrouping(value)) {
9388
+ throw new Error(`Unknown model grouping '${value}'. Choose: ${MODEL_GROUPINGS.join(", ")}.`);
9389
+ }
9390
+ return value;
9391
+ }
9392
+ async function settingsCommand(options = {}) {
9393
+ let grouping;
9394
+ if (options.modelGrouping !== undefined) {
9395
+ grouping = parseModelGrouping(options.modelGrouping);
9396
+ } else {
9397
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
9398
+ throw new Error("Choose a grouping with `cr settings --model-grouping <model-provider|account-provider|none>`.");
9399
+ }
9400
+ const choice = await ve({
9401
+ message: "How should the model selector group models?",
9402
+ initialValue: loadConfig().modelGrouping || "none",
9403
+ options: [
9404
+ { value: "model-provider", label: MODEL_GROUPING_LABELS["model-provider"], hint: "Group by the model's owner, such as OpenAI or Anthropic" },
9405
+ { value: "account-provider", label: MODEL_GROUPING_LABELS["account-provider"], hint: "Group by pool provider, such as Codex or Antigravity" },
9406
+ { value: "none", label: MODEL_GROUPING_LABELS.none, hint: "One model list" }
9407
+ ]
9408
+ });
9409
+ if (pD(choice))
9410
+ throw new Error("Settings cancelled.");
9411
+ grouping = parseModelGrouping(String(choice));
9412
+ }
9413
+ saveConfig({ modelGrouping: grouping });
9414
+ M2.success(`Model grouping saved: ${MODEL_GROUPING_LABELS[grouping]}`);
9415
+ }
8651
9416
 
8652
9417
  // src/bridge/launch-proxy.ts
8653
9418
  import { randomUUID } from "node:crypto";
8654
9419
  import { createServer as createServer2, request as httpRequest } from "node:http";
8655
9420
  import { request as httpsRequest } from "node:https";
8656
9421
  var HOP_HEADERS = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host"]);
8657
- async function startLaunchProxy(gatewayUrl, apiKey, harness) {
9422
+ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey) {
8658
9423
  const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
8659
9424
  if (!["http:", "https:"].includes(upstream.protocol) || upstream.username || upstream.password) {
8660
9425
  throw new Error("ClosedRouter server URL must use HTTP or HTTPS without embedded credentials.");
@@ -8662,7 +9427,7 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness) {
8662
9427
  const launchId = randomUUID();
8663
9428
  const transport = upstream.protocol === "https:" ? httpsRequest : httpRequest;
8664
9429
  const server = createServer2((incoming, outgoing) => {
8665
- const authorized = incoming.headers.authorization === `Bearer ${apiKey}` || incoming.headers["x-api-key"] === apiKey || incoming.headers["x-goog-api-key"] === apiKey;
9430
+ const authorized = incoming.headers.authorization === `Bearer ${localKey}` || incoming.headers["x-api-key"] === localKey || incoming.headers["x-goog-api-key"] === localKey;
8666
9431
  if (!authorized) {
8667
9432
  outgoing.writeHead(401).end();
8668
9433
  return;
@@ -8683,6 +9448,11 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness) {
8683
9448
  }
8684
9449
  headers["x-closedrouter-harness"] = harness;
8685
9450
  headers["x-closedrouter-launch-id"] = launchId;
9451
+ if (localKey !== apiKey) {
9452
+ delete headers["x-api-key"];
9453
+ delete headers["x-goog-api-key"];
9454
+ headers.authorization = `Bearer ${apiKey}`;
9455
+ }
8686
9456
  const request = transport(target, { method: incoming.method, headers }, (response) => {
8687
9457
  const responseHeaders = { ...response.headers };
8688
9458
  for (const key of HOP_HEADERS)
@@ -8778,6 +9548,628 @@ async function startAgyModelProxy(gatewayUrl, apiKey, model) {
8778
9548
  };
8779
9549
  }
8780
9550
 
9551
+ // src/commands/agy-model.ts
9552
+ import { randomUUID as randomUUID2 } from "node:crypto";
9553
+ import { existsSync as existsSync9, readFileSync as readFileSync8, renameSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
9554
+ import { join as join9 } from "node:path";
9555
+ function record(value) {
9556
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9557
+ }
9558
+ function registerAgyLaunchModel(homeDir, model, transportModel) {
9559
+ const path = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
9560
+ const settings = JSON.parse(readFileSync8(path, "utf8"));
9561
+ if (!record(settings) || settings.modelProvider !== "gemini") {
9562
+ throw new Error(`Set "modelProvider": "gemini" in ${path} before starting Antigravity through ClosedRouter.`);
9563
+ }
9564
+ const config = settings.customModelsConfig ?? {};
9565
+ if (!record(config))
9566
+ throw new Error(`customModelsConfig must be an object in ${path}.`);
9567
+ const models = config.customModels ?? {};
9568
+ if (!record(models))
9569
+ throw new Error(`customModelsConfig.customModels must be an object in ${path}.`);
9570
+ const label = `ClosedRouter: ${model}`;
9571
+ const entry = { modelName: transportModel, apiProvider: "gemini" };
9572
+ const existing = models[label];
9573
+ if (existing !== undefined) {
9574
+ if (!record(existing) || existing.modelName !== transportModel || existing.apiProvider !== "gemini" || Object.keys(existing).some((key) => !["modelName", "apiProvider"].includes(key))) {
9575
+ throw new Error(`The custom model '${label}' already has different settings in ${path}. Rename that entry before launching it with ClosedRouter.`);
9576
+ }
9577
+ return label;
9578
+ }
9579
+ settings.customModelsConfig = { ...config, customModels: { ...models, [label]: entry } };
9580
+ const temporary = `${path}.closedrouter-${randomUUID2()}.tmp`;
9581
+ try {
9582
+ writeFileSync2(temporary, `${JSON.stringify(settings, null, 2)}
9583
+ `, { flag: "wx", mode: statSync(path).mode & 511 });
9584
+ renameSync(temporary, path);
9585
+ } finally {
9586
+ if (existsSync9(temporary))
9587
+ unlinkSync(temporary);
9588
+ }
9589
+ return label;
9590
+ }
9591
+
9592
+ // src/commands/cursor-launch.ts
9593
+ import { execFileSync } from "node:child_process";
9594
+ import { homedir as homedir9 } from "node:os";
9595
+ import { join as join10 } from "node:path";
9596
+ var CURSOR_LOCAL_INSTALL_URL = "https://cursor.com/internal-local-install-nHZIc6RZgTXIfai0aO?darwin=true&channel=prod";
9597
+ var MANAGED_OPTIONS = new Set([
9598
+ "--model",
9599
+ "--base-url",
9600
+ "--local-agent-api-key",
9601
+ "--api-key",
9602
+ "--auth-token",
9603
+ "--endpoint",
9604
+ "-e",
9605
+ "--agent-endpoint",
9606
+ "--enable-bedrock",
9607
+ "--enable-local-bedrock"
9608
+ ]);
9609
+ function buildCursorLaunchPlan(serverUrl, apiKey, model, toolArgs = [], homeDir = homedir9(), executablePath = "agent-local") {
9610
+ const conflicting = toolArgs.find((arg) => MANAGED_OPTIONS.has(arg.split("=", 1)[0]) || /^-e[^-]/.test(arg));
9611
+ if (conflicting) {
9612
+ const option = conflicting.startsWith("-e") && !conflicting.startsWith("--") ? "-e" : conflicting.split("=", 1)[0];
9613
+ throw new Error(`Remove ${option}; ClosedRouter manages the Cursor model, provider, and authentication. Put other arguments after --.`);
9614
+ }
9615
+ const env = { ...process.env };
9616
+ for (const name of [
9617
+ "CURSOR_API_KEY",
9618
+ "CURSOR_AUTH_TOKEN",
9619
+ "CURSOR_ENABLE_BEDROCK",
9620
+ "CURSOR_ENABLE_LOCAL_BEDROCK",
9621
+ "CURSOR_LOCAL_AGENT_API_KEY_HELPER",
9622
+ "CURSOR_LOCAL_AGENT_ALLOW_CURSOR_HOST"
9623
+ ])
9624
+ delete env[name];
9625
+ env.CURSOR_LOCAL_AGENT_BASE_URL = `${serverUrl.replace(/\/+$/, "")}/v1`;
9626
+ env.CURSOR_LOCAL_AGENT_API_KEY = apiKey;
9627
+ env.CURSOR_ENABLE_AUTHLESS = "1";
9628
+ env.CURSOR_CONFIG_DIR = join10(process.env.CLOSEDROUTER_CONFIG_DIR || join10(homeDir, ".closedrouter"), "cursor-launch");
9629
+ env.CURSOR_DATA_DIR = env.CURSOR_CONFIG_DIR;
9630
+ return { tool: "cursor", command: executablePath, args: ["--authless", "--model", model, ...toolArgs], env };
9631
+ }
9632
+ function resolveCursorLocalExecutable(executablePath, homeDir = homedir9()) {
9633
+ const candidates = executablePath ? [executablePath] : [
9634
+ "agent-local",
9635
+ "cursor-agent-local",
9636
+ join10(homeDir, ".local", "bin", "agent-local"),
9637
+ join10(homeDir, ".local", "bin", "cursor-agent-local")
9638
+ ];
9639
+ const env = buildCursorLaunchPlan("http://127.0.0.1:1", "cr-sk-preflight", "preflight", [], homeDir).env;
9640
+ for (const command of candidates) {
9641
+ try {
9642
+ execFileSync(command, ["--authless", "about", "--format", "json"], {
9643
+ env,
9644
+ timeout: 1e4,
9645
+ stdio: "ignore"
9646
+ });
9647
+ return command;
9648
+ } catch {}
9649
+ }
9650
+ throw new Error(`Cursor requires the separate Agent Local package (agent-local or cursor-agent-local).${executablePath ? ` '${executablePath}' could not start in authless mode.` : ""} Install it from ${CURSOR_LOCAL_INSTALL_URL}, or pass --path <cursor-agent-local>. The regular Cursor agent CLI cannot use the ClosedRouter pool.`);
9651
+ }
9652
+
9653
+ // src/commands/desktop-launch.ts
9654
+ import { spawn as spawn3 } from "node:child_process";
9655
+ import { randomUUID as randomUUID3 } from "node:crypto";
9656
+ import { accessSync, constants, existsSync as existsSync11, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
9657
+ import { homedir as homedir10 } from "node:os";
9658
+ import { dirname, isAbsolute, join as join11, relative, resolve } from "node:path";
9659
+
9660
+ // src/commands/desktop-inspector.ts
9661
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
9662
+ import { setTimeout as delay } from "node:timers/promises";
9663
+ async function installDesktopInspectorHook(hookPath, logPath, signal) {
9664
+ const deadline = Date.now() + 15000;
9665
+ let target;
9666
+ while (Date.now() < deadline && !signal.aborted) {
9667
+ if (existsSync10(logPath))
9668
+ target = /Debugger listening on (ws:\/\/127\.0\.0\.1:\d+\/[^\s]+)/.exec(readFileSync9(logPath, "utf8"))?.[1];
9669
+ if (target)
9670
+ break;
9671
+ await delay(100, undefined, { signal });
9672
+ }
9673
+ if (!target)
9674
+ throw new Error("Antigravity did not expose its launch inspector. Close the paused app and retry with --open-only.");
9675
+ const Socket = "Bun" in globalThis ? globalThis.WebSocket : wrapper_default;
9676
+ const socket = new Socket(target);
9677
+ let sequence = 0;
9678
+ const pending = new Map;
9679
+ let resolvePaused = () => {};
9680
+ const paused = new Promise((resolve) => {
9681
+ resolvePaused = resolve;
9682
+ });
9683
+ const timer = setTimeout(() => socket.close(), Math.max(1, deadline - Date.now()));
9684
+ const abort = () => socket.close();
9685
+ signal.addEventListener("abort", abort, { once: true });
9686
+ const closed = () => {
9687
+ for (const item of pending.values())
9688
+ item.reject(new Error("Antigravity launch inspector disconnected before the hook was installed."));
9689
+ pending.clear();
9690
+ };
9691
+ socket.addEventListener("close", closed);
9692
+ socket.addEventListener("message", (event) => {
9693
+ const message = JSON.parse(String(event.data));
9694
+ if (message.method === "Debugger.paused")
9695
+ resolvePaused();
9696
+ if (message.id !== undefined) {
9697
+ const item = pending.get(message.id);
9698
+ if (!item)
9699
+ return;
9700
+ pending.delete(message.id);
9701
+ if (message.error)
9702
+ item.reject(new Error(message.error.message));
9703
+ else
9704
+ item.resolve(message.result || {});
9705
+ }
9706
+ });
9707
+ const rpc = (method, params = {}) => new Promise((resolve, reject) => {
9708
+ const id = ++sequence;
9709
+ pending.set(id, { resolve, reject });
9710
+ socket.send(JSON.stringify({ id, method, params }));
9711
+ });
9712
+ try {
9713
+ await new Promise((resolve, reject) => {
9714
+ socket.addEventListener("open", () => resolve(), { once: true });
9715
+ socket.addEventListener("error", () => reject(new Error("Could not connect to Antigravity's launch inspector.")), { once: true });
9716
+ socket.addEventListener("close", () => reject(new Error("Antigravity's launch inspector closed.")), { once: true });
9717
+ });
9718
+ await rpc("Debugger.enable");
9719
+ await rpc("Runtime.runIfWaitingForDebugger");
9720
+ await Promise.race([paused, delay(Math.max(1, deadline - Date.now()), undefined, { signal }).then(() => {
9721
+ throw new Error("Antigravity did not pause before loading its application.");
9722
+ })]);
9723
+ const result = await rpc("Runtime.evaluate", {
9724
+ expression: `process.getBuiltinModule('module').createRequire(process.cwd() + '/closedrouter-hook.cjs')(${JSON.stringify(hookPath)}); 'closedrouter-hook-loaded'`,
9725
+ returnByValue: true
9726
+ });
9727
+ if (result.exceptionDetails || result.result?.value !== "closedrouter-hook-loaded")
9728
+ throw new Error("Could not install Antigravity's request hook. Close the paused app and use --open-only.");
9729
+ await rpc("Debugger.resume");
9730
+ await rpc("Runtime.evaluate", { expression: "setTimeout(() => process.getBuiltinModule('inspector').close(), 100).unref(); true", returnByValue: true });
9731
+ } finally {
9732
+ clearTimeout(timer);
9733
+ signal.removeEventListener("abort", abort);
9734
+ socket.close();
9735
+ }
9736
+ }
9737
+
9738
+ // src/commands/desktop-launch.ts
9739
+ var APP_NAMES = {
9740
+ codex: ["Codex", "ChatGPT"],
9741
+ claude: ["Claude"],
9742
+ cursor: ["Cursor"],
9743
+ agy: ["Antigravity", "Antigravity IDE"]
9744
+ };
9745
+ function isDesktopTool(tool) {
9746
+ return Object.hasOwn(APP_NAMES, tool);
9747
+ }
9748
+ function resolveDesktopExecutable(tool, explicitPath, platform = process.platform, home = homedir10()) {
9749
+ if (explicitPath) {
9750
+ const path = resolve(explicitPath);
9751
+ if (!existsSync11(path))
9752
+ throw new Error(`Desktop app not found at ${path}.`);
9753
+ if (platform === "darwin" && path.endsWith(".app"))
9754
+ return path;
9755
+ accessSync(path, constants.X_OK);
9756
+ return path;
9757
+ }
9758
+ const names = APP_NAMES[tool];
9759
+ const candidates = platform === "darwin" ? names.flatMap((name) => [join11("/Applications", `${name}.app`), join11(home, "Applications", `${name}.app`)]) : platform === "win32" ? names.flatMap((name) => [
9760
+ join11(process.env.LOCALAPPDATA || join11(home, "AppData", "Local"), "Programs", name, `${name}.exe`),
9761
+ join11(process.env.ProgramFiles || "C:\\Program Files", name, `${name}.exe`)
9762
+ ]) : names.flatMap((name) => [join11("/opt", name.toLowerCase(), name.toLowerCase()), join11("/usr/bin", `${name.toLowerCase()}-desktop`)]);
9763
+ const installed = candidates.find((path) => existsSync11(path));
9764
+ if (!installed)
9765
+ throw new Error(`${names[0]} desktop app was not found. Pass --path <app-bundle-or-executable>.`);
9766
+ return installed;
9767
+ }
9768
+ function validateDesktopArgs(args) {
9769
+ const managed = ["--user-data-dir", "--profile", "--config", "--model", "--boot-1p-once", "--inspect", "--inspect-brk", "-p", "-c", "-m"];
9770
+ const conflict = args.find((arg) => managed.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
9771
+ if (conflict)
9772
+ throw new Error(`Remove ${conflict}; ClosedRouter manages the desktop profile and model.`);
9773
+ }
9774
+ function createDesktopProfile(tool) {
9775
+ const root = join11(getConfigDir(), "desktop", tool);
9776
+ mkdirSync2(root, { recursive: true, mode: 448 });
9777
+ return mkdtempSync(join11(root, "session-"));
9778
+ }
9779
+ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey, model, toolArgs = [], platform = process.platform) {
9780
+ validateDesktopArgs(toolArgs);
9781
+ const env = { ...process.env };
9782
+ delete env.ELECTRON_RUN_AS_NODE;
9783
+ delete env.NODE_OPTIONS;
9784
+ const files = [];
9785
+ const appEnv = {};
9786
+ let userData = join11(profileDir, "user-data");
9787
+ if (tool === "codex") {
9788
+ appEnv.CODEX_HOME = join11(profileDir, "codex-home");
9789
+ appEnv.CODEX_ELECTRON_USER_DATA_PATH = userData;
9790
+ }
9791
+ if (tool === "codex" && baseUrl && localKey && model) {
9792
+ const codexHome = join11(profileDir, "codex-home");
9793
+ appEnv.CODEX_HOME = codexHome;
9794
+ appEnv.CODEX_ELECTRON_USER_DATA_PATH = userData;
9795
+ files.push({ path: join11(codexHome, "config.toml"), contents: [
9796
+ `model = ${JSON.stringify(model)}`,
9797
+ 'model_provider = "closedrouter"',
9798
+ "",
9799
+ "[model_providers.closedrouter]",
9800
+ 'name = "ClosedRouter"',
9801
+ `base_url = ${JSON.stringify(`${baseUrl.replace(/\/+$/, "")}/v1`)}`,
9802
+ 'wire_api = "responses"',
9803
+ "requires_openai_auth = true",
9804
+ ""
9805
+ ].join(`
9806
+ `) });
9807
+ files.push({ path: join11(codexHome, "auth.json"), contents: JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: localKey }) });
9808
+ }
9809
+ if (tool === "claude" && baseUrl && localKey && model) {
9810
+ userData = join11(profileDir, "user-data-3p");
9811
+ const id = randomUUID3();
9812
+ const json = (value) => `${JSON.stringify(value, null, 2)}
9813
+ `;
9814
+ files.push({ path: join11(userData, "claude_desktop_config.json"), contents: json({ deploymentMode: "3p" }) });
9815
+ files.push({ path: join11(userData, "configLibrary", "_meta.json"), contents: json({ appliedId: id, entries: [{ id, name: "ClosedRouter" }] }) });
9816
+ files.push({ path: join11(userData, "configLibrary", `${id}.json`), contents: json({
9817
+ inferenceProvider: "gateway",
9818
+ inferenceGatewayBaseUrl: baseUrl,
9819
+ inferenceGatewayApiKey: localKey,
9820
+ inferenceGatewayAuthScheme: "bearer",
9821
+ disableDeploymentModeChooser: true,
9822
+ inferenceModels: [{ name: model }]
9823
+ }) });
9824
+ }
9825
+ Object.assign(env, appEnv);
9826
+ const args = [`--user-data-dir=${userData}`, ...toolArgs];
9827
+ if (platform === "darwin" && executable.endsWith(".app")) {
9828
+ return { command: "/usr/bin/open", args: [
9829
+ "-W",
9830
+ "-n",
9831
+ "-a",
9832
+ executable,
9833
+ ...Object.entries(appEnv).flatMap(([key, value]) => ["--env", `${key}=${value}`]),
9834
+ "--args",
9835
+ ...args
9836
+ ], env, files, profileDir };
9837
+ }
9838
+ return { command: executable, args, env, files, profileDir };
9839
+ }
9840
+ function writeDesktopLaunchPlan(plan) {
9841
+ mkdirSync2(plan.profileDir, { recursive: true, mode: 448 });
9842
+ for (const file of plan.files) {
9843
+ const local = relative(plan.profileDir, file.path);
9844
+ if (!isAbsolute(file.path) || local.startsWith("..") || isAbsolute(local))
9845
+ throw new Error("Desktop configuration must stay inside its launch profile.");
9846
+ mkdirSync2(dirname(file.path), { recursive: true, mode: 448 });
9847
+ writeFileSync3(file.path, file.contents, { mode: 384 });
9848
+ }
9849
+ }
9850
+ function assertClaudeDesktopGatewaySupport(executable) {
9851
+ if (!executable.endsWith(".app"))
9852
+ return;
9853
+ const archive = join11(executable, "Contents", "Resources", "app.asar");
9854
+ const source = readFileSync10(archive);
9855
+ if (!source.includes(Buffer.from("inferenceGatewayBaseUrl"))) {
9856
+ throw new Error("This Claude Desktop build does not include inference gateway support. Update Claude or use cr start claude.");
9857
+ }
9858
+ }
9859
+ async function runDesktopLaunch(plan) {
9860
+ return await new Promise((resolveExit, reject) => {
9861
+ const controller = new AbortController;
9862
+ const child = spawn3(plan.command, plan.args, { env: plan.env, stdio: "inherit" });
9863
+ const interrupt = () => {
9864
+ controller.abort();
9865
+ child.kill("SIGTERM");
9866
+ };
9867
+ const cleanup = () => {
9868
+ controller.abort();
9869
+ process.off("SIGINT", interrupt);
9870
+ process.off("SIGTERM", interrupt);
9871
+ };
9872
+ process.once("SIGINT", interrupt);
9873
+ process.once("SIGTERM", interrupt);
9874
+ child.once("error", (error) => {
9875
+ cleanup();
9876
+ reject(error);
9877
+ });
9878
+ child.once("exit", (code) => {
9879
+ cleanup();
9880
+ resolveExit(code ?? 1);
9881
+ });
9882
+ if (plan.inspectorHook) {
9883
+ installDesktopInspectorHook(plan.inspectorHook.path, plan.inspectorHook.logPath, controller.signal).catch((error) => {
9884
+ child.kill("SIGTERM");
9885
+ cleanup();
9886
+ reject(error);
9887
+ });
9888
+ }
9889
+ });
9890
+ }
9891
+
9892
+ // src/commands/cursor-desktop.ts
9893
+ import { spawn as spawn4 } from "node:child_process";
9894
+ import { chmodSync, existsSync as existsSync12, mkdirSync as mkdirSync3 } from "node:fs";
9895
+ import { homedir as homedir11 } from "node:os";
9896
+ import { dirname as dirname2, join as join12 } from "node:path";
9897
+ var CURSOR_APPLICATION_STORAGE = "src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser";
9898
+ function cursorDesktopSupportsModel(model) {
9899
+ return !model.startsWith("claude-") && !model.startsWith("gemini-");
9900
+ }
9901
+ async function writeSqlite(dbPath, sql) {
9902
+ let database;
9903
+ if ("Bun" in globalThis) {
9904
+ const runtimeModule = "bun:sqlite";
9905
+ const sqlite = await import(runtimeModule);
9906
+ database = new sqlite.Database(dbPath);
9907
+ } else {
9908
+ try {
9909
+ const sqlite = await import("node:sqlite");
9910
+ database = new sqlite.DatabaseSync(dbPath);
9911
+ } catch {}
9912
+ }
9913
+ if (database) {
9914
+ try {
9915
+ database.exec(sql);
9916
+ } finally {
9917
+ database.close();
9918
+ }
9919
+ return;
9920
+ }
9921
+ await new Promise((resolve, reject) => {
9922
+ const child = spawn4("sqlite3", [dbPath], { stdio: ["pipe", "ignore", "ignore"] });
9923
+ child.once("error", () => reject(new Error("Cursor desktop configuration requires Bun, Node 22+, or the sqlite3 executable.")));
9924
+ child.once("exit", (code) => code === 0 ? resolve() : reject(new Error("Could not write Cursor desktop configuration.")));
9925
+ child.stdin.on("error", () => {});
9926
+ child.stdin.end(sql);
9927
+ });
9928
+ }
9929
+ function defaultCursorDatabase() {
9930
+ const root = process.platform === "darwin" ? join12(homedir11(), "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA || join12(homedir11(), "AppData", "Roaming") : join12(homedir11(), ".config");
9931
+ return join12(root, "Cursor", "User", "globalStorage", "state.vscdb");
9932
+ }
9933
+ async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = defaultCursorDatabase()) {
9934
+ if (!cursorDesktopSupportsModel(model))
9935
+ throw new Error("Cursor desktop selects a separate provider for Claude/Gemini model names. Choose an OpenAI-compatible custom model or use cr start cursor with the local CLI runtime.");
9936
+ const dbPath = join12(profileDir, "user-data", "User", "globalStorage", "state.vscdb");
9937
+ mkdirSync3(dirname2(dbPath), { recursive: true, mode: 448 });
9938
+ const items = new Map;
9939
+ if (existsSync12(sourceDb)) {
9940
+ const auth = await querySqliteKeyValue(sourceDb, "cursorAuth/");
9941
+ for (const name of ["accessToken", "refreshToken", "cachedEmail", "stripeMembershipAuthId", "membershipType", "cachedSignUpType"]) {
9942
+ const key = `cursorAuth/${name}`;
9943
+ const value = auth.get(key);
9944
+ if (value !== undefined)
9945
+ items.set(key, value);
9946
+ }
9947
+ }
9948
+ items.set("cursorAuth/openAIKey", apiKey);
9949
+ items.set(CURSOR_APPLICATION_STORAGE, JSON.stringify({
9950
+ useOpenAIKey: true,
9951
+ openAIBaseUrl: `${baseUrl.replace(/\/+$/, "")}/v1`,
9952
+ availableAPIKeyModels: [model],
9953
+ havePerformedSettingsServiceMigration: true,
9954
+ aiSettings: {
9955
+ userAddedModels: [model],
9956
+ modelOverrideEnabled: [model],
9957
+ modelOverrideDisabled: [],
9958
+ modelConfig: Object.fromEntries(["cmd-k", "composer", "background-composer", "plan-execution", "spec", "quick-agent"].map((feature) => [feature, { modelName: model, maxMode: false, selectedModels: [{ modelId: model, parameters: [] }] }]))
9959
+ }
9960
+ }));
9961
+ items.set("lastSingleModelPreference", JSON.stringify({ composer: model, "cmd-k": model }));
9962
+ const quote = (value) => `'${value.replace(/'/g, "''")}'`;
9963
+ await writeSqlite(dbPath, [
9964
+ "BEGIN IMMEDIATE;",
9965
+ "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB);",
9966
+ ...[...items].map(([key, value]) => `INSERT OR REPLACE INTO ItemTable (key, value) VALUES (${quote(key)}, ${quote(value)});`),
9967
+ "COMMIT;"
9968
+ ].join(`
9969
+ `));
9970
+ chmodSync(dbPath, 384);
9971
+ return dbPath;
9972
+ }
9973
+
9974
+ // src/commands/agy-desktop.ts
9975
+ import { createServer as createServer4 } from "node:http";
9976
+ import { StringDecoder } from "node:string_decoder";
9977
+ import { randomUUID as randomUUID4 } from "node:crypto";
9978
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
9979
+ import { homedir as homedir12 } from "node:os";
9980
+ import { join as join13 } from "node:path";
9981
+ var MODS_CONTROL_URL = "http://127.0.0.1:59998";
9982
+ var MODS_PROXY_URL = "http://127.0.0.1:59997";
9983
+ async function assertAntigravityModsAvailable() {
9984
+ try {
9985
+ const response = await fetch(`${MODS_CONTROL_URL}/action`, {
9986
+ method: "POST",
9987
+ headers: { "content-type": "application/json" },
9988
+ body: JSON.stringify({ action: "getCockpitAccounts" }),
9989
+ signal: AbortSignal.timeout(2000)
9990
+ });
9991
+ if (!response.ok)
9992
+ throw new Error("Not ready");
9993
+ const body = await response.json();
9994
+ if (!body || typeof body !== "object")
9995
+ throw new Error("Unexpected control service");
9996
+ } catch {
9997
+ throw new Error(`Antigravity desktop routing requires the running antigravity-mods daemon at ${MODS_CONTROL_URL}. Start ${join13(homedir12(), "Projects", "antigravity-mods", "bin", "antigravity-ui-daemon.js")} or use --open-only.`);
9998
+ }
9999
+ }
10000
+ function assertAntigravityDesktopHookSupport(executable) {
10001
+ if (!executable.endsWith(".app"))
10002
+ throw new Error("Antigravity desktop routing currently requires a macOS app bundle. Use --open-only on other platforms.");
10003
+ const archive = join13(executable, "Contents", "Resources", "app.asar");
10004
+ const source = readFileSync11(archive);
10005
+ for (const marker of ["--cloud_code_endpoint", "--api_server_url", "startLanguageServer"]) {
10006
+ if (!source.includes(Buffer.from(marker)))
10007
+ throw new Error("This Antigravity build has an unsupported language-server launcher. Use --open-only.");
10008
+ }
10009
+ const framework = join13(executable, "Contents", "Frameworks", "Electron Framework.framework", "Versions", "A", "Electron Framework");
10010
+ if (!existsSync13(framework))
10011
+ throw new Error("Could not verify Antigravity's Electron hook support.");
10012
+ const binary = readFileSync11(framework);
10013
+ const index = binary.indexOf(Buffer.from("dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX"));
10014
+ if (index < 0 || binary[index + 32 + 5] !== 49)
10015
+ throw new Error("This Antigravity build disables launch inspector hooks. Use --open-only.");
10016
+ }
10017
+ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL) {
10018
+ const capability = randomUUID4();
10019
+ const controllers = new Set;
10020
+ const server = createServer4(async (incoming, outgoing) => {
10021
+ const controller = new AbortController;
10022
+ controllers.add(controller);
10023
+ outgoing.on("close", () => {
10024
+ if (!outgoing.writableFinished)
10025
+ controller.abort();
10026
+ });
10027
+ try {
10028
+ const url = new URL(incoming.url || "/", "http://localhost");
10029
+ const prefix = `/${capability}/`;
10030
+ if (!url.pathname.startsWith(prefix)) {
10031
+ outgoing.writeHead(401).end();
10032
+ return;
10033
+ }
10034
+ const path = url.pathname.slice(prefix.length - 1);
10035
+ const generation = /:(streamGenerateContent|generateContent)$/.exec(path);
10036
+ const chunks = [];
10037
+ let size = 0;
10038
+ for await (const chunk of incoming) {
10039
+ size += chunk.length;
10040
+ if (size > 32 * 1024 * 1024) {
10041
+ outgoing.writeHead(413).end();
10042
+ return;
10043
+ }
10044
+ chunks.push(Buffer.from(chunk));
10045
+ }
10046
+ const raw = Buffer.concat(chunks);
10047
+ const headers = {};
10048
+ let target;
10049
+ let payload = raw;
10050
+ if (generation) {
10051
+ if (incoming.method !== "POST") {
10052
+ outgoing.writeHead(405).end();
10053
+ return;
10054
+ }
10055
+ const body = JSON.parse(raw.toString("utf8"));
10056
+ const request = body.request || body;
10057
+ payload = Buffer.from(JSON.stringify(request));
10058
+ target = `${gatewayUrl}/v1beta/models/${encodeURIComponent(model)}:${generation[1]}${generation[1] === "streamGenerateContent" ? "?alt=sse" : ""}`;
10059
+ headers.authorization = `Bearer ${localKey}`;
10060
+ headers["content-type"] = "application/json";
10061
+ } else {
10062
+ target = `${modsUrl}${path}${url.search}`;
10063
+ for (const name of ["authorization", "content-type", "user-agent"]) {
10064
+ const value = incoming.headers[name];
10065
+ if (typeof value === "string")
10066
+ headers[name] = value;
10067
+ }
10068
+ }
10069
+ const response = await fetch(target, {
10070
+ method: incoming.method,
10071
+ headers,
10072
+ signal: controller.signal,
10073
+ body: incoming.method === "GET" || incoming.method === "HEAD" ? undefined : new Uint8Array(payload)
10074
+ });
10075
+ if (!generation || !response.ok) {
10076
+ outgoing.writeHead(response.status, { "content-type": response.headers.get("content-type") || "application/json" });
10077
+ if (response.body)
10078
+ for await (const chunk of response.body)
10079
+ outgoing.write(chunk);
10080
+ outgoing.end();
10081
+ } else if (generation[1] === "generateContent") {
10082
+ outgoing.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ response: await response.json() }));
10083
+ } else {
10084
+ outgoing.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
10085
+ const decoder = new StringDecoder("utf8");
10086
+ let buffer = "";
10087
+ const emit = (event) => {
10088
+ const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join(`
10089
+ `);
10090
+ if (!data || data === "[DONE]")
10091
+ return;
10092
+ outgoing.write(`data: ${JSON.stringify({ response: JSON.parse(data) })}
10093
+
10094
+ `);
10095
+ };
10096
+ if (response.body)
10097
+ for await (const chunk of response.body) {
10098
+ buffer += decoder.write(Buffer.from(chunk));
10099
+ let match;
10100
+ while (match = /\r?\n\r?\n/.exec(buffer)) {
10101
+ emit(buffer.slice(0, match.index));
10102
+ buffer = buffer.slice(match.index + match[0].length);
10103
+ }
10104
+ }
10105
+ buffer += decoder.end();
10106
+ if (buffer.trim())
10107
+ emit(buffer);
10108
+ outgoing.end();
10109
+ }
10110
+ } catch {
10111
+ if (!outgoing.headersSent)
10112
+ outgoing.writeHead(502, { "content-type": "application/json" }).end(JSON.stringify({ error: { message: "Antigravity desktop adapter request failed." } }));
10113
+ else
10114
+ outgoing.destroy();
10115
+ } finally {
10116
+ controllers.delete(controller);
10117
+ }
10118
+ });
10119
+ await new Promise((resolve, reject) => {
10120
+ server.once("error", reject);
10121
+ server.listen(0, "127.0.0.1", () => {
10122
+ server.off("error", reject);
10123
+ resolve();
10124
+ });
10125
+ });
10126
+ const address = server.address();
10127
+ if (!address || typeof address === "string")
10128
+ throw new Error("Could not start Antigravity desktop adapter.");
10129
+ return {
10130
+ baseUrl: `http://127.0.0.1:${address.port}/${capability}`,
10131
+ close: () => new Promise((resolve, reject) => {
10132
+ for (const controller of controllers)
10133
+ controller.abort();
10134
+ server.close((error) => error ? reject(error) : resolve());
10135
+ server.closeAllConnections();
10136
+ })
10137
+ };
10138
+ }
10139
+ function addAntigravityDesktopHook(plan, endpoint) {
10140
+ const path = join13(plan.profileDir, "antigravity-hook.cjs");
10141
+ const contents = `const cp = require('node:child_process');
10142
+ const spawn = cp.spawn;
10143
+ cp.spawn = function(command, args, options) {
10144
+ if (typeof command === 'string' && /(?:^|[\\\\/])language_server(?:\\.exe)?$/.test(command) && Array.isArray(args)) {
10145
+ args = args.slice();
10146
+ for (const [flag, value] of [['--cloud_code_endpoint', ${JSON.stringify(endpoint)}]]) {
10147
+ const index = args.indexOf(flag);
10148
+ if (index < 0) throw new Error('Unsupported Antigravity language-server flags');
10149
+ args[index + 1] = value;
10150
+ }
10151
+ // antigravity-mods discovers its own sessions using the space-separated
10152
+ // csrf flag. Use the equivalent Go flag syntax to keep its periodic global
10153
+ // SetCloudCodeURL pass from replacing this managed session's gateway.
10154
+ const csrf = args.indexOf('--csrf_token');
10155
+ if (csrf >= 0) args.splice(csrf, 2, '--csrf_token=' + args[csrf + 1]);
10156
+ options = { ...options, env: { ...options?.env } };
10157
+ delete options.env.NODE_OPTIONS;
10158
+ }
10159
+ return spawn.call(this, command, args, options);
10160
+ };
10161
+ require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10162
+ `;
10163
+ plan.files.push({ path, contents });
10164
+ const logPath = join13(plan.profileDir, "inspector.log");
10165
+ plan.inspectorHook = { path, logPath };
10166
+ plan.args.push("--inspect-brk=127.0.0.1:0");
10167
+ if (plan.command === "/usr/bin/open") {
10168
+ const index = plan.args.indexOf("--args");
10169
+ plan.args.splice(index, 0, "--stderr", logPath);
10170
+ }
10171
+ }
10172
+
8781
10173
  // src/commands/start.ts
8782
10174
  function isGatewayModelAvailable(availableModels, model) {
8783
10175
  return availableModels.includes(model);
@@ -8788,8 +10180,30 @@ var TOOL_OPTIONS = [
8788
10180
  { value: "gemini", label: "Gemini CLI", hint: "Google Gemini API" },
8789
10181
  { value: "agy", label: "Antigravity CLI", hint: "Google Gemini API via your ClosedRouter pool" },
8790
10182
  { value: "aider", label: "Aider", hint: "OpenAI-compatible API" },
8791
- { value: "opencode", label: "OpenCode", hint: "OpenAI-compatible API" }
10183
+ { value: "opencode", label: "OpenCode", hint: "OpenAI-compatible API" },
10184
+ { value: "cursor", label: "Cursor Agent Local", hint: "OpenAI-compatible API; separate preview package required" }
10185
+ ];
10186
+ var DESKTOP_TOOL_OPTIONS = [
10187
+ { value: "codex-desktop", label: "Codex Desktop", hint: "Responses API with an isolated app profile" },
10188
+ { value: "antigravity-desktop", label: "Antigravity Desktop", hint: "ClosedRouter routing through your antigravity-mods hook" },
10189
+ { value: "cursor-desktop", label: "Cursor Desktop", hint: "Automatic API-key setup; requires a public gateway" },
10190
+ { value: "claude-desktop", label: "Claude Desktop", hint: "Inference gateway with an isolated app profile" }
8792
10191
  ];
10192
+ function getStartToolOptions(desktopOnly = false) {
10193
+ return desktopOnly ? [...DESKTOP_TOOL_OPTIONS] : [...TOOL_OPTIONS, ...DESKTOP_TOOL_OPTIONS];
10194
+ }
10195
+ function resolveStartTarget(value, desktop = false) {
10196
+ const normalized = value.toLowerCase();
10197
+ desktop ||= normalized.endsWith("-desktop");
10198
+ const name = normalized.replace(/-desktop$/, "");
10199
+ const tool = name === "antigravity" ? "agy" : name;
10200
+ if (!TOOL_OPTIONS.some((option) => option.value === tool)) {
10201
+ throw new Error(`Unsupported tool '${value}'. Choose: ${getStartToolOptions().map((option) => option.value).join(", ")}.`);
10202
+ }
10203
+ if (desktop && !isDesktopTool(tool))
10204
+ throw new Error(`Desktop launch is not supported for ${tool}. Choose codex, claude, cursor, or antigravity.`);
10205
+ return { tool, desktop };
10206
+ }
8793
10207
  function normalizeServerUrl(serverUrl) {
8794
10208
  return serverUrl.replace(/\/+$/, "");
8795
10209
  }
@@ -8802,10 +10216,13 @@ function assertNoManagedCodexArgs(args) {
8802
10216
  throw new Error(`Remove ${conflicting}; ClosedRouter manages the Codex profile and model. Put other arguments after --.`);
8803
10217
  }
8804
10218
  }
8805
- function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir9(), executablePath, agyRouting) {
10219
+ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir13(), executablePath, agyRouting) {
8806
10220
  const baseUrl = normalizeServerUrl(serverUrl);
8807
10221
  const env = { ...process.env };
8808
10222
  const command = executablePath || tool;
10223
+ if (tool === "cursor") {
10224
+ return buildCursorLaunchPlan(baseUrl, apiKey, model, toolArgs, homeDir, executablePath);
10225
+ }
8809
10226
  if (tool === "claude") {
8810
10227
  delete env.ANTHROPIC_AUTH_TOKEN;
8811
10228
  env.ANTHROPIC_BASE_URL = baseUrl;
@@ -8825,9 +10242,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
8825
10242
  }
8826
10243
  if (tool === "codex") {
8827
10244
  assertNoManagedCodexArgs(toolArgs);
8828
- const codexHome = process.env.CODEX_HOME || join9(homeDir, ".codex");
10245
+ const codexHome = process.env.CODEX_HOME || join14(homeDir, ".codex");
8829
10246
  const profileName = "closedrouter-launch";
8830
- const configPath = join9(codexHome, `${profileName}.config.toml`);
10247
+ const configPath = join14(codexHome, `${profileName}.config.toml`);
8831
10248
  const configLines = [
8832
10249
  `model = ${tomlString(model)}`,
8833
10250
  `model_provider = ${tomlString(profileName)}`,
@@ -8853,7 +10270,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
8853
10270
  env.GEMINI_API_KEY = apiKey;
8854
10271
  env.GEMINI_MODEL = model;
8855
10272
  env.GOOGLE_GEMINI_BASE_URL = baseUrl;
8856
- const configPath = join9(homeDir, ".closedrouter", "gemini-launch.settings.json");
10273
+ const configPath = join14(homeDir, ".closedrouter", "gemini-launch.settings.json");
8857
10274
  env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = configPath;
8858
10275
  return {
8859
10276
  tool,
@@ -8916,9 +10333,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
8916
10333
  };
8917
10334
  }
8918
10335
  function writeOwnedConfig(file) {
8919
- mkdirSync2(dirname(file.path), { recursive: true, mode: 448 });
10336
+ mkdirSync4(dirname3(file.path), { recursive: true, mode: 448 });
8920
10337
  try {
8921
- const existing = readFileSync8(file.path, "utf8");
10338
+ const existing = readFileSync12(file.path, "utf8");
8922
10339
  if (file.path.endsWith(".config.toml") && !existing.includes('name = "ClosedRouter"')) {
8923
10340
  throw new Error(`Refusing to replace an existing Codex profile at ${file.path}.`);
8924
10341
  }
@@ -8926,46 +10343,53 @@ function writeOwnedConfig(file) {
8926
10343
  if (error instanceof Error && error.message.startsWith("Refusing to replace"))
8927
10344
  throw error;
8928
10345
  }
8929
- writeFileSync2(file.path, file.contents, { encoding: "utf8", mode: 384 });
8930
- }
8931
- function modelId(model) {
8932
- return String(model.id || model.name || "");
10346
+ writeFileSync4(file.path, file.contents, { encoding: "utf8", mode: 384 });
8933
10347
  }
8934
10348
  function modelSupportsHarness(model, tool) {
8935
10349
  const parameters = model.supported_parameters;
8936
10350
  return (model.status === undefined || model.status === "available") && (model.available_tool_account_count === undefined || model.available_tool_account_count > 0) && (!Array.isArray(parameters) || parameters.includes("tools"));
8937
10351
  }
8938
- function discoverAgyModels(command) {
10352
+ function discoverAgyTransportModel(command, apiKey) {
10353
+ const output = execFileSync2(command, ["models"], {
10354
+ encoding: "utf8",
10355
+ timeout: 1e4,
10356
+ stdio: ["ignore", "pipe", "ignore"],
10357
+ env: { ...process.env, GEMINI_API_KEY: apiKey, GOOGLE_GEMINI_BASE_URL: "http://127.0.0.1:1" }
10358
+ });
10359
+ const models = output.split(/\r?\n/).map((line) => line.split("\t")[0]).filter((id) => /^gemini-[\w.-]+-(?:high|medium|low)$/.test(id)).map((id) => id.replace(/-(?:high|medium|low)$/, ""));
10360
+ if (!models.length)
10361
+ throw new Error("Antigravity did not report any Gemini API models. Update agy and select Gemini API key mode.");
10362
+ return models.includes("gemini-3.8-flash") ? "gemini-3.8-flash" : models[0];
10363
+ }
10364
+ function assertAgyCustomModel(command, label, apiKey) {
8939
10365
  try {
8940
- const output = execFileSync(command, ["models"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] });
8941
- return new Set(output.split(/\r?\n/).map((line) => line.split(/\s+/)[0]).filter((id) => /^gemini-[\w.-]+-(?:high|medium|low)$/.test(id)).map((id) => id.replace(/-(?:high|medium|low)$/, "")));
10366
+ const output = execFileSync2(command, ["models"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, GEMINI_API_KEY: apiKey, GOOGLE_GEMINI_BASE_URL: "http://127.0.0.1:1" } });
10367
+ if (output.split(/\r?\n/).some((line) => line.split("\t")[0] === label))
10368
+ return;
8942
10369
  } catch {
8943
10370
  throw new Error("Could not list Antigravity CLI models. Install or update `agy`, then try `cr start agy` again.");
8944
10371
  }
10372
+ throw new Error(`This version of agy did not recognize '${label}'. Update agy to use ClosedRouter's accurate model display.`);
8945
10373
  }
8946
10374
  function assertAgyGeminiMode(homeDir) {
8947
- const settingsPath = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
10375
+ const settingsPath = join14(homeDir, ".gemini", "antigravity-cli", "settings.json");
8948
10376
  try {
8949
- const settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
10377
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
8950
10378
  if (settings?.modelProvider === "gemini")
8951
10379
  return;
8952
10380
  } catch {}
8953
10381
  throw new Error(`Set "modelProvider": "gemini" in ${settingsPath} before starting Antigravity through ClosedRouter.`);
8954
10382
  }
8955
- async function selectTool(tool) {
8956
- const cleaned = tool?.toLowerCase();
8957
- if (cleaned) {
8958
- if (TOOL_OPTIONS.some((option) => option.value === cleaned))
8959
- return cleaned;
8960
- throw new Error(`Unsupported tool '${tool}'. Choose: ${TOOL_OPTIONS.map((option) => option.value).join(", ")}.`);
8961
- }
10383
+ async function selectTool(tool, desktop = false) {
10384
+ if (tool)
10385
+ return resolveStartTarget(tool, desktop);
8962
10386
  const choice = await ve({
8963
10387
  message: "Which coding tool should ClosedRouter configure and start?",
8964
- options: TOOL_OPTIONS.map(({ value, label, hint }) => ({ value, label, hint }))
10388
+ options: getStartToolOptions(desktop)
8965
10389
  });
8966
10390
  if (pD(choice))
8967
10391
  throw new Error("Start cancelled.");
8968
- return choice;
10392
+ return resolveStartTarget(choice, desktop);
8969
10393
  }
8970
10394
  async function ensurePlatformKey(tool, client, options) {
8971
10395
  const config = loadConfig();
@@ -8984,20 +10408,44 @@ async function ensurePlatformKey(tool, client, options) {
8984
10408
  }
8985
10409
  async function runTool(plan) {
8986
10410
  return await new Promise((resolveExit, rejectExit) => {
8987
- const child = spawn3(plan.command, plan.args, { env: plan.env, stdio: "inherit" });
10411
+ const child = spawn5(plan.command, plan.args, { env: plan.env, stdio: "inherit" });
8988
10412
  child.once("error", rejectExit);
8989
10413
  child.once("exit", (code) => resolveExit(code ?? 1));
8990
10414
  });
8991
10415
  }
8992
10416
  async function startCommand(options = {}) {
8993
- const tool = await selectTool(options.tool);
10417
+ const groupingOverride = options.groupBy === undefined ? undefined : parseModelGrouping(options.groupBy);
10418
+ const { tool, desktop } = await selectTool(options.tool, options.desktop);
10419
+ if (options.openOnly && !desktop)
10420
+ throw new Error("--open-only requires --desktop.");
10421
+ if (desktop && !isDesktopTool(tool))
10422
+ throw new Error(`Desktop launch is not supported for ${tool}. Choose codex, claude, cursor, or antigravity.`);
10423
+ if (desktop)
10424
+ validateDesktopArgs(options.toolArgs || []);
10425
+ const executablePath = desktop && isDesktopTool(tool) ? resolveDesktopExecutable(tool, options.executablePath) : tool === "cursor" ? resolveCursorLocalExecutable(options.executablePath) : options.executablePath;
10426
+ if (desktop && isDesktopTool(tool) && options.openOnly) {
10427
+ if (options.model || options.key || options.server)
10428
+ throw new Error("Open-only desktop launches do not use --model, --key, or --server.");
10429
+ const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), undefined, undefined, undefined, options.toolArgs);
10430
+ writeDesktopLaunchPlan(plan);
10431
+ M2.warn(`Opening ${tool === "agy" ? "Antigravity" : tool} desktop only. This session is not routed through ClosedRouter.`);
10432
+ process.exitCode = await runDesktopLaunch(plan);
10433
+ return;
10434
+ }
10435
+ if (desktop && tool === "claude")
10436
+ assertClaudeDesktopGatewaySupport(executablePath);
10437
+ if (desktop && tool === "agy") {
10438
+ assertAntigravityDesktopHookSupport(executablePath);
10439
+ await assertAntigravityModsAvailable();
10440
+ }
8994
10441
  const config = loadConfig();
8995
10442
  const serverUrl = normalizeServerUrl(options.server || config.serverUrl);
8996
10443
  const client = new ClosedRouterClient({ baseUrl: serverUrl, sessionToken: config.sessionToken });
8997
10444
  const apiKey = await ensurePlatformKey(tool, client, options);
8998
10445
  client.setApiKey(apiKey);
8999
10446
  const models = await client.listGatewayModels({ preferApiKey: true });
9000
- const availableModels = models.filter((item) => modelSupportsHarness(item, tool)).map(modelId).filter(Boolean).sort();
10447
+ const eligibleModels = models.filter((item) => modelSupportsHarness(item, tool) && (!desktop || tool !== "cursor" || cursorDesktopSupportsModel(gatewayModelId(item))));
10448
+ const availableModels = [...new Set(eligibleModels.map(gatewayModelId).filter(Boolean))].sort();
9001
10449
  let model = options.model?.trim();
9002
10450
  if (!model) {
9003
10451
  if (availableModels.length === 0) {
@@ -9006,29 +10454,59 @@ async function startCommand(options = {}) {
9006
10454
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
9007
10455
  throw new Error(`Choose a model with --model. Available models: ${availableModels.slice(0, 12).join(", ")}`);
9008
10456
  }
9009
- const choice = await ve({
9010
- message: "Select a model from your ClosedRouter pool:",
9011
- options: availableModels.map((id) => ({ value: id, label: id }))
10457
+ model = await selectPoolModel(eligibleModels, {
10458
+ toolLabel: desktop ? `${tool === "agy" ? "Antigravity" : tool} desktop` : TOOL_OPTIONS.find((option) => option.value === tool)?.label,
10459
+ grouping: groupingOverride || config.modelGrouping || "none",
10460
+ favorites: config.favoriteModels,
10461
+ onFavoritesChange: (favoriteModels) => {
10462
+ saveConfig({ favoriteModels });
10463
+ },
10464
+ onGroupingChange: (modelGrouping) => {
10465
+ saveConfig({ modelGrouping });
10466
+ }
9012
10467
  });
9013
- if (pD(choice))
9014
- throw new Error("Start cancelled.");
9015
- model = String(choice);
9016
10468
  }
9017
10469
  if (!isGatewayModelAvailable(availableModels, model)) {
9018
10470
  throw new Error(`Model '${model}' is not available for ${tool} with this platform key. Agent clients require a provider that supports gateway function tools. Choose another model from ClosedRouter's model catalog.`);
9019
10471
  }
9020
- if (tool === "agy")
9021
- assertAgyGeminiMode(homedir9());
9022
- const supportedAgyModels = tool === "agy" ? discoverAgyModels(options.executablePath || "agy") : undefined;
9023
- if (supportedAgyModels && supportedAgyModels.size === 0) {
9024
- throw new Error("Antigravity did not report any Gemini API models. Update `agy` and select Gemini API key mode in its settings.");
10472
+ if (desktop && tool === "cursor") {
10473
+ const plan = buildDesktopLaunchPlan("cursor", executablePath, createDesktopProfile("cursor"), undefined, undefined, undefined, options.toolArgs);
10474
+ writeDesktopLaunchPlan(plan);
10475
+ await configureCursorDesktop(plan.profileDir, serverUrl, apiKey, model);
10476
+ M2.info(`Starting Cursor desktop with ${model}. API key, gateway URL, and model selection are configured. Profile: ${plan.profileDir}.`);
10477
+ M2.warn("Cursor's standard editor sends BYOK requests through Cursor's servers, so the gateway URL must be publicly reachable. Cursor account and feature restrictions still apply.");
10478
+ process.exitCode = await runDesktopLaunch(plan);
10479
+ return;
9025
10480
  }
9026
- const recognizedModel = supportedAgyModels ? supportedAgyModels.has("gemini-3.8-flash") ? "gemini-3.8-flash" : [...supportedAgyModels][0] : undefined;
10481
+ if (desktop && (tool === "codex" || tool === "claude" || tool === "agy")) {
10482
+ const localKey = randomUUID5();
10483
+ const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey);
10484
+ let agyDesktopProxy;
10485
+ try {
10486
+ const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs);
10487
+ if (tool === "agy") {
10488
+ agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model);
10489
+ addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl);
10490
+ }
10491
+ writeDesktopLaunchPlan(plan);
10492
+ M2.info(`Starting ${tool} desktop with ${model} through ${serverUrl}. Profile: ${plan.profileDir}. Keep cr running until you close this app instance.`);
10493
+ process.exitCode = await runDesktopLaunch(plan);
10494
+ } finally {
10495
+ await agyDesktopProxy?.close();
10496
+ await relay.close();
10497
+ }
10498
+ return;
10499
+ }
10500
+ if (tool === "agy")
10501
+ assertAgyGeminiMode(homedir13());
10502
+ const recognizedModel = tool === "agy" ? registerAgyLaunchModel(homedir13(), model, discoverAgyTransportModel(options.executablePath || "agy", apiKey)) : undefined;
10503
+ if (recognizedModel)
10504
+ assertAgyCustomModel(options.executablePath || "agy", recognizedModel, apiKey);
9027
10505
  const relay = await startLaunchProxy(serverUrl, apiKey, tool);
9028
10506
  let agyProxy;
9029
10507
  try {
9030
10508
  agyProxy = tool === "agy" ? await startAgyModelProxy(relay.baseUrl, apiKey, model) : undefined;
9031
- const plan = buildLaunchPlan(tool, relay.baseUrl, apiKey, model, options.toolArgs, homedir9(), options.executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined);
10509
+ const plan = buildLaunchPlan(tool, relay.baseUrl, apiKey, model, options.toolArgs, homedir13(), executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined);
9032
10510
  if (plan.configFile)
9033
10511
  writeOwnedConfig(plan.configFile);
9034
10512
  M2.info(`${import_picocolors12.default.green("Starting")} ${import_picocolors12.default.bold(TOOL_OPTIONS.find((option) => option.value === tool)?.label || tool)} with ${import_picocolors12.default.cyan(model)} through ${import_picocolors12.default.dim(serverUrl)}`);
@@ -9085,7 +10563,7 @@ process.on("warning", (warning) => {
9085
10563
  console.warn(warning.name, warning.message);
9086
10564
  });
9087
10565
  var program2 = new Command2;
9088
- var cliVersion = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version;
10566
+ var cliVersion = JSON.parse(readFileSync13(new URL("../package.json", import.meta.url), "utf8")).version;
9089
10567
  program2.name("closedrouter").description("ClosedRouter CLI: Import local AI subscriptions & API keys into your shared token pool").version(cliVersion).option("-s, --server <url>", "ClosedRouter server base URL (default: http://localhost:8000)").action(async (options) => {
9090
10568
  await onboardCommand({ server: options.server });
9091
10569
  });
@@ -9133,14 +10611,20 @@ program2.command("gateway").alias("daemon").alias("bridge").description("Start t
9133
10611
  claudePath: options.claudePath
9134
10612
  });
9135
10613
  });
9136
- program2.command("start [tool] [tool-args...]").alias("launch").description("Configure and start a coding CLI through the ClosedRouter gateway").option("-s, --server <url>", "ClosedRouter server base URL").option("-k, --key <key>", "Platform API key; otherwise use the saved CLI key").option("-m, --model <model>", "Gateway model to use").option("--path <path>", "Path to the installed tool executable").allowUnknownOption().action(async (tool, toolArgs, options) => {
10614
+ program2.command("settings").description("Configure the CLI model selector").option("--model-grouping <grouping>", "Group models by model-provider, account-provider, or none").action(async (options) => {
10615
+ await settingsCommand({ modelGrouping: options.modelGrouping });
10616
+ });
10617
+ program2.command("start [tool] [tool-args...]").alias("launch").description("Configure and start a coding CLI or desktop app through ClosedRouter").option("--desktop", "Launch the desktop app for codex, claude, cursor, or antigravity").option("--open-only", "Open an isolated desktop app without gateway configuration").option("-s, --server <url>", "ClosedRouter server base URL").option("-k, --key <key>", "Platform API key; otherwise use the saved CLI key").option("-m, --model <model>", "Gateway model to use").option("--group-by <grouping>", "Override model grouping for this launch: model-provider, account-provider, or none").option("--path <path>", "Path to the installed CLI, desktop executable, or macOS app bundle").allowUnknownOption().action(async (tool, toolArgs, options) => {
9137
10618
  await startCommand({
9138
10619
  tool,
9139
10620
  toolArgs: toolArgs || [],
9140
10621
  server: options.server || program2.opts().server,
9141
10622
  key: options.key,
9142
10623
  model: options.model,
9143
- executablePath: options.path
10624
+ groupBy: options.groupBy,
10625
+ executablePath: options.path,
10626
+ desktop: options.desktop,
10627
+ openOnly: options.openOnly
9144
10628
  });
9145
10629
  });
9146
10630
  program2.parseAsync(process.argv).catch((error) => {