feat(cli): group model selector by provider with search
Replace the flat numbered model list with provider-grouped browsing (combos first, then providers by alias order), full-text search across all models, and manual custom model ID entry. A single available category opens directly into its model list. Also bump root and cli packages to 0.5.75 and ignore packed `9router-*` tarballs. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -88,4 +88,5 @@ graphify-out/*
|
||||
.next-analyze/*
|
||||
|
||||
# Kiro local workspace state
|
||||
.kiro/
|
||||
.kiro/
|
||||
9router-*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "9router",
|
||||
"version": "0.5.69",
|
||||
"version": "0.5.75",
|
||||
"description": "9Router CLI - Start and manage 9Router server",
|
||||
"bin": {
|
||||
"9router": "./cli.js"
|
||||
|
||||
@@ -55,7 +55,7 @@ async function getAvailableModelsGrouped() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display model list and prompt for selection
|
||||
* Display model list and prompt for selection with provider grouping & search
|
||||
* @param {string} title - Title to display
|
||||
* @param {string} currentValue - Current selected value (optional)
|
||||
* @param {Object} options - { excludeCombos?: boolean }
|
||||
@@ -70,62 +70,199 @@ async function selectModelFromList(title, currentValue = "", options = {}) {
|
||||
if (totalModels === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build flat list for selection
|
||||
const allModels = [];
|
||||
|
||||
// Display
|
||||
clearScreen();
|
||||
console.log(`\n🎯 ${title}`);
|
||||
console.log("=".repeat(50));
|
||||
if (currentValue) {
|
||||
console.log(`Current: ${currentValue}\n`);
|
||||
} else {
|
||||
console.log();
|
||||
}
|
||||
|
||||
let idx = 1;
|
||||
|
||||
// Combos first (skipped when excludeCombos is true)
|
||||
|
||||
// All models for flat search
|
||||
const allModelsList = [
|
||||
...combos,
|
||||
...Object.values(groups).flat()
|
||||
];
|
||||
|
||||
// Build category list
|
||||
const categories = [];
|
||||
if (combos.length > 0) {
|
||||
console.log("[Combos]");
|
||||
combos.forEach(combo => {
|
||||
console.log(` ${idx}. ${combo}`);
|
||||
allModels.push(combo);
|
||||
idx++;
|
||||
categories.push({
|
||||
id: "combos",
|
||||
name: "[Combos]",
|
||||
models: combos
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Provider groups in order (by alias)
|
||||
|
||||
const sortedProviders = Object.keys(groups).sort((a, b) => {
|
||||
const idxA = PROVIDER_ALIAS_ORDER.indexOf(a);
|
||||
const idxB = PROVIDER_ALIAS_ORDER.indexOf(b);
|
||||
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB);
|
||||
});
|
||||
|
||||
sortedProviders.forEach(provider => {
|
||||
|
||||
sortedProviders.forEach((provider) => {
|
||||
const providerName = PROVIDER_ALIAS_NAMES[provider] || provider;
|
||||
console.log(`[${providerName}]`);
|
||||
groups[provider].forEach(model => {
|
||||
console.log(` ${idx}. ${model}`);
|
||||
allModels.push(model);
|
||||
idx++;
|
||||
categories.push({
|
||||
id: provider,
|
||||
name: providerName,
|
||||
models: groups[provider]
|
||||
});
|
||||
console.log();
|
||||
});
|
||||
|
||||
console.log(" 0. Cancel\n");
|
||||
|
||||
// Prompt for number input
|
||||
const input = await prompt("Enter number: ");
|
||||
const num = parseInt(input, 10);
|
||||
|
||||
if (isNaN(num) || num === 0 || num < 0 || num > allModels.length) {
|
||||
return null;
|
||||
|
||||
let filterQuery = null;
|
||||
|
||||
while (true) {
|
||||
clearScreen();
|
||||
console.log(`\n🎯 ${title}`);
|
||||
console.log("=".repeat(50));
|
||||
if (currentValue) {
|
||||
console.log(`Current: ${currentValue}\n`);
|
||||
} else {
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Active search view
|
||||
if (filterQuery !== null) {
|
||||
const q = filterQuery.toLowerCase().trim();
|
||||
const matched = allModelsList.filter((m) => m.toLowerCase().includes(q));
|
||||
|
||||
console.log(`🔍 Search results for "${filterQuery}": (${matched.length} found)\n`);
|
||||
if (matched.length === 0) {
|
||||
console.log(" No matching models found.\n");
|
||||
console.log(" 0. ← Back to providers");
|
||||
console.log(" s. Search again\n");
|
||||
const act = await prompt("Select option: ");
|
||||
if (act.toLowerCase() === "s") {
|
||||
const newQ = await prompt("Enter search keyword: ");
|
||||
filterQuery = newQ.trim() || null;
|
||||
} else {
|
||||
filterQuery = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
matched.forEach((m, i) => {
|
||||
console.log(` ${i + 1}. ${m}`);
|
||||
});
|
||||
console.log("\n 0. ← Back to providers");
|
||||
console.log(" s. Search again\n");
|
||||
|
||||
const input = await prompt("Enter number to select (or 0/s): ");
|
||||
if (input.toLowerCase() === "s") {
|
||||
const newQ = await prompt("Enter search keyword: ");
|
||||
filterQuery = newQ.trim() || null;
|
||||
continue;
|
||||
}
|
||||
const num = parseInt(input, 10);
|
||||
if (isNaN(num) || num === 0) {
|
||||
filterQuery = null;
|
||||
continue;
|
||||
}
|
||||
if (num > 0 && num <= matched.length) {
|
||||
return matched[num - 1];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If only 1 category exists, jump straight into its model list
|
||||
if (categories.length === 1) {
|
||||
const singleCategory = categories[0];
|
||||
console.log(`[${singleCategory.name}]`);
|
||||
singleCategory.models.forEach((m, i) => {
|
||||
console.log(` ${i + 1}. ${m}`);
|
||||
});
|
||||
console.log();
|
||||
console.log(" s. 🔍 Search models");
|
||||
console.log(" m. ✍️ Enter custom model ID");
|
||||
console.log(" 0. Cancel\n");
|
||||
|
||||
const input = await prompt("Enter choice (number / s / m / 0): ");
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed || trimmed === "0") return null;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (lower === "s") {
|
||||
const q = await prompt("Enter search keyword: ");
|
||||
if (q.trim()) filterQuery = q.trim();
|
||||
continue;
|
||||
}
|
||||
if (lower === "m") {
|
||||
const customModel = await prompt("Enter custom model ID: ");
|
||||
if (customModel.trim()) return customModel.trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
const num = parseInt(trimmed, 10);
|
||||
if (!isNaN(num) && num > 0 && num <= singleCategory.models.length) {
|
||||
return singleCategory.models[num - 1];
|
||||
}
|
||||
filterQuery = trimmed;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiple categories view
|
||||
console.log("[Providers & Groups]");
|
||||
categories.forEach((cat, i) => {
|
||||
console.log(` ${i + 1}. ${cat.name} (${cat.models.length} models)`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(" s. 🔍 Search models");
|
||||
console.log(" m. ✍️ Enter custom model ID");
|
||||
console.log(" 0. Cancel\n");
|
||||
|
||||
const input = await prompt("Enter choice (number / keyword / s / m): ");
|
||||
const trimmed = input.trim();
|
||||
|
||||
if (!trimmed || trimmed === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (lower === "s") {
|
||||
const q = await prompt("Enter search keyword: ");
|
||||
if (q.trim()) {
|
||||
filterQuery = q.trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lower === "m") {
|
||||
const customModel = await prompt("Enter custom model ID: ");
|
||||
if (customModel.trim()) {
|
||||
return customModel.trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const num = parseInt(trimmed, 10);
|
||||
// Selected a category
|
||||
if (!isNaN(num) && num > 0 && num <= categories.length) {
|
||||
const selectedCategory = categories[num - 1];
|
||||
|
||||
while (true) {
|
||||
clearScreen();
|
||||
console.log(`\n🎯 ${title} > ${selectedCategory.name}`);
|
||||
console.log("=".repeat(50));
|
||||
if (currentValue) {
|
||||
console.log(`Current: ${currentValue}\n`);
|
||||
} else {
|
||||
console.log();
|
||||
}
|
||||
|
||||
selectedCategory.models.forEach((m, i) => {
|
||||
console.log(` ${i + 1}. ${m}`);
|
||||
});
|
||||
console.log("\n 0. ← Back\n");
|
||||
|
||||
const modelChoice = await prompt("Enter number to select (0 to back): ");
|
||||
const modelNum = parseInt(modelChoice, 10);
|
||||
if (isNaN(modelNum) || modelNum === 0) {
|
||||
break;
|
||||
}
|
||||
if (modelNum > 0 && modelNum <= selectedCategory.models.length) {
|
||||
return selectedCategory.models[modelNum - 1];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// User typed text directly -> treat as search query
|
||||
filterQuery = trimmed;
|
||||
}
|
||||
|
||||
return allModels[num - 1];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "9router-app",
|
||||
"version": "0.5.69",
|
||||
"version": "0.5.75",
|
||||
"description": "9Router web dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user