Polish AI settings integrations UI (#1409)
This commit is contained in:
@@ -124,6 +124,9 @@ export const enAiMessages: Messages = {
|
||||
'ai.cursor.apiKeyFromEnv': 'From environment',
|
||||
'ai.cursor.apiKey': 'API Key',
|
||||
'ai.cursor.apiKeyPlaceholder': 'Enter Cursor API key',
|
||||
'ai.cursor.apiKeyPlaceholder.env': 'Using CURSOR_API_KEY; enter a key to override',
|
||||
'ai.cursor.apiKeyEnvHint': 'Cursor can use CURSOR_API_KEY from your shell. Save a key here only if you want Netcatty to override it.',
|
||||
'ai.cursor.apiKeyOverrideHint': 'Netcatty will use the saved key here before CURSOR_API_KEY.',
|
||||
'ai.cursor.saveApiKey': 'Save',
|
||||
'ai.cursor.saved': 'Saved',
|
||||
'ai.cursor.showApiKey': 'Show API key',
|
||||
|
||||
@@ -124,6 +124,9 @@ export const ruAiMessages: Messages = {
|
||||
'ai.cursor.apiKeyFromEnv': 'Из окружения',
|
||||
'ai.cursor.apiKey': 'API-ключ',
|
||||
'ai.cursor.apiKeyPlaceholder': 'Введите API-ключ Cursor',
|
||||
'ai.cursor.apiKeyPlaceholder.env': 'Используется CURSOR_API_KEY; введите ключ для замены',
|
||||
'ai.cursor.apiKeyEnvHint': 'Cursor может использовать CURSOR_API_KEY из shell. Сохраняйте ключ здесь только если хотите переопределить его в Netcatty.',
|
||||
'ai.cursor.apiKeyOverrideHint': 'Netcatty сначала использует сохранённый здесь ключ, затем CURSOR_API_KEY.',
|
||||
'ai.cursor.saveApiKey': 'Сохранить',
|
||||
'ai.cursor.saved': 'Сохранено',
|
||||
'ai.cursor.showApiKey': 'Показать API-ключ',
|
||||
|
||||
@@ -124,6 +124,9 @@ export const zhCNAiMessages: Messages = {
|
||||
'ai.cursor.apiKeyFromEnv': '来自环境变量',
|
||||
'ai.cursor.apiKey': 'API Key',
|
||||
'ai.cursor.apiKeyPlaceholder': '输入 Cursor API Key',
|
||||
'ai.cursor.apiKeyPlaceholder.env': '已使用 CURSOR_API_KEY;填写后会覆盖',
|
||||
'ai.cursor.apiKeyEnvHint': '已检测到本机 CURSOR_API_KEY。留空即可继续使用,填写保存后会覆盖它。',
|
||||
'ai.cursor.apiKeyOverrideHint': '当前优先使用这里保存的 Key;清空保存后会回到 CURSOR_API_KEY。',
|
||||
'ai.cursor.saveApiKey': '保存',
|
||||
'ai.cursor.saved': '已保存',
|
||||
'ai.cursor.showApiKey': '显示 API Key',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DiscoveredAgent, ExternalAgentConfig } from '../../infrastructure/
|
||||
import { getExternalAgentSdkBackend } from '../../infrastructure/ai/managedAgents';
|
||||
|
||||
interface NetcattyBridge {
|
||||
aiDiscoverAgents(options?: { refreshShellEnv?: boolean }): Promise<DiscoveredAgent[]>;
|
||||
aiDiscoverAgents(options?: { refreshShellEnv?: boolean; apiKeyPresent?: boolean }): Promise<DiscoveredAgent[]>;
|
||||
}
|
||||
|
||||
function getBridge(): NetcattyBridge | undefined {
|
||||
@@ -19,20 +19,27 @@ export function useAgentDiscovery(
|
||||
const [discoveredAgents, setDiscoveredAgents] = useState<DiscoveredAgent[]>([]);
|
||||
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||
|
||||
const cursorApiKeyPresent = externalAgents.some(
|
||||
(agent) => agent.id === "discovered_cursor" && Boolean(agent.apiKey),
|
||||
);
|
||||
|
||||
const discover = useCallback(async (discoverOptions?: { refreshShellEnv?: boolean }) => {
|
||||
const bridge = getBridge();
|
||||
if (!bridge) return;
|
||||
|
||||
setIsDiscovering(true);
|
||||
try {
|
||||
const agents = await bridge.aiDiscoverAgents(discoverOptions);
|
||||
const agents = await bridge.aiDiscoverAgents({
|
||||
...discoverOptions,
|
||||
apiKeyPresent: cursorApiKeyPresent,
|
||||
});
|
||||
setDiscoveredAgents(agents);
|
||||
} catch (err) {
|
||||
console.error('Agent discovery failed:', err);
|
||||
} finally {
|
||||
setIsDiscovering(false);
|
||||
}
|
||||
}, []);
|
||||
}, [cursorApiKeyPresent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Sub-components live in ./ai/ directory:
|
||||
* - ProviderCard, ProviderConfigForm, AddProviderDropdown
|
||||
* - ModelSelector, ProviderIconBadge
|
||||
* - ModelSelector
|
||||
* - CodexConnectionCard, ClaudeCodeCard, CodebuddyCard
|
||||
* - SafetySettings
|
||||
*/
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
getBridge,
|
||||
normalizeCodexBridgeError,
|
||||
} from "./ai/types";
|
||||
import { ProviderIconBadge } from "./ai/ProviderIconBadge";
|
||||
import { ProviderCard } from "./ai/ProviderCard";
|
||||
import { AddProviderDropdown } from "./ai/AddProviderDropdown";
|
||||
import { CodexConnectionCard } from "./ai/CodexConnectionCard";
|
||||
@@ -614,7 +613,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
|
||||
<SettingsSection
|
||||
title={t('ai.codex')}
|
||||
leading={<ProviderIconBadge providerId="openai" size="sm" />}
|
||||
leading={<AgentIconBadge agent={{ id: "codex", icon: "openai", name: "Codex CLI" }} variant="plain" className="h-5 w-5 text-muted-foreground/90" />}
|
||||
>
|
||||
<CodexConnectionCard
|
||||
pathInfo={codexPathInfo}
|
||||
@@ -636,7 +635,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
|
||||
<SettingsSection
|
||||
title={t('ai.claude.title')}
|
||||
leading={<ProviderIconBadge providerId="claude" size="sm" />}
|
||||
leading={<AgentIconBadge agent={{ id: "claude", icon: "claude", name: "Claude Code" }} variant="plain" className="h-5 w-5 text-muted-foreground/90" />}
|
||||
>
|
||||
<ClaudeCodeCard
|
||||
pathInfo={claudePathInfo}
|
||||
@@ -655,7 +654,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
|
||||
<SettingsSection
|
||||
title={t('ai.copilot.title')}
|
||||
leading={<ProviderIconBadge providerId="copilot" size="sm" />}
|
||||
leading={<AgentIconBadge agent={{ id: "copilot", icon: "copilot", name: "GitHub Copilot CLI" }} variant="plain" className="h-5 w-5 text-muted-foreground/90" />}
|
||||
>
|
||||
<CopilotCliCard
|
||||
pathInfo={copilotPathInfo}
|
||||
@@ -668,7 +667,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
|
||||
<SettingsSection
|
||||
title={t('ai.cursor.title')}
|
||||
leading={<AgentIconBadge agent={{ id: "cursor", icon: "cursor", name: "Cursor" }} size="xs" variant="plain" />}
|
||||
leading={<AgentIconBadge agent={{ id: "cursor", icon: "cursor", name: "Cursor" }} variant="plain" className="h-5 w-5 text-muted-foreground/90" />}
|
||||
>
|
||||
<CursorSdkCard
|
||||
pathInfo={cursorPathInfo}
|
||||
@@ -681,7 +680,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
|
||||
<SettingsSection
|
||||
title={t('ai.codebuddy.title')}
|
||||
leading={<ProviderIconBadge providerId="codebuddy" size="sm" />}
|
||||
leading={<AgentIconBadge agent={{ id: "codebuddy", icon: "codebuddy", name: "CodeBuddy Code" }} variant="plain" className="h-5 w-5 text-muted-foreground/90" />}
|
||||
>
|
||||
<CodebuddyCard
|
||||
pathInfo={codebuddyPathInfo}
|
||||
@@ -752,20 +751,20 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<SettingCard padded className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<SettingCard padded className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-muted-foreground/80 leading-5">
|
||||
{t('ai.userSkills.description')}
|
||||
</p>
|
||||
{userSkillsStatus?.directoryPath ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
{t('ai.userSkills.location')}:{" "}
|
||||
<span className="font-mono">{userSkillsStatus.directoryPath}</span>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="text-xs text-muted-foreground/80">
|
||||
{isLoadingUserSkills
|
||||
? t('ai.userSkills.loading')
|
||||
: userSkillsStatus?.ok
|
||||
@@ -777,25 +776,25 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
</div>
|
||||
|
||||
{userSkillsStatus?.ok && userSkillsStatus.skills && userSkillsStatus.skills.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="border-t border-border/60 divide-y divide-border/60">
|
||||
{userSkillsStatus.skills.map((skill) => (
|
||||
<div
|
||||
key={skill.id}
|
||||
className="rounded-md border border-border/60 bg-background/70 p-3"
|
||||
className="py-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="font-medium">{skill.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{skill.description}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono break-all">
|
||||
<div className="text-sm font-medium">{skill.name}</div>
|
||||
<div className="text-xs text-muted-foreground leading-5">{skill.description}</div>
|
||||
<div className="text-xs text-muted-foreground/80 font-mono break-all">
|
||||
{skill.directoryName}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={
|
||||
skill.status === "ready"
|
||||
? "rounded-full bg-emerald-500/10 px-2 py-1 text-xs font-medium text-emerald-600"
|
||||
: "rounded-full bg-amber-500/10 px-2 py-1 text-xs font-medium text-amber-600"
|
||||
? "text-xs font-medium text-emerald-500 shrink-0"
|
||||
: "text-xs font-medium text-amber-500 shrink-0"
|
||||
}
|
||||
>
|
||||
{skill.status === "ready"
|
||||
@@ -804,7 +803,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
{skill.warnings.length > 0 ? (
|
||||
<div className="mt-3 space-y-1 text-sm text-amber-700">
|
||||
<div className="mt-2 space-y-1 text-xs text-amber-500">
|
||||
{skill.warnings.map((warning, index) => (
|
||||
<div key={`${skill.id}-${index}`} className="flex items-start gap-2">
|
||||
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
|
||||
@@ -817,7 +816,7 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
|
||||
))}
|
||||
</div>
|
||||
) : userSkillsStatus?.ok ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="border-t border-border/60 pt-3 text-sm text-muted-foreground">
|
||||
{t('ai.userSkills.empty')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useI18n } from "../../../../application/i18n/I18nProvider";
|
||||
import { Button } from "../../../ui/button";
|
||||
import { cn } from "../../../../lib/utils";
|
||||
import type { AgentPathInfo } from "./types";
|
||||
import { ProviderIconBadge } from "./ProviderIconBadge";
|
||||
import { parseEnvLines, serializeEnvLines } from "./codebuddyConfigEnv";
|
||||
|
||||
const INTERNET_ENV_OPTIONS = [
|
||||
@@ -67,17 +66,11 @@ export const CodebuddyCard: React.FC<{
|
||||
: "text-amber-500";
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-muted/20 p-4 space-y-3">
|
||||
<div className="rounded-lg border bg-card p-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProviderIconBadge providerId="codebuddy" size="sm" />
|
||||
<span className="text-sm font-medium">{t('ai.codebuddy.title')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2 leading-5">
|
||||
{t('ai.codebuddy.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p className="min-w-0 text-xs text-muted-foreground leading-5">
|
||||
{t('ai.codebuddy.description')}
|
||||
</p>
|
||||
<div className={cn("text-xs font-medium shrink-0", statusClassName)}>
|
||||
{statusText}
|
||||
</div>
|
||||
|
||||
@@ -54,6 +54,7 @@ export const CursorSdkCard: React.FC<{
|
||||
const hasStoredApiKey = Boolean(encryptedApiKey);
|
||||
const usesEnvApiKey = pathInfo?.authSource === "CURSOR_API_KEY";
|
||||
const hasAnyApiKey = hasStoredApiKey || usesEnvApiKey;
|
||||
const canSave = !isSaving && !isDecrypting && (Boolean(apiKeyDraft.trim()) || hasStoredApiKey);
|
||||
|
||||
const installStatus = isResolvingPath
|
||||
? t("ai.cursor.detecting")
|
||||
@@ -114,7 +115,13 @@ export const CursorSdkCard: React.FC<{
|
||||
setSaved(false);
|
||||
setApiKeyDraft(event.target.value);
|
||||
}}
|
||||
placeholder={isDecrypting ? t("ai.providers.apiKey.decrypting") : t("ai.cursor.apiKeyPlaceholder")}
|
||||
placeholder={
|
||||
isDecrypting
|
||||
? t("ai.providers.apiKey.decrypting")
|
||||
: usesEnvApiKey && !hasStoredApiKey
|
||||
? t("ai.cursor.apiKeyPlaceholder.env")
|
||||
: t("ai.cursor.apiKeyPlaceholder")
|
||||
}
|
||||
disabled={isDecrypting}
|
||||
className="w-full h-8 rounded-md border border-input bg-background px-3 pr-9 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||
/>
|
||||
@@ -127,7 +134,7 @@ export const CursorSdkCard: React.FC<{
|
||||
{showApiKey ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleSave} disabled={isSaving || isDecrypting}>
|
||||
<Button variant="outline" size="sm" onClick={handleSave} disabled={!canSave}>
|
||||
{saved ? <Check size={14} className="mr-1.5" /> : null}
|
||||
{saved ? t("ai.cursor.saved") : t("ai.cursor.saveApiKey")}
|
||||
</Button>
|
||||
@@ -136,6 +143,16 @@ export const CursorSdkCard: React.FC<{
|
||||
{t("ai.cursor.check")}
|
||||
</Button>
|
||||
</div>
|
||||
{usesEnvApiKey && !hasStoredApiKey ? (
|
||||
<p className="text-[11px] text-muted-foreground leading-4">
|
||||
{t("ai.cursor.apiKeyEnvHint")}
|
||||
</p>
|
||||
) : null}
|
||||
{usesEnvApiKey && hasStoredApiKey ? (
|
||||
<p className="text-[11px] text-muted-foreground leading-4">
|
||||
{t("ai.cursor.apiKeyOverrideHint")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -163,13 +163,13 @@ export const QuickMessagesSettings: React.FC<QuickMessagesSettingsProps> = ({
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<SettingCard padded className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<SettingCard padded className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground/80 leading-5">
|
||||
{t("ai.quickMessages.description")}
|
||||
</p>
|
||||
|
||||
{showEditor ? (
|
||||
<div className="rounded-md border border-border/60 bg-background/70 p-4 space-y-3">
|
||||
<div className="rounded-md border border-border/60 bg-background/40 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium">
|
||||
{isCreating ? t("ai.quickMessages.createTitle") : t("ai.quickMessages.editTitle")}
|
||||
@@ -249,23 +249,23 @@ export const QuickMessagesSettings: React.FC<QuickMessagesSettingsProps> = ({
|
||||
) : null}
|
||||
|
||||
{sortedMessages.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="border-t border-border/60 divide-y divide-border/60">
|
||||
{sortedMessages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className="rounded-md border border-border/60 bg-background/70 p-3"
|
||||
className="py-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare size={14} className="text-primary/70 shrink-0" />
|
||||
<span className="font-medium">{message.name}</span>
|
||||
<span className="text-xs font-mono text-muted-foreground">/{message.slug}</span>
|
||||
<MessageSquare size={14} className="text-muted-foreground shrink-0" />
|
||||
<span className="text-sm font-medium">{message.name}</span>
|
||||
<span className="text-xs font-mono text-muted-foreground/80">/{message.slug}</span>
|
||||
</div>
|
||||
{message.description ? (
|
||||
<p className="text-sm text-muted-foreground">{message.description}</p>
|
||||
<p className="text-xs text-muted-foreground leading-5">{message.description}</p>
|
||||
) : null}
|
||||
<p className="text-xs text-muted-foreground/80 line-clamp-2 whitespace-pre-wrap">
|
||||
<p className="text-xs text-muted-foreground/70 line-clamp-2 whitespace-pre-wrap">
|
||||
{message.content}
|
||||
</p>
|
||||
</div>
|
||||
@@ -273,7 +273,7 @@ export const QuickMessagesSettings: React.FC<QuickMessagesSettingsProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => beginEdit(message)}
|
||||
aria-label={t("ai.quickMessages.editTitle")}
|
||||
>
|
||||
@@ -282,7 +282,7 @@ export const QuickMessagesSettings: React.FC<QuickMessagesSettingsProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => handleDelete(message)}
|
||||
aria-label={t("ai.quickMessages.confirmDelete", { name: message.name })}
|
||||
>
|
||||
@@ -294,8 +294,7 @@ export const QuickMessagesSettings: React.FC<QuickMessagesSettingsProps> = ({
|
||||
))}
|
||||
</div>
|
||||
) : !showEditor ? (
|
||||
<div className="rounded-lg border border-dashed border-border/60 p-6 text-center">
|
||||
<MessageSquare size={24} className="mx-auto text-muted-foreground mb-2" />
|
||||
<div className="border-t border-border/60 pt-3 text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">{t("ai.quickMessages.empty")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -458,6 +458,30 @@ test("resolve-cli exposes Cursor SDK support when API key is saved in settings",
|
||||
}
|
||||
});
|
||||
|
||||
test("resolve-cli reports settings as Cursor auth source when settings and env keys both exist", async () => {
|
||||
const { bridge, restore } = loadBridgeWithMocks({
|
||||
resolveCliFromPath: () => "/usr/local/bin/cursor",
|
||||
shellEnv: { CURSOR_API_KEY: "env-key" },
|
||||
});
|
||||
const ipcMain = createIpcMainStub();
|
||||
bridge.init({ sessions: new Map(), sftpClients: new Map(), electronModule: { app: { getPath: () => process.cwd() } } });
|
||||
bridge.registerHandlers(ipcMain);
|
||||
|
||||
try {
|
||||
const resolveCli = ipcMain.handlers.get("netcatty:ai:resolve-cli");
|
||||
const result = await resolveCli(
|
||||
{ sender: { id: 1 } },
|
||||
{ command: "cursor", customPath: "", apiKeyPresent: true },
|
||||
);
|
||||
|
||||
assert.equal(result.available, true);
|
||||
assert.equal(result.authenticated, true);
|
||||
assert.equal(result.authSource, "settings");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("resolve-cli can refresh shell env before resolving Cursor", async () => {
|
||||
let refreshed = false;
|
||||
const { bridge, restore } = loadBridgeWithMocks({
|
||||
@@ -483,6 +507,28 @@ test("resolve-cli can refresh shell env before resolving Cursor", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("discover exposes Cursor SDK support when API key is saved in settings", async () => {
|
||||
const { bridge, restore } = loadBridgeWithMocks({
|
||||
resolveCliFromPath: () => null,
|
||||
});
|
||||
const ipcMain = createIpcMainStub();
|
||||
bridge.init({ sessions: new Map(), sftpClients: new Map(), electronModule: { app: { getPath: () => process.cwd() } } });
|
||||
bridge.registerHandlers(ipcMain);
|
||||
|
||||
try {
|
||||
const discover = ipcMain.handlers.get("netcatty:ai:agents:discover");
|
||||
const agents = await discover({ sender: { id: 1 } }, { apiKeyPresent: true });
|
||||
const cursor = agents.find((agent) => agent.command === "cursor");
|
||||
|
||||
assert.equal(cursor?.path, "cursor");
|
||||
assert.equal(cursor?.available, true);
|
||||
assert.equal(cursor?.authenticated, true);
|
||||
assert.equal(cursor?.authSource, "settings");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("discover can refresh shell env before scanning Cursor", async () => {
|
||||
let refreshed = false;
|
||||
const { bridge, restore } = loadBridgeWithMocks({
|
||||
|
||||
@@ -26,7 +26,7 @@ async function probeCursorSdkAvailability(shellEnv, options = {}) {
|
||||
installed: true,
|
||||
available: authenticated,
|
||||
authenticated,
|
||||
authSource: hasEnvApiKey ? "CURSOR_API_KEY" : hasSettingsApiKey ? "settings" : null,
|
||||
authSource: hasSettingsApiKey ? "settings" : hasEnvApiKey ? "CURSOR_API_KEY" : null,
|
||||
version: "Cursor SDK",
|
||||
};
|
||||
}
|
||||
@@ -58,8 +58,9 @@ function registerAgentDiscoveryHandlers(ctx) {
|
||||
for (const agent of knownAgents) {
|
||||
let cursorSdkStatus = null;
|
||||
if (agent.command === "cursor") {
|
||||
if (!shellEnv.CURSOR_API_KEY) continue;
|
||||
cursorSdkStatus = await probeCursorSdkAvailability(shellEnv);
|
||||
cursorSdkStatus = await probeCursorSdkAvailability(shellEnv, {
|
||||
apiKeyPresent: Boolean(options?.apiKeyPresent),
|
||||
});
|
||||
if (!cursorSdkStatus.available) continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ const { getDriver, listBackends } = require("./index.cjs");
|
||||
const { buildSdkAgentEnv } = require("./env.cjs");
|
||||
const { buildInjectedMcpServers } = require("./injectMcp.cjs");
|
||||
const { createStreamEmitter } = require("./emit.cjs");
|
||||
const crypto = require("node:crypto");
|
||||
const { realpathSync } = require("node:fs");
|
||||
|
||||
const VALID_BACKENDS = new Set(listBackends());
|
||||
@@ -41,17 +40,6 @@ function normalizeHistoryMessages(historyMessages) {
|
||||
.filter((msg) => msg.content.length > 0);
|
||||
}
|
||||
|
||||
function summarizeSecret(value) {
|
||||
const text = String(value || "");
|
||||
if (!text) return null;
|
||||
return {
|
||||
length: text.length,
|
||||
prefix: text.slice(0, 4),
|
||||
suffix: text.slice(-4),
|
||||
sha256: crypto.createHash("sha256").update(text).digest("hex").slice(0, 16),
|
||||
};
|
||||
}
|
||||
|
||||
function logCursorApiKeySummary({ requestedAgentEnv, shellEnv, env }) {
|
||||
const requestedKey = requestedAgentEnv?.CURSOR_API_KEY;
|
||||
const shellKey = shellEnv?.CURSOR_API_KEY;
|
||||
@@ -65,7 +53,7 @@ function logCursorApiKeySummary({ requestedAgentEnv, shellEnv, env }) {
|
||||
: "missing";
|
||||
console.info("[Cursor SDK] API key summary", {
|
||||
source,
|
||||
effective: summarizeSecret(effectiveKey),
|
||||
hasEffectiveKey: Boolean(effectiveKey),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user