├── node-serverless ├── src │ ├── assets │ │ ├── .gitkeep │ │ ├── handsome.png │ │ ├── gemini-logo.webp │ │ └── whatsapp-bg.jpg │ ├── favicon.ico │ ├── app │ │ ├── app.routes.ts │ │ ├── app.config.ts │ │ ├── app.config.server.ts │ │ ├── search │ │ │ ├── search.component.spec.ts │ │ │ ├── search.component.ts │ │ │ ├── search.component.html │ │ │ └── search.component.scss │ │ ├── messages │ │ │ ├── messages.component.spec.ts │ │ │ ├── messages.component.ts │ │ │ ├── messages.component.html │ │ │ └── messages.component.scss │ │ ├── message-input │ │ │ ├── message-input.component.spec.ts │ │ │ ├── message-input.component.html │ │ │ ├── message-input.component.scss │ │ │ └── message-input.component.ts │ │ ├── app.component.spec.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ └── app.component.ts │ ├── styles.scss │ ├── main.ts │ ├── main.server.ts │ ├── types │ │ └── message.ts │ └── index.html ├── tools │ ├── npm-server.sh │ └── npm-client.sh ├── .dockerignore ├── Dockerfile.dev ├── server │ ├── Dockerfile.dev │ ├── package.json │ ├── config │ │ └── env.js.sample │ ├── gemini_functions │ │ ├── exchange_rate.js │ │ ├── wikipedia.js │ │ └── vertexai_google_merch_store_search.js │ ├── server.js │ └── package-lock.json ├── tsconfig.spec.json ├── .editorconfig ├── tsconfig.app.json ├── Dockerfile ├── tsconfig.json ├── package.json ├── server.ts └── angular.json ├── python-setup ├── search_tools │ ├── __init__.py │ ├── vertexai_search.py │ ├── exchange_rate.py │ └── wikipedia.py ├── requirements.txt ├── Dockerfile └── app.py ├── python-serverless ├── .dockerignore ├── requirements.txt ├── tools │ └── flask-server.sh ├── Dockerfile.dev ├── Dockerfile └── app.py ├── .env.sample ├── tools └── terminal_scripts │ ├── utils │ ├── load_script_env_vars.sh │ ├── echo_and_run.sh │ ├── save_to_dotenv.sh │ └── prompt_user_input.sh │ └── first_time_setup.sh ├── .gitignore ├── docker-compose.yml ├── Makefile ├── README.md └── vertex-ai-datastore-files └── phone_shop_items.json /node-serverless/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python-setup/search_tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /node-serverless/tools/npm-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm i 4 | 5 | nodemon 6 | -------------------------------------------------------------------------------- /node-serverless/tools/npm-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm i 4 | 5 | npm run start 6 | -------------------------------------------------------------------------------- /python-serverless/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | tools 4 | Dockerfile 5 | Dockerfile.dev 6 | venv 7 | -------------------------------------------------------------------------------- /node-serverless/.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | env.js 3 | server/config/env.js 4 | node_modules 5 | server/node_modules 6 | dist 7 | -------------------------------------------------------------------------------- /node-serverless/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Weiyuan-Lane/vertex-ai-search-galore/HEAD/node-serverless/src/favicon.ico -------------------------------------------------------------------------------- /node-serverless/src/assets/handsome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Weiyuan-Lane/vertex-ai-search-galore/HEAD/node-serverless/src/assets/handsome.png -------------------------------------------------------------------------------- /node-serverless/src/assets/gemini-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Weiyuan-Lane/vertex-ai-search-galore/HEAD/node-serverless/src/assets/gemini-logo.webp -------------------------------------------------------------------------------- /node-serverless/src/assets/whatsapp-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Weiyuan-Lane/vertex-ai-search-galore/HEAD/node-serverless/src/assets/whatsapp-bg.jpg -------------------------------------------------------------------------------- /python-setup/requirements.txt: -------------------------------------------------------------------------------- 1 | google-cloud-aiplatform[langchain,reasoningengine]==1.70.0 2 | google-cloud-discoveryengine==0.12.3 3 | python-dotenv==1.0.1 4 | -------------------------------------------------------------------------------- /python-serverless/requirements.txt: -------------------------------------------------------------------------------- 1 | google-cloud-aiplatform[langchain,reasoningengine]==1.70.0 2 | google-cloud-discoveryengine==0.12.3 3 | python-dotenv==1.0.1 4 | Flask==3.0.3 5 | -------------------------------------------------------------------------------- /node-serverless/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:22.10.0-alpine3.20 2 | 3 | COPY ./node-serverless/tools/npm-client.sh /opt/run/npm-client.sh 4 | 5 | ENTRYPOINT ["/opt/run/npm-client.sh"] 6 | -------------------------------------------------------------------------------- /python-serverless/tools/flask-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python3 -m venv ./venv 4 | source ./venv/bin/activate 5 | pip3 install -r requirements.txt 6 | 7 | flask --debug run -h 0.0.0.0 -p 5000 8 | -------------------------------------------------------------------------------- /node-serverless/server/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:22.10.0-alpine3.20 2 | 3 | COPY ./node-serverless/tools/npm-server.sh /opt/run/npm-server.sh 4 | 5 | RUN npm install -g nodemon 6 | 7 | ENTRYPOINT ["/opt/run/npm-server.sh"] 8 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { SearchComponent } from './search/search.component'; 3 | 4 | export const routes: Routes = [ 5 | { path: 'search', component: SearchComponent }, 6 | ]; 7 | -------------------------------------------------------------------------------- /node-serverless/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | .mat-typography p { 6 | margin: 2px; 7 | } 8 | -------------------------------------------------------------------------------- /python-serverless/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM python:3.9.20-alpine3.19 2 | 3 | COPY ./python-serverless/tools/flask-server.sh /opt/run/flask-server.sh 4 | RUN apk add --no-cache gcc libc-dev geos-dev geos &&\ 5 | pip3 install gunicorn 6 | 7 | ENTRYPOINT ["/opt/run/flask-server.sh"] 8 | -------------------------------------------------------------------------------- /node-serverless/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | GCP_PROJECT_ID=my-project-id 2 | GCP_PROJECT_NUMBER=0123456789 3 | GCP_REGION=us-central1 4 | GCP_GEMINI_VER=gemini-1.5-flash-001 5 | GCP_VERTEXAI_SEARCH_ENGINE_ID=some-id-from-vertex-ai-builder 6 | GCP_VERTEXAI_SEARCH_LOCATION=global 7 | GCP_PYTHON_SERVERLESS_URL= 8 | GCP_VERTEXAI_REASONING_ENGINE_BUCKET= 9 | -------------------------------------------------------------------------------- /node-serverless/src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | import { config } from './app/app.config.server'; 4 | 5 | const bootstrap = () => bootstrapApplication(AppComponent, config); 6 | 7 | export default bootstrap; 8 | -------------------------------------------------------------------------------- /python-serverless/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.20-alpine3.19 2 | 3 | WORKDIR /opt/app 4 | 5 | COPY . . 6 | RUN apk add --no-cache gcc libc-dev geos-dev geos &&\ 7 | pip3 install gunicorn &&\ 8 | ls -la &&\ 9 | pip3 install -r requirements.txt 10 | 11 | CMD ["gunicorn", "--bind", ":8080", "--workers", "1", "--threads", "4", "app:app"] 12 | -------------------------------------------------------------------------------- /tools/terminal_scripts/utils/load_script_env_vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TMP_DIR=".tmp" 4 | mkdir -p $TMP_DIR 5 | 6 | GCP_PROJECT_ID_FILE=".gcpprojectid" 7 | TERRAFORM_DIR=terraform 8 | 9 | ECHO_YELLOW_COLOR='\033[1;33m' 10 | ECHO_RED_COLOR='\033[0;31m' 11 | ECHO_GREEN_COLOR='\033[0;32m' 12 | ECHO_WHITE_COLOR='\033[1;37m' 13 | ECHO_NO_COLOR='\033[0m' 14 | -------------------------------------------------------------------------------- /node-serverless/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /node-serverless/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /tools/terminal_scripts/utils/echo_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # INPUT VARIABLES 4 | # $1 - Command to execute 5 | # 6 | # OUTPUT VARIABLES 7 | # None 8 | # 9 | echoAndRun(){ 10 | echo "${ECHO_GREEN_COLOR}Running${ECHO_NO_COLOR} \"$1\"" 11 | $1 12 | echo "" 13 | } 14 | 15 | echoWithColor(){ 16 | echo "${ECHO_GREEN_COLOR}$1${ECHO_NO_COLOR}" 17 | } 18 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideClientHydration } from '@angular/platform-browser'; 6 | 7 | export const appConfig: ApplicationConfig = { 8 | providers: [provideRouter(routes), provideClientHydration()] 9 | }; 10 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; 2 | import { provideServerRendering } from '@angular/platform-server'; 3 | import { appConfig } from './app.config'; 4 | 5 | const serverConfig: ApplicationConfig = { 6 | providers: [ 7 | provideServerRendering() 8 | ] 9 | }; 10 | 11 | export const config = mergeApplicationConfig(appConfig, serverConfig); 12 | -------------------------------------------------------------------------------- /node-serverless/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [ 7 | "node", 8 | "dom-speech-recognition" 9 | ], 10 | }, 11 | "files": [ 12 | "src/main.ts", 13 | "src/main.server.ts", 14 | "server.ts" 15 | ], 16 | "include": [ 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /python-setup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM google/cloud-sdk:496.0.0-alpine 2 | 3 | ARG WORKDIR=/opt/setup 4 | ARG VENV_DIR=/opt/venv 5 | ENV CRC32C_PURE_PYTHON=1 6 | 7 | WORKDIR $WORKDIR 8 | COPY ./python-setup/requirements.txt $WORKDIR 9 | 10 | RUN apk add --no-cache python3 python3-dev py3-pip build-base geos-dev geos crc32c && \ 11 | python3 -m venv $VENV_DIR && \ 12 | source $VENV_DIR/bin/activate && \ 13 | pip3 install --upgrade pip && \ 14 | pip3 install -r requirements.txt 15 | 16 | ENTRYPOINT ["sh", "-c"] 17 | CMD ["while true ; do sleep 60 ; done"] 18 | -------------------------------------------------------------------------------- /node-serverless/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@google-cloud/discoveryengine": "^1.14.0", 14 | "@google-cloud/vertexai": "~1.9.0", 15 | "axios": "~1.7.7", 16 | "cors": "^2.8.5", 17 | "express": "^4.21.1", 18 | "marked": "~12.0.2" 19 | }, 20 | "devDependencies": { 21 | "nodemon": "^3.1.7" 22 | }, 23 | "nodemonConfig": { 24 | "exec": "node server.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /node-serverless/src/app/search/search.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SearchComponent } from './search.component'; 4 | 5 | describe('SearchComponent', () => { 6 | let component: SearchComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SearchComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SearchComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /node-serverless/src/app/messages/messages.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessagesComponent } from './messages.component'; 4 | 5 | describe('MessagesComponent', () => { 6 | let component: MessagesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MessagesComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MessagesComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tools/terminal_scripts/utils/save_to_dotenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # INPUT VARIABLES 4 | # $1 - Env variable name in .env file to write for 5 | # $1 - Env variable value in .env file to write for 6 | # 7 | # Example usage: 8 | # 9 | # saveToDotEnv "GCP_PROJECT_ID" "my-gcp-project-id" 10 | # 11 | # OUTPUT VARIABLES 12 | # None 13 | # 14 | saveToDotEnv(){ 15 | local _envVarToWrite=$1 16 | local _envValueToWrite=$2 17 | 18 | if [ -f ".env" ]; then 19 | if grep -q "$_envVarToWrite=" .env; then 20 | sed -i '' "s/^$_envVarToWrite=.*/$_envVarToWrite=$_envValueToWrite/" .env 21 | else 22 | echo "$_envVarToWrite=$_envValueToWrite" >> .env 23 | fi 24 | else 25 | echo "$_envVarToWrite=$_envValueToWrite" > .env 26 | fi 27 | } 28 | -------------------------------------------------------------------------------- /node-serverless/src/app/message-input/message-input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageInputComponent } from './message-input.component'; 4 | 5 | describe('MessageInputComponent', () => { 6 | let component: MessageInputComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MessageInputComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MessageInputComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /node-serverless/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build Stage 2 | FROM node:22.10.0-alpine3.20 AS build 3 | 4 | # Create and change to the app directory. 5 | WORKDIR /opt/app 6 | 7 | # Copy local code to the container image. 8 | COPY . . 9 | 10 | # Install dependencies and build the Angular app 11 | RUN npm install -g @angular/cli@latest && \ 12 | npm install && \ 13 | ng build 14 | 15 | # Stage 2: Production Stage 16 | FROM node:22.10.0-alpine3.20 17 | 18 | # Create and change to the app directory. 19 | WORKDIR /opt/app/server 20 | 21 | # Copy the built files from the build stage 22 | COPY --from=build /opt/app/dist /opt/app/dist 23 | 24 | # Copy the server code 25 | COPY --from=build /opt/app/server /opt/app/server 26 | 27 | # Install server dependencies 28 | RUN npm install 29 | 30 | # Run the web service on container startup. 31 | CMD ["node", "server.js"] 32 | -------------------------------------------------------------------------------- /node-serverless/server/config/env.js.sample: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Copy this file in local environment to "env.js" 3 | This file is used as a way to load your environment variables and secret 4 | from GCP's Secret Manager, from Cloud Run! 5 | 6 | If you prefer reading from environment variables, you can also replace the 7 | following codewith `process.env.SOME_VAR` references 8 | ******************************************************************************/ 9 | 10 | module.exports = { 11 | GCP_PROJECT_ID: 'my-project-id', 12 | GCP_PROJECT_ID_NUMBER: '0123456789', 13 | GCP_REGION: 'us-central1', 14 | GCP_GEMINI_VER: 'gemini-1.5-flash-001', 15 | GCP_VERTEXAI_SEARCH_ENGINE_ID: 'some-id-from-vertex-ai-builder', 16 | GCP_VERTEXAI_SEARCH_LOCATION: 'global', 17 | PORT: 8080, 18 | }; 19 | -------------------------------------------------------------------------------- /node-serverless/src/types/message.ts: -------------------------------------------------------------------------------- 1 | type StringObject = { 2 | stringValue: string; 3 | } 4 | 5 | type MerchResult = { 6 | name: StringObject; 7 | gms_desc: StringObject; 8 | link: StringObject; 9 | gms_name: StringObject; 10 | id: StringObject; 11 | price: StringObject; 12 | } 13 | 14 | type PhoneResult = { 15 | name: StringObject; 16 | condition: StringObject; 17 | availability: StringObject; 18 | link: StringObject; 19 | id: StringObject; 20 | price: StringObject; 21 | currency: StringObject; 22 | } 23 | 24 | type Message = { 25 | content: string|MerchResult|PhoneResult; 26 | timestamp: Date; 27 | state: string; 28 | triggerSearch?: boolean; 29 | audioSynthesis?: boolean; 30 | delayAudioSynthesis?: boolean; 31 | audio?: { 32 | language: string; 33 | voice: string; 34 | } 35 | } 36 | 37 | export { 38 | Message, 39 | PhoneResult, 40 | MerchResult 41 | } 42 | -------------------------------------------------------------------------------- /node-serverless/src/app/messages/messages.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MerchResult, Message, PhoneResult } from '../../types/message'; 4 | 5 | @Component({ 6 | selector: 'app-messages', 7 | standalone: true, 8 | imports: [ 9 | CommonModule 10 | ], 11 | templateUrl: './messages.component.html', 12 | styleUrl: './messages.component.scss' 13 | }) 14 | export class MessagesComponent { 15 | @Input('messages') messages: Message[] = []; 16 | 17 | getMerchContent(message: Message): MerchResult { 18 | return message.content as MerchResult; 19 | } 20 | 21 | getPhoneContent(message: Message): PhoneResult { 22 | return message.content as PhoneResult; 23 | } 24 | 25 | getHourVal(date: Date): string { 26 | return `0${date.getHours()}`.slice(-2); 27 | } 28 | 29 | getMinuteVal(date: Date): string { 30 | return `0${date.getMinutes()}`.slice(-2); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /node-serverless/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": [ 22 | "ES2022", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .tmp 3 | __pycache__ 4 | 5 | # See http://help.github.com/ignore-files/ for more about ignoring files. 6 | 7 | # Compiled output 8 | node-serverless/dist 9 | node-serverless/tmp 10 | node-serverless/out-tsc 11 | node-serverless/bazel-out 12 | 13 | # Node 14 | node-serverless/server/node_modules 15 | node-serverless/node_modules 16 | node-serverless/server/config/env.js 17 | node-serverless/server/npm-debug.log 18 | node-serverless/npm-debug.log 19 | node-serverless/server/yarn-error.log 20 | node-serverless/yarn-error.log 21 | 22 | # Python 23 | python-serverless/venv 24 | 25 | 26 | # IDEs and editors 27 | .idea/ 28 | .project 29 | .classpath 30 | .c9/ 31 | *.launch 32 | .settings/ 33 | *.sublime-workspace 34 | 35 | # Visual Studio Code 36 | .vscode/ 37 | .history/* 38 | 39 | # Miscellaneous 40 | node-serverless/.angular/cache 41 | .sass-cache/ 42 | /connect.lock 43 | /coverage 44 | /libpeerconnection.log 45 | testem.log 46 | /typings 47 | 48 | # System files 49 | .DS_Store 50 | Thumbs.db 51 | 52 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'chat-with-gemini' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('chat-with-gemini'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, chat-with-gemini'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /python-setup/search_tools/vertexai_search.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | import requests 3 | import os 4 | 5 | load_dotenv("/opt/env/.env") 6 | 7 | GCP_PYTHON_SERVERLESS_URL = os.getenv("GCP_PYTHON_SERVERLESS_URL") 8 | 9 | def query(product_name: str, product_description: str): 10 | """ 11 | Use this tool to only search for Google Merch Store products. Else don't use this tool. 12 | 13 | Find a product with the product_name and product_description from 14 | Google Merch Shop and returns a dictionary containing product details. 15 | """ 16 | 17 | query = product_name + " " + product_description 18 | url = f"{GCP_PYTHON_SERVERLESS_URL}/search.json?query={query}&page=1&page_size=1" 19 | results = requests.get(url).json() 20 | target_result = results[0]['document']['structData'] 21 | 22 | productDetails = f""" 23 | {target_result['gms_name']} is a product sold at Google Merch Shop. The price is {target_result['price']}. 24 | {target_result['gms_desc']}. You can buy the product at their web site: {target_result['link']}" 25 | """ 26 | 27 | return {"productDetails": productDetails} 28 | -------------------------------------------------------------------------------- /python-setup/search_tools/exchange_rate.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def query( 4 | currency_from: str, 5 | currency_to: str 6 | ): 7 | """ 8 | Use this tool to only retrieves the exchange rate between two currencies. Else don't use this tool. 9 | 10 | Uses the Frankfurter API (https://api.frankfurter.app/) to obtain 11 | exchange rate data. 12 | 13 | Args: 14 | currency_from: The base currency (3-letter currency code). 15 | Defaults to "USD" (US Dollar). 16 | currency_to: The target currency (3-letter currency code). 17 | Defaults to "EUR" (Euro). 18 | 19 | Returns: 20 | dict: A dictionary containing the exchange rate information. 21 | Example: {"amount": 1.0, "base": "USD", "date": "2023-11-24", 22 | "rates": {"EUR": 0.95534}} 23 | """ 24 | data = {} 25 | try: 26 | response = requests.get( 27 | f"https://api.frankfurter.app/latest", 28 | params={"base": currency_from, "symbols": currency_to}, 29 | ) 30 | data = response.json() 31 | except: 32 | return {"message": f"Sorry, I couldn't find the exchange rate between {currency_from} and {currency_to}."} 33 | 34 | return data 35 | -------------------------------------------------------------------------------- /node-serverless/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SearchWithVertexAI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /node-serverless/server/gemini_functions/exchange_rate.js: -------------------------------------------------------------------------------- 1 | const { FunctionDeclarationSchemaType } = require('@google-cloud/vertexai'); 2 | const axios = require('axios'); 3 | 4 | const getExchangeRateFunctionDeclaration = { 5 | name: 'get_exchange_rate', 6 | description: 'Get the exchange rate from one currency to another currency', 7 | parameters: { 8 | type: FunctionDeclarationSchemaType.OBJECT, 9 | properties: { 10 | currency_from: { 11 | type: FunctionDeclarationSchemaType.STRING, 12 | description: 'The currency to convert from' 13 | }, 14 | currency_to: { 15 | type: FunctionDeclarationSchemaType.STRING, 16 | description: 'The currency to convert to' 17 | }, 18 | }, 19 | required: ['currency_from', 'currency_to'], 20 | }, 21 | }; 22 | 23 | async function getExchangeRate(currencyFrom, currencyTo) { 24 | try { 25 | const response = await axios.get('https://api.frankfurter.app/latest', { 26 | params: { 27 | base: currencyFrom, 28 | symbols: currencyTo 29 | } 30 | }); 31 | 32 | return response.data; 33 | } catch (e) { 34 | console.error(e); 35 | return null; 36 | } 37 | } 38 | 39 | module.exports = { 40 | getExchangeRateFunctionDeclaration, 41 | getExchangeRate, 42 | }; 43 | -------------------------------------------------------------------------------- /python-setup/search_tools/wikipedia.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def query( 4 | query: str, 5 | ): 6 | """ 7 | Use this tool to search for information on a places from Wikipedia. Else don't use this tool. 8 | 9 | Args: 10 | query: the name of object or topic to find. 11 | 12 | Example: {"answer": "Southeast Asia is home to over 650 million people, representing hundreds of ethnic groups and languages. Major countries like Indonesia, the Philippines, and Vietnam each have distinct cultures and traditions."} 13 | """ 14 | 15 | try: 16 | wiki_title = search_wiki_title(query) 17 | wiki_full_text = get_wiki_full_text(wiki_title) 18 | except Exception as e: 19 | return {"answer": "Sorry, I couldn't find any information on that topic."} 20 | 21 | return {"answer": wiki_full_text} 22 | 23 | # Search for a relevant wikipedia article title 24 | def search_wiki_title(query): 25 | url = f"https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={query}&srlimit=1&format=json" 26 | resp = requests.get(url) 27 | return resp.json()["query"]["search"][0]["title"] 28 | 29 | # From previously found title, get the article text 30 | def get_wiki_full_text(wiki_title): 31 | url = f"https://en.wikipedia.org/w/api.php?action=query&prop=extracts&titles={wiki_title}&explaintext=true&format=json" 32 | response = requests.get(url) 33 | data = response.json() 34 | page_id = next(iter(data["query"]["pages"])) 35 | plain_text = data["query"]["pages"][page_id]["extract"] 36 | return plain_text 37 | -------------------------------------------------------------------------------- /node-serverless/src/app/message-input/message-input.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 13 | 20 | 27 |
28 | -------------------------------------------------------------------------------- /node-serverless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-with-gemini", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --host 0.0.0.0", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "serve:ssr:chat-with-gemini": "node dist/chat-with-gemini/server/server.mjs" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^17.3.0", 15 | "@angular/cdk": "^17.3.6", 16 | "@angular/common": "^17.3.0", 17 | "@angular/compiler": "^17.3.0", 18 | "@angular/core": "^17.3.0", 19 | "@angular/forms": "^17.3.0", 20 | "@angular/material": "^17.3.6", 21 | "@angular/platform-browser": "^17.3.0", 22 | "@angular/platform-browser-dynamic": "^17.3.0", 23 | "@angular/platform-server": "^17.3.0", 24 | "@angular/router": "^17.3.0", 25 | "@angular/ssr": "^17.3.6", 26 | "express": "^4.18.2", 27 | "rxjs": "~7.8.0", 28 | "tslib": "^2.3.0", 29 | "zone.js": "~0.14.3" 30 | }, 31 | "devDependencies": { 32 | "@types/dom-speech-recognition": "^0.0.4", 33 | "@angular-devkit/build-angular": "^17.3.6", 34 | "@angular/cli": "^17.3.6", 35 | "@angular/compiler-cli": "^17.3.0", 36 | "@types/express": "^4.17.17", 37 | "@types/jasmine": "~5.1.0", 38 | "@types/node": "^18.18.0", 39 | "jasmine-core": "~5.1.0", 40 | "karma": "~6.4.0", 41 | "karma-chrome-launcher": "~3.2.0", 42 | "karma-coverage": "~2.2.0", 43 | "karma-jasmine": "~5.1.0", 44 | "karma-jasmine-html-reporter": "~2.1.0", 45 | "typescript": "~5.4.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /node-serverless/server.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { CommonEngine } from '@angular/ssr'; 3 | import express from 'express'; 4 | import { fileURLToPath } from 'node:url'; 5 | import { dirname, join, resolve } from 'node:path'; 6 | import bootstrap from './src/main.server'; 7 | 8 | // The Express app is exported so that it can be used by serverless Functions. 9 | export function app(): express.Express { 10 | const server = express(); 11 | const serverDistFolder = dirname(fileURLToPath(import.meta.url)); 12 | const browserDistFolder = resolve(serverDistFolder, '../browser'); 13 | const indexHtml = join(serverDistFolder, 'index.server.html'); 14 | 15 | const commonEngine = new CommonEngine(); 16 | 17 | server.set('view engine', 'html'); 18 | server.set('views', browserDistFolder); 19 | 20 | // Example Express Rest API endpoints 21 | // server.get('/api/**', (req, res) => { }); 22 | // Serve static files from /browser 23 | server.get('*.*', express.static(browserDistFolder, { 24 | maxAge: '1y' 25 | })); 26 | 27 | // All regular routes use the Angular engine 28 | server.get('*', (req, res, next) => { 29 | const { protocol, originalUrl, baseUrl, headers } = req; 30 | 31 | commonEngine 32 | .render({ 33 | bootstrap, 34 | documentFilePath: indexHtml, 35 | url: `${protocol}://${headers.host}${originalUrl}`, 36 | publicPath: browserDistFolder, 37 | providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], 38 | }) 39 | .then((html) => res.send(html)) 40 | .catch((err) => next(err)); 41 | }); 42 | 43 | return server; 44 | } 45 | 46 | function run(): void { 47 | const port = process.env['PORT'] || 4000; 48 | 49 | // Start up the Node server 50 | const server = app(); 51 | server.listen(port, () => { 52 | console.log(`Node Express server listening on http://localhost:${port}`); 53 | }); 54 | } 55 | 56 | run(); 57 | -------------------------------------------------------------------------------- /tools/terminal_scripts/utils/prompt_user_input.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TMP_DIR=".tmp" 4 | mkdir -p $TMP_DIR 5 | PROMPT_DIR="prompt" 6 | mkdir -p $TMP_DIR/$PROMPT_DIR 7 | 8 | # INPUT VARIABLES 9 | # $1 - Global variable name to write prompt into 10 | # $2 - Input prompt message for user 11 | # $3 - Reuse cache prompt message for user 12 | # $4 - Reuse cache message reminder for user 13 | # $5 - Valid input message for user 14 | # $6 - Invalid input message for user 15 | # 16 | # Example usage: 17 | # 18 | # promptUserEntry \ 19 | # 'gcpProjectID' \ 20 | # 'Please input your Google Cloud Platform Project ID:' \ 21 | # 'Use the same project id? (Y/n):' \ 22 | # 'You previously inputted GCP Project ID as' \ 23 | # 'You inputted GCP Project ID as' \ 24 | # 'Please input a valid value.' 25 | # 26 | # 27 | # OUTPUT VARIABLES 28 | # None 29 | # 30 | promptUserEntry(){ 31 | local _outputVar=$1 32 | local _inputPrompt=$2 33 | local _reuseCacheValuePrompt=$3 34 | local _reuseCacheValueMessage=$4 35 | local _validInputMessage=$5 36 | local _invalidInputMessage=$6 37 | 38 | local _promptCacheFilename="$TMP_DIR/$PROMPT_DIR/$_outputVar" 39 | local _reuseCacheDecision="" 40 | local _userPrompt="" 41 | 42 | if [ -e $_promptCacheFilename ]; then 43 | _userPrompt=`cat $_promptCacheFilename` 44 | echo "$_reuseCacheValueMessage \"$_userPrompt\"" 45 | 46 | while true; do 47 | read -p "$(echo $_reuseCacheValuePrompt) " _reuseCacheDecision 48 | case $_reuseCacheDecision in 49 | Y|y ) echo ""; eval $_outputVar="'$_userPrompt'"; return 0;; 50 | N|n ) break;; 51 | * ) echo $_invalidInputMessage;; 52 | esac 53 | done 54 | fi 55 | 56 | while true; do 57 | read -p "$(echo $_inputPrompt) " _userPrompt 58 | case $_userPrompt in 59 | "") echo $_invalidInputMessage;; 60 | * ) echo $_userPrompt > $_promptCacheFilename; break;; 61 | esac 62 | done 63 | echo "$_validInputMessage \"$_userPrompt\"\n" 64 | 65 | eval $_outputVar="'$_userPrompt'" 66 | } 67 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | node-serverless-server: 3 | platform: linux/amd64 4 | build: 5 | context: . 6 | dockerfile: node-serverless/server/Dockerfile.dev 7 | working_dir: /opt/app/server 8 | container_name: node-serverless-server 9 | environment: 10 | - GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json 11 | - NODE_ENV=development 12 | volumes: 13 | - ./node-serverless:/opt/app/ 14 | - $HOME/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro 15 | ports: 16 | - 8080:8080 17 | 18 | node-serverless-client: 19 | platform: linux/amd64 20 | build: 21 | context: . 22 | dockerfile: node-serverless/Dockerfile.dev 23 | working_dir: /opt/app 24 | container_name: node-serverless-client 25 | environment: 26 | - GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json 27 | - NODE_ENV=development 28 | volumes: 29 | - ./node-serverless:/opt/app/ 30 | - $HOME/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro 31 | ports: 32 | - 4200:4200 33 | 34 | python-serverless: 35 | platform: linux/amd64 36 | build: 37 | context: . 38 | dockerfile: python-serverless/Dockerfile.dev 39 | working_dir: /opt/app 40 | container_name: python-serverless 41 | environment: 42 | - GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json 43 | volumes: 44 | - ./python-serverless:/opt/app/ 45 | - $HOME/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro 46 | - .env:/opt/env/.env:ro 47 | ports: 48 | - 5000:5000 49 | 50 | python-setup: 51 | platform: linux/amd64 52 | build: 53 | context: . 54 | dockerfile: python-setup/Dockerfile 55 | working_dir: /opt/setup 56 | container_name: python-setup 57 | environment: 58 | - GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json 59 | volumes: 60 | - ./python-setup:/opt/setup/ 61 | - $HOME/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro 62 | - .env:/opt/env/.env:ro 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent 2 | 3 | # Only run the following to get .env file if the file exists 4 | ifneq ("$(wildcard .env)","") 5 | include .env 6 | export $(shell sed 's/=.*//' .env) 7 | endif 8 | 9 | # Setup commands -------------------------------------------------------------- 10 | 11 | first-time-setup: 12 | ./tools/terminal_scripts/first_time_setup.sh 13 | 14 | run_python_setup_command: 15 | ifdef resource_id 16 | @docker exec -t python-setup bash -c "python3 -m venv /opt/venv && source /opt/venv/bin/activate && python3 app.py $(command) --resource_id=$(resource_id)" 17 | else 18 | @docker exec -t python-setup bash -c "python3 -m venv /opt/venv && source /opt/venv/bin/activate && python3 app.py $(command)" 19 | endif 20 | 21 | # Shell commands -------------------------------------------------------------- 22 | 23 | bash_python_setup: 24 | @docker exec -it python-setup bash 25 | 26 | bash_node_server: 27 | @docker exec -it node-serverless-server sh 28 | 29 | bash_node_client: 30 | @docker exec -it node-serverless-client sh 31 | 32 | bash_python_server: 33 | @docker exec -it python-serverless sh 34 | 35 | node_client_build: 36 | @docker exec -it node-serverless-client sh -c "npm run build" 37 | 38 | # Docker commands ------------------------------------------------------------- 39 | 40 | docker_build_node_serverless: 41 | @cd node-serverless && docker build --platform linux/amd64 -t node-serverless-server . 42 | 43 | docker_test_node_serverless: 44 | @docker run -v $$(pwd)/node-serverless/server/config:/opt/app/server/config -v $$HOME/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro -e GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json -p 8080:8080 node-serverless-server:latest 45 | 46 | docker_build_python_serverless: 47 | @cd python-serverless && DOCKER_BUILDKIT=0 docker build --platform linux/amd64 -t python-serverless-server . 48 | 49 | docker_test_python_serverless: 50 | @docker run -v $$(pwd)/.env:/opt/env/.env -v $$HOME/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro -e GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json -p 8080:8080 python-serverless-server:latest 51 | -------------------------------------------------------------------------------- /node-serverless/src/app/messages/messages.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{ message.content }} 5 | 8 |
9 |
10 |
11 | 12 |
13 |
14 |

{{ getMerchContent(message).name.stringValue }}

15 |

16 | {{ getMerchContent(message).gms_desc.stringValue }} 17 |

18 | Price: {{ getMerchContent(message).price.stringValue }} 19 |
20 |
21 | Buy at Merch Store 22 |
23 |
24 |
25 |

{{ getPhoneContent(message).name.stringValue }}

26 | Price: {{ getPhoneContent(message).price.stringValue }} {{ getPhoneContent(message).currency.stringValue }} 27 |
28 |
29 | Buy at Phone Store 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /node-serverless/src/app/message-input/message-input.component.scss: -------------------------------------------------------------------------------- 1 | /* Compose */ 2 | 3 | .conversation-compose { 4 | display: flex; 5 | flex-direction: row; 6 | align-items: flex-end; 7 | overflow: hidden; 8 | height: 50px; 9 | width: 100%; 10 | z-index: 2; 11 | } 12 | 13 | .conversation-compose div, 14 | .conversation-compose input { 15 | background: #fff; 16 | height: 100%; 17 | } 18 | 19 | .conversation-compose .emoji { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | background: white; 24 | border-radius: 5px 0 0 5px; 25 | flex: 0 0 auto; 26 | margin-left: 8px; 27 | width: 48px; 28 | } 29 | 30 | .conversation-compose .input-msg { 31 | border: 0; 32 | flex: 1 1 auto; 33 | font-size: 16px; 34 | margin: 0; 35 | outline: none; 36 | min-width: 50px; 37 | } 38 | 39 | .conversation-compose .photo { 40 | flex: 0 0 auto; 41 | border-radius: 0 0 5px 0; 42 | text-align: center; 43 | position: relative; 44 | width: 48px; 45 | } 46 | 47 | .conversation-compose .photo:after { 48 | border-width: 0px 0 10px 10px; 49 | border-color: transparent transparent transparent #fff; 50 | border-style: solid; 51 | position: absolute; 52 | width: 0; 53 | height: 0; 54 | content: ""; 55 | top: 0; 56 | right: -10px; 57 | } 58 | 59 | .conversation-compose .photo i { 60 | display: block; 61 | color: #7d8488; 62 | font-size: 24px; 63 | transform: translate(-50%, -50%); 64 | position: relative; 65 | top: 50%; 66 | left: 50%; 67 | } 68 | 69 | .conversation-compose .send { 70 | background: transparent; 71 | border: 0; 72 | cursor: pointer; 73 | flex: 0 0 auto; 74 | margin-left: 8px; 75 | margin-right: 8px; 76 | padding: 0; 77 | position: relative; 78 | outline: none; 79 | } 80 | 81 | .conversation-compose .send .circle { 82 | background: #008a7c; 83 | border-radius: 50%; 84 | color: #fff; 85 | position: relative; 86 | width: 48px; 87 | height: 48px; 88 | display: flex; 89 | align-items: center; 90 | justify-content: center; 91 | } 92 | 93 | .conversation-compose .send .circle i { 94 | font-size: 24px; 95 | margin-left: 5px; 96 | } 97 | 98 | .mic { 99 | padding: 0; 100 | border: none; 101 | height: 100%; 102 | cursor: pointer; 103 | } 104 | -------------------------------------------------------------------------------- /node-serverless/server/gemini_functions/wikipedia.js: -------------------------------------------------------------------------------- 1 | const { FunctionDeclarationSchemaType } = require('@google-cloud/vertexai'); 2 | const axios = require('axios'); 3 | 4 | const getWikipediaContentFunctionDeclaration = { 5 | name: 'get_wikipedia_content', 6 | description: 'Wikipedia is an online encyclopedia that provides information on a wide range of topics. This function will return the content of a Wikipedia article based on the subject provided.', 7 | parameters: { 8 | type: FunctionDeclarationSchemaType.OBJECT, 9 | properties: { 10 | subject: { 11 | type: FunctionDeclarationSchemaType.STRING, 12 | description: 'The name of object or topic to find.' 13 | }, 14 | }, 15 | required: ['subject'], 16 | }, 17 | }; 18 | 19 | async function getWikipediaContent(subject) { 20 | try { 21 | const subjectTitle = await searchWikiTitle(subject); 22 | if (subjectTitle) { 23 | const answer = await getWikiFullText(subjectTitle); 24 | return answer; 25 | } 26 | 27 | return null; 28 | } catch (e) { 29 | console.error(e); 30 | return null; 31 | } 32 | } 33 | 34 | // Search for a relevant wikipedia article title 35 | async function searchWikiTitle(subject) { 36 | const response = await axios.get('https://en.wikipedia.org/w/api.php', { 37 | params: { 38 | action: 'query', 39 | list: 'search', 40 | srsearch: subject, 41 | srlimit: 1, 42 | format: 'json', 43 | } 44 | }); 45 | 46 | if (response?.data?.query?.search && 47 | response?.data?.query?.search.length > 0 && 48 | response?.data?.query?.search[0]?.title) { 49 | 50 | return response.data.query.search[0].title 51 | } 52 | 53 | return null; 54 | } 55 | 56 | // From previously found title, get the article text 57 | async function getWikiFullText(wikiTitle){ 58 | const response = await axios.get('https://en.wikipedia.org/w/api.php', { 59 | params: { 60 | action: 'query', 61 | prop: 'extracts', 62 | titles: wikiTitle, 63 | explaintext: 'true', 64 | format: 'json', 65 | } 66 | }); 67 | 68 | if (response?.data?.query?.pages) { 69 | const keys = Object.keys(response?.data?.query?.pages); 70 | const pages = response?.data?.query?.pages; 71 | 72 | if (keys.length > 0 && pages[keys[0]]?.extract) { 73 | return pages[keys[0]].extract; 74 | } 75 | } 76 | 77 | return null; 78 | } 79 | 80 | module.exports = { 81 | getWikipediaContentFunctionDeclaration, 82 | getWikipediaContent, 83 | }; 84 | -------------------------------------------------------------------------------- /node-serverless/server/gemini_functions/vertexai_google_merch_store_search.js: -------------------------------------------------------------------------------- 1 | const { FunctionDeclarationSchemaType } = require('@google-cloud/vertexai'); 2 | const { SearchServiceClient } = require('@google-cloud/discoveryengine').v1beta; 3 | const env = require('../config/env'); 4 | 5 | // Init SearchServiceClient 6 | const searchClient = new SearchServiceClient({ 7 | apiEndpoint: `${env.GCP_VERTEXAI_SEARCH_LOCATION}-discoveryengine.googleapis.com`, 8 | }); 9 | const searchServingConfig = `projects/${env.GCP_PROJECT_ID_NUMBER}/locations/${env.GCP_VERTEXAI_SEARCH_LOCATION}/collections/default_collection/engines/${env.GCP_VERTEXAI_SEARCH_ENGINE_ID}/servingConfigs/default_search:search` 10 | 11 | // Function declaration 12 | const googleMerchStoreSearchDeclaration = { 13 | name: 'google_merch_store_search', 14 | description:` 15 | Use this tool to only search for Google Merch Store products, as well as Pixel and Android phone products. 16 | 17 | Find a product with the productName and productDescription and returns a JSON object containing product details. 18 | `, 19 | parameters: { 20 | type: FunctionDeclarationSchemaType.OBJECT, 21 | properties: { 22 | productName: { 23 | type: FunctionDeclarationSchemaType.STRING, 24 | description: 'Product name to search for' 25 | }, 26 | productDescription: { 27 | type: FunctionDeclarationSchemaType.STRING, 28 | description: 'Product description to search for' 29 | }, 30 | }, 31 | required: ['productName', 'productDescription'], 32 | }, 33 | }; 34 | 35 | // Actual function to perform 36 | async function googleMerchStoreSearch(productName, productDescription) { 37 | const request = { 38 | pageSize: 10, 39 | query: `${productName} ${productDescription}`, 40 | servingConfig: searchServingConfig, 41 | }; 42 | 43 | const response = await searchClient.search(request, { 44 | autoPaginate: false, 45 | }); 46 | 47 | // Index 2 is search response - See here for more - https://cloud.google.com/generative-ai-app-builder/docs/preview-search-results 48 | const searchResults = response[2].results; 49 | const formattedSearchResults = searchResults.map((result) => { 50 | // You might want to add some type from your datastore results so you don't need to process it here 51 | return result?.document?.structData?.fields || {}; 52 | }).filter((result) => { 53 | return Object.keys(result).length > 0 54 | }); 55 | 56 | return formattedSearchResults; 57 | } 58 | 59 | module.exports = { 60 | googleMerchStoreSearchDeclaration, 61 | googleMerchStoreSearch, 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /python-serverless/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, jsonify 3 | import json 4 | from dotenv import load_dotenv 5 | from google.api_core.client_options import ClientOptions 6 | from google.cloud import discoveryengine_v1 as discoveryengine 7 | 8 | load_dotenv("/opt/env/.env") 9 | 10 | 11 | app = Flask(__name__) 12 | 13 | GCP_PROJECT_ID = os.getenv("GCP_PROJECT_ID") 14 | GCP_VERTEXAI_SEARCH_ENGINE_ID = os.getenv("GCP_VERTEXAI_SEARCH_ENGINE_ID") 15 | GCP_VERTEXAI_SEARCH_LOCATION = os.getenv("GCP_VERTEXAI_SEARCH_LOCATION") 16 | 17 | search_client_options = ClientOptions(api_endpoint=f"{GCP_VERTEXAI_SEARCH_LOCATION}-discoveryengine.googleapis.com") 18 | search_client = discoveryengine.SearchServiceClient( 19 | client_options=search_client_options 20 | ) 21 | search_serving_config = f"projects/{GCP_PROJECT_ID}/locations/{GCP_VERTEXAI_SEARCH_LOCATION}/collections/default_collection/engines/{GCP_VERTEXAI_SEARCH_ENGINE_ID}/servingConfigs/default_search:search" 22 | 23 | def do_vertexai_search(search_query, page_size=10, offset=0): 24 | # build a search request 25 | request = discoveryengine.SearchRequest( 26 | serving_config = search_serving_config, 27 | query = search_query, 28 | page_size = page_size, 29 | offset = offset, 30 | ) 31 | 32 | # search 33 | resp_pager = search_client.search(request) 34 | 35 | # parse the results 36 | response = discoveryengine.SearchResponse( 37 | results = resp_pager.results, 38 | facets = resp_pager.facets, 39 | total_size = resp_pager.total_size, 40 | attribution_token = resp_pager.attribution_token, 41 | next_page_token = resp_pager.next_page_token, 42 | corrected_query = resp_pager.corrected_query, 43 | summary = resp_pager.summary, 44 | ) 45 | response_json = json.loads( 46 | discoveryengine.SearchResponse.to_json( 47 | response, 48 | including_default_value_fields=True, 49 | use_integers_for_enums=False, 50 | ) 51 | ) 52 | 53 | return response_json['results'] 54 | 55 | @app.route("/search.json", methods=["GET"]) 56 | def vertex_ai_search_engine(): 57 | query = request.args.get("query") 58 | if not query: 59 | return jsonify({"error": "'query' parameter is required as non-empty string"}), 400 60 | 61 | page = request.args.get("page", type=int, default=1) 62 | if page and page < 1: 63 | return jsonify({"error": "'page' parameter is required as positive int"}), 400 64 | 65 | per_page = request.args.get("per_page", type=int, default=10) 66 | if per_page and (per_page < 1 or per_page > 100): 67 | return jsonify({"error": "'per_page' parameter is required as positive int smaller than 100"}), 400 68 | 69 | if page < 1: 70 | page = 1 71 | if per_page < 1: 72 | per_page = 10 73 | 74 | offset = page * per_page - per_page 75 | 76 | results = do_vertexai_search(query, page_size=per_page, offset=offset) 77 | return jsonify(results), 200 78 | 79 | if __name__ == "__main__": 80 | app.run(debug=True, host="0.0.0.0", port=5000) 81 | -------------------------------------------------------------------------------- /node-serverless/src/app/search/search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MerchResult, Message, PhoneResult } from '../../types/message'; 4 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 8 | 9 | @Component({ 10 | selector: 'app-search', 11 | standalone: true, 12 | imports: [ 13 | CommonModule, 14 | MatProgressSpinnerModule, 15 | FormsModule, 16 | HttpClientModule, 17 | MatCardModule, 18 | ], 19 | templateUrl: './search.component.html', 20 | styleUrl: './search.component.scss' 21 | }) 22 | export class SearchComponent { 23 | isInitial: boolean = true; 24 | resultCount: number = 0; 25 | secondsTaken: number = 0; 26 | results: Array = []; 27 | searching: boolean = true; 28 | geminiResponse: string = ''; 29 | 30 | public searchText: string = ''; 31 | 32 | constructor(private http: HttpClient) { 33 | } 34 | 35 | public search() { 36 | // Reset states 37 | this.searching = true; 38 | this.resultCount = 0; 39 | this.secondsTaken = 0; 40 | this.results = []; 41 | this.geminiResponse = ''; 42 | 43 | // Always set this, escape back to home screen 44 | if (this.searchText === '') { 45 | this.isInitial = true; 46 | return; 47 | } 48 | 49 | this.isInitial = false; 50 | const startTime = Date.now(); 51 | 52 | // Simulate search operation 53 | return this.http.post<{message: string, results: Array}>('/search', { 54 | message: this.searchText, 55 | }) 56 | .subscribe((data) => { 57 | if (data.results && data.results.length > 0) { 58 | data.results.forEach((result) => { 59 | if (result.type?.stringValue === 'phone') { 60 | this.results.push({ 61 | content: result as PhoneResult, 62 | timestamp: new Date(), 63 | state: 'gemini-result-phone', 64 | }); 65 | } else { 66 | this.results.push({ 67 | content: result as MerchResult, 68 | timestamp: new Date(), 69 | state: 'gemini-result-merch', 70 | }); 71 | } 72 | }); 73 | } else { 74 | this.geminiResponse = data.message; 75 | } 76 | 77 | const endTime = Date.now(); 78 | this.secondsTaken = (endTime - startTime) / 1000; 79 | this.resultCount = this.results.length; 80 | this.searching = false; 81 | }); 82 | } 83 | 84 | getMerchContent(message: Message): MerchResult { 85 | return message.content as MerchResult; 86 | } 87 | 88 | getPhoneContent(message: Message): PhoneResult { 89 | return message.content as PhoneResult; 90 | } 91 | 92 | getHourVal(date: Date): string { 93 | return `0${date.getHours()}`.slice(-2); 94 | } 95 | 96 | getMinuteVal(date: Date): string { 97 | return `0${date.getMinutes()}`.slice(-2); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 | Avatar 41 |
42 |
43 | Gemini 44 | online 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 | 61 |
62 | 65 | 66 |
67 |
68 | 71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /node-serverless/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "chat-with-gemini": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist/chat-with-gemini", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": [ 24 | "zone.js" 25 | ], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "@angular/material/prebuilt-themes/indigo-pink.css", 34 | "src/styles.scss" 35 | ], 36 | "scripts": [], 37 | "server": "src/main.server.ts", 38 | "prerender": true, 39 | "ssr": { 40 | "entry": "server.ts" 41 | } 42 | }, 43 | "configurations": { 44 | "production": { 45 | "budgets": [ 46 | { 47 | "type": "initial", 48 | "maximumWarning": "500kb", 49 | "maximumError": "1mb" 50 | }, 51 | { 52 | "type": "anyComponentStyle", 53 | "maximumWarning": "5kb", 54 | "maximumError": "10kb" 55 | } 56 | ], 57 | "outputHashing": "all" 58 | }, 59 | "development": { 60 | "optimization": false, 61 | "extractLicenses": false, 62 | "sourceMap": true 63 | } 64 | }, 65 | "defaultConfiguration": "production" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "configurations": { 70 | "production": { 71 | "buildTarget": "chat-with-gemini:build:production" 72 | }, 73 | "development": { 74 | "buildTarget": "chat-with-gemini:build:development" 75 | } 76 | }, 77 | "defaultConfiguration": "development" 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "buildTarget": "chat-with-gemini:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "polyfills": [ 89 | "zone.js", 90 | "zone.js/testing" 91 | ], 92 | "tsConfig": "tsconfig.spec.json", 93 | "inlineStyleLanguage": "scss", 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ], 98 | "styles": [ 99 | "@angular/material/prebuilt-themes/indigo-pink.css", 100 | "src/styles.scss" 101 | ], 102 | "scripts": [] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "cli": { 109 | "analytics": false 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tools/terminal_scripts/first_time_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Include all common script dependencies -------------------------------------- 4 | source 'tools/terminal_scripts/utils/load_script_env_vars.sh' 5 | source 'tools/terminal_scripts/utils/echo_and_run.sh' 6 | source 'tools/terminal_scripts/utils/prompt_user_input.sh' 7 | source 'tools/terminal_scripts/utils/save_to_dotenv.sh' 8 | # ----------------------------------------------------------------- Include End 9 | 10 | promptUserEntry \ 11 | 'gcpProjectId' \ 12 | "${ECHO_YELLOW_COLOR}Please input your Google Cloud Platform Project ID:${ECHO_NO_COLOR}" \ 13 | "${ECHO_YELLOW_COLOR}Use the same project id? (${ECHO_WHITE_COLOR}Y/n${ECHO_YELLOW_COLOR}):${ECHO_NO_COLOR}" \ 14 | "${ECHO_YELLOW_COLOR}You previously inputted GCP Project ID as${ECHO_NO_COLOR}" \ 15 | "${ECHO_YELLOW_COLOR}You inputted GCP Project ID as${ECHO_NO_COLOR}" \ 16 | "${ECHO_RED_COLOR}Please input a valid value.${ECHO_NO_COLOR}" 17 | 18 | promptUserEntry \ 19 | 'gcpLocation' \ 20 | "${ECHO_YELLOW_COLOR}Please input your intended GCP region (See region available here - https://cloud.google.com/about/locations):${ECHO_NO_COLOR}" \ 21 | "${ECHO_YELLOW_COLOR}Use the same region? (${ECHO_WHITE_COLOR}Y/n${ECHO_YELLOW_COLOR}):${ECHO_NO_COLOR}" \ 22 | "${ECHO_YELLOW_COLOR}You previously inputted region as${ECHO_NO_COLOR}" \ 23 | "${ECHO_YELLOW_COLOR}You inputted GCP region as${ECHO_NO_COLOR}" \ 24 | "${ECHO_RED_COLOR}Please input a valid value.${ECHO_NO_COLOR}" 25 | 26 | promptUserEntry \ 27 | 'geminiModel' \ 28 | "${ECHO_YELLOW_COLOR}Please input your intended Gemini model version (See versions available here - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#supported-models):${ECHO_NO_COLOR}" \ 29 | "${ECHO_YELLOW_COLOR}Use the same version? (${ECHO_WHITE_COLOR}Y/n${ECHO_YELLOW_COLOR}):${ECHO_NO_COLOR}" \ 30 | "${ECHO_YELLOW_COLOR}You previously inputted Gemini model as${ECHO_NO_COLOR}" \ 31 | "${ECHO_YELLOW_COLOR}You inputted Gemini model as${ECHO_NO_COLOR}" \ 32 | "${ECHO_RED_COLOR}Please input a valid value.${ECHO_NO_COLOR}" 33 | 34 | promptUserEntry \ 35 | 'gcpVertexAISearchEngineId' \ 36 | "${ECHO_YELLOW_COLOR}Please input your Vertex AI Search Engine Id (Create here - https://console.cloud.google.com/gen-app-builder/engines):${ECHO_NO_COLOR}" \ 37 | "${ECHO_YELLOW_COLOR}Use the same version? (${ECHO_WHITE_COLOR}Y/n${ECHO_YELLOW_COLOR}):${ECHO_NO_COLOR}" \ 38 | "${ECHO_YELLOW_COLOR}You previously inputted Search Engine Id as${ECHO_NO_COLOR}" \ 39 | "${ECHO_YELLOW_COLOR}You inputted Search Engine Id as${ECHO_NO_COLOR}" \ 40 | "${ECHO_RED_COLOR}Please input a valid value.${ECHO_NO_COLOR}" 41 | 42 | promptUserEntry \ 43 | 'gcpVertexAISearchLocation' \ 44 | "${ECHO_YELLOW_COLOR}Please input your Vertex AI Search Location (See here - https://console.cloud.google.com/gen-app-builder/engines):${ECHO_NO_COLOR}" \ 45 | "${ECHO_YELLOW_COLOR}Use the same version? (${ECHO_WHITE_COLOR}Y/n${ECHO_YELLOW_COLOR}):${ECHO_NO_COLOR}" \ 46 | "${ECHO_YELLOW_COLOR}You previously inputted Search Engine Location as${ECHO_NO_COLOR}" \ 47 | "${ECHO_YELLOW_COLOR}You inputted Search Engine Location as${ECHO_NO_COLOR}" \ 48 | "${ECHO_RED_COLOR}Please input a valid value.${ECHO_NO_COLOR}" 49 | 50 | gcpProjectIdNum=`gcloud projects describe $gcpProjectId --format="value(projectNumber)"` 51 | 52 | saveToDotEnv "GCP_PROJECT_ID" $gcpProjectId 53 | saveToDotEnv "GCP_PROJECT_NUMBER" $gcpProjectIdNum 54 | saveToDotEnv "GCP_REGION" $gcpLocation 55 | saveToDotEnv "GCP_GEMINI_VER" $geminiModel 56 | saveToDotEnv "GCP_VERTEXAI_SEARCH_ENGINE_ID" $gcpVertexAISearchEngineId 57 | saveToDotEnv "GCP_VERTEXAI_SEARCH_LOCATION" $gcpVertexAISearchLocation 58 | 59 | tee ./node-serverless/server/config/env.js < /dev/null 60 | module.exports = { 61 | GCP_PROJECT_ID: '$gcpProjectId', 62 | GCP_PROJECT_ID_NUMBER: '$gcpProjectIdNum', 63 | GCP_REGION: '$gcpLocation', 64 | GCP_GEMINI_VER: '$geminiModel', 65 | GCP_VERTEXAI_SEARCH_ENGINE_ID: '$gcpVertexAISearchEngineId', 66 | GCP_VERTEXAI_SEARCH_LOCATION: '$gcpVertexAISearchLocation', 67 | PORT: 8080, 68 | }; 69 | EOF 70 | -------------------------------------------------------------------------------- /node-serverless/src/app/messages/messages.component.scss: -------------------------------------------------------------------------------- 1 | 2 | /* Messages */ 3 | 4 | .message { 5 | color: #000; 6 | clear: both; 7 | line-height: 18px; 8 | font-size: 15px; 9 | padding: 8px; 10 | position: relative; 11 | margin: 8px 0; 12 | max-width: 85%; 13 | word-wrap: break-word; 14 | z-index: -1; 15 | } 16 | 17 | .message:after { 18 | position: absolute; 19 | content: ""; 20 | width: 0; 21 | height: 0; 22 | border-style: solid; 23 | } 24 | 25 | .metadata { 26 | display: inline-block; 27 | float: right; 28 | padding: 0 0 0 7px; 29 | position: relative; 30 | bottom: -4px; 31 | } 32 | 33 | .metadata .time { 34 | color: rgba(0, 0, 0, .45); 35 | font-size: 11px; 36 | display: inline-block; 37 | } 38 | 39 | .metadata .tick { 40 | display: inline-block; 41 | margin-left: 2px; 42 | position: relative; 43 | top: 4px; 44 | height: 16px; 45 | width: 16px; 46 | } 47 | 48 | .metadata .tick svg { 49 | position: absolute; 50 | transition: .5s ease-in-out; 51 | } 52 | 53 | .metadata .tick svg:first-child { 54 | -webkit-backface-visibility: hidden; 55 | backface-visibility: hidden; 56 | -webkit-transform: perspective(800px) rotateY(180deg); 57 | transform: perspective(800px) rotateY(180deg); 58 | } 59 | 60 | .metadata .tick svg:last-child { 61 | -webkit-backface-visibility: hidden; 62 | backface-visibility: hidden; 63 | -webkit-transform: perspective(800px) rotateY(0deg); 64 | transform: perspective(800px) rotateY(0deg); 65 | } 66 | 67 | .metadata .tick-animation svg:first-child { 68 | -webkit-transform: perspective(800px) rotateY(0); 69 | transform: perspective(800px) rotateY(0); 70 | } 71 | 72 | .metadata .tick-animation svg:last-child { 73 | -webkit-transform: perspective(800px) rotateY(-179.9deg); 74 | transform: perspective(800px) rotateY(-179.9deg); 75 | } 76 | 77 | .message:first-child { 78 | margin: 16px 0 8px; 79 | } 80 | 81 | .message.received { 82 | background: #fff; 83 | border-radius: 0px 5px 5px 5px; 84 | float: left; 85 | } 86 | 87 | .message.received .metadata { 88 | padding: 0 0 0 16px; 89 | } 90 | 91 | .message.received:after { 92 | border-width: 0px 10px 10px 0; 93 | border-color: transparent #fff transparent transparent; 94 | top: 0; 95 | left: -10px; 96 | } 97 | 98 | .message.sent { 99 | background: #e1ffc7; 100 | border-radius: 5px 0px 5px 5px; 101 | float: right; 102 | } 103 | 104 | .message.sent:after { 105 | border-width: 0px 0 10px 10px; 106 | border-color: transparent transparent transparent #e1ffc7; 107 | top: 0; 108 | right: -10px; 109 | } 110 | 111 | .search-item { 112 | color: #000; 113 | clear: both; 114 | line-height: 18px; 115 | font-size: 15px; 116 | padding: 8px; 117 | position: relative; 118 | margin: 8px 0; 119 | word-wrap: break-word; 120 | background: #fff; 121 | border-radius: 5px; 122 | opacity: 0.5; 123 | transition: opacity 0.3s, box-shadow 0.3s; 124 | max-width: 550px; 125 | padding: 20px; 126 | 127 | &:hover { 128 | opacity: 1; 129 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 130 | } 131 | 132 | .item-name { 133 | font-size: 1.5em; 134 | margin-bottom: 8px; 135 | } 136 | 137 | .item-desc { 138 | font-size: 1em; 139 | margin-bottom: 12px; 140 | } 141 | 142 | .item-actions { 143 | display: block; 144 | clear: both; 145 | min-height: 35px; 146 | } 147 | 148 | .item-link { 149 | float: right; 150 | margin: 0 5px; 151 | padding: 8px 16px; 152 | font-size: 1em; 153 | color: #007bff; 154 | border: 2px solid #007bff; 155 | border-radius: 5px; 156 | background: transparent; 157 | text-decoration: none; 158 | transition: background 0.3s, color 0.3s; 159 | 160 | &:hover { 161 | background: #007bff; 162 | color: #fff; 163 | } 164 | } 165 | 166 | .item-action-bar { 167 | margin-top: 50px; 168 | margin-bottom: 15px; 169 | } 170 | 171 | .item-price { 172 | font-size: 0.8em; 173 | font-weight: bold; 174 | float: right; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; 3 | } 4 | 5 | .page { 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | 13 | .marvel-device .screen { 14 | text-align: left; 15 | height: 100%; 16 | } 17 | 18 | .marvel-device.nexus5 { 19 | height: 100%; 20 | width: 100%; 21 | } 22 | 23 | .screen-container { 24 | height: 100%; 25 | } 26 | 27 | /* Status Bar */ 28 | 29 | .status-bar { 30 | height: 25px; 31 | background: #004e45; 32 | color: #fff; 33 | font-size: 14px; 34 | padding: 0 8px; 35 | } 36 | 37 | .status-bar:after { 38 | content: ""; 39 | display: table; 40 | clear: both; 41 | } 42 | 43 | .status-bar div { 44 | float: right; 45 | position: relative; 46 | top: 50%; 47 | transform: translateY(-50%); 48 | margin: 0 0 0 8px; 49 | font-weight: 600; 50 | } 51 | 52 | /* Chat */ 53 | 54 | .chat { 55 | height: calc(100% - 69px); 56 | } 57 | 58 | .chat-container { 59 | height: 100%; 60 | } 61 | 62 | /* User Bar */ 63 | 64 | .user-bar { 65 | height: 55px; 66 | background: #005e54; 67 | color: #fff; 68 | padding: 0 8px; 69 | font-size: 24px; 70 | position: relative; 71 | z-index: 1; 72 | } 73 | 74 | .user-bar:after { 75 | content: ""; 76 | display: table; 77 | clear: both; 78 | } 79 | 80 | .user-bar div { 81 | float: left; 82 | transform: translateY(-50%); 83 | position: relative; 84 | top: 50%; 85 | } 86 | 87 | .user-bar .actions { 88 | float: right; 89 | margin: 0 0 0 20px; 90 | } 91 | 92 | .user-bar .actions.more { 93 | margin: 0 12px 0 32px; 94 | } 95 | 96 | .user-bar .actions.attachment { 97 | margin: 0 0 0 30px; 98 | } 99 | 100 | .user-bar .actions.attachment i { 101 | display: block; 102 | transform: rotate(-45deg); 103 | } 104 | 105 | .user-bar .avatar { 106 | margin: 0 0 0 5px; 107 | width: 36px; 108 | height: 36px; 109 | } 110 | 111 | .user-bar .avatar img { 112 | border-radius: 50%; 113 | box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1); 114 | display: block; 115 | width: 100%; 116 | } 117 | 118 | .user-bar .name { 119 | font-size: 17px; 120 | font-weight: 600; 121 | text-overflow: ellipsis; 122 | letter-spacing: 0.3px; 123 | margin: 0 0 0 8px; 124 | overflow: hidden; 125 | white-space: nowrap; 126 | width: 110px; 127 | } 128 | 129 | .user-bar .status { 130 | display: block; 131 | font-size: 13px; 132 | font-weight: 400; 133 | letter-spacing: 0; 134 | } 135 | 136 | /* Conversation */ 137 | 138 | .conversation { 139 | height: calc(100% - 22px); 140 | position: relative; 141 | background: #efe7dd url("/assets/whatsapp-bg.jpg") repeat; 142 | z-index: 0; 143 | padding-bottom: 10px; 144 | } 145 | 146 | .conversation ::-webkit-scrollbar { 147 | transition: all .5s; 148 | width: 5px; 149 | height: 1px; 150 | z-index: 10; 151 | } 152 | 153 | .conversation ::-webkit-scrollbar-track { 154 | background: transparent; 155 | } 156 | 157 | .conversation ::-webkit-scrollbar-thumb { 158 | background: #b3ada7; 159 | } 160 | 161 | .conversation .conversation-container { 162 | height: calc(100% - 68px); 163 | box-shadow: inset 0 10px 10px -10px #000000; 164 | overflow-x: hidden; 165 | padding: 0 16px; 166 | margin-bottom: 5px; 167 | } 168 | 169 | .conversation .conversation-container:after { 170 | content: ""; 171 | display: table; 172 | clear: both; 173 | } 174 | 175 | /* Small Screens */ 176 | 177 | @media (max-width: 768px) { 178 | .marvel-device.nexus5 { 179 | border-radius: 0; 180 | flex: none; 181 | padding: 0; 182 | max-width: none; 183 | overflow: hidden; 184 | height: 100%; 185 | width: 100%; 186 | } 187 | 188 | .marvel-device > .screen .chat { 189 | visibility: visible; 190 | } 191 | 192 | .marvel-device { 193 | visibility: hidden; 194 | } 195 | 196 | .marvel-device .status-bar { 197 | display: none; 198 | } 199 | 200 | .screen-container { 201 | position: absolute; 202 | top: 0; 203 | left: 0; 204 | right: 0; 205 | bottom: 0; 206 | } 207 | 208 | .conversation { 209 | height: calc(100vh - 65px); 210 | } 211 | .conversation .conversation-container { 212 | height: calc(100vh - 120px); 213 | } 214 | } -------------------------------------------------------------------------------- /python-setup/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import vertexai 4 | import requests 5 | from vertexai.preview import reasoning_engines 6 | from dotenv import load_dotenv 7 | 8 | # Tools ======================================================================= 9 | # from search_tools.wikipedia import query as wikipedia_query 10 | # from search_tools.vertexai_search import query as search_google_merch_shop_query 11 | # from search_tools.exchange_rate import query as exchange_rate_query 12 | # ============================================================================= 13 | 14 | load_dotenv("/opt/env/.env") 15 | 16 | GCP_PROJECT_ID = os.getenv("GCP_PROJECT_ID") 17 | GCP_REGION = os.getenv("GCP_REGION") 18 | GCP_GEMINI_VER = os.getenv("GCP_GEMINI_VER") 19 | GCP_VERTEXAI_REASONING_ENGINE_BUCKET = os.getenv("GCP_VERTEXAI_REASONING_ENGINE_BUCKET") 20 | GCP_PYTHON_SERVERLESS_URL = os.getenv("GCP_PYTHON_SERVERLESS_URL") 21 | 22 | vertexai.init( 23 | project=GCP_PROJECT_ID, 24 | location=GCP_REGION, 25 | staging_bucket=GCP_VERTEXAI_REASONING_ENGINE_BUCKET, 26 | ) 27 | 28 | def vertex_ai_query(product_name: str, product_description: str): 29 | """ 30 | Use this tool to only search for Google Merch Store products. Else don't use this tool. 31 | 32 | Find a product with the product_name and product_description from 33 | Google Merch Shop and returns a dictionary containing product details. 34 | """ 35 | 36 | query = product_name + " " + product_description 37 | url = f"{GCP_PYTHON_SERVERLESS_URL}/search.json?query={query}&page=1&page_size=1" 38 | results = requests.get(url).json() 39 | 40 | target_result = results[0]['document']['structData'] 41 | 42 | productDetails = f""" 43 | {target_result['gms_name']} is a product sold at Google Merch Shop. The price is {target_result['price']}. 44 | {target_result['gms_desc']}. You can buy the product at their web site: {target_result['link']}" 45 | """ 46 | 47 | return {"productDetails": productDetails} 48 | 49 | def get_agent(): 50 | return reasoning_engines.LangchainAgent( 51 | model = GCP_GEMINI_VER, 52 | model_kwargs = { 53 | "temperature": 0.28, 54 | "max_output_tokens": 1000, 55 | "top_p": 0.95, 56 | "top_k": None, 57 | }, 58 | tools = [ 59 | vertex_ai_query 60 | ], 61 | agent_executor_kwargs={"return_intermediate_steps": True}, 62 | ) 63 | 64 | def deploy_reasoning_engine(): 65 | agent = get_agent() 66 | reasoning_engines.ReasoningEngine.create( 67 | agent, 68 | display_name="", 69 | description="", 70 | requirements=[ 71 | "google-cloud-aiplatform[langchain,reasoningengine]", 72 | "requests", 73 | ], 74 | ) 75 | 76 | def list_deployed_reasoning_engines(): 77 | print(reasoning_engines.ReasoningEngine.list()) 78 | 79 | def query_remote(resource_id): 80 | remote_app = reasoning_engines.ReasoningEngine(resource_id) 81 | query = remote_app.query(input="Google Bike Enamel Pin. Where can I buy it? Write the answer in plaintext.") 82 | print(f"\n input: {query['input']}") 83 | print(f"output: {query['output']}") 84 | 85 | def delete_reasoning_engine(resource_id): 86 | remote_app = reasoning_engines.ReasoningEngine(resource_id) 87 | remote_app.delete() 88 | 89 | if __name__ == "__main__": 90 | parser = argparse.ArgumentParser(description="Manage Reasoning Engines") 91 | parser.add_argument("command", choices=["deploy", "list", "delete", "test", "query_remote"], help="Command to execute") 92 | parser.add_argument("--resource_id", help="Resource ID for remote instance") 93 | 94 | args = parser.parse_args() 95 | print(args) 96 | 97 | if args.command == "deploy": 98 | deploy_reasoning_engine() 99 | elif args.command == "list": 100 | list_deployed_reasoning_engines() 101 | elif args.command == "query_remote": 102 | if args.resource_id is not None: 103 | query_remote(args.resource_id) 104 | else: 105 | print("Please provide a resource ID for querying remote instance") 106 | elif args.command == "delete": 107 | if args.resource_id: 108 | delete_reasoning_engine(args.resource_id) 109 | else: 110 | print("Please provide a resource ID for deletion") 111 | elif args.command == "test": 112 | agent = get_agent() 113 | query = agent.query(input="Google Bike Enamel Pin. Where can I buy it? Write the answer in plaintext.") 114 | print(f"\n input: {query['input']}") 115 | print(f"output: {query['output']}") 116 | -------------------------------------------------------------------------------- /node-serverless/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, ViewChild } from '@angular/core'; 2 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 3 | import { RouterOutlet } from '@angular/router'; 4 | import { CommonModule } from '@angular/common'; 5 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 6 | import { MessageInputComponent } from './message-input/message-input.component'; 7 | import { MessagesComponent } from './messages/messages.component'; 8 | import { Message, PhoneResult, MerchResult } from '../types/message'; 9 | import { Router, NavigationEnd } from '@angular/router'; 10 | 11 | let SpeechSynthesisApi: SpeechSynthesis; 12 | 13 | if (typeof window !== "undefined") { 14 | SpeechSynthesisApi = window.speechSynthesis; 15 | } 16 | 17 | type SpeechSynthesisUtteranceProps = { 18 | text: string, 19 | language: string, 20 | voice: string, 21 | } 22 | 23 | @Component({ 24 | selector: 'app-root', 25 | standalone: true, 26 | imports: [ 27 | RouterOutlet, 28 | HttpClientModule, 29 | MatProgressSpinnerModule, 30 | CommonModule, 31 | MessagesComponent, 32 | MessageInputComponent 33 | ], 34 | templateUrl: './app.component.html', 35 | styleUrl: './app.component.scss' 36 | }) 37 | export class AppComponent { 38 | title = 'search-with-gemini'; 39 | isRoot: boolean = false; 40 | 41 | @ViewChild('scroll', { read: ElementRef }) 42 | public scroll!: ElementRef; 43 | 44 | public messages: Message[] = []; 45 | public loaderStatus: boolean = false; 46 | 47 | constructor(private http: HttpClient, private router: Router) { 48 | this.router.events.subscribe(event => { 49 | if (event instanceof NavigationEnd) { 50 | this.isRoot = this.router.url === '/'; 51 | } 52 | }); 53 | } 54 | 55 | public scrollBottom() { 56 | this.scroll.nativeElement.scrollTop = this.scroll.nativeElement.scrollHeight; 57 | } 58 | 59 | public sendMessage(message: Message) { 60 | const { content, triggerSearch } = message; 61 | this.pushMessage(message); 62 | setTimeout(() => { 63 | this.scrollBottom(); 64 | }); 65 | 66 | // Only propagate message to the server if the message is from the user 67 | if (triggerSearch) { 68 | this.loaderStatus = true; 69 | return this.http.post<{message: string, results: Array}>('/search', { 70 | message: content, 71 | }) 72 | .subscribe((data) => { 73 | this.loaderStatus = false; 74 | this.pushMessage({ 75 | content: data.message, 76 | timestamp: new Date(), 77 | state: 'gemini', 78 | audioSynthesis: message.delayAudioSynthesis, 79 | }); 80 | if (data.results && data.results.length > 0) { 81 | data.results.forEach((result) => { 82 | if (result.type?.stringValue === 'phone') { 83 | this.pushMessage({ 84 | content: result as PhoneResult, 85 | timestamp: new Date(), 86 | state: 'gemini-result-phone', 87 | }); 88 | } else { 89 | this.pushMessage({ 90 | content: result as MerchResult, 91 | timestamp: new Date(), 92 | state: 'gemini-result-merch', 93 | }); 94 | } 95 | }); 96 | } 97 | 98 | setTimeout(() => { 99 | this.scrollBottom(); 100 | }); 101 | }); 102 | } 103 | 104 | return null; 105 | } 106 | 107 | pushMessage(message: Message): void { 108 | this.messages.push(message); 109 | 110 | // Speak only when triggered 111 | if (message.audioSynthesis) { 112 | this.speechSynthesis({ 113 | text: message.content as string, 114 | language: message?.audio?.language || 'en-GB', 115 | voice: message?.audio?.voice || 'Daniel (English (United Kingdom))', 116 | }); 117 | } 118 | } 119 | 120 | getSpeechSynthesisUtterance({ 121 | text, 122 | language, 123 | voice, 124 | }: SpeechSynthesisUtteranceProps): SpeechSynthesisUtterance { 125 | const speechDiv = document.createElement('div'); 126 | speechDiv.innerHTML = text; 127 | const utterance = new SpeechSynthesisUtterance(speechDiv.innerText); 128 | 129 | const voices = SpeechSynthesisApi.getVoices(); 130 | const selectedVoice = voices.find((currentVoice) => { 131 | return currentVoice.lang === language && currentVoice.name === voice; 132 | }); 133 | 134 | if (selectedVoice) { 135 | utterance.voice = selectedVoice; 136 | } 137 | 138 | return utterance; 139 | } 140 | 141 | speechSynthesis(props: SpeechSynthesisUtteranceProps) { 142 | const utterance = this.getSpeechSynthesisUtterance(props); 143 | SpeechSynthesisApi.speak(utterance); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Vertex AI Search Galore 3 | 4 | Hi! I wrote this project as a way to test out 3 tools in Google Cloud: 5 | - [Vertex AI Agent Builder](https://console.cloud.google.com/gen-app-builder) (for using Vertex AI Search) 6 | - [Vertex AI Studio](https://console.cloud.google.com/vertex-ai) (for using Vertex AI Studio and SDK in code) 7 | - [Reasoning Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/reasoning-engine) (managed runtime for orchestration frameworks such as LangChain) 8 | 9 | The project is inspired from [Google's own Vertex AI Agent Builder & Flutter Demo](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/sample-apps/photo-discovery/README.md) 10 | 11 | # Structure 12 | 13 | This application can be split in 3 portions 14 | 15 | 1. [Python Setup](./python-setup/) - used for setting up Reasoning Engine instance in Google Cloud (you can only deploy from code currently) 16 | - This instance can also interact with your python Cloud Run instance to retrieve results from Vertex AI Search, performing both the Retrieval from the tool and Generation from the Gemini model used to complete the RAG requirement here. 17 | 2. [Python Serverless](./python-serverless/) - used to debug and deploy a serverless Cloud Run application that uses the python Vertex AI SDK 18 | - This Cloud Run instance runs a single route - `GET /search.json` with query parameters `query`, `page` and `page_size` to perform the retrieval function from Vertex AI Search using the Discovery Engine API 19 | 3. [Node Serverless](./node-serverless/) - used to debug and deploy a serverlsss Cloud Run application that uses the node Vertex AI SDK 20 | - This Cloud Run instance runs an Angular application on the root path, as well as a `POST /search` route to chat with Gemini, and retrieve results from Vertex AI Search using the Discovery Engine API 21 | 22 | # Setup 23 | 24 | Firstly, setup Vertex AI Search from [here](https://console.cloud.google.com/gen-app-builder). 25 | 26 | When creating the search type app, you can use the datastores listed in [the stock CSV files from this repository](./vertex-ai-datastore-files/) 27 | 28 | Once done, run the following command to create the required `.env` and `env.js` files needed for the python and node server instances 29 | ``` 30 | make first-time-setup 31 | ``` 32 | 33 | # Quick start commands 34 | To start the node and python servers, simply run 35 | ``` 36 | docker-compose up 37 | ``` 38 | 39 | Check out the [docker-compose configuration](./docker-compose.yml) to understand the wiring for each docker container. Essentially: 40 | - [localhost:8080](http://localhost:8080) points to the Node server 41 | - [localhost:4200](http://localhost:4200) points to the Angular client 42 | - [localhost:5000](http://localhost:5000) points to the Python server 43 | - "python-setup" is running in a container, but no port servicing (you need to use the following commands in the documentation to interact with Reasoning Engine) 44 | 45 | ## Debugging your local containers - shelling in 46 | 47 | Use any of the commands below to shell into your running instances (from `docker-compose up`) 48 | ``` 49 | make bash_python_setup 50 | make bash_node_server 51 | make bash_node_client 52 | make bash_python_server 53 | make node_client_build 54 | ``` 55 | 56 | ## Deploying and Interacting with Reasoning Engine 57 | 58 | Once you have run `docker-compose up`, you can interact with a local instance of Reasoning Engine, as well as deploy a managed runtime in Google Cloud (so you don't have to manage it yourself!) 59 | 60 | To test a query with a local instance of Reasoning Engine 61 | ``` 62 | make run_python_setup_command command=test 63 | ``` 64 | 65 | To deploy the existing `python-setup` code to Reasoning Engine's managed runtime as a remote agent 66 | ``` 67 | make run_python_setup_command command=deploy 68 | ``` 69 | 70 | To list Reasoning Engine's managed runtime instances in your project 71 | ``` 72 | make run_python_setup_command command=list 73 | ``` 74 | 75 | To delete Reasoning Engine's managed runtime instances (get the resource id from the list command above) 76 | ``` 77 | make run_python_setup_command command=delete resource_id=12345 78 | ``` 79 | 80 | ## Deploying with Cloud Run 81 | 82 | Before deploying the `node-serverless` code, we need to build the docker image for it, with the following command. Note that this bundles both the frontend Angular code and backend Node server into the same image 83 | ``` 84 | make docker_build_node_serverless 85 | ``` 86 | 87 | As for `python-serverless` backend Python server, the following command will work to build the docker image 88 | ``` 89 | make docker_build_python_serverless 90 | ``` 91 | 92 | Once the docker images are ready, check out the [Cloud Run documentation](https://cloud.google.com/run/docs/deploying) on pushing your images up and deploying it! 93 | 94 | # End 95 | 96 | Have fun with playing with these Vertex AI tools in Google Cloud 97 | 98 | ### Disclaimer 99 | Google Cloud credits are provided for this project #VertexAISprint 100 | -------------------------------------------------------------------------------- /node-serverless/src/app/search/search.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 | 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | 61 |
62 | 65 | 66 |
67 | 68 | 69 | 70 |
71 | Avatar 72 |
73 | Gemini 74 | Ask me anything 75 |
76 | 77 |

78 |

79 |
80 |
81 | 82 |
83 |

About {{ resultCount }} results ({{ secondsTaken }} seconds)

84 |

No results found ({{ secondsTaken }} seconds)

85 | 86 |
87 |
88 |

{{ getMerchContent(result).name.stringValue }}

89 | {{ getMerchContent(result).link.stringValue }} 90 |

{{ getMerchContent(result).gms_desc.stringValue }}

91 |

Price: {{ getMerchContent(result).price.stringValue }}

92 |
93 | 94 |
95 |

{{ getPhoneContent(result).name.stringValue }}

96 | {{ getPhoneContent(result).link.stringValue }} 97 |

Price: {{ getMerchContent(result).price.stringValue }}

98 |
99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /node-serverless/src/app/search/search.component.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #FCFCFC; 3 | overflow: hidden; 4 | } 5 | 6 | mat-card { 7 | max-width: 800px; 8 | margin-left: 190px; 9 | margin-top: 30px; 10 | } 11 | 12 | div.search-page { 13 | .logo{ 14 | margin-top:250px; 15 | margin-bottom:20px; 16 | text-align: center; 17 | width: 100%; 18 | 19 | img { 20 | width: 400px; 21 | } 22 | } 23 | .bar{ 24 | margin:0 auto; 25 | width:535px; 26 | border-radius:30px; 27 | padding: 0 20px; 28 | border:1px solid #dcdcdc; 29 | } 30 | .bar:hover{ 31 | box-shadow: 1px 1px 8px 1px #dcdcdc; 32 | } 33 | .bar:focus-within{ 34 | box-shadow: 1px 1px 8px 1px #dcdcdc; 35 | outline:none; 36 | } 37 | .searchbar{ 38 | height:45px; 39 | border:none; 40 | width:500px; 41 | font-size:16px; 42 | outline: none; 43 | } 44 | .voice{ 45 | height:20px; 46 | position:relative; 47 | top:5px; 48 | left:10px; 49 | } 50 | .buttons{ 51 | margin-top:30px; 52 | text-align: center; 53 | } 54 | .button{ 55 | background-color: #f5f5f5; 56 | border:none; 57 | color:#707070; 58 | font-size:15px; 59 | padding: 10px 20px; 60 | margin:5px; 61 | border-radius:4px; 62 | outline:none; 63 | } 64 | .button:hover{ 65 | border: 1px solid #c8c8c8; 66 | padding: 9px 19px; 67 | color:#808080; 68 | } 69 | .button:focus{ 70 | border:1px solid #4885ed; 71 | padding: 9px 19px; 72 | } 73 | } 74 | 75 | 76 | div.search-results { 77 | #topbar { 78 | display: flex; 79 | height: 64px; 80 | width: 100%; 81 | align-items: flex-end; 82 | position: fixed; 83 | background-color: white; 84 | } 85 | 86 | #searchbarimage { 87 | height: 30px; 88 | padding: 0px 28px 10px 30px; 89 | cursor: pointer; 90 | } 91 | 92 | html, body { 93 | margin: 0; 94 | padding: 0; 95 | height: auto; 96 | } 97 | 98 | #searchbarmic { 99 | height: 45px; 100 | width: 35px; 101 | margin-left: auto; 102 | background-color: white; 103 | } 104 | 105 | #searchbarmic img { 106 | height: 25px; 107 | } 108 | 109 | #searchbarbutton { 110 | height: 45px; 111 | width: 55px; 112 | position: relative; 113 | margin-right: -2px; 114 | background-color: white; 115 | } 116 | 117 | #searchbarbutton svg { 118 | height: 25px; 119 | width: 35px; 120 | position: relative; 121 | left: -3px; 122 | } 123 | 124 | #searchbarbutton svg path { 125 | fill: #4285F4 126 | } 127 | 128 | #searchbar { 129 | width: 625px; 130 | height: 45px; 131 | border-radius: 100px; 132 | border-color: lightgray; 133 | border-style: solid; 134 | border-width: 1px; 135 | font-size: 16px; 136 | position: relative; 137 | bottom: 5px; 138 | overflow: hidden; 139 | display: flex; 140 | z-index: 100; 141 | } 142 | 143 | #searchbar:hover { 144 | box-shadow: 0px 2px 5px rgba(0,0,0,0.1); 145 | } 146 | 147 | #searchbar > input { 148 | height: 45px; 149 | border-style: none; 150 | font-size: 16px; 151 | line-height: 45px; 152 | padding-left: 20px; 153 | flex: 1; 154 | } 155 | 156 | #searchbar > input:focus { 157 | outline: none; 158 | } 159 | 160 | #searchbar button { 161 | border: none; 162 | cursor: pointer; 163 | } 164 | 165 | #searchbar button:focus { 166 | outline: none; 167 | } 168 | 169 | #optionsmenuactive::after { 170 | content: ''; 171 | display: block; 172 | position: absolute; 173 | width: 100%; 174 | height: 3px; 175 | background-color: #4285F4; 176 | left: 5px; 177 | bottom: 0px; 178 | } 179 | 180 | #optionsmenu2 { 181 | list-style: none; 182 | display: flex; 183 | flex-direction: row; 184 | padding: 0px; 185 | margin: 0px; 186 | margin-left: 100px; 187 | } 188 | 189 | #optionsmenu2 li { 190 | padding: 0px 10px 15px 20px; 191 | } 192 | 193 | #searchresultsarea { 194 | margin-left: 190px; 195 | font-family: 'Arial'; 196 | } 197 | 198 | #searchresultsnumber { 199 | font-size: 0.8rem; 200 | color: gray; 201 | } 202 | 203 | .searchresult { 204 | margin-left: 8px; 205 | } 206 | 207 | .searchresult h2 { 208 | font-size: 19px; 209 | line-height: 18px; 210 | font-weight: normal; 211 | color: rgb(29, 1, 189); 212 | margin-bottom: 0px; 213 | margin-top: 25px; 214 | } 215 | 216 | .searchresult a { 217 | font-size: 14px; 218 | line-height: 14px; 219 | color: green; 220 | margin-bottom: 0px; 221 | } 222 | 223 | .searchresult button { 224 | font-size: 10px; 225 | line-height: 14px; 226 | color: green; 227 | margin-bottom: 0px; 228 | padding: 0px; 229 | border-width: 0px; 230 | background-color: white; 231 | } 232 | 233 | #optionsbar { 234 | width: 100%; 235 | height: 50px; 236 | border-width: 0px; 237 | border-bottom: 1px; 238 | border-color: lightgray; 239 | border-style: solid; 240 | display: flex; 241 | align-items: flex-end; 242 | font-family: 'Arial'; 243 | font-size: 13px; 244 | color: rgb(112, 112, 112); 245 | padding-top: 64px; 246 | } 247 | 248 | #optionsmenu1 { 249 | list-style: none; 250 | display: flex; 251 | flex-direction: row; 252 | padding: 0px; 253 | margin: 0px; 254 | margin-left: 190px; 255 | } 256 | 257 | #optionsmenu1 li { 258 | padding: 0px 10px 15px 20px; 259 | } 260 | 261 | #optionsmenuactive { 262 | color: #4285F4; 263 | font-weight: bold; 264 | position: relative; 265 | z-index: -1; 266 | } 267 | 268 | .searchresult p { 269 | width: 625px; 270 | font-size: 13px; 271 | margin-top: 0px; 272 | color: rgb(82, 82, 82); 273 | } 274 | } 275 | 276 | ul.header-menu { 277 | list-style-type: none; 278 | overflow: hidden; 279 | right: 10px; 280 | top: 0px; 281 | position: fixed; 282 | margin: 20px 0; 283 | 284 | li { 285 | float: right; 286 | } 287 | 288 | li a { 289 | color: #000; 290 | display: block; 291 | text-align: center; 292 | padding: 10px 10px; 293 | text-decoration: none; 294 | font-size:14px; 295 | } 296 | li a:hover { 297 | text-decoration: underline; 298 | } 299 | 300 | .grid{ 301 | height:23px; 302 | position:relative; 303 | bottom:4px; 304 | } 305 | 306 | .signbutton{ 307 | background-color: #4885ed; 308 | color: #fff; 309 | border:none; 310 | border-radius:3px; 311 | padding:7px 10px; 312 | position:relative; 313 | bottom:7px; 314 | font-weight:bold; 315 | } 316 | } -------------------------------------------------------------------------------- /node-serverless/src/app/message-input/message-input.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, EventEmitter, NgZone, Output } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { Message } from '../../types/message'; 4 | 5 | let SpeechRecognitionApi: { 6 | new (): SpeechRecognition; 7 | prototype: SpeechRecognition; 8 | }; 9 | 10 | if (typeof window !== "undefined") { 11 | SpeechRecognitionApi = (window.SpeechRecognition || window.webkitSpeechRecognition); 12 | } 13 | 14 | @Component({ 15 | selector: 'app-message-input', 16 | standalone: true, 17 | imports: [ 18 | FormsModule 19 | ], 20 | templateUrl: './message-input.component.html', 21 | styleUrl: './message-input.component.scss' 22 | }) 23 | export class MessageInputComponent { 24 | @Output() sendMessageEvent = new EventEmitter(); 25 | 26 | constructor(private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone) {} 27 | 28 | public messageText: string = ''; 29 | 30 | onKeyup(event: KeyboardEvent): void { 31 | if (event.key === 'Enter') { 32 | event.preventDefault(); 33 | this.submitMessage(); 34 | } 35 | } 36 | 37 | submitMessage(): void { 38 | if (!this.messageText || this.messageText === '') { 39 | return; 40 | } 41 | 42 | this.sendMessageEvent.emit({ 43 | content: this.messageText, 44 | timestamp: new Date(), 45 | state: 'me', 46 | triggerSearch: true, 47 | }); 48 | this.messageText = ''; 49 | } 50 | 51 | formatString(input: string): string { 52 | const trimmed = input.trim(); 53 | if (trimmed.length === 0) { 54 | return trimmed; 55 | } 56 | return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); 57 | } 58 | 59 | submitAudioOption(): void { 60 | const recognition = new SpeechRecognitionApi(); 61 | recognition.continuous = false; 62 | recognition.interimResults = false; 63 | recognition.maxAlternatives = 1; 64 | 65 | recognition.onresult = (event: SpeechRecognitionEvent) => { 66 | const lastIndex = event.results.length - 1; 67 | const { transcript: result } = event.results[lastIndex][0]; 68 | const formattedResult = this.formatString(result); 69 | 70 | this.ngZone.run(() => { 71 | // Easter egg for handsome 72 | if (this.isHandsome(formattedResult)){ 73 | this.sendMessageEvent.emit({ 74 | content: formattedResult, 75 | timestamp: new Date(), 76 | state: 'me', 77 | }); 78 | 79 | this.sendMessageEvent.emit({ 80 | content: `Yes sir, you are very handsome. In fact, some uncles and aunties might even say`, 81 | timestamp: new Date(), 82 | state: 'gemini', 83 | audioSynthesis: true, 84 | }); 85 | 86 | // Why is this combination sound so sarcastic, lol 87 | // this.sendMessageEvent.emit({ 88 | // content: `Wow, you so "yan dao" 🙄`, 89 | // timestamp: new Date(), 90 | // state: 'gemini', 91 | // audioSynthesis: true, 92 | // audio: { 93 | // language: 'zh-HK', 94 | // voice: 'Google 粤語(香港)', 95 | // } 96 | // }); 97 | 98 | // Cantonese only 99 | this.sendMessageEvent.emit({ 100 | content: `靚仔`, 101 | timestamp: new Date(), 102 | state: 'gemini', 103 | audioSynthesis: true, 104 | audio: { 105 | language: 'zh-HK', 106 | voice: 'Sinji', 107 | } 108 | }); 109 | 110 | // EN only 111 | // this.sendMessageEvent.emit({ 112 | // content: `Handsome`, 113 | // timestamp: new Date(), 114 | // state: 'gemini', 115 | // audioSynthesis: true, 116 | // audio: { 117 | // language: 'en-GB', 118 | // voice: 'Rocko (English (United Kingdom))', 119 | // } 120 | // }); 121 | 122 | // // PH only 123 | // this.sendMessageEvent.emit({ 124 | // content: `matikas`, 125 | // timestamp: new Date(), 126 | // state: 'gemini', 127 | // audioSynthesis: true, 128 | // audio: { 129 | // language: 'id-ID', 130 | // voice: 'Damayanti', 131 | // } 132 | // }); 133 | 134 | // // ID only 135 | // this.sendMessageEvent.emit({ 136 | // content: `cakep`, 137 | // timestamp: new Date(), 138 | // state: 'gemini', 139 | // audioSynthesis: true, 140 | // audio: { 141 | // language: 'id-ID', 142 | // voice: 'Damayanti', 143 | // } 144 | // }); 145 | 146 | // Thai only 147 | // this.sendMessageEvent.emit({ 148 | // content: `หน้าตาดี`, 149 | // timestamp: new Date(), 150 | // state: 'gemini', 151 | // audioSynthesis: true, 152 | // audio: { 153 | // language: 'th-TH', 154 | // voice: 'Kanya', 155 | // } 156 | // }); 157 | 158 | // Vietnamese only 159 | // this.sendMessageEvent.emit({ 160 | // content: `đẹp trai`, 161 | // timestamp: new Date(), 162 | // state: 'gemini', 163 | // audioSynthesis: true, 164 | // audio: { 165 | // language: 'vi-VN', 166 | // voice: 'Linh', 167 | // } 168 | // }); 169 | 170 | return; 171 | } 172 | 173 | if (this.isDoubtful(formattedResult)) { 174 | this.sendMessageEvent.emit({ 175 | content: formattedResult, 176 | timestamp: new Date(), 177 | state: 'me', 178 | }); 179 | 180 | this.sendMessageEvent.emit({ 181 | content: `Why you buay paiseh`, 182 | timestamp: new Date(), 183 | state: 'gemini', 184 | audioSynthesis: true, 185 | audio: { 186 | language: 'zh-HK', 187 | voice: 'Google 粤語(香港)', 188 | } 189 | }); 190 | return; 191 | } 192 | 193 | // Hello there 194 | if (this.isHello(formattedResult)){ 195 | this.sendMessageEvent.emit({ 196 | content: formattedResult, 197 | timestamp: new Date(), 198 | state: 'me', 199 | }); 200 | 201 | this.sendMessageEvent.emit({ 202 | content: `Yes sir, how can I help you?`, 203 | timestamp: new Date(), 204 | state: 'gemini', 205 | audioSynthesis: true, 206 | }); 207 | 208 | return; 209 | } 210 | 211 | this.sendMessageEvent.emit({ 212 | content: formattedResult, 213 | timestamp: new Date(), 214 | state: 'me', 215 | triggerSearch: true, 216 | delayAudioSynthesis: true, 217 | }); 218 | 219 | this.sendMessageEvent.emit({ 220 | content: `Yes sir, I will search for "${formattedResult}"`, 221 | timestamp: new Date(), 222 | state: 'gemini', 223 | audioSynthesis: true, 224 | }); 225 | 226 | // Force Angular to perform a full rerender 227 | this.changeDetectorRef.markForCheck(); 228 | this.changeDetectorRef.detectChanges(); 229 | }); 230 | }; 231 | 232 | recognition.start(); 233 | } 234 | 235 | isHandsome(text: string): boolean { 236 | return text.toLowerCase().includes('handsome'); 237 | } 238 | 239 | isDoubtful(text: string): boolean { 240 | return text.toLowerCase().includes('what do you mean'); 241 | } 242 | 243 | isHello(text: string): boolean { 244 | return text.replace(/\n\s*$/m, '').toLowerCase() === 'hello'; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /node-serverless/server/server.js: -------------------------------------------------------------------------------- 1 | const {VertexAI} = require('@google-cloud/vertexai'); 2 | const env = require('./config/env'); 3 | const path = require('path'); 4 | const marked = require('marked'); 5 | const cors = require('cors'); 6 | 7 | // Init express for this node server 8 | const express = require('express'); 9 | const app = express(); 10 | app.use(express.json()); 11 | // Enable origin for angular in local environment 12 | if (process.env.NODE_ENV === 'development') { 13 | app.use(cors({ origin: 'http://localhost:4200' })); 14 | } 15 | 16 | // HTML Content 17 | app.use(express.static(path.join(__dirname, '../dist/chat-with-gemini/browser'))); 18 | 19 | app.get('*', (_, res) => { 20 | res.sendFile(path.join(__dirname, '../dist/chat-with-gemini/browser/index.html')); 21 | }); 22 | 23 | const port = env.PORT; 24 | app.listen(port, () => { 25 | console.log(`Server listening at http://localhost:${port}`); 26 | }); 27 | 28 | /***************************************************************************** 29 | * All Vertex AI related code will be added below this comment block * 30 | *****************************************************************************/ 31 | 32 | const { 33 | googleMerchStoreSearchDeclaration, 34 | googleMerchStoreSearch, 35 | } = require('./gemini_functions/vertexai_google_merch_store_search'); 36 | const { 37 | getExchangeRateFunctionDeclaration, 38 | getExchangeRate, 39 | } = require('./gemini_functions/exchange_rate'); 40 | const { 41 | getWikipediaContentFunctionDeclaration, 42 | getWikipediaContent, 43 | } = require('./gemini_functions/wikipedia'); 44 | 45 | 46 | const INVALID_RESULTS_MESSAGE = 'No results were found'; 47 | 48 | app.post('/search', async (req, res) => { 49 | try { 50 | const query = req.body.message; 51 | if (!query || query === '') { 52 | return res.status(400).json({ error: 'Missing message parameter' }); 53 | } 54 | 55 | // // Main post request to the search endpoint ******************************* 56 | const { message, results } = await searchWithNode(query); 57 | const strippedMessage = message.replace(/\n\s*$/m, ''); 58 | res.status(200).send({ 59 | message: strippedMessage, 60 | results: results, 61 | }); 62 | //************************************************************************* 63 | 64 | } catch (e) { 65 | console.error(e); 66 | res.status(500).json({ error: 'Internal Server Error'}); 67 | } 68 | }); 69 | 70 | // Initialize Vertex with your Cloud project and location 71 | const vertex_ai = new VertexAI({ 72 | project: env.GCP_PROJECT_ID, 73 | location: env.GCP_REGION 74 | }); 75 | 76 | const searchEngineModel = vertex_ai.getGenerativeModel({ 77 | model: env.GCP_GEMINI_VER, 78 | systemInstruction: { 79 | role: 'system', 80 | parts: [{ 81 | 'text': ` 82 | You are a search engine. 83 | 84 | Please return the most relevant search results for the given query. 85 | If the user tries to chat with you, return the message '${INVALID_RESULTS_MESSAGE}'. 86 | `, 87 | }] 88 | }, 89 | generationConfig: { 90 | 'maxOutputTokens': 2048, 91 | 'temperature': 1, 92 | 'topP': 0.95, 93 | }, 94 | safetySettings: [ 95 | { 96 | 'category': 'HARM_CATEGORY_HATE_SPEECH', 97 | 'threshold': 'BLOCK_NONE' 98 | }, 99 | { 100 | 'category': 'HARM_CATEGORY_DANGEROUS_CONTENT', 101 | 'threshold': 'BLOCK_NONE' 102 | }, 103 | { 104 | 'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 105 | 'threshold': 'BLOCK_NONE' 106 | }, 107 | { 108 | 'category': 'HARM_CATEGORY_HARASSMENT', 109 | 'threshold': 'BLOCK_NONE' 110 | } 111 | ], 112 | }); 113 | 114 | // Implemented search logic with Gemini model 115 | const searchTools = { 116 | function_declarations: [ 117 | googleMerchStoreSearchDeclaration, 118 | getExchangeRateFunctionDeclaration, 119 | getWikipediaContentFunctionDeclaration, 120 | ], 121 | }; 122 | 123 | async function searchWithNode(query) { 124 | // 1. Initial search logic 125 | const request = { 126 | contents: [{ 127 | role: 'user', 128 | parts: [{ 129 | text: query, 130 | }] 131 | }], 132 | tools: [ 133 | searchTools, 134 | ], 135 | }; 136 | 137 | // 2. Extracting the search results 138 | const { output, searchResults } = await searchEngineGenerateContent({ 139 | request, 140 | firstCall: true, 141 | }); 142 | 143 | // 3. Return the search results 144 | const markedOutput = marked.parse(output); 145 | return { 146 | message: markedOutput, 147 | results: searchResults, 148 | } 149 | } 150 | 151 | function isFunctionCall(candidates) { 152 | return candidates?.length > 0 && 153 | candidates[0]?.content?.parts?.length > 0 && 154 | !!(candidates[0].content.parts[0]?.functionCall?.name); 155 | } 156 | 157 | async function functionCallLogic(priorRequest, candidates) { 158 | let output = INVALID_RESULTS_MESSAGE, searchResults = []; 159 | let request = priorRequest; 160 | 161 | // Single function calling 162 | if (candidates.length === 1) { 163 | const functionCallObj = candidates[0].content.parts[0].functionCall; 164 | const functionCallName = functionCallObj.name; 165 | const functionCallArgs = functionCallObj.args; 166 | request.contents.push({ 167 | role: 'model', 168 | parts: [{ functionCall: functionCallObj }], 169 | }); 170 | 171 | switch (functionCallName) { 172 | case googleMerchStoreSearchDeclaration.name: 173 | searchResults = await googleMerchStoreSearch(functionCallArgs.productName, functionCallArgs.productDescription); 174 | output = `Found ${searchResults.length} results`; 175 | break; 176 | 177 | case getExchangeRateFunctionDeclaration.name: 178 | const exchangeRateData = await getExchangeRate(functionCallArgs.currency_from, functionCallArgs.currency_to); 179 | if (!exchangeRateData) { 180 | break; 181 | } 182 | 183 | request.contents.push({ 184 | role: 'user', 185 | parts: [ 186 | { 187 | functionResponse: { 188 | name: getExchangeRateFunctionDeclaration.name, 189 | response: {name: getExchangeRateFunctionDeclaration.name, content: exchangeRateData}, 190 | }, 191 | }, 192 | ] 193 | }); 194 | ({ output, searchResults } = await searchEngineGenerateContent({ request, firstCall: false })); 195 | break; 196 | 197 | case getWikipediaContentFunctionDeclaration.name: 198 | const wikiContent = await getWikipediaContent(functionCallArgs.subject); 199 | if (!wikiContent) { 200 | break; 201 | } 202 | 203 | request.contents.push({ 204 | role: 'user', 205 | parts: [ 206 | { 207 | functionResponse: { 208 | name: getWikipediaContentFunctionDeclaration.name, 209 | response: {name: getWikipediaContentFunctionDeclaration.name, content: wikiContent}, 210 | }, 211 | }, 212 | ] 213 | }); 214 | ({ output, searchResults } = await searchEngineGenerateContent({ request, firstCall: false })); 215 | 216 | 217 | default: 218 | break; 219 | } 220 | 221 | // Disabling parallel function calling for now 222 | } else if (candidates.length >= 2) { 223 | searchResults = []; 224 | output = `Could you ask a more specific question?`; 225 | } 226 | 227 | return { 228 | output, 229 | searchResults, 230 | } 231 | } 232 | 233 | async function searchEngineGenerateContent({ request, firstCall = false }) { 234 | const modelContent = await searchEngineModel.generateContent(request); 235 | const response = modelContent.response; 236 | let output = INVALID_RESULTS_MESSAGE, searchResults = []; 237 | 238 | // Only allow function call for first call - no recursive function calls 239 | if (firstCall && isFunctionCall(response.candidates)){ 240 | ({ output, searchResults } = await functionCallLogic(request, response.candidates)); 241 | } else { 242 | output = response?.candidates[0]?.content?.parts[0]?.text || INVALID_RESULTS_MESSAGE; 243 | } 244 | 245 | return { 246 | output, 247 | searchResults, 248 | }; 249 | } 250 | -------------------------------------------------------------------------------- /vertex-ai-datastore-files/phone_shop_items.json: -------------------------------------------------------------------------------- 1 | {"id": "Pixel_Phone_6","link": "https://shop.merch.google/product/pixel-phone-6/","name": "Google Pixel Phone 6","description": "The Google Pixel Phone 6 features a stunning display, powerful processor, and the latest Android OS. Perfect for all your mobile needs.","price": 699.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_6.jpg","mpn": "PP6","type": "phone"} 2 | {"id": "Pixel_Phone_6_Pro","link": "https://shop.merch.google/product/pixel-phone-6-pro/","name": "Google Pixel Phone 6 Pro","description": "Upgrade to the Google Pixel Phone 6 Pro for enhanced performance and a superior camera experience. Capture every moment in stunning detail.","price": 899.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_6_pro.jpg","mpn": "PP6PRO","type": "phone"} 3 | {"id": "Pixel_Phone_5","link": "https://shop.merch.google/product/pixel-phone-5/","name": "Google Pixel Phone 5","description": "Experience the future of mobile technology with the Google Pixel Phone 5. Featuring a sleek design and cutting-edge features.","price": 599.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_5.jpg","mpn": "PP5","type": "phone"} 4 | {"id": "Pixel_Phone_5a","link": "https://shop.merch.google/product/pixel-phone-5a/","name": "Google Pixel Phone 5a","description": "The Google Pixel Phone 5a offers a large display, long battery life, and powerful performance for all your entertainment needs.","price": 499.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_5a.jpg","mpn": "PP5A","type": "phone"} 5 | {"id": "Pixel_Phone_4","link": "https://shop.merch.google/product/pixel-phone-4/","name": "Google Pixel Phone 4","description": "Compact and powerful, the Google Pixel Phone 4 is perfect for those who want a smaller device without compromising on features.","price": 399.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_4.jpg","mpn": "PP4","type": "phone"} 6 | {"id": "Pixel_Phone_4_XL","link": "https://shop.merch.google/product/pixel-phone-4-xl/","name": "Google Pixel Phone 4 XL","description": "The Google Pixel Phone 4 XL is designed for professionals. With advanced security features and top-notch performance, it's the ultimate business phone.","price": 699.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_4_xl.jpg","mpn": "PP4XL","type": "phone"} 7 | {"id": "Pixel_Phone_3","link": "https://shop.merch.google/product/pixel-phone-3/","name": "Google Pixel Phone 3","description": "Affordable and reliable, the Google Pixel Phone 3 offers great performance and essential features at a budget-friendly price.","price": 299.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_3.jpg","mpn": "PP3","type": "phone"} 8 | {"id": "Pixel_Phone_3a","link": "https://shop.merch.google/product/pixel-phone-3a/","name": "Google Pixel Phone 3a","description": "The Google Pixel Phone 3a is the pinnacle of mobile technology, featuring a stunning display, incredible camera, and unmatched performance.","price": 349.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_3a.jpg","mpn": "PP3A","type": "phone"} 9 | {"id": "Pixel_Phone_2","link": "https://shop.merch.google/product/pixel-phone-2/","name": "Google Pixel Phone 2","description": "Built for the active lifestyle, the Google Pixel Phone 2 is rugged, water-resistant, and packed with features to keep you connected on the go.","price": 199.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_2.jpg","mpn": "PP2","type": "phone"} 10 | {"id": "Pixel_Phone_2_XL","link": "https://shop.merch.google/product/pixel-phone-2-xl/","name": "Google Pixel Phone 2 XL","description": "Eco-friendly and powerful, the Google Pixel Phone 2 XL is made from recycled materials and offers top-notch performance with a minimal environmental impact.","price": 249.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/pixel_phone_2_xl.jpg","mpn": "PP2XL","type": "phone"} 11 | {"id": "Android_Phone_X1","link": "https://shop.merch.google/product/android-phone-x1/","name": "Android Phone X1","description": "The Android Phone X1 features a sleek design, powerful processor, and a stunning display. Perfect for all your mobile needs.","price": 699.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_x1.jpg","mpn": "APX1","type": "phone"} 12 | {"id": "Android_Phone_X2","link": "https://shop.merch.google/product/android-phone-x2/","name": "Android Phone X2","description": "Upgrade to the Android Phone X2 for enhanced performance and a superior camera experience. Capture every moment in stunning detail.","price": 799.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_x2.jpg","mpn": "APX2","type": "phone"} 13 | {"id": "Android_Phone_X3","link": "https://shop.merch.google/product/android-phone-x3/","name": "Android Phone X3","description": "Experience the future of mobile technology with the Android Phone X3. Featuring a foldable display and cutting-edge features.","price": 999.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_x3.jpg","mpn": "APX3","type": "phone"} 14 | {"id": "Android_Phone_XL","link": "https://shop.merch.google/product/android-phone-xl/","name": "Android Phone XL","description": "The Android Phone XL offers a large display, long battery life, and powerful performance for all your entertainment needs.","price": 899.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_xl.jpg","mpn": "APXL","type": "phone"} 15 | {"id": "Android_Phone_Mini","link": "https://shop.merch.google/product/android-phone-mini/","name": "Android Phone Mini","description": "Compact and powerful, the Android Phone Mini is perfect for those who want a smaller device without compromising on features.","price": 599.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_mini.jpg","mpn": "APMINI","type": "phone"} 16 | {"id": "Android_Phone_Pro","link": "https://shop.merch.google/product/android-phone-pro/","name": "Android Phone Pro","description": "The Android Phone Pro is designed for professionals. With advanced security features and top-notch performance, it's the ultimate business phone.","price": 1099.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_pro.jpg","mpn": "APPRO","type": "phone"} 17 | {"id": "Android_Phone_Lite","link": "https://shop.merch.google/product/android-phone-lite/","name": "Android Phone Lite","description": "Affordable and reliable, the Android Phone Lite offers great performance and essential features at a budget-friendly price.","price": 399.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_lite.jpg","mpn": "APLITE","type": "phone"} 18 | {"id": "Android_Phone_Ultra","link": "https://shop.merch.google/product/android-phone-ultra/","name": "Android Phone Ultra","description": "The Android Phone Ultra is the pinnacle of mobile technology, featuring a stunning display, incredible camera, and unmatched performance.","price": 1299.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_ultra.jpg","mpn": "APULTRA","type": "phone"} 19 | {"id": "Android_Phone_Sport","link": "https://shop.merch.google/product/android-phone-sport/","name": "Android Phone Sport","description": "Built for the active lifestyle, the Android Phone Sport is rugged, water-resistant, and packed with features to keep you connected on the go.","price": 749.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_sport.jpg","mpn": "APSPORT","type": "phone"} 20 | {"id": "Android_Phone_Eco","link": "https://shop.merch.google/product/android-phone-eco/","name": "Android Phone Eco","description": "Eco-friendly and powerful, the Android Phone Eco is made from recycled materials and offers top-notch performance with a minimal environmental impact.","price": 649.00,"currency": "USD","availability": "IN_STOCK","condition": "NEW","image_link": "https://example.com/images/android_phone_eco.jpg","mpn": "APECO","type": "phone"} 21 | -------------------------------------------------------------------------------- /node-serverless/server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@google-cloud/discoveryengine": "^1.14.0", 13 | "@google-cloud/vertexai": "~1.9.0", 14 | "axios": "~1.7.7", 15 | "cors": "^2.8.5", 16 | "express": "^4.21.1", 17 | "marked": "~12.0.2" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "^3.1.7" 21 | } 22 | }, 23 | "node_modules/@google-cloud/discoveryengine": { 24 | "version": "1.14.0", 25 | "resolved": "https://registry.npmjs.org/@google-cloud/discoveryengine/-/discoveryengine-1.14.0.tgz", 26 | "integrity": "sha512-mwx/+Q3LpmyQbnIxhKNP3i8jgGyLm7NeyXJP11XwdbXOqvsOGrjsb6SOkNL2NzWVZFGtqP9K6jWz/9a0L1ZWYA==", 27 | "dependencies": { 28 | "google-gax": "^4.0.3" 29 | }, 30 | "engines": { 31 | "node": ">=14.0.0" 32 | } 33 | }, 34 | "node_modules/@google-cloud/vertexai": { 35 | "version": "1.9.0", 36 | "resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.0.tgz", 37 | "integrity": "sha512-8brlcJwFXI4fPuBtsDNQqCdWZmz8gV9jeEKOU0vc5H2SjehCQpXK/NwuSEr916zbhlBHtg/sU37qQQdgvh5BRA==", 38 | "dependencies": { 39 | "google-auth-library": "^9.1.0" 40 | }, 41 | "engines": { 42 | "node": ">=18.0.0" 43 | } 44 | }, 45 | "node_modules/@grpc/grpc-js": { 46 | "version": "1.12.2", 47 | "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", 48 | "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", 49 | "dependencies": { 50 | "@grpc/proto-loader": "^0.7.13", 51 | "@js-sdsl/ordered-map": "^4.4.2" 52 | }, 53 | "engines": { 54 | "node": ">=12.10.0" 55 | } 56 | }, 57 | "node_modules/@grpc/proto-loader": { 58 | "version": "0.7.13", 59 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", 60 | "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", 61 | "dependencies": { 62 | "lodash.camelcase": "^4.3.0", 63 | "long": "^5.0.0", 64 | "protobufjs": "^7.2.5", 65 | "yargs": "^17.7.2" 66 | }, 67 | "bin": { 68 | "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" 69 | }, 70 | "engines": { 71 | "node": ">=6" 72 | } 73 | }, 74 | "node_modules/@js-sdsl/ordered-map": { 75 | "version": "4.4.2", 76 | "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", 77 | "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", 78 | "funding": { 79 | "type": "opencollective", 80 | "url": "https://opencollective.com/js-sdsl" 81 | } 82 | }, 83 | "node_modules/@protobufjs/aspromise": { 84 | "version": "1.1.2", 85 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 86 | "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" 87 | }, 88 | "node_modules/@protobufjs/base64": { 89 | "version": "1.1.2", 90 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 91 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" 92 | }, 93 | "node_modules/@protobufjs/codegen": { 94 | "version": "2.0.4", 95 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 96 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" 97 | }, 98 | "node_modules/@protobufjs/eventemitter": { 99 | "version": "1.1.0", 100 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 101 | "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" 102 | }, 103 | "node_modules/@protobufjs/fetch": { 104 | "version": "1.1.0", 105 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 106 | "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 107 | "dependencies": { 108 | "@protobufjs/aspromise": "^1.1.1", 109 | "@protobufjs/inquire": "^1.1.0" 110 | } 111 | }, 112 | "node_modules/@protobufjs/float": { 113 | "version": "1.0.2", 114 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 115 | "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" 116 | }, 117 | "node_modules/@protobufjs/inquire": { 118 | "version": "1.1.0", 119 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 120 | "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" 121 | }, 122 | "node_modules/@protobufjs/path": { 123 | "version": "1.1.2", 124 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 125 | "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" 126 | }, 127 | "node_modules/@protobufjs/pool": { 128 | "version": "1.1.0", 129 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 130 | "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" 131 | }, 132 | "node_modules/@protobufjs/utf8": { 133 | "version": "1.1.0", 134 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 135 | "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" 136 | }, 137 | "node_modules/@tootallnate/once": { 138 | "version": "2.0.0", 139 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", 140 | "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", 141 | "engines": { 142 | "node": ">= 10" 143 | } 144 | }, 145 | "node_modules/@types/caseless": { 146 | "version": "0.12.5", 147 | "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", 148 | "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" 149 | }, 150 | "node_modules/@types/long": { 151 | "version": "4.0.2", 152 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", 153 | "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" 154 | }, 155 | "node_modules/@types/node": { 156 | "version": "22.7.7", 157 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", 158 | "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", 159 | "dependencies": { 160 | "undici-types": "~6.19.2" 161 | } 162 | }, 163 | "node_modules/@types/request": { 164 | "version": "2.48.12", 165 | "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", 166 | "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", 167 | "dependencies": { 168 | "@types/caseless": "*", 169 | "@types/node": "*", 170 | "@types/tough-cookie": "*", 171 | "form-data": "^2.5.0" 172 | } 173 | }, 174 | "node_modules/@types/request/node_modules/form-data": { 175 | "version": "2.5.2", 176 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", 177 | "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", 178 | "dependencies": { 179 | "asynckit": "^0.4.0", 180 | "combined-stream": "^1.0.6", 181 | "mime-types": "^2.1.12", 182 | "safe-buffer": "^5.2.1" 183 | }, 184 | "engines": { 185 | "node": ">= 0.12" 186 | } 187 | }, 188 | "node_modules/@types/tough-cookie": { 189 | "version": "4.0.5", 190 | "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", 191 | "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" 192 | }, 193 | "node_modules/abort-controller": { 194 | "version": "3.0.0", 195 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 196 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 197 | "dependencies": { 198 | "event-target-shim": "^5.0.0" 199 | }, 200 | "engines": { 201 | "node": ">=6.5" 202 | } 203 | }, 204 | "node_modules/accepts": { 205 | "version": "1.3.8", 206 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 207 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 208 | "dependencies": { 209 | "mime-types": "~2.1.34", 210 | "negotiator": "0.6.3" 211 | }, 212 | "engines": { 213 | "node": ">= 0.6" 214 | } 215 | }, 216 | "node_modules/agent-base": { 217 | "version": "7.1.1", 218 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", 219 | "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", 220 | "dependencies": { 221 | "debug": "^4.3.4" 222 | }, 223 | "engines": { 224 | "node": ">= 14" 225 | } 226 | }, 227 | "node_modules/agent-base/node_modules/debug": { 228 | "version": "4.3.4", 229 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 230 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 231 | "dependencies": { 232 | "ms": "2.1.2" 233 | }, 234 | "engines": { 235 | "node": ">=6.0" 236 | }, 237 | "peerDependenciesMeta": { 238 | "supports-color": { 239 | "optional": true 240 | } 241 | } 242 | }, 243 | "node_modules/agent-base/node_modules/ms": { 244 | "version": "2.1.2", 245 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 246 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 247 | }, 248 | "node_modules/ansi-regex": { 249 | "version": "5.0.1", 250 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 251 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 252 | "engines": { 253 | "node": ">=8" 254 | } 255 | }, 256 | "node_modules/ansi-styles": { 257 | "version": "4.3.0", 258 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 259 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 260 | "dependencies": { 261 | "color-convert": "^2.0.1" 262 | }, 263 | "engines": { 264 | "node": ">=8" 265 | }, 266 | "funding": { 267 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 268 | } 269 | }, 270 | "node_modules/anymatch": { 271 | "version": "3.1.3", 272 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 273 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 274 | "dev": true, 275 | "dependencies": { 276 | "normalize-path": "^3.0.0", 277 | "picomatch": "^2.0.4" 278 | }, 279 | "engines": { 280 | "node": ">= 8" 281 | } 282 | }, 283 | "node_modules/array-flatten": { 284 | "version": "1.1.1", 285 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 286 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 287 | }, 288 | "node_modules/asynckit": { 289 | "version": "0.4.0", 290 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 291 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 292 | }, 293 | "node_modules/axios": { 294 | "version": "1.7.7", 295 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", 296 | "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", 297 | "dependencies": { 298 | "follow-redirects": "^1.15.6", 299 | "form-data": "^4.0.0", 300 | "proxy-from-env": "^1.1.0" 301 | } 302 | }, 303 | "node_modules/balanced-match": { 304 | "version": "1.0.2", 305 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 306 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 307 | "dev": true 308 | }, 309 | "node_modules/base64-js": { 310 | "version": "1.5.1", 311 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 312 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 313 | "funding": [ 314 | { 315 | "type": "github", 316 | "url": "https://github.com/sponsors/feross" 317 | }, 318 | { 319 | "type": "patreon", 320 | "url": "https://www.patreon.com/feross" 321 | }, 322 | { 323 | "type": "consulting", 324 | "url": "https://feross.org/support" 325 | } 326 | ] 327 | }, 328 | "node_modules/bignumber.js": { 329 | "version": "9.1.2", 330 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", 331 | "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", 332 | "engines": { 333 | "node": "*" 334 | } 335 | }, 336 | "node_modules/binary-extensions": { 337 | "version": "2.3.0", 338 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 339 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 340 | "dev": true, 341 | "engines": { 342 | "node": ">=8" 343 | }, 344 | "funding": { 345 | "url": "https://github.com/sponsors/sindresorhus" 346 | } 347 | }, 348 | "node_modules/body-parser": { 349 | "version": "1.20.3", 350 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 351 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 352 | "dependencies": { 353 | "bytes": "3.1.2", 354 | "content-type": "~1.0.5", 355 | "debug": "2.6.9", 356 | "depd": "2.0.0", 357 | "destroy": "1.2.0", 358 | "http-errors": "2.0.0", 359 | "iconv-lite": "0.4.24", 360 | "on-finished": "2.4.1", 361 | "qs": "6.13.0", 362 | "raw-body": "2.5.2", 363 | "type-is": "~1.6.18", 364 | "unpipe": "1.0.0" 365 | }, 366 | "engines": { 367 | "node": ">= 0.8", 368 | "npm": "1.2.8000 || >= 1.4.16" 369 | } 370 | }, 371 | "node_modules/brace-expansion": { 372 | "version": "1.1.11", 373 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 374 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 375 | "dev": true, 376 | "dependencies": { 377 | "balanced-match": "^1.0.0", 378 | "concat-map": "0.0.1" 379 | } 380 | }, 381 | "node_modules/braces": { 382 | "version": "3.0.3", 383 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 384 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 385 | "dev": true, 386 | "dependencies": { 387 | "fill-range": "^7.1.1" 388 | }, 389 | "engines": { 390 | "node": ">=8" 391 | } 392 | }, 393 | "node_modules/buffer-equal-constant-time": { 394 | "version": "1.0.1", 395 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 396 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 397 | }, 398 | "node_modules/bytes": { 399 | "version": "3.1.2", 400 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 401 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 402 | "engines": { 403 | "node": ">= 0.8" 404 | } 405 | }, 406 | "node_modules/call-bind": { 407 | "version": "1.0.7", 408 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 409 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 410 | "dependencies": { 411 | "es-define-property": "^1.0.0", 412 | "es-errors": "^1.3.0", 413 | "function-bind": "^1.1.2", 414 | "get-intrinsic": "^1.2.4", 415 | "set-function-length": "^1.2.1" 416 | }, 417 | "engines": { 418 | "node": ">= 0.4" 419 | }, 420 | "funding": { 421 | "url": "https://github.com/sponsors/ljharb" 422 | } 423 | }, 424 | "node_modules/chokidar": { 425 | "version": "3.6.0", 426 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 427 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 428 | "dev": true, 429 | "dependencies": { 430 | "anymatch": "~3.1.2", 431 | "braces": "~3.0.2", 432 | "glob-parent": "~5.1.2", 433 | "is-binary-path": "~2.1.0", 434 | "is-glob": "~4.0.1", 435 | "normalize-path": "~3.0.0", 436 | "readdirp": "~3.6.0" 437 | }, 438 | "engines": { 439 | "node": ">= 8.10.0" 440 | }, 441 | "funding": { 442 | "url": "https://paulmillr.com/funding/" 443 | }, 444 | "optionalDependencies": { 445 | "fsevents": "~2.3.2" 446 | } 447 | }, 448 | "node_modules/cliui": { 449 | "version": "8.0.1", 450 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 451 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 452 | "dependencies": { 453 | "string-width": "^4.2.0", 454 | "strip-ansi": "^6.0.1", 455 | "wrap-ansi": "^7.0.0" 456 | }, 457 | "engines": { 458 | "node": ">=12" 459 | } 460 | }, 461 | "node_modules/color-convert": { 462 | "version": "2.0.1", 463 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 464 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 465 | "dependencies": { 466 | "color-name": "~1.1.4" 467 | }, 468 | "engines": { 469 | "node": ">=7.0.0" 470 | } 471 | }, 472 | "node_modules/color-name": { 473 | "version": "1.1.4", 474 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 475 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 476 | }, 477 | "node_modules/combined-stream": { 478 | "version": "1.0.8", 479 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 480 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 481 | "dependencies": { 482 | "delayed-stream": "~1.0.0" 483 | }, 484 | "engines": { 485 | "node": ">= 0.8" 486 | } 487 | }, 488 | "node_modules/concat-map": { 489 | "version": "0.0.1", 490 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 491 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 492 | "dev": true 493 | }, 494 | "node_modules/content-disposition": { 495 | "version": "0.5.4", 496 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 497 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 498 | "dependencies": { 499 | "safe-buffer": "5.2.1" 500 | }, 501 | "engines": { 502 | "node": ">= 0.6" 503 | } 504 | }, 505 | "node_modules/content-type": { 506 | "version": "1.0.5", 507 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 508 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 509 | "engines": { 510 | "node": ">= 0.6" 511 | } 512 | }, 513 | "node_modules/cookie": { 514 | "version": "0.7.1", 515 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 516 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 517 | "engines": { 518 | "node": ">= 0.6" 519 | } 520 | }, 521 | "node_modules/cookie-signature": { 522 | "version": "1.0.6", 523 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 524 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 525 | }, 526 | "node_modules/cors": { 527 | "version": "2.8.5", 528 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 529 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 530 | "license": "MIT", 531 | "dependencies": { 532 | "object-assign": "^4", 533 | "vary": "^1" 534 | }, 535 | "engines": { 536 | "node": ">= 0.10" 537 | } 538 | }, 539 | "node_modules/debug": { 540 | "version": "2.6.9", 541 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 542 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 543 | "dependencies": { 544 | "ms": "2.0.0" 545 | } 546 | }, 547 | "node_modules/define-data-property": { 548 | "version": "1.1.4", 549 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 550 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 551 | "dependencies": { 552 | "es-define-property": "^1.0.0", 553 | "es-errors": "^1.3.0", 554 | "gopd": "^1.0.1" 555 | }, 556 | "engines": { 557 | "node": ">= 0.4" 558 | }, 559 | "funding": { 560 | "url": "https://github.com/sponsors/ljharb" 561 | } 562 | }, 563 | "node_modules/delayed-stream": { 564 | "version": "1.0.0", 565 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 566 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 567 | "engines": { 568 | "node": ">=0.4.0" 569 | } 570 | }, 571 | "node_modules/depd": { 572 | "version": "2.0.0", 573 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 574 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 575 | "engines": { 576 | "node": ">= 0.8" 577 | } 578 | }, 579 | "node_modules/destroy": { 580 | "version": "1.2.0", 581 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 582 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 583 | "engines": { 584 | "node": ">= 0.8", 585 | "npm": "1.2.8000 || >= 1.4.16" 586 | } 587 | }, 588 | "node_modules/duplexify": { 589 | "version": "4.1.3", 590 | "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", 591 | "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", 592 | "dependencies": { 593 | "end-of-stream": "^1.4.1", 594 | "inherits": "^2.0.3", 595 | "readable-stream": "^3.1.1", 596 | "stream-shift": "^1.0.2" 597 | } 598 | }, 599 | "node_modules/ecdsa-sig-formatter": { 600 | "version": "1.0.11", 601 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 602 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 603 | "dependencies": { 604 | "safe-buffer": "^5.0.1" 605 | } 606 | }, 607 | "node_modules/ee-first": { 608 | "version": "1.1.1", 609 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 610 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 611 | }, 612 | "node_modules/emoji-regex": { 613 | "version": "8.0.0", 614 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 615 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 616 | }, 617 | "node_modules/encodeurl": { 618 | "version": "2.0.0", 619 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 620 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 621 | "engines": { 622 | "node": ">= 0.8" 623 | } 624 | }, 625 | "node_modules/end-of-stream": { 626 | "version": "1.4.4", 627 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 628 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 629 | "dependencies": { 630 | "once": "^1.4.0" 631 | } 632 | }, 633 | "node_modules/es-define-property": { 634 | "version": "1.0.0", 635 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 636 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 637 | "dependencies": { 638 | "get-intrinsic": "^1.2.4" 639 | }, 640 | "engines": { 641 | "node": ">= 0.4" 642 | } 643 | }, 644 | "node_modules/es-errors": { 645 | "version": "1.3.0", 646 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 647 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 648 | "engines": { 649 | "node": ">= 0.4" 650 | } 651 | }, 652 | "node_modules/escalade": { 653 | "version": "3.2.0", 654 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 655 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 656 | "engines": { 657 | "node": ">=6" 658 | } 659 | }, 660 | "node_modules/escape-html": { 661 | "version": "1.0.3", 662 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 663 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 664 | }, 665 | "node_modules/etag": { 666 | "version": "1.8.1", 667 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 668 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 669 | "engines": { 670 | "node": ">= 0.6" 671 | } 672 | }, 673 | "node_modules/event-target-shim": { 674 | "version": "5.0.1", 675 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 676 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 677 | "engines": { 678 | "node": ">=6" 679 | } 680 | }, 681 | "node_modules/express": { 682 | "version": "4.21.1", 683 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", 684 | "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", 685 | "dependencies": { 686 | "accepts": "~1.3.8", 687 | "array-flatten": "1.1.1", 688 | "body-parser": "1.20.3", 689 | "content-disposition": "0.5.4", 690 | "content-type": "~1.0.4", 691 | "cookie": "0.7.1", 692 | "cookie-signature": "1.0.6", 693 | "debug": "2.6.9", 694 | "depd": "2.0.0", 695 | "encodeurl": "~2.0.0", 696 | "escape-html": "~1.0.3", 697 | "etag": "~1.8.1", 698 | "finalhandler": "1.3.1", 699 | "fresh": "0.5.2", 700 | "http-errors": "2.0.0", 701 | "merge-descriptors": "1.0.3", 702 | "methods": "~1.1.2", 703 | "on-finished": "2.4.1", 704 | "parseurl": "~1.3.3", 705 | "path-to-regexp": "0.1.10", 706 | "proxy-addr": "~2.0.7", 707 | "qs": "6.13.0", 708 | "range-parser": "~1.2.1", 709 | "safe-buffer": "5.2.1", 710 | "send": "0.19.0", 711 | "serve-static": "1.16.2", 712 | "setprototypeof": "1.2.0", 713 | "statuses": "2.0.1", 714 | "type-is": "~1.6.18", 715 | "utils-merge": "1.0.1", 716 | "vary": "~1.1.2" 717 | }, 718 | "engines": { 719 | "node": ">= 0.10.0" 720 | } 721 | }, 722 | "node_modules/extend": { 723 | "version": "3.0.2", 724 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 725 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 726 | }, 727 | "node_modules/fill-range": { 728 | "version": "7.1.1", 729 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 730 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 731 | "dev": true, 732 | "dependencies": { 733 | "to-regex-range": "^5.0.1" 734 | }, 735 | "engines": { 736 | "node": ">=8" 737 | } 738 | }, 739 | "node_modules/finalhandler": { 740 | "version": "1.3.1", 741 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 742 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 743 | "dependencies": { 744 | "debug": "2.6.9", 745 | "encodeurl": "~2.0.0", 746 | "escape-html": "~1.0.3", 747 | "on-finished": "2.4.1", 748 | "parseurl": "~1.3.3", 749 | "statuses": "2.0.1", 750 | "unpipe": "~1.0.0" 751 | }, 752 | "engines": { 753 | "node": ">= 0.8" 754 | } 755 | }, 756 | "node_modules/follow-redirects": { 757 | "version": "1.15.9", 758 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 759 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 760 | "funding": [ 761 | { 762 | "type": "individual", 763 | "url": "https://github.com/sponsors/RubenVerborgh" 764 | } 765 | ], 766 | "engines": { 767 | "node": ">=4.0" 768 | }, 769 | "peerDependenciesMeta": { 770 | "debug": { 771 | "optional": true 772 | } 773 | } 774 | }, 775 | "node_modules/form-data": { 776 | "version": "4.0.1", 777 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", 778 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 779 | "dependencies": { 780 | "asynckit": "^0.4.0", 781 | "combined-stream": "^1.0.8", 782 | "mime-types": "^2.1.12" 783 | }, 784 | "engines": { 785 | "node": ">= 6" 786 | } 787 | }, 788 | "node_modules/forwarded": { 789 | "version": "0.2.0", 790 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 791 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 792 | "engines": { 793 | "node": ">= 0.6" 794 | } 795 | }, 796 | "node_modules/fresh": { 797 | "version": "0.5.2", 798 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 799 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 800 | "engines": { 801 | "node": ">= 0.6" 802 | } 803 | }, 804 | "node_modules/fsevents": { 805 | "version": "2.3.3", 806 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 807 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 808 | "dev": true, 809 | "hasInstallScript": true, 810 | "optional": true, 811 | "os": [ 812 | "darwin" 813 | ], 814 | "engines": { 815 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 816 | } 817 | }, 818 | "node_modules/function-bind": { 819 | "version": "1.1.2", 820 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 821 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 822 | "funding": { 823 | "url": "https://github.com/sponsors/ljharb" 824 | } 825 | }, 826 | "node_modules/gaxios": { 827 | "version": "6.5.0", 828 | "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.5.0.tgz", 829 | "integrity": "sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==", 830 | "dependencies": { 831 | "extend": "^3.0.2", 832 | "https-proxy-agent": "^7.0.1", 833 | "is-stream": "^2.0.0", 834 | "node-fetch": "^2.6.9", 835 | "uuid": "^9.0.1" 836 | }, 837 | "engines": { 838 | "node": ">=14" 839 | } 840 | }, 841 | "node_modules/gcp-metadata": { 842 | "version": "6.1.0", 843 | "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", 844 | "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", 845 | "dependencies": { 846 | "gaxios": "^6.0.0", 847 | "json-bigint": "^1.0.0" 848 | }, 849 | "engines": { 850 | "node": ">=14" 851 | } 852 | }, 853 | "node_modules/get-caller-file": { 854 | "version": "2.0.5", 855 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 856 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 857 | "engines": { 858 | "node": "6.* || 8.* || >= 10.*" 859 | } 860 | }, 861 | "node_modules/get-intrinsic": { 862 | "version": "1.2.4", 863 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 864 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 865 | "dependencies": { 866 | "es-errors": "^1.3.0", 867 | "function-bind": "^1.1.2", 868 | "has-proto": "^1.0.1", 869 | "has-symbols": "^1.0.3", 870 | "hasown": "^2.0.0" 871 | }, 872 | "engines": { 873 | "node": ">= 0.4" 874 | }, 875 | "funding": { 876 | "url": "https://github.com/sponsors/ljharb" 877 | } 878 | }, 879 | "node_modules/glob-parent": { 880 | "version": "5.1.2", 881 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 882 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 883 | "dev": true, 884 | "dependencies": { 885 | "is-glob": "^4.0.1" 886 | }, 887 | "engines": { 888 | "node": ">= 6" 889 | } 890 | }, 891 | "node_modules/google-auth-library": { 892 | "version": "9.9.0", 893 | "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.9.0.tgz", 894 | "integrity": "sha512-9l+zO07h1tDJdIHN74SpnWIlNR+OuOemXlWJlLP9pXy6vFtizgpEzMuwJa4lqY9UAdiAv5DVd5ql0Am916I+aA==", 895 | "dependencies": { 896 | "base64-js": "^1.3.0", 897 | "ecdsa-sig-formatter": "^1.0.11", 898 | "gaxios": "^6.1.1", 899 | "gcp-metadata": "^6.1.0", 900 | "gtoken": "^7.0.0", 901 | "jws": "^4.0.0" 902 | }, 903 | "engines": { 904 | "node": ">=14" 905 | } 906 | }, 907 | "node_modules/google-gax": { 908 | "version": "4.4.1", 909 | "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", 910 | "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", 911 | "dependencies": { 912 | "@grpc/grpc-js": "^1.10.9", 913 | "@grpc/proto-loader": "^0.7.13", 914 | "@types/long": "^4.0.0", 915 | "abort-controller": "^3.0.0", 916 | "duplexify": "^4.0.0", 917 | "google-auth-library": "^9.3.0", 918 | "node-fetch": "^2.7.0", 919 | "object-hash": "^3.0.0", 920 | "proto3-json-serializer": "^2.0.2", 921 | "protobufjs": "^7.3.2", 922 | "retry-request": "^7.0.0", 923 | "uuid": "^9.0.1" 924 | }, 925 | "engines": { 926 | "node": ">=14" 927 | } 928 | }, 929 | "node_modules/gopd": { 930 | "version": "1.0.1", 931 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 932 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 933 | "dependencies": { 934 | "get-intrinsic": "^1.1.3" 935 | }, 936 | "funding": { 937 | "url": "https://github.com/sponsors/ljharb" 938 | } 939 | }, 940 | "node_modules/gtoken": { 941 | "version": "7.1.0", 942 | "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", 943 | "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", 944 | "dependencies": { 945 | "gaxios": "^6.0.0", 946 | "jws": "^4.0.0" 947 | }, 948 | "engines": { 949 | "node": ">=14.0.0" 950 | } 951 | }, 952 | "node_modules/has-flag": { 953 | "version": "3.0.0", 954 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 955 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 956 | "dev": true, 957 | "engines": { 958 | "node": ">=4" 959 | } 960 | }, 961 | "node_modules/has-property-descriptors": { 962 | "version": "1.0.2", 963 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 964 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 965 | "dependencies": { 966 | "es-define-property": "^1.0.0" 967 | }, 968 | "funding": { 969 | "url": "https://github.com/sponsors/ljharb" 970 | } 971 | }, 972 | "node_modules/has-proto": { 973 | "version": "1.0.3", 974 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 975 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 976 | "engines": { 977 | "node": ">= 0.4" 978 | }, 979 | "funding": { 980 | "url": "https://github.com/sponsors/ljharb" 981 | } 982 | }, 983 | "node_modules/has-symbols": { 984 | "version": "1.0.3", 985 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 986 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 987 | "engines": { 988 | "node": ">= 0.4" 989 | }, 990 | "funding": { 991 | "url": "https://github.com/sponsors/ljharb" 992 | } 993 | }, 994 | "node_modules/hasown": { 995 | "version": "2.0.2", 996 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 997 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 998 | "dependencies": { 999 | "function-bind": "^1.1.2" 1000 | }, 1001 | "engines": { 1002 | "node": ">= 0.4" 1003 | } 1004 | }, 1005 | "node_modules/http-errors": { 1006 | "version": "2.0.0", 1007 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1008 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1009 | "dependencies": { 1010 | "depd": "2.0.0", 1011 | "inherits": "2.0.4", 1012 | "setprototypeof": "1.2.0", 1013 | "statuses": "2.0.1", 1014 | "toidentifier": "1.0.1" 1015 | }, 1016 | "engines": { 1017 | "node": ">= 0.8" 1018 | } 1019 | }, 1020 | "node_modules/http-proxy-agent": { 1021 | "version": "5.0.0", 1022 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", 1023 | "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", 1024 | "dependencies": { 1025 | "@tootallnate/once": "2", 1026 | "agent-base": "6", 1027 | "debug": "4" 1028 | }, 1029 | "engines": { 1030 | "node": ">= 6" 1031 | } 1032 | }, 1033 | "node_modules/http-proxy-agent/node_modules/agent-base": { 1034 | "version": "6.0.2", 1035 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 1036 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 1037 | "dependencies": { 1038 | "debug": "4" 1039 | }, 1040 | "engines": { 1041 | "node": ">= 6.0.0" 1042 | } 1043 | }, 1044 | "node_modules/http-proxy-agent/node_modules/debug": { 1045 | "version": "4.3.7", 1046 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 1047 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 1048 | "dependencies": { 1049 | "ms": "^2.1.3" 1050 | }, 1051 | "engines": { 1052 | "node": ">=6.0" 1053 | }, 1054 | "peerDependenciesMeta": { 1055 | "supports-color": { 1056 | "optional": true 1057 | } 1058 | } 1059 | }, 1060 | "node_modules/http-proxy-agent/node_modules/ms": { 1061 | "version": "2.1.3", 1062 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1063 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1064 | }, 1065 | "node_modules/https-proxy-agent": { 1066 | "version": "7.0.4", 1067 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", 1068 | "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", 1069 | "dependencies": { 1070 | "agent-base": "^7.0.2", 1071 | "debug": "4" 1072 | }, 1073 | "engines": { 1074 | "node": ">= 14" 1075 | } 1076 | }, 1077 | "node_modules/https-proxy-agent/node_modules/debug": { 1078 | "version": "4.3.4", 1079 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 1080 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 1081 | "dependencies": { 1082 | "ms": "2.1.2" 1083 | }, 1084 | "engines": { 1085 | "node": ">=6.0" 1086 | }, 1087 | "peerDependenciesMeta": { 1088 | "supports-color": { 1089 | "optional": true 1090 | } 1091 | } 1092 | }, 1093 | "node_modules/https-proxy-agent/node_modules/ms": { 1094 | "version": "2.1.2", 1095 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1096 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1097 | }, 1098 | "node_modules/iconv-lite": { 1099 | "version": "0.4.24", 1100 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1101 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1102 | "dependencies": { 1103 | "safer-buffer": ">= 2.1.2 < 3" 1104 | }, 1105 | "engines": { 1106 | "node": ">=0.10.0" 1107 | } 1108 | }, 1109 | "node_modules/ignore-by-default": { 1110 | "version": "1.0.1", 1111 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 1112 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 1113 | "dev": true 1114 | }, 1115 | "node_modules/inherits": { 1116 | "version": "2.0.4", 1117 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1118 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1119 | }, 1120 | "node_modules/ipaddr.js": { 1121 | "version": "1.9.1", 1122 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1123 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 1124 | "engines": { 1125 | "node": ">= 0.10" 1126 | } 1127 | }, 1128 | "node_modules/is-binary-path": { 1129 | "version": "2.1.0", 1130 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1131 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1132 | "dev": true, 1133 | "dependencies": { 1134 | "binary-extensions": "^2.0.0" 1135 | }, 1136 | "engines": { 1137 | "node": ">=8" 1138 | } 1139 | }, 1140 | "node_modules/is-extglob": { 1141 | "version": "2.1.1", 1142 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1143 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1144 | "dev": true, 1145 | "engines": { 1146 | "node": ">=0.10.0" 1147 | } 1148 | }, 1149 | "node_modules/is-fullwidth-code-point": { 1150 | "version": "3.0.0", 1151 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1152 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1153 | "engines": { 1154 | "node": ">=8" 1155 | } 1156 | }, 1157 | "node_modules/is-glob": { 1158 | "version": "4.0.3", 1159 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1160 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1161 | "dev": true, 1162 | "dependencies": { 1163 | "is-extglob": "^2.1.1" 1164 | }, 1165 | "engines": { 1166 | "node": ">=0.10.0" 1167 | } 1168 | }, 1169 | "node_modules/is-number": { 1170 | "version": "7.0.0", 1171 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1172 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1173 | "dev": true, 1174 | "engines": { 1175 | "node": ">=0.12.0" 1176 | } 1177 | }, 1178 | "node_modules/is-stream": { 1179 | "version": "2.0.1", 1180 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 1181 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", 1182 | "engines": { 1183 | "node": ">=8" 1184 | }, 1185 | "funding": { 1186 | "url": "https://github.com/sponsors/sindresorhus" 1187 | } 1188 | }, 1189 | "node_modules/json-bigint": { 1190 | "version": "1.0.0", 1191 | "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", 1192 | "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", 1193 | "dependencies": { 1194 | "bignumber.js": "^9.0.0" 1195 | } 1196 | }, 1197 | "node_modules/jwa": { 1198 | "version": "2.0.0", 1199 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", 1200 | "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", 1201 | "dependencies": { 1202 | "buffer-equal-constant-time": "1.0.1", 1203 | "ecdsa-sig-formatter": "1.0.11", 1204 | "safe-buffer": "^5.0.1" 1205 | } 1206 | }, 1207 | "node_modules/jws": { 1208 | "version": "4.0.0", 1209 | "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", 1210 | "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", 1211 | "dependencies": { 1212 | "jwa": "^2.0.0", 1213 | "safe-buffer": "^5.0.1" 1214 | } 1215 | }, 1216 | "node_modules/lodash.camelcase": { 1217 | "version": "4.3.0", 1218 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 1219 | "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 1220 | }, 1221 | "node_modules/long": { 1222 | "version": "5.2.3", 1223 | "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", 1224 | "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" 1225 | }, 1226 | "node_modules/marked": { 1227 | "version": "12.0.2", 1228 | "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", 1229 | "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", 1230 | "bin": { 1231 | "marked": "bin/marked.js" 1232 | }, 1233 | "engines": { 1234 | "node": ">= 18" 1235 | } 1236 | }, 1237 | "node_modules/media-typer": { 1238 | "version": "0.3.0", 1239 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1240 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 1241 | "engines": { 1242 | "node": ">= 0.6" 1243 | } 1244 | }, 1245 | "node_modules/merge-descriptors": { 1246 | "version": "1.0.3", 1247 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 1248 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 1249 | "funding": { 1250 | "url": "https://github.com/sponsors/sindresorhus" 1251 | } 1252 | }, 1253 | "node_modules/methods": { 1254 | "version": "1.1.2", 1255 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1256 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 1257 | "engines": { 1258 | "node": ">= 0.6" 1259 | } 1260 | }, 1261 | "node_modules/mime": { 1262 | "version": "1.6.0", 1263 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1264 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1265 | "bin": { 1266 | "mime": "cli.js" 1267 | }, 1268 | "engines": { 1269 | "node": ">=4" 1270 | } 1271 | }, 1272 | "node_modules/mime-db": { 1273 | "version": "1.52.0", 1274 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1275 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1276 | "engines": { 1277 | "node": ">= 0.6" 1278 | } 1279 | }, 1280 | "node_modules/mime-types": { 1281 | "version": "2.1.35", 1282 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1283 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1284 | "dependencies": { 1285 | "mime-db": "1.52.0" 1286 | }, 1287 | "engines": { 1288 | "node": ">= 0.6" 1289 | } 1290 | }, 1291 | "node_modules/minimatch": { 1292 | "version": "3.1.2", 1293 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1294 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1295 | "dev": true, 1296 | "dependencies": { 1297 | "brace-expansion": "^1.1.7" 1298 | }, 1299 | "engines": { 1300 | "node": "*" 1301 | } 1302 | }, 1303 | "node_modules/ms": { 1304 | "version": "2.0.0", 1305 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1306 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 1307 | }, 1308 | "node_modules/negotiator": { 1309 | "version": "0.6.3", 1310 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1311 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1312 | "engines": { 1313 | "node": ">= 0.6" 1314 | } 1315 | }, 1316 | "node_modules/node-fetch": { 1317 | "version": "2.7.0", 1318 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 1319 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 1320 | "dependencies": { 1321 | "whatwg-url": "^5.0.0" 1322 | }, 1323 | "engines": { 1324 | "node": "4.x || >=6.0.0" 1325 | }, 1326 | "peerDependencies": { 1327 | "encoding": "^0.1.0" 1328 | }, 1329 | "peerDependenciesMeta": { 1330 | "encoding": { 1331 | "optional": true 1332 | } 1333 | } 1334 | }, 1335 | "node_modules/nodemon": { 1336 | "version": "3.1.7", 1337 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", 1338 | "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", 1339 | "dev": true, 1340 | "dependencies": { 1341 | "chokidar": "^3.5.2", 1342 | "debug": "^4", 1343 | "ignore-by-default": "^1.0.1", 1344 | "minimatch": "^3.1.2", 1345 | "pstree.remy": "^1.1.8", 1346 | "semver": "^7.5.3", 1347 | "simple-update-notifier": "^2.0.0", 1348 | "supports-color": "^5.5.0", 1349 | "touch": "^3.1.0", 1350 | "undefsafe": "^2.0.5" 1351 | }, 1352 | "bin": { 1353 | "nodemon": "bin/nodemon.js" 1354 | }, 1355 | "engines": { 1356 | "node": ">=10" 1357 | }, 1358 | "funding": { 1359 | "type": "opencollective", 1360 | "url": "https://opencollective.com/nodemon" 1361 | } 1362 | }, 1363 | "node_modules/nodemon/node_modules/debug": { 1364 | "version": "4.3.7", 1365 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 1366 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 1367 | "dev": true, 1368 | "dependencies": { 1369 | "ms": "^2.1.3" 1370 | }, 1371 | "engines": { 1372 | "node": ">=6.0" 1373 | }, 1374 | "peerDependenciesMeta": { 1375 | "supports-color": { 1376 | "optional": true 1377 | } 1378 | } 1379 | }, 1380 | "node_modules/nodemon/node_modules/ms": { 1381 | "version": "2.1.3", 1382 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1383 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1384 | "dev": true 1385 | }, 1386 | "node_modules/normalize-path": { 1387 | "version": "3.0.0", 1388 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1389 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1390 | "dev": true, 1391 | "engines": { 1392 | "node": ">=0.10.0" 1393 | } 1394 | }, 1395 | "node_modules/object-assign": { 1396 | "version": "4.1.1", 1397 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1398 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1399 | "license": "MIT", 1400 | "engines": { 1401 | "node": ">=0.10.0" 1402 | } 1403 | }, 1404 | "node_modules/object-hash": { 1405 | "version": "3.0.0", 1406 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 1407 | "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 1408 | "engines": { 1409 | "node": ">= 6" 1410 | } 1411 | }, 1412 | "node_modules/object-inspect": { 1413 | "version": "1.13.2", 1414 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 1415 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 1416 | "engines": { 1417 | "node": ">= 0.4" 1418 | }, 1419 | "funding": { 1420 | "url": "https://github.com/sponsors/ljharb" 1421 | } 1422 | }, 1423 | "node_modules/on-finished": { 1424 | "version": "2.4.1", 1425 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1426 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1427 | "dependencies": { 1428 | "ee-first": "1.1.1" 1429 | }, 1430 | "engines": { 1431 | "node": ">= 0.8" 1432 | } 1433 | }, 1434 | "node_modules/once": { 1435 | "version": "1.4.0", 1436 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1437 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1438 | "dependencies": { 1439 | "wrappy": "1" 1440 | } 1441 | }, 1442 | "node_modules/parseurl": { 1443 | "version": "1.3.3", 1444 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1445 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1446 | "engines": { 1447 | "node": ">= 0.8" 1448 | } 1449 | }, 1450 | "node_modules/path-to-regexp": { 1451 | "version": "0.1.10", 1452 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", 1453 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" 1454 | }, 1455 | "node_modules/picomatch": { 1456 | "version": "2.3.1", 1457 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1458 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1459 | "dev": true, 1460 | "engines": { 1461 | "node": ">=8.6" 1462 | }, 1463 | "funding": { 1464 | "url": "https://github.com/sponsors/jonschlinkert" 1465 | } 1466 | }, 1467 | "node_modules/proto3-json-serializer": { 1468 | "version": "2.0.2", 1469 | "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", 1470 | "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", 1471 | "dependencies": { 1472 | "protobufjs": "^7.2.5" 1473 | }, 1474 | "engines": { 1475 | "node": ">=14.0.0" 1476 | } 1477 | }, 1478 | "node_modules/protobufjs": { 1479 | "version": "7.4.0", 1480 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", 1481 | "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", 1482 | "hasInstallScript": true, 1483 | "dependencies": { 1484 | "@protobufjs/aspromise": "^1.1.2", 1485 | "@protobufjs/base64": "^1.1.2", 1486 | "@protobufjs/codegen": "^2.0.4", 1487 | "@protobufjs/eventemitter": "^1.1.0", 1488 | "@protobufjs/fetch": "^1.1.0", 1489 | "@protobufjs/float": "^1.0.2", 1490 | "@protobufjs/inquire": "^1.1.0", 1491 | "@protobufjs/path": "^1.1.2", 1492 | "@protobufjs/pool": "^1.1.0", 1493 | "@protobufjs/utf8": "^1.1.0", 1494 | "@types/node": ">=13.7.0", 1495 | "long": "^5.0.0" 1496 | }, 1497 | "engines": { 1498 | "node": ">=12.0.0" 1499 | } 1500 | }, 1501 | "node_modules/proxy-addr": { 1502 | "version": "2.0.7", 1503 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1504 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1505 | "dependencies": { 1506 | "forwarded": "0.2.0", 1507 | "ipaddr.js": "1.9.1" 1508 | }, 1509 | "engines": { 1510 | "node": ">= 0.10" 1511 | } 1512 | }, 1513 | "node_modules/proxy-from-env": { 1514 | "version": "1.1.0", 1515 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1516 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 1517 | }, 1518 | "node_modules/pstree.remy": { 1519 | "version": "1.1.8", 1520 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1521 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1522 | "dev": true 1523 | }, 1524 | "node_modules/qs": { 1525 | "version": "6.13.0", 1526 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 1527 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 1528 | "dependencies": { 1529 | "side-channel": "^1.0.6" 1530 | }, 1531 | "engines": { 1532 | "node": ">=0.6" 1533 | }, 1534 | "funding": { 1535 | "url": "https://github.com/sponsors/ljharb" 1536 | } 1537 | }, 1538 | "node_modules/range-parser": { 1539 | "version": "1.2.1", 1540 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1541 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1542 | "engines": { 1543 | "node": ">= 0.6" 1544 | } 1545 | }, 1546 | "node_modules/raw-body": { 1547 | "version": "2.5.2", 1548 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1549 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1550 | "dependencies": { 1551 | "bytes": "3.1.2", 1552 | "http-errors": "2.0.0", 1553 | "iconv-lite": "0.4.24", 1554 | "unpipe": "1.0.0" 1555 | }, 1556 | "engines": { 1557 | "node": ">= 0.8" 1558 | } 1559 | }, 1560 | "node_modules/readable-stream": { 1561 | "version": "3.6.2", 1562 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 1563 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 1564 | "dependencies": { 1565 | "inherits": "^2.0.3", 1566 | "string_decoder": "^1.1.1", 1567 | "util-deprecate": "^1.0.1" 1568 | }, 1569 | "engines": { 1570 | "node": ">= 6" 1571 | } 1572 | }, 1573 | "node_modules/readdirp": { 1574 | "version": "3.6.0", 1575 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1576 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1577 | "dev": true, 1578 | "dependencies": { 1579 | "picomatch": "^2.2.1" 1580 | }, 1581 | "engines": { 1582 | "node": ">=8.10.0" 1583 | } 1584 | }, 1585 | "node_modules/require-directory": { 1586 | "version": "2.1.1", 1587 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1588 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1589 | "engines": { 1590 | "node": ">=0.10.0" 1591 | } 1592 | }, 1593 | "node_modules/retry-request": { 1594 | "version": "7.0.2", 1595 | "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", 1596 | "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", 1597 | "dependencies": { 1598 | "@types/request": "^2.48.8", 1599 | "extend": "^3.0.2", 1600 | "teeny-request": "^9.0.0" 1601 | }, 1602 | "engines": { 1603 | "node": ">=14" 1604 | } 1605 | }, 1606 | "node_modules/safe-buffer": { 1607 | "version": "5.2.1", 1608 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1609 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1610 | "funding": [ 1611 | { 1612 | "type": "github", 1613 | "url": "https://github.com/sponsors/feross" 1614 | }, 1615 | { 1616 | "type": "patreon", 1617 | "url": "https://www.patreon.com/feross" 1618 | }, 1619 | { 1620 | "type": "consulting", 1621 | "url": "https://feross.org/support" 1622 | } 1623 | ] 1624 | }, 1625 | "node_modules/safer-buffer": { 1626 | "version": "2.1.2", 1627 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1628 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1629 | }, 1630 | "node_modules/semver": { 1631 | "version": "7.6.3", 1632 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", 1633 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", 1634 | "dev": true, 1635 | "bin": { 1636 | "semver": "bin/semver.js" 1637 | }, 1638 | "engines": { 1639 | "node": ">=10" 1640 | } 1641 | }, 1642 | "node_modules/send": { 1643 | "version": "0.19.0", 1644 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1645 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1646 | "dependencies": { 1647 | "debug": "2.6.9", 1648 | "depd": "2.0.0", 1649 | "destroy": "1.2.0", 1650 | "encodeurl": "~1.0.2", 1651 | "escape-html": "~1.0.3", 1652 | "etag": "~1.8.1", 1653 | "fresh": "0.5.2", 1654 | "http-errors": "2.0.0", 1655 | "mime": "1.6.0", 1656 | "ms": "2.1.3", 1657 | "on-finished": "2.4.1", 1658 | "range-parser": "~1.2.1", 1659 | "statuses": "2.0.1" 1660 | }, 1661 | "engines": { 1662 | "node": ">= 0.8.0" 1663 | } 1664 | }, 1665 | "node_modules/send/node_modules/encodeurl": { 1666 | "version": "1.0.2", 1667 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1668 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1669 | "engines": { 1670 | "node": ">= 0.8" 1671 | } 1672 | }, 1673 | "node_modules/send/node_modules/ms": { 1674 | "version": "2.1.3", 1675 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1676 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1677 | }, 1678 | "node_modules/serve-static": { 1679 | "version": "1.16.2", 1680 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1681 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1682 | "dependencies": { 1683 | "encodeurl": "~2.0.0", 1684 | "escape-html": "~1.0.3", 1685 | "parseurl": "~1.3.3", 1686 | "send": "0.19.0" 1687 | }, 1688 | "engines": { 1689 | "node": ">= 0.8.0" 1690 | } 1691 | }, 1692 | "node_modules/set-function-length": { 1693 | "version": "1.2.2", 1694 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1695 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1696 | "dependencies": { 1697 | "define-data-property": "^1.1.4", 1698 | "es-errors": "^1.3.0", 1699 | "function-bind": "^1.1.2", 1700 | "get-intrinsic": "^1.2.4", 1701 | "gopd": "^1.0.1", 1702 | "has-property-descriptors": "^1.0.2" 1703 | }, 1704 | "engines": { 1705 | "node": ">= 0.4" 1706 | } 1707 | }, 1708 | "node_modules/setprototypeof": { 1709 | "version": "1.2.0", 1710 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1711 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1712 | }, 1713 | "node_modules/side-channel": { 1714 | "version": "1.0.6", 1715 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 1716 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 1717 | "dependencies": { 1718 | "call-bind": "^1.0.7", 1719 | "es-errors": "^1.3.0", 1720 | "get-intrinsic": "^1.2.4", 1721 | "object-inspect": "^1.13.1" 1722 | }, 1723 | "engines": { 1724 | "node": ">= 0.4" 1725 | }, 1726 | "funding": { 1727 | "url": "https://github.com/sponsors/ljharb" 1728 | } 1729 | }, 1730 | "node_modules/simple-update-notifier": { 1731 | "version": "2.0.0", 1732 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 1733 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 1734 | "dev": true, 1735 | "dependencies": { 1736 | "semver": "^7.5.3" 1737 | }, 1738 | "engines": { 1739 | "node": ">=10" 1740 | } 1741 | }, 1742 | "node_modules/statuses": { 1743 | "version": "2.0.1", 1744 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1745 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1746 | "engines": { 1747 | "node": ">= 0.8" 1748 | } 1749 | }, 1750 | "node_modules/stream-events": { 1751 | "version": "1.0.5", 1752 | "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", 1753 | "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", 1754 | "dependencies": { 1755 | "stubs": "^3.0.0" 1756 | } 1757 | }, 1758 | "node_modules/stream-shift": { 1759 | "version": "1.0.3", 1760 | "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", 1761 | "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" 1762 | }, 1763 | "node_modules/string_decoder": { 1764 | "version": "1.3.0", 1765 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1766 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1767 | "dependencies": { 1768 | "safe-buffer": "~5.2.0" 1769 | } 1770 | }, 1771 | "node_modules/string-width": { 1772 | "version": "4.2.3", 1773 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1774 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1775 | "dependencies": { 1776 | "emoji-regex": "^8.0.0", 1777 | "is-fullwidth-code-point": "^3.0.0", 1778 | "strip-ansi": "^6.0.1" 1779 | }, 1780 | "engines": { 1781 | "node": ">=8" 1782 | } 1783 | }, 1784 | "node_modules/strip-ansi": { 1785 | "version": "6.0.1", 1786 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1787 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1788 | "dependencies": { 1789 | "ansi-regex": "^5.0.1" 1790 | }, 1791 | "engines": { 1792 | "node": ">=8" 1793 | } 1794 | }, 1795 | "node_modules/stubs": { 1796 | "version": "3.0.0", 1797 | "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", 1798 | "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" 1799 | }, 1800 | "node_modules/supports-color": { 1801 | "version": "5.5.0", 1802 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1803 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1804 | "dev": true, 1805 | "dependencies": { 1806 | "has-flag": "^3.0.0" 1807 | }, 1808 | "engines": { 1809 | "node": ">=4" 1810 | } 1811 | }, 1812 | "node_modules/teeny-request": { 1813 | "version": "9.0.0", 1814 | "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", 1815 | "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", 1816 | "dependencies": { 1817 | "http-proxy-agent": "^5.0.0", 1818 | "https-proxy-agent": "^5.0.0", 1819 | "node-fetch": "^2.6.9", 1820 | "stream-events": "^1.0.5", 1821 | "uuid": "^9.0.0" 1822 | }, 1823 | "engines": { 1824 | "node": ">=14" 1825 | } 1826 | }, 1827 | "node_modules/teeny-request/node_modules/agent-base": { 1828 | "version": "6.0.2", 1829 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 1830 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 1831 | "dependencies": { 1832 | "debug": "4" 1833 | }, 1834 | "engines": { 1835 | "node": ">= 6.0.0" 1836 | } 1837 | }, 1838 | "node_modules/teeny-request/node_modules/debug": { 1839 | "version": "4.3.7", 1840 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 1841 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 1842 | "dependencies": { 1843 | "ms": "^2.1.3" 1844 | }, 1845 | "engines": { 1846 | "node": ">=6.0" 1847 | }, 1848 | "peerDependenciesMeta": { 1849 | "supports-color": { 1850 | "optional": true 1851 | } 1852 | } 1853 | }, 1854 | "node_modules/teeny-request/node_modules/https-proxy-agent": { 1855 | "version": "5.0.1", 1856 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 1857 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 1858 | "dependencies": { 1859 | "agent-base": "6", 1860 | "debug": "4" 1861 | }, 1862 | "engines": { 1863 | "node": ">= 6" 1864 | } 1865 | }, 1866 | "node_modules/teeny-request/node_modules/ms": { 1867 | "version": "2.1.3", 1868 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1869 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1870 | }, 1871 | "node_modules/to-regex-range": { 1872 | "version": "5.0.1", 1873 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1874 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1875 | "dev": true, 1876 | "dependencies": { 1877 | "is-number": "^7.0.0" 1878 | }, 1879 | "engines": { 1880 | "node": ">=8.0" 1881 | } 1882 | }, 1883 | "node_modules/toidentifier": { 1884 | "version": "1.0.1", 1885 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1886 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1887 | "engines": { 1888 | "node": ">=0.6" 1889 | } 1890 | }, 1891 | "node_modules/touch": { 1892 | "version": "3.1.1", 1893 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", 1894 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", 1895 | "dev": true, 1896 | "bin": { 1897 | "nodetouch": "bin/nodetouch.js" 1898 | } 1899 | }, 1900 | "node_modules/tr46": { 1901 | "version": "0.0.3", 1902 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1903 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 1904 | }, 1905 | "node_modules/type-is": { 1906 | "version": "1.6.18", 1907 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1908 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1909 | "dependencies": { 1910 | "media-typer": "0.3.0", 1911 | "mime-types": "~2.1.24" 1912 | }, 1913 | "engines": { 1914 | "node": ">= 0.6" 1915 | } 1916 | }, 1917 | "node_modules/undefsafe": { 1918 | "version": "2.0.5", 1919 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1920 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 1921 | "dev": true 1922 | }, 1923 | "node_modules/undici-types": { 1924 | "version": "6.19.8", 1925 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 1926 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" 1927 | }, 1928 | "node_modules/unpipe": { 1929 | "version": "1.0.0", 1930 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1931 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1932 | "engines": { 1933 | "node": ">= 0.8" 1934 | } 1935 | }, 1936 | "node_modules/util-deprecate": { 1937 | "version": "1.0.2", 1938 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1939 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1940 | }, 1941 | "node_modules/utils-merge": { 1942 | "version": "1.0.1", 1943 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1944 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1945 | "engines": { 1946 | "node": ">= 0.4.0" 1947 | } 1948 | }, 1949 | "node_modules/uuid": { 1950 | "version": "9.0.1", 1951 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", 1952 | "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", 1953 | "funding": [ 1954 | "https://github.com/sponsors/broofa", 1955 | "https://github.com/sponsors/ctavan" 1956 | ], 1957 | "bin": { 1958 | "uuid": "dist/bin/uuid" 1959 | } 1960 | }, 1961 | "node_modules/vary": { 1962 | "version": "1.1.2", 1963 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1964 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1965 | "engines": { 1966 | "node": ">= 0.8" 1967 | } 1968 | }, 1969 | "node_modules/webidl-conversions": { 1970 | "version": "3.0.1", 1971 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1972 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 1973 | }, 1974 | "node_modules/whatwg-url": { 1975 | "version": "5.0.0", 1976 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1977 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1978 | "dependencies": { 1979 | "tr46": "~0.0.3", 1980 | "webidl-conversions": "^3.0.0" 1981 | } 1982 | }, 1983 | "node_modules/wrap-ansi": { 1984 | "version": "7.0.0", 1985 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1986 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1987 | "dependencies": { 1988 | "ansi-styles": "^4.0.0", 1989 | "string-width": "^4.1.0", 1990 | "strip-ansi": "^6.0.0" 1991 | }, 1992 | "engines": { 1993 | "node": ">=10" 1994 | }, 1995 | "funding": { 1996 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1997 | } 1998 | }, 1999 | "node_modules/wrappy": { 2000 | "version": "1.0.2", 2001 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2002 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 2003 | }, 2004 | "node_modules/y18n": { 2005 | "version": "5.0.8", 2006 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 2007 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 2008 | "engines": { 2009 | "node": ">=10" 2010 | } 2011 | }, 2012 | "node_modules/yargs": { 2013 | "version": "17.7.2", 2014 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 2015 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 2016 | "dependencies": { 2017 | "cliui": "^8.0.1", 2018 | "escalade": "^3.1.1", 2019 | "get-caller-file": "^2.0.5", 2020 | "require-directory": "^2.1.1", 2021 | "string-width": "^4.2.3", 2022 | "y18n": "^5.0.5", 2023 | "yargs-parser": "^21.1.1" 2024 | }, 2025 | "engines": { 2026 | "node": ">=12" 2027 | } 2028 | }, 2029 | "node_modules/yargs-parser": { 2030 | "version": "21.1.1", 2031 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 2032 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 2033 | "engines": { 2034 | "node": ">=12" 2035 | } 2036 | } 2037 | } 2038 | } 2039 | --------------------------------------------------------------------------------