2026-01-04 21:58:59 -05:00
|
|
|
"use client";
|
|
|
|
|
|
2026-01-20 23:45:19 -05:00
|
|
|
import { useState, useEffect, useCallback } from "react";
|
2026-01-04 21:58:59 -05:00
|
|
|
import { Card, CardSkeleton } from "@/shared/components";
|
|
|
|
|
import { CLI_TOOLS } from "@/shared/constants/cliTools";
|
2026-03-04 23:25:03 -05:00
|
|
|
import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
2026-05-10 10:54:54 -04:00
|
|
|
import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, HermesToolCard, DefaultToolCard, OpenCodeToolCard, CoworkToolCard, CopilotToolCard, ClineToolCard, KiloToolCard, MitmLinkCard } from "./components";
|
2026-04-10 07:16:14 -04:00
|
|
|
import { MITM_TOOLS } from "@/shared/constants/cliTools";
|
2026-01-04 21:58:59 -05:00
|
|
|
|
|
|
|
|
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
|
|
|
|
|
2026-05-09 06:48:20 -04:00
|
|
|
const ALL_STATUSES_URL = "/api/cli-tools/all-statuses";
|
2026-02-25 04:32:05 -05:00
|
|
|
|
2026-01-04 21:58:59 -05:00
|
|
|
export default function CLIToolsPageClient({ machineId }) {
|
|
|
|
|
const [connections, setConnections] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [expandedTool, setExpandedTool] = useState(null);
|
|
|
|
|
const [modelMappings, setModelMappings] = useState({});
|
|
|
|
|
const [cloudEnabled, setCloudEnabled] = useState(false);
|
2026-02-21 04:42:46 -05:00
|
|
|
const [tunnelEnabled, setTunnelEnabled] = useState(false);
|
2026-03-27 00:02:56 -04:00
|
|
|
const [tunnelPublicUrl, setTunnelPublicUrl] = useState("");
|
feat: add STT support, Gemini TTS, and expand usage tracking
- Speech-to-Text: full pipeline with sttCore handler, /v1/audio/transcriptions
endpoint, sttConfig for OpenAI, Gemini, Groq, Deepgram, AssemblyAI,
HuggingFace, NVIDIA Parakeet; new 9router-stt skill
- Gemini TTS: add gemini provider with 30 prebuilt voices and TTS_PROVIDER_CONFIG
- Usage: implement GLM (intl/cn) and MiniMax (intl/cn) quota fetchers; refactor
Gemini CLI usage to use retrieveUserQuota with per-model buckets
- Disabled models: lowdb-backed disabledModelsDb + /api/models/disabled route
- Header search: reusable Zustand store (headerSearchStore) wired into Header
- CLI tools: add Claude Cowork tool card and cowork-settings API
- Providers: introduce mediaPriority sorting in getProvidersByKind, add
Kimi K2.6, reorder hermes, drop qwen STT kind
- UI: expand media-providers/[kind]/[id] page (+314), enhance OAuthModal,
ModelSelectModal, ProviderTopology, ProxyPools, ProviderLimits
- Assets: refresh provider PNGs (alicode, byteplus, cloudflare-ai, nvidia,
ollama, vertex, volcengine-ark) and add aws-polly, fal-ai, jina-ai, recraft,
runwayml, stability-ai, topaz, black-forest-labs
2026-05-04 23:32:59 -04:00
|
|
|
const [tailscaleEnabled, setTailscaleEnabled] = useState(false);
|
|
|
|
|
const [tailscaleUrl, setTailscaleUrl] = useState("");
|
2026-01-04 21:58:59 -05:00
|
|
|
const [apiKeys, setApiKeys] = useState([]);
|
2026-02-25 04:32:05 -05:00
|
|
|
const [toolStatuses, setToolStatuses] = useState({});
|
2026-01-04 21:58:59 -05:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchConnections();
|
|
|
|
|
loadCloudSettings();
|
|
|
|
|
fetchApiKeys();
|
2026-02-25 04:32:05 -05:00
|
|
|
fetchAllStatuses();
|
2026-01-04 21:58:59 -05:00
|
|
|
}, []);
|
|
|
|
|
|
2026-02-25 04:32:05 -05:00
|
|
|
const fetchAllStatuses = async () => {
|
|
|
|
|
try {
|
2026-05-09 06:48:20 -04:00
|
|
|
const res = await fetch(ALL_STATUSES_URL);
|
|
|
|
|
if (res.ok) setToolStatuses(await res.json());
|
2026-02-25 04:32:05 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
console.log("Error fetching tool statuses:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-04 21:58:59 -05:00
|
|
|
const loadCloudSettings = async () => {
|
|
|
|
|
try {
|
2026-02-21 04:42:46 -05:00
|
|
|
const [settingsRes, tunnelRes] = await Promise.all([
|
|
|
|
|
fetch("/api/settings"),
|
|
|
|
|
fetch("/api/tunnel/status"),
|
|
|
|
|
]);
|
|
|
|
|
if (settingsRes.ok) {
|
|
|
|
|
const data = await settingsRes.json();
|
2026-01-04 21:58:59 -05:00
|
|
|
setCloudEnabled(data.cloudEnabled || false);
|
|
|
|
|
}
|
2026-02-21 04:42:46 -05:00
|
|
|
if (tunnelRes.ok) {
|
|
|
|
|
const data = await tunnelRes.json();
|
feat: add STT support, Gemini TTS, and expand usage tracking
- Speech-to-Text: full pipeline with sttCore handler, /v1/audio/transcriptions
endpoint, sttConfig for OpenAI, Gemini, Groq, Deepgram, AssemblyAI,
HuggingFace, NVIDIA Parakeet; new 9router-stt skill
- Gemini TTS: add gemini provider with 30 prebuilt voices and TTS_PROVIDER_CONFIG
- Usage: implement GLM (intl/cn) and MiniMax (intl/cn) quota fetchers; refactor
Gemini CLI usage to use retrieveUserQuota with per-model buckets
- Disabled models: lowdb-backed disabledModelsDb + /api/models/disabled route
- Header search: reusable Zustand store (headerSearchStore) wired into Header
- CLI tools: add Claude Cowork tool card and cowork-settings API
- Providers: introduce mediaPriority sorting in getProvidersByKind, add
Kimi K2.6, reorder hermes, drop qwen STT kind
- UI: expand media-providers/[kind]/[id] page (+314), enhance OAuthModal,
ModelSelectModal, ProviderTopology, ProxyPools, ProviderLimits
- Assets: refresh provider PNGs (alicode, byteplus, cloudflare-ai, nvidia,
ollama, vertex, volcengine-ark) and add aws-polly, fal-ai, jina-ai, recraft,
runwayml, stability-ai, topaz, black-forest-labs
2026-05-04 23:32:59 -04:00
|
|
|
setTunnelEnabled(!!(data.tunnel?.enabled || data.tunnel?.settingsEnabled));
|
|
|
|
|
setTunnelPublicUrl(data.tunnel?.publicUrl || "");
|
|
|
|
|
setTailscaleEnabled(!!(data.tailscale?.enabled || data.tailscale?.settingsEnabled));
|
|
|
|
|
setTailscaleUrl(data.tailscale?.tunnelUrl || "");
|
2026-02-21 04:42:46 -05:00
|
|
|
}
|
2026-01-04 21:58:59 -05:00
|
|
|
} catch (error) {
|
2026-02-21 04:42:46 -05:00
|
|
|
console.log("Error loading settings:", error);
|
2026-01-04 21:58:59 -05:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchApiKeys = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/keys");
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setApiKeys(data.keys || []);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log("Error fetching API keys:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchConnections = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/providers");
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
setConnections(data.connections || []);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log("Error fetching connections:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-04 23:25:03 -05:00
|
|
|
const getActiveProviders = () => connections.filter(c => c.isActive !== false);
|
2026-01-04 21:58:59 -05:00
|
|
|
|
|
|
|
|
const getAllAvailableModels = () => {
|
|
|
|
|
const activeProviders = getActiveProviders();
|
|
|
|
|
const models = [];
|
|
|
|
|
const seenModels = new Set();
|
|
|
|
|
activeProviders.forEach(conn => {
|
|
|
|
|
const alias = PROVIDER_ID_TO_ALIAS[conn.provider] || conn.provider;
|
|
|
|
|
const providerModels = getModelsByProviderId(conn.provider);
|
|
|
|
|
providerModels.forEach(m => {
|
|
|
|
|
const modelValue = `${alias}/${m.id}`;
|
|
|
|
|
if (!seenModels.has(modelValue)) {
|
|
|
|
|
seenModels.add(modelValue);
|
2026-03-04 23:25:03 -05:00
|
|
|
models.push({ value: modelValue, label: `${alias}/${m.id}`, provider: conn.provider, alias, connectionName: conn.name, modelId: m.id });
|
2026-01-04 21:58:59 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return models;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-20 23:45:19 -05:00
|
|
|
const handleModelMappingChange = useCallback((toolId, modelAlias, targetModel) => {
|
|
|
|
|
setModelMappings(prev => {
|
2026-03-04 23:25:03 -05:00
|
|
|
if (prev[toolId]?.[modelAlias] === targetModel) return prev;
|
|
|
|
|
return { ...prev, [toolId]: { ...prev[toolId], [modelAlias]: targetModel } };
|
2026-01-20 23:45:19 -05:00
|
|
|
});
|
|
|
|
|
}, []);
|
2026-01-04 21:58:59 -05:00
|
|
|
|
|
|
|
|
const getBaseUrl = () => {
|
2026-03-27 00:02:56 -04:00
|
|
|
if (tunnelEnabled && tunnelPublicUrl) return tunnelPublicUrl;
|
2026-03-04 23:25:03 -05:00
|
|
|
if (cloudEnabled && CLOUD_URL) return CLOUD_URL;
|
|
|
|
|
if (typeof window !== "undefined") return window.location.origin;
|
2026-05-09 11:48:07 -04:00
|
|
|
return "http://localhost:20128";
|
2026-01-04 21:58:59 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2026-03-04 23:25:03 -05:00
|
|
|
<div className="flex flex-col gap-4">
|
|
|
|
|
<CardSkeleton />
|
|
|
|
|
<CardSkeleton />
|
|
|
|
|
<CardSkeleton />
|
2026-01-04 21:58:59 -05:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const availableModels = getAllAvailableModels();
|
|
|
|
|
const hasActiveProviders = availableModels.length > 0;
|
|
|
|
|
|
|
|
|
|
const renderToolCard = (toolId, tool) => {
|
|
|
|
|
const commonProps = {
|
|
|
|
|
tool,
|
|
|
|
|
isExpanded: expandedTool === toolId,
|
|
|
|
|
onToggle: () => setExpandedTool(expandedTool === toolId ? null : toolId),
|
|
|
|
|
baseUrl: getBaseUrl(),
|
|
|
|
|
apiKeys,
|
2026-05-05 09:22:21 -04:00
|
|
|
tunnelEnabled,
|
|
|
|
|
tunnelPublicUrl,
|
|
|
|
|
tailscaleEnabled,
|
|
|
|
|
tailscaleUrl,
|
2026-01-04 21:58:59 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
switch (toolId) {
|
|
|
|
|
case "claude":
|
|
|
|
|
return (
|
|
|
|
|
<ClaudeToolCard
|
|
|
|
|
key={toolId}
|
|
|
|
|
{...commonProps}
|
|
|
|
|
activeProviders={getActiveProviders()}
|
|
|
|
|
modelMappings={modelMappings[toolId] || {}}
|
|
|
|
|
onModelMappingChange={(alias, target) => handleModelMappingChange(toolId, alias, target)}
|
|
|
|
|
hasActiveProviders={hasActiveProviders}
|
|
|
|
|
cloudEnabled={cloudEnabled}
|
2026-02-25 04:32:05 -05:00
|
|
|
initialStatus={toolStatuses.claude}
|
2026-01-04 21:58:59 -05:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
case "codex":
|
2026-02-25 04:32:05 -05:00
|
|
|
return <CodexToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.codex} />;
|
2026-03-02 22:10:03 -05:00
|
|
|
case "opencode":
|
|
|
|
|
return <OpenCodeToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.opencode} />;
|
feat: add STT support, Gemini TTS, and expand usage tracking
- Speech-to-Text: full pipeline with sttCore handler, /v1/audio/transcriptions
endpoint, sttConfig for OpenAI, Gemini, Groq, Deepgram, AssemblyAI,
HuggingFace, NVIDIA Parakeet; new 9router-stt skill
- Gemini TTS: add gemini provider with 30 prebuilt voices and TTS_PROVIDER_CONFIG
- Usage: implement GLM (intl/cn) and MiniMax (intl/cn) quota fetchers; refactor
Gemini CLI usage to use retrieveUserQuota with per-model buckets
- Disabled models: lowdb-backed disabledModelsDb + /api/models/disabled route
- Header search: reusable Zustand store (headerSearchStore) wired into Header
- CLI tools: add Claude Cowork tool card and cowork-settings API
- Providers: introduce mediaPriority sorting in getProvidersByKind, add
Kimi K2.6, reorder hermes, drop qwen STT kind
- UI: expand media-providers/[kind]/[id] page (+314), enhance OAuthModal,
ModelSelectModal, ProviderTopology, ProxyPools, ProviderLimits
- Assets: refresh provider PNGs (alicode, byteplus, cloudflare-ai, nvidia,
ollama, vertex, volcengine-ark) and add aws-polly, fal-ai, jina-ai, recraft,
runwayml, stability-ai, topaz, black-forest-labs
2026-05-04 23:32:59 -04:00
|
|
|
case "cowork":
|
|
|
|
|
return (
|
|
|
|
|
<CoworkToolCard
|
|
|
|
|
key={toolId}
|
|
|
|
|
{...commonProps}
|
|
|
|
|
activeProviders={getActiveProviders()}
|
|
|
|
|
hasActiveProviders={hasActiveProviders}
|
|
|
|
|
cloudEnabled={cloudEnabled}
|
|
|
|
|
cloudUrl={CLOUD_URL}
|
|
|
|
|
tunnelEnabled={tunnelEnabled}
|
|
|
|
|
tunnelPublicUrl={tunnelPublicUrl}
|
|
|
|
|
tailscaleEnabled={tailscaleEnabled}
|
|
|
|
|
tailscaleUrl={tailscaleUrl}
|
|
|
|
|
initialStatus={toolStatuses.cowork}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-02-06 08:53:20 -05:00
|
|
|
case "droid":
|
2026-02-25 04:32:05 -05:00
|
|
|
return <DroidToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.droid} />;
|
2026-02-06 08:53:20 -05:00
|
|
|
case "openclaw":
|
2026-02-25 04:32:05 -05:00
|
|
|
return <OpenClawToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.openclaw} />;
|
2026-04-23 05:39:31 -04:00
|
|
|
case "hermes":
|
|
|
|
|
return <HermesToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.hermes} />;
|
2026-05-09 06:48:20 -04:00
|
|
|
case "copilot":
|
|
|
|
|
return <CopilotToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.copilot} />;
|
2026-05-10 10:54:54 -04:00
|
|
|
case "cline":
|
|
|
|
|
return <ClineToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.cline} />;
|
|
|
|
|
case "kilo":
|
|
|
|
|
return <KiloToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.kilo} />;
|
2026-01-04 21:58:59 -05:00
|
|
|
default:
|
2026-03-02 22:10:03 -05:00
|
|
|
return <DefaultToolCard key={toolId} toolId={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} tunnelEnabled={tunnelEnabled} />;
|
2026-01-04 21:58:59 -05:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-19 04:32:29 -04:00
|
|
|
const regularTools = Object.entries(CLI_TOOLS);
|
2026-04-10 07:16:14 -04:00
|
|
|
const mitmTools = Object.entries(MITM_TOOLS);
|
2026-03-04 23:25:03 -05:00
|
|
|
|
2026-01-04 21:58:59 -05:00
|
|
|
return (
|
2026-05-01 05:34:07 -04:00
|
|
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-1 sm:px-0">
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
<h1 className="text-xl font-semibold text-text-main sm:text-2xl">CLI Tools</h1>
|
|
|
|
|
<p className="text-sm text-text-muted">Configure local coding tools to use your 9Router providers.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-3 sm:gap-4">
|
2026-03-04 23:25:03 -05:00
|
|
|
{regularTools.map(([toolId, tool]) => renderToolCard(toolId, tool))}
|
2026-01-04 21:58:59 -05:00
|
|
|
</div>
|
2026-05-01 05:34:07 -04:00
|
|
|
<div className="grid gap-3 sm:gap-4">
|
|
|
|
|
<div className="flex items-center gap-2 px-1">
|
|
|
|
|
<span className="material-symbols-outlined text-[18px] text-primary">security</span>
|
|
|
|
|
<h2 className="text-sm font-semibold text-text-main">MITM Tools</h2>
|
|
|
|
|
</div>
|
2026-04-10 07:16:14 -04:00
|
|
|
{mitmTools.map(([toolId, tool]) => (
|
|
|
|
|
<MitmLinkCard key={toolId} tool={tool} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-01-04 21:58:59 -05:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|