(),
{ contextType: 'film' }
)
@@ -86,6 +86,7 @@ const contextLabel = computed(() => {
if (props.contextType === 'film') return 'Film recommendations'
if (props.contextType === 'song') return 'Song recommendations'
if (props.contextType === 'podcast') return 'Podcast recommendations'
+ if (props.contextType === 'websites') return 'Websites'
return 'Content'
})
diff --git a/packages/app/src/components/content/FilmDetail.vue b/packages/app/src/components/content/FilmDetail.vue
index 1d719ec3..378338b2 100644
--- a/packages/app/src/components/content/FilmDetail.vue
+++ b/packages/app/src/components/content/FilmDetail.vue
@@ -1,22 +1,22 @@
@@ -16,8 +15,7 @@ import LoadingFilmCard from './LoadingFilmCard.vue'
withDefaults(
defineProps<{
count?: number
- hasText?: boolean
}>(),
- { count: 12, hasText: true }
+ { count: 12 }
)
diff --git a/packages/app/src/components/content/MagazineGrid.vue b/packages/app/src/components/content/MagazineGrid.vue
new file mode 100644
index 00000000..95941fdd
--- /dev/null
+++ b/packages/app/src/components/content/MagazineGrid.vue
@@ -0,0 +1,289 @@
+
+
+
+
+
diff --git a/packages/app/src/components/content/NewsCard.vue b/packages/app/src/components/content/NewsCard.vue
new file mode 100644
index 00000000..227d3c41
--- /dev/null
+++ b/packages/app/src/components/content/NewsCard.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
diff --git a/packages/app/src/components/content/NewsGrid.vue b/packages/app/src/components/content/NewsGrid.vue
new file mode 100644
index 00000000..50419311
--- /dev/null
+++ b/packages/app/src/components/content/NewsGrid.vue
@@ -0,0 +1,214 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+ {{ filteredArticles.length }} {{ variant === 'websites' ? 'websites' : 'articles' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/app/src/components/content/PodcastCard.vue b/packages/app/src/components/content/PodcastCard.vue
index 48451a69..9ef7b52f 100644
--- a/packages/app/src/components/content/PodcastCard.vue
+++ b/packages/app/src/components/content/PodcastCard.vue
@@ -49,20 +49,28 @@
diff --git a/packages/app/src/stores/articleOverlay.ts b/packages/app/src/stores/articleOverlay.ts
new file mode 100644
index 00000000..f88c14d8
--- /dev/null
+++ b/packages/app/src/stores/articleOverlay.ts
@@ -0,0 +1,37 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+const SAFE_URL_SCHEME = /^https?:\/\//i
+
+export const useArticleOverlayStore = defineStore('articleOverlay', () => {
+ const isOpen = ref(false)
+ const url = ref
(null)
+ const title = ref('')
+ const content = ref(null)
+ const imgSrc = ref(null)
+
+ function open(articleUrl: string, articleTitle = '', articleContent?: string, articleImgSrc?: string) {
+ const trimmed = String(articleUrl ?? '').trim()
+ if (!SAFE_URL_SCHEME.test(trimmed)) return
+ try {
+ new URL(trimmed)
+ } catch {
+ return
+ }
+ url.value = trimmed
+ title.value = String(articleTitle ?? 'Article').slice(0, 200)
+ content.value = typeof articleContent === 'string' && articleContent.trim().length > 0 ? articleContent.trim() : null
+ imgSrc.value = typeof articleImgSrc === 'string' && articleImgSrc.trim().length > 0 ? articleImgSrc.trim() : null
+ isOpen.value = true
+ }
+
+ function close() {
+ isOpen.value = false
+ url.value = null
+ title.value = ''
+ content.value = null
+ imgSrc.value = null
+ }
+
+ return { isOpen, url, title, content, imgSrc, open, close }
+})
diff --git a/packages/app/src/stores/chat.ts b/packages/app/src/stores/chat.ts
index 7ae0f533..0652ae8e 100644
--- a/packages/app/src/stores/chat.ts
+++ b/packages/app/src/stores/chat.ts
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
-import type { Message, Conversation } from '@aiui/core/types/message'
+import type { Message, Conversation, WebSearchResult } from '@aiui/core/types/message'
const isDev = import.meta.env.DEV
let saveTimer: ReturnType | null = null
@@ -52,6 +52,8 @@ export const useChatStore = defineStore('chat', () => {
const savedSide = localStorage.getItem('aiui-panel-side') as 'left' | 'right' | null
const panelSide = ref<'left' | 'right'>(savedSide ?? 'right')
+ const webSearchEnabled = ref(localStorage.getItem('aiui-web-search') !== 'false')
+
// Load chats from server on startup — _loaded gate prevents the watcher
// from overwriting the file with empty data before the load completes
if (isDev) {
@@ -75,6 +77,10 @@ export const useChatStore = defineStore('chat', () => {
localStorage.setItem('aiui-panel-side', val)
})
+ watch(webSearchEnabled, (val) => {
+ localStorage.setItem('aiui-web-search', String(val))
+ })
+
watch(
[conversations, activeConversationId],
([conv, active]) => {
@@ -134,6 +140,13 @@ export const useChatStore = defineStore('chat', () => {
last.content += text
}
+ function setMessageWebResults(conversationId: string, messageId: string, results: WebSearchResult[]) {
+ const conv = conversations.value.get(conversationId)
+ if (!conv) return
+ const msg = conv.messages.find((m) => m.id === messageId)
+ if (msg) msg.webResults = results
+ }
+
function switchSide() {
panelSide.value = panelSide.value === 'right' ? 'left' : 'right'
}
@@ -161,9 +174,11 @@ export const useChatStore = defineStore('chat', () => {
isStreaming,
loaded,
panelSide,
+ webSearchEnabled,
createConversation,
addMessage,
appendToLastMessage,
+ setMessageWebResults,
switchSide,
setActiveConversation,
deleteConversation,
diff --git a/packages/app/src/styles/main.css b/packages/app/src/styles/main.css
index 53beb48a..bd578253 100644
--- a/packages/app/src/styles/main.css
+++ b/packages/app/src/styles/main.css
@@ -328,7 +328,7 @@ body {
position: absolute;
inset: 0;
border-radius: inherit;
- padding: 2px;
+ padding: 1px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
@@ -407,7 +407,7 @@ body {
.light .nav-tab-active {
background: linear-gradient(135deg, #fefdf9 0%, #faf9f6 100%) !important;
color: #0a0a0a !important;
- border: 1px solid #e8e8e8;
+ border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.06),
inset 0 1px 0 rgba(254, 253, 249, 1) !important;
@@ -510,34 +510,50 @@ input:focus-visible {
color: transparent;
}
-/* ===== SCROLLBAR — Archy custom gradient scrollbar ===== */
+/* ===== SCROLLBAR — slim, elegant ===== */
+
+.custom-scrollbar {
+ scrollbar-gutter: stable;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
+}
.custom-scrollbar::-webkit-scrollbar {
- width: 10px;
+ width: 5px;
}
.custom-scrollbar::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.2);
- border-radius: 10px;
+ background: transparent;
+ margin: 8px 0;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.1) 100%);
+ background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
- border: 2px solid rgba(0, 0, 0, 0.2);
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.2) 100%);
+ background: rgba(255, 255, 255, 0.25);
}
-.light .custom-scrollbar::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.04);
+.custom-scrollbar::-webkit-scrollbar-thumb:active {
+ background: rgba(255, 255, 255, 0.35);
+}
+
+.light .custom-scrollbar {
+ scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
}
.light .custom-scrollbar::-webkit-scrollbar-thumb {
- background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.08) 100%);
- border: 2px solid rgba(255, 255, 255, 0.5);
+ background: rgba(0, 0, 0, 0.12);
+}
+
+.light .custom-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.light .custom-scrollbar::-webkit-scrollbar-thumb:active {
+ background: rgba(0, 0, 0, 0.28);
}
.scrollbar-hide {
diff --git a/packages/app/vite-rss.ts b/packages/app/vite-rss.ts
new file mode 100644
index 00000000..c8d51383
--- /dev/null
+++ b/packages/app/vite-rss.ts
@@ -0,0 +1,141 @@
+import type { Plugin } from 'vite'
+import type { Connect } from 'vite'
+import Parser from 'rss-parser'
+
+export interface RssArticle {
+ title: string
+ url: string
+ content?: string
+ imgSrc?: string
+}
+
+const RSS_PATHS = ['/feed', '/rss', '/atom.xml', '/feed.xml', '/?feed=rss', '/rss.xml', '/.rss', '/feeds/posts/default']
+
+function extractImgFromItem(item: Record): string | undefined {
+ const enc = item.enclosure as { url?: string; type?: string } | undefined
+ if (enc?.url && /^https?:\/\//i.test(enc.url)) {
+ const t = (enc.type ?? '').toLowerCase()
+ if (t.startsWith('image/') || t === '') return enc.url
+ }
+ const thumb = item['media:thumbnail'] as { $?: { url?: string }; url?: string } | undefined
+ if (thumb?.$?.url) return thumb.$.url
+ if (typeof thumb?.url === 'string') return thumb.url
+ const arr = thumb as unknown[]
+ if (Array.isArray(arr) && arr[0]?.$?.url) return (arr[0] as { $: { url: string } }).$.url
+ const media = item['media:content'] as { $?: { url?: string }; url?: string } | undefined
+ if (media?.$?.url) return media.$.url
+ if (typeof media?.url === 'string') return media.url
+ const group = item['media:group'] as { 'media:content'?: Array<{ $?: { url?: string } }> } | undefined
+ if (group?.['media:content']?.[0]?.$?.url) return group['media:content'][0].$.url
+ const itunes = item['itunes:image'] as { href?: string } | undefined
+ if (itunes?.href) return itunes.href
+ const content = item.content ?? item.summary
+ const html = typeof content === 'string' ? content : ''
+ const imgMatch = html.match(/
]+src=["']([^"']+)["']/i)
+ if (imgMatch?.[1]) return imgMatch[1]
+ return undefined
+}
+
+async function tryParseFeed(parser: Parser, feedUrl: string): Promise {
+ try {
+ const feed = await parser.parseURL(feedUrl)
+ if (!feed?.items?.length) return null
+ return feed.items.slice(0, 10).map((item) => {
+ const rawContent = item.contentSnippet ?? item.content ?? item.summary
+ const contentStr = typeof rawContent === 'string' ? rawContent : ''
+ const imgSrc = extractImgFromItem(item as Record)
+ return {
+ title: (item.title ?? '').trim() || 'Untitled',
+ url: (item.link ?? item.guid ?? '').trim() || feedUrl,
+ content: contentStr.trim().slice(0, 15000),
+ imgSrc: imgSrc && /^https?:\/\//i.test(imgSrc) ? imgSrc : undefined,
+ }
+ })
+ } catch {
+ return null
+ }
+}
+
+function discoverFeedUrl(siteUrl: string): string[] {
+ try {
+ const u = new URL(siteUrl)
+ const base = `${u.protocol}//${u.host}`
+ return RSS_PATHS.map((path) => base + path)
+ } catch {
+ return []
+ }
+}
+
+async function fetchRssFromUrls(urls: string[]): Promise {
+ const parser = new Parser({
+ timeout: 5000,
+ headers: { 'User-Agent': 'AIUI/1.0 (RSS reader)' },
+ })
+
+ const seen = new Set()
+ const articles: RssArticle[] = []
+
+ for (const url of urls.slice(0, 5)) {
+ const candidates = discoverFeedUrl(url)
+ for (const feedUrl of candidates) {
+ const items = await tryParseFeed(parser, feedUrl)
+ if (!items?.length) continue
+ for (const a of items) {
+ const k = a.url.toLowerCase()
+ if (seen.has(k)) continue
+ seen.add(k)
+ articles.push(a)
+ }
+ break // found a feed for this site, move to next URL
+ }
+ }
+
+ return articles.slice(0, 15)
+}
+
+function createRssMiddleware() {
+ return async (req: Connect.IncomingMessage, res: any, next: () => void) => {
+ if (req.method !== 'GET') return next()
+ const requestUrl = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`)
+ if (!requestUrl.pathname.startsWith('/api/rss-articles')) return next()
+
+ const urls = requestUrl.searchParams.getAll('url').map((u) => u.trim()).filter(Boolean)
+
+ if (urls.length === 0) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Missing urls (array or comma-separated)' }))
+ return
+ }
+
+ const safe = urls.filter((u) => /^https?:\/\//i.test(u.trim())).slice(0, 8)
+ if (safe.length === 0) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'No valid https URLs' }))
+ return
+ }
+
+ try {
+ const articles = await fetchRssFromUrls(safe)
+ res.setHeader('Content-Type', 'application/json')
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.setHeader('Cache-Control', 'public, max-age=300')
+ res.end(JSON.stringify({ articles }))
+ } catch (err) {
+ console.error('[rss]', err)
+ res.writeHead(502, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: String(err) }))
+ }
+ }
+}
+
+export function rssPlugin(): Plugin {
+ return {
+ name: 'aiui-rss',
+ configureServer(server) {
+ server.middlewares.use(createRssMiddleware())
+ },
+ configurePreviewServer(server) {
+ server.middlewares.use(createRssMiddleware())
+ },
+ }
+}
diff --git a/packages/app/vite-web-search.ts b/packages/app/vite-web-search.ts
new file mode 100644
index 00000000..aa87737d
--- /dev/null
+++ b/packages/app/vite-web-search.ts
@@ -0,0 +1,135 @@
+import type { Plugin } from 'vite'
+import type { Connect } from 'vite'
+import { loadEnv } from 'vite'
+import { search as searchDuckDuckGo } from 'duck-duck-scrape'
+
+export interface WebSearchResult {
+ title: string
+ url: string
+ content?: string
+ imgSrc?: string
+ engine?: string
+}
+
+const FALLBACK_INSTANCES = [
+ 'https://searx.tiekoetter.com',
+ 'https://search.bus-hit.me',
+ 'https://paulgo.io',
+]
+
+async function fetchFromSearXNG(
+ searchUrl: string,
+ q: string,
+): Promise<{ results?: { title?: string; url?: string; content?: string; img_src?: string; thumbnail?: string; engine?: string }[] } | null> {
+ try {
+ const searchRes = await fetch(searchUrl, {
+ headers: { Accept: 'application/json', 'User-Agent': 'AIUI/1.0' },
+ signal: AbortSignal.timeout(6000),
+ })
+ if (!searchRes.ok) return null
+ return (await searchRes.json()) as { results?: { title?: string; url?: string; content?: string; img_src?: string; thumbnail?: string; engine?: string }[] }
+ } catch {
+ return null
+ }
+}
+
+function createWebSearchMiddleware(searxUrl: string | undefined) {
+ return async (req: Connect.IncomingMessage, res: any, next: () => void) => {
+ if (req.method !== 'GET') return next()
+ const url = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`)
+ const q = url.searchParams.get('q')?.trim()
+ if (!q) {
+ res.writeHead(400, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Missing q (query)' }))
+ return
+ }
+
+ const instances = searxUrl ? [searxUrl] : FALLBACK_INSTANCES
+
+ let data: { results?: { title?: string; url?: string; content?: string; img_src?: string; thumbnail?: string; engine?: string }[] } | null = null
+ let lastError = ''
+
+ for (const baseUrl of instances) {
+ const normalized = baseUrl.replace(/\/$/, '')
+ const searchUrl = `${normalized}/search?${new URLSearchParams({ q, format: 'json', pageno: '1' })}`
+ console.log('[web-search]', q.slice(0, 40), '→', normalized)
+ data = await fetchFromSearXNG(searchUrl, q)
+ if (data?.results && data.results.length > 0) {
+ console.log('[web-search]', data.results.length, 'results from', normalized)
+ break
+ }
+ lastError = data ? 'no results' : 'request failed'
+ console.warn('[web-search]', normalized, lastError, '— trying next')
+ }
+
+ if (!data) {
+ console.warn('[web-search] SearXNG failed, trying DuckDuckGo fallback')
+ try {
+ const ddg = await searchDuckDuckGo(q)
+ if (ddg?.results?.length) {
+ const results: WebSearchResult[] = ddg.results
+ .filter((r: { title?: string; url?: string }) => r.title && r.url)
+ .slice(0, 6)
+ .map((r: { title: string; url: string; description?: string; icon?: string }) => ({
+ title: r.title,
+ url: r.url,
+ content: r.description ?? undefined,
+ imgSrc: r.icon ?? undefined,
+ }))
+ console.log('[web-search] DuckDuckGo fallback:', results.length, 'results')
+ res.setHeader('Content-Type', 'application/json')
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.setHeader('Cache-Control', 'public, max-age=300')
+ res.end(JSON.stringify({ results }))
+ return
+ }
+ } catch (ddgErr) {
+ console.warn('[web-search] DuckDuckGo fallback failed:', ddgErr)
+ }
+ console.error('[web-search] All search backends failed:', lastError)
+ res.writeHead(502, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Web search unavailable. Try SEARXNG_URL in .env.local.' }))
+ return
+ }
+
+ try {
+ const results: WebSearchResult[] = (data.results ?? [])
+ .filter((r) => r.title && r.url)
+ .slice(0, 6)
+ .map((r) => ({
+ title: r.title ?? '',
+ url: r.url ?? '',
+ content: r.content ?? undefined,
+ imgSrc: r.img_src || r.thumbnail || undefined,
+ engine: r.engine ?? undefined,
+ }))
+
+ res.setHeader('Content-Type', 'application/json')
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.setHeader('Cache-Control', 'public, max-age=300')
+ res.end(JSON.stringify({ results }))
+ } catch (err) {
+ console.error('[web-search]', err)
+ res.writeHead(502, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: String(err) }))
+ }
+ }
+}
+
+export function webSearchPlugin(): Plugin {
+ let searxUrl: string | undefined
+
+ return {
+ name: 'aiui-web-search',
+ configResolved(config) {
+ const env = loadEnv(config.mode, process.cwd(), '')
+ searxUrl = env.SEARXNG_URL ?? env.VITE_SEARXNG_URL
+ },
+ configureServer(server) {
+ server.middlewares.use('/api/web-search', createWebSearchMiddleware(searxUrl))
+ },
+ configurePreviewServer(server) {
+ server.middlewares.use('/api/web-search', createWebSearchMiddleware(searxUrl))
+ },
+ }
+}
diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts
index d8eb6732..876e2d07 100644
--- a/packages/app/vite.config.ts
+++ b/packages/app/vite.config.ts
@@ -6,6 +6,8 @@ import { resolve } from 'path'
import { tmdbPlugin } from './vite-tmdb'
import { devChatsPlugin } from './vite-dev-chats'
import { musicSearchPlugin } from './vite-music-search'
+import { webSearchPlugin } from './vite-web-search'
+import { rssPlugin } from './vite-rss'
export default defineConfig({
plugins: [
@@ -14,6 +16,8 @@ export default defineConfig({
tmdbPlugin(),
devChatsPlugin(),
musicSearchPlugin(),
+ webSearchPlugin(),
+ rssPlugin(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg', 'icon.svg', 'apple-touch-icon-180x180.png'],
@@ -56,6 +60,14 @@ export default defineConfig({
urlPattern: /^https:\/\/openrouter\.ai\/.*/i,
handler: 'NetworkOnly',
},
+ {
+ urlPattern: /\/api\/web-search\?.*/i,
+ handler: 'NetworkOnly',
+ },
+ {
+ urlPattern: /\/api\/rss-articles\?.*/i,
+ handler: 'NetworkOnly',
+ },
],
},
devOptions: {
diff --git a/packages/core/src/types/message.ts b/packages/core/src/types/message.ts
index f693f343..1519d822 100644
--- a/packages/core/src/types/message.ts
+++ b/packages/core/src/types/message.ts
@@ -1,5 +1,13 @@
import type { ContentBlock } from './content'
+export interface WebSearchResult {
+ title: string
+ url: string
+ content?: string
+ /** Image/thumbnail URL from search engine (e.g. SearXNG img_src) */
+ imgSrc?: string
+}
+
export interface Message {
id: string
role: 'user' | 'assistant' | 'system'
@@ -11,6 +19,8 @@ export interface Message {
replyTo?: string
reactions?: Reaction[]
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'
+ /** Web search results (articles, links) attached when query used web search */
+ webResults?: WebSearchResult[]
}
export interface Reaction {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8dd423d0..c3beb0dd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -39,9 +39,15 @@ importers:
'@vitejs/plugin-vue':
specifier: latest
version: 6.0.4(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.8.3))
+ duck-duck-scrape:
+ specifier: ^2.2.7
+ version: 2.2.7
eslint:
specifier: latest
version: 10.0.2(jiti@2.6.1)
+ rss-parser:
+ specifier: ^3.13.0
+ version: 3.13.0
tailwindcss:
specifier: latest
version: 4.2.1
@@ -1436,6 +1442,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ duck-duck-scrape@2.2.7:
+ resolution: {integrity: sha512-BEcglwnfx5puJl90KQfX+Q2q5vCguqyMpZcSRPBWk8OY55qWwV93+E+7DbIkrGDW4qkqPfUvtOUdi0lXz6lEMQ==}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1452,6 +1461,9 @@ packages:
resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==}
engines: {node: '>=10.13.0'}
+ entities@2.2.0:
+ resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
+
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
@@ -1693,6 +1705,13 @@ packages:
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+ html-entities@2.6.0:
+ resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
idb@7.1.1:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
@@ -2035,6 +2054,11 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ needle@3.3.1:
+ resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==}
+ engines: {node: '>= 4.4.x'}
+ hasBin: true
+
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -2216,6 +2240,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rss-parser@3.13.0:
+ resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
+
safe-array-concat@1.1.3:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
@@ -2231,6 +2258,13 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sax@1.5.0:
+ resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
+ engines: {node: '>=11.0.0'}
+
scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
@@ -2721,6 +2755,14 @@ packages:
workbox-window@7.4.0:
resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==}
+ xml2js@0.5.0:
+ resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
+ engines: {node: '>=4.0.0'}
+
+ xmlbuilder@11.0.1:
+ resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
+ engines: {node: '>=4.0'}
+
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -4130,6 +4172,11 @@ snapshots:
detect-libc@2.1.2: {}
+ duck-duck-scrape@2.2.7:
+ dependencies:
+ html-entities: 2.6.0
+ needle: 3.3.1
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4147,6 +4194,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ entities@2.2.0: {}
+
entities@7.0.1: {}
es-abstract@1.24.1:
@@ -4482,6 +4531,12 @@ snapshots:
hookable@5.5.3: {}
+ html-entities@2.6.0: {}
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
idb@7.1.1: {}
ignore@5.3.2: {}
@@ -4775,6 +4830,11 @@ snapshots:
natural-compare@1.4.0: {}
+ needle@3.3.1:
+ dependencies:
+ iconv-lite: 0.6.3
+ sax: 1.5.0
+
node-releases@2.0.27: {}
object-inspect@1.13.4: {}
@@ -4983,6 +5043,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
+ rss-parser@3.13.0:
+ dependencies:
+ entities: 2.2.0
+ xml2js: 0.5.0
+
safe-array-concat@1.1.3:
dependencies:
call-bind: 1.0.8
@@ -5004,6 +5069,10 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
+ safer-buffer@2.1.2: {}
+
+ sax@1.5.0: {}
+
scule@1.3.0: {}
semver@6.3.1: {}
@@ -5593,6 +5662,13 @@ snapshots:
'@types/trusted-types': 2.0.7
workbox-core: 7.4.0
+ xml2js@0.5.0:
+ dependencies:
+ sax: 1.5.0
+ xmlbuilder: 11.0.1
+
+ xmlbuilder@11.0.1: {}
+
yallist@3.1.1: {}
yaml@2.8.2: {}