From cc9aee68ee3d50745512dd2e7b52a9a0f8564239 Mon Sep 17 00:00:00 2001 From: Bun Bun Date: Wed, 17 Jun 2026 06:48:47 +0000 Subject: [PATCH] =?UTF-8?q?TaskDecay=20v1.0.0=20MVP=20=E2=80=94=20smart=20?= =?UTF-8?q?task=20review=20&=20archive=20Chrome=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + icon128.png | Bin 0 -> 202 bytes icon16.png | Bin 0 -> 202 bytes icon48.png | Bin 0 -> 202 bytes lib/apis/index.js | 63 ++ lib/apis/notion.js | 90 ++ lib/apis/ticktick.js | 60 ++ lib/apis/todoist.js | 51 ++ lib/background.js | 143 +++ lib/content.js | 238 +++++ lib/options.js | 82 ++ lib/popup.js | 92 ++ lib/review.js | 96 ++ lib/scoring.js | 72 ++ lib/storage.js | 56 ++ lib/types.js | 14 + manifest.json | 44 + package-lock.json | 2003 ++++++++++++++++++++++++++++++++++++++++++ package.json | 18 + src/apis/index.ts | 69 ++ src/apis/notion.ts | 99 +++ src/apis/ticktick.ts | 63 ++ src/apis/todoist.ts | 56 ++ src/background.ts | 146 +++ src/content.css | 17 + src/content.ts | 240 +++++ src/options.html | 100 +++ src/options.ts | 87 ++ src/popup.html | 80 ++ src/popup.ts | 98 +++ src/review.html | 89 ++ src/review.ts | 116 +++ src/scoring.test.js | 75 ++ src/scoring.ts | 88 ++ src/storage.ts | 64 ++ src/types.ts | 62 ++ tsconfig.json | 15 + vite.config.ts | 18 + 38 files changed, 4710 insertions(+) create mode 100644 .gitignore create mode 100644 icon128.png create mode 100644 icon16.png create mode 100644 icon48.png create mode 100644 lib/apis/index.js create mode 100644 lib/apis/notion.js create mode 100644 lib/apis/ticktick.js create mode 100644 lib/apis/todoist.js create mode 100644 lib/background.js create mode 100644 lib/content.js create mode 100644 lib/options.js create mode 100644 lib/popup.js create mode 100644 lib/review.js create mode 100644 lib/scoring.js create mode 100644 lib/storage.js create mode 100644 lib/types.js create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/apis/index.ts create mode 100644 src/apis/notion.ts create mode 100644 src/apis/ticktick.ts create mode 100644 src/apis/todoist.ts create mode 100644 src/background.ts create mode 100644 src/content.css create mode 100644 src/content.ts create mode 100644 src/options.html create mode 100644 src/options.ts create mode 100644 src/popup.html create mode 100644 src/popup.ts create mode 100644 src/review.html create mode 100644 src/review.ts create mode 100644 src/scoring.test.js create mode 100644 src/scoring.ts create mode 100644 src/storage.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a8645 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.verdict +*.log +.venv/ +.DS_Store diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf49866ee736df6d35434f6ae674fabbdd603f9 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?- zG7x6;t5)LxG9*h}BT9nv(@M${i&7cN%ggmL^RkPR6AM!H@{7`Ezq647Dsu63aSXBO zO-_(tU7T=($*t`Qv%?yWBRmdoc#q^cykR<`=kSK>h@Zn7o+IBJ-Y^{DbI9QbvcSX} jjw5D0R}8q=>=+p8l2{a97cZX#G?Ky7)z4*}Q$iB}1&ln3 literal 0 HcmV?d00001 diff --git a/icon16.png b/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf49866ee736df6d35434f6ae674fabbdd603f9 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?- zG7x6;t5)LxG9*h}BT9nv(@M${i&7cN%ggmL^RkPR6AM!H@{7`Ezq647Dsu63aSXBO zO-_(tU7T=($*t`Qv%?yWBRmdoc#q^cykR<`=kSK>h@Zn7o+IBJ-Y^{DbI9QbvcSX} jjw5D0R}8q=>=+p8l2{a97cZX#G?Ky7)z4*}Q$iB}1&ln3 literal 0 HcmV?d00001 diff --git a/icon48.png b/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf49866ee736df6d35434f6ae674fabbdd603f9 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?- zG7x6;t5)LxG9*h}BT9nv(@M${i&7cN%ggmL^RkPR6AM!H@{7`Ezq647Dsu63aSXBO zO-_(tU7T=($*t`Qv%?yWBRmdoc#q^cykR<`=kSK>h@Zn7o+IBJ-Y^{DbI9QbvcSX} jjw5D0R}8q=>=+p8l2{a97cZX#G?Ky7)z4*}Q$iB}1&ln3 literal 0 HcmV?d00001 diff --git a/lib/apis/index.js b/lib/apis/index.js new file mode 100644 index 0000000..40e14c4 --- /dev/null +++ b/lib/apis/index.js @@ -0,0 +1,63 @@ +import { fetchTodoistTasks, closeTodoistTask, deleteTodoistTask } from './todoist.js'; +import { fetchTickTickTasks, closeTickTickTask, deleteTickTickTask } from './ticktick.js'; +import { fetchNotionTasks, archiveNotionTask, deleteNotionTask } from './notion.js'; +export async function fetchAllTasks(providers) { + const all = []; + const errors = []; + if (providers.todoist.enabled && providers.todoist.token) { + try { + const tasks = await fetchTodoistTasks(providers.todoist); + all.push(...tasks); + } + catch (e) { + errors.push(`Todoist: ${e.message}`); + } + } + if (providers.ticktick.enabled && providers.ticktick.token) { + try { + const tasks = await fetchTickTickTasks(providers.ticktick); + all.push(...tasks); + } + catch (e) { + errors.push(`TickTick: ${e.message}`); + } + } + if (providers.notion.enabled && providers.notion.token) { + try { + const tasks = await fetchNotionTasks(providers.notion); + all.push(...tasks); + } + catch (e) { + errors.push(`Notion: ${e.message}`); + } + } + if (errors.length > 0 && all.length === 0) { + throw new Error(errors.join('; ')); + } + return all; +} +export async function completeTask(task, token) { + if (task.source === 'todoist') + await closeTodoistTask(task.id, token); + else if (task.source === 'ticktick') + await closeTickTickTask(task.id, token); + else if (task.source === 'notion') + await archiveNotionTask(task.id, token); +} +export async function deleteTask(task, token) { + if (task.source === 'todoist') + await deleteTodoistTask(task.id, token); + else if (task.source === 'ticktick') + await deleteTickTickTask(task.id, token); + else if (task.source === 'notion') + await deleteNotionTask(task.id, token); +} +export function getTokenForSource(source, providers) { + if (source === 'todoist') + return providers.todoist.token; + if (source === 'ticktick') + return providers.ticktick.token; + if (source === 'notion') + return providers.notion.token; + return ''; +} diff --git a/lib/apis/notion.js b/lib/apis/notion.js new file mode 100644 index 0000000..2204bad --- /dev/null +++ b/lib/apis/notion.js @@ -0,0 +1,90 @@ +// Notion API — queries a database for tasks +export async function fetchNotionTasks(config) { + if (!config.token) + throw new Error('Notion token not configured'); + if (!config.projectIds || config.projectIds.length === 0) { + throw new Error('Notion database ID not configured. Set a database ID in options.'); + } + const headers = { + 'Authorization': `Bearer ${config.token}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }; + const allTasks = []; + for (const dbId of config.projectIds) { + const res = await fetch(`https://api.notion.com/v1/databases/${dbId}/query`, { + method: 'POST', + headers, + body: JSON.stringify({ page_size: 100 }), + }); + if (!res.ok) { + console.warn(`Notion DB query failed for ${dbId}: ${res.status}`); + continue; + } + const data = await res.json(); + const results = data.results || []; + for (const page of results) { + const props = page.properties || {}; + const titleProp = Object.values(props).find((p) => p.type === 'title'); + const title = titleProp?.title?.[0]?.plain_text || 'Untitled'; + const statusProp = Object.values(props).find((p) => p.type === 'status' || p.type === 'select' || p.type === 'checkbox'); + let done = false; + if (statusProp?.type === 'checkbox') + done = statusProp.checkbox; + else if (statusProp?.type === 'status') + done = statusProp.status?.name === 'Done'; + else if (statusProp?.type === 'select') + done = statusProp.select?.name === 'Done'; + if (done) + continue; + const dateProp = Object.values(props).find((p) => p.type === 'date'); + const dueDate = dateProp?.date?.start ? new Date(dateProp.date.start) : null; + const multiSelect = Object.values(props).filter((p) => p.type === 'multi_select'); + const labels = multiSelect.flatMap((p) => p.multi_select?.map((s) => s.name) || []); + const created = page.created_time ? new Date(page.created_time) : new Date(); + const updated = page.last_edited_time ? new Date(page.last_edited_time) : created; + allTasks.push({ + id: `notion-${page.id}`, + title, + priority: 1, + createdAt: created, + updatedAt: updated, + completedAt: null, + dueDate, + projectId: `notion-${dbId}`, + projectName: dbId.slice(0, 8), + labels, + url: page.url, + source: 'notion', + raw: page, + }); + } + } + return allTasks; +} +export async function archiveNotionTask(taskId, token) { + const realId = taskId.replace('notion-', ''); + const res = await fetch(`https://api.notion.com/v1/pages/${realId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ archived: true }), + }); + if (!res.ok) + throw new Error(`Failed to archive Notion task: ${res.status}`); +} +export async function deleteNotionTask(taskId, token) { + const realId = taskId.replace('notion-', ''); + const res = await fetch(`https://api.notion.com/v1/blocks/${realId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + 'Notion-Version': '2022-06-28', + }, + }); + if (!res.ok) + throw new Error(`Failed to delete Notion task: ${res.status}`); +} diff --git a/lib/apis/ticktick.js b/lib/apis/ticktick.js new file mode 100644 index 0000000..5073810 --- /dev/null +++ b/lib/apis/ticktick.js @@ -0,0 +1,60 @@ +// TickTick API v2 — uses login token or OAuth access token +export async function fetchTickTickTasks(config) { + if (!config.token) + throw new Error('TickTick token not configured'); + const headers = { + 'Authorization': `Bearer ${config.token}`, + 'Content-Type': 'application/json', + }; + // Get all projects first + const projectsRes = await fetch('https://api.ticktick.com/open/v1/project', { headers }); + const projects = projectsRes.ok ? await projectsRes.json() : []; + const projectList = Array.isArray(projects) ? projects : (projects || []); + const allTasks = []; + for (const proj of projectList) { + const pid = proj.id; + const tasksRes = await fetch(`https://api.ticktick.com/open/v1/project/${pid}/task`, { headers }); + if (!tasksRes.ok) + continue; + const tasks = await tasksRes.json(); + const taskList = Array.isArray(tasks) ? tasks : (tasks || []); + for (const t of taskList) { + if (t.status === 2) + continue; // completed + allTasks.push({ + id: `ticktick-${t.id}`, + title: t.title, + priority: (t.priority || 0) + 1, + createdAt: new Date(t.createdTime || Date.now()), + updatedAt: new Date(t.modifiedTime || t.createdTime || Date.now()), + completedAt: null, + dueDate: t.dueDate ? new Date(t.dueDate) : null, + projectId: `ticktick-${pid}`, + projectName: proj.name || null, + labels: t.tags || [], + url: `https://ticktick.com/webapp/#p/${pid}/task/${t.id}`, + source: 'ticktick', + raw: t, + }); + } + } + return allTasks; +} +export async function closeTickTickTask(taskId, token) { + const realId = taskId.replace('ticktick-', ''); + const res = await fetch(`https://api.ticktick.com/open/v1/task/${realId}/complete`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) + throw new Error(`Failed to close TickTick task: ${res.status}`); +} +export async function deleteTickTickTask(taskId, token) { + const realId = taskId.replace('ticktick-', ''); + const res = await fetch(`https://api.ticktick.com/open/v1/task/${realId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) + throw new Error(`Failed to delete TickTick task: ${res.status}`); +} diff --git a/lib/apis/todoist.js b/lib/apis/todoist.js new file mode 100644 index 0000000..c922c16 --- /dev/null +++ b/lib/apis/todoist.js @@ -0,0 +1,51 @@ +export async function fetchTodoistTasks(config) { + if (!config.token) + throw new Error('Todoist token not configured'); + const headers = { + 'Authorization': `Bearer ${config.token}`, + 'Content-Type': 'application/json', + }; + // Fetch all active tasks + const tasksRes = await fetch('https://api.todoist.com/rest/v2/tasks', { headers }); + if (!tasksRes.ok) { + throw new Error(`Todoist API error: ${tasksRes.status} ${tasksRes.statusText}`); + } + const tasks = await tasksRes.json(); + // Fetch projects for names + const projectsRes = await fetch('https://api.todoist.com/rest/v2/projects', { headers }); + const projects = projectsRes.ok ? await projectsRes.json() : []; + const projectMap = new Map(projects.map((p) => [p.id, p.name])); + return tasks.map((t) => ({ + id: `todoist-${t.id}`, + title: t.content, + priority: t.priority || 1, + createdAt: new Date(t.created_at || Date.now()), + updatedAt: new Date(t.updated_at || t.created_at || Date.now()), + completedAt: null, + dueDate: t.due?.date ? new Date(t.due.date) : null, + projectId: t.project_id ? `todoist-${t.project_id}` : null, + projectName: projectMap.get(t.project_id) || null, + labels: t.labels || [], + url: `https://todoist.com/app/task/${t.id}`, + source: 'todoist', + raw: t, + })); +} +export async function closeTodoistTask(taskId, token) { + const realId = taskId.replace('todoist-', ''); + const res = await fetch(`https://api.todoist.com/rest/v2/tasks/${realId}/close`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) + throw new Error(`Failed to close Todoist task: ${res.status}`); +} +export async function deleteTodoistTask(taskId, token) { + const realId = taskId.replace('todoist-', ''); + const res = await fetch(`https://api.todoist.com/rest/v2/tasks/${realId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) + throw new Error(`Failed to delete Todoist task: ${res.status}`); +} diff --git a/lib/background.js b/lib/background.js new file mode 100644 index 0000000..f5e4439 --- /dev/null +++ b/lib/background.js @@ -0,0 +1,143 @@ +import { fetchAllTasks, getTokenForSource, completeTask, deleteTask } from './apis/index.js'; +import { getSettings, getStoredTasks, setStoredTasks, setLastSync, addDismissedTask, addArchivedTask } from './storage.js'; +import { getReviewTasks, getAutoArchiveCandidates } from './scoring.js'; +// Background service worker for TaskDecay +// Handles periodic sync, badge count, and alarm-based review reminders +chrome.runtime.onInstalled.addListener(() => { + chrome.alarms.create('taskdecay-sync', { periodInMinutes: 60 }); + chrome.alarms.create('taskdecay-review-reminder', { periodInMinutes: 60 * 24 }); + updateBadge(); +}); +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === 'taskdecay-sync') { + await syncTasks(); + } + else if (alarm.name === 'taskdecay-review-reminder') { + await checkReviewReminder(); + } +}); +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + const msg = message; + if (msg.type === 'sync') { + syncTasks().then(sendResponse).catch(e => sendResponse({ error: e.message })); + return true; + } + if (msg.type === 'getReviewTasks') { + getReviewData().then(sendResponse).catch(e => sendResponse({ error: e.message })); + return true; + } + if (msg.type === 'dismiss') { + addDismissedTask(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: e.message })); + return true; + } + if (msg.type === 'archive') { + archiveTask(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: e.message })); + return true; + } + if (msg.type === 'complete') { + completeTaskById(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: e.message })); + return true; + } + if (msg.type === 'delete') { + deleteTaskById(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: e.message })); + return true; + } + if (msg.type === 'openReview') { + chrome.tabs.create({ url: chrome.runtime.getURL('src/review.html') }); + sendResponse({ ok: true }); + return true; + } +}); +async function syncTasks() { + const settings = await getSettings(); + const tasks = await fetchAllTasks(settings.providers); + await setStoredTasks(tasks); + await setLastSync(new Date().toISOString()); + await updateBadge(); + return { count: tasks.length, syncedAt: new Date().toISOString() }; +} +async function getReviewData() { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const review = getReviewTasks(tasks, settings); + const autoArchive = getAutoArchiveCandidates(tasks, settings); + return { review, autoArchiveCount: autoArchive.length, total: tasks.length }; +} +async function archiveTask(taskId) { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const task = tasks.find(t => t.id === taskId); + if (!task) + throw new Error('Task not found'); + const token = getTokenForSource(task.source, settings.providers); + if (!token) + throw new Error('No token for source'); + await completeTask(task, token); + await addArchivedTask(taskId); + await updateBadge(); +} +async function completeTaskById(taskId) { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const task = tasks.find(t => t.id === taskId); + if (!task) + throw new Error('Task not found'); + const token = getTokenForSource(task.source, settings.providers); + if (!token) + throw new Error('No token for source'); + await completeTask(task, token); + await addArchivedTask(taskId); + await updateBadge(); +} +async function deleteTaskById(taskId) { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const task = tasks.find(t => t.id === taskId); + if (!task) + throw new Error('Task not found'); + const token = getTokenForSource(task.source, settings.providers); + if (!token) + throw new Error('No token for source'); + await deleteTask(task, token); + await addArchivedTask(taskId); + await updateBadge(); +} +async function checkReviewReminder() { + const settings = await getSettings(); + if (!settings.lastReviewDate) + return; + const last = new Date(settings.lastReviewDate); + const now = new Date(); + const daysSince = (now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24); + if (daysSince >= settings.reviewIntervalDays) { + const tasks = await getStoredTasks(); + const review = getReviewTasks(tasks, settings); + if (review.length > 0) { + chrome.notifications.create('review-reminder', { + type: 'basic', + iconUrl: 'icon128.png', + title: 'TaskDecay Review Ready', + message: `${review.length} tasks need your attention. Open the review session to clean your backlog.`, + buttons: [{ title: 'Start Review' }], + }); + } + } +} +async function updateBadge() { + try { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const review = getReviewTasks(tasks, settings); + const count = review.length; + chrome.action.setBadgeText({ text: count > 0 ? String(count) : '' }); + chrome.action.setBadgeBackgroundColor({ color: '#e74c3c' }); + } + catch { + chrome.action.setBadgeText({ text: '' }); + } +} +chrome.notifications.onButtonClicked.addListener((notificationId) => { + if (notificationId === 'review-reminder') { + chrome.tabs.create({ url: chrome.runtime.getURL('src/review.html') }); + } +}); diff --git a/lib/content.js b/lib/content.js new file mode 100644 index 0000000..f85b083 --- /dev/null +++ b/lib/content.js @@ -0,0 +1,238 @@ +const BADGE_CLASS = 'taskdecay-injected-badge'; +function detectTaskManager() { + const host = window.location.host; + if (host.includes('todoist.com')) + return 'todoist'; + if (host.includes('ticktick.com')) + return 'ticktick'; + if (host.includes('notion.so')) + return 'notion'; + return null; +} +function extractTaskIdFromUrl() { + const url = window.location.href; + const platform = detectTaskManager(); + if (platform === 'todoist') { + const m = url.match(/task\/(\d+)/); + if (m) + return `todoist-${m[1]}`; + } + if (platform === 'ticktick') { + const m = url.match(/task\/(\w+)/); + if (m) + return `ticktick-${m[1]}`; + } + if (platform === 'notion') { + const m = url.match(/([a-f0-9]{32})/); + if (m) + return `notion-${m[1]}`; + } + return null; +} +function injectStyles() { + if (document.getElementById('taskdecay-styles')) + return; + const style = document.createElement('style'); + style.id = 'taskdecay-styles'; + style.textContent = ` + .taskdecay-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + font-family: system-ui, -apple-system, sans-serif; + margin-left: 8px; + cursor: pointer; + transition: opacity 0.2s; + } + .taskdecay-badge:hover { opacity: 0.8; } + .taskdecay-badge-warm { background: #f39c12; color: #fff; } + .taskdecay-badge-hot { background: #e74c3c; color: #fff; } + .taskdecay-badge-cold { background: #3498db; color: #fff; } + .taskdecay-toast { + position: fixed; + bottom: 20px; + right: 20px; + background: #1a1a1a; + color: #fff; + padding: 12px 16px; + border-radius: 8px; + font-family: system-ui, -apple-system, sans-serif; + font-size: 13px; + z-index: 999999; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + max-width: 320px; + line-height: 1.4; + } + .taskdecay-toast a { + color: #58a6ff; + text-decoration: none; + display: block; + margin-top: 8px; + } + .taskdecay-toast button { + background: #e74c3c; + border: none; + color: #fff; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + margin-top: 8px; + font-size: 12px; + } + `; + document.head.appendChild(style); +} +function showToast(message, action) { + const existing = document.querySelector('.taskdecay-toast'); + if (existing) + existing.remove(); + const toast = document.createElement('div'); + toast.className = 'taskdecay-toast'; + toast.textContent = message; + if (action) { + const btn = document.createElement('button'); + btn.textContent = action.label; + btn.onclick = () => { action.onClick(); toast.remove(); }; + toast.appendChild(btn); + } + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 8000); +} +async function getTaskDecayInfo(taskId) { + return new Promise((resolve) => { + chrome.runtime.sendMessage({ type: 'getReviewTasks' }, (response) => { + if (!response || response.error) { + resolve(null); + return; + } + const match = response.review?.find((r) => r.task.id === taskId); + if (match) { + resolve({ + taskId: match.task.id, + score: match.score.score, + ageDays: match.score.ageDays, + explanation: match.score.explanation, + }); + } + else { + resolve(null); + } + }); + }); +} +function addBadgeToTodoist(info) { + const selectors = [ + '[data-testid="task-details-modal"] [data-testid="task-title"]', + '[data-testid="task-details-modal"] h1', + '.task_content', + '[data-item-id] .task_content', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && !el.parentElement?.querySelector('.taskdecay-badge')) { + const badge = document.createElement('span'); + badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`; + badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`; + badge.title = info.explanation; + badge.onclick = (e) => { + e.stopPropagation(); + chrome.runtime.sendMessage({ type: 'openReview' }); + }; + el.parentElement?.insertBefore(badge, el.nextSibling); + break; + } + } +} +function addBadgeToTickTick(info) { + const selectors = [ + '.task-title', + '.task-title-text', + '[class*="taskTitle"]', + 'h1', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && !el.parentElement?.querySelector('.taskdecay-badge')) { + const badge = document.createElement('span'); + badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`; + badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`; + badge.title = info.explanation; + badge.onclick = (e) => { + e.stopPropagation(); + chrome.runtime.sendMessage({ type: 'openReview' }); + }; + el.parentElement?.insertBefore(badge, el.nextSibling); + break; + } + } +} +function addBadgeToNotion(info) { + const selectors = [ + '[data-testid="page-title"]', + '.notion-page-block', + 'h1', + '[class*="title" i]', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && !el.parentElement?.querySelector('.taskdecay-badge')) { + const badge = document.createElement('span'); + badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`; + badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`; + badge.title = info.explanation; + badge.onclick = (e) => { + e.stopPropagation(); + chrome.runtime.sendMessage({ type: 'openReview' }); + }; + el.parentElement?.insertBefore(badge, el.nextSibling); + break; + } + } +} +async function injectBadge() { + const taskId = extractTaskIdFromUrl(); + if (!taskId) + return; + const info = await getTaskDecayInfo(taskId); + if (!info) + return; + const platform = detectTaskManager(); + if (platform === 'todoist') + addBadgeToTodoist(info); + else if (platform === 'ticktick') + addBadgeToTickTick(info); + else if (platform === 'notion') + addBadgeToNotion(info); +} +function init() { + injectStyles(); + injectBadge(); + // Re-inject on URL changes (SPA navigation) + let lastUrl = window.location.href; + const observer = new MutationObserver(() => { + if (window.location.href !== lastUrl) { + lastUrl = window.location.href; + setTimeout(injectBadge, 800); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} +else { + init(); +} +chrome.runtime.onMessage.addListener((message) => { + const msg = message; + if (msg.type === 'showToast') { + showToast(msg.text, msg.action ? { label: msg.action.label, onClick: () => { + chrome.runtime.sendMessage({ type: msg.action.type }); + } } : undefined); + } +}); +export {}; diff --git a/lib/options.js b/lib/options.js new file mode 100644 index 0000000..5ce9f6a --- /dev/null +++ b/lib/options.js @@ -0,0 +1,82 @@ +import { getSettings, setSettings, clearAllTasks } from './storage.js'; +import { fetchAllTasks } from './apis/index.js'; +document.addEventListener('DOMContentLoaded', async () => { + const el = (id) => document.getElementById(id); + const settings = await getSettings(); + el('todoistEnabled').checked = settings.providers.todoist.enabled; + el('todoistToken').value = settings.providers.todoist.token; + el('ticktickEnabled').checked = settings.providers.ticktick.enabled; + el('ticktickToken').value = settings.providers.ticktick.token; + el('notionEnabled').checked = settings.providers.notion.enabled; + el('notionToken').value = settings.providers.notion.token; + el('notionDbs').value = (settings.providers.notion.projectIds || []).join(', '); + el('decayThreshold').value = String(settings.decayThresholdDays); + el('reviewInterval').value = String(settings.reviewIntervalDays); + el('maxTasks').value = String(settings.maxTasksPerReview); + el('autoArchive').value = String(settings.autoArchiveDays); + document.getElementById('saveBtn')?.addEventListener('click', async () => { + const status = document.getElementById('status'); + status.textContent = ''; + status.className = 'status'; + const newSettings = { + ...settings, + providers: { + todoist: { + enabled: el('todoistEnabled').checked, + token: el('todoistToken').value.trim(), + lastSync: settings.providers.todoist.lastSync, + projectIds: null, + }, + ticktick: { + enabled: el('ticktickEnabled').checked, + token: el('ticktickToken').value.trim(), + lastSync: settings.providers.ticktick.lastSync, + projectIds: null, + }, + notion: { + enabled: el('notionEnabled').checked, + token: el('notionToken').value.trim(), + lastSync: settings.providers.notion.lastSync, + projectIds: el('notionDbs').value.split(',').map(s => s.trim()).filter(Boolean), + }, + }, + decayThresholdDays: parseInt(el('decayThreshold').value) || 14, + reviewIntervalDays: parseInt(el('reviewInterval').value) || 7, + maxTasksPerReview: parseInt(el('maxTasks').value) || 20, + autoArchiveDays: parseInt(el('autoArchive').value) || 90, + }; + await setSettings(newSettings); + status.textContent = 'Settings saved.'; + status.classList.add('success'); + }); + document.getElementById('testBtn')?.addEventListener('click', async () => { + const status = document.getElementById('status'); + status.textContent = 'Testing...'; + status.className = 'status'; + try { + const testSettings = { + ...settings, + providers: { + todoist: { enabled: el('todoistEnabled').checked, token: el('todoistToken').value.trim(), lastSync: null, projectIds: null }, + ticktick: { enabled: el('ticktickEnabled').checked, token: el('ticktickToken').value.trim(), lastSync: null, projectIds: null }, + notion: { enabled: el('notionEnabled').checked, token: el('notionToken').value.trim(), lastSync: null, projectIds: el('notionDbs').value.split(',').map(s => s.trim()).filter(Boolean) }, + }, + }; + const tasks = await fetchAllTasks(testSettings.providers); + status.textContent = `Connection OK! Found ${tasks.length} tasks across enabled providers.`; + status.classList.add('success'); + } + catch (e) { + status.textContent = `Error: ${e.message}`; + status.classList.add('error'); + } + }); + document.getElementById('clearBtn')?.addEventListener('click', async () => { + if (!confirm('Clear all stored tasks and sync data?')) + return; + await clearAllTasks(); + const status = document.getElementById('status'); + status.textContent = 'All data cleared.'; + status.className = 'status success'; + }); +}); diff --git a/lib/popup.js b/lib/popup.js new file mode 100644 index 0000000..6a29504 --- /dev/null +++ b/lib/popup.js @@ -0,0 +1,92 @@ +document.addEventListener('DOMContentLoaded', () => { + const main = document.getElementById('main'); + const syncStatus = document.getElementById('syncStatus'); + const countBadge = document.getElementById('countBadge'); + async function load() { + try { + const res = await chrome.runtime.sendMessage({ type: 'getReviewTasks' }); + if (res.error) { + syncStatus.textContent = ''; + renderError(res.error); + return; + } + const review = res.review || []; + const total = res.total || 0; + const autoArchiveCount = res.autoArchiveCount || 0; + syncStatus.textContent = `${total} total tasks synced`; + if (review.length === 0) { + countBadge.style.display = 'none'; + renderEmpty(); + } + else { + countBadge.textContent = String(review.length); + countBadge.style.display = 'inline-block'; + renderStats(review, autoArchiveCount); + } + } + catch (e) { + syncStatus.textContent = ''; + renderError(e.message); + } + } + function renderEmpty() { + main.innerHTML = ` +
No decayed tasks found. Great job keeping up!
+ + Open Settings + `; + document.getElementById('syncBtn')?.addEventListener('click', doSync); + } + function renderError(msg) { + main.innerHTML = ` +
${escapeHtml(msg)}
+ + Open Settings + `; + document.getElementById('syncBtn')?.addEventListener('click', doSync); + } + function renderStats(review, autoArchiveCount) { + const hot = review.filter((r) => r.score.score >= 70).length; + const warm = review.filter((r) => r.score.score >= 40 && r.score.score < 70).length; + const cold = review.filter((r) => r.score.score < 40).length; + const oldest = review.reduce((a, b) => a.score.ageDays > b.score.ageDays ? a : b); + main.innerHTML = ` +
Hot (≥70)${hot}
+
Warm (40-69)${warm}
+
Cold (<40)${cold}
+
Oldest task${Math.round(oldest.score.ageDays)} days
+ ${autoArchiveCount > 0 ? `
Auto-archive ready${autoArchiveCount}
` : ''} + + + Open Settings + `; + document.getElementById('reviewBtn')?.addEventListener('click', () => { + chrome.runtime.sendMessage({ type: 'openReview' }); + window.close(); + }); + document.getElementById('syncBtn')?.addEventListener('click', doSync); + } + async function doSync() { + const btn = document.getElementById('syncBtn'); + if (btn) + btn.textContent = 'Syncing...'; + try { + const res = await chrome.runtime.sendMessage({ type: 'sync' }); + if (res.error) { + renderError(res.error); + return; + } + await load(); + } + catch (e) { + renderError(e.message); + } + } + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + load(); +}); +export {}; diff --git a/lib/review.js b/lib/review.js new file mode 100644 index 0000000..e48d6a5 --- /dev/null +++ b/lib/review.js @@ -0,0 +1,96 @@ +let items = []; +let doneIds = new Set(); +document.addEventListener('DOMContentLoaded', async () => { + const container = document.getElementById('container'); + const progress = document.getElementById('progress'); + try { + const res = await chrome.runtime.sendMessage({ type: 'getReviewTasks' }); + if (res.error) { + container.innerHTML = `
${escapeHtml(res.error)}
`; + return; + } + items = res.review || []; + render(); + } + catch (e) { + container.innerHTML = `
${escapeHtml(e.message)}
`; + } + function render() { + const remaining = items.filter(i => !doneIds.has(i.task.id)); + progress.textContent = `${items.length - remaining.length} / ${items.length} reviewed`; + if (remaining.length === 0) { + container.innerHTML = ` +
+

All caught up!

+

No more decayed tasks to review. Come back next week.

+
+ `; + chrome.runtime.sendMessage({ type: 'setLastReviewDate', date: new Date().toISOString() }).catch(() => { }); + return; + } + container.innerHTML = ''; + for (const item of remaining) { + container.appendChild(renderCard(item)); + } + } + function renderCard(item) { + const card = document.createElement('div'); + card.className = 'card'; + card.id = `card-${item.task.id}`; + const pillClass = item.score.score >= 70 ? 'score-hot' : item.score.score >= 40 ? 'score-warm' : 'score-cold'; + card.innerHTML = ` +
+
${escapeHtml(item.task.title)}
+
${item.score.score} decay
+
+
+ ${escapeHtml(item.task.source)} + ${item.task.projectName ? `Project: ${escapeHtml(item.task.projectName)}` : ''} + ${item.task.labels.length > 0 ? `Labels: ${escapeHtml(item.task.labels.join(', '))}` : ''} + Age: ${Math.round(item.score.ageDays)} days +
+
${escapeHtml(item.score.explanation)}
+
+ + + + Open +
+ `; + card.querySelectorAll('button[data-action]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const target = e.currentTarget; + const action = target.getAttribute('data-action'); + const id = target.getAttribute('data-id'); + target.textContent = '...'; + try { + if (action === 'complete') { + await chrome.runtime.sendMessage({ type: 'complete', taskId: id }); + } + else if (action === 'delete') { + await chrome.runtime.sendMessage({ type: 'delete', taskId: id }); + } + else if (action === 'dismiss') { + await chrome.runtime.sendMessage({ type: 'dismiss', taskId: id }); + } + doneIds.add(id); + const c = document.getElementById(`card-${id}`); + if (c) { + c.classList.add('done'); + } + setTimeout(render, 300); + } + catch (err) { + target.textContent = 'Error'; + } + }); + }); + return card; + } + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +}); +export {}; diff --git a/lib/scoring.js b/lib/scoring.js new file mode 100644 index 0000000..2d9305b --- /dev/null +++ b/lib/scoring.js @@ -0,0 +1,72 @@ +export function scoreDecay(task, settings, allTasks) { + const now = Date.now(); + const ageMs = now - task.createdAt.getTime(); + const ageDays = ageMs / (1000 * 60 * 60 * 24); + // Priority weight: 1 = lowest, 4 = highest; invert so low-priority tasks decay faster + const priorityWeight = (5 - task.priority) * 15; // 15-60 range + // Project activity: if many tasks in same project are completed recently, project is active + const projectTasks = allTasks.filter(t => t.projectId === task.projectId && t.projectId !== null); + const recentProjectActivity = projectTasks.filter(t => t.completedAt && (now - t.completedAt.getTime()) < 30 * 24 * 60 * 60 * 1000).length; + const projectActivity = Math.min(30, recentProjectActivity * 5); // 0-30 + // Label density: more labels = more mental overhead = higher decay + const labelDensity = task.labels.length * 8; + // Due urgency: if overdue, add urgency; if far future, reduce decay + let dueUrgency = 0; + if (task.dueDate) { + const dueMs = task.dueDate.getTime() - now; + if (dueMs < 0) { + dueUrgency = 20; // overdue + } + else if (dueMs < 7 * 24 * 60 * 60 * 1000) { + dueUrgency = -10; // due soon, reduce decay + } + } + // Auto-archive: if older than autoArchiveDays, cap at 100 + let score = Math.round((Math.min(ageDays, settings.autoArchiveDays) / settings.autoArchiveDays) * 50 + + priorityWeight + + projectActivity + + labelDensity + + dueUrgency); + score = Math.max(0, Math.min(100, score)); + let explanation = `Age: ${Math.round(ageDays)} days`; + if (priorityWeight > 30) + explanation += `; Low priority (${task.priority})`; + if (projectActivity > 10) + explanation += `; Active project`; + if (labelDensity > 0) + explanation += `; ${task.labels.length} labels`; + if (dueUrgency > 0) + explanation += `; Overdue`; + if (dueUrgency < 0) + explanation += `; Due soon`; + return { + score, + ageDays: Math.round(ageDays * 10) / 10, + priorityWeight, + projectActivity, + labelDensity, + dueUrgency, + explanation, + }; +} +export function getReviewTasks(tasks, settings) { + const scored = tasks + .filter(t => !t.completedAt) + .filter(t => { + const ageDays = (Date.now() - t.createdAt.getTime()) / (1000 * 60 * 60 * 24); + return ageDays >= settings.decayThresholdDays; + }) + .filter(t => !settings.dismissedTasks.includes(t.id)) + .filter(t => !settings.archivedTasks.includes(t.id)) + .map(t => ({ task: t, score: scoreDecay(t, settings, tasks) })) + .sort((a, b) => b.score.score - a.score.score); + return scored.slice(0, settings.maxTasksPerReview); +} +export function getAutoArchiveCandidates(tasks, settings) { + return tasks.filter(t => { + if (t.completedAt) + return false; + const ageDays = (Date.now() - t.createdAt.getTime()) / (1000 * 60 * 60 * 24); + return ageDays >= settings.autoArchiveDays && !settings.archivedTasks.includes(t.id); + }); +} diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..627704f --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,56 @@ +import { DEFAULT_SETTINGS } from './types.js'; +const SETTINGS_KEY = 'taskdecay_settings_v1'; +const TASKS_KEY = 'taskdecay_tasks_v1'; +const SYNC_KEY = 'taskdecay_last_sync_v1'; +export async function getSettings() { + const result = await chrome.storage.local.get([SETTINGS_KEY]); + const stored = result[SETTINGS_KEY]; + if (!stored) + return JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); + const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored; + return { ...DEFAULT_SETTINGS, ...parsed }; +} +export async function setSettings(settings) { + await chrome.storage.local.set({ [SETTINGS_KEY]: JSON.stringify(settings) }); +} +export async function getStoredTasks() { + const result = await chrome.storage.local.get([TASKS_KEY]); + const stored = result[TASKS_KEY]; + if (!stored) + return []; + const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored; + return parsed.map((t) => ({ + ...t, + createdAt: new Date(t.createdAt), + updatedAt: new Date(t.updatedAt), + completedAt: t.completedAt ? new Date(t.completedAt) : null, + dueDate: t.dueDate ? new Date(t.dueDate) : null, + })); +} +export async function setStoredTasks(tasks) { + await chrome.storage.local.set({ [TASKS_KEY]: JSON.stringify(tasks) }); +} +export async function getLastSync() { + const result = await chrome.storage.local.get([SYNC_KEY]); + return result[SYNC_KEY] || null; +} +export async function setLastSync(date) { + await chrome.storage.local.set({ [SYNC_KEY]: date }); +} +export async function addDismissedTask(taskId) { + const settings = await getSettings(); + if (!settings.dismissedTasks.includes(taskId)) { + settings.dismissedTasks.push(taskId); + await setSettings(settings); + } +} +export async function addArchivedTask(taskId) { + const settings = await getSettings(); + if (!settings.archivedTasks.includes(taskId)) { + settings.archivedTasks.push(taskId); + await setSettings(settings); + } +} +export async function clearAllTasks() { + await chrome.storage.local.remove([TASKS_KEY, SYNC_KEY]); +} diff --git a/lib/types.js b/lib/types.js new file mode 100644 index 0000000..f8981ac --- /dev/null +++ b/lib/types.js @@ -0,0 +1,14 @@ +export const DEFAULT_SETTINGS = { + providers: { + todoist: { enabled: false, token: '', lastSync: null, projectIds: null }, + ticktick: { enabled: false, token: '', lastSync: null, projectIds: null }, + notion: { enabled: false, token: '', lastSync: null, projectIds: null }, + }, + decayThresholdDays: 14, + reviewIntervalDays: 7, + maxTasksPerReview: 20, + autoArchiveDays: 90, + lastReviewDate: null, + dismissedTasks: [], + archivedTasks: [], +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..268a861 --- /dev/null +++ b/manifest.json @@ -0,0 +1,44 @@ +{ + "manifest_version": 3, + "name": "TaskDecay", + "version": "1.0.0", + "description": "Smart Task Review & Archive — surface buried tasks, score by age + priority, and clean your backlog with one click.", + "permissions": [ + "storage", + "activeTab", + "alarms" + ], + "host_permissions": [ + "https://api.todoist.com/*", + "https://api.ticktick.com/*", + "https://api.notion.com/*", + "https://todoist.com/*", + "https://ticktick.com/*", + "https://notion.so/*" + ], + "background": { + "service_worker": "src/background.ts" + }, + "action": { + "default_popup": "src/popup.html", + "default_icon": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + } + }, + "options_page": "src/options.html", + "content_scripts": [ + { + "matches": ["https://todoist.com/*", "https://ticktick.com/*", "https://www.notion.so/*", "https://notion.so/*"], + "js": ["src/content.ts"], + "css": ["src/content.css"], + "run_at": "document_idle" + } + ], + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e88fe61 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2003 @@ +{ + "name": "taskdecay", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "taskdecay", + "version": "1.0.0", + "dependencies": { + "tsx": "^4.22.4" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.6.1.tgz", + "integrity": "sha512-0RmYlUtQGvHXCz3B/FW7P5j+RBZUc4mk3GIRPXCx5a52N8AQ0e35GvE+Pcap/TKuI09PJgcRTdXeBOavtLVyYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "node-html-parser": "^7.0.2", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.80.0", + "rxjs": "7.5.7", + "tinyglobby": "^0.2.15" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce0286f --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "taskdecay", + "version": "1.0.0", + "description": "Smart Task Review & Archive for Buried Tasks — Chrome Extension", + "type": "module", + "scripts": { + "build": "vite build", + "test": "tsx --test src/scoring.test.js" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "typescript": "^5.7.0", + "vite": "^6.0.0" + }, + "dependencies": { + "tsx": "^4.22.4" + } +} diff --git a/src/apis/index.ts b/src/apis/index.ts new file mode 100644 index 0000000..ab3dfad --- /dev/null +++ b/src/apis/index.ts @@ -0,0 +1,69 @@ +import { Task, ProviderConfig } from '../types.js'; +import { fetchTodoistTasks, closeTodoistTask, deleteTodoistTask } from './todoist.js'; +import { fetchTickTickTasks, closeTickTickTask, deleteTickTickTask } from './ticktick.js'; +import { fetchNotionTasks, archiveNotionTask, deleteNotionTask } from './notion.js'; + +export async function fetchAllTasks(providers: { + todoist: ProviderConfig; + ticktick: ProviderConfig; + notion: ProviderConfig; +}): Promise { + const all: Task[] = []; + const errors: string[] = []; + + if (providers.todoist.enabled && providers.todoist.token) { + try { + const tasks = await fetchTodoistTasks(providers.todoist); + all.push(...tasks); + } catch (e) { + errors.push(`Todoist: ${(e as Error).message}`); + } + } + + if (providers.ticktick.enabled && providers.ticktick.token) { + try { + const tasks = await fetchTickTickTasks(providers.ticktick); + all.push(...tasks); + } catch (e) { + errors.push(`TickTick: ${(e as Error).message}`); + } + } + + if (providers.notion.enabled && providers.notion.token) { + try { + const tasks = await fetchNotionTasks(providers.notion); + all.push(...tasks); + } catch (e) { + errors.push(`Notion: ${(e as Error).message}`); + } + } + + if (errors.length > 0 && all.length === 0) { + throw new Error(errors.join('; ')); + } + + return all; +} + +export async function completeTask(task: Task, token: string): Promise { + if (task.source === 'todoist') await closeTodoistTask(task.id, token); + else if (task.source === 'ticktick') await closeTickTickTask(task.id, token); + else if (task.source === 'notion') await archiveNotionTask(task.id, token); +} + +export async function deleteTask(task: Task, token: string): Promise { + if (task.source === 'todoist') await deleteTodoistTask(task.id, token); + else if (task.source === 'ticktick') await deleteTickTickTask(task.id, token); + else if (task.source === 'notion') await deleteNotionTask(task.id, token); +} + +export function getTokenForSource(source: string, providers: { + todoist: ProviderConfig; + ticktick: ProviderConfig; + notion: ProviderConfig; +}): string { + if (source === 'todoist') return providers.todoist.token; + if (source === 'ticktick') return providers.ticktick.token; + if (source === 'notion') return providers.notion.token; + return ''; +} diff --git a/src/apis/notion.ts b/src/apis/notion.ts new file mode 100644 index 0000000..6676962 --- /dev/null +++ b/src/apis/notion.ts @@ -0,0 +1,99 @@ +import { Task, ProviderConfig } from '../types.js'; + +// Notion API — queries a database for tasks +export async function fetchNotionTasks(config: ProviderConfig): Promise { + if (!config.token) throw new Error('Notion token not configured'); + if (!config.projectIds || config.projectIds.length === 0) { + throw new Error('Notion database ID not configured. Set a database ID in options.'); + } + + const headers = { + 'Authorization': `Bearer ${config.token}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }; + + const allTasks: Task[] = []; + for (const dbId of config.projectIds) { + const res = await fetch(`https://api.notion.com/v1/databases/${dbId}/query`, { + method: 'POST', + headers, + body: JSON.stringify({ page_size: 100 }), + }); + if (!res.ok) { + console.warn(`Notion DB query failed for ${dbId}: ${res.status}`); + continue; + } + const data = await res.json(); + const results = data.results || []; + + for (const page of results) { + const props = page.properties || {}; + const titleProp = Object.values(props).find((p: any) => p.type === 'title') as any; + const title = titleProp?.title?.[0]?.plain_text || 'Untitled'; + + const statusProp = Object.values(props).find((p: any) => + p.type === 'status' || p.type === 'select' || p.type === 'checkbox' + ) as any; + let done = false; + if (statusProp?.type === 'checkbox') done = statusProp.checkbox; + else if (statusProp?.type === 'status') done = statusProp.status?.name === 'Done'; + else if (statusProp?.type === 'select') done = statusProp.select?.name === 'Done'; + + if (done) continue; + + const dateProp = Object.values(props).find((p: any) => p.type === 'date') as any; + const dueDate = dateProp?.date?.start ? new Date(dateProp.date.start) : null; + + const multiSelect = Object.values(props).filter((p: any) => p.type === 'multi_select') as any[]; + const labels = multiSelect.flatMap((p: any) => p.multi_select?.map((s: any) => s.name) || []); + + const created = page.created_time ? new Date(page.created_time) : new Date(); + const updated = page.last_edited_time ? new Date(page.last_edited_time) : created; + + allTasks.push({ + id: `notion-${page.id}`, + title, + priority: 1, + createdAt: created, + updatedAt: updated, + completedAt: null, + dueDate, + projectId: `notion-${dbId}`, + projectName: dbId.slice(0, 8), + labels, + url: page.url, + source: 'notion', + raw: page, + }); + } + } + + return allTasks; +} + +export async function archiveNotionTask(taskId: string, token: string): Promise { + const realId = taskId.replace('notion-', ''); + const res = await fetch(`https://api.notion.com/v1/pages/${realId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ archived: true }), + }); + if (!res.ok) throw new Error(`Failed to archive Notion task: ${res.status}`); +} + +export async function deleteNotionTask(taskId: string, token: string): Promise { + const realId = taskId.replace('notion-', ''); + const res = await fetch(`https://api.notion.com/v1/blocks/${realId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + 'Notion-Version': '2022-06-28', + }, + }); + if (!res.ok) throw new Error(`Failed to delete Notion task: ${res.status}`); +} diff --git a/src/apis/ticktick.ts b/src/apis/ticktick.ts new file mode 100644 index 0000000..d9374bd --- /dev/null +++ b/src/apis/ticktick.ts @@ -0,0 +1,63 @@ +import { Task, ProviderConfig } from '../types.js'; + +// TickTick API v2 — uses login token or OAuth access token +export async function fetchTickTickTasks(config: ProviderConfig): Promise { + if (!config.token) throw new Error('TickTick token not configured'); + + const headers = { + 'Authorization': `Bearer ${config.token}`, + 'Content-Type': 'application/json', + }; + + // Get all projects first + const projectsRes = await fetch('https://api.ticktick.com/open/v1/project', { headers }); + const projects = projectsRes.ok ? await projectsRes.json() : []; + const projectList = Array.isArray(projects) ? projects : (projects || []); + + const allTasks: Task[] = []; + for (const proj of projectList) { + const pid = proj.id; + const tasksRes = await fetch(`https://api.ticktick.com/open/v1/project/${pid}/task`, { headers }); + if (!tasksRes.ok) continue; + const tasks = await tasksRes.json(); + const taskList = Array.isArray(tasks) ? tasks : (tasks || []); + for (const t of taskList) { + if (t.status === 2) continue; // completed + allTasks.push({ + id: `ticktick-${t.id}`, + title: t.title, + priority: (t.priority || 0) + 1, + createdAt: new Date(t.createdTime || Date.now()), + updatedAt: new Date(t.modifiedTime || t.createdTime || Date.now()), + completedAt: null, + dueDate: t.dueDate ? new Date(t.dueDate) : null, + projectId: `ticktick-${pid}`, + projectName: proj.name || null, + labels: t.tags || [], + url: `https://ticktick.com/webapp/#p/${pid}/task/${t.id}`, + source: 'ticktick', + raw: t, + }); + } + } + + return allTasks; +} + +export async function closeTickTickTask(taskId: string, token: string): Promise { + const realId = taskId.replace('ticktick-', ''); + const res = await fetch(`https://api.ticktick.com/open/v1/task/${realId}/complete`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) throw new Error(`Failed to close TickTick task: ${res.status}`); +} + +export async function deleteTickTickTask(taskId: string, token: string): Promise { + const realId = taskId.replace('ticktick-', ''); + const res = await fetch(`https://api.ticktick.com/open/v1/task/${realId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) throw new Error(`Failed to delete TickTick task: ${res.status}`); +} diff --git a/src/apis/todoist.ts b/src/apis/todoist.ts new file mode 100644 index 0000000..4f27c37 --- /dev/null +++ b/src/apis/todoist.ts @@ -0,0 +1,56 @@ +import { Task, ProviderConfig } from './types.js'; + +export async function fetchTodoistTasks(config: ProviderConfig): Promise { + if (!config.token) throw new Error('Todoist token not configured'); + + const headers = { + 'Authorization': `Bearer ${config.token}`, + 'Content-Type': 'application/json', + }; + + // Fetch all active tasks + const tasksRes = await fetch('https://api.todoist.com/rest/v2/tasks', { headers }); + if (!tasksRes.ok) { + throw new Error(`Todoist API error: ${tasksRes.status} ${tasksRes.statusText}`); + } + const tasks = await tasksRes.json(); + + // Fetch projects for names + const projectsRes = await fetch('https://api.todoist.com/rest/v2/projects', { headers }); + const projects = projectsRes.ok ? await projectsRes.json() : []; + const projectMap = new Map(projects.map((p: any) => [p.id, p.name])); + + return tasks.map((t: any): Task => ({ + id: `todoist-${t.id}`, + title: t.content, + priority: t.priority || 1, + createdAt: new Date(t.created_at || Date.now()), + updatedAt: new Date(t.updated_at || t.created_at || Date.now()), + completedAt: null, + dueDate: t.due?.date ? new Date(t.due.date) : null, + projectId: t.project_id ? `todoist-${t.project_id}` : null, + projectName: projectMap.get(t.project_id) || null, + labels: t.labels || [], + url: `https://todoist.com/app/task/${t.id}`, + source: 'todoist', + raw: t, + })); +} + +export async function closeTodoistTask(taskId: string, token: string): Promise { + const realId = taskId.replace('todoist-', ''); + const res = await fetch(`https://api.todoist.com/rest/v2/tasks/${realId}/close`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) throw new Error(`Failed to close Todoist task: ${res.status}`); +} + +export async function deleteTodoistTask(taskId: string, token: string): Promise { + const realId = taskId.replace('todoist-', ''); + const res = await fetch(`https://api.todoist.com/rest/v2/tasks/${realId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) throw new Error(`Failed to delete Todoist task: ${res.status}`); +} diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..d92ee36 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,146 @@ +import { fetchAllTasks, getTokenForSource, completeTask, deleteTask } from './apis/index.js'; +import { getSettings, setSettings, getStoredTasks, setStoredTasks, setLastSync, addDismissedTask, addArchivedTask } from './storage.js'; +import { getReviewTasks, getAutoArchiveCandidates } from './scoring.js'; + +// Background service worker for TaskDecay +// Handles periodic sync, badge count, and alarm-based review reminders + +chrome.runtime.onInstalled.addListener(() => { + chrome.alarms.create('taskdecay-sync', { periodInMinutes: 60 }); + chrome.alarms.create('taskdecay-review-reminder', { periodInMinutes: 60 * 24 }); + updateBadge(); +}); + +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === 'taskdecay-sync') { + await syncTasks(); + } else if (alarm.name === 'taskdecay-review-reminder') { + await checkReviewReminder(); + } +}); + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + const msg = message as any; + if (msg.type === 'sync') { + syncTasks().then(sendResponse).catch(e => sendResponse({ error: (e as Error).message })); + return true; + } + if (msg.type === 'getReviewTasks') { + getReviewData().then(sendResponse).catch(e => sendResponse({ error: (e as Error).message })); + return true; + } + if (msg.type === 'dismiss') { + addDismissedTask(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message })); + return true; + } + if (msg.type === 'archive') { + archiveTask(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message })); + return true; + } + if (msg.type === 'complete') { + completeTaskById(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message })); + return true; + } + if (msg.type === 'delete') { + deleteTaskById(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message })); + return true; + } + if (msg.type === 'openReview') { + chrome.tabs.create({ url: chrome.runtime.getURL('src/review.html') }); + sendResponse({ ok: true }); + return true; + } +}); + +async function syncTasks() { + const settings = await getSettings(); + const tasks = await fetchAllTasks(settings.providers); + await setStoredTasks(tasks); + await setLastSync(new Date().toISOString()); + await updateBadge(); + return { count: tasks.length, syncedAt: new Date().toISOString() }; +} + +async function getReviewData() { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const review = getReviewTasks(tasks, settings); + const autoArchive = getAutoArchiveCandidates(tasks, settings); + return { review, autoArchiveCount: autoArchive.length, total: tasks.length }; +} + +async function archiveTask(taskId: string) { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const task = tasks.find(t => t.id === taskId); + if (!task) throw new Error('Task not found'); + const token = getTokenForSource(task.source, settings.providers); + if (!token) throw new Error('No token for source'); + await completeTask(task, token); + await addArchivedTask(taskId); + await updateBadge(); +} + +async function completeTaskById(taskId: string) { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const task = tasks.find(t => t.id === taskId); + if (!task) throw new Error('Task not found'); + const token = getTokenForSource(task.source, settings.providers); + if (!token) throw new Error('No token for source'); + await completeTask(task, token); + await addArchivedTask(taskId); + await updateBadge(); +} + +async function deleteTaskById(taskId: string) { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const task = tasks.find(t => t.id === taskId); + if (!task) throw new Error('Task not found'); + const token = getTokenForSource(task.source, settings.providers); + if (!token) throw new Error('No token for source'); + await deleteTask(task, token); + await addArchivedTask(taskId); + await updateBadge(); +} + +async function checkReviewReminder() { + const settings = await getSettings(); + if (!settings.lastReviewDate) return; + const last = new Date(settings.lastReviewDate); + const now = new Date(); + const daysSince = (now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24); + if (daysSince >= settings.reviewIntervalDays) { + const tasks = await getStoredTasks(); + const review = getReviewTasks(tasks, settings); + if (review.length > 0) { + chrome.notifications.create('review-reminder', { + type: 'basic', + iconUrl: 'icon128.png', + title: 'TaskDecay Review Ready', + message: `${review.length} tasks need your attention. Open the review session to clean your backlog.`, + buttons: [{ title: 'Start Review' }], + }); + } + } +} + +async function updateBadge() { + try { + const settings = await getSettings(); + const tasks = await getStoredTasks(); + const review = getReviewTasks(tasks, settings); + const count = review.length; + chrome.action.setBadgeText({ text: count > 0 ? String(count) : '' }); + chrome.action.setBadgeBackgroundColor({ color: '#e74c3c' }); + } catch { + chrome.action.setBadgeText({ text: '' }); + } +} + +chrome.notifications.onButtonClicked.addListener((notificationId) => { + if (notificationId === 'review-reminder') { + chrome.tabs.create({ url: chrome.runtime.getURL('src/review.html') }); + } +}); diff --git a/src/content.css b/src/content.css new file mode 100644 index 0000000..60432fd --- /dev/null +++ b/src/content.css @@ -0,0 +1,17 @@ +.taskdecay-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + font-family: system-ui, -apple-system, sans-serif; + margin-left: 8px; + cursor: pointer; + transition: opacity 0.2s; +} +.taskdecay-badge:hover { opacity: 0.8; } +.taskdecay-badge-warm { background: #f39c12; color: #fff; } +.taskdecay-badge-hot { background: #e74c3c; color: #fff; } +.taskdecay-badge-cold { background: #3498db; color: #fff; } diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..be90acd --- /dev/null +++ b/src/content.ts @@ -0,0 +1,240 @@ +const BADGE_CLASS = 'taskdecay-injected-badge'; + +interface DecayedTaskInfo { + taskId: string; + score: number; + ageDays: number; + explanation: string; +} + +function detectTaskManager(): 'todoist' | 'ticktick' | 'notion' | null { + const host = window.location.host; + if (host.includes('todoist.com')) return 'todoist'; + if (host.includes('ticktick.com')) return 'ticktick'; + if (host.includes('notion.so')) return 'notion'; + return null; +} + +function extractTaskIdFromUrl(): string | null { + const url = window.location.href; + const platform = detectTaskManager(); + if (platform === 'todoist') { + const m = url.match(/task\/(\d+)/); + if (m) return `todoist-${m[1]}`; + } + if (platform === 'ticktick') { + const m = url.match(/task\/(\w+)/); + if (m) return `ticktick-${m[1]}`; + } + if (platform === 'notion') { + const m = url.match(/([a-f0-9]{32})/); + if (m) return `notion-${m[1]}`; + } + return null; +} + +function injectStyles() { + if (document.getElementById('taskdecay-styles')) return; + const style = document.createElement('style'); + style.id = 'taskdecay-styles'; + style.textContent = ` + .taskdecay-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + font-family: system-ui, -apple-system, sans-serif; + margin-left: 8px; + cursor: pointer; + transition: opacity 0.2s; + } + .taskdecay-badge:hover { opacity: 0.8; } + .taskdecay-badge-warm { background: #f39c12; color: #fff; } + .taskdecay-badge-hot { background: #e74c3c; color: #fff; } + .taskdecay-badge-cold { background: #3498db; color: #fff; } + .taskdecay-toast { + position: fixed; + bottom: 20px; + right: 20px; + background: #1a1a1a; + color: #fff; + padding: 12px 16px; + border-radius: 8px; + font-family: system-ui, -apple-system, sans-serif; + font-size: 13px; + z-index: 999999; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + max-width: 320px; + line-height: 1.4; + } + .taskdecay-toast a { + color: #58a6ff; + text-decoration: none; + display: block; + margin-top: 8px; + } + .taskdecay-toast button { + background: #e74c3c; + border: none; + color: #fff; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + margin-top: 8px; + font-size: 12px; + } + `; + document.head.appendChild(style); +} + +function showToast(message: string, action?: { label: string; onClick: () => void }) { + const existing = document.querySelector('.taskdecay-toast'); + if (existing) existing.remove(); + const toast = document.createElement('div'); + toast.className = 'taskdecay-toast'; + toast.textContent = message; + if (action) { + const btn = document.createElement('button'); + btn.textContent = action.label; + btn.onclick = () => { action.onClick(); toast.remove(); }; + toast.appendChild(btn); + } + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 8000); +} + +async function getTaskDecayInfo(taskId: string): Promise { + return new Promise((resolve) => { + chrome.runtime.sendMessage({ type: 'getReviewTasks' }, (response) => { + if (!response || response.error) { resolve(null); return; } + const match = response.review?.find((r: any) => r.task.id === taskId); + if (match) { + resolve({ + taskId: match.task.id, + score: match.score.score, + ageDays: match.score.ageDays, + explanation: match.score.explanation, + }); + } else { + resolve(null); + } + }); + }); +} + +function addBadgeToTodoist(info: DecayedTaskInfo) { + const selectors = [ + '[data-testid="task-details-modal"] [data-testid="task-title"]', + '[data-testid="task-details-modal"] h1', + '.task_content', + '[data-item-id] .task_content', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && !el.parentElement?.querySelector('.taskdecay-badge')) { + const badge = document.createElement('span'); + badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`; + badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`; + badge.title = info.explanation; + badge.onclick = (e) => { + e.stopPropagation(); + chrome.runtime.sendMessage({ type: 'openReview' }); + }; + el.parentElement?.insertBefore(badge, el.nextSibling); + break; + } + } +} + +function addBadgeToTickTick(info: DecayedTaskInfo) { + const selectors = [ + '.task-title', + '.task-title-text', + '[class*="taskTitle"]', + 'h1', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && !el.parentElement?.querySelector('.taskdecay-badge')) { + const badge = document.createElement('span'); + badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`; + badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`; + badge.title = info.explanation; + badge.onclick = (e) => { + e.stopPropagation(); + chrome.runtime.sendMessage({ type: 'openReview' }); + }; + el.parentElement?.insertBefore(badge, el.nextSibling); + break; + } + } +} + +function addBadgeToNotion(info: DecayedTaskInfo) { + const selectors = [ + '[data-testid="page-title"]', + '.notion-page-block', + 'h1', + '[class*="title" i]', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && !el.parentElement?.querySelector('.taskdecay-badge')) { + const badge = document.createElement('span'); + badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`; + badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`; + badge.title = info.explanation; + badge.onclick = (e) => { + e.stopPropagation(); + chrome.runtime.sendMessage({ type: 'openReview' }); + }; + el.parentElement?.insertBefore(badge, el.nextSibling); + break; + } + } +} + +async function injectBadge() { + const taskId = extractTaskIdFromUrl(); + if (!taskId) return; + const info = await getTaskDecayInfo(taskId); + if (!info) return; + + const platform = detectTaskManager(); + if (platform === 'todoist') addBadgeToTodoist(info); + else if (platform === 'ticktick') addBadgeToTickTick(info); + else if (platform === 'notion') addBadgeToNotion(info); +} + +function init() { + injectStyles(); + injectBadge(); + + // Re-inject on URL changes (SPA navigation) + let lastUrl = window.location.href; + const observer = new MutationObserver(() => { + if (window.location.href !== lastUrl) { + lastUrl = window.location.href; + setTimeout(injectBadge, 800); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} + +chrome.runtime.onMessage.addListener((message) => { + const msg = message as any; + if (msg.type === 'showToast') { + showToast(msg.text, msg.action ? { label: msg.action.label, onClick: () => { + chrome.runtime.sendMessage({ type: msg.action.type }); + }} : undefined); + } +}); diff --git a/src/options.html b/src/options.html new file mode 100644 index 0000000..3c7015f --- /dev/null +++ b/src/options.html @@ -0,0 +1,100 @@ + + + + + + TaskDecay Settings + + + +

TaskDecay Settings

+
+

Providers

+
+ + +
+ + + + +
+ + +
+ + + + +
+ + +
+ + + + + +
+ +
+

Behavior

+ + + + + + + + +
+ + + + +
+ + + + diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..c1acbfc --- /dev/null +++ b/src/options.ts @@ -0,0 +1,87 @@ +import { getSettings, setSettings, clearAllTasks } from './storage.js'; +import { fetchAllTasks } from './apis/index.js'; + +document.addEventListener('DOMContentLoaded', async () => { + const el = (id: string) => document.getElementById(id) as HTMLInputElement; + + const settings = await getSettings(); + el('todoistEnabled').checked = settings.providers.todoist.enabled; + el('todoistToken').value = settings.providers.todoist.token; + el('ticktickEnabled').checked = settings.providers.ticktick.enabled; + el('ticktickToken').value = settings.providers.ticktick.token; + el('notionEnabled').checked = settings.providers.notion.enabled; + el('notionToken').value = settings.providers.notion.token; + el('notionDbs').value = (settings.providers.notion.projectIds || []).join(', '); + el('decayThreshold').value = String(settings.decayThresholdDays); + el('reviewInterval').value = String(settings.reviewIntervalDays); + el('maxTasks').value = String(settings.maxTasksPerReview); + el('autoArchive').value = String(settings.autoArchiveDays); + + document.getElementById('saveBtn')?.addEventListener('click', async () => { + const status = document.getElementById('status')!; + status.textContent = ''; + status.className = 'status'; + + const newSettings = { + ...settings, + providers: { + todoist: { + enabled: el('todoistEnabled').checked, + token: el('todoistToken').value.trim(), + lastSync: settings.providers.todoist.lastSync, + projectIds: null, + }, + ticktick: { + enabled: el('ticktickEnabled').checked, + token: el('ticktickToken').value.trim(), + lastSync: settings.providers.ticktick.lastSync, + projectIds: null, + }, + notion: { + enabled: el('notionEnabled').checked, + token: el('notionToken').value.trim(), + lastSync: settings.providers.notion.lastSync, + projectIds: el('notionDbs').value.split(',').map(s => s.trim()).filter(Boolean), + }, + }, + decayThresholdDays: parseInt(el('decayThreshold').value) || 14, + reviewIntervalDays: parseInt(el('reviewInterval').value) || 7, + maxTasksPerReview: parseInt(el('maxTasks').value) || 20, + autoArchiveDays: parseInt(el('autoArchive').value) || 90, + }; + + await setSettings(newSettings); + status.textContent = 'Settings saved.'; + status.classList.add('success'); + }); + + document.getElementById('testBtn')?.addEventListener('click', async () => { + const status = document.getElementById('status')!; + status.textContent = 'Testing...'; + status.className = 'status'; + try { + const testSettings = { + ...settings, + providers: { + todoist: { enabled: el('todoistEnabled').checked, token: el('todoistToken').value.trim(), lastSync: null, projectIds: null }, + ticktick: { enabled: el('ticktickEnabled').checked, token: el('ticktickToken').value.trim(), lastSync: null, projectIds: null }, + notion: { enabled: el('notionEnabled').checked, token: el('notionToken').value.trim(), lastSync: null, projectIds: el('notionDbs').value.split(',').map(s => s.trim()).filter(Boolean) }, + }, + }; + const tasks = await fetchAllTasks(testSettings.providers); + status.textContent = `Connection OK! Found ${tasks.length} tasks across enabled providers.`; + status.classList.add('success'); + } catch (e) { + status.textContent = `Error: ${(e as Error).message}`; + status.classList.add('error'); + } + }); + + document.getElementById('clearBtn')?.addEventListener('click', async () => { + if (!confirm('Clear all stored tasks and sync data?')) return; + await clearAllTasks(); + const status = document.getElementById('status')!; + status.textContent = 'All data cleared.'; + status.className = 'status success'; + }); +}); diff --git a/src/popup.html b/src/popup.html new file mode 100644 index 0000000..99d1d68 --- /dev/null +++ b/src/popup.html @@ -0,0 +1,80 @@ + + + + + + TaskDecay + + + +
+

TaskDecay

+ +
+
+
Loading...
+
+ + + diff --git a/src/popup.ts b/src/popup.ts new file mode 100644 index 0000000..a5713cd --- /dev/null +++ b/src/popup.ts @@ -0,0 +1,98 @@ +document.addEventListener('DOMContentLoaded', () => { + const main = document.getElementById('main')!; + const syncStatus = document.getElementById('syncStatus')!; + const countBadge = document.getElementById('countBadge')!; + + async function load() { + try { + const res = await chrome.runtime.sendMessage({ type: 'getReviewTasks' }); + if (res.error) { + syncStatus.textContent = ''; + renderError(res.error); + return; + } + + const review = res.review || []; + const total = res.total || 0; + const autoArchiveCount = res.autoArchiveCount || 0; + + syncStatus.textContent = `${total} total tasks synced`; + + if (review.length === 0) { + countBadge.style.display = 'none'; + renderEmpty(); + } else { + countBadge.textContent = String(review.length); + countBadge.style.display = 'inline-block'; + renderStats(review, autoArchiveCount); + } + } catch (e) { + syncStatus.textContent = ''; + renderError((e as Error).message); + } + } + + function renderEmpty() { + main.innerHTML = ` +
No decayed tasks found. Great job keeping up!
+ + Open Settings + `; + document.getElementById('syncBtn')?.addEventListener('click', doSync); + } + + function renderError(msg: string) { + main.innerHTML = ` +
${escapeHtml(msg)}
+ + Open Settings + `; + document.getElementById('syncBtn')?.addEventListener('click', doSync); + } + + function renderStats(review: any[], autoArchiveCount: number) { + const hot = review.filter((r: any) => r.score.score >= 70).length; + const warm = review.filter((r: any) => r.score.score >= 40 && r.score.score < 70).length; + const cold = review.filter((r: any) => r.score.score < 40).length; + const oldest = review.reduce((a: any, b: any) => a.score.ageDays > b.score.ageDays ? a : b); + + main.innerHTML = ` +
Hot (≥70)${hot}
+
Warm (40-69)${warm}
+
Cold (<40)${cold}
+
Oldest task${Math.round(oldest.score.ageDays)} days
+ ${autoArchiveCount > 0 ? `
Auto-archive ready${autoArchiveCount}
` : ''} + + + Open Settings + `; + document.getElementById('reviewBtn')?.addEventListener('click', () => { + chrome.runtime.sendMessage({ type: 'openReview' }); + window.close(); + }); + document.getElementById('syncBtn')?.addEventListener('click', doSync); + } + + async function doSync() { + const btn = document.getElementById('syncBtn') as HTMLButtonElement; + if (btn) btn.textContent = 'Syncing...'; + try { + const res = await chrome.runtime.sendMessage({ type: 'sync' }); + if (res.error) { + renderError(res.error); + return; + } + await load(); + } catch (e) { + renderError((e as Error).message); + } + } + + function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + load(); +}); diff --git a/src/review.html b/src/review.html new file mode 100644 index 0000000..125abdd --- /dev/null +++ b/src/review.html @@ -0,0 +1,89 @@ + + + + + + TaskDecay Review Session + + + +
+

TaskDecay Review Session

+
Loading...
+
+
+
+

Loading your backlog...

+
+
+ + + diff --git a/src/review.ts b/src/review.ts new file mode 100644 index 0000000..68c5bd7 --- /dev/null +++ b/src/review.ts @@ -0,0 +1,116 @@ +interface ReviewItem { + task: { + id: string; + title: string; + source: string; + projectName: string | null; + labels: string[]; + url: string; + }; + score: { + score: number; + ageDays: number; + explanation: string; + }; +} + +let items: ReviewItem[] = []; +let doneIds = new Set(); + +document.addEventListener('DOMContentLoaded', async () => { + const container = document.getElementById('container')!; + const progress = document.getElementById('progress')!; + + try { + const res = await chrome.runtime.sendMessage({ type: 'getReviewTasks' }); + if (res.error) { + container.innerHTML = `
${escapeHtml(res.error)}
`; + return; + } + items = res.review || []; + render(); + } catch (e) { + container.innerHTML = `
${escapeHtml((e as Error).message)}
`; + } + + function render() { + const remaining = items.filter(i => !doneIds.has(i.task.id)); + progress.textContent = `${items.length - remaining.length} / ${items.length} reviewed`; + + if (remaining.length === 0) { + container.innerHTML = ` +
+

All caught up!

+

No more decayed tasks to review. Come back next week.

+
+ `; + chrome.runtime.sendMessage({ type: 'setLastReviewDate', date: new Date().toISOString() }).catch(() => {}); + return; + } + + container.innerHTML = ''; + for (const item of remaining) { + container.appendChild(renderCard(item)); + } + } + + function renderCard(item: ReviewItem): HTMLElement { + const card = document.createElement('div'); + card.className = 'card'; + card.id = `card-${item.task.id}`; + + const pillClass = item.score.score >= 70 ? 'score-hot' : item.score.score >= 40 ? 'score-warm' : 'score-cold'; + + card.innerHTML = ` +
+
${escapeHtml(item.task.title)}
+
${item.score.score} decay
+
+
+ ${escapeHtml(item.task.source)} + ${item.task.projectName ? `Project: ${escapeHtml(item.task.projectName)}` : ''} + ${item.task.labels.length > 0 ? `Labels: ${escapeHtml(item.task.labels.join(', '))}` : ''} + Age: ${Math.round(item.score.ageDays)} days +
+
${escapeHtml(item.score.explanation)}
+
+ + + + Open +
+ `; + + card.querySelectorAll('button[data-action]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const target = e.currentTarget as HTMLElement; + const action = target.getAttribute('data-action')!; + const id = target.getAttribute('data-id')!; + target.textContent = '...'; + try { + if (action === 'complete') { + await chrome.runtime.sendMessage({ type: 'complete', taskId: id }); + } else if (action === 'delete') { + await chrome.runtime.sendMessage({ type: 'delete', taskId: id }); + } else if (action === 'dismiss') { + await chrome.runtime.sendMessage({ type: 'dismiss', taskId: id }); + } + doneIds.add(id); + const c = document.getElementById(`card-${id}`); + if (c) { c.classList.add('done'); } + setTimeout(render, 300); + } catch (err) { + target.textContent = 'Error'; + } + }); + }); + + return card; + } + + function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +}); diff --git a/src/scoring.test.js b/src/scoring.test.js new file mode 100644 index 0000000..c2c9db9 --- /dev/null +++ b/src/scoring.test.js @@ -0,0 +1,75 @@ +import { scoreDecay, getReviewTasks, getAutoArchiveCandidates } from './scoring.js'; +import assert from 'node:assert'; +import test from 'node:test'; + +function makeTask(overrides = {}) { + return { + id: 't-1', + title: 'Test task', + priority: 1, + createdAt: new Date(Date.now() - 20 * 86400000), + updatedAt: new Date(), + completedAt: null, + dueDate: null, + projectId: 'p-1', + projectName: 'Project A', + labels: [], + url: 'https://example.com', + source: 'todoist', + raw: {}, + ...overrides, + }; +} + +const settings = { + providers: { + todoist: { enabled: true, token: '', lastSync: null, projectIds: null }, + ticktick: { enabled: false, token: '', lastSync: null, projectIds: null }, + notion: { enabled: false, token: '', lastSync: null, projectIds: null }, + }, + decayThresholdDays: 14, + reviewIntervalDays: 7, + maxTasksPerReview: 20, + autoArchiveDays: 90, + lastReviewDate: null, + dismissedTasks: [], + archivedTasks: [], +}; + +test('scoreDecay gives higher score for older tasks', () => { + const old = makeTask({ createdAt: new Date(Date.now() - 60 * 86400000) }); + const young = makeTask({ createdAt: new Date(Date.now() - 5 * 86400000) }); + const all = [old, young]; + const sOld = scoreDecay(old, settings, all); + const sYoung = scoreDecay(young, settings, all); + assert(sOld.score > sYoung.score, 'older task should have higher decay score'); +}); + +test('scoreDecay caps at 100 and floors at 0', () => { + const t = makeTask({ createdAt: new Date(Date.now() - 500 * 86400000) }); + const s = scoreDecay(t, settings, [t]); + assert(s.score >= 0 && s.score <= 100, 'score should be within [0,100]'); +}); + +test('getReviewTasks filters by age threshold and sorts by score', () => { + const a = makeTask({ id: 'a', createdAt: new Date(Date.now() - 30 * 86400000), priority: 1 }); + const b = makeTask({ id: 'b', createdAt: new Date(Date.now() - 10 * 86400000), priority: 4 }); + const c = makeTask({ id: 'c', createdAt: new Date(Date.now() - 100 * 86400000), priority: 1 }); + const review = getReviewTasks([a, b, c], settings); + assert.strictEqual(review.length, 2, 'should exclude task under threshold'); + assert.strictEqual(review[0].task.id, 'c', 'highest decay should be first'); +}); + +test('getReviewTasks excludes completed and dismissed', () => { + const t = makeTask({ id: 'x', completedAt: new Date() }); + const review = getReviewTasks([t], settings); + assert.strictEqual(review.length, 0, 'completed should be excluded'); +}); + +test('getAutoArchiveCandidates selects old unarchived tasks', () => { + const old = makeTask({ id: 'old', createdAt: new Date(Date.now() - 100 * 86400000) }); + const young = makeTask({ id: 'young', createdAt: new Date(Date.now() - 10 * 86400000) }); + const cand = getAutoArchiveCandidates([old, young], settings); + assert.strictEqual(cand.length, 1); + assert.strictEqual(cand[0].id, 'old'); +}); diff --git a/src/scoring.ts b/src/scoring.ts new file mode 100644 index 0000000..aef0691 --- /dev/null +++ b/src/scoring.ts @@ -0,0 +1,88 @@ +import { Task, DecayScore, Settings } from './types.js'; + +export function scoreDecay(task: Task, settings: Settings, allTasks: Task[]): DecayScore { + const now = Date.now(); + const ageMs = now - task.createdAt.getTime(); + const ageDays = ageMs / (1000 * 60 * 60 * 24); + + // Priority weight: 1 = lowest, 4 = highest; invert so low-priority tasks decay faster + const priorityWeight = (5 - task.priority) * 15; // 15-60 range + + // Project activity: if many tasks in same project are completed recently, project is active + const projectTasks = allTasks.filter(t => t.projectId === task.projectId && t.projectId !== null); + const recentProjectActivity = projectTasks.filter( + t => t.completedAt && (now - t.completedAt.getTime()) < 30 * 24 * 60 * 60 * 1000 + ).length; + const projectActivity = Math.min(30, recentProjectActivity * 5); // 0-30 + + // Label density: more labels = more mental overhead = higher decay + const labelDensity = task.labels.length * 8; + + // Due urgency: if overdue, add urgency; if far future, reduce decay + let dueUrgency = 0; + if (task.dueDate) { + const dueMs = task.dueDate.getTime() - now; + if (dueMs < 0) { + dueUrgency = 20; // overdue + } else if (dueMs < 7 * 24 * 60 * 60 * 1000) { + dueUrgency = -10; // due soon, reduce decay + } + } + + // Auto-archive: if older than autoArchiveDays, cap at 100 + let score = Math.round( + (Math.min(ageDays, settings.autoArchiveDays) / settings.autoArchiveDays) * 50 + + priorityWeight + + projectActivity + + labelDensity + + dueUrgency + ); + + score = Math.max(0, Math.min(100, score)); + + let explanation = `Age: ${Math.round(ageDays)} days`; + if (priorityWeight > 30) explanation += `; Low priority (${task.priority})`; + if (projectActivity > 10) explanation += `; Active project`; + if (labelDensity > 0) explanation += `; ${task.labels.length} labels`; + if (dueUrgency > 0) explanation += `; Overdue`; + if (dueUrgency < 0) explanation += `; Due soon`; + + return { + score, + ageDays: Math.round(ageDays * 10) / 10, + priorityWeight, + projectActivity, + labelDensity, + dueUrgency, + explanation, + }; +} + +export function getReviewTasks( + tasks: Task[], + settings: Settings +): { task: Task; score: DecayScore }[] { + const scored = tasks + .filter(t => !t.completedAt) + .filter(t => { + const ageDays = (Date.now() - t.createdAt.getTime()) / (1000 * 60 * 60 * 24); + return ageDays >= settings.decayThresholdDays; + }) + .filter(t => !settings.dismissedTasks.includes(t.id)) + .filter(t => !settings.archivedTasks.includes(t.id)) + .map(t => ({ task: t, score: scoreDecay(t, settings, tasks) })) + .sort((a, b) => b.score.score - a.score.score); + + return scored.slice(0, settings.maxTasksPerReview); +} + +export function getAutoArchiveCandidates( + tasks: Task[], + settings: Settings +): Task[] { + return tasks.filter(t => { + if (t.completedAt) return false; + const ageDays = (Date.now() - t.createdAt.getTime()) / (1000 * 60 * 60 * 24); + return ageDays >= settings.autoArchiveDays && !settings.archivedTasks.includes(t.id); + }); +} diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..be3f0b8 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,64 @@ +import { Settings, DEFAULT_SETTINGS, Task } from './types.js'; + +const SETTINGS_KEY = 'taskdecay_settings_v1'; +const TASKS_KEY = 'taskdecay_tasks_v1'; +const SYNC_KEY = 'taskdecay_last_sync_v1'; + +export async function getSettings(): Promise { + const result = await chrome.storage.local.get([SETTINGS_KEY]); + const stored = result[SETTINGS_KEY]; + if (!stored) return JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); + const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored; + return { ...DEFAULT_SETTINGS, ...parsed }; +} + +export async function setSettings(settings: Settings): Promise { + await chrome.storage.local.set({ [SETTINGS_KEY]: JSON.stringify(settings) }); +} + +export async function getStoredTasks(): Promise { + const result = await chrome.storage.local.get([TASKS_KEY]); + const stored = result[TASKS_KEY]; + if (!stored) return []; + const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored; + return parsed.map((t: Task) => ({ + ...t, + createdAt: new Date(t.createdAt), + updatedAt: new Date(t.updatedAt), + completedAt: t.completedAt ? new Date(t.completedAt) : null, + dueDate: t.dueDate ? new Date(t.dueDate) : null, + })); +} + +export async function setStoredTasks(tasks: Task[]): Promise { + await chrome.storage.local.set({ [TASKS_KEY]: JSON.stringify(tasks) }); +} + +export async function getLastSync(): Promise { + const result = await chrome.storage.local.get([SYNC_KEY]); + return result[SYNC_KEY] || null; +} + +export async function setLastSync(date: string): Promise { + await chrome.storage.local.set({ [SYNC_KEY]: date }); +} + +export async function addDismissedTask(taskId: string): Promise { + const settings = await getSettings(); + if (!settings.dismissedTasks.includes(taskId)) { + settings.dismissedTasks.push(taskId); + await setSettings(settings); + } +} + +export async function addArchivedTask(taskId: string): Promise { + const settings = await getSettings(); + if (!settings.archivedTasks.includes(taskId)) { + settings.archivedTasks.push(taskId); + await setSettings(settings); + } +} + +export async function clearAllTasks(): Promise { + await chrome.storage.local.remove([TASKS_KEY, SYNC_KEY]); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2c66ef4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,62 @@ +export interface Task { + id: string; + title: string; + priority: number; // 1-4 normalized + createdAt: Date; + updatedAt: Date; + completedAt: Date | null; + dueDate: Date | null; + projectId: string | null; + projectName: string | null; + labels: string[]; + url: string; + source: 'todoist' | 'ticktick' | 'notion'; + raw: unknown; +} + +export interface DecayScore { + score: number; // 0-100, higher = more decayed/needs attention + ageDays: number; + priorityWeight: number; + projectActivity: number; + labelDensity: number; + dueUrgency: number; + explanation: string; +} + +export interface ProviderConfig { + enabled: boolean; + token: string; + lastSync: string | null; + projectIds: string[] | null; +} + +export interface Settings { + providers: { + todoist: ProviderConfig; + ticktick: ProviderConfig; + notion: ProviderConfig; + }; + decayThresholdDays: number; + reviewIntervalDays: number; + maxTasksPerReview: number; + autoArchiveDays: number; + lastReviewDate: string | null; + dismissedTasks: string[]; + archivedTasks: string[]; +} + +export const DEFAULT_SETTINGS: Settings = { + providers: { + todoist: { enabled: false, token: '', lastSync: null, projectIds: null }, + ticktick: { enabled: false, token: '', lastSync: null, projectIds: null }, + notion: { enabled: false, token: '', lastSync: null, projectIds: null }, + }, + decayThresholdDays: 14, + reviewIntervalDays: 7, + maxTasksPerReview: 20, + autoArchiveDays: 90, + lastReviewDate: null, + dismissedTasks: [], + archivedTasks: [], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ffcaaa5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "lib", + "rootDir": "src", + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "sourceMap": false + }, + "include": ["src/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..39caa34 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import { crx } from '@crxjs/vite-plugin' +import manifest from './manifest.json' assert { type: 'json' } + +export default defineConfig({ + build: { + outDir: 'dist', + emptyOutDir: true, + rollupOptions: { + input: { + popup: 'src/popup.html', + options: 'src/options.html', + review: 'src/review.html' + } + } + }, + plugins: [crx({ manifest })] +})