fix: update cli tools

This commit is contained in:
2026-07-12 22:44:11 +07:00
parent 43a0c90dac
commit 330eb4d936
37 changed files with 441 additions and 8073 deletions
-37
View File
@@ -357,38 +357,6 @@ async function deleteCombo(id) {
return makeRequest("DELETE", `/api/combos/${id}`);
}
// ============================================================================
// CLI TOOLS API
// ============================================================================
/**
* Get CLI tool settings
* @param {string} tool - Tool name: claude | codex | droid | openclaw
* @returns {Promise<Object>} { success, data: { installed, has9Router, ... } }
*/
async function getCliToolSettings(tool) {
return makeRequest("GET", `/api/cli-tools/${tool}-settings`);
}
/**
* Apply CLI tool settings (POST)
* @param {string} tool - Tool name: claude | codex | droid | openclaw
* @param {Object} body - Payload depends on tool
* @returns {Promise<Object>} { success, data }
*/
async function applyCliToolSettings(tool, body) {
return makeRequest("POST", `/api/cli-tools/${tool}-settings`, body);
}
/**
* Reset CLI tool settings (DELETE)
* @param {string} tool - Tool name: claude | codex | droid | openclaw
* @returns {Promise<Object>} { success, data }
*/
async function resetCliToolSettings(tool) {
return makeRequest("DELETE", `/api/cli-tools/${tool}-settings`);
}
// ============================================================================
// SETTINGS API
// ============================================================================
@@ -528,11 +496,6 @@ module.exports = {
updateCombo,
deleteCombo,
// CLI Tools
getCliToolSettings,
applyCliToolSettings,
resetCliToolSettings,
// Settings
getSettings,
updateSettings,
-618
View File
@@ -1,618 +0,0 @@
const api = require("../api/client");
const { pause, confirm } = require("../utils/input");
const { showStatus } = require("../utils/display");
const { selectModelFromList } = require("../utils/modelSelector");
const { showMenuWithBack } = require("../utils/menuHelper");
const { getEndpoint } = require("../utils/endpoint");
const COLORS = {
reset: "\x1b[0m",
green: "\x1b[32m",
red: "\x1b[31m",
dim: "\x1b[2m",
cyan: "\x1b[36m"
};
// Claude model types with defaults (matching Web UI)
const CLAUDE_MODEL_TYPES = [
{ id: "sonnet", name: "Sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-4-5-20250929" },
{ id: "opus", name: "Opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-5-20251101" },
{ id: "haiku", name: "Haiku", envKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL", defaultValue: "cc/claude-haiku-4-5-20251001" },
];
// ─── Shared helpers ───────────────────────────────────────────────────────────
/**
* Get first available API key from server
* @returns {Promise<string|null>}
*/
async function getFirstApiKey() {
const result = await api.getApiKeys();
const keys = result.success ? (result.data.keys || []) : [];
return keys.length > 0 ? keys[0].key : null;
}
// ─── Claude Code ──────────────────────────────────────────────────────────────
/**
* Build header showing current Claude config status
* @returns {Promise<string>}
*/
async function buildClaudeHeader() {
const result = await api.getCliToolSettings("claude");
if (!result.success) return ` ${COLORS.red}Failed to load settings${COLORS.reset}`;
const settings = result.data.settings;
const currentUrl = settings?.env?.ANTHROPIC_BASE_URL;
const currentKey = settings?.env?.ANTHROPIC_AUTH_TOKEN;
const lines = [];
if (currentUrl) {
lines.push(`Status: ${COLORS.green}✓ Configured${COLORS.reset}`);
lines.push(`Endpoint: ${COLORS.cyan}${currentUrl}${COLORS.reset}`);
if (currentKey) {
lines.push(`API Key: ${COLORS.dim}${currentKey.substring(0, 10)}...${COLORS.reset}`);
}
} else {
lines.push(`Status: ${COLORS.red}✗ Not configured${COLORS.reset}`);
lines.push(`${COLORS.dim}Run "Quick Setup" to configure${COLORS.reset}`);
}
return lines.join("\n");
}
/**
* Get current Claude model from settings
* @param {string} envKey
* @returns {Promise<string>}
*/
async function getClaudeModel(envKey) {
const result = await api.getCliToolSettings("claude");
return result.success ? (result.data.settings?.env?.[envKey] || "Not set") : "Not set";
}
/**
* Quick setup for Claude Code — sets endpoint, key, and all default models
* @param {number} port
*/
async function claudeQuickSetup(port) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
if (!apiKey) {
showStatus("No API keys found. Create one in API Keys menu first.", "error");
await pause();
return;
}
const env = { ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: apiKey, API_TIMEOUT_MS: "600000" };
CLAUDE_MODEL_TYPES.forEach(t => { env[t.envKey] = t.defaultValue; });
const result = await api.applyCliToolSettings("claude", { env });
showStatus(result.success ? "Quick Setup completed!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Select and save a specific Claude model type
* @param {Object} modelType
* @param {number} port
*/
async function claudeSelectModel(modelType, port) {
const current = await getClaudeModel(modelType.envKey);
const selected = await selectModelFromList(`Select ${modelType.name} Model`, current, { excludeCombos: true });
if (!selected) return;
const env = { [modelType.envKey]: selected };
// Also set base URL if not configured yet
const settingsResult = await api.getCliToolSettings("claude");
if (!settingsResult.data?.settings?.env?.ANTHROPIC_BASE_URL) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
env.ANTHROPIC_BASE_URL = endpoint;
env.API_TIMEOUT_MS = "600000";
if (apiKey) env.ANTHROPIC_AUTH_TOKEN = apiKey;
}
const result = await api.applyCliToolSettings("claude", { env });
showStatus(result.success ? `${modelType.name}${selected} saved!` : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Reset Claude Code settings
*/
async function claudeReset() {
const result = await api.resetCliToolSettings("claude");
showStatus(result.success ? "Settings reset successfully!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Claude Code submenu
* @param {number} port
* @param {Array<string>} breadcrumb
*/
async function showClaudeCodeMenu(port, breadcrumb = []) {
await showMenuWithBack({
title: "🔧 Claude Code Settings",
breadcrumb,
headerContent: buildClaudeHeader,
refresh: async () => ({
sonnet: await getClaudeModel("ANTHROPIC_DEFAULT_SONNET_MODEL"),
opus: await getClaudeModel("ANTHROPIC_DEFAULT_OPUS_MODEL"),
haiku: await getClaudeModel("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
}),
items: [
{
label: "⚡ Quick Setup (recommended)",
action: async () => { await claudeQuickSetup(port); return true; }
},
{
label: (d) => `Sonnet → ${d.sonnet}`,
action: async () => { await claudeSelectModel(CLAUDE_MODEL_TYPES[0], port); return true; }
},
{
label: (d) => `Opus → ${d.opus}`,
action: async () => { await claudeSelectModel(CLAUDE_MODEL_TYPES[1], port); return true; }
},
{
label: (d) => `Haiku → ${d.haiku}`,
action: async () => { await claudeSelectModel(CLAUDE_MODEL_TYPES[2], port); return true; }
},
{
label: "Reset to Default",
action: async () => { await claudeReset(); return true; }
}
]
});
}
// ─── Codex CLI ────────────────────────────────────────────────────────────────
/**
* Build header showing current Codex config status
* @returns {Promise<string>}
*/
async function buildCodexHeader() {
const result = await api.getCliToolSettings("codex");
if (!result.success) return ` ${COLORS.red}Failed to load settings${COLORS.reset}`;
const { installed, has9Router, config } = result.data;
if (!installed) return `Status: ${COLORS.red}✗ Codex CLI not installed${COLORS.reset}`;
if (!has9Router) {
return [
`Status: ${COLORS.red}✗ Not configured${COLORS.reset}`,
`${COLORS.dim}Run "Quick Setup" to configure${COLORS.reset}`
].join("\n");
}
// Parse base_url and model from raw TOML string
const baseUrlMatch = config && config.match(/base_url\s*=\s*"([^"]+)"/);
const modelMatch = config && config.match(/^model\s*=\s*"([^"]+)"/m);
const baseUrl = baseUrlMatch ? baseUrlMatch[1] : "";
const model = modelMatch ? modelMatch[1] : "";
const lines = [`Status: ${COLORS.green}✓ Configured${COLORS.reset}`];
if (baseUrl) lines.push(`Endpoint: ${COLORS.cyan}${baseUrl}${COLORS.reset}`);
if (model) lines.push(`Model: ${COLORS.dim}${model}${COLORS.reset}`);
return lines.join("\n");
}
/**
* Quick setup for Codex CLI
* @param {number} port
*/
async function codexQuickSetup(port) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
if (!apiKey) {
showStatus("No API keys found. Create one in API Keys menu first.", "error");
await pause();
return;
}
// Get model selection
const model = await selectModelFromList("Select Codex Model", "cx/claude-sonnet-4-5-20250929", { excludeCombos: true });
if (!model) return;
const result = await api.applyCliToolSettings("codex", { baseUrl: endpoint, apiKey, model });
showStatus(result.success ? "Codex setup completed!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Reset Codex CLI settings
*/
async function codexReset() {
const result = await api.resetCliToolSettings("codex");
showStatus(result.success ? "Codex settings reset!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Codex CLI submenu
* @param {number} port
* @param {Array<string>} breadcrumb
*/
async function showCodexMenu(port, breadcrumb = []) {
await showMenuWithBack({
title: "🤖 Codex CLI Settings",
breadcrumb,
headerContent: buildCodexHeader,
refresh: async () => ({}),
items: [
{
label: "⚡ Quick Setup",
action: async () => { await codexQuickSetup(port); return true; }
},
{
label: "Reset to Default",
action: async () => { await codexReset(); return true; }
}
]
});
}
// ─── Factory Droid ────────────────────────────────────────────────────────────
/**
* Build header showing current Droid config status
* @returns {Promise<string>}
*/
async function buildDroidHeader() {
const result = await api.getCliToolSettings("droid");
if (!result.success) return ` ${COLORS.red}Failed to load settings${COLORS.reset}`;
const { installed, has9Router, settings } = result.data;
if (!installed) return `Status: ${COLORS.red}✗ Factory Droid not installed${COLORS.reset}`;
if (!has9Router) {
return [
`Status: ${COLORS.red}✗ Not configured${COLORS.reset}`,
`${COLORS.dim}Run "Quick Setup" to configure${COLORS.reset}`
].join("\n");
}
// Extract 9Router custom model config
const custom = settings?.customModels?.find(m => m.id === "custom:9Router-0");
const lines = [`Status: ${COLORS.green}✓ Configured${COLORS.reset}`];
if (custom?.baseUrl) lines.push(`Endpoint: ${COLORS.cyan}${custom.baseUrl}${COLORS.reset}`);
if (custom?.model) lines.push(`Model: ${COLORS.dim}${custom.model}${COLORS.reset}`);
return lines.join("\n");
}
/**
* Quick setup for Factory Droid
* @param {number} port
*/
async function droidQuickSetup(port) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
if (!apiKey) {
showStatus("No API keys found. Create one in API Keys menu first.", "error");
await pause();
return;
}
const model = await selectModelFromList("Select Droid Model", "cc/claude-sonnet-4-5-20250929", { excludeCombos: true });
if (!model) return;
const result = await api.applyCliToolSettings("droid", { baseUrl: endpoint, apiKey, model });
showStatus(result.success ? "Factory Droid setup completed!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Reset Factory Droid settings
*/
async function droidReset() {
const result = await api.resetCliToolSettings("droid");
showStatus(result.success ? "Factory Droid settings reset!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Factory Droid submenu
* @param {number} port
* @param {Array<string>} breadcrumb
*/
async function showDroidMenu(port, breadcrumb = []) {
await showMenuWithBack({
title: "🤖 Factory Droid Settings",
breadcrumb,
headerContent: buildDroidHeader,
refresh: async () => ({}),
items: [
{
label: "⚡ Quick Setup",
action: async () => { await droidQuickSetup(port); return true; }
},
{
label: "Reset to Default",
action: async () => { await droidReset(); return true; }
}
]
});
}
// ─── Open Claw ────────────────────────────────────────────────────────────────
/**
* Build header showing current OpenClaw config status
* @returns {Promise<string>}
*/
async function buildOpenClawHeader() {
const result = await api.getCliToolSettings("openclaw");
if (!result.success) return ` ${COLORS.red}Failed to load settings${COLORS.reset}`;
const { installed, has9Router, settings } = result.data;
if (!installed) return `Status: ${COLORS.red}✗ Open Claw not installed${COLORS.reset}`;
if (!has9Router) {
return [
`Status: ${COLORS.red}✗ Not configured${COLORS.reset}`,
`${COLORS.dim}Run "Quick Setup" to configure${COLORS.reset}`
].join("\n");
}
// Extract 9Router provider config
const provider = settings?.models?.providers?.["9router"];
const primary = settings?.agents?.defaults?.model?.primary || "";
const model = primary.startsWith("9router/") ? primary.replace("9router/", "") : (provider?.models?.[0]?.id || "");
const lines = [`Status: ${COLORS.green}✓ Configured${COLORS.reset}`];
if (provider?.baseUrl) lines.push(`Endpoint: ${COLORS.cyan}${provider.baseUrl}${COLORS.reset}`);
if (model) lines.push(`Model: ${COLORS.dim}${model}${COLORS.reset}`);
return lines.join("\n");
}
/**
* Quick setup for Open Claw
* @param {number} port
*/
async function openClawQuickSetup(port) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
if (!apiKey) {
showStatus("No API keys found. Create one in API Keys menu first.", "error");
await pause();
return;
}
const model = await selectModelFromList("Select OpenClaw Model", "cc/claude-sonnet-4-5-20250929", { excludeCombos: true });
if (!model) return;
const result = await api.applyCliToolSettings("openclaw", { baseUrl: endpoint, apiKey, model });
showStatus(result.success ? "Open Claw setup completed!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Reset Open Claw settings
*/
async function openClawReset() {
const result = await api.resetCliToolSettings("openclaw");
showStatus(result.success ? "Open Claw settings reset!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
/**
* Open Claw submenu
* @param {number} port
* @param {Array<string>} breadcrumb
*/
async function showOpenClawMenu(port, breadcrumb = []) {
await showMenuWithBack({
title: "🦞 Open Claw Settings",
breadcrumb,
headerContent: buildOpenClawHeader,
refresh: async () => ({}),
items: [
{
label: "⚡ Quick Setup",
action: async () => { await openClawQuickSetup(port); return true; }
},
{
label: "Reset to Default",
action: async () => { await openClawReset(); return true; }
}
]
});
}
// ─── OpenCode CLI ─────────────────────────────────────────────────────────────
async function buildOpenCodeHeader() {
const result = await api.getCliToolSettings("opencode");
if (!result.success) return ` ${COLORS.red}Failed to load settings${COLORS.reset}`;
const { installed, has9Router, opencode } = result.data;
if (!installed) return `Status: ${COLORS.red}✗ OpenCode CLI not installed${COLORS.reset}`;
if (!has9Router) {
return [
`Status: ${COLORS.red}✗ Not configured${COLORS.reset}`,
`${COLORS.dim}Run "Quick Setup" to configure${COLORS.reset}`
].join("\n");
}
const lines = [`Status: ${COLORS.green}✓ Configured${COLORS.reset}`];
if (opencode?.baseURL) lines.push(`Endpoint: ${COLORS.cyan}${opencode.baseURL}${COLORS.reset}`);
if (opencode?.activeModel) lines.push(`Active: ${COLORS.dim}${opencode.activeModel}${COLORS.reset}`);
if (Array.isArray(opencode?.models) && opencode.models.length > 0) {
lines.push(`Models: ${COLORS.dim}${opencode.models.join(", ")}${COLORS.reset}`);
}
return lines.join("\n");
}
async function openCodeQuickSetup(port) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
if (!apiKey) {
showStatus("No API keys found. Create one in API Keys menu first.", "error");
await pause();
return;
}
// Pick first model (also becomes active model by default)
const firstModel = await selectModelFromList("Select Active Model (OpenCode)", "", { excludeCombos: true });
if (!firstModel) return;
const models = [firstModel];
// Optionally add more models
while (true) {
const more = await confirm(`Add another model? (current: ${models.length})`);
if (!more) break;
const next = await selectModelFromList(`Add Model #${models.length + 1}`, models.join(", "), { excludeCombos: true });
if (!next) break;
if (!models.includes(next)) models.push(next);
}
// Optional subagent model
let subagentModel = firstModel;
const wantSubagent = await confirm(`Set a different subagent model? (default: ${firstModel})`);
if (wantSubagent) {
const picked = await selectModelFromList("Select Subagent Model", firstModel, { excludeCombos: true });
if (picked) subagentModel = picked;
}
const result = await api.applyCliToolSettings("opencode", {
baseUrl: endpoint,
apiKey,
models,
activeModel: firstModel,
subagentModel,
});
showStatus(result.success ? "OpenCode setup completed!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
async function openCodeReset() {
const result = await api.resetCliToolSettings("opencode");
showStatus(result.success ? "OpenCode settings reset!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
async function showOpenCodeMenu(port, breadcrumb = []) {
await showMenuWithBack({
title: "💻 OpenCode CLI Settings",
breadcrumb,
headerContent: buildOpenCodeHeader,
refresh: async () => ({}),
items: [
{ label: "⚡ Quick Setup", action: async () => { await openCodeQuickSetup(port); return true; } },
{ label: "Reset to Default", action: async () => { await openCodeReset(); return true; } }
]
});
}
// ─── Hermes Agent ─────────────────────────────────────────────────────────────
async function buildHermesHeader() {
const result = await api.getCliToolSettings("hermes");
if (!result.success) return ` ${COLORS.red}Failed to load settings${COLORS.reset}`;
const { installed, has9Router, settings } = result.data;
if (!installed) return `Status: ${COLORS.red}✗ Hermes Agent not installed${COLORS.reset}`;
if (!has9Router) {
return [
`Status: ${COLORS.red}✗ Not configured${COLORS.reset}`,
`${COLORS.dim}Run "Quick Setup" to configure${COLORS.reset}`
].join("\n");
}
const model = settings?.model || {};
const lines = [`Status: ${COLORS.green}✓ Configured${COLORS.reset}`];
if (model.base_url) lines.push(`Endpoint: ${COLORS.cyan}${model.base_url}${COLORS.reset}`);
if (model.default) lines.push(`Model: ${COLORS.dim}${model.default}${COLORS.reset}`);
return lines.join("\n");
}
async function hermesQuickSetup(port) {
const { endpoint } = await getEndpoint(port);
const apiKey = await getFirstApiKey();
if (!apiKey) {
showStatus("No API keys found. Create one in API Keys menu first.", "error");
await pause();
return;
}
const model = await selectModelFromList("Select Hermes Model", "", { excludeCombos: true });
if (!model) return;
const result = await api.applyCliToolSettings("hermes", { baseUrl: endpoint, apiKey, model });
showStatus(result.success ? "Hermes setup completed!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
async function hermesReset() {
const result = await api.resetCliToolSettings("hermes");
showStatus(result.success ? "Hermes settings reset!" : `Failed: ${result.error}`, result.success ? "success" : "error");
await pause();
}
async function showHermesMenu(port, breadcrumb = []) {
await showMenuWithBack({
title: "⚡ Hermes Agent Settings",
breadcrumb,
headerContent: buildHermesHeader,
refresh: async () => ({}),
items: [
{ label: "⚡ Quick Setup", action: async () => { await hermesQuickSetup(port); return true; } },
{ label: "Reset to Default", action: async () => { await hermesReset(); return true; } }
]
});
}
// ─── Main CLI Tools Menu ──────────────────────────────────────────────────────
/**
* Main CLI Tools menu
* @param {number} port
* @param {Array<string>} breadcrumb
*/
async function showCliToolsMenu(port, breadcrumb = []) {
const { endpoint } = await getEndpoint(port);
await showMenuWithBack({
title: "🔧 CLI Tools",
breadcrumb,
headerContent: `Configure CLI tools to use 9Router\nEndpoint: ${endpoint}`,
items: [
{
label: "Claude Code",
action: async () => { await showClaudeCodeMenu(port, [...breadcrumb, "Claude Code"]); return true; }
},
{
label: "Codex CLI",
action: async () => { await showCodexMenu(port, [...breadcrumb, "Codex CLI"]); return true; }
},
{
label: "Factory Droid",
action: async () => { await showDroidMenu(port, [...breadcrumb, "Factory Droid"]); return true; }
},
{
label: "Open Claw",
action: async () => { await showOpenClawMenu(port, [...breadcrumb, "Open Claw"]); return true; }
},
{
label: "OpenCode",
action: async () => { await showOpenCodeMenu(port, [...breadcrumb, "OpenCode"]); return true; }
},
{
label: "Hermes",
action: async () => { await showHermesMenu(port, [...breadcrumb, "Hermes"]); return true; }
}
]
});
}
module.exports = { showCliToolsMenu };
-8
View File
@@ -4,7 +4,6 @@ const { showProvidersMenu } = require("./menus/providers");
const { showApiKeysMenu } = require("./menus/apiKeys");
const { showCombosMenu } = require("./menus/combos");
const { showSettingsMenu } = require("./menus/settings");
const { showCliToolsMenu } = require("./menus/cliTools");
const COLORS = {
reset: "\x1b[0m",
@@ -99,13 +98,6 @@ async function startTerminalUI(port) {
return true;
}
},
{
label: "CLI Tools",
action: async () => {
await showCliToolsMenu(port, [...basePath, "CLI Tools"]);
return true;
}
},
{
label: "Settings",
action: async () => {