From 667804043cec64a9171c89c95f2dc2ae4c20aa52 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 18 Aug 2026 16:58:27 -0400 Subject: [PATCH] Implement cli commands to configure ai clients --- .../virtualApiKey/listMyVirtualApiKeys.ts | 23 +++++++++- server/routers/virtualApiKey/types.ts | 2 + .../resource/[resourceGuid]/keys/page.tsx | 7 ++- src/components/UserVirtualApiKeys.tsx | 11 ++++- .../ai-client-config/AiClientConfigCard.tsx | 23 +++++++--- .../AiClientConfigSection.tsx | 10 +++- .../LauncherResourcePanel.tsx | 2 + src/lib/aiClientConfig.ts | 46 ++++++++++++++----- 8 files changed, 99 insertions(+), 25 deletions(-) diff --git a/server/routers/virtualApiKey/listMyVirtualApiKeys.ts b/server/routers/virtualApiKey/listMyVirtualApiKeys.ts index 46531560a..b1a724468 100644 --- a/server/routers/virtualApiKey/listMyVirtualApiKeys.ts +++ b/server/routers/virtualApiKey/listMyVirtualApiKeys.ts @@ -20,6 +20,7 @@ import { toPublicVirtualApiKey } from "@server/lib/virtualApiKey"; import type { ListMyVirtualApiKeysResponse } from "@server/routers/virtualApiKey/types"; +import { formatPublicResourceAccess } from "@server/routers/launcher/formatLauncherAccess"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -125,11 +126,19 @@ export async function listMyVirtualApiKeys( let resourceId: number | undefined; let resourceName: string | undefined; + let resourceNiceId: string | undefined; + let resourceAccessUrl: string | null = null; if (resourceGuid) { const [resource] = await db .select({ resourceId: resources.resourceId, - name: resources.name + name: resources.name, + niceId: resources.niceId, + mode: resources.mode, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + proxyPort: resources.proxyPort, + wildcard: resources.wildcard }) .from(resources) .where( @@ -151,6 +160,14 @@ export async function listMyVirtualApiKeys( resourceId = resource.resourceId; resourceName = resource.name; + resourceNiceId = resource.niceId; + resourceAccessUrl = formatPublicResourceAccess({ + mode: resource.mode ?? "", + fullDomain: resource.fullDomain, + ssl: resource.ssl, + proxyPort: resource.proxyPort, + wildcard: resource.wildcard + }).accessUrl; } const [user] = await db @@ -223,7 +240,9 @@ export async function listMyVirtualApiKeys( manualKeys: manualRows.map((row) => toKeyWithResources(row, resourceIdsByKey) ), - ...(resourceName !== undefined ? { resourceName } : {}) + ...(resourceName !== undefined ? { resourceName } : {}), + ...(resourceNiceId !== undefined ? { resourceNiceId } : {}), + ...(resourceNiceId !== undefined ? { resourceAccessUrl } : {}) }, success: true, error: false, diff --git a/server/routers/virtualApiKey/types.ts b/server/routers/virtualApiKey/types.ts index e6a387446..9b528a9cd 100644 --- a/server/routers/virtualApiKey/types.ts +++ b/server/routers/virtualApiKey/types.ts @@ -23,6 +23,8 @@ export type ListMyVirtualApiKeysResponse = { userKey: VirtualApiKeyWithResources; manualKeys: VirtualApiKeyWithResources[]; resourceName?: string | null; + resourceNiceId?: string | null; + resourceAccessUrl?: string | null; }; export type GetMyVirtualApiKeyResponse = { diff --git a/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx b/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx index dd673ac85..ef3e18ee9 100644 --- a/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx +++ b/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx @@ -113,7 +113,12 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) { launcherMode showViewAsAdmin={isAdminOrOwner} > - + ); diff --git a/src/components/UserVirtualApiKeys.tsx b/src/components/UserVirtualApiKeys.tsx index fa0bf0eb8..6f06382a2 100644 --- a/src/components/UserVirtualApiKeys.tsx +++ b/src/components/UserVirtualApiKeys.tsx @@ -26,6 +26,10 @@ import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat"; type UserVirtualApiKeysProps = { orgId: string; initialData: ListMyVirtualApiKeysResponse; + /** The resource's niceId, when this page is scoped to a single resource. */ + resourceNiceId?: string; + /** The resource's real access URL, when known; falls back to a placeholder otherwise. */ + endpoint?: string; }; function OwnedKeySecret({ @@ -163,7 +167,9 @@ function ManualKeyRow({ export default function UserVirtualApiKeys({ orgId, - initialData + initialData, + resourceNiceId, + endpoint }: UserVirtualApiKeysProps) { const t = useTranslations(); const resourceName = initialData.resourceName; @@ -217,11 +223,12 @@ export default function UserVirtualApiKeys({ diff --git a/src/components/ai-client-config/AiClientConfigCard.tsx b/src/components/ai-client-config/AiClientConfigCard.tsx index 709e7ac33..8b6747896 100644 --- a/src/components/ai-client-config/AiClientConfigCard.tsx +++ b/src/components/ai-client-config/AiClientConfigCard.tsx @@ -40,6 +40,7 @@ type AiClientConfigCardProps = { keyAuth: AiClientAuthInput; description: string; iconSrc: { light: string; dark: string }; + resourceNiceId?: string; }; export function AiClientConfigCard({ @@ -48,7 +49,8 @@ export function AiClientConfigCard({ endpoint, keyAuth, description, - iconSrc + iconSrc, + resourceNiceId }: AiClientConfigCardProps) { const t = useTranslations(); const { theme } = useTheme(); @@ -94,16 +96,23 @@ export function AiClientConfigCard({ const guide = useMemo(() => { if (keyAuth.mode === "keyless") { - return buildAiClientGuide(clientId, endpoint, { mode: "keyless" }); + return buildAiClientGuide( + clientId, + endpoint, + { mode: "keyless" }, + resourceNiceId + ); } if (revealedKey === null) { return null; } - return buildAiClientGuide(clientId, endpoint, { - mode: "keyed", - key: revealedKey - }); - }, [clientId, endpoint, keyAuth.mode, revealedKey]); + return buildAiClientGuide( + clientId, + endpoint, + { mode: "keyed", key: revealedKey }, + resourceNiceId + ); + }, [clientId, endpoint, keyAuth.mode, revealedKey, resourceNiceId]); const preset = guide?.presets.find((p) => p.id === presetId) ?? guide?.presets[0]; diff --git a/src/components/ai-client-config/AiClientConfigSection.tsx b/src/components/ai-client-config/AiClientConfigSection.tsx index 5998bf615..364022e7f 100644 --- a/src/components/ai-client-config/AiClientConfigSection.tsx +++ b/src/components/ai-client-config/AiClientConfigSection.tsx @@ -27,6 +27,12 @@ type AiClientConfigSectionProps = { */ layout?: "wide" | "compact"; className?: string; + /** + * The resource's niceId, when known, so the displayed CLI commands can + * include `--resource ` and let `pangolin configure` skip its + * own resource picker. + */ + resourceNiceId?: string; }; // Each logo file is named for its own color, not the theme it belongs on, so @@ -54,7 +60,8 @@ export function AiClientConfigSection({ endpoint, auth, layout = "compact", - className + className, + resourceNiceId }: AiClientConfigSectionProps) { const t = useTranslations(); const isWide = layout === "wide"; @@ -93,6 +100,7 @@ export function AiClientConfigSection({ keyAuth={auth} description={descriptions[clientId]} iconSrc={CLIENT_LOGOS[clientId]} + resourceNiceId={resourceNiceId} /> ))} diff --git a/src/components/resource-launcher/LauncherResourcePanel.tsx b/src/components/resource-launcher/LauncherResourcePanel.tsx index 83f4ac14f..e35d9dce6 100644 --- a/src/components/resource-launcher/LauncherResourcePanel.tsx +++ b/src/components/resource-launcher/LauncherResourcePanel.tsx @@ -351,6 +351,7 @@ function PublicResourceDetails({ mode: "keyed", getKeyText: getAiKeyCopyText }} + resourceNiceId={launcherResource.niceId} /> ) : null} @@ -428,6 +429,7 @@ function PrivateResourceDetails({ ) : null} diff --git a/src/lib/aiClientConfig.ts b/src/lib/aiClientConfig.ts index 83cca4d6d..9107d62b9 100644 --- a/src/lib/aiClientConfig.ts +++ b/src/lib/aiClientConfig.ts @@ -59,12 +59,17 @@ function block( function buildCli( clientArg: "claude" | "codex" | "opencode", - auth: AiClientAuth + auth: AiClientAuth, + resourceNiceId?: string ): AiCliCommands { + const resourceFlag = resourceNiceId + ? ` --resource ${resourceNiceId}` + : ""; + const configure: AiConfigBlock = { id: `cli-configure-${clientArg}`, label: "Configure", - displayText: `pangolin configure ${clientArg}` + displayText: `pangolin configure ${clientArg}${resourceFlag}` }; if (auth.mode !== "keyed") { @@ -76,12 +81,16 @@ function buildCli( configureWithKey: { id: `cli-configure-key-${clientArg}`, label: "Configure with an API key", - displayText: `pangolin configure ${clientArg} ${auth.key}` + displayText: `pangolin configure ${clientArg} ${auth.key}${resourceFlag}` } }; } -function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { +function buildClaudeGuide( + endpoint: string, + auth: AiClientAuth, + resourceNiceId?: string +): AiClientGuide { const defaultSettings = block( "claude-default-settings", "~/.claude/settings.json", @@ -168,7 +177,7 @@ function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { return { id: "claude", name: AI_CLIENT_NAMES.claude, - cli: buildCli("claude", auth), + cli: buildCli("claude", auth, resourceNiceId), presets: [ { id: "default", @@ -194,7 +203,11 @@ function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { }; } -function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { +function buildCodexGuide( + endpoint: string, + auth: AiClientAuth, + resourceNiceId?: string +): AiClientGuide { const settings = block( "codex-settings", "~/.codex/config.toml", @@ -224,7 +237,7 @@ function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { return { id: "codex", name: AI_CLIENT_NAMES.codex, - cli: buildCli("codex", auth), + cli: buildCli("codex", auth, resourceNiceId), presets: [ { id: "default", @@ -235,7 +248,11 @@ function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { }; } -function buildOpencodeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { +function buildOpencodeGuide( + endpoint: string, + auth: AiClientAuth, + resourceNiceId?: string +): AiClientGuide { const config = block( "opencode-config", "Step 1: opencode.json", @@ -273,7 +290,7 @@ function buildOpencodeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide return { id: "opencode", name: AI_CLIENT_NAMES.opencode, - cli: buildCli("opencode", auth), + cli: buildCli("opencode", auth, resourceNiceId), presets: [ { id: "default", @@ -318,7 +335,11 @@ function buildCursorGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { const GUIDE_BUILDERS: Record< AiClientId, - (endpoint: string, auth: AiClientAuth) => AiClientGuide + ( + endpoint: string, + auth: AiClientAuth, + resourceNiceId?: string + ) => AiClientGuide > = { claude: buildClaudeGuide, codex: buildCodexGuide, @@ -329,7 +350,8 @@ const GUIDE_BUILDERS: Record< export function buildAiClientGuide( clientId: AiClientId, endpoint: string, - auth: AiClientAuth + auth: AiClientAuth, + resourceNiceId?: string ): AiClientGuide { - return GUIDE_BUILDERS[clientId](endpoint, auth); + return GUIDE_BUILDERS[clientId](endpoint, auth, resourceNiceId); }