diff --git a/package.json b/package.json index eb13514..8f62d6c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@vitejs/plugin-react": "^4.2.1", "antd": "^5.13.3", "axios": "^1.6.7", + "cheerio": "^1.0.0-rc.12", "dayjs": "^1.11.10", "html-to-text": "^9.0.5", "i18next": "^23.10.1", diff --git a/src/assets/locale/en/settings.json b/src/assets/locale/en/settings.json index 44e3c6e..a959a56 100644 --- a/src/assets/locale/en/settings.json +++ b/src/assets/locale/en/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "General Settings", - "heading": "Web UI Settings", "settings": { + "heading": "Web UI Settings", "speechRecognitionLang": { "label": "Speech Recognition Language", "placeholder": "Select a language" @@ -18,14 +18,25 @@ "dark": "Dark" } }, - "searchMode": { - "label": "Perform Simple Internet Search" - }, "deleteChatHistory": { "label": "Delete Chat History", "button": "Delete", "confirm": "Are you sure you want to delete your chat history? This action cannot be undone." } + }, + "webSearch": { + "heading": "Manage Web Search", + "searchMode": { + "label": "Perform Simple Internet Search" + }, + "provider": { + "label": "Search Engine", + "placeholder": "Select a search engine" + }, + "totalSearchResults": { + "label": "Total Search Results", + "placeholder": "Enter Total Search Results" + } } }, "manageModels": { @@ -205,7 +216,7 @@ } }, "manageSearch": { - "title": "Manage Web Search", + "title": "Manage Web Search", "heading": "Configure Web Search" } } \ No newline at end of file diff --git a/src/assets/locale/ja-JP/settings.json b/src/assets/locale/ja-JP/settings.json index ea7f090..da82fe3 100644 --- a/src/assets/locale/ja-JP/settings.json +++ b/src/assets/locale/ja-JP/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "一般設定", - "heading": "Web UIの設定", "settings": { + "heading": "Web UIの設定", "speechRecognitionLang": { "label": "音声認識の言語", "placeholder": "言語を選択" @@ -26,7 +26,21 @@ "button": "削除", "confirm": "チャット履歴を削除してもよろしいですか?この操作は元に戻せません。" } - } + }, + "webSearch": { + "heading": "ウェブ検索を管理する", + "searchMode": { + "label": "簡単なインターネット検索を実行する" + }, + "provider": { + "label": "検索エンジン", + "placeholder": "検索エンジンを選択する" + }, + "totalSearchResults": { + "label": "合計検索結果", + "placeholder": "合計検索結果を入力する" + } + } }, "manageModels": { "title": "モデルを管理", diff --git a/src/assets/locale/ml/settings.json b/src/assets/locale/ml/settings.json index 82dd19b..a0d7122 100644 --- a/src/assets/locale/ml/settings.json +++ b/src/assets/locale/ml/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "പൊതുവായ സെറ്റിംഗുകൾ", - "heading": "വെബ് UI സെറ്റിംഗുകൾ", "settings": { + "heading": "വെബ് UI സെറ്റിംഗുകൾ", "speechRecognitionLang": { "label": "സംഭാഷണ തിരിച്ചറിയല്‍ ഭാഷ", "placeholder": "ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക" @@ -26,6 +26,20 @@ "button": "ഇല്ലാതാക്കുക", "confirm": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല." } + }, + "webSearch": { + "heading": "വെബ്ബ് തിരച്ചിൽ നിയന്ത്രിക്കുക", + "searchMode": { + "label": "സരളമായ ഇന്റർനെറ്റ് തിരച്ചിൽ നടത്തുക" + }, + "provider": { + "label": "തിരച്ചിൽ എഞ്ചിൻ", + "placeholder": "തിരച്ചിൽ എഞ്ചിൻ തിരഞ്ഞെടുക്കുക" + }, + "totalSearchResults": { + "label": "ആകെ തിരച്ചിൽ ഫലങ്ങൾ", + "placeholder": "ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക" + } } }, "manageModels": { diff --git a/src/assets/locale/zh/settings.json b/src/assets/locale/zh/settings.json index 6ac32bf..edf3c83 100644 --- a/src/assets/locale/zh/settings.json +++ b/src/assets/locale/zh/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "一般设置", - "heading": "Web UI 设置", "settings": { + "heading": "Web UI 设置", "speechRecognitionLang": { "label": "语音识别语言", "placeholder": "选择一种语言" @@ -26,7 +26,21 @@ "button": "删除", "confirm": "您确定要删除聊天历史记录吗?这个操作不能撤销。" } - } + }, + "webSearch": { + "heading": "管理网络搜索", + "searchMode": { + "label": "执行简单的网际网路搜索" + }, + "provider": { + "label": "搜索引擎", + "placeholder": "选择一个搜索引擎" + }, + "totalSearchResults": { + "label": "总搜索结果", + "placeholder": "输入总搜索结果" + } + } }, "manageModels": { "title": "管理模型", diff --git a/src/components/Option/Settings/other.tsx b/src/components/Option/Settings/other.tsx index 5a61adb..3ca3154 100644 --- a/src/components/Option/Settings/other.tsx +++ b/src/components/Option/Settings/other.tsx @@ -17,18 +17,13 @@ export const SettingOther = () => { const { mode, toggleDarkMode } = useDarkMode() const { t } = useTranslation("settings") - const { - changeLocale, - locale, - supportLanguage - }= useI18n() - + const { changeLocale, locale, supportLanguage } = useI18n() return (

- {t("generalSettings.heading")} + {t("generalSettings.settings.heading")}

@@ -38,7 +33,9 @@ export const SettingOther = () => { + option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || + option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 + } + {...form.getInputProps("searchProvider")} + /> + +
+ + {t("generalSettings.webSearch.searchMode.label")} + + +
+
+ + {t("generalSettings.webSearch.totalSearchResults.label")} + + +
- { - setIsSimpleInternetSearch(checked) - queryClient.invalidateQueries({ - queryKey: ["fetchIsSimpleInternetSearch"] - }) - }} - /> +
+ +
+ ) } diff --git a/src/services/ollama.ts b/src/services/ollama.ts index 9a02c48..e8e65b1 100644 --- a/src/services/ollama.ts +++ b/src/services/ollama.ts @@ -296,19 +296,6 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => { await setWebSearchFollowUpPrompt(followUpPrompt) } -export const getIsSimpleInternetSearch = async () => { - const isSimpleInternetSearch = await storage.get("isSimpleInternetSearch") - if (!isSimpleInternetSearch || isSimpleInternetSearch.length === 0) { - return true - } - return isSimpleInternetSearch === "true" -} - - - -export const setIsSimpleInternetSearch = async (isSimpleInternetSearch: boolean) => { - await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString()) -} export const getPageShareUrl = async () => { const pageShareUrl = await storage.get("pageShareUrl") diff --git a/src/services/search.ts b/src/services/search.ts new file mode 100644 index 0000000..f3851b9 --- /dev/null +++ b/src/services/search.ts @@ -0,0 +1,79 @@ +import { Storage } from "@plasmohq/storage" + +const storage = new Storage() + +const TOTAL_SEARCH_RESULTS = 2 +const DEFAULT_PROVIDER = "google" + +const AVAILABLE_PROVIDERS = ["google", "duckduckgo"] as const + +export const getIsSimpleInternetSearch = async () => { + const isSimpleInternetSearch = await storage.get("isSimpleInternetSearch") + if (!isSimpleInternetSearch || isSimpleInternetSearch.length === 0) { + return true + } + return isSimpleInternetSearch === "true" +} + +export const setIsSimpleInternetSearch = async ( + isSimpleInternetSearch: boolean +) => { + await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString()) +} + +export const getSearchProvider = async (): Promise< + (typeof AVAILABLE_PROVIDERS)[number] +> => { + const searchProvider = await storage.get("searchProvider") + if (!searchProvider || searchProvider.length === 0) { + return DEFAULT_PROVIDER + } + return searchProvider as (typeof AVAILABLE_PROVIDERS)[number] +} + +export const setSearchProvider = async (searchProvider: string) => { + await storage.set("searchProvider", searchProvider) +} + +export const totalSearchResults = async () => { + const totalSearchResults = await storage.get("totalSearchResults") + if (!totalSearchResults || totalSearchResults.length === 0) { + return TOTAL_SEARCH_RESULTS + } + return parseInt(totalSearchResults) +} + +export const setTotalSearchResults = async (totalSearchResults: number) => { + await storage.set("totalSearchResults", totalSearchResults.toString()) +} + +export const getSearchSettings = async () => { + const [isSimpleInternetSearch, searchProvider, totalSearchResult] = + await Promise.all([ + getIsSimpleInternetSearch(), + getSearchProvider(), + totalSearchResults() + ]) + + return { + isSimpleInternetSearch, + searchProvider, + totalSearchResults: totalSearchResult + } +} + +export const setSearchSettings = async ({ + isSimpleInternetSearch, + searchProvider, + totalSearchResults +}: { + isSimpleInternetSearch: boolean + searchProvider: string + totalSearchResults: number +}) => { + await Promise.all([ + setIsSimpleInternetSearch(isSimpleInternetSearch), + setSearchProvider(searchProvider), + setTotalSearchResults(totalSearchResults) + ]) +} diff --git a/src/utils/search-provider.ts b/src/utils/search-provider.ts new file mode 100644 index 0000000..f23fb8f --- /dev/null +++ b/src/utils/search-provider.ts @@ -0,0 +1,10 @@ +export const SUPPORTED_SERACH_PROVIDERS = [ + { + label: "Google", + value: "google" + }, + { + label: "DuckDuckGo", + value: "duckduckgo" + } +] \ No newline at end of file diff --git a/src/web/local-duckduckgo.ts b/src/web/local-duckduckgo.ts new file mode 100644 index 0000000..51045d7 --- /dev/null +++ b/src/web/local-duckduckgo.ts @@ -0,0 +1,114 @@ +import { cleanUrl } from "@/libs/clean-url" +import { chromeRunTime } from "@/libs/runtime" +import { PageAssistHtmlLoader } from "@/loader/html" +import { + defaultEmbeddingChunkOverlap, + defaultEmbeddingChunkSize, + defaultEmbeddingModelForRag, + getOllamaURL +} from "@/services/ollama" +import { + getIsSimpleInternetSearch, + totalSearchResults +} from "@/services/search" +import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" +import type { Document } from "@langchain/core/documents" +import * as cheerio from "cheerio" +import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" +import { MemoryVectorStore } from "langchain/vectorstores/memory" + +export const localDuckDuckGoSearch = async (query: string) => { + await chromeRunTime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query)) + + const abortController = new AbortController() + setTimeout(() => abortController.abort(), 10000) + + const htmlString = await fetch( + "https://html.duckduckgo.com/html/?q=" + query, + { + signal: abortController.signal + } + ) + .then((response) => response.text()) + .catch() + + const $ = cheerio.load(htmlString) + + const searchResults = Array.from($("div.results_links_deep")).map( + (result) => { + const title = $(result).find("a.result__a").text() + const link = $(result) + .find("a.result__snippet") + .attr("href") + .replace("//duckduckgo.com/l/?uddg=", "") + const content = $(result).find("a.result__snippet").text() + const decodedLink = decodeURIComponent(link) + return { title, link: decodedLink, content } + } + ) + + return searchResults +} + +export const webDuckDuckGoSearch = async (query: string) => { + const results = await localDuckDuckGoSearch(query) + const TOTAL_SEARCH_RESULTS = await totalSearchResults() + const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) + + const isSimpleMode = await getIsSimpleInternetSearch() + + if (isSimpleMode) { + await getOllamaURL() + return searchResults.map((result) => { + return { + url: result.link, + content: result.content + } + }) + } + + const docs: Document>[] = [] + for (const result of searchResults) { + const loader = new PageAssistHtmlLoader({ + html: "", + url: result.link + }) + + const documents = await loader.loadByURL() + + documents.forEach((doc) => { + docs.push(doc) + }) + } + const ollamaUrl = await getOllamaURL() + + const embeddingModle = await defaultEmbeddingModelForRag() + const ollamaEmbedding = new OllamaEmbeddings({ + model: embeddingModle || "", + baseUrl: cleanUrl(ollamaUrl) + }) + + const chunkSize = await defaultEmbeddingChunkSize() + const chunkOverlap = await defaultEmbeddingChunkOverlap() + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize, + chunkOverlap + }) + + const chunks = await textSplitter.splitDocuments(docs) + + const store = new MemoryVectorStore(ollamaEmbedding) + + await store.addDocuments(chunks) + + const resultsWithEmbeddings = await store.similaritySearch(query, 3) + + const searchResult = resultsWithEmbeddings.map((result) => { + return { + url: result.metadata.url, + content: result.pageContent + } + }) + + return searchResult +} diff --git a/src/web/local-google.ts b/src/web/local-google.ts index 0f6f316..98fa2a8 100644 --- a/src/web/local-google.ts +++ b/src/web/local-google.ts @@ -1,3 +1,7 @@ +import { + getIsSimpleInternetSearch, + totalSearchResults +} from "@/services/search" import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" import type { Document } from "@langchain/core/documents" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" @@ -5,16 +9,13 @@ import { MemoryVectorStore } from "langchain/vectorstores/memory" import { cleanUrl } from "~/libs/clean-url" import { chromeRunTime } from "~/libs/runtime" import { PageAssistHtmlLoader } from "~/loader/html" -import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~/services/ollama" +import { + defaultEmbeddingChunkOverlap, + defaultEmbeddingChunkSize, + defaultEmbeddingModelForRag, + getOllamaURL +} from "~/services/ollama" -const BLOCKED_HOSTS = [ - "google.com", - "youtube.com", - "twitter.com", - "linkedin.com", -] - -const TOTAL_SEARCH_RESULTS = 2 export const localGoogleSearch = async (query: string) => { await chromeRunTime( @@ -40,23 +41,18 @@ export const localGoogleSearch = async (query: string) => { (result) => { const title = result.querySelector("h3")?.textContent const link = result.querySelector("a")?.getAttribute("href") - const content = Array.from(result.querySelectorAll("span")).map((span) => span.textContent).join(" ") + const content = Array.from(result.querySelectorAll("span")) + .map((span) => span.textContent) + .join(" ") return { title, link, content } } ) - const filteredSearchResults = searchResults - .filter( - (result) => - !result.link || - !BLOCKED_HOSTS.some((host) => result.link.includes(host)) - ) - .filter((result) => result.title && result.link) - return filteredSearchResults + return searchResults } - -export const webSearch = async (query: string) => { +export const webGoogleSearch = async (query: string) => { const results = await localGoogleSearch(query) + const TOTAL_SEARCH_RESULTS = await totalSearchResults() const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) const isSimpleMode = await getIsSimpleInternetSearch() @@ -71,7 +67,7 @@ export const webSearch = async (query: string) => { }) } - const docs: Document>[] = []; + const docs: Document>[] = [] for (const result of searchResults) { const loader = new PageAssistHtmlLoader({ html: "", @@ -89,14 +85,14 @@ export const webSearch = async (query: string) => { const embeddingModle = await defaultEmbeddingModelForRag() const ollamaEmbedding = new OllamaEmbeddings({ model: embeddingModle || "", - baseUrl: cleanUrl(ollamaUrl), + baseUrl: cleanUrl(ollamaUrl) }) - const chunkSize = await defaultEmbeddingChunkSize(); - const chunkOverlap = await defaultEmbeddingChunkOverlap(); + const chunkSize = await defaultEmbeddingChunkSize() + const chunkOverlap = await defaultEmbeddingChunkOverlap() const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize, - chunkOverlap, + chunkOverlap }) const chunks = await textSplitter.splitDocuments(docs) @@ -105,7 +101,6 @@ export const webSearch = async (query: string) => { await store.addDocuments(chunks) - const resultsWithEmbeddings = await store.similaritySearch(query, 3) const searchResult = resultsWithEmbeddings.map((result) => { @@ -116,4 +111,4 @@ export const webSearch = async (query: string) => { }) return searchResult -} \ No newline at end of file +} diff --git a/src/web/web.ts b/src/web/web.ts index e1eccd3..83675e7 100644 --- a/src/web/web.ts +++ b/src/web/web.ts @@ -1,42 +1,61 @@ import { getWebSearchPrompt } from "~/services/ollama" -import { webSearch } from "./local-google" +import { webGoogleSearch } from "./local-google" +import { webDuckDuckGoSearch } from "./local-duckduckgo" +import { getSearchProvider } from "@/services/search" const getHostName = (url: string) => { - try { - const hostname = new URL(url).hostname - return hostname - } catch (e) { - return "" - } + try { + const hostname = new URL(url).hostname + return hostname + } catch (e) { + return "" + } +} + +const searchWeb = (provider: string, query: string) => { + switch (provider) { + case "duckduckgo": + return webDuckDuckGoSearch(query) + default: + return webGoogleSearch(query) + } } export const getSystemPromptForWeb = async (query: string) => { - try { - const search = await webSearch(query) + try { + const searchProvider = await getSearchProvider() + const search = await searchWeb(searchProvider, query) - const search_results = search.map((result, idx) => `${result.content}`).join("\n") + const search_results = search + .map( + (result, idx) => + `${result.content}` + ) + .join("\n") - const current_date_time = new Date().toLocaleString() + const current_date_time = new Date().toLocaleString() - const system = await getWebSearchPrompt(); + const system = await getWebSearchPrompt() - const prompt = system.replace("{current_date_time}", current_date_time).replace("{search_results}", search_results) + const prompt = system + .replace("{current_date_time}", current_date_time) + .replace("{search_results}", search_results) + return { + prompt, + source: search.map((result) => { return { - prompt, - source: search.map((result) => { - return { - url: result.url, - name: getHostName(result.url), - type: "url", - } - }) - } - } catch (e) { - console.error(e) - return { - prompt: "", - source: [], + url: result.url, + name: getHostName(result.url), + type: "url" } + }) } -} \ No newline at end of file + } catch (e) { + console.error(e) + return { + prompt: "", + source: [] + } + } +} diff --git a/yarn.lock b/yarn.lock index 521c779..0ff8b4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1884,6 +1884,31 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -3194,7 +3219,7 @@ html-to-text@^9.0.5: htmlparser2 "^8.0.2" selderee "^0.11.0" -htmlparser2@^8.0.2: +htmlparser2@^8.0.1, htmlparser2@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== @@ -4998,6 +5023,14 @@ parse5-htmlparser2-tree-adapter@^6.0.0: dependencies: parse5 "^6.0.1" +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + parse5@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" @@ -5008,7 +5041,7 @@ parse5@^6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -parse5@^7.1.1: +parse5@^7.0.0, parse5@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== @@ -6974,6 +7007,7 @@ winreg@0.0.12: integrity sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==