diff --git a/CLAUDE.md b/CLAUDE.md index 0b2dc05..07ada22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,11 +77,21 @@ npm run typecheck # tsc --noEmit 6. **녹음 UI**: 9개 웨이브 바, cos 분포 가중치, 100ms 애니메이션 ## 현재 상태 -Phase: 4 완료 -마지막 완료: Phase 4 — Ollama LLM 연동 (텍스트 다듬기, 번역, 스트리밍) -다음 작업: Phase 3.5 — 커서 위치 히스토리 팝업 또는 Phase 5 — TTS + DB +Phase: 5 완료 +마지막 완료: Phase 5 — SQLite DB + HistoryService + DictionaryService + UI 완성 +다음 작업: Phase 3.5 — 커서 위치 히스토리 팝업 또는 Phase 6 — 커스텀 명령어 차단 이슈: SoX 미설치, @nut-tree-fork/nut-js 포크 사용 +### Phase 5 구현 내용 +- DB: better-sqlite3 + drizzle-orm (history/dictionary/stats 테이블, WAL 모드) +- HistoryService: CRUD + 검색 + 통계 + 보존 정책(30일), 세션 완료 시 자동 이력 저장 +- DictionaryService: CRUD + 검색 + 사용 횟수 추적 + STT 프롬프트 힌트 +- History UI: 목록 + 검색 + 삭제 + 복사 + 페이지네이션 +- Dictionary UI: 목록 + 검색 + 추가 다이얼로그 + 삭제 +- Dashboard: 실데이터 통계 (총/오늘 세션수, 시간, 단어수, 연속일수) +- IPC: history, dictionary, stats 핸들러 + preload API +- Bootstrap: DB 초기화 단계 추가 (critical) + ### Phase 4 구현 내용 - LocalLLMService: Ollama REST API 연동, 스트리밍 NDJSON 파싱, 가용성 폴링(5초) - 시스템 프롬프트: refine/translate/summarize/grammar/expand/custom 6개 액션 diff --git a/package-lock.json b/package-lock.json index 1793583..330175f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,13 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.0.0", "@mui/material": "^7.0.0", + "@nut-tree-fork/nut-js": "^4.2.6", "@rollup/rollup-win32-x64-msvc": "^4.60.1", + "better-sqlite3": "^12.8.0", + "drizzle-orm": "^0.45.2", "electron-log": "^5.2.0", "electron-store": "^10.0.0", + "nanoid": "^5.1.7", "node-record-lpcm16": "^1.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -25,6 +29,7 @@ }, "devDependencies": { "@electron-toolkit/tsconfig": "^1.0.1", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.13.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", @@ -1157,6 +1162,425 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@jimp/bmp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz", + "integrity": "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.12.tgz", + "integrity": "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.6.0" + } + }, + "node_modules/@jimp/custom": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", + "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", + "license": "MIT", + "dependencies": { + "@jimp/core": "^0.22.12" + } + }, + "node_modules/@jimp/gif": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.12.tgz", + "integrity": "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "gifwrap": "^0.10.1", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.12.tgz", + "integrity": "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "jpeg-js": "^0.4.4" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", + "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", + "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.12.tgz", + "integrity": "sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", + "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "tinycolor2": "^1.6.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.12.tgz", + "integrity": "sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.12.tgz", + "integrity": "sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", + "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.12.tgz", + "integrity": "sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.12.tgz", + "integrity": "sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.12.tgz", + "integrity": "sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.12.tgz", + "integrity": "sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-rotate": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-gaussian": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.12.tgz", + "integrity": "sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-invert": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.12.tgz", + "integrity": "sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.12.tgz", + "integrity": "sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-normalize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.12.tgz", + "integrity": "sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.12.tgz", + "integrity": "sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "load-bmfont": "^1.4.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", + "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", + "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-scale": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", + "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-shadow": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.12.tgz", + "integrity": "sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blur": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.12.tgz", + "integrity": "sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-color": ">=0.8.0", + "@jimp/plugin-resize": ">=0.8.0" + } + }, + "node_modules/@jimp/plugins": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.12.tgz", + "integrity": "sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==", + "license": "MIT", + "dependencies": { + "@jimp/plugin-blit": "^0.22.12", + "@jimp/plugin-blur": "^0.22.12", + "@jimp/plugin-circle": "^0.22.12", + "@jimp/plugin-color": "^0.22.12", + "@jimp/plugin-contain": "^0.22.12", + "@jimp/plugin-cover": "^0.22.12", + "@jimp/plugin-crop": "^0.22.12", + "@jimp/plugin-displace": "^0.22.12", + "@jimp/plugin-dither": "^0.22.12", + "@jimp/plugin-fisheye": "^0.22.12", + "@jimp/plugin-flip": "^0.22.12", + "@jimp/plugin-gaussian": "^0.22.12", + "@jimp/plugin-invert": "^0.22.12", + "@jimp/plugin-mask": "^0.22.12", + "@jimp/plugin-normalize": "^0.22.12", + "@jimp/plugin-print": "^0.22.12", + "@jimp/plugin-resize": "^0.22.12", + "@jimp/plugin-rotate": "^0.22.12", + "@jimp/plugin-scale": "^0.22.12", + "@jimp/plugin-shadow": "^0.22.12", + "@jimp/plugin-threshold": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.12.tgz", + "integrity": "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "pngjs": "^6.0.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.12.tgz", + "integrity": "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==", + "license": "MIT", + "dependencies": { + "utif2": "^4.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.12.tgz", + "integrity": "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==", + "license": "MIT", + "dependencies": { + "@jimp/bmp": "^0.22.12", + "@jimp/gif": "^0.22.12", + "@jimp/jpeg": "^0.22.12", + "@jimp/png": "^0.22.12", + "@jimp/tiff": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.12.tgz", + "integrity": "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.3" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1474,6 +1898,160 @@ "node": ">= 8" } }, + "node_modules/@nut-tree-fork/default-clipboard-provider": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/default-clipboard-provider/-/default-clipboard-provider-4.2.6.tgz", + "integrity": "sha512-Hzqj57rheIMGtsS4zK4//kOhaX5FxMluOiz+4TVaHXx+idZS/bPhZwd8e6o1w1GT0PVJOUIP+4CdUe//k5VRig==", + "license": "Apache-2.0", + "dependencies": { + "clipboardy": "2.3.0" + } + }, + "node_modules/@nut-tree-fork/libnut": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut/-/libnut-4.2.6.tgz", + "integrity": "sha512-2FCiTBokMGrMl4eL/trEIO+mtpkXpdPHoVKdTBmW8UBIbhCbrCKmnXb2skWGfVs+U3q7o5EYDjVTNUYaUWbaxQ==", + "license": "Apache-2.0", + "dependencies": { + "@nut-tree-fork/libnut-darwin": "2.7.5", + "@nut-tree-fork/libnut-linux": "2.7.5", + "@nut-tree-fork/libnut-win32": "2.7.5" + }, + "engines": { + "node": ">=10.15.3" + } + }, + "node_modules/@nut-tree-fork/libnut-darwin": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut-darwin/-/libnut-darwin-2.7.5.tgz", + "integrity": "sha512-LbqtPtMPTJUcg4XoPP2jsU1wc8flBcGyKTerKsIfK9cD7nBHROnO0QksbrsbSWEpLym8T8fRtuU7XEY83l6Z2Q==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "bindings": "1.5.0" + }, + "engines": { + "node": ">=10.15.3" + }, + "optionalDependencies": { + "@nut-tree-fork/node-mac-permissions": "2.2.1" + } + }, + "node_modules/@nut-tree-fork/libnut-linux": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut-linux/-/libnut-linux-2.7.5.tgz", + "integrity": "sha512-uxaXEcRKnFObAljsoR6tLOBUU1dJ2sctloG6gFgCBGN7+k6Jdv6jZfOuNjd/fpdq2C5WPMm0rtn9EE7h5J3Jcg==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "bindings": "1.5.0" + }, + "engines": { + "node": ">=10.15.3" + }, + "optionalDependencies": { + "@nut-tree-fork/node-mac-permissions": "2.2.1" + } + }, + "node_modules/@nut-tree-fork/libnut-win32": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut-win32/-/libnut-win32-2.7.5.tgz", + "integrity": "sha512-yqC87zvmFcDPwFrRU40DYhN0xmEVM3aSkOuyF0IX+y1x+HWSu/i0PNklATpPBhGid3QVb/TOHuVoaraMrUFCNw==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "bindings": "1.5.0" + }, + "engines": { + "node": ">=10.15.3" + }, + "optionalDependencies": { + "@nut-tree-fork/node-mac-permissions": "2.2.1" + } + }, + "node_modules/@nut-tree-fork/node-mac-permissions": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/node-mac-permissions/-/node-mac-permissions-2.2.1.tgz", + "integrity": "sha512-iSfOTDiBZ7VDa17PoQje5rUaZSvSAaq+XEyXCmhPuQwV5XuNU02Grv6oFhsdpz89w7+UvB/8KX/cX5IYQ5o2Bw==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "1.5.0", + "node-addon-api": "5.0.0" + } + }, + "node_modules/@nut-tree-fork/nut-js": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/nut-js/-/nut-js-4.2.6.tgz", + "integrity": "sha512-aI/WCX7gE1HFGPH3EZP/UWqpNMM1NMoM/EkXqp7pKMgXFCi8e5+o5p+jd/QOYpmALv9bQg7+s69nI7FONbMqDg==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "os": [ + "linux", + "darwin", + "win32" + ], + "dependencies": { + "@nut-tree-fork/default-clipboard-provider": "4.2.6", + "@nut-tree-fork/libnut": "4.2.6", + "@nut-tree-fork/provider-interfaces": "4.2.6", + "@nut-tree-fork/shared": "4.2.6", + "jimp": "0.22.10", + "node-abort-controller": "3.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@nut-tree-fork/provider-interfaces": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/provider-interfaces/-/provider-interfaces-4.2.6.tgz", + "integrity": "sha512-brtRegDkLSV0sa5DUAigjWf6hCoamBNPb/hKK9AQlW+j3BxQ/8djaEdEB2cihqUh1ZjEtgPyXRqpCWSdKCX68A==", + "license": "Apache-2.0", + "dependencies": { + "@nut-tree-fork/shared": "4.2.6" + } + }, + "node_modules/@nut-tree-fork/shared": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/shared/-/shared-4.2.6.tgz", + "integrity": "sha512-xZaa0YtJt/DDDq/i1vZkabjq8HOWzfhXieMai61cMbYD11J6VhAfhV23ZtQEM02WG7nc2LKjl4UwRnQCteikwA==", + "license": "Apache-2.0", + "dependencies": { + "jimp": "0.22.10", + "node-abort-controller": "3.1.1" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1863,6 +2441,12 @@ "node": ">=10" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1908,6 +2492,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -2384,6 +2978,18 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2466,6 +3072,32 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2518,6 +3150,26 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.14", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", @@ -2531,6 +3183,60 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -2586,6 +3292,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -2595,6 +3325,15 @@ "node": "*" } }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2662,6 +3401,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -2706,6 +3454,26 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "license": "MIT", + "dependencies": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -2901,6 +3669,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2953,6 +3730,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -2983,6 +3769,11 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dot-prop": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", @@ -2998,6 +3789,131 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/electron": { "version": "33.4.11", "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", @@ -3448,6 +4364,135 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3564,6 +4609,29 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -3609,6 +4677,32 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -3679,6 +4773,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3745,6 +4855,16 @@ "node": "*" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -3929,6 +5049,26 @@ "node": ">=10.19.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3939,6 +5079,21 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3981,7 +5136,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/is-arrayish": { @@ -4005,6 +5165,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4015,6 +5190,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "license": "MIT" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4038,13 +5219,61 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jimp": { + "version": "0.22.10", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.10.tgz", + "integrity": "sha512-lCaHIJAgTOsplyJzC1w/laxSxrbSsEBw4byKwXgUdMmh+ayPsnidTblenQm+IvhIs44Gcuvlb6pd2LQ0wcKaKg==", + "license": "MIT", + "dependencies": { + "@jimp/custom": "^0.22.10", + "@jimp/plugins": "^0.22.10", + "@jimp/types": "^0.22.10", + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4165,6 +5394,22 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4249,6 +5494,18 @@ "node": ">=10" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -4270,6 +5527,15 @@ "node": ">=4" } }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -4286,6 +5552,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4293,10 +5574,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", "funding": [ { "type": "github", @@ -4305,12 +5585,18 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4318,6 +5604,57 @@ "dev": true, "license": "MIT" }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -4372,6 +5709,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4391,6 +5749,12 @@ "node": ">= 0.4" } }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4427,6 +5791,15 @@ "node": ">=8" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4459,6 +5832,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4471,6 +5850,34 @@ "node": ">=6" } }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4551,12 +5958,38 @@ "node": ">= 14.16" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4576,6 +6009,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -4605,6 +6068,52 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4631,6 +6140,15 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4710,6 +6228,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -4763,6 +6305,68 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4934,6 +6538,35 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5018,6 +6651,57 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5058,6 +6742,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5071,6 +6764,15 @@ "node": ">=8" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5084,6 +6786,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/stubborn-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", @@ -5142,6 +6861,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5149,6 +6910,12 @@ "dev": true, "license": "MIT" }, + "node_modules/timm": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", + "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5156,6 +6923,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -5210,6 +6983,29 @@ "node": ">=14.0.0" } }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -5223,6 +7019,18 @@ "typescript": ">=4.8.4" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5343,6 +7151,21 @@ "punycode": "^2.1.0" } }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -5492,6 +7315,28 @@ } } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -5547,6 +7392,55 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 9af1581..1ad844f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "license": "MIT", "devDependencies": { "@electron-toolkit/tsconfig": "^1.0.1", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.13.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", @@ -39,9 +40,13 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.0.0", "@mui/material": "^7.0.0", + "@nut-tree-fork/nut-js": "^4.2.6", "@rollup/rollup-win32-x64-msvc": "^4.60.1", + "better-sqlite3": "^12.8.0", + "drizzle-orm": "^0.45.2", "electron-log": "^5.2.0", "electron-store": "^10.0.0", + "nanoid": "^5.1.7", "node-record-lpcm16": "^1.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/src/main/bootstrap.ts b/src/main/bootstrap.ts index b9d7828..f349b64 100644 --- a/src/main/bootstrap.ts +++ b/src/main/bootstrap.ts @@ -6,6 +6,8 @@ import { initConfigService } from './services/ConfigService' import { getHotkeyService } from './services/HotkeyService' import { getVoiceModeService } from './services/VoiceModeService' import { getLocalLLMService } from './services/LocalLLMService' +import { getHistoryService } from './services/HistoryService' +import { initDatabase } from './db' import { createMainWindow, preloadPopupWindows, @@ -30,6 +32,7 @@ export async function bootstrap(): Promise { const steps: BootstrapStep[] = [ { name: 'logger', critical: false, fn: initLogger }, { name: 'config', critical: false, fn: initConfig }, + { name: 'database', critical: true, fn: initDB }, { name: 'create-windows', critical: true, fn: createWindows }, { name: 'tray', critical: false, fn: initTray }, { name: 'ipc-handlers', critical: true, fn: initIpcHandlers }, @@ -65,6 +68,10 @@ async function initConfig(): Promise { initConfigService() } +async function initDB(): Promise { + initDatabase() +} + async function createWindows(): Promise { createMainWindow() } @@ -106,11 +113,26 @@ async function initVoiceMode(): Promise { } }) - voiceMode.on('session-completed', ({ finalText }) => { + voiceMode.on('session-completed', ({ session, finalText }) => { hideRecordingTip() if (finalText.length > 0) { showResultPopup(finalText) } + + // 이력 저장 + try { + const wordCount = finalText.split(/\s+/).filter((w) => w.length > 0).length + getHistoryService().create({ + originalText: session.transcription || finalText, + polishedText: session.processedText, + mode: session.mode === 'hands-free' ? 'dictation' : session.mode, + status: 'completed', + duration: (Date.now() - session.startedAt) / 1000, + wordCount + }) + } catch (error) { + logger.warn(`Failed to save history: ${error instanceof Error ? error.message : String(error)}`) + } }) voiceMode.on('session-cancelled', () => { diff --git a/src/main/db/index.ts b/src/main/db/index.ts new file mode 100644 index 0000000..d563553 --- /dev/null +++ b/src/main/db/index.ts @@ -0,0 +1,107 @@ +// src/main/db/index.ts +// 설계서 03의 DB 초기화 코드 + +import Database from 'better-sqlite3' +import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' +import { app } from 'electron' +import path from 'path' +import * as schema from './schema' +import { getLogger } from '../services/LoggerService' + +const logger = getLogger('database') + +let db: BetterSQLite3Database | null = null +let sqlite: Database.Database | null = null + +export function initDatabase(): BetterSQLite3Database { + const dbPath = path.join(app.getPath('userData'), 'd3ro-voice.db') + logger.info(`Initializing database at: ${dbPath}`) + + sqlite = new Database(dbPath) + + sqlite.pragma('journal_mode = WAL') + sqlite.pragma('foreign_keys = ON') + sqlite.pragma('busy_timeout = 5000') + + // 테이블 생성 (마이그레이션 대신 직접 생성) + sqlite.exec(` + CREATE TABLE IF NOT EXISTS history ( + id TEXT PRIMARY KEY, + original_text TEXT NOT NULL, + polished_text TEXT, + focused_app TEXT, + focused_app_name TEXT, + focused_app_window_title TEXT, + mode TEXT NOT NULL DEFAULT 'dictation', + status TEXT NOT NULL DEFAULT 'completed', + error_code TEXT, + audio_local_path TEXT, + duration REAL NOT NULL, + detected_language TEXT, + mic_device TEXT, + word_count INTEGER NOT NULL DEFAULT 0, + stt_model TEXT, + llm_model TEXT, + stt_latency_ms INTEGER, + llm_latency_ms INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + app_version TEXT NOT NULL DEFAULT '1.0.0' + ); + + CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_history_status ON history(status); + CREATE INDEX IF NOT EXISTS idx_history_detected_language ON history(detected_language); + CREATE INDEX IF NOT EXISTS idx_history_focused_app_name ON history(focused_app_name); + CREATE INDEX IF NOT EXISTS idx_history_mode ON history(mode); + + CREATE TABLE IF NOT EXISTS dictionary ( + id TEXT PRIMARY KEY, + word TEXT NOT NULL, + pronunciation TEXT, + category TEXT NOT NULL DEFAULT 'user', + usage_count INTEGER NOT NULL DEFAULT 0, + last_used_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE UNIQUE INDEX IF NOT EXISTS idx_dictionary_word_category ON dictionary(word, category); + CREATE INDEX IF NOT EXISTS idx_dictionary_created_at ON dictionary(created_at); + CREATE INDEX IF NOT EXISTS idx_dictionary_usage_count ON dictionary(usage_count DESC); + + CREATE TABLE IF NOT EXISTS stats ( + id INTEGER PRIMARY KEY CHECK (id = 1), + total_duration REAL NOT NULL DEFAULT 0, + total_words INTEGER NOT NULL DEFAULT 0, + session_count INTEGER NOT NULL DEFAULT 0, + streak_days INTEGER NOT NULL DEFAULT 0, + last_session_at INTEGER, + last_updated INTEGER NOT NULL + ); + + INSERT OR IGNORE INTO stats (id, total_duration, total_words, session_count, streak_days, last_updated) + VALUES (1, 0, 0, 0, 0, ${Date.now()}); + `) + + db = drizzle(sqlite, { schema }) + + logger.info('Database initialized') + return db +} + +export function getDatabase(): BetterSQLite3Database { + if (!db) { + throw new Error('Database not initialized. Call initDatabase() first.') + } + return db +} + +export function closeDatabase(): void { + if (sqlite) { + sqlite.close() + sqlite = null + db = null + logger.info('Database closed') + } +} diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts new file mode 100644 index 0000000..8e2a1d8 --- /dev/null +++ b/src/main/db/schema.ts @@ -0,0 +1,83 @@ +// src/main/db/schema.ts +// 설계서 03의 drizzle-orm 스키마 정의 + +import { sqliteTable, text, integer, real, index, uniqueIndex } from 'drizzle-orm/sqlite-core' + +// ── history ────────────────────────────────────────────── +export const history = sqliteTable( + 'history', + { + id: text('id').primaryKey(), + originalText: text('original_text').notNull(), + polishedText: text('polished_text'), + focusedApp: text('focused_app'), + focusedAppName: text('focused_app_name'), + focusedAppWindowTitle: text('focused_app_window_title'), + mode: text('mode', { enum: ['dictation', 'translate', 'command'] }) + .notNull() + .default('dictation'), + status: text('status', { enum: ['completed', 'cancelled', 'error'] }) + .notNull() + .default('completed'), + errorCode: text('error_code'), + audioLocalPath: text('audio_local_path'), + duration: real('duration').notNull(), + detectedLanguage: text('detected_language'), + micDevice: text('mic_device'), + wordCount: integer('word_count').notNull().default(0), + sttModel: text('stt_model'), + llmModel: text('llm_model'), + sttLatencyMs: integer('stt_latency_ms'), + llmLatencyMs: integer('llm_latency_ms'), + createdAt: integer('created_at').notNull(), + updatedAt: integer('updated_at').notNull(), + appVersion: text('app_version').notNull().default('1.0.0') + }, + (table) => [ + index('idx_history_created_at').on(table.createdAt), + index('idx_history_status').on(table.status), + index('idx_history_detected_language').on(table.detectedLanguage), + index('idx_history_focused_app_name').on(table.focusedAppName), + index('idx_history_mode').on(table.mode) + ] +) + +// ── dictionary ─────────────────────────────────────────── +export const dictionary = sqliteTable( + 'dictionary', + { + id: text('id').primaryKey(), + word: text('word').notNull(), + pronunciation: text('pronunciation'), + category: text('category', { enum: ['user', 'auto', 'technical'] }) + .notNull() + .default('user'), + usageCount: integer('usage_count').notNull().default(0), + lastUsedAt: integer('last_used_at'), + createdAt: integer('created_at').notNull(), + updatedAt: integer('updated_at').notNull() + }, + (table) => [ + uniqueIndex('idx_dictionary_word_category').on(table.word, table.category), + index('idx_dictionary_created_at').on(table.createdAt), + index('idx_dictionary_usage_count').on(table.usageCount) + ] +) + +// ── stats ──────────────────────────────────────────────── +export const stats = sqliteTable('stats', { + id: integer('id').primaryKey(), + totalDuration: real('total_duration').notNull().default(0), + totalWords: integer('total_words').notNull().default(0), + sessionCount: integer('session_count').notNull().default(0), + streakDays: integer('streak_days').notNull().default(0), + lastSessionAt: integer('last_session_at'), + lastUpdated: integer('last_updated').notNull() +}) + +// ── 타입 추출 ──────────────────────────────────────────── +export type History = typeof history.$inferSelect +export type NewHistory = typeof history.$inferInsert +export type Dictionary = typeof dictionary.$inferSelect +export type NewDictionary = typeof dictionary.$inferInsert +export type Stats = typeof stats.$inferSelect diff --git a/src/main/ipc/dictionary-handlers.ts b/src/main/ipc/dictionary-handlers.ts new file mode 100644 index 0000000..163bde1 --- /dev/null +++ b/src/main/ipc/dictionary-handlers.ts @@ -0,0 +1,56 @@ +// src/main/ipc/dictionary-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { getDictionaryService } from '../services/DictionaryService' +import type { + DictionaryQueryParams, + DictionaryAddParams, + DictionaryUpdateParams, + DictionaryDeleteParams, + DictionarySearchParams +} from '@shared/types' + +export function registerDictionaryHandlers(): void { + ipcMain.handle(IPC_CHANNELS.DICTIONARY.GET_ALL, async (_event, params: DictionaryQueryParams) => { + try { + return ipcSuccess(getDictionaryService().list(params)) + } catch { + return ipcError(ErrorCode.DBQueryFailed, 'Failed to get dictionary') + } + }) + + ipcMain.handle(IPC_CHANNELS.DICTIONARY.ADD, async (_event, params: DictionaryAddParams) => { + try { + return ipcSuccess(getDictionaryService().add(params)) + } catch { + return ipcError(ErrorCode.DBWriteFailed, 'Failed to add dictionary entry') + } + }) + + ipcMain.handle(IPC_CHANNELS.DICTIONARY.UPDATE, async (_event, params: DictionaryUpdateParams) => { + try { + return ipcSuccess(getDictionaryService().update(params)) + } catch { + return ipcError(ErrorCode.DBWriteFailed, 'Failed to update dictionary entry') + } + }) + + ipcMain.handle(IPC_CHANNELS.DICTIONARY.DELETE, async (_event, params: DictionaryDeleteParams) => { + try { + getDictionaryService().delete(params.id) + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.DBWriteFailed, 'Failed to delete dictionary entry') + } + }) + + ipcMain.handle(IPC_CHANNELS.DICTIONARY.SEARCH, async (_event, params: DictionarySearchParams) => { + try { + return ipcSuccess(getDictionaryService().search(params)) + } catch { + return ipcError(ErrorCode.DBQueryFailed, 'Failed to search dictionary') + } + }) +} diff --git a/src/main/ipc/history-handlers.ts b/src/main/ipc/history-handlers.ts new file mode 100644 index 0000000..05467d7 --- /dev/null +++ b/src/main/ipc/history-handlers.ts @@ -0,0 +1,64 @@ +// src/main/ipc/history-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { getHistoryService } from '../services/HistoryService' +import type { + HistoryQueryParams, + HistoryGetByIdParams, + HistoryDeleteParams, + HistorySearchParams +} from '@shared/types' + +export function registerHistoryHandlers(): void { + ipcMain.handle(IPC_CHANNELS.HISTORY.GET_ALL, async (_event, params: HistoryQueryParams) => { + try { + return ipcSuccess(getHistoryService().list(params)) + } catch { + return ipcError(ErrorCode.DBQueryFailed, 'Failed to get history') + } + }) + + ipcMain.handle(IPC_CHANNELS.HISTORY.GET_BY_ID, async (_event, params: HistoryGetByIdParams) => { + try { + return ipcSuccess(getHistoryService().getById(params.id)) + } catch { + return ipcError(ErrorCode.DBQueryFailed, 'Failed to get history entry') + } + }) + + ipcMain.handle(IPC_CHANNELS.HISTORY.DELETE, async (_event, params: HistoryDeleteParams) => { + try { + getHistoryService().delete(params.id) + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.DBWriteFailed, 'Failed to delete history entry') + } + }) + + ipcMain.handle(IPC_CHANNELS.HISTORY.DELETE_ALL, async () => { + try { + getHistoryService().deleteAll() + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.DBWriteFailed, 'Failed to delete all history') + } + }) + + ipcMain.handle(IPC_CHANNELS.HISTORY.SEARCH, async (_event, params: HistorySearchParams) => { + try { + return ipcSuccess(getHistoryService().search(params)) + } catch { + return ipcError(ErrorCode.DBQueryFailed, 'Failed to search history') + } + }) + + ipcMain.handle(IPC_CHANNELS.STATS.GET_SUMMARY, async () => { + try { + return ipcSuccess(getHistoryService().getStats()) + } catch { + return ipcError(ErrorCode.DBQueryFailed, 'Failed to get stats') + } + }) +} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 8daeffb..50d8266 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -8,6 +8,8 @@ import { registerVoiceHandlers } from './voice-handlers' import { registerSTTHandlers } from './stt-handlers' import { registerHotkeyHandlers } from './hotkey-handlers' import { registerLLMHandlers } from './llm-handlers' +import { registerHistoryHandlers } from './history-handlers' +import { registerDictionaryHandlers } from './dictionary-handlers' import { getLogger } from '../services/LoggerService' const logger = getLogger('ipc') @@ -21,5 +23,7 @@ export function registerAllIpcHandlers(): void { registerSTTHandlers() registerHotkeyHandlers() registerLLMHandlers() + registerHistoryHandlers() + registerDictionaryHandlers() logger.info('All IPC handlers registered') } diff --git a/src/main/services/DictionaryService.ts b/src/main/services/DictionaryService.ts new file mode 100644 index 0000000..7774892 --- /dev/null +++ b/src/main/services/DictionaryService.ts @@ -0,0 +1,170 @@ +// src/main/services/DictionaryService.ts +// 사용자 커스텀 단어 사전. 설계서 01/03 IDictionaryService 구현. + +import { eq, like, desc, count, sql } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { getDatabase } from '../db' +import { dictionary } from '../db/schema' +import type { Dictionary, NewDictionary } from '../db/schema' +import { getLogger } from './LoggerService' +import type { + DictionaryEntry, + DictionaryQueryParams, + DictionaryPage, + DictionaryAddParams, + DictionaryUpdateParams, + DictionarySearchParams +} from '@shared/types' + +const logger = getLogger('DictionaryService') + +class DictionaryService { + add(params: DictionaryAddParams): DictionaryEntry { + const db = getDatabase() + const now = Date.now() + const id = nanoid() + + const entry: NewDictionary = { + id, + word: params.word, + pronunciation: params.pronunciation ?? null, + category: params.category ?? 'user', + usageCount: 0, + lastUsedAt: null, + createdAt: now, + updatedAt: now + } + + db.insert(dictionary).values(entry).run() + logger.info(`Dictionary entry added: "${params.word}"`) + return this._toEntry(entry as Dictionary) + } + + update(params: DictionaryUpdateParams): DictionaryEntry | null { + const db = getDatabase() + const existing = db.select().from(dictionary).where(eq(dictionary.id, params.id)).get() + if (!existing) return null + + const updates: Partial = { updatedAt: Date.now() } + if (params.word !== undefined) updates.word = params.word + if (params.pronunciation !== undefined) updates.pronunciation = params.pronunciation + if (params.category !== undefined) updates.category = params.category + + db.update(dictionary).set(updates).where(eq(dictionary.id, params.id)).run() + + const updated = db.select().from(dictionary).where(eq(dictionary.id, params.id)).get() + return updated ? this._toEntry(updated) : null + } + + delete(id: string): boolean { + const db = getDatabase() + const result = db.delete(dictionary).where(eq(dictionary.id, id)).run() + return result.changes > 0 + } + + list(params: DictionaryQueryParams): DictionaryPage { + const db = getDatabase() + const { page, pageSize } = params + + const totalResult = db.select({ count: count() }).from(dictionary).get() + const total = totalResult?.count ?? 0 + + const entries = db + .select() + .from(dictionary) + .orderBy(desc(dictionary.createdAt)) + .limit(pageSize) + .offset(page * pageSize) + .all() + + return { + entries: entries.map((e) => this._toEntry(e)), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } + } + + search(params: DictionarySearchParams): DictionaryPage { + const db = getDatabase() + const { query, page, pageSize } = params + const pattern = `%${query}%` + + const totalResult = db + .select({ count: count() }) + .from(dictionary) + .where(like(dictionary.word, pattern)) + .get() + const total = totalResult?.count ?? 0 + + const entries = db + .select() + .from(dictionary) + .where(like(dictionary.word, pattern)) + .orderBy(desc(dictionary.usageCount)) + .limit(pageSize) + .offset(page * pageSize) + .all() + + return { + entries: entries.map((e) => this._toEntry(e)), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } + } + + incrementUsage(id: string): void { + const db = getDatabase() + db.update(dictionary) + .set({ + usageCount: sql`usage_count + 1`, + lastUsedAt: Date.now() + }) + .where(eq(dictionary.id, id)) + .run() + } + + /** + * 사전 단어 목록을 STT initialPrompt 형태로 반환한다. + */ + getPromptHints(limit = 50): string { + const db = getDatabase() + const words = db + .select({ word: dictionary.word }) + .from(dictionary) + .orderBy(desc(dictionary.usageCount)) + .limit(limit) + .all() + + return words.map((w) => w.word).join(', ') + } + + dispose(): void { + logger.info('DictionaryService disposed') + } + + private _toEntry(row: Dictionary): DictionaryEntry { + return { + id: row.id, + word: row.word, + pronunciation: row.pronunciation, + category: row.category as DictionaryEntry['category'], + usageCount: row.usageCount, + lastUsedAt: row.lastUsedAt, + createdAt: row.createdAt, + updatedAt: row.updatedAt + } + } +} + +let instance: DictionaryService | null = null + +export function getDictionaryService(): DictionaryService { + if (!instance) { + instance = new DictionaryService() + } + return instance +} diff --git a/src/main/services/HistoryService.ts b/src/main/services/HistoryService.ts new file mode 100644 index 0000000..29642e7 --- /dev/null +++ b/src/main/services/HistoryService.ts @@ -0,0 +1,219 @@ +// src/main/services/HistoryService.ts +// SQLite 기반 전사/명령 이력 저장. 설계서 01/03 IHistoryService 구현. + +import { eq, desc, like, and, sql, count } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { getDatabase } from '../db' +import { history, stats } from '../db/schema' +import type { History, NewHistory } from '../db/schema' +import { getLogger } from './LoggerService' +import type { + HistoryEntry, + HistoryQueryParams, + HistoryPage, + HistorySearchParams, + StatsSummary +} from '@shared/types' + +const logger = getLogger('HistoryService') + +class HistoryService { + create(input: Omit): HistoryEntry { + const db = getDatabase() + const now = Date.now() + const id = nanoid() + + const entry: NewHistory = { + id, + ...input, + createdAt: now, + updatedAt: now + } + + db.insert(history).values(entry).run() + + // stats 싱글톤 업데이트 + this._updateStats(input.duration, input.wordCount) + + logger.info(`History entry created: ${id}`) + return this._toEntry(entry as History) + } + + getById(id: string): HistoryEntry | null { + const db = getDatabase() + const row = db.select().from(history).where(eq(history.id, id)).get() + return row ? this._toEntry(row) : null + } + + list(params: HistoryQueryParams): HistoryPage { + const db = getDatabase() + const { page, pageSize } = params + + const totalResult = db.select({ count: count() }).from(history).get() + const total = totalResult?.count ?? 0 + + const entries = db + .select() + .from(history) + .orderBy(desc(history.createdAt)) + .limit(pageSize) + .offset(page * pageSize) + .all() + + return { + entries: entries.map((e) => this._toEntry(e)), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } + } + + search(params: HistorySearchParams): HistoryPage { + const db = getDatabase() + const { query, page, pageSize } = params + const pattern = `%${query}%` + + const conditions = and( + like(history.originalText, pattern) + ) + + const totalResult = db + .select({ count: count() }) + .from(history) + .where(conditions) + .get() + const total = totalResult?.count ?? 0 + + const entries = db + .select() + .from(history) + .where(conditions) + .orderBy(desc(history.createdAt)) + .limit(pageSize) + .offset(page * pageSize) + .all() + + return { + entries: entries.map((e) => this._toEntry(e)), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } + } + + delete(id: string): boolean { + const db = getDatabase() + const result = db.delete(history).where(eq(history.id, id)).run() + return result.changes > 0 + } + + deleteAll(): void { + const db = getDatabase() + db.delete(history).run() + logger.info('All history entries deleted') + } + + getStats(): StatsSummary { + const db = getDatabase() + const row = db.select().from(stats).where(eq(stats.id, 1)).get() + + const todayStart = new Date() + todayStart.setHours(0, 0, 0, 0) + const todayMs = todayStart.getTime() + + const todayResult = db + .select({ + sessions: count(), + duration: sql`COALESCE(SUM(duration), 0)`, + words: sql`COALESCE(SUM(word_count), 0)` + }) + .from(history) + .where( + and( + sql`created_at >= ${todayMs}`, + eq(history.status, 'completed') + ) + ) + .get() + + return { + totalRecordingTimeMs: (row?.totalDuration ?? 0) * 1000, + totalWordCount: row?.totalWords ?? 0, + totalSessionCount: row?.sessionCount ?? 0, + todayRecordingTimeMs: (todayResult?.duration ?? 0) * 1000, + todayWordCount: todayResult?.words ?? 0, + todaySessionCount: todayResult?.sessions ?? 0, + streakDays: row?.streakDays ?? 0 + } + } + + /** + * 보존 정책에 따라 오래된 항목 정리 (30일 초과) + */ + runRetentionCleanup(): number { + const db = getDatabase() + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 + const result = db.delete(history).where(sql`created_at < ${cutoff}`).run() + if (result.changes > 0) { + logger.info(`Retention cleanup: ${result.changes} old entries removed`) + } + return result.changes + } + + dispose(): void { + logger.info('HistoryService disposed') + } + + private _updateStats(duration: number, wordCount: number): void { + const db = getDatabase() + const now = Date.now() + + db.update(stats) + .set({ + totalDuration: sql`total_duration + ${duration}`, + totalWords: sql`total_words + ${wordCount}`, + sessionCount: sql`session_count + 1`, + lastSessionAt: now, + lastUpdated: now + }) + .where(eq(stats.id, 1)) + .run() + } + + private _toEntry(row: History): HistoryEntry { + return { + id: row.id, + originalText: row.originalText, + polishedText: row.polishedText, + focusedApp: row.focusedApp, + focusedAppName: row.focusedAppName, + focusedAppWindowTitle: row.focusedAppWindowTitle, + mode: row.mode as HistoryEntry['mode'], + status: row.status as HistoryEntry['status'], + errorCode: row.errorCode, + audioLocalPath: row.audioLocalPath, + duration: row.duration, + detectedLanguage: row.detectedLanguage, + micDevice: row.micDevice, + wordCount: row.wordCount, + sttModel: row.sttModel, + llmModel: row.llmModel, + sttLatencyMs: row.sttLatencyMs, + llmLatencyMs: row.llmLatencyMs, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + appVersion: row.appVersion + } + } +} + +let instance: HistoryService | null = null + +export function getHistoryService(): HistoryService { + if (!instance) { + instance = new HistoryService() + } + return instance +} diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 32828ef..e20b12e 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -26,3 +26,5 @@ export { getVoiceModeService } from './VoiceModeService' export { getAudioCaptureService } from './AudioCaptureService' export { getTextInsertService } from './TextInsertService' export { getLocalLLMService } from './LocalLLMService' +export { getHistoryService } from './HistoryService' +export { getDictionaryService } from './DictionaryService' diff --git a/src/preload/index.ts b/src/preload/index.ts index 3329222..c3a03ca 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -47,6 +47,20 @@ import type { SetServerUrlParams, LLMStatusChangedEvent, LLMProcessProgressEvent, + HistoryQueryParams, + HistoryPage, + HistoryGetByIdParams, + HistoryEntry, + HistoryDeleteParams, + HistorySearchParams, + DictionaryQueryParams, + DictionaryPage, + DictionaryEntry, + DictionaryAddParams, + DictionaryUpdateParams, + DictionaryDeleteParams, + DictionarySearchParams, + StatsSummary, PermissionStatus } from '@shared/types' import type { IPCResult } from '@shared/errors' @@ -169,6 +183,40 @@ const electronAPI = { on(IPC_CHANNELS.LLM.PROCESS_PROGRESS, cb) }, + // ── History ──────────────────────────────────────────── + history: { + getAll: (params: HistoryQueryParams) => + invoke(IPC_CHANNELS.HISTORY.GET_ALL, params), + getById: (params: HistoryGetByIdParams) => + invoke(IPC_CHANNELS.HISTORY.GET_BY_ID, params), + delete: (params: HistoryDeleteParams) => + invoke(IPC_CHANNELS.HISTORY.DELETE, params), + deleteAll: () => invoke(IPC_CHANNELS.HISTORY.DELETE_ALL), + search: (params: HistorySearchParams) => + invoke(IPC_CHANNELS.HISTORY.SEARCH, params), + onAdded: (cb: (e: HistoryEntry) => void): Unsubscribe => + on(IPC_CHANNELS.HISTORY.ADDED, cb) + }, + + // ── Dictionary ───────────────────────────────────────── + dictionary: { + getAll: (params: DictionaryQueryParams) => + invoke(IPC_CHANNELS.DICTIONARY.GET_ALL, params), + add: (params: DictionaryAddParams) => + invoke(IPC_CHANNELS.DICTIONARY.ADD, params), + update: (params: DictionaryUpdateParams) => + invoke(IPC_CHANNELS.DICTIONARY.UPDATE, params), + delete: (params: DictionaryDeleteParams) => + invoke(IPC_CHANNELS.DICTIONARY.DELETE, params), + search: (params: DictionarySearchParams) => + invoke(IPC_CHANNELS.DICTIONARY.SEARCH, params) + }, + + // ── Stats ────────────────────────────────────────────── + stats: { + getSummary: () => invoke(IPC_CHANNELS.STATS.GET_SUMMARY) + }, + // ── Window ───────────────────────────────────────────── window: { minimize: () => send(IPC_CHANNELS.WINDOW.MINIMIZE), diff --git a/src/renderer/components/AppLayout.tsx b/src/renderer/components/AppLayout.tsx index 5c113dd..9f28b9a 100644 --- a/src/renderer/components/AppLayout.tsx +++ b/src/renderer/components/AppLayout.tsx @@ -17,6 +17,8 @@ import HistoryIcon from '@mui/icons-material/History' import MenuBookIcon from '@mui/icons-material/MenuBook' import SettingsIcon from '@mui/icons-material/Settings' import { DashboardPage } from '../pages/DashboardPage' +import { HistoryPage } from '../pages/HistoryPage' +import { DictionaryPage } from '../pages/DictionaryPage' import { SettingsModal } from './SettingsModal' import { StatusBar } from './StatusBar' @@ -98,16 +100,8 @@ export function AppLayout(): React.ReactElement { }} > {currentRoute === 'dashboard' && } - {currentRoute === 'history' && ( - - History (Phase 5) - - )} - {currentRoute === 'dictionary' && ( - - Dictionary (Phase 5) - - )} + {currentRoute === 'history' && } + {currentRoute === 'dictionary' && } diff --git a/src/renderer/pages/DashboardPage.tsx b/src/renderer/pages/DashboardPage.tsx index fb62ae3..bffe483 100644 --- a/src/renderer/pages/DashboardPage.tsx +++ b/src/renderer/pages/DashboardPage.tsx @@ -1,10 +1,12 @@ // src/renderer/pages/DashboardPage.tsx +import { useState, useEffect } from 'react' import { Box, Card, CardContent, Typography, Grid } from '@mui/material' import MicIcon from '@mui/icons-material/Mic' import TimerIcon from '@mui/icons-material/Timer' import TextFieldsIcon from '@mui/icons-material/TextFields' import TodayIcon from '@mui/icons-material/Today' +import type { StatsSummary } from '@shared/types' interface StatCardProps { title: string @@ -28,7 +30,33 @@ function StatCard({ title, value, icon }: StatCardProps): React.ReactElement { ) } +function formatTime(ms: number): string { + const totalSec = Math.round(ms / 1000) + const hours = Math.floor(totalSec / 3600) + const minutes = Math.floor((totalSec % 3600) / 60) + const seconds = totalSec % 60 + if (hours > 0) return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` + return `${minutes}:${seconds.toString().padStart(2, '0')}` +} + export function DashboardPage(): React.ReactElement { + const [stats, setStats] = useState(null) + + useEffect(() => { + window.electronAPI.stats.getSummary().then((result) => { + if (result.success) setStats(result.data) + }) + + // 30초마다 갱신 + const interval = setInterval(() => { + window.electronAPI.stats.getSummary().then((result) => { + if (result.success) setStats(result.data) + }) + }, 30000) + + return () => clearInterval(interval) + }, []) + return ( @@ -37,30 +65,66 @@ export function DashboardPage(): React.ReactElement { - } /> + } + /> - } /> + } + /> - } /> + } + /> - } /> + } + /> - + {/* Today's stats */} + - Recent Sessions + Today - - - - No sessions yet. Press the hotkey to start recording. - - - + + + + + Sessions + {stats?.todaySessionCount ?? 0} + + + + + + + Time + {formatTime(stats?.todayRecordingTimeMs ?? 0)} + + + + + + + Words + {stats?.todayWordCount ?? 0} + + + + ) diff --git a/src/renderer/pages/DictionaryPage.tsx b/src/renderer/pages/DictionaryPage.tsx new file mode 100644 index 0000000..a3818d4 --- /dev/null +++ b/src/renderer/pages/DictionaryPage.tsx @@ -0,0 +1,173 @@ +// src/renderer/pages/DictionaryPage.tsx + +import { useState, useEffect, useCallback } from 'react' +import { + Box, + Typography, + TextField, + Button, + List, + ListItem, + ListItemText, + IconButton, + Chip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Card, + CardContent, + InputAdornment +} from '@mui/material' +import AddIcon from '@mui/icons-material/Add' +import DeleteIcon from '@mui/icons-material/Delete' +import SearchIcon from '@mui/icons-material/Search' +import type { DictionaryEntry, DictionaryPage as DictPageData } from '@shared/types' + +const PAGE_SIZE = 50 + +export function DictionaryPage(): React.ReactElement { + const [data, setData] = useState(null) + const [search, setSearch] = useState('') + const [loading, setLoading] = useState(true) + const [addOpen, setAddOpen] = useState(false) + const [newWord, setNewWord] = useState('') + const [newPronunciation, setNewPronunciation] = useState('') + + const loadData = useCallback(async () => { + setLoading(true) + const result = search.trim() + ? await window.electronAPI.dictionary.search({ query: search, page: 0, pageSize: PAGE_SIZE }) + : await window.electronAPI.dictionary.getAll({ page: 0, pageSize: PAGE_SIZE }) + + if (result.success) { + setData(result.data) + } + setLoading(false) + }, [search]) + + useEffect(() => { + loadData() + }, [loadData]) + + const handleAdd = async () => { + if (!newWord.trim()) return + await window.electronAPI.dictionary.add({ + word: newWord.trim(), + pronunciation: newPronunciation.trim() || undefined + }) + setNewWord('') + setNewPronunciation('') + setAddOpen(false) + loadData() + } + + const handleDelete = async (id: string) => { + await window.electronAPI.dictionary.delete({ id }) + loadData() + } + + return ( + + + + Dictionary + + + + + setSearch(e.target.value)} + fullWidth + sx={{ mb: 2 }} + slotProps={{ + input: { + startAdornment: ( + + + + ) + } + }} + /> + + {loading ? ( + Loading... + ) : !data || data.entries.length === 0 ? ( + + + + {search ? 'No words found.' : 'No words yet. Add custom words for better STT accuracy.'} + + + + ) : ( + + {data.entries.map((entry: DictionaryEntry) => ( + handleDelete(entry.id)}> + + + } + > + + {entry.pronunciation && ( + + [{entry.pronunciation}] + + )} + + + + } + /> + + ))} + + )} + + {/* Add Word Dialog */} + setAddOpen(false)} maxWidth="xs" fullWidth> + Add Word + + setNewWord(e.target.value)} + fullWidth + autoFocus + sx={{ mt: 1 }} + /> + setNewPronunciation(e.target.value)} + fullWidth + sx={{ mt: 2 }} + /> + + + + + + + + ) +} diff --git a/src/renderer/pages/HistoryPage.tsx b/src/renderer/pages/HistoryPage.tsx new file mode 100644 index 0000000..ac37681 --- /dev/null +++ b/src/renderer/pages/HistoryPage.tsx @@ -0,0 +1,161 @@ +// src/renderer/pages/HistoryPage.tsx + +import { useState, useEffect, useCallback } from 'react' +import { + Box, + Typography, + TextField, + List, + ListItem, + ListItemText, + IconButton, + Chip, + Pagination, + Card, + CardContent, + InputAdornment +} from '@mui/material' +import SearchIcon from '@mui/icons-material/Search' +import DeleteIcon from '@mui/icons-material/Delete' +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import type { HistoryEntry, HistoryPage as HistoryPageData } from '@shared/types' + +const PAGE_SIZE = 20 + +export function HistoryPage(): React.ReactElement { + const [data, setData] = useState(null) + const [page, setPage] = useState(0) + const [search, setSearch] = useState('') + const [loading, setLoading] = useState(true) + + const loadData = useCallback(async () => { + setLoading(true) + const result = search.trim() + ? await window.electronAPI.history.search({ query: search, page, pageSize: PAGE_SIZE }) + : await window.electronAPI.history.getAll({ page, pageSize: PAGE_SIZE }) + + if (result.success) { + setData(result.data) + } + setLoading(false) + }, [page, search]) + + useEffect(() => { + loadData() + }, [loadData]) + + const handleDelete = async (id: string) => { + await window.electronAPI.history.delete({ id }) + loadData() + } + + const handleCopy = (text: string) => { + navigator.clipboard.writeText(text) + } + + const formatDate = (ts: number) => { + return new Date(ts).toLocaleString('ko-KR', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + const formatDuration = (sec: number) => { + const m = Math.floor(sec / 60) + const s = Math.round(sec % 60) + return `${m}:${s.toString().padStart(2, '0')}` + } + + return ( + + + History + + + { + setSearch(e.target.value) + setPage(0) + }} + fullWidth + sx={{ mb: 2 }} + slotProps={{ + input: { + startAdornment: ( + + + + ) + } + }} + /> + + {loading ? ( + Loading... + ) : !data || data.entries.length === 0 ? ( + + + + {search ? 'No results found.' : 'No history yet.'} + + + + ) : ( + <> + + {data.entries.map((entry: HistoryEntry) => ( + + handleCopy(entry.polishedText || entry.originalText)} + > + + + handleDelete(entry.id)}> + + + + } + > + + + {formatDate(entry.createdAt)} + + + {entry.detectedLanguage && ( + + )} + + + } + primaryTypographyProps={{ sx: { pr: 8 } }} + /> + + ))} + + + {data.totalPages > 1 && ( + + setPage(p - 1)} + /> + + )} + + )} + + ) +}