mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
Merge pull request #913 from carmin777/feat/qoder-pat-qodercli
feat(qoder): support PAT via qodercli and remove stale qoder.cn defaults
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
isOpenAICompatibleProvider,
|
||||
isAnthropicCompatibleProvider,
|
||||
isClaudeCodeCompatibleProvider,
|
||||
supportsApiKeyOnFreeProvider,
|
||||
} from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
@@ -872,7 +873,10 @@ export default function ProviderDetailPage() {
|
||||
: (FREE_PROVIDERS as any)[providerId] ||
|
||||
(OAUTH_PROVIDERS as any)[providerId] ||
|
||||
(APIKEY_PROVIDERS as any)[providerId];
|
||||
const isOAuth = !!(FREE_PROVIDERS as any)[providerId] || !!(OAUTH_PROVIDERS as any)[providerId];
|
||||
const providerSupportsOAuth =
|
||||
!!(FREE_PROVIDERS as any)[providerId] || !!(OAUTH_PROVIDERS as any)[providerId];
|
||||
const providerSupportsPat = supportsApiKeyOnFreeProvider(providerId);
|
||||
const isOAuth = providerSupportsOAuth && !providerSupportsPat;
|
||||
const models = getModelsByProviderId(providerId);
|
||||
const providerAlias = getProviderAlias(providerId);
|
||||
const isManagedAvailableModelsProvider = isCompatible || providerId === "openrouter";
|
||||
@@ -1062,6 +1066,14 @@ export default function ProviderDetailPage() {
|
||||
setShowOAuthModal(false);
|
||||
}, [fetchConnections]);
|
||||
|
||||
const openPrimaryAddFlow = useCallback(() => {
|
||||
if (isOAuth) {
|
||||
setShowOAuthModal(true);
|
||||
return;
|
||||
}
|
||||
setShowAddApiKeyModal(true);
|
||||
}, [isOAuth]);
|
||||
|
||||
const handleSaveApiKey = async (formData) => {
|
||||
try {
|
||||
const res = await fetch("/api/providers", {
|
||||
@@ -2216,13 +2228,16 @@ export default function ProviderDetailPage() {
|
||||
</button>
|
||||
)}
|
||||
{!isCompatible ? (
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => (isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true))}
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" icon="add" onClick={openPrimaryAddFlow}>
|
||||
{providerSupportsPat ? "Add PAT" : t("add")}
|
||||
</Button>
|
||||
{providerId === "qoder" && (
|
||||
<Button size="sm" variant="secondary" onClick={() => setShowOAuthModal(true)}>
|
||||
Experimental OAuth
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
connections.length === 0 && (
|
||||
<Button size="sm" icon="add" onClick={() => setShowAddApiKeyModal(true)}>
|
||||
@@ -2242,12 +2257,16 @@ export default function ProviderDetailPage() {
|
||||
<p className="text-text-main font-medium mb-1">{t("noConnectionsYet")}</p>
|
||||
<p className="text-sm text-text-muted mb-4">{t("addFirstConnectionHint")}</p>
|
||||
{!isCompatible && (
|
||||
<Button
|
||||
icon="add"
|
||||
onClick={() => (isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true))}
|
||||
>
|
||||
{t("addConnection")}
|
||||
</Button>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button icon="add" onClick={openPrimaryAddFlow}>
|
||||
{providerSupportsPat ? "Add PAT" : t("addConnection")}
|
||||
</Button>
|
||||
{providerId === "qoder" && (
|
||||
<Button variant="secondary" onClick={() => setShowOAuthModal(true)}>
|
||||
Experimental OAuth
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -2264,7 +2283,7 @@ export default function ProviderDetailPage() {
|
||||
<ConnectionRow
|
||||
key={conn.id}
|
||||
connection={conn}
|
||||
isOAuth={isOAuth}
|
||||
isOAuth={conn.authType === "oauth"}
|
||||
isFirst={index === 0}
|
||||
isLast={index === sorted.length - 1}
|
||||
onMoveUp={() => handleSwapPriority(conn, sorted[index - 1])}
|
||||
@@ -2285,8 +2304,10 @@ export default function ProviderDetailPage() {
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
onDelete={() => handleDelete(conn.id)}
|
||||
onReauth={isOAuth ? () => setShowOAuthModal(true) : undefined}
|
||||
onRefreshToken={isOAuth ? () => handleRefreshToken(conn.id) : undefined}
|
||||
onReauth={conn.authType === "oauth" ? () => setShowOAuthModal(true) : undefined}
|
||||
onRefreshToken={
|
||||
conn.authType === "oauth" ? () => handleRefreshToken(conn.id) : undefined
|
||||
}
|
||||
isRefreshing={refreshingId === conn.id}
|
||||
onApplyCodexAuthLocal={
|
||||
providerId === "codex"
|
||||
@@ -2361,7 +2382,7 @@ export default function ProviderDetailPage() {
|
||||
<ConnectionRow
|
||||
key={conn.id}
|
||||
connection={conn}
|
||||
isOAuth={isOAuth}
|
||||
isOAuth={conn.authType === "oauth"}
|
||||
isFirst={gi === 0 && index === 0}
|
||||
isLast={gi === groupKeys.length - 1 && index === groupConns.length - 1}
|
||||
onMoveUp={() =>
|
||||
@@ -2388,8 +2409,14 @@ export default function ProviderDetailPage() {
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
onDelete={() => handleDelete(conn.id)}
|
||||
onReauth={isOAuth ? () => setShowOAuthModal(true) : undefined}
|
||||
onRefreshToken={isOAuth ? () => handleRefreshToken(conn.id) : undefined}
|
||||
onReauth={
|
||||
conn.authType === "oauth" ? () => setShowOAuthModal(true) : undefined
|
||||
}
|
||||
onRefreshToken={
|
||||
conn.authType === "oauth"
|
||||
? () => handleRefreshToken(conn.id)
|
||||
: undefined
|
||||
}
|
||||
isRefreshing={refreshingId === conn.id}
|
||||
onApplyCodexAuthLocal={
|
||||
providerId === "codex"
|
||||
@@ -4376,6 +4403,7 @@ function AddApiKeyModal({
|
||||
const isVertex = provider === "vertex";
|
||||
const defaultRegion = "us-central1";
|
||||
const isGlm = provider === "glm";
|
||||
const isQoder = provider === "qoder";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -4501,16 +4529,27 @@ function AddApiKeyModal({
|
||||
label={t("nameLabel")}
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder={t("productionKey")}
|
||||
placeholder={isQoder ? "Qoder PAT" : t("productionKey")}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
label={t("apiKeyLabel")}
|
||||
label={isQoder ? "Personal Access Token" : t("apiKeyLabel")}
|
||||
type="password"
|
||||
value={formData.apiKey}
|
||||
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
||||
className="flex-1"
|
||||
placeholder={isVertex ? "Cole o Service Account JSON aqui" : undefined}
|
||||
placeholder={
|
||||
isVertex
|
||||
? "Cole o Service Account JSON aqui"
|
||||
: isQoder
|
||||
? "Paste your Qoder Personal Access Token"
|
||||
: undefined
|
||||
}
|
||||
hint={
|
||||
isQoder
|
||||
? "Supported path: PAT via qodercli. Browser OAuth remains experimental."
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="pt-6">
|
||||
<Button
|
||||
|
||||
@@ -168,9 +168,11 @@ export default function ProvidersPage() {
|
||||
};
|
||||
|
||||
const getProviderStats = (providerId, authType) => {
|
||||
const providerConnections = connections.filter(
|
||||
(c) => c.provider === providerId && c.authType === authType
|
||||
);
|
||||
const providerConnections = connections.filter((c) => {
|
||||
if (c.provider !== providerId) return false;
|
||||
if (authType === "free") return true;
|
||||
return c.authType === authType;
|
||||
});
|
||||
|
||||
// Helper: check if connection is effectively active (cooldown expired)
|
||||
const getEffectiveStatus = (conn) => {
|
||||
@@ -217,13 +219,17 @@ export default function ProvidersPage() {
|
||||
|
||||
// Toggle all connections for a provider on/off
|
||||
const handleToggleProvider = async (providerId: string, authType: string, newActive: boolean) => {
|
||||
const providerConns = connections.filter(
|
||||
(c) => c.provider === providerId && c.authType === authType
|
||||
);
|
||||
const providerConns = connections.filter((c) => {
|
||||
if (c.provider !== providerId) return false;
|
||||
if (authType === "free") return true;
|
||||
return c.authType === authType;
|
||||
});
|
||||
// Optimistically update UI
|
||||
setConnections((prev) =>
|
||||
prev.map((c) =>
|
||||
c.provider === providerId && c.authType === authType ? { ...c, isActive: newActive } : c
|
||||
c.provider === providerId && (authType === "free" || c.authType === authType)
|
||||
? { ...c, isActive: newActive }
|
||||
: c
|
||||
)
|
||||
);
|
||||
// Fire API calls in parallel
|
||||
@@ -444,9 +450,9 @@ export default function ProvidersPage() {
|
||||
key={key}
|
||||
providerId={key}
|
||||
provider={info}
|
||||
stats={getProviderStats(key, "oauth")}
|
||||
stats={getProviderStats(key, "free")}
|
||||
authType="free"
|
||||
onToggle={(active) => handleToggleProvider(key, "oauth", active)}
|
||||
onToggle={(active) => handleToggleProvider(key, "free", active)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,14 @@ export async function GET(
|
||||
if (action === "authorize") {
|
||||
const redirectUri = searchParams.get("redirect_uri") || "http://localhost:8080/callback";
|
||||
const authData = generateAuthData(provider, redirectUri);
|
||||
if (provider === "qoder" && !authData.authUrl) {
|
||||
return NextResponse.json({
|
||||
...authData,
|
||||
supported: false,
|
||||
error:
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token.",
|
||||
});
|
||||
}
|
||||
return NextResponse.json(authData);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/shared/constants/providers";
|
||||
import { PROVIDER_MODELS } from "@/shared/constants/models";
|
||||
import { getModelIsHidden } from "@/lib/localDb";
|
||||
import { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -99,6 +100,7 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
|
||||
{ id: "glm-4.7", name: "GLM 4.7" },
|
||||
{ id: "kimi-k2.5", name: "Kimi K2.5" },
|
||||
],
|
||||
qoder: () => getStaticQoderModels(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,6 +92,7 @@ const OAUTH_TEST_CONFIG = {
|
||||
const CLI_RUNTIME_PROVIDER_MAP = {
|
||||
cline: "cline",
|
||||
kilocode: "kilo",
|
||||
qoder: "qoder",
|
||||
};
|
||||
|
||||
/** POST body is optional; when present, only known fields are validated. */
|
||||
@@ -206,8 +207,12 @@ function classifyFailure({
|
||||
);
|
||||
}
|
||||
|
||||
async function getProviderRuntimeStatus(provider: string) {
|
||||
const toolId = CLI_RUNTIME_PROVIDER_MAP[provider];
|
||||
async function getProviderRuntimeStatus(connection: any) {
|
||||
const provider = typeof connection?.provider === "string" ? connection.provider : "";
|
||||
let toolId = CLI_RUNTIME_PROVIDER_MAP[provider];
|
||||
if (provider === "qoder" && connection?.authType !== "apikey") {
|
||||
toolId = null;
|
||||
}
|
||||
if (!toolId) return null;
|
||||
|
||||
try {
|
||||
@@ -566,7 +571,7 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
|
||||
let result;
|
||||
const startTime = Date.now();
|
||||
const runtime = await getProviderRuntimeStatus(provider);
|
||||
const runtime = await getProviderRuntimeStatus(connection);
|
||||
|
||||
if ((runtime as any)?.diagnosis) {
|
||||
result = {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { createProviderSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { normalizeQoderPatProviderData } from "@omniroute/open-sse/services/qoderCli.ts";
|
||||
|
||||
// GET /api/providers - List all connections
|
||||
export async function GET() {
|
||||
@@ -61,6 +62,7 @@ export async function POST(request: Request) {
|
||||
// Business validation
|
||||
const isValidProvider =
|
||||
APIKEY_PROVIDERS[provider] ||
|
||||
provider === "qoder" ||
|
||||
isOpenAICompatibleProvider(provider) ||
|
||||
isAnthropicCompatibleProvider(provider);
|
||||
|
||||
@@ -72,6 +74,10 @@ export async function POST(request: Request) {
|
||||
const allowMultipleCompatibleConnections =
|
||||
process.env.ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE === "true";
|
||||
|
||||
if (provider === "qoder") {
|
||||
providerSpecificData = normalizeQoderPatProviderData(providerSpecificData || {});
|
||||
}
|
||||
|
||||
if (isOpenAICompatibleProvider(provider)) {
|
||||
const node: any = await getProviderNodeById(provider);
|
||||
if (!node) {
|
||||
|
||||
@@ -65,12 +65,25 @@ export const QWEN_CONFIG = {
|
||||
};
|
||||
|
||||
// Qoder OAuth Configuration (Authorization Code)
|
||||
const QODER_OAUTH_AUTHORIZE_URL = process.env.QODER_OAUTH_AUTHORIZE_URL || "";
|
||||
const QODER_OAUTH_TOKEN_URL = process.env.QODER_OAUTH_TOKEN_URL || "";
|
||||
const QODER_OAUTH_USERINFO_URL = process.env.QODER_OAUTH_USERINFO_URL || "";
|
||||
const QODER_OAUTH_CLIENT_ID = process.env.QODER_OAUTH_CLIENT_ID || "";
|
||||
const QODER_OAUTH_CLIENT_SECRET = process.env.QODER_OAUTH_CLIENT_SECRET || "";
|
||||
const QODER_OAUTH_ENABLED =
|
||||
!!QODER_OAUTH_AUTHORIZE_URL &&
|
||||
!!QODER_OAUTH_TOKEN_URL &&
|
||||
!!QODER_OAUTH_USERINFO_URL &&
|
||||
!!QODER_OAUTH_CLIENT_ID &&
|
||||
!!QODER_OAUTH_CLIENT_SECRET;
|
||||
|
||||
export const QODER_CONFIG = {
|
||||
clientId: process.env.QODER_OAUTH_CLIENT_ID || "10009311001",
|
||||
clientSecret: process.env.QODER_OAUTH_CLIENT_SECRET || "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW",
|
||||
authorizeUrl: "https://qoder.cn/oauth",
|
||||
tokenUrl: "https://qoder.cn/oauth/token",
|
||||
userInfoUrl: "https://qoder.cn/api/oauth/getUserInfo",
|
||||
enabled: QODER_OAUTH_ENABLED,
|
||||
clientId: QODER_OAUTH_CLIENT_ID,
|
||||
clientSecret: QODER_OAUTH_CLIENT_SECRET,
|
||||
authorizeUrl: QODER_OAUTH_AUTHORIZE_URL,
|
||||
tokenUrl: QODER_OAUTH_TOKEN_URL,
|
||||
userInfoUrl: QODER_OAUTH_USERINFO_URL,
|
||||
extraParams: {
|
||||
loginMethod: "phone",
|
||||
type: "phone",
|
||||
|
||||
@@ -4,6 +4,9 @@ export const qoder = {
|
||||
config: QODER_CONFIG,
|
||||
flowType: "authorization_code",
|
||||
buildAuthUrl: (config, redirectUri, state) => {
|
||||
if (!config?.enabled || !config?.authorizeUrl) {
|
||||
return null;
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
loginMethod: config.extraParams.loginMethod,
|
||||
type: config.extraParams.type,
|
||||
@@ -14,6 +17,11 @@ export const qoder = {
|
||||
return `${config.authorizeUrl}?${params.toString()}`;
|
||||
},
|
||||
exchangeToken: async (config, code, redirectUri) => {
|
||||
if (!config?.enabled || !config?.tokenUrl) {
|
||||
throw new Error(
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token."
|
||||
);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
@@ -49,6 +57,11 @@ export const qoder = {
|
||||
return await response.json();
|
||||
},
|
||||
postExchange: async (tokens) => {
|
||||
if (!QODER_CONFIG.enabled || !QODER_CONFIG.userInfoUrl) {
|
||||
throw new Error(
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token."
|
||||
);
|
||||
}
|
||||
const userInfoRes = await fetch(
|
||||
`${QODER_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`,
|
||||
{ headers: { Accept: "application/json" } }
|
||||
|
||||
@@ -20,6 +20,12 @@ export class QoderService {
|
||||
* Build Qoder authorization URL
|
||||
*/
|
||||
buildAuthUrl(redirectUri: string, state: string) {
|
||||
if (!this.config?.enabled || !this.config?.authorizeUrl) {
|
||||
throw new Error(
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token."
|
||||
);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
loginMethod: this.config.extraParams.loginMethod,
|
||||
type: this.config.extraParams.type,
|
||||
@@ -35,6 +41,12 @@ export class QoderService {
|
||||
* Exchange authorization code for tokens
|
||||
*/
|
||||
async exchangeCode(code: string, redirectUri: string) {
|
||||
if (!this.config?.enabled || !this.config?.tokenUrl) {
|
||||
throw new Error(
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token."
|
||||
);
|
||||
}
|
||||
|
||||
// Create Basic Auth header
|
||||
const basicAuth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString(
|
||||
"base64"
|
||||
@@ -68,6 +80,12 @@ export class QoderService {
|
||||
* Get user info from Qoder
|
||||
*/
|
||||
async getUserInfo(accessToken: string) {
|
||||
if (!this.config?.enabled || !this.config?.userInfoUrl) {
|
||||
throw new Error(
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token."
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.config.userInfoUrl}?accessToken=${encodeURIComponent(accessToken)}`,
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isAnthropicCompatibleProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
} from "@/shared/constants/providers";
|
||||
import { validateQoderCliPat } from "@omniroute/open-sse/services/qoderCli.ts";
|
||||
|
||||
const OPENAI_LIKE_FORMATS = new Set(["openai", "openai-responses"]);
|
||||
const GEMINI_LIKE_FORMATS = new Set(["gemini", "gemini-cli"]);
|
||||
@@ -747,6 +748,8 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
|
||||
// ── Specialty provider validation ──
|
||||
const SPECIALTY_VALIDATORS = {
|
||||
qoder: ({ apiKey, providerSpecificData }: any) =>
|
||||
validateQoderCliPat({ apiKey, providerSpecificData }),
|
||||
deepgram: validateDeepgramProvider,
|
||||
assemblyai: validateAssemblyAIProvider,
|
||||
nanobanana: validateNanoBananaProvider,
|
||||
|
||||
@@ -313,6 +313,13 @@ export default function OAuthModal({
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
if (!data.authUrl) {
|
||||
throw new Error(
|
||||
data.error ||
|
||||
"Browser OAuth is unavailable for this provider in the current environment. Use the supported auth method instead."
|
||||
);
|
||||
}
|
||||
|
||||
setAuthData({ ...data, redirectUri });
|
||||
|
||||
// For non-true-localhost (LAN IPs, remote) or manual fallback: use manual input mode (user pastes callback URL)
|
||||
|
||||
@@ -17,6 +17,12 @@ export const FREE_PROVIDERS = {
|
||||
kiro: { id: "kiro", alias: "kr", name: "Kiro AI", icon: "psychology_alt", color: "#FF6B35" },
|
||||
};
|
||||
|
||||
export const FREE_APIKEY_PROVIDER_IDS = new Set(["qoder"]);
|
||||
|
||||
export function supportsApiKeyOnFreeProvider(providerId) {
|
||||
return FREE_APIKEY_PROVIDER_IDS.has(providerId);
|
||||
}
|
||||
|
||||
// OAuth Providers
|
||||
export const OAUTH_PROVIDERS = {
|
||||
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757" },
|
||||
|
||||
@@ -109,6 +109,16 @@ const CLI_TOOLS: Record<string, any> = {
|
||||
config: ".config/opencode/opencode.json",
|
||||
},
|
||||
},
|
||||
qoder: {
|
||||
defaultCommand: "qodercli",
|
||||
envBinKey: "CLI_QODER_BIN",
|
||||
requiresBinary: true,
|
||||
healthcheckTimeoutMs: 12000,
|
||||
paths: {
|
||||
config: ".qoder/settings.json",
|
||||
auth: ".qoder/auth.json",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const isWindows = () => process.platform === "win32";
|
||||
@@ -390,6 +400,7 @@ const getKnownToolPaths = (toolId: string): string[] => {
|
||||
cline: [["cline.cmd", "cline"]],
|
||||
kilo: [["kilocode.cmd", "kilocode"]],
|
||||
opencode: [["opencode.cmd", "opencode"]],
|
||||
qoder: [["qodercli.exe", "qodercli"]],
|
||||
};
|
||||
|
||||
const bins = toolBins[toolId] || [];
|
||||
|
||||
Reference in New Issue
Block a user