summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/ui/App.tsx12
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts9
-rw-r--r--packages/cli/src/ui/hooks/usePrivacySettings.ts135
-rw-r--r--packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx104
-rw-r--r--packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.tsx55
-rw-r--r--packages/cli/src/ui/privacy/GeminiPrivacyNotice.tsx58
-rw-r--r--packages/cli/src/ui/privacy/PrivacyNotice.tsx42
-rw-r--r--packages/core/src/code_assist/server.ts47
-rw-r--r--packages/core/src/code_assist/types.ts10
-rw-r--r--packages/core/src/config/config.ts5
-rw-r--r--packages/core/src/core/client.ts3
-rw-r--r--packages/core/src/core/contentGenerator.ts1
-rw-r--r--packages/core/src/index.ts2
13 files changed, 468 insertions, 15 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index f2347d35..bccba9e6 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -71,6 +71,7 @@ import { checkForUpdates } from './utils/updateCheck.js';
import ansiEscapes from 'ansi-escapes';
import { OverflowProvider } from './contexts/OverflowContext.js';
import { ShowMoreLines } from './components/ShowMoreLines.js';
+import { PrivacyNotice } from './privacy/PrivacyNotice.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@@ -130,6 +131,11 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null);
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
+ const [showPrivacyNotice, setShowPrivacyNotice] = useState<boolean>(false);
+
+ const openPrivacyNotice = useCallback(() => {
+ setShowPrivacyNotice(true);
+ }, []);
const errorCount = useMemo(
() => consoleMessages.filter((msg) => msg.type === 'error').length,
@@ -277,6 +283,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
toggleCorgiMode,
showToolDescriptions,
setQuittingMessages,
+ openPrivacyNotice,
);
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
@@ -712,6 +719,11 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
onExit={exitEditorDialog}
/>
</Box>
+ ) : showPrivacyNotice ? (
+ <PrivacyNotice
+ onExit={() => setShowPrivacyNotice(false)}
+ config={config}
+ />
) : (
<>
<LoadingIndicator
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index bd0e38b8..b07c82c2 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -76,6 +76,7 @@ export const useSlashCommandProcessor = (
toggleCorgiMode: () => void,
showToolDescriptions: boolean = false,
setQuittingMessages: (message: HistoryItem[]) => void,
+ openPrivacyNotice: () => void,
) => {
const session = useSessionStats();
const gitService = useMemo(() => {
@@ -255,6 +256,13 @@ export const useSlashCommandProcessor = (
},
},
{
+ name: 'privacy',
+ description: 'display the privacy notice',
+ action: (_mainCommand, _subCommand, _args) => {
+ openPrivacyNotice();
+ },
+ },
+ {
name: 'stats',
altName: 'usage',
description: 'check session stats',
@@ -1022,6 +1030,7 @@ export const useSlashCommandProcessor = (
setQuittingMessages,
pendingCompressionItemRef,
setPendingCompressionItem,
+ openPrivacyNotice,
]);
const handleSlashCommand = useCallback(
diff --git a/packages/cli/src/ui/hooks/usePrivacySettings.ts b/packages/cli/src/ui/hooks/usePrivacySettings.ts
new file mode 100644
index 00000000..44824def
--- /dev/null
+++ b/packages/cli/src/ui/hooks/usePrivacySettings.ts
@@ -0,0 +1,135 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { GaxiosError } from 'gaxios';
+import { useState, useEffect, useCallback } from 'react';
+import { Config, CodeAssistServer, UserTierId } from '@google/gemini-cli-core';
+
+export interface PrivacyState {
+ isLoading: boolean;
+ error?: string;
+ isFreeTier?: boolean;
+ dataCollectionOptIn?: boolean;
+}
+
+export const usePrivacySettings = (config: Config) => {
+ const [privacyState, setPrivacyState] = useState<PrivacyState>({
+ isLoading: true,
+ });
+
+ useEffect(() => {
+ const fetchInitialState = async () => {
+ setPrivacyState({
+ isLoading: true,
+ });
+ try {
+ const server = getCodeAssistServer(config);
+ const tier = await getTier(server);
+ if (tier !== UserTierId.FREE) {
+ // We don't need to fetch opt-out info since non-free tier
+ // data gathering is already worked out some other way.
+ setPrivacyState({
+ isLoading: false,
+ isFreeTier: false,
+ });
+ return;
+ }
+
+ const optIn = await getRemoteDataCollectionOptIn(server);
+ setPrivacyState({
+ isLoading: false,
+ isFreeTier: true,
+ dataCollectionOptIn: optIn,
+ });
+ } catch (e) {
+ setPrivacyState({
+ isLoading: false,
+ error: e instanceof Error ? e.message : String(e),
+ });
+ }
+ };
+ fetchInitialState();
+ }, [config]);
+
+ const updateDataCollectionOptIn = useCallback(
+ async (optIn: boolean) => {
+ try {
+ const server = getCodeAssistServer(config);
+ const updatedOptIn = await setRemoteDataCollectionOptIn(server, optIn);
+ setPrivacyState({
+ isLoading: false,
+ isFreeTier: true,
+ dataCollectionOptIn: updatedOptIn,
+ });
+ } catch (e) {
+ setPrivacyState({
+ isLoading: false,
+ error: e instanceof Error ? e.message : String(e),
+ });
+ }
+ },
+ [config],
+ );
+
+ return {
+ privacyState,
+ updateDataCollectionOptIn,
+ };
+};
+
+function getCodeAssistServer(config: Config): CodeAssistServer {
+ const server = config.getGeminiClient().getContentGenerator();
+ // Neither of these cases should ever happen.
+ if (!(server instanceof CodeAssistServer)) {
+ throw new Error('Oauth not being used');
+ } else if (!server.projectId) {
+ throw new Error('Oauth not being used');
+ }
+ return server;
+}
+
+async function getTier(server: CodeAssistServer): Promise<UserTierId> {
+ const loadRes = await server.loadCodeAssist({
+ cloudaicompanionProject: server.projectId,
+ metadata: {
+ ideType: 'IDE_UNSPECIFIED',
+ platform: 'PLATFORM_UNSPECIFIED',
+ pluginType: 'GEMINI',
+ duetProject: server.projectId,
+ },
+ });
+ if (!loadRes.currentTier) {
+ throw new Error('User does not have a current tier');
+ }
+ return loadRes.currentTier.id;
+}
+
+async function getRemoteDataCollectionOptIn(
+ server: CodeAssistServer,
+): Promise<boolean> {
+ try {
+ const resp = await server.getCodeAssistGlobalUserSetting();
+ return resp.freeTierDataCollectionOptin;
+ } catch (e) {
+ if (e instanceof GaxiosError) {
+ if (e.response?.status === 404) {
+ return true;
+ }
+ }
+ throw e;
+ }
+}
+
+async function setRemoteDataCollectionOptIn(
+ server: CodeAssistServer,
+ optIn: boolean,
+): Promise<boolean> {
+ const resp = await server.setCodeAssistGlobalUserSetting({
+ cloudaicompanionProject: server.projectId,
+ freeTierDataCollectionOptin: optIn,
+ });
+ return resp.freeTierDataCollectionOptin;
+}
diff --git a/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx b/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx
new file mode 100644
index 00000000..f9341bf9
--- /dev/null
+++ b/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Newline, Text } from 'ink';
+import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
+import { usePrivacySettings } from '../hooks/usePrivacySettings.js';
+import { CloudPaidPrivacyNotice } from './CloudPaidPrivacyNotice.js';
+import { Config } from '@google/gemini-cli-core';
+import { Colors } from '../colors.js';
+
+interface CloudFreePrivacyNoticeProps {
+ config: Config;
+ onExit: () => void;
+}
+
+export const CloudFreePrivacyNotice = ({
+ config,
+ onExit,
+}: CloudFreePrivacyNoticeProps) => {
+ const { privacyState, updateDataCollectionOptIn } =
+ usePrivacySettings(config);
+
+ if (privacyState.isLoading) {
+ return <Text color={Colors.Gray}>Loading...</Text>;
+ }
+
+ if (privacyState.error) {
+ return (
+ <Text color={Colors.AccentRed}>
+ Error loading Opt-in settings: {privacyState.error}
+ </Text>
+ );
+ }
+
+ if (privacyState.isFreeTier === false) {
+ return <CloudPaidPrivacyNotice onExit={onExit} />;
+ }
+
+ const items = [
+ { label: 'Yes', value: true },
+ { label: 'No', value: false },
+ ];
+
+ return (
+ <Box flexDirection="column" marginY={1}>
+ <Text bold color={Colors.AccentPurple}>
+ Gemini Code Assist for Individuals Privacy Notice
+ </Text>
+ <Newline />
+ <Text>
+ This notice and our Privacy Policy
+ <Text color={Colors.AccentBlue}>[1]</Text> describe how Gemini Code
+ Assist handles your data. Please read them carefully.
+ </Text>
+ <Newline />
+ <Text>
+ When you use Gemini Code Assist for individuals with Gemini CLI, Google
+ collects your prompts, related code, generated output, code edits,
+ related feature usage information, and your feedback to provide,
+ improve, and develop Google products and services and machine learning
+ technologies.
+ </Text>
+ <Newline />
+ <Text>
+ To help with quality and improve our products (such as generative
+ machine-learning models), human reviewers may read, annotate, and
+ process the data collected above. We take steps to protect your privacy
+ as part of this process. This includes disconnecting the data from your
+ Google Account before reviewers see or annotate it, and storing those
+ disconnected copies for up to 18 months. Please don&apos;t submit
+ confidential information or any data you wouldn&apos;t want a reviewer
+ to see or Google to use to improve our products, services and
+ machine-learning technologies.
+ </Text>
+ <Newline />
+ <Box flexDirection="column">
+ <Text>
+ Allow Google to use this data to develop and improve our products?
+ </Text>
+ <RadioButtonSelect
+ items={items}
+ initialIndex={privacyState.dataCollectionOptIn ? 0 : 1}
+ onSelect={(value) => {
+ updateDataCollectionOptIn(value);
+ // Only exit if there was no error.
+ if (!privacyState.error) {
+ onExit();
+ }
+ }}
+ />
+ </Box>
+ <Newline />
+ <Text>
+ <Text color={Colors.AccentBlue}>[1]</Text>{' '}
+ https://policies.google.com/privacy
+ </Text>
+ <Newline />
+ <Text color={Colors.Gray}>Press Enter to choose an option and exit.</Text>
+ </Box>
+ );
+};
diff --git a/packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.tsx b/packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.tsx
new file mode 100644
index 00000000..e50dcd4b
--- /dev/null
+++ b/packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.tsx
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Newline, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+
+interface CloudPaidPrivacyNoticeProps {
+ onExit: () => void;
+}
+
+export const CloudPaidPrivacyNotice = ({
+ onExit,
+}: CloudPaidPrivacyNoticeProps) => {
+ useInput((input, key) => {
+ if (key.escape) {
+ onExit();
+ }
+ });
+
+ return (
+ <Box flexDirection="column" marginBottom={1}>
+ <Text bold color={Colors.AccentPurple}>
+ Vertex AI Notice
+ </Text>
+ <Newline />
+ <Text>
+ Service Specific Terms<Text color={Colors.AccentBlue}>[1]</Text> are
+ incorporated into the agreement under which Google has agreed to provide
+ Google Cloud Platform<Text color={Colors.AccentGreen}>[2]</Text> to
+ Customer (the “Agreement”). If the Agreement authorizes the resale or
+ supply of Google Cloud Platform under a Google Cloud partner or reseller
+ program, then except for in the section entitled “Partner-Specific
+ Terms”, all references to Customer in the Service Specific Terms mean
+ Partner or Reseller (as applicable), and all references to Customer Data
+ in the Service Specific Terms mean Partner Data. Capitalized terms used
+ but not defined in the Service Specific Terms have the meaning given to
+ them in the Agreement.
+ </Text>
+ <Newline />
+ <Text>
+ <Text color={Colors.AccentBlue}>[1]</Text>{' '}
+ https://cloud.google.com/terms/service-terms
+ </Text>
+ <Text>
+ <Text color={Colors.AccentGreen}>[2]</Text>{' '}
+ https://cloud.google.com/terms/services
+ </Text>
+ <Newline />
+ <Text color={Colors.Gray}>Press Esc to exit.</Text>
+ </Box>
+ );
+};
diff --git a/packages/cli/src/ui/privacy/GeminiPrivacyNotice.tsx b/packages/cli/src/ui/privacy/GeminiPrivacyNotice.tsx
new file mode 100644
index 00000000..57030ac3
--- /dev/null
+++ b/packages/cli/src/ui/privacy/GeminiPrivacyNotice.tsx
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Newline, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+
+interface GeminiPrivacyNoticeProps {
+ onExit: () => void;
+}
+
+export const GeminiPrivacyNotice = ({ onExit }: GeminiPrivacyNoticeProps) => {
+ useInput((input, key) => {
+ if (key.escape) {
+ onExit();
+ }
+ });
+
+ return (
+ <Box flexDirection="column" marginBottom={1}>
+ <Text bold color={Colors.AccentPurple}>
+ Gemini API Key Notice
+ </Text>
+ <Newline />
+ <Text>
+ By using the Gemini API<Text color={Colors.AccentBlue}>[1]</Text>,
+ Google AI Studio
+ <Text color={Colors.AccentRed}>[2]</Text>, and the other Google
+ developer services that reference these terms (collectively, the
+ &quot;APIs&quot; or &quot;Services&quot;), you are agreeing to Google
+ APIs Terms of Service (the &quot;API Terms&quot;)
+ <Text color={Colors.AccentGreen}>[3]</Text>, and the Gemini API
+ Additional Terms of Service (the &quot;Additional Terms&quot;)
+ <Text color={Colors.AccentPurple}>[4]</Text>.
+ </Text>
+ <Newline />
+ <Text>
+ <Text color={Colors.AccentBlue}>[1]</Text>{' '}
+ https://ai.google.dev/docs/gemini_api_overview
+ </Text>
+ <Text>
+ <Text color={Colors.AccentRed}>[2]</Text> https://aistudio.google.com/
+ </Text>
+ <Text>
+ <Text color={Colors.AccentGreen}>[3]</Text>{' '}
+ https://developers.google.com/terms
+ </Text>
+ <Text>
+ <Text color={Colors.AccentPurple}>[4]</Text>{' '}
+ https://ai.google.dev/gemini-api/terms
+ </Text>
+ <Newline />
+ <Text color={Colors.Gray}>Press Esc to exit.</Text>
+ </Box>
+ );
+};
diff --git a/packages/cli/src/ui/privacy/PrivacyNotice.tsx b/packages/cli/src/ui/privacy/PrivacyNotice.tsx
new file mode 100644
index 00000000..b12b3648
--- /dev/null
+++ b/packages/cli/src/ui/privacy/PrivacyNotice.tsx
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box } from 'ink';
+import { type Config, AuthType } from '@google/gemini-cli-core';
+import { GeminiPrivacyNotice } from './GeminiPrivacyNotice.js';
+import { CloudPaidPrivacyNotice } from './CloudPaidPrivacyNotice.js';
+import { CloudFreePrivacyNotice } from './CloudFreePrivacyNotice.js';
+
+interface PrivacyNoticeProps {
+ onExit: () => void;
+ config: Config;
+}
+
+const PrivacyNoticeText = ({
+ config,
+ onExit,
+}: {
+ config: Config;
+ onExit: () => void;
+}) => {
+ const authType = config.getContentGeneratorConfig()?.authType;
+
+ switch (authType) {
+ case AuthType.USE_GEMINI:
+ return <GeminiPrivacyNotice onExit={onExit} />;
+ case AuthType.USE_VERTEX_AI:
+ return <CloudPaidPrivacyNotice onExit={onExit} />;
+ case AuthType.LOGIN_WITH_GOOGLE_PERSONAL:
+ default:
+ return <CloudFreePrivacyNotice config={config} onExit={onExit} />;
+ }
+};
+
+export const PrivacyNotice = ({ onExit, config }: PrivacyNoticeProps) => (
+ <Box borderStyle="round" padding={1} flexDirection="column">
+ <PrivacyNoticeText config={config} onExit={onExit} />
+ </Box>
+);
diff --git a/packages/core/src/code_assist/server.ts b/packages/core/src/code_assist/server.ts
index 5798289a..1eaf9217 100644
--- a/packages/core/src/code_assist/server.ts
+++ b/packages/core/src/code_assist/server.ts
@@ -6,28 +6,30 @@
import { AuthClient } from 'google-auth-library';
import {
- LoadCodeAssistResponse,
+ CodeAssistGlobalUserSettingResponse,
LoadCodeAssistRequest,
- OnboardUserRequest,
+ LoadCodeAssistResponse,
LongrunningOperationResponse,
+ OnboardUserRequest,
+ SetCodeAssistGlobalUserSettingRequest,
} from './types.js';
import {
- GenerateContentResponse,
- GenerateContentParameters,
CountTokensParameters,
- EmbedContentResponse,
CountTokensResponse,
EmbedContentParameters,
+ EmbedContentResponse,
+ GenerateContentParameters,
+ GenerateContentResponse,
} from '@google/genai';
import * as readline from 'readline';
import { ContentGenerator } from '../core/contentGenerator.js';
import {
+ CaCountTokenResponse,
CaGenerateContentResponse,
- toGenerateContentRequest,
+ fromCountTokenResponse,
fromGenerateContentResponse,
toCountTokenRequest,
- fromCountTokenResponse,
- CaCountTokenResponse,
+ toGenerateContentRequest,
} from './converter.js';
import { PassThrough } from 'node:stream';
@@ -93,6 +95,21 @@ export class CodeAssistServer implements ContentGenerator {
);
}
+ async getCodeAssistGlobalUserSetting(): Promise<CodeAssistGlobalUserSettingResponse> {
+ return await this.getEndpoint<CodeAssistGlobalUserSettingResponse>(
+ 'getCodeAssistGlobalUserSetting',
+ );
+ }
+
+ async setCodeAssistGlobalUserSetting(
+ req: SetCodeAssistGlobalUserSettingRequest,
+ ): Promise<CodeAssistGlobalUserSettingResponse> {
+ return await this.callEndpoint<CodeAssistGlobalUserSettingResponse>(
+ 'setCodeAssistGlobalUserSetting',
+ req,
+ );
+ }
+
async countTokens(req: CountTokensParameters): Promise<CountTokensResponse> {
const resp = await this.callEndpoint<CaCountTokenResponse>(
'countTokens',
@@ -126,6 +143,20 @@ export class CodeAssistServer implements ContentGenerator {
return res.data as T;
}
+ async getEndpoint<T>(method: string, signal?: AbortSignal): Promise<T> {
+ const res = await this.auth.request({
+ url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`,
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...this.httpOptions.headers,
+ },
+ responseType: 'json',
+ signal,
+ });
+ return res.data as T;
+ }
+
async streamEndpoint<T>(
method: string,
req: object,
diff --git a/packages/core/src/code_assist/types.ts b/packages/core/src/code_assist/types.ts
index 67257a50..4c395e57 100644
--- a/packages/core/src/code_assist/types.ts
+++ b/packages/core/src/code_assist/types.ts
@@ -173,3 +173,13 @@ export interface HelpLinkUrl {
description: string;
url: string;
}
+
+export interface SetCodeAssistGlobalUserSettingRequest {
+ cloudaicompanionProject?: string;
+ freeTierDataCollectionOptin: boolean;
+}
+
+export interface CodeAssistGlobalUserSettingResponse {
+ cloudaicompanionProject?: string;
+ freeTierDataCollectionOptin: boolean;
+}
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index b266512c..75b73a85 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -226,11 +226,6 @@ export class Config {
}
async refreshAuth(authMethod: AuthType) {
- // Check if this is actually a switch to a different auth method
- const previousAuthType = this.contentGeneratorConfig?.authType;
- const _isAuthMethodSwitch =
- previousAuthType && previousAuthType !== authMethod;
-
// Always use the original default model when switching auth methods
// This ensures users don't stay on Flash after switching between auth types
// and allows API key users to get proper fallback behavior from getEffectiveModel
diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts
index ae67c3b4..1a629b2c 100644
--- a/packages/core/src/core/client.ts
+++ b/packages/core/src/core/client.ts
@@ -71,7 +71,8 @@ export class GeminiClient {
);
this.chat = await this.startChat();
}
- private getContentGenerator(): ContentGenerator {
+
+ getContentGenerator(): ContentGenerator {
if (!this.contentGenerator) {
throw new Error('Content generator not initialized');
}
diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts
index 7021adc2..82fe5ee9 100644
--- a/packages/core/src/core/contentGenerator.ts
+++ b/packages/core/src/core/contentGenerator.ts
@@ -70,7 +70,6 @@ export async function createContentGeneratorConfig(
return contentGeneratorConfig;
}
- //
if (authType === AuthType.USE_GEMINI && geminiApiKey) {
contentGeneratorConfig.apiKey = geminiApiKey;
contentGeneratorConfig.model = await getEffectiveModel(
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 3a123452..aff37f50 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -21,6 +21,8 @@ export * from './core/nonInteractiveToolExecutor.js';
export * from './code_assist/codeAssist.js';
export * from './code_assist/oauth2.js';
+export * from './code_assist/server.js';
+export * from './code_assist/types.js';
// Export utilities
export * from './utils/paths.js';