From b8a38452446df4cf3090920b2547ac015c1c85fd Mon Sep 17 00:00:00 2001 From: TonyGeez Date: Sun, 5 Oct 2025 18:18:15 -0400 Subject: [PATCH] Add model selector command --- package.json | 3 +- pnpm-lock.yaml | 271 ++++++++++++++++++++++ pnpm-workspace.yaml | 2 + src/cli.ts | 9 +- src/utils/modelSelector.ts | 462 +++++++++++++++++++++++++++++++++++++ ui/package-lock.json | 139 +++++++++++ ui/tsconfig.tsbuildinfo | 2 +- 7 files changed, 885 insertions(+), 3 deletions(-) create mode 100644 pnpm-workspace.yaml create mode 100644 src/utils/modelSelector.ts diff --git a/package.json b/package.json index c2b2371..25fbf6e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "rotating-file-stream": "^3.2.7", "shell-quote": "^1.8.3", "tiktoken": "^1.0.21", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "@inquirer/prompts": "^5.0.0" }, "devDependencies": { "@types/node": "^24.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33f63b2..d34a372 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fastify/static': specifier: ^8.2.0 version: 8.2.0 + '@inquirer/prompts': + specifier: ^5.0.0 + version: 5.5.0 '@musistudio/llms': specifier: ^1.0.35 version: 1.0.35(ws@8.18.3) @@ -259,6 +262,66 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@inquirer/checkbox@2.5.0': + resolution: {integrity: sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==} + engines: {node: '>=18'} + + '@inquirer/confirm@3.2.0': + resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} + engines: {node: '>=18'} + + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} + engines: {node: '>=18'} + + '@inquirer/editor@2.2.0': + resolution: {integrity: sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==} + engines: {node: '>=18'} + + '@inquirer/expand@2.3.0': + resolution: {integrity: sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} + engines: {node: '>=18'} + + '@inquirer/input@2.3.0': + resolution: {integrity: sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==} + engines: {node: '>=18'} + + '@inquirer/number@1.1.0': + resolution: {integrity: sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==} + engines: {node: '>=18'} + + '@inquirer/password@2.2.0': + resolution: {integrity: sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==} + engines: {node: '>=18'} + + '@inquirer/prompts@5.5.0': + resolution: {integrity: sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog==} + engines: {node: '>=18'} + + '@inquirer/rawlist@2.3.0': + resolution: {integrity: sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==} + engines: {node: '>=18'} + + '@inquirer/search@1.1.0': + resolution: {integrity: sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==} + engines: {node: '>=18'} + + '@inquirer/select@2.5.0': + resolution: {integrity: sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==} + engines: {node: '>=18'} + + '@inquirer/type@1.5.5': + resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} + engines: {node: '>=18'} + + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} + engines: {node: '>=18'} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -290,9 +353,18 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + + '@types/node@22.18.8': + resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} + '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -311,6 +383,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -351,6 +427,13 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -433,6 +516,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -561,6 +648,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -669,6 +760,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -716,6 +811,10 @@ packages: openurl@1.1.1: resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -812,6 +911,9 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + secure-json-parse@4.0.0: resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} @@ -912,6 +1014,10 @@ packages: tiktoken@1.0.22: resolution: {integrity: sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -927,11 +1033,18 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} @@ -966,6 +1079,10 @@ packages: engines: {node: '>= 8'} hasBin: true + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -989,6 +1106,10 @@ packages: utf-8-validate: optional: true + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + snapshots: '@anthropic-ai/sdk@0.54.0': {} @@ -1128,6 +1249,106 @@ snapshots: - supports-color - utf-8-validate + '@inquirer/checkbox@2.5.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.13 + '@inquirer/type': 1.5.5 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/confirm@3.2.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + + '@inquirer/core@9.2.1': + dependencies: + '@inquirer/figures': 1.0.13 + '@inquirer/type': 2.0.0 + '@types/mute-stream': 0.0.4 + '@types/node': 22.18.8 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/editor@2.2.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + external-editor: 3.1.0 + + '@inquirer/expand@2.3.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + yoctocolors-cjs: 2.1.3 + + '@inquirer/figures@1.0.13': {} + + '@inquirer/input@2.3.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + + '@inquirer/number@1.1.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + + '@inquirer/password@2.2.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + ansi-escapes: 4.3.2 + + '@inquirer/prompts@5.5.0': + dependencies: + '@inquirer/checkbox': 2.5.0 + '@inquirer/confirm': 3.2.0 + '@inquirer/editor': 2.2.0 + '@inquirer/expand': 2.3.0 + '@inquirer/input': 2.3.0 + '@inquirer/number': 1.1.0 + '@inquirer/password': 2.2.0 + '@inquirer/rawlist': 2.3.0 + '@inquirer/search': 1.1.0 + '@inquirer/select': 2.5.0 + + '@inquirer/rawlist@2.3.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + yoctocolors-cjs: 2.1.3 + + '@inquirer/search@1.1.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.13 + '@inquirer/type': 1.5.5 + yoctocolors-cjs: 2.1.3 + + '@inquirer/select@2.5.0': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.13 + '@inquirer/type': 1.5.5 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/type@1.5.5': + dependencies: + mute-stream: 1.0.0 + + '@inquirer/type@2.0.0': + dependencies: + mute-stream: 1.0.0 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -1179,10 +1400,20 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 24.3.0 + + '@types/node@22.18.8': + dependencies: + undici-types: 6.21.0 + '@types/node@24.3.0': dependencies: undici-types: 7.10.0 + '@types/wrap-ansi@3.0.0': {} + abstract-logging@2.0.1: {} agent-base@7.1.4: {} @@ -1198,6 +1429,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -1230,6 +1465,10 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chardet@0.7.0: {} + + cli-width@4.1.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -1327,6 +1566,12 @@ snapshots: extend@3.0.2: {} + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -1529,6 +1774,10 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + inherits@2.0.4: {} interpret@1.4.0: {} @@ -1613,6 +1862,8 @@ snapshots: ms@2.1.3: {} + mute-stream@1.0.0: {} + nice-try@1.0.5: {} node-domexception@1.0.0: {} @@ -1643,6 +1894,8 @@ snapshots: openurl@1.1.1: {} + os-tmpdir@1.0.2: {} + p-finally@1.0.0: {} package-json-from-dist@1.0.1: {} @@ -1727,6 +1980,8 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + secure-json-parse@4.0.0: {} semver@5.7.2: {} @@ -1809,6 +2064,10 @@ snapshots: tiktoken@1.0.22: {} + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -1819,8 +2078,12 @@ snapshots: tr46@0.0.3: {} + type-fest@0.21.3: {} + typescript@5.9.2: {} + undici-types@6.21.0: {} + undici-types@7.10.0: {} undici@7.16.0: {} @@ -1846,6 +2109,12 @@ snapshots: dependencies: isexe: 2.0.0 + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -1861,3 +2130,5 @@ snapshots: wrappy@1.0.2: {} ws@8.18.3: {} + + yoctocolors-cjs@2.1.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..c5739b7 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +ignoredBuiltDependencies: + - esbuild diff --git a/src/cli.ts b/src/cli.ts index 7e9692f..b857e9d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,7 @@ import { isServiceRunning, getServiceInfo, } from "./utils/processCheck"; +import { runModelSelector } from "./utils/modelSelector"; // ADD THIS LINE import { version } from "../package.json"; import { spawn, exec } from "child_process"; import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants"; @@ -26,6 +27,7 @@ Commands: status Show server status statusline Integrated statusline code Execute claude command + model Interactive model selection and configuration ui Open the web UI in browser -v, version Show version information -h, help Show help information @@ -33,6 +35,7 @@ Commands: Example: ccr start ccr code "Write a Hello World" + ccr model ccr ui `; @@ -109,6 +112,10 @@ async function main() { } }); break; + // ADD THIS CASE + case "model": + await runModelSelector(); + break; case "code": if (!isRunning) { console.log("Service not running, starting service..."); @@ -321,4 +328,4 @@ async function main() { } } -main().catch(console.error); +main().catch(console.error); \ No newline at end of file diff --git a/src/utils/modelSelector.ts b/src/utils/modelSelector.ts new file mode 100644 index 0000000..7c85742 --- /dev/null +++ b/src/utils/modelSelector.ts @@ -0,0 +1,462 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { select, input, confirm } from '@inquirer/prompts'; + +// ANSI color codes +const RESET = "\x1B[0m"; +const DIM = "\x1B[2m"; +const BOLDGREEN = "\x1B[1m\x1B[32m"; +const CYAN = "\x1B[36m"; +const BOLDCYAN = "\x1B[1m\x1B[36m"; +const GREEN = "\x1B[32m"; +const YELLOW = "\x1B[33m"; +const BOLDYELLOW = "\x1B[1m\x1B[33m"; + +interface TransformerConfig { + use: Array; + [key: string]: any; +} + +interface Provider { + name: string; + api_base_url: string; + api_key: string; + models: string[]; + transformer?: TransformerConfig; +} + +interface RouterConfig { + default: string; + background?: string; + think?: string; + longContext?: string; + longContextThreshold?: number; + webSearch?: string; + image?: string; + [key: string]: string | number | undefined; +} + +interface Config { + Providers: Provider[]; + Router: RouterConfig; + [key: string]: any; +} + +interface ModelResult { + providerName: string; + modelName: string; + modelType: string; +} + +const AVAILABLE_TRANSFORMERS = [ + 'anthropic', + 'deepseek', + 'gemini', + 'openrouter', + 'groq', + 'maxtoken', + 'tooluse', + 'gemini-cli', + 'reasoning', + 'sampling', + 'enhancetool', + 'cleancache', + 'vertex-gemini', + 'chutes-glm', + 'qwen-cli', + 'rovo-cli' +]; + +function getConfigPath(): string { + const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude-code-router'); + const configPath = path.join(configDir, 'config.json'); + + if (!fs.existsSync(configPath)) { + throw new Error(`config.json not found at ${configPath}`); + } + + return configPath; +} + +function loadConfig(): Config { + const configPath = getConfigPath(); + return JSON.parse(fs.readFileSync(configPath, 'utf-8')); +} + +function saveConfig(config: Config): void { + const configPath = getConfigPath(); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); + console.log(`${GREEN}✓ config.json updated successfully${RESET}\n`); +} + +function getAllModels(config: Config) { + const models: any[] = []; + for (const provider of config.Providers) { + for (const model of provider.models) { + models.push({ + name: `${BOLDCYAN}${provider.name}${RESET} → ${CYAN} ${model}`, + value: `${provider.name},${model}`, + description: `\n${BOLDCYAN}Provider:${RESET} ${provider.name}`, + provider: provider.name, + model: model + }); + } + } + return models; +} + +function displayCurrentConfig(config: Config): void { + console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`); + console.log(`${BOLDCYAN} Current Configuration${RESET}`); + console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`); + + const formatModel = (routerValue?: string | number) => { + if (!routerValue || typeof routerValue === 'number') { + return `${DIM}Not configured${RESET}`; + } + const [provider, model] = routerValue.split(','); + return `${YELLOW}${provider}${RESET} | ${model}\n ${DIM}- ${routerValue}${RESET}`; + }; + + console.log(`${BOLDCYAN}Default Model:${RESET}`); + console.log(` ${formatModel(config.Router.default)}\n`); + + if (config.Router.background) { + console.log(`${BOLDCYAN}Background Model:${RESET}`); + console.log(` ${formatModel(config.Router.background)}\n`); + } + + if (config.Router.think) { + console.log(`${BOLDCYAN}Think Model:${RESET}`); + console.log(` ${formatModel(config.Router.think)}\n`); + } + + if (config.Router.longContext) { + console.log(`${BOLDCYAN}Long Context Model:${RESET}`); + console.log(` ${formatModel(config.Router.longContext)}\n`); + } + + if (config.Router.webSearch) { + console.log(`${BOLDCYAN}Web Search Model:${RESET}`); + console.log(` ${formatModel(config.Router.webSearch)}\n`); + } + + if (config.Router.image) { + console.log(`${BOLDCYAN}Image Model:${RESET}`); + console.log(` ${formatModel(config.Router.image)}\n`); + } + + console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`); + console.log(`${BOLDCYAN} Add/Update Model${RESET}`); + console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`); +} + +async function selectModelType() { + return await select({ + message: `${BOLDYELLOW}Which model configuration do you want to update?${RESET}`, + choices: [ + { name: 'Default Model', value: 'default' }, + { name: 'Background Model', value: 'background' }, + { name: 'Think Model', value: 'think' }, + { name: 'Long Context Model', value: 'longContext' }, + { name: 'Web Search Model', value: 'webSearch' }, + { name: 'Image Model', value: 'image' }, + { name: `${BOLDGREEN}+ Add New Model${RESET}`, value: 'addModel' } + ] + }); +} + +async function selectModel(config: Config, modelType: string) { + const models = getAllModels(config); + + return await select({ + message: `\n${BOLDYELLOW}Select a model for ${modelType}:${RESET}`, + choices: models, + pageSize: 15 + }); +} + +async function configureTransformers(): Promise { + const useTransformers = await confirm({ + message: `\n${BOLDYELLOW}Add transformer configuration?${RESET}`, + default: false + }); + + if (!useTransformers) { + return undefined; + } + + const transformers: Array = []; + let addMore = true; + + while (addMore) { + const transformer = await select({ + message: `\n${BOLDYELLOW}Select a transformer:${RESET}`, + choices: AVAILABLE_TRANSFORMERS.map(t => ({ name: t, value: t })), + pageSize: 15 + }) as string; + + // Check if transformer needs options + if (transformer === 'maxtoken') { + const maxTokens = await input({ + message: `\n${BOLDYELLOW}Max tokens:${RESET}`, + default: '30000', + validate: (value: string) => { + const num = parseInt(value); + if (isNaN(num) || num <= 0) { + return 'Please enter a valid positive number'; + } + return true; + } + }); + transformers.push(['maxtoken', { max_tokens: parseInt(maxTokens) }]); + } else if (transformer === 'openrouter') { + const addProvider = await confirm({ + message: `\n${BOLDYELLOW}Add provider routing options?${RESET}`, + default: false + }); + + if (addProvider) { + const providerInput = await input({ + message: 'Provider (e.g., moonshotai/fp8):', + validate: (value: string) => value.trim() !== '' || 'Provider cannot be empty' + }); + transformers.push(['openrouter', { provider: { only: [providerInput] } }]); + } else { + transformers.push(transformer); + } + } else { + transformers.push(transformer); + } + + addMore = await confirm({ + message: `\n${BOLDYELLOW}Add another transformer?${RESET}`, + default: false + }); + } + + return { use: transformers }; +} + +async function addNewModel(config: Config): Promise { + const providerChoices = config.Providers.map(p => ({ + name: p.name, + value: p.name + })); + + providerChoices.push({ name: `${BOLDGREEN}+ Add New Provider${RESET}`, value: '__new__' }); + + const selectedProvider = await select({ + message: `\n${BOLDYELLOW}Select provider for the new model:${RESET}`, + choices: providerChoices + }) as string; + + if (selectedProvider === '__new__') { + return await addNewProvider(config); + } else { + return await addModelToExistingProvider(config, selectedProvider); + } +} + +async function addModelToExistingProvider(config: Config, providerName: string): Promise { + const modelName = await input({ + message: `\n${BOLDYELLOW}Enter the model name:${RESET}`, + validate: (value: string) => { + if (!value.trim()) { + return 'Model name cannot be empty'; + } + return true; + } + }); + + const provider = config.Providers.find(p => p.name === providerName); + + if (!provider) { + console.log(`${YELLOW}Provider not found${RESET}`); + return null; + } + + if (provider.models.includes(modelName)) { + console.log(`${YELLOW}Model already exists in provider${RESET}`); + return null; + } + + provider.models.push(modelName); + + // Ask about model-specific transformers + const addModelTransformer = await confirm({ + message: `\n${BOLDYELLOW}Add model-specific transformer configuration?${RESET}`, + default: false + }); + + if (addModelTransformer) { + const transformerConfig = await configureTransformers(); + if (transformerConfig && provider.transformer) { + provider.transformer[modelName] = transformerConfig; + } + } + + saveConfig(config); + + console.log(`${GREEN}✓ Model "${modelName}" added to provider "${providerName}"${RESET}`); + + const setAsDefault = await confirm({ + message: `\n${BOLDYELLOW}Do you want to set this model in router configuration?${RESET}`, + default: false + }); + + if (setAsDefault) { + const modelType = await select({ + message: `\n${BOLDYELLOW}Select configuration type:${RESET}`, + choices: [ + { name: 'Default Model', value: 'default' }, + { name: 'Background Model', value: 'background' }, + { name: 'Think Model', value: 'think' }, + { name: 'Long Context Model', value: 'longContext' }, + { name: 'Web Search Model', value: 'webSearch' }, + { name: 'Image Model', value: 'image' } + ] + }) as string; + + return { providerName, modelName, modelType }; + } + + return null; +} + +async function addNewProvider(config: Config): Promise { + console.log(`\n${BOLDCYAN}Adding New Provider${RESET}\n`); + + const providerName = await input({ + message: `${BOLDYELLOW}Provider name:${RESET}`, + validate: (value: string) => { + if (!value.trim()) { + return 'Provider name cannot be empty'; + } + if (config.Providers.some(p => p.name === value)) { + return 'Provider already exists'; + } + return true; + } + }); + + const apiBaseUrl = await input({ + message: `\n${BOLDYELLOW}API base URL:${RESET}`, + validate: (value: string) => { + if (!value.trim()) { + return 'API base URL cannot be empty'; + } + try { + new URL(value); + return true; + } catch { + return 'Please enter a valid URL'; + } + } + }); + + const apiKey = await input({ + message: `\n${BOLDYELLOW}API key:${RESET}`, + validate: (value: string) => { + if (!value.trim()) { + return 'API key cannot be empty'; + } + return true; + } + }); + + const modelsInput = await input({ + message: `\n${BOLDYELLOW}Model names (comma-separated):${RESET}`, + validate: (value: string) => { + if (!value.trim()) { + return 'At least one model name is required'; + } + return true; + } + }); + + const models = modelsInput.split(',').map(m => m.trim()).filter(m => m); + + const newProvider: Provider = { + name: providerName, + api_base_url: apiBaseUrl, + api_key: apiKey, + models: models + }; + + // Global transformer configuration + const transformerConfig = await configureTransformers(); + if (transformerConfig) { + newProvider.transformer = transformerConfig; + } + + config.Providers.push(newProvider); + saveConfig(config); + + console.log(`${GREEN}\n✓ Provider "${providerName}" added successfully${RESET}`); + + const setAsDefault = await confirm({ + message: `\n${BOLDYELLOW}Do you want to set one of these models in router configuration?${RESET}`, + default: false + }); + + if (setAsDefault && models.length > 0) { + let selectedModel = models[0]; + + if (models.length > 1) { + selectedModel = await select({ + message: `\n${BOLDYELLOW}Select which model to configure:${RESET}`, + choices: models.map(m => ({ name: m, value: m })) + }) as string; + } + + const modelType = await select({ + message: `\n${BOLDYELLOW}Select configuration type:${RESET}`, + choices: [ + { name: 'Default Model', value: 'default' }, + { name: 'Background Model', value: 'background' }, + { name: 'Think Model', value: 'think' }, + { name: 'Long Context Model', value: 'longContext' }, + { name: 'Web Search Model', value: 'webSearch' }, + { name: 'Image Model', value: 'image' } + ] + }) as string; + + return { providerName, modelName: selectedModel, modelType }; + } + + return null; +} + +export async function runModelSelector(): Promise { + console.clear(); + + try { + let config = loadConfig(); + displayCurrentConfig(config); + + const action = await selectModelType() as string; + + if (action === 'addModel') { + const result = await addNewModel(config); + + if (result) { + config = loadConfig(); + config.Router[result.modelType] = `${result.providerName},${result.modelName}`; + saveConfig(config); + console.log(`${GREEN}✓ ${result.modelType} set to ${result.providerName},${result.modelName}${RESET}`); + } + } else { + const selectedModel = await selectModel(config, action) as string; + config.Router[action] = selectedModel; + saveConfig(config); + + console.log(`${GREEN}✓ ${action} model updated to: ${selectedModel}${RESET}`); + } + + displayCurrentConfig(config); + } catch (error: any) { + console.error(`${YELLOW}Error:${RESET}`, error.message); + process.exit(1); + } +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index e558a47..c01453b 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/vite": "^4.1.11", "class-variance-authority": "^0.7.1", @@ -1181,6 +1182,32 @@ } } }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1247,6 +1274,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", @@ -1495,6 +1537,43 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1542,6 +1621,66 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo index 28cd675..3df91f4 100644 --- a/ui/tsconfig.tsbuildinfo +++ b/ui/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/debugpage.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/requesthistorydrawer.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/ConfigProvider.tsx","./src/components/DebugPage.tsx","./src/components/JsonEditor.tsx","./src/components/LogViewer.tsx","./src/components/Login.tsx","./src/components/ProtectedRoute.tsx","./src/components/ProviderList.tsx","./src/components/Providers.tsx","./src/components/PublicRoute.tsx","./src/components/RequestHistoryDrawer.tsx","./src/components/Router.tsx","./src/components/SettingsDialog.tsx","./src/components/StatusLineConfigDialog.tsx","./src/components/StatusLineImportExport.tsx","./src/components/TransformerList.tsx","./src/components/Transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"} \ No newline at end of file