diff --git a/core/config/ConfigHandler.ts b/core/config/ConfigHandler.ts index 64669f68d8..1ac5dffde4 100644 --- a/core/config/ConfigHandler.ts +++ b/core/config/ConfigHandler.ts @@ -177,6 +177,7 @@ export class ConfigHandler { const localProfileLoader = new LocalProfileLoader( ide, ideSettingsPromise, + controlPlaneClient, writeLog, ); this.profiles = [new ProfileLifecycleManager(localProfileLoader)]; diff --git a/core/config/load.ts b/core/config/load.ts index 3609fd2415..992cd644b8 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -36,6 +36,7 @@ import CustomLLMClass from "../llm/llms/CustomLLM.js"; import FreeTrial from "../llm/llms/FreeTrial.js"; import { llmFromDescription } from "../llm/llms/index.js"; +import ContinueProxyContextProvider from "../context/providers/ContinueProxyContextProvider.js"; import { fetchwithRequestOptions } from "../util/fetchWithOptions.js"; import { copyOf } from "../util/index.js"; import mergeJson from "../util/merge.js"; @@ -59,6 +60,8 @@ import { getPromptFiles, slashCommandFromPromptFile, } from "./promptFile.js"; +import CodebaseContextProvider from "../context/providers/CodebaseContextProvider.js"; + const { execSync } = require("child_process"); function resolveSerializedConfig(filepath: string): SerializedContinueConfig { @@ -94,13 +97,16 @@ function loadSerializedConfig( workspaceConfigs: ContinueRcJson[], ideSettings: IdeSettings, ideType: IdeType, + overrideConfigJson: SerializedContinueConfig | undefined, ): SerializedContinueConfig { const configPath = getConfigJsonPath(ideType); - let config: SerializedContinueConfig; - try { - config = resolveSerializedConfig(configPath); - } catch (e) { - throw new Error(`Failed to parse config.json: ${e}`); + let config: SerializedContinueConfig = overrideConfigJson!; + if (!config) { + try { + config = resolveSerializedConfig(configPath); + } catch (e) { + throw new Error(`Failed to parse config.json: ${e}`); + } } if (config.allowAnonymousTelemetry === undefined) { @@ -210,6 +216,7 @@ async function intermediateToFinalConfig( ideSettings: IdeSettings, uniqueId: string, writeLog: (log: string) => Promise, + workOsAccessToken: string | undefined, allowFreeTrial: boolean = true, ): Promise { // Auto-detect models @@ -347,8 +354,15 @@ async function intermediateToFinalConfig( ).filter((x) => x !== undefined) as BaseLLM[]; } + // These context providers are always included, regardless of what, if anything, + // the user has configured in config.json + const DEFAULT_CONTEXT_PROVIDERS = [ + new FileContextProvider({}), + new CodebaseContextProvider({}), + ]; + // Context providers - const contextProviders: IContextProvider[] = [new FileContextProvider({})]; + const contextProviders: IContextProvider[] = DEFAULT_CONTEXT_PROVIDERS; for (const provider of config.contextProviders || []) { if (isContextProviderWithParams(provider)) { const cls = contextProviderClassFromName(provider.name) as any; @@ -356,7 +370,15 @@ async function intermediateToFinalConfig( console.warn(`Unknown context provider ${provider.name}`); continue; } - contextProviders.push(new cls(provider.params)); + const instance: IContextProvider = new cls(provider.params); + + // Handle continue-proxy + if (instance.description.title === "continue-proxy") { + (instance as ContinueProxyContextProvider).workOsAccessToken = + workOsAccessToken; + } + + contextProviders.push(instance); } else { contextProviders.push(new CustomContextProviderClass(provider)); } @@ -536,9 +558,16 @@ async function loadFullConfigNode( ideType: IdeType, uniqueId: string, writeLog: (log: string) => Promise, + workOsAccessToken: string | undefined, + overrideConfigJson: SerializedContinueConfig | undefined, ): Promise { // Serialized config - let serialized = loadSerializedConfig(workspaceConfigs, ideSettings, ideType); + let serialized = loadSerializedConfig( + workspaceConfigs, + ideSettings, + ideType, + overrideConfigJson, + ); // Convert serialized to intermediate config let intermediate = await serializedToIntermediateConfig(serialized, ide); @@ -584,6 +613,7 @@ async function loadFullConfigNode( ideSettings, uniqueId, writeLog, + workOsAccessToken, ); return finalConfig; } diff --git a/core/config/profile/ControlPlaneProfileLoader.ts b/core/config/profile/ControlPlaneProfileLoader.ts index e8a2762a5d..0602088bd6 100644 --- a/core/config/profile/ControlPlaneProfileLoader.ts +++ b/core/config/profile/ControlPlaneProfileLoader.ts @@ -6,20 +6,8 @@ import { SerializedContinueConfig, } from "../.."; import { ControlPlaneClient } from "../../control-plane/client"; -import { TeamAnalytics } from "../../control-plane/TeamAnalytics"; -import ContinueProxy from "../../llm/llms/stubs/ContinueProxy"; -import { Telemetry } from "../../util/posthog"; -import { - defaultContextProvidersJetBrains, - defaultContextProvidersVsCode, - defaultSlashCommandsJetBrains, - defaultSlashCommandsVscode, -} from "../default"; -import { - intermediateToFinalConfig, - serializedToIntermediateConfig, -} from "../load"; import { IProfileLoader } from "./IProfileLoader"; +import doLoadConfig from "./doLoadConfig"; export default class ControlPlaneProfileLoader implements IProfileLoader { private static RELOAD_INTERVAL = 1000 * 60 * 15; // every 15 minutes @@ -49,58 +37,20 @@ export default class ControlPlaneProfileLoader implements IProfileLoader { } async doLoadConfig(): Promise { - const ideInfo = await this.ide.getIdeInfo(); const settings = this.workspaceSettings ?? ((await this.controlPlaneClient.getSettingsForWorkspace( this.profileId, )) as any); - - // First construct a SerializedContinueConfig from the ControlPlaneSettings (TODO) const serializedConfig: SerializedContinueConfig = settings; - serializedConfig.contextProviders ??= - ideInfo.ideType === "vscode" - ? defaultContextProvidersVsCode - : defaultContextProvidersJetBrains; - serializedConfig.slashCommands ??= - ideInfo.ideType === "vscode" - ? defaultSlashCommandsVscode - : defaultSlashCommandsJetBrains; - - const intermediateConfig = await serializedToIntermediateConfig( - serializedConfig, + return doLoadConfig( this.ide, - ); - - const uniqueId = await this.ide.getUniqueId(); - const finalConfig = await intermediateToFinalConfig( - intermediateConfig, - this.ide, - await this.ideSettingsPromise, - uniqueId, + this.ideSettingsPromise, + this.controlPlaneClient, this.writeLog, + serializedConfig, ); - - // Set up team analytics/telemetry - await Telemetry.setup(true, uniqueId, ideInfo.extensionVersion); - await TeamAnalytics.setup( - settings.analytics, - uniqueId, - ideInfo.extensionVersion, - ); - - [ - ...finalConfig.models, - ...(finalConfig.tabAutocompleteModels ?? []), - ].forEach(async (model) => { - if (model.providerName === "continue-proxy") { - const accessToken = await this.controlPlaneClient.getAccessToken(); - (model as ContinueProxy).workOsAccessToken = accessToken; - } - }); - - return finalConfig; } setIsActive(isActive: boolean): void {} diff --git a/core/config/profile/LocalProfileLoader.ts b/core/config/profile/LocalProfileLoader.ts index ec6fba1896..a79406d499 100644 --- a/core/config/profile/LocalProfileLoader.ts +++ b/core/config/profile/LocalProfileLoader.ts @@ -1,6 +1,6 @@ -import { ContinueConfig, ContinueRcJson, IDE, IdeSettings } from "../.."; -import { Telemetry } from "../../util/posthog"; -import { loadFullConfigNode } from "../load"; +import { ContinueConfig, IDE, IdeSettings } from "../.."; +import { ControlPlaneClient } from "../../control-plane/client"; +import doLoadConfig from "./doLoadConfig"; import { IProfileLoader } from "./IProfileLoader"; export default class LocalProfileLoader implements IProfileLoader { @@ -11,42 +11,18 @@ export default class LocalProfileLoader implements IProfileLoader { constructor( private ide: IDE, private ideSettingsPromise: Promise, - // private controlPlaneClient: ControlPlaneClient, + private controlPlaneClient: ControlPlaneClient, private writeLog: (message: string) => Promise, ) {} async doLoadConfig(): Promise { - let workspaceConfigs: ContinueRcJson[] = []; - try { - workspaceConfigs = await this.ide.getWorkspaceConfigs(); - } catch (e) { - console.warn("Failed to load workspace configs"); - } - - const ideInfo = await this.ide.getIdeInfo(); - const uniqueId = await this.ide.getUniqueId(); - const ideSettings = await this.ideSettingsPromise; - - const newConfig = await loadFullConfigNode( + return doLoadConfig( this.ide, - workspaceConfigs, - ideSettings, - ideInfo.ideType, - uniqueId, + this.ideSettingsPromise, + this.controlPlaneClient, this.writeLog, + undefined, ); - newConfig.allowAnonymousTelemetry = - newConfig.allowAnonymousTelemetry && - (await this.ide.isTelemetryEnabled()); - - // Setup telemetry only after (and if) we know it is enabled - await Telemetry.setup( - newConfig.allowAnonymousTelemetry ?? true, - await this.ide.getUniqueId(), - ideInfo.extensionVersion, - ); - - return newConfig; } setIsActive(isActive: boolean): void {} diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts new file mode 100644 index 0000000000..35f7bdf254 --- /dev/null +++ b/core/config/profile/doLoadConfig.ts @@ -0,0 +1,69 @@ +import { + ContinueRcJson, + IDE, + IdeSettings, + SerializedContinueConfig, +} from "../.."; +import { ControlPlaneClient } from "../../control-plane/client"; +import { TeamAnalytics } from "../../control-plane/TeamAnalytics"; +import ContinueProxy from "../../llm/llms/stubs/ContinueProxy"; +import { Telemetry } from "../../util/posthog"; +import { loadFullConfigNode } from "../load"; + +export default async function doLoadConfig( + ide: IDE, + ideSettingsPromise: Promise, + controlPlaneClient: ControlPlaneClient, + writeLog: (message: string) => Promise, + overrideConfigJson: SerializedContinueConfig | undefined, +) { + let workspaceConfigs: ContinueRcJson[] = []; + try { + workspaceConfigs = await ide.getWorkspaceConfigs(); + } catch (e) { + console.warn("Failed to load workspace configs"); + } + + const ideInfo = await ide.getIdeInfo(); + const uniqueId = await ide.getUniqueId(); + const ideSettings = await ideSettingsPromise; + const workOsAccessToken = await controlPlaneClient.getAccessToken(); + + const newConfig = await loadFullConfigNode( + ide, + workspaceConfigs, + ideSettings, + ideInfo.ideType, + uniqueId, + writeLog, + workOsAccessToken, + overrideConfigJson, + ); + newConfig.allowAnonymousTelemetry = + newConfig.allowAnonymousTelemetry && (await ide.isTelemetryEnabled()); + + // Setup telemetry only after (and if) we know it is enabled + await Telemetry.setup( + newConfig.allowAnonymousTelemetry ?? true, + await ide.getUniqueId(), + ideInfo.extensionVersion, + ); + + if (newConfig.analytics) { + await TeamAnalytics.setup( + newConfig.analytics as any, // TODO: Need to get rid of index.d.ts once and for all + uniqueId, + ideInfo.extensionVersion, + ); + } + + [...newConfig.models, ...(newConfig.tabAutocompleteModels ?? [])].forEach( + async (model) => { + if (model.providerName === "continue-proxy") { + (model as ContinueProxy).workOsAccessToken = workOsAccessToken; + } + }, + ); + + return newConfig; +} diff --git a/core/context/providers/ContinueProxyContextProvider.ts b/core/context/providers/ContinueProxyContextProvider.ts new file mode 100644 index 0000000000..dc1c9ef222 --- /dev/null +++ b/core/context/providers/ContinueProxyContextProvider.ts @@ -0,0 +1,76 @@ +import { CONTROL_PLANE_URL } from "../../control-plane/client.js"; +import { + ContextItem, + ContextProviderDescription, + ContextProviderExtras, + ContextSubmenuItem, + LoadSubmenuItemsArgs, +} from "../../index.js"; +import { BaseContextProvider } from "../index.js"; + +class ContinueProxyContextProvider extends BaseContextProvider { + static description: ContextProviderDescription = { + title: "continue-proxy", + displayTitle: "Continue Proxy", + description: "Retrieve a context item from a Continue for Teams add-on", + type: "submenu", + }; + + workOsAccessToken: string | undefined = undefined; + + override get description(): ContextProviderDescription { + return { + title: + this.options.title || ContinueProxyContextProvider.description.title, + displayTitle: + this.options.displayTitle || + ContinueProxyContextProvider.description.displayTitle, + description: + this.options.description || + ContinueProxyContextProvider.description.description, + type: this.options.type || ContinueProxyContextProvider.description.type, + }; + } + + async loadSubmenuItems( + args: LoadSubmenuItemsArgs, + ): Promise { + const response = await args.fetch( + new URL(`/proxy/context/${this.options.id}/list`, CONTROL_PLANE_URL), + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.workOsAccessToken}`, + }, + }, + ); + const data = await response.json(); + return data.items; + } + + async getContextItems( + query: string, + extras: ContextProviderExtras, + ): Promise { + const response = await extras.fetch( + new URL(`/proxy/context/${this.options.id}/retrieve`, CONTROL_PLANE_URL), + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.workOsAccessToken}`, + }, + body: JSON.stringify({ + query: query || "", + fullInput: extras.fullInput, + }), + }, + ); + + const items: any = await response.json(); + return items; + } +} + +export default ContinueProxyContextProvider; diff --git a/core/context/providers/DocsContextProvider.ts b/core/context/providers/DocsContextProvider.ts index c79ee48857..c2c6af7f0d 100644 --- a/core/context/providers/DocsContextProvider.ts +++ b/core/context/providers/DocsContextProvider.ts @@ -157,7 +157,10 @@ class DocsContextProvider extends BaseContextProvider { name: "Instructions", description: "Instructions", content: - "Use the above documentation to answer the following question. You should not reference anything outside of what is shown, unless it is a commonly known concept. Reference URLs whenever possible using markdown formatting. If there isn't enough information to answer the question, suggest where the user might look to learn more.", + "Use the above documentation to answer the following question. You should not reference " + + "anything outside of what is shown, unless it is a commonly known concept. Reference URLs " + + "whenever possible using markdown formatting. If there isn't enough information to answer " + + "the question, suggest where the user might look to learn more.", }, ]; } @@ -167,7 +170,9 @@ class DocsContextProvider extends BaseContextProvider { ): Promise { const ideInfo = await args.ide.getIdeInfo(); const isJetBrains = ideInfo.ideType === "jetbrains"; - const configSites = this.options?.sites || []; + const configSites = [ + ...new Set([...(this.options?.sites || []), ...(args.config.docs || [])]), + ]; const submenuItemsMap = new Map(); if (!isJetBrains) { diff --git a/core/context/providers/index.ts b/core/context/providers/index.ts index ac1d0431ae..462fe0db49 100644 --- a/core/context/providers/index.ts +++ b/core/context/providers/index.ts @@ -2,6 +2,7 @@ import { ContextProviderName } from "../../index.js"; import { BaseContextProvider } from "../index.js"; import CodeContextProvider from "./CodeContextProvider.js"; import CodebaseContextProvider from "./CodebaseContextProvider.js"; +import ContinueProxyContextProvider from "./ContinueProxyContextProvider.js"; import CurrentFileContextProvider from "./CurrentFileContextProvider.js"; import DatabaseContextProvider from "./DatabaseContextProvider.js"; import DiffContextProvider from "./DiffContextProvider.js"; @@ -40,7 +41,6 @@ const Providers: (typeof BaseContextProvider)[] = [ HttpContextProvider, SearchContextProvider, OSContextProvider, - CodebaseContextProvider, ProblemsContextProvider, FolderContextProvider, DocsContextProvider, @@ -51,6 +51,7 @@ const Providers: (typeof BaseContextProvider)[] = [ CodeContextProvider, CurrentFileContextProvider, URLContextProvider, + ContinueProxyContextProvider, ]; export function contextProviderClassFromName( diff --git a/core/context/retrieval/retrieval.ts b/core/context/retrieval/retrieval.ts index 3d7f083834..e6ff30e17d 100644 --- a/core/context/retrieval/retrieval.ts +++ b/core/context/retrieval/retrieval.ts @@ -24,7 +24,10 @@ export async function retrieveContextItemsFromEmbeddings( (await extras.ide.getIdeInfo()).ideType === "jetbrains" ) { throw new Error( - "The transformers.js context provider is not currently supported in JetBrains. For now, you can use Ollama to set up local embeddings, or use our 'free-trial' embeddings provider. See here to learn more: https://docs.continue.dev/walkthroughs/codebase-embeddings#embeddings-providers", + "The transformers.js context provider is not currently supported in JetBrains. " + + "For now, you can use Ollama to set up local embeddings, or use our 'free-trial' " + + "embeddings provider. See here to learn more: " + + "https://docs.continue.dev/walkthroughs/codebase-embeddings#embeddings-providers", ); } @@ -81,7 +84,9 @@ export async function retrieveContextItemsFromEmbeddings( return [ ...results.map((r) => { - const name = `${getRelativePath(r.filepath, workspaceDirs)} (${r.startLine}-${r.endLine})`; + const name = `${getRelativePath(r.filepath, workspaceDirs)} (${ + r.startLine + }-${r.endLine})`; const description = `${r.filepath} (${r.startLine}-${r.endLine})`; return { name, diff --git a/core/control-plane/auth/index.ts b/core/control-plane/auth/index.ts new file mode 100644 index 0000000000..ecb6221c74 --- /dev/null +++ b/core/control-plane/auth/index.ts @@ -0,0 +1,20 @@ +import { v4 as uuidv4 } from "uuid"; + +const CLIENT_ID = "client_01J0FW6XN8N2XJAECF7NE0Y65J"; +// const CLIENT_ID = "client_01J0FW6XCPMJMQ3CG51RB4HBZQ"; // Staging + +export async function getAuthUrlForTokenPage(): Promise { + const url = new URL("https://api.workos.com/user_management/authorize"); + const params = { + response_type: "code", + client_id: CLIENT_ID, + redirect_uri: "https://app.continue.dev/tokens/callback", + // redirect_uri: "http://localhost:3000/tokens/callback", + state: uuidv4(), + provider: "authkit", + }; + Object.keys(params).forEach((key) => + url.searchParams.append(key, params[key as keyof typeof params]), + ); + return url.toString(); +} diff --git a/core/control-plane/client.ts b/core/control-plane/client.ts index f254d13d09..616f9edbf4 100644 --- a/core/control-plane/client.ts +++ b/core/control-plane/client.ts @@ -72,10 +72,14 @@ export class ControlPlaneClient { return []; } - const resp = await this.request(`/workspaces`, { - method: "GET", - }); - return (await resp.json()) as any; + try { + const resp = await this.request(`/workspaces`, { + method: "GET", + }); + return (await resp.json()) as any; + } catch (e) { + return []; + } } async getSettingsForWorkspace(workspaceId: string): Promise { diff --git a/core/core.ts b/core/core.ts index 5cf32478e3..f247914fe5 100644 --- a/core/core.ts +++ b/core/core.ts @@ -17,10 +17,10 @@ import { import { createNewPromptFile } from "./config/promptFile.js"; import { addModel, addOpenAIKey, deleteModel } from "./config/util.js"; import { ContinueServerClient } from "./continueServer/stubs/client.js"; -import { ControlPlaneClient } from "./control-plane/client.js"; +import { getAuthUrlForTokenPage } from "./control-plane/auth"; +import { ControlPlaneClient } from "./control-plane/client"; import { CodebaseIndexer, PauseToken } from "./indexing/CodebaseIndexer.js"; import { DocsService } from "./indexing/docs/DocsService.js"; -import TransformersJsEmbeddingsProvider from "./indexing/embeddings/TransformersJsEmbeddingsProvider.js"; import Ollama from "./llm/llms/Ollama.js"; import type { FromCoreProtocol, ToCoreProtocol } from "./protocol"; import { GlobalContext } from "./util/GlobalContext.js"; @@ -143,21 +143,19 @@ export class Core { ); // Index on initialization - this.ide - .getWorkspaceDirs() - .then(async (dirs) => { - // Respect pauseCodebaseIndexOnStart user settings - if (ideSettings.pauseCodebaseIndexOnStart) { - await this.messenger.request("indexProgress", { - progress: 100, - desc: "Initial Indexing Skipped", - status: "paused", - }); - return; - } + this.ide.getWorkspaceDirs().then(async (dirs) => { + // Respect pauseCodebaseIndexOnStart user settings + if (ideSettings.pauseCodebaseIndexOnStart) { + await this.messenger.request("indexProgress", { + progress: 100, + desc: "Initial Indexing Skipped", + status: "paused", + }); + return; + } - this.refreshCodebaseIndex(dirs); - }); + this.refreshCodebaseIndex(dirs); + }); }); const getLlm = async () => { @@ -280,8 +278,12 @@ export class Core { return; } - const siteIndexingOptions: SiteIndexingConfig[] = ((mProvider) => - mProvider?.options?.sites || [])({ ...provider }); + const siteIndexingOptions: SiteIndexingConfig[] = ((mProvider) => [ + ...new Set([ + ...(mProvider?.options?.sites || []), + ...(config.docs || []), + ]), + ])({ ...provider }); for (const site of siteIndexingOptions) { await this.getEmbeddingsProviderAndIndexDoc(site, msg.data.reIndex); @@ -294,6 +296,7 @@ export class Core { const items = config.contextProviders ?.find((provider) => provider.description.title === msg.data.title) ?.loadSubmenuItems({ + config, ide: this.ide, fetch: (url, init) => fetchwithRequestOptions(url, init, config.requestOptions), @@ -656,6 +659,10 @@ export class Core { on("didChangeControlPlaneSessionInfo", async (msg) => { this.configHandler.updateControlPlaneSessionInfo(msg.data.sessionInfo); }); + on("auth/getAuthUrl", async (msg) => { + const url = await getAuthUrlForTokenPage(); + return { url }; + }); } private indexingCancellationController: AbortController | undefined; diff --git a/core/index.d.ts b/core/index.d.ts index bd8f99401e..d8cfe8dbae 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -137,6 +137,7 @@ export interface ContextProviderExtras { } export interface LoadSubmenuItemsArgs { + config: ContinueConfig; ide: IDE; fetch: FetchFunction; } @@ -422,8 +423,8 @@ export interface IdeSettings { remoteConfigSyncPeriod: number; userToken: string; enableControlServerBeta: boolean; - pauseCodebaseIndexOnStart: boolean - enableDebugLogs: boolean + pauseCodebaseIndexOnStart: boolean; + enableDebugLogs: boolean; } export interface IDE { @@ -858,6 +859,12 @@ interface ExperimentalConfig { quickActions?: QuickActionConfig[]; } +interface AnalyticsConfig { + type: string; + url?: string; + clientKey?: string; +} + // config.json export interface SerializedContinueConfig { env?: string[]; @@ -878,6 +885,7 @@ export interface SerializedContinueConfig { ui?: ContinueUIConfig; reranker?: RerankerDescription; experimental?: ExperimentalConfig; + analytics?: AnalyticsConfig; } export type ConfigMergeType = "merge" | "overwrite"; @@ -928,6 +936,8 @@ export interface Config { reranker?: RerankerDescription | Reranker; /** Experimental configuration */ experimental?: ExperimentalConfig; + /** Analytics configuration */ + analytics?: AnalyticsConfig; } // in the actual Continue source code @@ -948,6 +958,8 @@ export interface ContinueConfig { ui?: ContinueUIConfig; reranker?: Reranker; experimental?: ExperimentalConfig; + analytics?: AnalyticsConfig; + docs?: SiteIndexingConfig[]; } export interface BrowserSerializedContinueConfig { @@ -965,4 +977,5 @@ export interface BrowserSerializedContinueConfig { ui?: ContinueUIConfig; reranker?: RerankerDescription; experimental?: ExperimentalConfig; + analytics?: AnalyticsConfig; } diff --git a/core/llm/llms/stubs/ContinueProxy.ts b/core/llm/llms/stubs/ContinueProxy.ts index 99eba5f73e..3049abc3b1 100644 --- a/core/llm/llms/stubs/ContinueProxy.ts +++ b/core/llm/llms/stubs/ContinueProxy.ts @@ -1,4 +1,5 @@ import type { LLMOptions, ModelProvider } from "../../.."; +import { CONTROL_PLANE_URL } from "../../../control-plane/client"; import OpenAI from "../OpenAI.js"; class ContinueProxy extends OpenAI { @@ -16,8 +17,7 @@ class ContinueProxy extends OpenAI { } static providerName: ModelProvider = "continue-proxy"; static defaultOptions: Partial = { - apiBase: - "https://control-plane-api-service-i3dqylpbqa-uc.a.run.app/model-proxy/v1", + apiBase: new URL("/model-proxy/v1", CONTROL_PLANE_URL).toString(), useLegacyCompletionsEndpoint: false, }; diff --git a/core/protocol/core.ts b/core/protocol/core.ts index 3ccc7ed16c..aa43bdd094 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -147,4 +147,6 @@ export type ToCoreFromIdeOrWebviewProtocol = { addAutocompleteModel: [{ model: ModelDescription }, void]; "profiles/switch": [{ id: string }, undefined]; + + "auth/getAuthUrl": [undefined, { url: string }]; }; diff --git a/core/util/paths.ts b/core/util/paths.ts index 1e8a5d7728..cec9b0a958 100644 --- a/core/util/paths.ts +++ b/core/util/paths.ts @@ -205,7 +205,7 @@ export async function migrate( if (!fs.existsSync(migrationPath)) { try { - await callback(); + await Promise.resolve(callback()); fs.writeFileSync(migrationPath, ""); } catch (e) { console.warn(`Migration ${id} failed`, e); diff --git a/docs/static/schemas/config.json b/docs/static/schemas/config.json index 3cc82db368..868018a01f 100644 --- a/docs/static/schemas/config.json +++ b/docs/static/schemas/config.json @@ -1828,6 +1828,33 @@ "title": "config.json", "type": "object", "properties": { + "docs": { + "title": "Docs", + "description": "A list of documentation sites to be indexed", + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The title of the documentation site" + }, + "startUrl": { + "type": "string", + "description": "The starting URL for indexing the documentation" + }, + "rootUrl": { + "type": "string", + "description": "The root URL of the documentation site" + }, + "maxDepth": { + "type": "integer", + "description": "The maximum depth to crawl the documentation site" + } + }, + "required": ["title", "startUrl"] + } + }, "allowAnonymousTelemetry": { "title": "Allow Anonymous Telemetry", "markdownDescription": "If this field is set to True, we will collect anonymous telemetry as described in the documentation page on telemetry. If set to `false`, we will not collect any data. Learn more in [the docs](https://docs.continue.dev/telemetry).", diff --git a/eval/.gitignore b/eval/.gitignore new file mode 100644 index 0000000000..2a31c3eee0 --- /dev/null +++ b/eval/.gitignore @@ -0,0 +1 @@ +repos \ No newline at end of file diff --git a/extensions/intellij/gradle.properties b/extensions/intellij/gradle.properties index caf2e7e8c8..d131d7c37e 100644 --- a/extensions/intellij/gradle.properties +++ b/extensions/intellij/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.continuedev.continueintellijextension pluginName = continue-intellij-extension pluginRepositoryUrl = https://github.com/continuedev/continue # SemVer format -> https://semver.org -pluginVersion = 0.0.55 +pluginVersion = 0.0.56 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 223 diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/activities/ContinuePluginStartupActivity.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/activities/ContinuePluginStartupActivity.kt index 4a785a4d83..444a4d3970 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/activities/ContinuePluginStartupActivity.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/activities/ContinuePluginStartupActivity.kt @@ -1,5 +1,8 @@ package com.github.continuedev.continueintellijextension.activities +import com.github.continuedev.continueintellijextension.auth.AuthListener +import com.github.continuedev.continueintellijextension.auth.ContinueAuthService +import com.github.continuedev.continueintellijextension.auth.ControlPlaneSessionInfo import com.github.continuedev.continueintellijextension.constants.getContinueGlobalPath import com.github.continuedev.continueintellijextension.`continue`.* import com.github.continuedev.continueintellijextension.listeners.ContinuePluginSelectionListener @@ -25,6 +28,7 @@ import java.nio.file.Files import java.nio.file.Paths import javax.swing.* import com.intellij.ide.plugins.PluginManager +import com.intellij.openapi.components.service import com.intellij.openapi.extensions.PluginId fun showTutorial(project: Project) { @@ -126,6 +130,32 @@ class ContinuePluginStartupActivity : StartupActivity, Disposable, DumbAware { } }) + // Listen for clicking settings button to start the auth flow + val authService = service() + val initialSessionInfo = authService.loadControlPlaneSessionInfo() + + if (initialSessionInfo != null) { + val data = mapOf( + "sessionInfo" to initialSessionInfo + ) + continuePluginService.coreMessenger?.request("didChangeControlPlaneSessionInfo", data, null) { _ -> } + continuePluginService.sendToWebview("didChangeControlPlaneSessionInfo", data) + } + + connection.subscribe(AuthListener.TOPIC, object : AuthListener { + override fun startAuthFlow() { + authService.startAuthFlow(project) + } + + override fun handleUpdatedSessionInfo(sessionInfo: ControlPlaneSessionInfo?) { + val data = mapOf( + "sessionInfo" to sessionInfo + ) + continuePluginService.coreMessenger?.request("didChangeControlPlaneSessionInfo", data, null) { _ -> } + continuePluginService.sendToWebview("didChangeControlPlaneSessionInfo", data) + } + }) + GlobalScope.async(Dispatchers.IO) { val listener = ContinuePluginSelectionListener( diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/AuthListener.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/AuthListener.kt new file mode 100644 index 0000000000..b6449936c3 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/AuthListener.kt @@ -0,0 +1,12 @@ +package com.github.continuedev.continueintellijextension.auth +import com.intellij.util.messages.Topic + +interface AuthListener { + fun startAuthFlow() + + fun handleUpdatedSessionInfo(sessionInfo: ControlPlaneSessionInfo?) + + companion object { + val TOPIC = Topic.create("StartAuthFlow", AuthListener::class.java) + } +} \ No newline at end of file diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthDialog.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthDialog.kt new file mode 100644 index 0000000000..7197bf3481 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthDialog.kt @@ -0,0 +1,34 @@ +package com.github.continuedev.continueintellijextension.auth + +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import javax.swing.JComponent +import javax.swing.JPanel +import java.awt.BorderLayout + +class ContinueAuthDialog(private val onTokenEntered: (String) -> Unit) : DialogWrapper(true) { + private val tokenField = JBTextField() + + init { + init() + title = "Enter Continue Authentication Token" + } + + override fun createCenterPanel(): JComponent { + val panel = JPanel(BorderLayout()) + panel.add(JBLabel("Please enter your Continue authentication token:"), BorderLayout.NORTH) + panel.add(tokenField, BorderLayout.CENTER) + return panel + } + + override fun doOKAction() { + val token = tokenField.text + if (token.isNotBlank()) { + onTokenEntered(token) + super.doOKAction() + } else { + setErrorText("Please enter a valid token") + } + } +} diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt new file mode 100644 index 0000000000..29a294c8bf --- /dev/null +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt @@ -0,0 +1,207 @@ +package com.github.continuedev.continueintellijextension.auth + +import com.github.continuedev.continueintellijextension.services.ContinuePluginService +import com.intellij.credentialStore.Credentials +import com.intellij.ide.passwordSafe.PasswordSafe +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.remoteServer.util.CloudConfigurationUtil.createCredentialAttributes +import java.awt.Desktop +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.minidev.json.JSONObject +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.net.URL + +@Service +class ContinueAuthService { + companion object { + fun getInstance(): ContinueAuthService = service() + private const val CREDENTIALS_USER = "ContinueAuthUser" + private const val ACCESS_TOKEN_KEY = "ContinueAccessToken" + private const val REFRESH_TOKEN_KEY = "ContinueRefreshToken" + private const val ACCOUNT_ID_KEY = "ContinueAccountId" + private const val ACCOUNT_LABEL_KEY = "ContinueAccountLabel" + private const val CONTROL_PLANE_URL = "https://control-plane-api-service-i3dqylpbqa-uc.a.run.app" +// private const val CONTROL_PLANE_URL = "http://localhost:3001" + } + + fun startAuthFlow(project: Project) { + // Open login page + openSignInPage(project) + + // Open a dialog where the user should paste their sign-in token + com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { + val dialog = ContinueAuthDialog() { token -> + // Store the token + handleNewRefreshToken(token) + } + dialog.show() + } + } + + fun signOut() { + // Clear the stored tokens + setAccessToken("") + setRefreshToken("") + setAccountId("") + setAccountLabel("") + } + + private fun handleNewRefreshToken(token: String) { + // Launch a coroutine to call the suspend function + kotlinx.coroutines.GlobalScope.launch { + try { + val response = refreshToken(token) + val accessToken = response["accessToken"] as? String + val refreshToken = response["refreshToken"] as? String + val user = response["user"] as? Map<*, *> + val firstName = user?.get("firstName") as? String + val lastName = user?.get("lastName") as? String + val label = "$firstName $lastName" + val id = user?.get("id") as? String + + // Persist the session info + setRefreshToken(refreshToken!!) + val sessionInfo = ControlPlaneSessionInfo(accessToken!!, ControlPlaneSessionInfo.Account(id!!, label)) + setControlPlaneSessionInfo(sessionInfo) + + // Notify listeners + ApplicationManager.getApplication().messageBus.syncPublisher(AuthListener.TOPIC).handleUpdatedSessionInfo(sessionInfo) + + } catch (e: Exception) { + // Handle any exceptions + println("Exception while refreshing token: ${e.message}") + } + } + } + + private suspend fun refreshToken(refreshToken: String) = withContext(Dispatchers.IO) { + val client = OkHttpClient() + val url = URL(CONTROL_PLANE_URL).toURI().resolve("/auth/refresh").toURL() + val jsonBody = JSONObject().apply { + put("refreshToken", refreshToken) + } + + val requestBody = jsonBody.toString().toRequestBody("application/json".toMediaType()) + + val request = Request.Builder() + .url(url) + .post(requestBody) + .header("Content-Type", "application/json") + .build() + + val response = client.newCall(request).execute() + + val responseBody = response.body?.string() + val gson = com.google.gson.Gson() + val responseMap = gson.fromJson(responseBody, Map::class.java) + + responseMap + } + + + private fun openSignInPage(project: Project) { + val coreMessenger = project.service().coreMessenger + coreMessenger?.request("auth/getAuthUrl", null, null) { response -> + val authUrl = (response as? Map<*, *>)?.get("url") as? String + if (authUrl != null) { + // Open the auth URL in the browser + Desktop.getDesktop().browse(java.net.URI(authUrl)) + } + } + } + + private fun retrieveSecret(key: String): String? { + val attributes = createCredentialAttributes(key, CREDENTIALS_USER) + val passwordSafe: PasswordSafe = PasswordSafe.instance + + val credentials: Credentials? = passwordSafe[attributes!!] + return credentials?.getPasswordAsString() + } + + private fun storeSecret(key: String, secret: String) { + val attributes = createCredentialAttributes(key, CREDENTIALS_USER) + val passwordSafe: PasswordSafe = PasswordSafe.instance + + val credentials = Credentials(CREDENTIALS_USER, secret) + passwordSafe.set(attributes!!, credentials) + } + + private fun getAccessToken(): String? { + return retrieveSecret(ACCESS_TOKEN_KEY) + } + + private fun setAccessToken(token: String) { + storeSecret(ACCESS_TOKEN_KEY, token) + } + + private fun getRefreshToken(): String? { + return retrieveSecret(REFRESH_TOKEN_KEY) + } + + private fun setRefreshToken(token: String) { + storeSecret(REFRESH_TOKEN_KEY, token) + } + + fun getAccountId(): String? { + return PropertiesComponent.getInstance().getValue(ACCOUNT_ID_KEY) + } + + fun setAccountId(id: String) { + PropertiesComponent.getInstance().setValue(ACCOUNT_ID_KEY, id) + } + + fun getAccountLabel(): String? { + return PropertiesComponent.getInstance().getValue(ACCOUNT_LABEL_KEY) + } + + fun setAccountLabel(label: String) { + PropertiesComponent.getInstance().setValue(ACCOUNT_LABEL_KEY, label) + } + + // New method to load all info as an object + fun loadControlPlaneSessionInfo(): ControlPlaneSessionInfo? { + val accessToken = getAccessToken() + val accountId = getAccountId() + val accountLabel = getAccountLabel() + + return if (accessToken != null && accountId != null && accountLabel != null) { + ControlPlaneSessionInfo( + accessToken = accessToken, + account = ControlPlaneSessionInfo.Account( + id = accountId, + label = accountLabel + ) + ) + } else { + null + } + } + + // New method to set all info from a ControlPlaneSessionInfo object + fun setControlPlaneSessionInfo(info: ControlPlaneSessionInfo) { + setAccessToken(info.accessToken) + setAccountId(info.account.id) + setAccountLabel(info.account.label) + } + +} + +// Data class to represent the ControlPlaneSessionInfo +data class ControlPlaneSessionInfo( + val accessToken: String, + val account: Account +) { + data class Account( + val id: String, + val label: String + ) +} diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/CoreMessenger.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/CoreMessenger.kt index 505a53bbdc..ded887c760 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/CoreMessenger.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/CoreMessenger.kt @@ -147,7 +147,9 @@ class CoreMessenger(private val project: Project, esbuildPath: String, continueC "applyToFile", "getGitHubAuthToken", "setGitHubAuthToken", - "pathSep" + "pathSep", + "getControlPlaneSessionInfo", + "logoutOfControlPlane" ) private val PASS_THROUGH_TO_WEBVIEW = listOf( diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IdeProtocolClient.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IdeProtocolClient.kt index c999cfcab8..e8aa27b6c9 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IdeProtocolClient.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IdeProtocolClient.kt @@ -1,6 +1,8 @@ package com.github.continuedev.continueintellijextension.`continue` import com.github.continuedev.continueintellijextension.* +import com.github.continuedev.continueintellijextension.auth.AuthListener +import com.github.continuedev.continueintellijextension.auth.ContinueAuthService import com.github.continuedev.continueintellijextension.constants.* import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings import com.github.continuedev.continueintellijextension.services.ContinuePluginService @@ -232,9 +234,28 @@ class IdeProtocolClient ( respond(mapOf( "remoteConfigServerUrl" to settings.continueState.remoteConfigServerUrl, "remoteConfigSyncPeriod" to settings.continueState.remoteConfigSyncPeriod, - "userToken" to settings.continueState.userToken + "userToken" to settings.continueState.userToken, + "enableControlServerBeta" to settings.continueState.enableContinueTeamsBeta )) } + "getControlPlaneSessionInfo" -> { + val silent = (data as? Map)?.get("silent") as? Boolean ?: false + + val authService = service() + if (silent) { + val sessionInfo = authService.loadControlPlaneSessionInfo() + respond(sessionInfo) + } else { + authService.startAuthFlow(project) + respond(null) + } + } + "logoutOfControlPlane" -> { + val authService = service() + authService.signOut() + ApplicationManager.getApplication().messageBus.syncPublisher(AuthListener.TOPIC).handleUpdatedSessionInfo(null) + respond(null) + } "getIdeInfo" -> { val applicationInfo = ApplicationInfo.getInstance() val ideName: String = applicationInfo.fullApplicationName diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt index 748796d4cf..6ff8277a56 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt @@ -1,20 +1,13 @@ package com.github.continuedev.continueintellijextension.services import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.PersistentStateComponent -import com.intellij.openapi.components.ServiceManager -import com.intellij.openapi.components.State -import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.* import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.DumbAware import com.intellij.util.messages.Topic import java.awt.GridBagConstraints import java.awt.GridBagLayout -import javax.swing.JCheckBox -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField +import javax.swing.* class ContinueSettingsComponent: DumbAware { val panel: JPanel = JPanel(GridBagLayout()) @@ -22,6 +15,7 @@ class ContinueSettingsComponent: DumbAware { val remoteConfigSyncPeriod: JTextField = JTextField() val userToken: JTextField = JTextField() val enableTabAutocomplete: JCheckBox = JCheckBox("Enable Tab Autocomplete") + val enableContinueTeamsBeta: JCheckBox = JCheckBox("Enable Continue for Teams Beta (requires restart)") init { val constraints = GridBagConstraints() @@ -34,7 +28,6 @@ class ContinueSettingsComponent: DumbAware { panel.add(JLabel("Remote Config Server URL:"), constraints) constraints.gridy++ - constraints.gridy++ panel.add(remoteConfigServerUrl, constraints) constraints.gridy++ panel.add(JLabel("Remote Config Sync Period (in minutes):"), constraints) @@ -47,6 +40,8 @@ class ContinueSettingsComponent: DumbAware { constraints.gridy++ panel.add(enableTabAutocomplete, constraints) constraints.gridy++ + panel.add(enableContinueTeamsBeta, constraints) + constraints.gridy++ // Add a "filler" component that takes up all remaining vertical space constraints.weighty = 1.0 @@ -69,6 +64,7 @@ open class ContinueExtensionSettings : PersistentStateComponent implements UriHandler { diff --git a/gui/src/components/ProfileSwitcher.tsx b/gui/src/components/ProfileSwitcher.tsx index 4733fc1959..1401788832 100644 --- a/gui/src/components/ProfileSwitcher.tsx +++ b/gui/src/components/ProfileSwitcher.tsx @@ -22,7 +22,7 @@ import { IdeMessengerContext } from "../context/IdeMessenger"; import { useAuth } from "../hooks/useAuth"; import { useWebviewListener } from "../hooks/useWebviewListener"; import { RootState } from "../redux/store"; -import { getFontSize, isJetBrains } from "../util"; +import { getFontSize } from "../util"; import HeaderButtonWithText from "./HeaderButtonWithText"; const StyledListbox = styled(Listbox)` @@ -235,7 +235,7 @@ function ProfileSwitcher(props: {}) { {/* Only show login if beta explicitly enabled */} - {!isJetBrains() && controlServerBetaEnabled && ( + {controlServerBetaEnabled && ( { - if (session.account) { + if (session?.account) { logout(); } else { login(); diff --git a/gui/src/components/modelSelection/ModelSelect.tsx b/gui/src/components/modelSelection/ModelSelect.tsx index fae117615f..9e68813aca 100644 --- a/gui/src/components/modelSelection/ModelSelect.tsx +++ b/gui/src/components/modelSelection/ModelSelect.tsx @@ -218,10 +218,7 @@ function ModelSelect() { >
{modelSelectTitle(defaultModel) || "Select model"}
-
diff --git a/gui/src/hooks/useSubmenuContextProviders.tsx b/gui/src/hooks/useSubmenuContextProviders.tsx index 8c1bd86eb9..45ee6f2c6c 100644 --- a/gui/src/hooks/useSubmenuContextProviders.tsx +++ b/gui/src/hooks/useSubmenuContextProviders.tsx @@ -124,7 +124,10 @@ function useSubmenuContextProviders() { query, MINISEARCH_OPTIONS, ); - console.debug(`Search results for ${providerTitle}:`, results.length); + console.debug( + `Search results for ${providerTitle}:`, + results.length, + ); return results.map((result) => { return { ...result, providerTitle }; }); diff --git a/packages/config-types/package.json b/packages/config-types/package.json index 4a4d269e05..b323c64497 100644 --- a/packages/config-types/package.json +++ b/packages/config-types/package.json @@ -1,6 +1,6 @@ { "name": "@continuedev/config-types", - "version": "1.0.8", + "version": "1.0.9", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/config-types/src/index.ts b/packages/config-types/src/index.ts index 8d0a1b5888..fa676bec0a 100644 --- a/packages/config-types/src/index.ts +++ b/packages/config-types/src/index.ts @@ -195,6 +195,14 @@ export const devDataSchema = z.object({ }); export type DevData = z.infer; +export const siteIndexingConfigSchema = z.object({ + startUrl: z.string(), + rootUrl: z.string(), + title: z.string(), + maxDepth: z.string().optional(), + faviconUrl: z.string().optional(), +}); + export const configJsonSchema = z.object({ models: z.array(modelDescriptionSchema), tabAutocompleteModel: modelDescriptionSchema.optional(), @@ -212,5 +220,6 @@ export const configJsonSchema = z.object({ disableIndexing: z.boolean().optional(), tabAutocompleteOptions: tabAutocompleteOptionsSchema.optional(), ui: uiOptionsSchema.optional(), + docs: z.array(siteIndexingConfigSchema).optional(), }); export type ConfigJson = z.infer;