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/*
|
.next-analyze/*
|
||||||
|
|
||||||
# Kiro local workspace state
|
# Kiro local workspace state
|
||||||
.kiro/
|
.kiro/
|
||||||
|
9router-*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router",
|
"name": "9router",
|
||||||
"version": "0.5.69",
|
"version": "0.5.75",
|
||||||
"description": "9Router CLI - Start and manage 9Router server",
|
"description": "9Router CLI - Start and manage 9Router server",
|
||||||
"bin": {
|
"bin": {
|
||||||
"9router": "./cli.js"
|
"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} title - Title to display
|
||||||
* @param {string} currentValue - Current selected value (optional)
|
* @param {string} currentValue - Current selected value (optional)
|
||||||
* @param {Object} options - { excludeCombos?: boolean }
|
* @param {Object} options - { excludeCombos?: boolean }
|
||||||
@@ -70,62 +70,199 @@ async function selectModelFromList(title, currentValue = "", options = {}) {
|
|||||||
if (totalModels === 0) {
|
if (totalModels === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build flat list for selection
|
// All models for flat search
|
||||||
const allModels = [];
|
const allModelsList = [
|
||||||
|
...combos,
|
||||||
// Display
|
...Object.values(groups).flat()
|
||||||
clearScreen();
|
];
|
||||||
console.log(`\n🎯 ${title}`);
|
|
||||||
console.log("=".repeat(50));
|
// Build category list
|
||||||
if (currentValue) {
|
const categories = [];
|
||||||
console.log(`Current: ${currentValue}\n`);
|
|
||||||
} else {
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
let idx = 1;
|
|
||||||
|
|
||||||
// Combos first (skipped when excludeCombos is true)
|
|
||||||
if (combos.length > 0) {
|
if (combos.length > 0) {
|
||||||
console.log("[Combos]");
|
categories.push({
|
||||||
combos.forEach(combo => {
|
id: "combos",
|
||||||
console.log(` ${idx}. ${combo}`);
|
name: "[Combos]",
|
||||||
allModels.push(combo);
|
models: combos
|
||||||
idx++;
|
|
||||||
});
|
});
|
||||||
console.log();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider groups in order (by alias)
|
|
||||||
const sortedProviders = Object.keys(groups).sort((a, b) => {
|
const sortedProviders = Object.keys(groups).sort((a, b) => {
|
||||||
const idxA = PROVIDER_ALIAS_ORDER.indexOf(a);
|
const idxA = PROVIDER_ALIAS_ORDER.indexOf(a);
|
||||||
const idxB = PROVIDER_ALIAS_ORDER.indexOf(b);
|
const idxB = PROVIDER_ALIAS_ORDER.indexOf(b);
|
||||||
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB);
|
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB);
|
||||||
});
|
});
|
||||||
|
|
||||||
sortedProviders.forEach(provider => {
|
sortedProviders.forEach((provider) => {
|
||||||
const providerName = PROVIDER_ALIAS_NAMES[provider] || provider;
|
const providerName = PROVIDER_ALIAS_NAMES[provider] || provider;
|
||||||
console.log(`[${providerName}]`);
|
categories.push({
|
||||||
groups[provider].forEach(model => {
|
id: provider,
|
||||||
console.log(` ${idx}. ${model}`);
|
name: providerName,
|
||||||
allModels.push(model);
|
models: groups[provider]
|
||||||
idx++;
|
|
||||||
});
|
});
|
||||||
console.log();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(" 0. Cancel\n");
|
let filterQuery = null;
|
||||||
|
|
||||||
// Prompt for number input
|
while (true) {
|
||||||
const input = await prompt("Enter number: ");
|
clearScreen();
|
||||||
const num = parseInt(input, 10);
|
console.log(`\n🎯 ${title}`);
|
||||||
|
console.log("=".repeat(50));
|
||||||
if (isNaN(num) || num === 0 || num < 0 || num > allModels.length) {
|
if (currentValue) {
|
||||||
return null;
|
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 = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.5.69",
|
"version": "0.5.75",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user