diff --git a/package.json b/package.json index 1acedc3..5743421 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "create:prototype": "node scripts/create-prototype.mjs", "prepare:artifacts": "node scripts/prepare-artifacts.mjs", "publish": "node scripts/publish.mjs", - "test": "node --test && pnpm --recursive --if-present --filter './prototypes/**' test", + "test": "node --test 'test/*.test.mjs' && pnpm --recursive --if-present --filter './prototypes/**' test", "validate:prototypes": "node scripts/validate-prototypes.mjs" }, "dependencies": { "@vercel/blob": "^2.8.0", - "es-module-lexer": "^2.3.1" + "es-module-lexer": "^2.3.1", + "esbuild": "0.17.14" }, "engines": { "node": ">=22", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e525240..974ce48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: es-module-lexer: specifier: ^2.3.1 version: 2.3.1 + esbuild: + specifier: 0.17.14 + version: 0.17.14 packages/extension-base: dependencies: @@ -44,19 +47,19 @@ importers: version: 30.0.1 roamjs-components: specifier: 'catalog:' - version: 0.89.0(8b3dba144aa4c6a891ee791b6ddc21c2) + version: 0.89.0(dd2f1f144015bc13ec70826008c78270) use-sync-external-store: specifier: 'catalog:' version: 1.6.0(react@17.0.2) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0)) prototypes/loaded-dialog: dependencies: roamjs-components: specifier: 'catalog:' - version: 0.89.0(8b3dba144aa4c6a891ee791b6ddc21c2) + version: 0.89.0(dd2f1f144015bc13ec70826008c78270) use-sync-external-store: specifier: 'catalog:' version: 1.6.0(react@17.0.2) @@ -69,13 +72,13 @@ importers: version: 30.0.1 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0)) prototypes/properties-panel: dependencies: roamjs-components: specifier: 'catalog:' - version: 0.89.0(8b3dba144aa4c6a891ee791b6ddc21c2) + version: 0.89.0(dd2f1f144015bc13ec70826008c78270) use-sync-external-store: specifier: 'catalog:' version: 1.6.0(react@17.0.2) @@ -94,14 +97,117 @@ importers: version: 30.0.1 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0)) + + prototypes/zotero-roam-mcp: + dependencies: + '@blueprintjs/core': + specifier: ^3.54.0 + version: 3.54.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/datetime': + specifier: ^3.24.1 + version: 3.24.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/icons': + specifier: ^3.33.0 + version: 3.33.0 + '@blueprintjs/popover2': + specifier: ^0.14.0 + version: 0.14.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/select': + specifier: ^3.19.1 + version: 3.19.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@tanstack/query-core': + specifier: ^4.36.1 + version: 4.44.0 + '@tanstack/query-persist-client-core': + specifier: ^4.36.1 + version: 4.44.0 + '@tanstack/react-query': + specifier: ^4.36.1 + version: 4.44.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@tanstack/react-query-persist-client': + specifier: ^4.36.1 + version: 4.44.0(@tanstack/react-query@4.44.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + axios: + specifier: ^1.8.2 + version: 1.20.0(debug@4.4.3) + axios-retry: + specifier: ^4.5.0 + version: 4.5.0(axios@1.20.0) + idb: + specifier: ^6.1.5 + version: 6.1.5 + react: + specifier: ^17.0.2 + version: 17.0.2 + react-dom: + specifier: ^17.0.2 + version: 17.0.2(react@17.0.2) + scite-badge: + specifier: github:scitedotai/scite-badge#v8.0.6-prod + version: https://codeload.github.com/scitedotai/scite-badge/tar.gz/80bda7fa058531a1b3f107b91f43d0cb35b6375f + tributejs: + specifier: ^5.1.3 + version: 5.1.3 + devDependencies: + '@storybook/test': + specifier: ^8.3.2 + version: 8.6.15(storybook@8.6.18) + '@swc/core': + specifier: ^1.7.26 + version: 1.16.1 + '@testing-library/dom': + specifier: ^8.20.1 + version: 8.20.1 + '@testing-library/react': + specifier: ^12.1.5 + version: 12.1.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@testing-library/react-hooks': + specifier: ^8.0.1 + version: 8.0.1(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@vitejs/plugin-react-swc': + specifier: ^3.7.0 + version: 3.11.0(vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1)) + fake-indexeddb: + specifier: ^4.0.2 + version: 4.0.2 + jsdom: + specifier: 26.0.0 + version: 26.0.0 + msw: + specifier: ^2.4.9 + version: 2.15.0(@types/node@26.2.0)(typescript@4.9.5) + sass: + specifier: ^1.79.3 + version: 1.103.1 + typescript: + specifier: ^4.9.5 + version: 4.9.5 + vite: + specifier: ^5.4.19 + version: 5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + vite-plugin-externals: + specifier: ^0.6.2 + version: 0.6.2(vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1)) + vitest: + specifier: ^2.1.1 + version: 2.1.9(@types/node@26.2.0)(jsdom@26.0.0)(lightningcss@1.33.0)(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(sass@1.103.1) + vitest-mock-extended: + specifier: ^2.0.2 + version: 2.0.2(typescript@4.9.5)(vitest@2.1.9(@types/node@26.2.0)(jsdom@26.0.0)(lightningcss@1.33.0)(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(sass@1.103.1)) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/css-color@6.0.7': resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} engines: {node: ^22.13.0 || >=24.0.0} @@ -208,15 +314,15 @@ packages: '@blueprintjs/colors@4.2.1': resolution: {integrity: sha512-Cx7J2YnUuxn+fi+y5XtXnBB7+cFHN4xBrRkaAetp78i3VTCXjUk+d1omrOr8TqbRucUXTdrhbZOUHpzRLFcJpQ==} - '@blueprintjs/core@3.50.4': - resolution: {integrity: sha512-FYGr1ZMH1si2acdZaTI4gqhvwgb0yOWcv7otYdi/tvC4ReId2gsm2i+bDw/eBMYsH+OSFYZT0C6hFvHV3HrkPA==} + '@blueprintjs/core@3.54.0': + resolution: {integrity: sha512-u2c1s6MNn0ocxhnC6CuiG5g3KV6b4cKUvSobznepA9SC3/AL1s3XOvT7DLWoHRv2B/vBOHFYEDzLw2/vlcGGZg==} hasBin: true peerDependencies: react: ^15.3.0 || 16 || 17 react-dom: ^15.3.0 || 16 || 17 - '@blueprintjs/datetime@3.23.14': - resolution: {integrity: sha512-zunYX1pP1T7DiTljqOCyJkngIsus4VOy9ceCt9LHMt7nb1rBsV86jX57A3V/OCNedv+KPHL0KfPeCRCNU6o//A==} + '@blueprintjs/datetime@3.24.1': + resolution: {integrity: sha512-+k5Zpmtzqqec1b+wiNMfXuw1N2pDUAlcuMAHQkZGbmrNKocpqMKLU6S6bPLGBsG0fwAbzQdYYiCCVz65m2z4Uw==} peerDependencies: react: ^15.3.0 || 16 || 17 react-dom: ^15.3.0 || 16 || 17 @@ -224,8 +330,13 @@ packages: '@blueprintjs/icons@3.33.0': resolution: {integrity: sha512-Q6qoSDIm0kRYQZISm59UUcDCpV3oeHulkLuh3bSlw0HhcSjvEQh2PSYbtaifM60Q4aK4PCd6bwJHg7lvF1x5fQ==} - '@blueprintjs/select@3.18.6': - resolution: {integrity: sha512-xxhtxKIYxSDjnaUyzNAaM0ffHHW02FU0VuEG2A9IxueX3sfGNN5HQKKxFHIEfCWabRApEILGTzRx42uvfdSUww==} + '@blueprintjs/popover2@0.14.0': + resolution: {integrity: sha512-skpa73gDB4zqLc1FzEalrgpXW0DjzJk8bA32HHxXVzCR9ELwYsfc0Zo464tS7727OBsN+U/AHXeNXrAVloagnQ==} + peerDependencies: + react: ^16.8.0 || ^17 + + '@blueprintjs/select@3.19.1': + resolution: {integrity: sha512-8UJIZMaWXRMQHr14wbmzJc/CklcSKxOU5JUux0xXKQz/hDW/g1a650tlwJmnxufvRdShbGinlVfHupCs0EL6sw==} peerDependencies: react: ^15.3.0 || 16 || 17 react-dom: ^15.3.0 || 16 || 17 @@ -238,10 +349,21 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + '@csstools/color-helpers@6.1.1': resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} engines: {node: '>=20.19.0'} + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-calc@3.3.0': resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} engines: {node: '>=20.19.0'} @@ -249,6 +371,13 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-color-parser@4.2.0': resolution: {integrity: sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==} engines: {node: '>=20.19.0'} @@ -256,6 +385,12 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@4.0.0': resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} engines: {node: '>=20.19.0'} @@ -270,142 +405,440 @@ packages: css-tree: optional: true + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.17.14': resolution: {integrity: sha512-eLOpPO1RvtsP71afiFTvS7tVFShJBCT0txiv/xjFBo5a7R7Gjw7X0IgIaFoLKhqXYAXhahoXm7qAmRXhY4guJg==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.17.14': resolution: {integrity: sha512-0CnlwnjDU8cks0yJLXfkaU/uoLyRf9VZJs4p1PskBr2AlAHeEsFEwJEo0of/Z3g+ilw5mpyDwThlxzNEIxOE4g==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.17.14': resolution: {integrity: sha512-nrfQYWBfLGfSGLvRVlt6xi63B5IbfHm3tZCdu/82zuFPQ7zez4XjmRtF/wIRYbJQ/DsZrxJdEvYFE67avYXyng==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.17.14': resolution: {integrity: sha512-eoSjEuDsU1ROwgBH/c+fZzuSyJUVXQTOIN9xuLs9dE/9HbV/A5IqdXHU1p2OfIMwBwOYJ9SFVGGldxeRCUJFyw==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.17.14': resolution: {integrity: sha512-zN0U8RWfrDttdFNkHqFYZtOH8hdi22z0pFm0aIJPsNC4QQZv7je8DWCX5iA4Zx6tRhS0CCc0XC2m7wKsbWEo5g==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.17.14': resolution: {integrity: sha512-z0VcD4ibeZWVQCW1O7szaLxGsx54gcCnajEJMdYoYjLiq4g1jrP2lMq6pk71dbS5+7op/L2Aod+erw+EUr28/A==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.17.14': resolution: {integrity: sha512-hd9mPcxfTgJlolrPlcXkQk9BMwNBvNBsVaUe5eNUqXut6weDQH8whcNaKNF2RO8NbpT6GY8rHOK2A9y++s+ehw==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.17.14': resolution: {integrity: sha512-FhAMNYOq3Iblcj9i+K0l1Fp/MHt+zBeRu/Qkf0LtrcFu3T45jcwB6A1iMsemQ42vR3GBhjNZJZTaCe3VFPbn9g==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.17.14': resolution: {integrity: sha512-BNTl+wSJ1omsH8s3TkQmIIIQHwvwJrU9u1ggb9XU2KTVM4TmthRIVyxSp2qxROJHhZuW/r8fht46/QE8hU8Qvg==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.17.14': resolution: {integrity: sha512-91OK/lQ5y2v7AsmnFT+0EyxdPTNhov3y2CWMdizyMfxSxRqHazXdzgBKtlmkU2KYIc+9ZK3Vwp2KyXogEATYxQ==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.17.14': resolution: {integrity: sha512-vp15H+5NR6hubNgMluqqKza85HcGJgq7t6rMH7O3Y6ApiOWPkvW2AJfNojUQimfTp6OUrACUXfR4hmpcENXoMQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.17.14': resolution: {integrity: sha512-90TOdFV7N+fgi6c2+GO9ochEkmm9kBAKnuD5e08GQMgMINOdOFHuYLPQ91RYVrnWwQ5683sJKuLi9l4SsbJ7Hg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.17.14': resolution: {integrity: sha512-NnBGeoqKkTugpBOBZZoktQQ1Yqb7aHKmHxsw43NddPB2YWLAlpb7THZIzsRsTr0Xw3nqiPxbA1H31ZMOG+VVPQ==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.17.14': resolution: {integrity: sha512-0qdlKScLXA8MGVy21JUKvMzCYWovctuP8KKqhtE5A6IVPq4onxXhSuhwDd2g5sRCzNDlDjitc5sX31BzDoL5Fw==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.17.14': resolution: {integrity: sha512-Hdm2Jo1yaaOro4v3+6/zJk6ygCqIZuSDJHdHaf8nVH/tfOuoEX5Riv03Ka15LmQBYJObUTNS1UdyoMk0WUn9Ww==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.17.14': resolution: {integrity: sha512-8KHF17OstlK4DuzeF/KmSgzrTWQrkWj5boluiiq7kvJCiQVzUrmSkaBvcLB2UgHpKENO2i6BthPkmUhNDaJsVw==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.17.14': resolution: {integrity: sha512-nVwpqvb3yyXztxIT2+VsxJhB5GCgzPdk1n0HHSnchRAcxqKO6ghXwHhJnr0j/B+5FSyEqSxF4q03rbA2fKXtUQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.17.14': resolution: {integrity: sha512-1RZ7uQQ9zcy/GSAJL1xPdN7NDdOOtNEGiJalg/MOzeakZeTrgH/DoCkbq7TaPDiPhWqnDF+4bnydxRqQD7il6g==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.17.14': resolution: {integrity: sha512-nqMjDsFwv7vp7msrwWRysnM38Sd44PKmW8EzV01YzDBTcTWUpczQg6mGao9VLicXSgW/iookNK6AxeogNVNDZA==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.17.14': resolution: {integrity: sha512-xrD0mccTKRBBIotrITV7WVQAwNJ5+1va6L0H9zN92v2yEdjfAN7864cUaZwJS7JPEs53bDTzKFbfqVlG2HhyKQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.17.14': resolution: {integrity: sha512-nXpkz9bbJrLLyUTYtRotSS3t5b+FOuljg8LgLdINWFs3FfqZMtbnBCZFUmBzQPyxqU87F8Av+3Nco/M3hEcu1w==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.17.14': resolution: {integrity: sha512-gPQmsi2DKTaEgG14hc3CHXHp62k8g6qr0Pas+I4lUxRMugGSATh/Bi8Dgusoz9IQ0IfdrvLpco6kujEIBoaogA==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@exodus/bytes@1.15.1': resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -421,6 +854,41 @@ packages: prop-types: ^15.0.0 react: '>=0.14.0' + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.3.0': + resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@12.0.1': + resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.8': + resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.1.0': + resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@istanbuljs/schema@0.1.6': resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} @@ -444,6 +912,19 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -456,16 +937,107 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-project/types@0.144.0': resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} - '@playwright/test@1.29.0': - resolution: {integrity: sha512-gp5PVBenxTJsm2bATWDNc2CCnrL5OaA/MXQdJwwkGQtqTjmY+ZOqAdLqo49O9MLTDh2vYh+tHWDnmFsILnWaeA==} - engines: {node: '>=14'} - deprecated: Please update to the latest version of Playwright to test up-to-date browsers. - hasBin: true - - '@rolldown/binding-android-arm64@1.2.4': + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + + '@playwright/test@1.29.0': + resolution: {integrity: sha512-gp5PVBenxTJsm2bATWDNc2CCnrL5OaA/MXQdJwwkGQtqTjmY+ZOqAdLqo49O9MLTDh2vYh+tHWDnmFsILnWaeA==} + engines: {node: '>=14'} + deprecated: Please update to the latest version of Playwright to test up-to-date browsers. + hasBin: true + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rolldown/binding-android-arm64@1.2.4': resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] @@ -549,9 +1121,137 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.63.1': + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.1': + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.1': + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.1': + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.1': + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.1': + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.63.1': + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.63.1': + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.63.1': + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.63.1': + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.63.1': + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.1': + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.1': + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.1': + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} + cpu: [x64] + os: [win32] + '@samepage/scripts@0.74.5': resolution: {integrity: sha512-0hy5IOG2+PXqetIQPuheTKYPnKxDHw4x0s9qjytlEgZC2faiYyMkAqK3MUumuocbz1OWX43+yPs7ujRAZqVl9g==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} @@ -611,10 +1311,181 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@storybook/core@8.6.18': + resolution: {integrity: sha512-dRBP2TnX6fGdS0T2mXBHjkS/3Nlu1ra1huovZVFuM67CYMzrhM/3hX/zru1vWSC5rqY93ZaAhjMciPW4pK5mMQ==} + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/instrumenter@8.6.15': + resolution: {integrity: sha512-TvHR/+yyIAOp/1bLulFai2kkhIBtAlBw7J6Jd9DKyInoGhTWNE1G1Y61jD5GWXX29AlwaHfzGUaX5NL1K+FJpg==} + peerDependencies: + storybook: ^8.6.15 + + '@storybook/test@8.6.15': + resolution: {integrity: sha512-EwquDRUDVvWcZds3T2abmB5wSN/Vattal4YtZ6fpBlIUqONV4o/cOBX39cFfQSUCBrIXIjQ6RmapQCHK/PvBYw==} + peerDependencies: + storybook: ^8.6.15 + + '@storybook/theming@8.6.18': + resolution: {integrity: sha512-n6OEjEtHupa2PdTwWzRepr7cO8NkDd4rgF6BKLitRbujOspLxzMBEqdphs+QLcuiCIgf33SqmEA64QWnbSMhPw==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@swc/core-darwin-arm64@1.16.1': + resolution: {integrity: sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.16.1': + resolution: {integrity: sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.16.1': + resolution: {integrity: sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.16.1': + resolution: {integrity: sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.16.1': + resolution: {integrity: sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.16.1': + resolution: {integrity: sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.16.1': + resolution: {integrity: sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.16.1': + resolution: {integrity: sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.16.1': + resolution: {integrity: sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.16.1': + resolution: {integrity: sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.16.1': + resolution: {integrity: sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.16.1': + resolution: {integrity: sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.16.1': + resolution: {integrity: sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.28': + resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} + + '@tanstack/query-core@4.44.0': + resolution: {integrity: sha512-swSgb7OiPRR3UuIL7NuDrZNSMGmQD+wdtHxPD7j60SvBEnxbXurl5XOirtGEX2gm2hbK6mC8kMV1I+uO3l0UOw==} + + '@tanstack/query-persist-client-core@4.44.0': + resolution: {integrity: sha512-SW2gsKDR4IGnOBP04QsyMkmSuGrB1DCngwOEJnUiGMjTVZrkFjmUrkdeYn39eXFaaCgPJnvRc+WysiSGwW1y1g==} + + '@tanstack/react-query-persist-client@4.44.0': + resolution: {integrity: sha512-MEb6w7t4OOVKqN7PWBsQlXMWj3ojytueFzNupc6+jGhT6pmeqx80S7e/H9nbTUHW7xwmkN4iqAuLgD+Zaihj8Q==} + peerDependencies: + '@tanstack/react-query': ^4.44.0 + + '@tanstack/react-query@4.44.0': + resolution: {integrity: sha512-RuIqHYrS98LrK/8kJJOJMMSQ/BCpojwsXDh7p0fBmp38ZOz6dlk+uyFRRusH+V+t3POoCsDOQ2zhomEYOeReXw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + + '@testing-library/jest-dom@6.5.0': + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react-hooks@8.0.1': + resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + + '@testing-library/react@12.1.5': + resolution: {integrity: sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==} + engines: {node: '>=12'} + peerDependencies: + react: <18.0.0 + react-dom: <18.0.0 + '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} @@ -630,6 +1501,12 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@testing-library/user-event@14.6.4': resolution: {integrity: sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==} engines: {node: '>=12', npm: '>=6'} @@ -706,9 +1583,15 @@ packages: '@types/scheduler@0.26.0': resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/sizzle@2.3.10': resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -853,9 +1736,31 @@ packages: resolution: {integrity: sha512-FGNvVZ5pgX9FaBqkPt6VkYFZ6bWAMDzYi7nxW+1Xt+Z4fn5PuTULVwsxjKc+0uKhysyWBQmvsmM50Oh6C2/oMA==} engines: {node: '>= 20'} + '@vitejs/plugin-react-swc@3.11.0': + resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.1.10': resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.1.10': resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: @@ -867,18 +1772,42 @@ packages: vite: optional: true + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.10': resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.1.10': resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.1.10': resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.1.10': resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} @@ -894,6 +1823,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -937,13 +1874,24 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.16.3: + resolution: {integrity: sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==} + engines: {node: '>=4'} + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -957,15 +1905,32 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - axios@0.33.0: - resolution: {integrity: sha512-juz19g7B3UWT9r3+tgRFQf2Bdy6IKDVroiyuVOUrkILVKHpqHL++K1l8ybvfdEP9B/VE72vk8vllbnedVCm/og==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios-retry@4.5.0: + resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==} + peerDependencies: + axios: 0.x || 1.x + + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer-es6@0.7.0: + resolution: {integrity: sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==} + engines: {node: '>=6.0.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -989,6 +1954,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-assert@1.2.1: + resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -1000,6 +1968,10 @@ packages: engines: {node: '>=10.12.0'} hasBin: true + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1019,10 +1991,18 @@ packages: ccount@1.1.0: resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1039,10 +2019,18 @@ packages: character-reference-invalid@1.1.4: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chrono-node@2.3.0: resolution: {integrity: sha512-70DhMEY3RiwB/9+Fstpu9cbI+ycO/WNHxCOI1Lvo+LLiVdhMVOJ8B3jzXgZHCiGbZc7SKHFsP7apeeuP/EAw+Q==} @@ -1052,6 +2040,10 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + clipboardy@2.3.0: resolution: {integrity: sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==} engines: {node: '>=8'} @@ -1059,6 +2051,10 @@ packages: cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1098,6 +2094,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-pure@3.50.0: resolution: {integrity: sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==} @@ -1116,6 +2116,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + cross-spawn@6.0.6: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} @@ -1131,11 +2134,18 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1147,6 +2157,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1173,10 +2187,22 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@1.1.2: resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} engines: {node: '>= 0.4'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -1184,6 +2210,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -1213,12 +2243,24 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@3.4.0: resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} dom4@2.1.6: resolution: {integrity: sha512-JkCVGnN4ofKGbjf5Uvc8mmxaATIErKQKSgACdBXpsQ3fY6DlIpAyWfiBSrGkttATssbDCp3psiAKWXk5gmjycA==} + domexception@1.0.1: + resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==} + deprecated: Use your platform's native DOMException instead + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1253,6 +2295,15 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-module-lexer@0.4.1: + resolution: {integrity: sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -1264,15 +2315,35 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + esbuild@0.17.14: resolution: {integrity: sha512-vOO5XhmVj/1XQR9NQ1UPq6qvMYL7QFJU57J5fKBKBKxp17uDt5PgxFDb4A2nEiXhr1qQs4x0F5+66hVVw4ruNw==} engines: {node: '>=12'} hasBin: true + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1288,10 +2359,22 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + fake-indexeddb@4.0.2: + resolution: {integrity: sha512-SdTwEhnakbgazc7W3WUXOJfGmhH0YfG4d+dRPOFoYDRTL6U5t8tvrmkf2W/C3W1jk2ylV7Wrnj44RASqpX/lEw==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1315,6 +2398,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1331,6 +2418,10 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -1346,6 +2437,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -1368,6 +2463,10 @@ packages: resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==} engines: {node: '>= 0.6.0'} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1407,9 +2506,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gud@1.0.0: resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1444,6 +2551,13 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1454,6 +2568,18 @@ packages: html-void-elements@1.0.5: resolution: {integrity: sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -1462,8 +2588,8 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - idb@6.0.0: - resolution: {integrity: sha512-+M367poGtpzAylX4pwcrZIa7cFQLfNkAOlMMLN2kw/2jGfJP6h+TB/unQNSVYwNtP8XqkLYrfuiVnxLQNP1tjA==} + idb@6.1.5: + resolution: {integrity: sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==} ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1471,6 +2597,13 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -1483,6 +2616,10 @@ packages: engines: {node: '>=10'} hasBin: true + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -1493,17 +2630,33 @@ packages: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-ci@2.0.0: resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} hasBin: true @@ -1532,6 +2685,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1539,9 +2696,17 @@ packages: is-hexadecimal@1.0.4: resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1553,6 +2718,18 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -1561,6 +2738,26 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -1568,6 +2765,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1583,6 +2783,10 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + jest-fetch-mock@3.2.0: + resolution: {integrity: sha512-7yGuHznS2mQSUr17H6Q/YMKExuey1ush3Vdvm1QsUzi7PzajSNBiAvMxtS1nucKJYiIF3g4qTtHxG9T10vJjWg==} + engines: {node: '>=12'} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -1600,6 +2804,19 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsdoc-type-pratt-parser@4.8.0: + resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} + engines: {node: '>=12.0.0'} + + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsdom@30.0.1: resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -1731,10 +2948,19 @@ packages: lodash.union@4.6.0: resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -1743,6 +2969,9 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1793,6 +3022,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -1806,6 +3039,20 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.15.0: + resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1817,11 +3064,23 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1841,6 +3100,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nwsapi@2.2.27: + resolution: {integrity: sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1849,6 +3111,10 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-is@1.1.6: resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} engines: {node: '>= 0.4'} @@ -1857,6 +3123,10 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -1872,6 +3142,10 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + os-paths@4.4.0: resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} engines: {node: '>= 6.0'} @@ -1880,6 +3154,9 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -1928,9 +3205,19 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1959,6 +3246,10 @@ packages: resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -2015,14 +3306,19 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -2031,6 +3327,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + query-string@6.14.1: + resolution: {integrity: sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2044,6 +3344,15 @@ packages: peerDependencies: react: 17.0.2 + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2058,6 +3367,13 @@ packages: peerDependencies: react: 0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0 + react-popper@2.3.0: + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + react-transition-group@2.9.0: resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} peerDependencies: @@ -2085,9 +3401,28 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + realistic-structured-clone@3.0.0: + resolution: {integrity: sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -2100,9 +3435,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resize-observer-polyfill@1.5.1: - resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -2112,6 +3444,9 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2164,6 +3499,14 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.63.1: + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2173,9 +3516,18 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass@1.103.1: + resolution: {integrity: sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==} + engines: {node: '>=20.19.0'} + hasBin: true + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -2183,6 +3535,17 @@ packages: scheduler@0.20.2: resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} + scite-badge@https://codeload.github.com/scitedotai/scite-badge/tar.gz/80bda7fa058531a1b3f107b91f43d0cb35b6375f: + resolution: {tarball: https://codeload.github.com/scitedotai/scite-badge/tar.gz/80bda7fa058531a1b3f107b91f43d0cb35b6375f} + version: 8.0.6 + + scite-widget@https://codeload.github.com/scitedotai/scite-widget/tar.gz/cfd6c21cab3209644c6bcc8544009486a411dab5: + resolution: {tarball: https://codeload.github.com/scitedotai/scite-widget/tar.gz/cfd6c21cab3209644c6bcc8544009486a411dab5} + version: 1.0.0 + peerDependencies: + react: ^17.0.2 + react-dom: ^17.0.2 + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -2192,6 +3555,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@3.1.2: + resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2219,12 +3585,32 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} @@ -2236,15 +3622,54 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + storybook@8.6.18: + resolution: {integrity: sha512-p8seiSI6FiVY6P3V0pG+5v7c8pDMehMAFRWEhG5XqIBSQszzOjDnW2rNvm3odoLKfo3V3P6Cs6Hv9ILzymULyQ==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2270,6 +3695,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2286,6 +3715,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwindcss@3.3.2: resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==} engines: {node: '>=14.0.0'} @@ -2310,9 +3743,15 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} @@ -2321,13 +3760,32 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.1: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.4.10: resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tldts@7.4.10: resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} hasBin: true @@ -2340,14 +3798,40 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tough-cookie@6.0.2: resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + tributejs@5.1.3: + resolution: {integrity: sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==} + + ts-essentials@10.2.1: + resolution: {integrity: sha512-+Id1fRkuir+CsgK2x04/icS2b4V1hQmq7ObzIrDjhN0ozfRYivnP7aaKMVJfLApQm0trjR39A6NIMVchiB9Erw==} + peerDependencies: + typescript: '>=4.5.0' + peerDependenciesMeta: + typescript: + optional: true + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -2365,9 +3849,6 @@ packages: '@swc/wasm': optional: true - tslib@1.13.0: - resolution: {integrity: sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==} - tslib@2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -2377,14 +3858,31 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@5.9.0: + resolution: {integrity: sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==} + engines: {node: '>=20'} + typed-styles@0.0.7: resolution: {integrity: sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==} + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} hasBin: true + typeson-registry@1.0.0-alpha.39: + resolution: {integrity: sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==} + engines: {node: '>=10.0.0'} + + typeson@6.1.0: + resolution: {integrity: sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==} + engines: {node: '>=0.1.14'} + undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -2403,6 +3901,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -2411,6 +3912,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -2418,6 +3922,48 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-externals@0.6.2: + resolution: {integrity: sha512-R5oVY8xDJjLXLTs2XDYzvYbc/RTZuIwOx2xcFbYf+/VXB6eJuatDgt8jzQ7kZ+IrgwQhe6tU8U2fTyy72C25CQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: '>=2.0.0' + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@8.2.1: resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2461,6 +4007,37 @@ packages: yaml: optional: true + vitest-mock-extended@2.0.2: + resolution: {integrity: sha512-n3MBqVITKyclZ0n0y66hkT4UiiEYFQn9tteAnIxT0MPz1Z8nFcPUG3Cf0cZOyoPOj/cq6Ab1XFw2lM/qM5EDWQ==} + peerDependencies: + typescript: 3.x || 4.x || 5.x + vitest: '>=2.0.0' + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.1.10: resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2516,14 +4093,44 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-mimetype@5.0.0: resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} engines: {node: '>=20'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@16.0.1: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2532,6 +4139,25 @@ packages: resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} engines: {node: ^22.14.0 || >=24.0.0} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -2553,6 +4179,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xdg-app-paths@5.5.1: resolution: {integrity: sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==} engines: {node: '>= 6.0'} @@ -2596,10 +4234,18 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs@16.2.2: resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} engines: {node: '>=10'} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -2617,8 +4263,18 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@alloc/quick-lru@5.2.0': {} + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@asamuzakjp/css-color@6.0.7': dependencies: '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -2830,10 +4486,11 @@ snapshots: dependencies: tslib: 2.5.3 - '@blueprintjs/core@3.50.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@blueprintjs/core@3.54.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@blueprintjs/colors': 4.2.1 '@blueprintjs/icons': 3.33.0 + '@juggle/resize-observer': 3.4.0 '@types/dom4': 2.0.4 classnames: 2.5.1 dom4: 2.1.6 @@ -2844,31 +4501,43 @@ snapshots: react-lifecycles-compat: 3.0.4 react-popper: 1.3.11(react@17.0.2) react-transition-group: 2.9.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - resize-observer-polyfill: 1.5.1 - tslib: 1.13.0 + tslib: 2.3.1 - '@blueprintjs/datetime@3.23.14(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@blueprintjs/datetime@3.24.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@blueprintjs/core': 3.50.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/core': 3.54.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) classnames: 2.5.1 react: 17.0.2 react-day-picker: 7.4.9(react@17.0.2) react-dom: 17.0.2(react@17.0.2) react-lifecycles-compat: 3.0.4 - tslib: 1.13.0 + tslib: 2.3.1 '@blueprintjs/icons@3.33.0': dependencies: classnames: 2.5.1 tslib: 2.3.1 - '@blueprintjs/select@3.18.6(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@blueprintjs/popover2@0.14.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@blueprintjs/core': 3.54.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@juggle/resize-observer': 3.4.0 + '@popperjs/core': 2.11.8 + classnames: 2.5.1 + dom4: 2.1.6 + react: 17.0.2 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + tslib: 2.3.1 + transitivePeerDependencies: + - react-dom + + '@blueprintjs/select@3.19.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@blueprintjs/core': 3.50.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/core': 3.54.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) classnames: 2.5.1 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - tslib: 1.13.0 + tslib: 2.3.1 '@bramus/specificity@2.4.2': dependencies: @@ -2878,13 +4547,27 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.1.1': {} + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-color-parser@4.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/color-helpers': 6.1.1 @@ -2892,6 +4575,10 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-tokenizer': 4.0.0 @@ -2900,74 +4587,223 @@ snapshots: optionalDependencies: css-tree: 3.2.1 + '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/android-arm64@0.17.14': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm@0.17.14': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-x64@0.17.14': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.17.14': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.17.14': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.17.14': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.17.14': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.17.14': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm@0.17.14': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-ia32@0.17.14': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-loong64@0.17.14': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.17.14': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.17.14': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.17.14': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-s390x@0.17.14': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-x64@0.17.14': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.17.14': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.17.14': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.17.14': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.17.14': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-ia32@0.17.14': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-x64@0.17.14': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@exodus/bytes@1.15.1': {} '@hypnosphi/create-react-context@0.3.1(prop-types@15.8.1)(react@17.0.2)': @@ -2977,6 +4813,33 @@ snapshots: react: 17.0.2 warning: 4.0.3 + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.3.0(@types/node@26.2.0)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@26.2.0) + '@inquirer/type': 4.1.0(@types/node@26.2.0) + optionalDependencies: + '@types/node': 26.2.0 + + '@inquirer/core@12.0.1(@types/node@26.2.0)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.2.0) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 26.2.0 + + '@inquirer/figures@2.0.8': {} + + '@inquirer/type@4.1.0(@types/node@26.2.0)': + optionalDependencies: + '@types/node': 26.2.0 + '@istanbuljs/schema@0.1.6': {} '@jcubic/lily@0.3.0': {} @@ -3000,6 +4863,20 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@juggle/resize-observer@3.4.0': {} + + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3012,13 +4889,83 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@oxc-project/types@0.144.0': {} + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.5 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + optional: true + '@playwright/test@1.29.0': dependencies: '@types/node': 26.2.0 playwright-core: 1.29.0 + '@popperjs/core@2.11.8': {} + '@rolldown/binding-android-arm64@1.2.4': optional: true @@ -3061,24 +5008,101 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.1': {} - '@samepage/scripts@0.74.5(@aws-sdk/client-lambda@3.1111.0)(@aws-sdk/client-s3@3.1111.0)(@samepage/testing@0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)))(archiver@5.3.2)(axios@0.33.0(debug@4.4.3))(debug@4.4.3)(dotenv@16.6.1)(esbuild@0.17.14)(patch-package@6.5.1)(tailwindcss@3.3.2(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)))(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2))(zod@4.1.11)': + '@rollup/rollup-android-arm-eabi@4.63.1': + optional: true + + '@rollup/rollup-android-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-x64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.1': + optional: true + + '@samepage/scripts@0.74.5(@aws-sdk/client-lambda@3.1111.0)(@aws-sdk/client-s3@3.1111.0)(@samepage/testing@0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)))(archiver@5.3.2)(axios@1.20.0(debug@4.4.3))(debug@4.4.3)(dotenv@16.6.1)(esbuild@0.17.14)(patch-package@6.5.1)(tailwindcss@3.3.2(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)))(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2))(zod@4.1.11)': dependencies: '@aws-sdk/client-lambda': 3.1111.0 '@aws-sdk/client-s3': 3.1111.0 - '@samepage/testing': 0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)) + '@samepage/testing': 0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)) archiver: 5.3.2 - axios: 0.33.0(debug@4.4.3) + axios: 1.20.0(debug@4.4.3) debug: 4.4.3 dotenv: 16.6.1 esbuild: 0.17.14 patch-package: 6.5.1 - tailwindcss: 3.3.2(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)) - ts-node: 10.9.2(@types/node@26.2.0)(typescript@7.0.2) + tailwindcss: 3.3.2(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)) + ts-node: 10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2) zod: 4.1.11 - '@samepage/testing@0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2))': + '@samepage/testing@0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2))': dependencies: '@playwright/test': 1.29.0 '@testing-library/react': 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -3088,42 +5112,174 @@ snapshots: debug: 4.4.3 dotenv: 16.6.1 jsdom: 30.0.1 - ts-node: 10.9.2(@types/node@26.2.0)(typescript@7.0.2) + ts-node: 10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2) '@smithy/core@3.33.2': dependencies: '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.5.2': + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@storybook/core@8.6.18(storybook@8.6.18)': + dependencies: + '@storybook/theming': 8.6.18(storybook@8.6.18) + better-opn: 3.0.2 + browser-assert: 1.2.1 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + jsdoc-type-pratt-parser: 4.8.0 + process: 0.11.10 + recast: 0.23.21 + semver: 7.8.5 + util: 0.12.5 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - storybook + - supports-color + - utf-8-validate + + '@storybook/global@5.0.0': {} + + '@storybook/instrumenter@8.6.15(storybook@8.6.18)': + dependencies: + '@storybook/global': 5.0.0 + '@vitest/utils': 2.1.9 + storybook: 8.6.18 + + '@storybook/test@8.6.15(storybook@8.6.18)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.6.15(storybook@8.6.18) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 8.6.18 + + '@storybook/theming@8.6.18(storybook@8.6.18)': + dependencies: + storybook: 8.6.18 + + '@swc/core-darwin-arm64@1.16.1': + optional: true + + '@swc/core-darwin-x64@1.16.1': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.16.1': + optional: true + + '@swc/core-linux-arm64-gnu@1.16.1': + optional: true + + '@swc/core-linux-arm64-musl@1.16.1': + optional: true + + '@swc/core-linux-ppc64-gnu@1.16.1': + optional: true + + '@swc/core-linux-s390x-gnu@1.16.1': + optional: true + + '@swc/core-linux-x64-gnu@1.16.1': + optional: true + + '@swc/core-linux-x64-musl@1.16.1': + optional: true + + '@swc/core-win32-arm64-msvc@1.16.1': + optional: true + + '@swc/core-win32-ia32-msvc@1.16.1': + optional: true + + '@swc/core-win32-x64-msvc@1.16.1': + optional: true + + '@swc/core@1.16.1': dependencies: - '@smithy/core': 3.33.2 - '@smithy/types': 4.17.2 - tslib: 2.8.1 + '@swc/counter': 0.1.3 + '@swc/types': 0.1.28 + optionalDependencies: + '@swc/core-darwin-arm64': 1.16.1 + '@swc/core-darwin-x64': 1.16.1 + '@swc/core-linux-arm-gnueabihf': 1.16.1 + '@swc/core-linux-arm64-gnu': 1.16.1 + '@swc/core-linux-arm64-musl': 1.16.1 + '@swc/core-linux-ppc64-gnu': 1.16.1 + '@swc/core-linux-s390x-gnu': 1.16.1 + '@swc/core-linux-x64-gnu': 1.16.1 + '@swc/core-linux-x64-musl': 1.16.1 + '@swc/core-win32-arm64-msvc': 1.16.1 + '@swc/core-win32-ia32-msvc': 1.16.1 + '@swc/core-win32-x64-msvc': 1.16.1 - '@smithy/fetch-http-handler@5.7.2': + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.28': dependencies: - '@smithy/core': 3.33.2 - '@smithy/types': 4.17.2 - tslib: 2.8.1 + '@swc/counter': 0.1.3 - '@smithy/node-http-handler@4.11.2': + '@tanstack/query-core@4.44.0': {} + + '@tanstack/query-persist-client-core@4.44.0': dependencies: - '@smithy/core': 3.33.2 - '@smithy/types': 4.17.2 - tslib: 2.8.1 + '@tanstack/query-core': 4.44.0 - '@smithy/signature-v4@5.7.2': + '@tanstack/react-query-persist-client@4.44.0(@tanstack/react-query@4.44.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': dependencies: - '@smithy/core': 3.33.2 - '@smithy/types': 4.17.2 - tslib: 2.8.1 + '@tanstack/query-persist-client-core': 4.44.0 + '@tanstack/react-query': 4.44.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@smithy/types@4.17.2': + '@tanstack/react-query@4.44.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - tslib: 2.8.1 + '@tanstack/query-core': 4.44.0 + react: 17.0.2 + use-sync-external-store: 1.6.0(react@17.0.2) + optionalDependencies: + react-dom: 17.0.2(react@17.0.2) - '@standard-schema/spec@1.1.0': {} + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 '@testing-library/dom@10.4.1': dependencies: @@ -3136,6 +5292,44 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.5.0': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.18.1 + redent: 3.0.0 + + '@testing-library/react-hooks@8.0.1(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.29.7 + react: 17.0.2 + react-error-boundary: 3.1.4(react@17.0.2) + optionalDependencies: + '@types/react': 17.0.39 + react-dom: 17.0.2(react@17.0.2) + + '@testing-library/react@12.1.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 8.20.1 + '@types/react-dom': 17.0.13 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@babel/runtime': 7.29.7 @@ -3146,6 +5340,10 @@ snapshots: '@types/react': 17.0.39 '@types/react-dom': 17.0.13 + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -3219,8 +5417,14 @@ snapshots: '@types/scheduler@0.26.0': {} + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 26.2.0 + '@types/sizzle@2.3.10': {} + '@types/statuses@2.0.6': {} + '@types/tough-cookie@4.0.5': {} '@types/unist@2.0.11': {} @@ -3311,6 +5515,28 @@ snapshots: '@vercel/cli-exec': 1.0.1 jose: 5.10.0 + '@vitejs/plugin-react-swc@3.11.0(vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.27 + '@swc/core': 1.16.1 + vite: 5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + transitivePeerDependencies: + - '@swc/helpers' + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 @@ -3320,23 +5546,52 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0))': + '@vitest/mocker@2.1.9(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.15.0(@types/node@26.2.0)(typescript@4.9.5) + vite: 5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + + '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0) + msw: 2.15.0(@types/node@26.2.0)(typescript@7.0.2) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0) + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.1 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@4.1.10': dependencies: '@vitest/utils': 4.1.10 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/snapshot@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 @@ -3344,8 +5599,29 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@4.1.10': {} + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 @@ -3360,6 +5636,14 @@ snapshots: acorn@8.18.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -3419,12 +5703,25 @@ snapshots: arg@5.0.2: {} + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + aria-query@5.3.0: dependencies: dequal: 2.0.3 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types@0.16.3: + dependencies: + tslib: 2.8.1 + async-retry@1.3.3: dependencies: retry: 0.13.1 @@ -3435,18 +5732,35 @@ snapshots: at-least-node@1.0.0: {} - axios@0.33.0(debug@4.4.3): + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios-retry@4.5.0(axios@1.20.0): + dependencies: + axios: 1.20.0(debug@4.4.3) + is-retry-allowed: 2.2.0 + + axios@1.20.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.6 - proxy-from-env: 1.1.0 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color balanced-match@1.0.2: {} + base64-arraybuffer-es6@0.7.0: {} + base64-js@1.5.1: {} + better-opn@3.0.2: + dependencies: + open: 8.4.2 + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -3474,6 +5788,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-assert@1.2.1: {} + buffer-crc32@0.2.13: {} buffer@5.7.1: @@ -3496,6 +5812,8 @@ snapshots: yargs: 16.2.2 yargs-parser: 20.2.9 + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -3517,8 +5835,21 @@ snapshots: ccount@1.1.0: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -3532,6 +5863,8 @@ snapshots: character-reference-invalid@1.1.4: {} + check-error@2.1.3: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -3544,6 +5877,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + chrono-node@2.3.0: dependencies: dayjs: 1.11.22 @@ -3552,6 +5889,8 @@ snapshots: classnames@2.5.1: {} + cli-width@4.1.0: {} + clipboardy@2.3.0: dependencies: arch: 2.2.0 @@ -3564,6 +5903,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} color-convert@2.0.1: @@ -3601,6 +5946,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + core-js-pure@3.50.0: {} core-util-is@1.0.3: {} @@ -3614,6 +5961,12 @@ snapshots: create-require@1.1.1: {} + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@6.0.6: dependencies: nice-try: 1.0.5 @@ -3635,14 +5988,26 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 + css.escape@1.5.1: {} + cssesc@3.0.0: {} + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.2.3: {} cytoscape@3.34.1: {} data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -3664,6 +6029,10 @@ snapshots: decimal.js@10.6.0: {} + decode-uri-component@0.2.2: {} + + deep-eql@5.0.2: {} + deep-equal@1.1.2: dependencies: is-arguments: 1.2.0 @@ -3673,6 +6042,27 @@ snapshots: object-keys: 1.1.1 regexp.prototype.flags: 1.5.4 + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -3683,6 +6073,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@2.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -3703,12 +6095,22 @@ snapshots: dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} + dom-helpers@3.4.0: dependencies: '@babel/runtime': 7.29.7 dom4@2.1.6: {} + domexception@1.0.1: + dependencies: + webidl-conversions: 4.0.2 + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -3733,6 +6135,22 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + + es-module-lexer@0.4.1: {} + + es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} es-object-atoms@1.1.2: @@ -3746,6 +6164,13 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + esbuild@0.17.14: optionalDependencies: '@esbuild/android-arm': 0.17.14 @@ -3771,8 +6196,65 @@ snapshots: '@esbuild/win32-ia32': 0.17.14 '@esbuild/win32-x64': 0.17.14 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + escalade@3.2.0: {} + esprima@4.0.1: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -3801,6 +6283,10 @@ snapshots: expect-type@1.4.0: {} + fake-indexeddb@4.0.2: + dependencies: + realistic-structured-clone: 3.0.0 + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3809,6 +6295,16 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3828,6 +6324,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + filter-obj@1.1.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3841,6 +6339,10 @@ snapshots: optionalDependencies: debug: 4.4.3 + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@2.0.0: dependencies: cross-spawn: 7.0.6 @@ -3860,6 +6362,12 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -3878,6 +6386,8 @@ snapshots: fuzzy@0.1.3: {} + generator-function@2.0.1: {} + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -3925,8 +6435,12 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.14.2: {} + gud@1.0.0: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -3970,6 +6484,15 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.15.1 @@ -3980,18 +6503,43 @@ snapshots: html-void-elements@1.0.5: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - idb@6.0.0: {} + idb@6.1.5: {} ieee754@1.2.1: {} immediate@3.0.6: {} + immutable@5.1.9: {} + + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -4008,6 +6556,12 @@ snapshots: line-reader: 0.4.0 xdg-basedir: 4.0.0 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + is-alphabetical@1.0.4: {} is-alphanumerical@1.0.4: @@ -4020,14 +6574,31 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.3.4: {} + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-buffer@2.0.5: {} + is-callable@1.2.7: {} + is-ci@2.0.0: dependencies: ci-info: 2.0.0 @@ -4049,28 +6620,73 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-hexadecimal@1.0.4: {} + is-map@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-potential-custom-element-name@1.0.1: {} - is-regex@1.2.1: + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-retry-allowed@2.2.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + is-string@1.1.1: dependencies: call-bound: 1.0.4 - gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.4 - is-stream@1.1.0: {} + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 - is-stream@2.0.1: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-wsl@2.2.0: dependencies: @@ -4078,6 +6694,8 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -4093,6 +6711,13 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jest-fetch-mock@3.2.0: + dependencies: + cross-fetch: 3.2.0 + domexception: 4.0.0 + transitivePeerDependencies: + - encoding + jiti@1.21.7: {} jose@5.10.0: {} @@ -4114,6 +6739,36 @@ snapshots: js-tokens@4.0.0: {} + jsdoc-type-pratt-parser@4.8.0: {} + + jsdom@26.0.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.6 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.27 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsdom@30.0.1: dependencies: '@asamuzakjp/css-color': 6.0.7 @@ -4238,14 +6893,24 @@ snapshots: lodash.union@4.6.0: {} + lodash@4.18.1: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + lru-cache@11.5.2: {} lz-string@1.5.0: {} + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4284,6 +6949,8 @@ snapshots: mimic-fn@2.1.0: {} + min-indent@1.0.1: {} + minimatch@3.1.5: dependencies: brace-expansion: 1.1.18 @@ -4296,6 +6963,59 @@ snapshots: ms@2.1.3: {} + msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5): + dependencies: + '@inquirer/confirm': 6.3.0(@types/node@26.2.0) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.2 + type-fest: 5.9.0 + until-async: 3.0.2 + yargs: 17.7.3 + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - '@types/node' + + msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2): + dependencies: + '@inquirer/confirm': 6.3.0(@types/node@26.2.0) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.2 + type-fest: 5.9.0 + until-async: 3.0.2 + yargs: 17.7.3 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@types/node' + optional: true + + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -4306,8 +7026,15 @@ snapshots: nice-try@1.0.5: {} + node-addon-api@7.1.1: + optional: true + node-domexception@1.0.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -4326,10 +7053,14 @@ snapshots: dependencies: path-key: 3.1.1 + nwsapi@2.2.27: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} + object-inspect@1.13.4: {} + object-is@1.1.6: dependencies: call-bind: 1.0.9 @@ -4337,6 +7068,15 @@ snapshots: object-keys@1.1.1: {} + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + obug@2.1.4: {} once@1.4.0: @@ -4352,10 +7092,18 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + os-paths@4.4.0: {} os-tmpdir@1.0.2: {} + outvariant@1.4.3: {} + p-finally@1.0.0: {} p-limit@3.1.0: @@ -4412,8 +7160,14 @@ snapshots: path-parse@1.0.7: {} + path-to-regexp@6.3.0: {} + + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -4428,6 +7182,8 @@ snapshots: popper.js@1.16.1: {} + possible-typed-array-names@1.1.0: {} + postcss-import@15.1.0(postcss@8.5.26): dependencies: postcss: 8.5.26 @@ -4440,13 +7196,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.26 - postcss-load-config@4.0.2(postcss@8.5.26)(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)): + postcss-load-config@4.0.2(postcss@8.5.26)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)): dependencies: lilconfig: 3.1.3 yaml: 2.9.0 optionalDependencies: postcss: 8.5.26 - ts-node: 10.9.2(@types/node@26.2.0)(typescript@7.0.2) + ts-node: 10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2) postcss-nested@6.2.0(postcss@8.5.26): dependencies: @@ -4478,6 +7234,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4488,7 +7246,7 @@ snapshots: dependencies: xtend: 4.0.2 - proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} pump@3.0.4: dependencies: @@ -4497,6 +7255,13 @@ snapshots: punycode@2.3.1: {} + query-string@6.14.1: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + queue-microtask@1.2.3: {} react-day-picker@7.4.9(react@17.0.2): @@ -4511,6 +7276,13 @@ snapshots: react: 17.0.2 scheduler: 0.20.2 + react-error-boundary@3.1.4(react@17.0.2): + dependencies: + '@babel/runtime': 7.29.7 + react: 17.0.2 + + react-fast-compare@3.2.2: {} + react-is@16.13.1: {} react-is@17.0.2: {} @@ -4528,6 +7300,14 @@ snapshots: typed-styles: 0.0.7 warning: 4.0.3 + react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@popperjs/core': 2.11.8 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-fast-compare: 3.2.2 + warning: 4.0.3 + react-transition-group@2.9.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: dom-helpers: 3.4.0 @@ -4570,12 +7350,35 @@ snapshots: dependencies: picomatch: 2.3.2 + readdirp@5.1.1: {} + + realistic-structured-clone@3.0.0: + dependencies: + domexception: 1.0.1 + typeson: 6.1.0 + typeson-registry: 1.0.0-alpha.39 + + recast@0.23.21: + dependencies: + ast-types: 0.16.3 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + refractor@3.6.0: dependencies: hastscript: 6.0.0 parse-entities: 2.0.0 prismjs: 1.27.0 + regenerator-runtime@0.13.11: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.9 @@ -4589,8 +7392,6 @@ snapshots: require-from-string@2.0.2: {} - resize-observer-polyfill@1.5.1: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -4600,6 +7401,8 @@ snapshots: retry@0.13.1: {} + rettime@0.11.11: {} + reusify@1.1.0: {} rimraf@2.7.1: @@ -4610,12 +7413,12 @@ snapshots: dependencies: glob: 7.2.3 - roamjs-components@0.89.0(8b3dba144aa4c6a891ee791b6ddc21c2): + roamjs-components@0.89.0(dd2f1f144015bc13ec70826008c78270): dependencies: - '@blueprintjs/core': 3.50.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@blueprintjs/datetime': 3.23.14(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@blueprintjs/select': 3.18.6(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@samepage/scripts': 0.74.5(@aws-sdk/client-lambda@3.1111.0)(@aws-sdk/client-s3@3.1111.0)(@samepage/testing@0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)))(archiver@5.3.2)(axios@0.33.0(debug@4.4.3))(debug@4.4.3)(dotenv@16.6.1)(esbuild@0.17.14)(patch-package@6.5.1)(tailwindcss@3.3.2(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)))(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2))(zod@4.1.11) + '@blueprintjs/core': 3.54.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/datetime': 3.24.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@blueprintjs/select': 3.19.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@samepage/scripts': 0.74.5(@aws-sdk/client-lambda@3.1111.0)(@aws-sdk/client-s3@3.1111.0)(@samepage/testing@0.74.5(@playwright/test@1.29.0)(@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@17.0.13)(@types/react@17.0.39)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1))(@types/jsdom@20.0.1)(c8@7.14.0)(debug@4.4.3)(dotenv@16.6.1)(jsdom@30.0.1)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)))(archiver@5.3.2)(axios@1.20.0(debug@4.4.3))(debug@4.4.3)(dotenv@16.6.1)(esbuild@0.17.14)(patch-package@6.5.1)(tailwindcss@3.3.2(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)))(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2))(zod@4.1.11) '@types/crypto-js': 4.1.1 '@types/cytoscape': 3.31.0 '@types/file-saver': 2.0.5 @@ -4634,7 +7437,7 @@ snapshots: file-saver: 2.0.2 fuzzy: 0.1.3 hast-util-to-html: 7.1.3 - idb: 6.0.0 + idb: 6.1.5 insect: 5.9.1 jsdom: 30.0.1 jszip: 3.10.0 @@ -4681,6 +7484,40 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.4 '@rolldown/binding-win32-x64-msvc': 1.2.4 + rollup@4.63.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.1 + '@rollup/rollup-android-arm64': 4.63.1 + '@rollup/rollup-darwin-arm64': 4.63.1 + '@rollup/rollup-darwin-x64': 4.63.1 + '@rollup/rollup-freebsd-arm64': 4.63.1 + '@rollup/rollup-freebsd-x64': 4.63.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 + '@rollup/rollup-linux-arm64-gnu': 4.63.1 + '@rollup/rollup-linux-arm64-musl': 4.63.1 + '@rollup/rollup-linux-loong64-gnu': 4.63.1 + '@rollup/rollup-linux-loong64-musl': 4.63.1 + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 + '@rollup/rollup-linux-ppc64-musl': 4.63.1 + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 + '@rollup/rollup-linux-riscv64-musl': 4.63.1 + '@rollup/rollup-linux-s390x-gnu': 4.63.1 + '@rollup/rollup-linux-x64-gnu': 4.63.1 + '@rollup/rollup-linux-x64-musl': 4.63.1 + '@rollup/rollup-openbsd-x64': 4.63.1 + '@rollup/rollup-openharmony-arm64': 4.63.1 + '@rollup/rollup-win32-arm64-msvc': 4.63.1 + '@rollup/rollup-win32-ia32-msvc': 4.63.1 + '@rollup/rollup-win32-x64-gnu': 4.63.1 + '@rollup/rollup-win32-x64-msvc': 4.63.1 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4689,8 +7526,22 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sass@1.103.1: + dependencies: + chokidar: 5.0.0 + immutable: 5.1.9 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.6.0 + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -4700,10 +7551,34 @@ snapshots: loose-envify: 1.4.0 object-assign: 4.1.1 + scite-badge@https://codeload.github.com/scitedotai/scite-badge/tar.gz/80bda7fa058531a1b3f107b91f43d0cb35b6375f: + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.5.1 + jest-fetch-mock: 3.2.0 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + regenerator-runtime: 0.13.11 + scite-widget: https://codeload.github.com/scitedotai/scite-widget/tar.gz/cfd6c21cab3209644c6bcc8544009486a411dab5(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + whatwg-fetch: 3.6.20 + transitivePeerDependencies: + - encoding + + scite-widget@https://codeload.github.com/scitedotai/scite-widget/tar.gz/cfd6c21cab3209644c6bcc8544009486a411dab5(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + classnames: 2.5.1 + query-string: 6.14.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + whatwg-fetch: 3.6.20 + semver@5.7.2: {} semver@7.8.5: {} + set-cookie-parser@3.1.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4734,10 +7609,40 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-swizzle@0.2.4: dependencies: is-arrayish: 0.3.4 @@ -4746,12 +7651,39 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + + sourcemap-codec@1.4.8: {} + space-separated-tokens@1.1.5: {} + split-on-first@1.1.0: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + + std-env@3.10.0: {} + std-env@4.2.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + storybook@8.6.18: + dependencies: + '@storybook/core': 8.6.18(storybook@8.6.18) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + strict-event-emitter@0.5.1: {} + + strict-uri-encode@2.0.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4780,6 +7712,10 @@ snapshots: strip-final-newline@2.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -4798,7 +7734,9 @@ snapshots: symbol-tree@3.2.4: {} - tailwindcss@3.3.2(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)): + tagged-tag@1.0.0: {} + + tailwindcss@3.3.2(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -4817,7 +7755,7 @@ snapshots: postcss: 8.5.26 postcss-import: 15.1.0(postcss@8.5.26) postcss-js: 4.1.0(postcss@8.5.26) - postcss-load-config: 4.0.2(postcss@8.5.26)(ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2)) + postcss-load-config: 4.0.2(postcss@8.5.26)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2)) postcss-nested: 6.2.0(postcss@8.5.26) postcss-selector-parser: 6.1.4 postcss-value-parser: 4.2.0 @@ -4850,8 +7788,12 @@ snapshots: throttleit@2.1.0: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinyexec@1.3.0: {} tinyglobby@0.2.17: @@ -4859,10 +7801,22 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + tinyrainbow@3.1.1: {} + tinyspy@3.0.2: {} + + tldts-core@6.1.86: {} + tldts-core@7.4.10: {} + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + tldts@7.4.10: dependencies: tldts-core: 7.4.10 @@ -4875,17 +7829,37 @@ snapshots: dependencies: is-number: 7.0.0 + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + tough-cookie@6.0.2: dependencies: tldts: 7.4.10 + tr46@0.0.3: {} + + tr46@2.1.0: + dependencies: + punycode: 2.3.1 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tr46@6.0.0: dependencies: punycode: 2.3.1 + tributejs@5.1.3: {} + + ts-essentials@10.2.1(typescript@4.9.5): + optionalDependencies: + typescript: 4.9.5 + ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@26.2.0)(typescript@7.0.2): + ts-node@10.9.2(@swc/core@1.16.1)(@types/node@26.2.0)(typescript@7.0.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.13 @@ -4902,8 +7876,8 @@ snapshots: typescript: 7.0.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - - tslib@1.13.0: {} + optionalDependencies: + '@swc/core': 1.16.1 tslib@2.3.1: {} @@ -4911,8 +7885,14 @@ snapshots: tslib@2.8.1: {} + type-fest@5.9.0: + dependencies: + tagged-tag: 1.0.0 + typed-styles@0.0.7: {} + typescript@4.9.5: {} + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -4936,6 +7916,14 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 + typeson-registry@1.0.0-alpha.39: + dependencies: + base64-arraybuffer-es6: 0.7.0 + typeson: 6.1.0 + whatwg-url: 8.7.0 + + typeson@6.1.0: {} + undici-types@8.3.0: {} undici@6.28.0: {} @@ -4946,12 +7934,22 @@ snapshots: universalify@2.0.1: {} + until-async@3.0.2: {} + use-sync-external-store@1.6.0(react@17.0.2): dependencies: react: 17.0.2 util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.22 + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -4960,7 +7958,44 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0): + vite-node@2.1.9(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-externals@0.6.2(vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1)): + dependencies: + acorn: 8.18.0 + es-module-lexer: 0.4.1 + fs-extra: 10.1.0 + magic-string: 0.25.9 + vite: 5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + + vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.26 + rollup: 4.63.1 + optionalDependencies: + '@types/node': 26.2.0 + fsevents: 2.3.3 + lightningcss: 1.33.0 + sass: 1.103.1 + + vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -4972,12 +8007,55 @@ snapshots: esbuild: 0.17.14 fsevents: 2.3.3 jiti: 1.21.7 + sass: 1.103.1 yaml: 2.9.0 - vitest@4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0)): + vitest-mock-extended@2.0.2(typescript@4.9.5)(vitest@2.1.9(@types/node@26.2.0)(jsdom@26.0.0)(lightningcss@1.33.0)(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(sass@1.103.1)): + dependencies: + ts-essentials: 10.2.1(typescript@4.9.5) + typescript: 4.9.5 + vitest: 2.1.9(@types/node@26.2.0)(jsdom@26.0.0)(lightningcss@1.33.0)(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(sass@1.103.1) + + vitest@2.1.9(@types/node@26.2.0)(jsdom@26.0.0)(lightningcss@1.33.0)(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(sass@1.103.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(msw@2.15.0(@types/node@26.2.0)(typescript@4.9.5))(vite@5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + vite-node: 2.1.9(@types/node@26.2.0)(lightningcss@1.33.0)(sass@1.103.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.2.0 + jsdom: 26.0.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@4.1.10(@types/node@26.2.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.2.0)(typescript@7.0.2))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -4994,7 +8072,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.17.14)(jiti@1.21.7)(sass@1.103.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.2.0 @@ -5016,10 +8094,31 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + + webidl-conversions@4.0.2: {} + + webidl-conversions@6.1.0: {} + + webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-fetch@3.6.20: {} + + whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@16.0.1: dependencies: '@exodus/bytes': 1.15.1 @@ -5036,6 +8135,42 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + whatwg-url@8.7.0: + dependencies: + lodash: 4.18.1 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -5057,6 +8192,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.21.3: {} + xdg-app-paths@5.5.1: dependencies: os-paths: 4.4.0 @@ -5086,6 +8223,8 @@ snapshots: yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.2: dependencies: cliui: 7.0.4 @@ -5096,6 +8235,16 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/prototypes/zotero-roam-mcp/CHANGELOG.md b/prototypes/zotero-roam-mcp/CHANGELOG.md new file mode 100644 index 0000000..433efd8 --- /dev/null +++ b/prototypes/zotero-roam-mcp/CHANGELOG.md @@ -0,0 +1,232 @@ +## Changelog + +This file continues the changelog of the vendored upstream fork, [8bitgentleman/zotero-roam](https://github.com/8bitgentleman/zotero-roam). Entries up to 0.7.29 are upstream history. + +### Unreleased + +#### Features + +- Expose key functions to AI agents through Roam's MCP server, via `extensionAPI.ai.addTool` + - `zotero-search-items` (read): search loaded items by citekey, DOI, Zotero key, or title + - `zotero-import-metadata` (edit): headless "Import metadata" for a citekey, with a guard against duplicate imports + - `zotero-import-notes` (edit): headless "Import notes" for a citekey + - Registration is feature-detected and skipped on Roam builds without extension AI tools + +### [0.7.29](https://github.com/8bitgentleman/zotero-roam/compare/0.7.28...0.7.29) - 2026-05-26 + +#### Bug Fixes + +- fix topbar icon disappearing after Roam topbar HTML update + - Fall back to `.rm-topbar__left-spacer` when `.rm-find-or-create-wrapper` is no longer present + +### [0.7.28](https://github.com/8bitgentleman/zotero-roam/compare/0.7.27...0.7.28) - 2026-03-15 + +#### Bug Fixes + +- update type constraints and mock factory for citationKey support + - `matchWithCurrentData()` generic now includes `citationKey` in its type constraint + - `makeItemMetadata()` mock factory derives `has_citekey`/`key` from `data.citationKey` + +### [0.7.27](https://github.com/8bitgentleman/zotero-roam/compare/0.7.26...0.7.27) - 2026-03-15 + +#### Bug Fixes + +- fix citekeys showing as random strings (e.g. @8NNHP96Y) in Zotero 8 + - Read native `citationKey` field from item data, introduced in Zotero 8 + - Falls back to Better BibTeX `extra` field format for backwards compatibility + - Items without either will still fall back to the internal Zotero key + +### [0.7.26](https://github.com/8bitgentleman/zotero-roam/compare/0.7.25...0.7.26) - 2026-01-31 + +#### Bug Fixes + +- fix JSON parsing error when viewing PDF linked notes + - Add try-catch around annotationPosition parsing in simplifyZoteroAnnotations() + - Fallback to default position {pageIndex: 0} if parsing fails + - Prevents crash when annotation position data is malformed or missing +- improve error messages for API failures + - Replace technical "AxiosError" messages with user-friendly descriptions + - Special handling for 404, 403, 500+ status codes and network errors + - Better UX when external APIs (Semantic Scholar) return errors + +#### Features + +- add check-citekeys.js script for Better BibTeX verification + - Helps users verify that citekeys are properly configured + - Shows which items have/don't have Citation Key in Extra field + - Provides guidance for fixing missing citekeys + +### [0.7.25](https://github.com/8bitgentleman/zotero-roam/compare/0.7.24...0.7.25) - 2026-01-29 + +#### Bug Fixes + +- prevent memory leaks causing app unresponsiveness on Apple Silicon Macs + - Limited Logger.logs array to 1000 entries with FIFO rotation + - Changed React Query cacheTime from Infinity to 30 minutes + - Fixed SmartBlocks event listener cleanup + - Fixed Tribute autocomplete instance cleanup +- downgrade jsdom to 26.0.0 to resolve ES module compatibility issues in test environment +- make Chromatic and Codecov CI steps conditional on secrets being available + +### [0.7.24](https://github.com/alixlahuec/zotero-roam/compare/0.7.23...0.7.24) - 2026-01-26 + +#### Features + +- update release automation + +#### Bug Fixes + +- prevent settings overwrite on reload ([#790](https://github.com/alixlahuec/zotero-roam/issues/790)) + +### [0.7.23](https://github.com/alixlahuec/zotero-roam/compare/0.7.22...0.7.23) - 2026-01-26 + +#### Security + +- update axios to v1.8.2 (security fix) +- update vite to v5.4.19 (security fix) +- update msw to v2.12.7 + +#### Bug Fixes + +- add publication year to default metadata template ([#23](https://github.com/alixlahuec/zotero-roam/issues/23)) +- fix bullet point formatting in notes import ([#26](https://github.com/alixlahuec/zotero-roam/issues/26)) +- add authors/editors conditional option ([#19](https://github.com/alixlahuec/zotero-roam/issues/19)) + +### [0.7.22](https://github.com/alixlahuec/zotero-roam/compare/0.7.21...0.7.22) - + +#### Features + +- add feature requests board ([`#483`](https://github.com/alixlahuec/zotero-roam/pull/483)) +#### Bug Fixes + +- update URLs for web links ([`#561`](https://github.com/alixlahuec/zotero-roam/pull/561)) + +### [0.7.21](https://github.com/alixlahuec/zotero-roam/compare/0.7.20...0.7.21) - 2023-09-30 + +#### Bug Fixes + +- icon context menu doesn't display correctly ([`#420`](https://github.com/alixlahuec/zotero-roam/pull/420)) + +### [0.7.20](https://github.com/alixlahuec/zotero-roam/compare/0.7.19...0.7.20) - 2023-09-09 + +#### Bug Fixes + +- incorrect items are shown in DNP menus ([`#396`](https://github.com/alixlahuec/zotero-roam/pull/396)) + +### [0.7.19](https://github.com/alixlahuec/zotero-roam/compare/0.7.18...0.7.19) - 2023-06-28 + +### [0.7.18](https://github.com/alixlahuec/zotero-roam/compare/0.7.17...0.7.18) - 2023-06-17 + +### [0.7.17](https://github.com/alixlahuec/zotero-roam/compare/0.7.16...0.7.17) - 2023-05-24 + +### [0.7.16](https://github.com/alixlahuec/zotero-roam/compare/0.7.15...0.7.16) - 2023-05-22 + +### [0.7.15](https://github.com/alixlahuec/zotero-roam/compare/0.7.14...0.7.15) - 2023-05-14 + +### [0.7.14](https://github.com/alixlahuec/zotero-roam/compare/0.7.13...0.7.14) - 2023-04-08 + +### [0.7.13](https://github.com/alixlahuec/zotero-roam/compare/0.7.12...0.7.13) - 2023-03-22 + +### [0.7.12](https://github.com/alixlahuec/zotero-roam/compare/0.7.11...0.7.12) - 2023-03-05 + +#### Features + +- split settings into tabs ([`#199`](https://github.com/alixlahuec/zotero-roam/pull/199)) +- support caching API data to local storage ([`#178`](https://github.com/alixlahuec/zotero-roam/pull/178)) +#### Bug Fixes + +- clean up default hooks ([`#200`](https://github.com/alixlahuec/zotero-roam/pull/200)) +- controls not displayed in logger when there are no errors ([`#193`](https://github.com/alixlahuec/zotero-roam/pull/193)) +- parsing of item citations in complex styles ([`#184`](https://github.com/alixlahuec/zotero-roam/pull/184)) + +### [0.7.11](https://github.com/alixlahuec/zotero-roam/compare/0.7.10...0.7.11) - 2023-02-09 + +#### Bug Fixes + +- excessive API calls due to broken `since` param ([`#176`](https://github.com/alixlahuec/zotero-roam/pull/176)) +- styling of multiselect input in Explorer ([`#173`](https://github.com/alixlahuec/zotero-roam/pull/173)) +- tags with hyphen and special characters aren't processed correctly ([`#153`](https://github.com/alixlahuec/zotero-roam/pull/153)) + +### [0.7.10](https://github.com/alixlahuec/zotero-roam/compare/0.7.9...0.7.10) - 2022-12-15 + +#### Features + +- show SemanticScholar items that don't have a DOI ([`#142`](https://github.com/alixlahuec/zotero-roam/pull/142)) +- add SmartBlocks commands for key and citekey ([`#140`](https://github.com/alixlahuec/zotero-roam/pull/140)) +- improve sorting and filtering in SemanticScholar panel ([`#139`](https://github.com/alixlahuec/zotero-roam/pull/139)) +- enable importing specific notes ([`#135`](https://github.com/alixlahuec/zotero-roam/pull/135)) +#### Bug Fixes + +- styling issues in tags selector ([`#144`](https://github.com/alixlahuec/zotero-roam/pull/144)) +- sizing and positioning of dialogs on smaller displays ([`#143`](https://github.com/alixlahuec/zotero-roam/pull/143)) + +### [0.7.9](https://github.com/alixlahuec/zotero-roam/compare/0.7.8...0.7.9) - 2022-12-08 + +#### Features + +- improve performance when opening the Search Panel ([`#131`](https://github.com/alixlahuec/zotero-roam/pull/131)) +- improve display of Zotero notes in drawer ([`#129`](https://github.com/alixlahuec/zotero-roam/pull/129)) +- improve parsing of Zotero notes ([`#127`](https://github.com/alixlahuec/zotero-roam/pull/127)) +- improve toaster and logs ([`#125`](https://github.com/alixlahuec/zotero-roam/pull/125)) +- give visual feedback on import errors ([`#124`](https://github.com/alixlahuec/zotero-roam/pull/124)) + +### [0.7.8](https://github.com/alixlahuec/zotero-roam/compare/0.7.7...0.7.8) - 2022-12-02 + +#### Features + +- add interface to view logs ([`#115`](https://github.com/alixlahuec/zotero-roam/pull/115)) +- expand formatting options for autocomplete ([`#114`](https://github.com/alixlahuec/zotero-roam/pull/114)) +- add custom nesting options for notes ([`#112`](https://github.com/alixlahuec/zotero-roam/pull/112)) +- sort annotations and notes for import ([`#102`](https://github.com/alixlahuec/zotero-roam/pull/102)) +#### Bug Fixes + +- ZOTEROITEMCOLLECTIONS doesn't return output ([`#110`](https://github.com/alixlahuec/zotero-roam/pull/110)) + +### [0.7.7](https://github.com/alixlahuec/zotero-roam/compare/0.7.6...0.7.7) - 2022-11-25 + +### [0.7.6](https://github.com/alixlahuec/zotero-roam/compare/0.7.5...0.7.6) - 2022-11-20 + +#### Bug Fixes + +- race condition with Smartblocks ([`#97`](https://github.com/alixlahuec/zotero-roam/pull/97)) +- fatal crash due to invalid shortcuts ([`#96`](https://github.com/alixlahuec/zotero-roam/pull/96)) + +### [0.7.5](https://github.com/alixlahuec/zotero-roam/compare/0.7.4...0.7.5) - 2022-11-17 + +#### Features + +- update setup for automated releases ([`#78`](https://github.com/alixlahuec/zotero-roam/pull/78)) + +### [0.7.4](https://github.com/alixlahuec/zotero-roam/compare/0.7.3...0.7.4) - 2022-09-30 + +### [0.7.3](https://github.com/alixlahuec/zotero-roam/compare/0.7.2...0.7.3) - 2022-09-17 + +#### Bug Fixes + +- dataRequests not processed in manual setup when specified as an Object ([`#61`](https://github.com/alixlahuec/zotero-roam/pull/61)) + +### [0.7.2](https://github.com/alixlahuec/zotero-roam/compare/0.7.1...0.7.2) - 2022-09-11 + + +## v0.7.0 + +#### New Features + +- **Integration: Zotero 6** + + Zotero's native annotations are explicitly supported by the extension. They can be imported directly into Roam, and have their own formatting system. +- **Integration: SmartBlocks** + + Metadata can now be formatted and imported to Roam using a SmartBlock. +- **Dashboard (beta)** + + View all your recently added/modified items in one place + + Manage your Zotero tags directly from Roam - edit, merge, delete + + Explore your Zotero libraries via complex queries +- **Web Import (beta)** + + The extension now supports adding items to Zotero from a URL. +- **Other** + + User settings are editable directly via the interface - no more `roam/js` code. + + Theming is now done through CSS variables. + +#### General Changes + +- Interface is more accessible, more consistent, and more reliable +- Extension icon shows detailed information about data retrieval status, as well as the current version and links to the extension docs \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/LICENSE b/prototypes/zotero-roam-mcp/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/prototypes/zotero-roam-mcp/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/prototypes/zotero-roam-mcp/README.md b/prototypes/zotero-roam-mcp/README.md new file mode 100644 index 0000000..7df5e96 --- /dev/null +++ b/prototypes/zotero-roam-mcp/README.md @@ -0,0 +1,39 @@ +# zoteroRoam (MCP fork) + +**This is a fork, not an original prototype.** It vendors [8bitgentleman/zotero-roam](https://github.com/8bitgentleman/zotero-roam) at v0.7.29, which is itself a fork of [alixlahuec/zotero-roam](https://github.com/alixlahuec/zotero-roam) (Apache-2.0, see `LICENSE`). On top of the upstream extension it adds **experimental headless MCP capabilities**: the extension registers AI tools with Roam, so that agents connected through [Roam's MCP server](https://github.com/Roam-Research/roam-tools) can search the Zotero library and import items into the graph without touching the UI. + +Everything else is unchanged upstream zoteroRoam: the search panel, page menus, SmartBlocks commands, and settings all work as documented upstream. + +## Status + +Internal prototype for evaluation by Discourse Graphs. The Roam API it builds on (`extensionAPI.ai.addTool`) is marked experimental by Roam and requires a Roam Desktop build with extension AI tools (Local API 1.1.5 or later). On older builds the tools are simply not registered and the extension behaves like the upstream release. + +## What it adds + +Once the extension is loaded and configured (Zotero API key plus library, as in upstream zoteroRoam), three tools appear in the `extensionTools` field of the Roam MCP's `get_graph_guidelines`, callable via `call_extension_tool`: + +- **`zotero-search-items`** (read): search the loaded Zotero items by citekey, DOI, Zotero item key, or title substring. Returns compact summaries, including each item's citekey and whether it already has a Roam page. +- **`zotero-import-metadata`** (edit): the headless equivalent of the "Import metadata" button. Creates the item's `[[@citekey]]` page if needed and imports its metadata using the user's configured settings (default formatter, custom function, or SmartBlock). Refused by default if the page already has content, to avoid duplicate imports; pass `allowDuplicate: true` to override. +- **`zotero-import-notes`** (edit): the headless equivalent of the "Import notes" button. Imports the item's notes and PDF annotations, formatted with the user's notes and annotations settings. + +The typical agent flow: a paper is already in Zotero. Call `zotero-search-items` to find its citekey and check `inGraph`, then `zotero-import-metadata` to create the citekey page with metadata. + +The tools read the user's current settings at call time and write to the graph exactly as the corresponding buttons would. Handler errors (unknown citekey, item without notes, page already populated) are returned to the agent as instructive messages. + +## Where the MCP code lives + +All additions are in [`src/services/ai-tools/`](src/services/ai-tools/), which holds the tool specs, handlers, and registration. They are wired up in [`src/loader.tsx`](src/loader.tsx) with feature detection. There is also a small `hasBlockChildren` helper and the `ai` namespace typings in [`src/services/roam/`](src/services/roam/). Searching the codebase for `registerAiTools` finds every touchpoint. + +## Deviations from repository conventions + +This prototype vendors a mature upstream codebase rather than starting from `packages/extension-base`, so it deliberately keeps upstream's conventions instead of this repository's: + +- It builds with its own Vite config (`dev/vite.config.mts`, adapted from upstream) instead of the shared esbuild CLI, and uses upstream's lifecycle export instead of `runExtension`. +- Upstream code uses synchronous `window.roamAlphaAPI.data.q`. Only the files under `src/services/ai-tools/` are new code. +- Tests are upstream's vitest suite (`*.test.ts`, run by this package's `test` script) plus the tests for the new AI-tools service. + +The build output honors the artifact contract: `dist/extension.js` (an ES module with a default `{onload, onunload}` export) and `dist/extension.css`. + +## Upstreaming + +The MCP capability was developed on the fork's own history first (branch `feat/roam-mcp-ai-tools` of 8bitgentleman/zotero-roam), so it can be offered upstream as a PR independently of this vendored copy. diff --git a/prototypes/zotero-roam-mcp/dev/vite.config.mts b/prototypes/zotero-roam-mcp/dev/vite.config.mts new file mode 100644 index 0000000..55f07d6 --- /dev/null +++ b/prototypes/zotero-roam-mcp/dev/vite.config.mts @@ -0,0 +1,78 @@ +import { resolve } from "path"; +import { defineConfig, PluginOption } from "vite"; + +import react from "@vitejs/plugin-react-swc"; +import { viteExternalsPlugin } from "vite-plugin-externals"; + +import pkg from "../package.json"; + + +// Adapted from upstream zotero-roam's dev/vite.config.mts: only the Roam +// developer-extension build is kept (upstream's roam/js and sandbox modes are +// dropped), targeting dist/ with no sourcemap per the roam-prototypes +// artifact contract. +export default defineConfig(() => { + // These libraries are provided as globals by the Roam app; under vitest + // they must resolve normally, since there is no Roam host page + const extraPlugins: PluginOption[] = process.env.VITEST ? [] : [ + viteExternalsPlugin({ + "@blueprintjs/core": ["Blueprint", "Core"], + "@blueprintjs/datetime": ["Blueprint", "DateTime"], + "@blueprintjs/select": ["Blueprint", "Select"], + "idb": "idb", + "react": "React", + "react-dom": "ReactDOM", + }, { useWindow: true }) + ]; + + return { + build: { + target: "es2021", + minify: true, + sourcemap: false, + outDir: "dist", + emptyOutDir: true, + copyPublicDir: false, + chunkSizeWarningLimit: 3000, + rollupOptions: { + input: { + "extension": resolve("src", "index") + }, + output: { + format: "es", + assetFileNames: "extension.[ext]", + entryFileNames: "[name].js" + }, + preserveEntrySignatures: "allow-extension" + } + }, + resolve: { + alias: { + "@clients": resolve("src", "clients"), + "@hooks": resolve("src", "hooks"), + ...(process.env.VITEST ? { "@services/roam": resolve("mocks", "roam.ts") } : {}), + "@services": resolve("src", "services"), + "Mocks": resolve("mocks"), + "Components": resolve("src", "components"), + "Styles": resolve("styles"), + "Types": resolve("src", "types") + } + }, + plugins: [react(), ...extraPlugins], + test: { + alias: { + "\.(css|sass)$": resolve("mocks", "style.ts") + }, + clearMocks: true, + define: { + "PACKAGE_VERSION": pkg.version + }, + environment: "jsdom", + globals: true, + setupFiles: ["dev/vitest.setup.js"], + typecheck: { + enabled: true + } + } + }; +}); diff --git a/prototypes/zotero-roam-mcp/dev/vitest.setup.js b/prototypes/zotero-roam-mcp/dev/vitest.setup.js new file mode 100644 index 0000000..fcf7ff5 --- /dev/null +++ b/prototypes/zotero-roam-mcp/dev/vitest.setup.js @@ -0,0 +1,15 @@ +import { setupServer } from "msw/node"; + +import { apiHandlers, fallbackHandler } from "Mocks"; + + +const server = setupServer( + ...apiHandlers, + fallbackHandler +); + +beforeAll(() => server.listen()); + +afterEach(() => server.resetHandlers()); + +afterAll(() => server.close()); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/citoid/index.ts b/prototypes/zotero-roam-mcp/mocks/citoid/index.ts new file mode 100644 index 0000000..d76ef94 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/citoid/index.ts @@ -0,0 +1,117 @@ +import { http, HttpResponse } from "msw"; +import { Mocks } from "Mocks"; + + +/* istanbul ignore next */ +const addSampleMetadata = () => ({ + key: "XRT92", + version: 0 +}); + +const addCitoidMetadata = ({ creators, date, itemType, tags = [], title, url, ...props }: Partial) => ({ + ...addSampleMetadata, + creators, + date, + itemType, + tags, + title, + url, + ...props +}); + +const addCreator = ( + [firstName, lastName, creatorType = "author"]: [string, string, Mocks.CreatorType?] +): Mocks.Creator => ({ + firstName, + lastName, + creatorType +}); + +export const goodIdentifier = "https://www.jmir.org/2021/9/e27283"; +export const badIdentifier = "https://projects.iq.harvard.edu/files/harvarduxgroup/files/ux-research-guide-sample-questions-for-user-interviews.pdf"; +export const semanticIdentifier = "https://doi.org/10.1370/afm.1918"; +export const semanticNotAddedIdentifier = "https://doi.org/10.3122/jabfm.2017.01.160355"; + +const data = { + [goodIdentifier]: { + ...addCitoidMetadata({ + abstractNote: "Chloe for COVID-19", + creators: [ + addCreator(["Sophia", "Siedlikowski"]), + addCreator(["Louis-Philippe", "Noël"]), + addCreator(["Stephanie Anne", "Moynihan"]), + addCreator(["Marc", "Robin"]) + ], + date: "", + itemType: "journalArticle", + publicationTitle: "Journal of Medical Internet Research", + title: "Chloe for COVID-19: Evolution of an Intelligent Conversational Agent to Address Infodemic Management Needs During the COVID-19 Pandemic", + url: goodIdentifier + }) + } as Mocks.Responses.CitoidSuccess[number], + [badIdentifier]: { + status: 500, + method: "get", + type: "https://mediawiki.org/wiki/HyperSwitch/errors/unknown_error", + uri: "/en.wikipedia.org/v1/data/citation/zotero/https%3A%2F%2Fprojects.iq.harvard.edu%2Ffiles%2Fharvarduxgroup%2Ffiles%2Fux-research-guide-sample-questions-for-user-interviews.pdf" + } as Mocks.Responses.CitoidError, + [semanticIdentifier]: { + ...addCitoidMetadata({ + abstractNote: "Recently, the recognition that medical care may contribute less to overall health than other aspects of people’s lives do has led policy makers, academics, and even some physicians to argue that clinicians should make screening and action on the social determinants of health their responsibility.", + creators: [ + addCreator(["Leif I.", "Solberg"]) + ], + date: "2016-03-01", + DOI: "10.1370/afm.1918", + extra: "PMID: 26951583", + ISSN: "1544-1709, 1544-1717", + issue: "2", + itemType: "journalArticle", + pages: "102–103", + publicationTitle: "The Annals of Family Medicine", + shortTitle: "Theory vs Practice", + tags: [ + { "tag": "primary healthcare","type": 1 }, + { "tag": "social determinants of health","type": 1 }, + { "tag": "professional burnout","type": 1 }, + { "tag": "healthcare delivery","type": 1 }, + { "tag": "physician’s roles","type": 1 } + ], + title: "Theory vs Practice: Should Primary Care Practice Take on Social Determinants of Health Now? No.", + rights: "© 2016 Annals of Family Medicine, Inc.", + url: "https://www.annfammed.org/content/14/2/102", + volume: "14" + }) + } as Mocks.Responses.CitoidSuccess[number], + [semanticNotAddedIdentifier]: { + ...addCitoidMetadata({ + abstractNote: "

Health extension programs represent an opportunity for practice-based research networks (PBRNs) and primary care practices to develop collaborations reaching beyond the clinic walls to address the upstream social determinants of health and engage in community-based research. The Health Extension Regional Officers (HEROs) program at the University of New Mexico described in this issue of the JABFM is an innovative model with a bidirectional approach to linking academic health centers to community-based practices and organizations. Health extension programs are local, influenced by history, relationships, and support. Oregon9s health extension workforce represents a diverse group that includes practice facilitators, community health workers, and Cooperative Extension agents. PBRNs are measuring success in terms of collaboration across a spectrum of health activities. The Oregon Rural Practice-based Research Network uses a “Four Pillars” model of community engagement, practice transformation, research, and education to involve researchers, health policy experts, educators, and health extension workers to improve community health.

", + creators: [ + addCreator(["Lyle J.", "Fagnan"]) + ], + date: "2017-01", + DOI: "10.3122/jabfm.2017.01.160355", + ISSN: "1557-2625, 1558-7118", + issue: "1", + itemType: "journalArticle", + pages: "10-12", + publicationTitle: "The Journal of the American Board of Family\n Medicine", + title: "Moving Upstream—Health Extension and Primary Care", + url: "https://doi.org/10.3122/jabfm.2017.01.160355", + volume: "30" + }) + } as Mocks.Responses.CitoidSuccess[number] +}; + +export const handleCitoid = http.get( + "https://en.wikipedia.org/api/rest_v1/data/citation/zotero/:identifier", + ({ params }) => { + const { identifier } = params; + const output = data[decodeURIComponent(`${identifier}`)]; + return HttpResponse.json([output], { status: output.status || 200 }); + } +); + +export { + data as citoids +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/handlers.ts b/prototypes/zotero-roam-mcp/mocks/handlers.ts new file mode 100644 index 0000000..c044e10 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/handlers.ts @@ -0,0 +1,49 @@ +/* istanbul ignore file */ +import { http, passthrough, HttpResponse } from "msw"; +import { handleAPIKey } from "./zotero/keys"; +import { handleBibliography } from "./zotero/bib"; +import { handleCitoid } from "./citoid"; +import { handleCollections } from "./zotero/collections"; +import { handleDeleted } from "./zotero/deleted"; +import { handleItems } from "./zotero/items"; +import { handleSemantic } from "./semantic-scholar"; +import { handleTags } from "./zotero/tags"; + + +export const fallbackHandler = http.get( + "*", + ({ request }) => { + return new HttpResponse(null, { status: 404, statusText: "You need to add a handler for " + request.url }); + } +); + +export const chromaticHandler = http.get( + "https://*.chromatic.com/assets/*", + () => passthrough() +); + +export const roamAssetsHandler = http.get( + "https://roamresearch.com/assets/*", + () => passthrough() +); + +export const sciteApiHandler = http.post( + "https://api.scite.ai/*", + () => passthrough() +); + +export const sciteAssetsHandler = http.get( + "https://cdn.scite.ai/assets/*", + () => passthrough() +); + +export const apiHandlers = [ + handleBibliography, + handleCitoid, + handleCollections, + handleDeleted, + ...handleItems, + handleSemantic, + ...handleTags, + handleAPIKey +]; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/index.ts b/prototypes/zotero-roam-mcp/mocks/index.ts new file mode 100644 index 0000000..79a23d1 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/index.ts @@ -0,0 +1,5 @@ +export * from "./citoid"; +export * from "./handlers"; +export * from "./semantic-scholar"; +export * from "./types"; +export * from "./zotero"; diff --git a/prototypes/zotero-roam-mcp/mocks/roam.ts b/prototypes/zotero-roam-mcp/mocks/roam.ts new file mode 100644 index 0000000..e3fb31f --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/roam.ts @@ -0,0 +1,135 @@ +import { fn } from "@storybook/test"; +import { makeOrdinal } from "../src/utils"; + + +export const uid_with_existing_block = "__UID_WITH_EXISTING_BLOCK__"; +export const uid_with_existing_block_with_children = "__UID_WITH_EXISTING_BLOCK_WITH_CHILDREN__"; +export const existing_block_uid = "__SOME_UID__"; +export const existing_block_uid_with_children = "__SOME_UID_WITH_CHILDREN__"; +export const existing_page_uid = "__PAGE_UID__"; +export const existing_page_with_content_uid = "__PAGE_WITH_CONTENT_UID__"; + +function addPaletteCommand(){} + +function findRoamBlock(_string, pageUID){ + switch(pageUID){ + case uid_with_existing_block: + return { + uid: existing_block_uid + }; + case uid_with_existing_block_with_children: + return { + uid: existing_block_uid_with_children, + children: [{ id: 123 }, { id: 456 }] + }; + default: + return false; + } +} + +const findRoamPage = fn((_title: string): string | false => false); + +function getAllPages(){ + return []; +} + +function getCitekeyPages() { + return new Map([]); +} + +function getCitekeyPagesWithEditTime(){ + return new Map([]); +} + +function getCurrentCursorLocation() { + return null; +} + +function getGraphName() { + return "mock-graph"; +} + +function getInitialedPages(keys) { + if (keys.includes("h")) { + return [ + { title: "housing", uid: "__some_uid__" } + ]; + } + + return []; +} + +function hasBlockChildren(uid) { + return [uid_with_existing_block_with_children, existing_page_with_content_uid].includes(uid); +} + +const importItemMetadata = fn(({ item }, uid) => { + const pageUID = uid || existing_page_uid; + return Promise.resolve({ + args: { blocks: [], uid: pageUID }, + error: null, + page: { new: !uid, title: "@" + item.key, uid: pageUID }, + raw: {}, + success: true + }); +}); + +const importItemNotes = fn(({ item }, uid) => { + const pageUID = uid || existing_page_uid; + return Promise.resolve({ + args: { blocks: [], uid: pageUID }, + error: null, + page: { new: !uid, title: "@" + item.key, uid: pageUID }, + raw: {}, + success: true + }); +}); + +function makeDNP(date: Date | any, { brackets = true }: { brackets?: boolean } = {}) { + const thisdate = date.constructor === Date ? date : new Date(date); + const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + const dateString = `${months[thisdate.getMonth()]} ${makeOrdinal(thisdate.getDate())}, ${thisdate.getFullYear()}`; + if (brackets) { + return `[[${dateString}]]`; + } else { + return dateString; + } +} + +function maybeReturnCursorToPlace(){} + +async function openInSidebarByUID(){} + +async function openPageByUID(){} + +function readDNP(string: string, { as_date = true }: { as_date?: boolean } = {}) { + const [/* match */, mm, dd, yy] = Array.from(string.matchAll(/(.+) ([0-9]+).{2}, ([0-9]{4})/g))[0]; + const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + + const parsedDate = [parseInt(yy), months.findIndex(month => month == mm), parseInt(dd)] as const; + + return as_date ? new Date(...parsedDate) : parsedDate; +} + +function removePaletteCommand(){} + +export { + addPaletteCommand, + findRoamBlock, + findRoamPage, + getAllPages, + getCitekeyPages, + getCitekeyPagesWithEditTime, + getCurrentCursorLocation, + getGraphName, + getInitialedPages, + hasBlockChildren, + importItemMetadata, + importItemNotes, + makeDNP, + maybeReturnCursorToPlace, + openInSidebarByUID, + openPageByUID, + readDNP, + removePaletteCommand +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/semantic-scholar/index.ts b/prototypes/zotero-roam-mcp/mocks/semantic-scholar/index.ts new file mode 100644 index 0000000..2b68893 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/semantic-scholar/index.ts @@ -0,0 +1,166 @@ +import { http, HttpResponse } from "msw"; +import { Mocks } from "Mocks"; + + +const data: Record = { + "10.1503/cmaj.210229": { + abstract: "CMAJ | November 8, 2021 | Volume 193 | Issue 44 © 2021 CMA Joule Inc. or its licensors M ost family physicians now report that they engage in some degree of social intervention in the management of patients.1 However, outside of community health centres, social interventions are still not a routine part of primary care practice and are not yet considered “standard of care.” Traditional primary care seldom included interventions such as social prescribing and health–legal partnerships until the early 1990s,2 and some practitioners still question whether social intervention is part of the primary care provider’s role.3 A small cadre of practition ers in high-income countries, including Canada, Australia, the United Kingdom and the United States, has been at the forefront of developing and evaluating interventions into social risks to health, which has led to a few social interventions being widely adopted, with positive impacts on broad markers of health. Producing high-quality, clinically actionable research on social interventions in primary care is challenging. The effects of living with social pressures such as poverty, racism or trauma are difficult to evaluate over the duration of a typical study using trad itional markers of change in physical or mental health. For this reason, the literature on social interventions in primary care often focuses on process rather than outcome measures, and on self-reported indicators of health and well-being.31 Despite these limitations, the literature points to a positive general impact on health of social interventions. We discuss accumulated evidence (Box 1) on social interventions and provide an overview of common primary care–based interventions (Table 1), highlighting their strengths, limitations and feasibility of implementation in different practice settings. We discuss levels of practice resources that would facilitate their implementation, and also suggest those interventions that could be led by an external community partner, which may provide an alternate avenue for less well-resourced practices. Although the incorporation of new interventions may seem daunting, especially to community-based health care providers without an interdisciplinary team, we have been involved in change processes with practices of all sizes to incorporate social interventions.", + arxivId: null, + authors: [ + { + authorId: "4534327", + name: "Gary Bloch", + url: "https://www.semanticscholar.org/author/4534327" + }, + { + authorId: "144502037", + name: "L. Rozmovits", + url: "https://www.semanticscholar.org/author/144502037" + } + ], + citationVelocity: 0, + citations: [ + { + arxivId: null, + authors: [ + { + authorId: "40589003", + name: "Margae Knox" + }, + { + authorId: "2168469519", + name: "Emily E Esteban" + }, + { + authorId: "2064818309", + name: "Elizabeth A Hernandez" + }, + { + authorId: "2058909364", + name: "M. Fleming" + }, + { + authorId: "2168458761", + name: "Nadia Safaeinilli" + }, + { + authorId: "3034212", + name: "Amanda L. Brewster" + } + ], + doi: "10.1136/bmjoq-2021-001807", + intent: [], + isInfluential: false, + paperId: "c3a1a1c6875a03b4d32fd0a36f54840aa6ba4fb7", + title: "Defining case management success: a qualitative study of case manager perspectives from a large-scale health and social needs support program", + url: "https://www.semanticscholar.org/paper/c3a1a1c6875a03b4d32fd0a36f54840aa6ba4fb7", + venue: "BMJ open quality", + year: 2022 + } + ], + corpusId: 243834202, + doi: "10.1503/cmaj.210229", + fieldsOfStudy: ["Medicine"], + influentialCitationCount: 0, + isOpenAccess: false, + isPublisherLicensed: true, + is_open_access: false, + is_publisher_licensed: true, + numCitedBy: 1, + numCiting: 76, + paperId: "de2f7fccff399072a81f18407547e0163bc95448", + references: [ + { + arxivId: null, + authors: [ + { + authorId: "3805349", + name: "L. Solberg" + } + ], + doi: "10.1370/afm.1918", + intent: [], + isInfluential: false, + paperId: "447a7aa39d71dda859564dc12326a43b66488c76", + title: "Theory vs Practice: Should Primary Care Practice Take on Social Determinants of Health Now? No.", + url: "https://www.semanticscholar.org/paper/447a7aa39d71dda859564dc12326a43b66488c76", + venue: "The Annals of Family Medicine", + year: 2016 + }, + { + arxivId: null, + authors: [ + { + authorId: "5709429", + name: "L. Fagnan" + } + ], + doi: "10.3122/jabfm.2017.01.160355", + intent: [], + isInfluential: false, + paperId: "da834ae1504204c6e14e4bcf8811c4ca250d4ada", + title: "Moving Upstream—Health Extension and Primary Care", + url: "https://www.semanticscholar.org/paper/da834ae1504204c6e14e4bcf8811c4ca250d4ada", + venue: "The Journal of the American Board of Family Medicine", + year: 2017 + }, + { + arxivId: null, + authors: [ + { + authorId: "4730239", + name: "A. Andermann" + } + ], + doi: "10.1186/s40985-018-0094-7", + intent: [], + isInfluential: false, + paperId: "f090bb04877612278bd614b951a8d753eb6b8b5b", + title: "Screening for social determinants of health in clinical care: moving from the margins to the mainstream", + url: "https://www.semanticscholar.org/paper/f090bb04877612278bd614b951a8d753eb6b8b5b", + venue: "Public Health Reviews", + year: 2018 + }, + { + arxivId: "arXiv:1501.00001", + authors: [ + { + authorId: "123456", + name: "Jane Doe" + } + ], + doi: false, + intent: ["background"], + isInfluential: true, + paperId: "__some_paper_id__", + title: "DOI-less paper: an influential work", + url: "", + venue: "Nature", + year: 2013 + } + ], + s2FieldsOfStudy: [ + { category: "Medicine", source: "external" }, + { category: "Medicine", source: "s2-fos-model" }, + { category: "Political Science", source: "s2-fos-model" } + ], + title: "Implementing social interventions in primary care", + topics: [], + url: "https://www.semanticscholar.org/paper/de2f7fccff399072a81f18407547e0163bc95448", + venue: "Canadian Medical Association Journal", + year: 2021 + } +}; + +export const handleSemantic = http.get( + "https://api.semanticscholar.org/v1/paper/:pub/:index", + ({ params }) => { + const { pub, index } = params; + const doi = [pub, index].join("/"); + return HttpResponse.json(data[doi]); + } +); + +export { + data as semantics +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/style.ts b/prototypes/zotero-roam-mcp/mocks/style.ts new file mode 100644 index 0000000..8cec2e9 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/style.ts @@ -0,0 +1 @@ +export {}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/types.ts b/prototypes/zotero-roam-mcp/mocks/types.ts new file mode 100644 index 0000000..742b462 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/types.ts @@ -0,0 +1,96 @@ +import { CitoidAPI } from "@clients/citoid"; +import { SemanticScholarAPI } from "@clients/semantic"; +import { ZoteroAPI } from "@clients/zotero"; +import { ZItemAnnotation, ZItemAttachment, ZItemNote, ZItemTop } from "Types/transforms"; + + +export namespace Mocks { + export type Annotation = ZItemAnnotation; + + export type Bib = ZoteroAPI.Responses.ItemGet<"biblatex">; + + export type Collection = ZoteroAPI.Collection; + + export type Creator = ZoteroAPI.Creator; + + export type CreatorType = ZoteroAPI.CreatorType; + + export type EntityLibrary = ZoteroAPI.EntityLibrary; + + export type ItemAttachment = ZItemAttachment; + + export type ItemBibliography = ZoteroAPI.Responses.ItemGet<"bib">; + + export type ItemNote = ZItemNote; + + export type ItemTop = ZItemTop; + + export type Tag = ZoteroAPI.Tag; + + export type Library = { + id: number, + links: { + alternate: { href: string, type: string } + }, + name: string, + path: string, + type: ZoteroAPI.LibraryTypeURI, + username?: string, + version: number + } + + export namespace RequestBody { + export type ItemsPost = Partial[]; + } + + export namespace RequestParams { + /* CITOID */ + export type Citoid = Pick<_RequestParams, "identifier">; + /* SEMANTIC SCHOLAR */ + export type SemanticScholarItem = Pick<_RequestParams, "pub" | "index">; + /* ZOTERO */ + export type Bibliography = Pick<_RequestParams, "libraryType" | "libraryID" | "itemKey">; + export type Collections = Pick<_RequestParams, "libraryType" | "libraryID">; + export type Deleted = Pick<_RequestParams, "libraryType" | "libraryID">; + export type Items = Pick<_RequestParams, "libraryType" | "libraryID">; + export type Permissions = Pick<_RequestParams, "apikey">; + export type Tags = Pick<_RequestParams, "libraryType" | "libraryID">; + } + + export namespace Responses { + /* CITOID */ + export type CitoidError = { + status: number, + method: "get", + type: string, + uri: string + }; + export type CitoidSuccess = CitoidAPI.AsZotero[]; + export type Citoid = CitoidSuccess | CitoidError; + /* SEMANTIC SCHOLAR */ + export type SemanticScholarItem = SemanticScholarAPI.Item; + /* ZOTERO */ + export type Bibliography = ZoteroAPI.Responses.ItemGet<"bib">; + export type Collections = ZoteroAPI.Responses.Collections; + export type Deleted = ZoteroAPI.Responses.Deleted; + export type ItemsGet = ZoteroAPI.Responses.ItemsGet<"biblatex"> | ZoteroAPI.Responses.ItemsGet<"data">; + // TODO: fix mock for writing items + export type ItemsPost = { failed: Record, unchanged: Record, success: Record, successful: Record }; + export type Permissions = ZoteroAPI.Responses.Permissions; + export type TagsGet = ZoteroAPI.Responses.Tags; + export type TagsDelete = never; + } +} + +type _RequestParams = { + /* CITOID */ + identifier: string, + /* SEMANTIC SCHOLAR */ + pub: string, + index: string, + /* ZOTERO */ + apikey: string, + itemKey: string, + libraryType: ZoteroAPI.LibraryTypeURI, + libraryID: string +}; diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/annotations.ts b/prototypes/zotero-roam-mcp/mocks/zotero/annotations.ts new file mode 100644 index 0000000..95a983a --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/annotations.ts @@ -0,0 +1,94 @@ +import { makeEntityLinks, makeLibraryMetadata } from "./common"; +import { libraries } from "./libraries"; +import { Mocks } from "Mocks"; + + +const { userLibrary } = libraries; + +type MakeAnnotArgs = { + data: Partial, + key: string, + library: Mocks.Library, + parentItem: string, + version: number +}; +const makeAnnot = ({ key, library, parentItem, version, data }: MakeAnnotArgs): Mocks.Annotation => { + return { + data: { + annotationColor: "#5fb236", + annotationComment: "", + annotationPageLabel: "25", + annotationPosition: "{\"pageIndex\":24,\"rects\":[[203.6,431.053,546.865,441.6],[203.6,419.056,536.829,429.603],[203.6,407.059,566.448,417.606],[203.6,395.062,564.521,405.609],[203.6,383.065,265.699,393.612]]}", + annotationSortIndex: "00024|001317|00350", + annotationText: "", + annotationType: "highlight", + dateAdded: "2022-03-18T13:00:00Z", + dateModified: "2022-04-02T02:00:00Z", + itemType: "annotation", + key, + parentItem, + relations: {}, + tags: [], + version, + ...data + }, + key, + links: makeEntityLinks({ key, library, parentItem }), + library: makeLibraryMetadata(library), + meta: {}, + version, + has_citekey: false + }; +}; + +export const sampleImageAnnot = makeAnnot({ + key: "X76XTRT", + library: userLibrary, + parentItem: "Z90BMEE", + version: 1222, + data: { + annotationComment: "Good figure to dissect", + annotationText: "", + annotationType: "image", + tags: [{ tag: "TODO" }] + } +}); + +export const sampleAnnot = makeAnnot({ + key: "A12BCDEF", + library: userLibrary, + parentItem: "P34QRSTU", + version: 1234, + data: { + annotationComment: "This is an interesting passage, let's look into it further later.", + annotationText: "Digital health literacy may have an impact on the use of digital health services such as virtual visits.", + annotationType: "highlight", + tags: [{ tag: "Important" },{ tag: "TODO" }] + } +}); + +export const sampleAnnotPrevPage = makeAnnot({ + key: "__PREV_PAGE_ANNOT__", + library: userLibrary, + parentItem: "__SOME_PARENT__", + version: 1279, + data: { + annotationComment: "This is a comment from a previous page", + annotationSortIndex: "00001|00002|00003", + annotationText: "some previous page's text", + annotationType: "highlight" + } +}); + +export const sampleAnnotLaterPage = makeAnnot({ + key: "__LATER_PAGE_ANNOT__", + library: userLibrary, + parentItem: "__SOME_PARENT__", + version: 1280, + data: { + annotationComment: "This is a comment from a later page", + annotationSortIndex: "00004|00005|00006", + annotationText: "some later page's text", + annotationType: "highlight" + } +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/bib.ts b/prototypes/zotero-roam-mcp/mocks/zotero/bib.ts new file mode 100644 index 0000000..936b737 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/bib.ts @@ -0,0 +1,81 @@ +import { http, HttpResponse } from "msw"; + +import { makeEntityLinks, makeLibraryMetadata, zotero } from "./common"; +import { libraries } from "./libraries"; + +import { ObjValues } from "../../src/types/helpers"; +import { Mocks } from "Mocks"; + + +const { userLibrary } = libraries; + +const addMetadata = ({ key, library, version }) => ({ + key, + version, + library: makeLibraryMetadata(library), + links: makeEntityLinks({ key, library }), + meta: { + numChildren: 0 + } +}); + +export const findBibliographyEntry = ({ key, path }: { key: string, path: string }): ObjValues => { + const [libraryType, libraryID] = path.split("/") as [Mocks.Library["type"], string]; + return Object.values(data).find(item => item.library.type + "s" == libraryType && item.library.id == Number(libraryID) && item.key == key)!; +}; + +const data: Record = { + "itemInLibrary": { + ...addMetadata({ + key: "PPD648N6", + library: userLibrary, + version: 1 + }), + meta: { + creatorSummary: "Bloch and Rozmovits", + parsedDate: "2021-11-08" + }, + bib: "
\n
Bloch, Gary, and Linda Rozmovits. “Implementing Social Interventions in Primary Care.” CMAJ 193, no. 44 (November 8, 2021): E1696–1701. https://doi.org/10.1503/cmaj.210229.
\n
" + }, + "itemFromUserLibrary": { + ...addMetadata({ + key: "PYTM394", + library: userLibrary, + version: 91 + }), + meta: { + creatorSummary: "Agarwal et al.", + parsedDate: "2021-07-22" + }, + bib: "
\n
Agarwal, Payal, Rick Wang, Christopher Meaney, Sakina Walji, Ali Damji, Navsheer Gill Toor, Gina Yip, et al. “Sociodemographic Differences in Patient Experience with Virtual Care during COVID-19.” medRxiv, July 22, 2021. https://www.medrxiv.org/content/10.1101/2021.07.19.21260373v1.
\n
" + } +}; + + +export const handleBibliography = http.get( + zotero(":libraryType/:libraryID/items/:itemKey"), + ({ request, params }) => { + const { libraryType, libraryID, itemKey } = params; + const url = new URL(request.url); + const includeFormats = (url.searchParams.get("include") || "").split(","); + + const { key, version, library, links, meta, ...outputs } = findBibliographyEntry({ key: `${itemKey}`, path: `${libraryType}/${libraryID}` }); + + const formatsData = Object.fromEntries( + Object.entries(outputs) + .filter(([format, _output]) => includeFormats.includes(format))) as { bib: string }; + + return HttpResponse.json({ + key, + version, + library, + links, + meta, + ...formatsData + }); + } +); + +export { + data as bibs +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/collections.ts b/prototypes/zotero-roam-mcp/mocks/zotero/collections.ts new file mode 100644 index 0000000..f0ebc8d --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/collections.ts @@ -0,0 +1,53 @@ +import { http, HttpResponse } from "msw"; +import { makeCollection, zotero } from "./common"; +import { libraries } from "./libraries"; +import { Mocks } from "Mocks"; + + +const { userLibrary, groupLibrary } = libraries; + +const data: Mocks.Collection[] = [ + makeCollection({ + key: "ABCDEF", + library: userLibrary, + name: "Important items", + version: 31 + }), + makeCollection({ + key: "PQRST", + library: groupLibrary, + name: "Reading List", + version: 14 + }) +]; + +export const findCollections = (type: Mocks.Library["type"], id: number, since: number) => { + return data.filter(cl => cl.library.type + "s" == type && cl.library.id == id && cl.version > since); +}; + +export const handleCollections = http.get( + zotero(":libraryType/:libraryID/collections"), + ({ request, params }) => { + const { libraryType, libraryID } = params; + const url = new URL(request.url); + const since = url.searchParams.get("since") || "0"; + + const { id, type, version } = Object.values(libraries).find(lib => lib.path == `${libraryType}/${libraryID}`)!; + const collections = findCollections(type, id, Number(since)); + + return HttpResponse.json( + collections, + { + headers: { + "last-modified-version": `${version}`, + // We're not mocking with additional requests + "total-results": `${Math.min(collections.length, 100)}` + } + } + ); + } +); + +export { + data as collections +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/common.ts b/prototypes/zotero-roam-mcp/mocks/zotero/common.ts new file mode 100644 index 0000000..0e1f815 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/common.ts @@ -0,0 +1,113 @@ +import { Mocks } from "Mocks"; + + +export const zotero = (URI: string) => "https://api.zotero.org/" + URI; + + +type MakeCollectionArgs = Omit & { + library: Mocks.Library, + hasParent?: Mocks.Collection["data"]["parentCollection"], + hasChildren?: number +}; +export const makeCollection = ( + { key, library, name, version, hasParent = false, hasChildren = 0 }: MakeCollectionArgs +): Mocks.Collection => ({ + data: { + key, + name, + parentCollection: hasParent, + relations: {}, + version + }, + key, + library: makeLibraryMetadata(library), + links: makeEntityLinks({ key, library }), + meta: { + numCollections: hasChildren, + numItems: 1 + }, + version +}); + + +type MakeEntityLinksArgs = { + key: string, + library: Mocks.Library, + parentItem?: string | false +}; +export const makeEntityLinks = ({ key, library, parentItem = false }: MakeEntityLinksArgs) => { + const { type, name, path } = library; + let upLink = {}; + + if(parentItem){ + upLink = { + up: { + href: `https://api.zotero.org/${path}/items/${parentItem}`, + type: "application/json" + } + }; + } + + return { + self: { + href: `https://api.zotero.org/${path}/items/${key}`, + type: "application/json" + }, + alternate: { + href: `https://www.zotero.org/${type == "users" ? name : path}/items/${key}`, + type: "text/html" + }, + ...upLink + }; +}; + + +type MakeItemMetadataArgs = { + citekey?: string | false, + itemType?: Mocks.ItemTop["data"]["itemType"], + key?: string, + library: Mocks.Library, + title?: string, + version?: number, + data?: Partial +}; +export const makeItemMetadata = ({ citekey = false, itemType = "journalArticle", key = "__NO_UNIQUE_KEY__", library, title = "", version = 1, data = {} }: MakeItemMetadataArgs): Omit => { + const effectiveCitekey = data.citationKey || citekey; + return ({ + data: { + creators: [], + collections: [], + dateAdded: "", + dateModified: "", + itemType, + key, + extra: citekey ? `Citation Key: ${citekey}` : "", + relations: {}, + tags: [], + title, + version, + ...data + }, + has_citekey: effectiveCitekey ? true : false, + key: effectiveCitekey || key, + library: makeLibraryMetadata(library), + links: makeEntityLinks({ key, library }), + version + }); +}; + + +export const makeLibraryMetadata = (library: Mocks.Library): Mocks.EntityLibrary => { + const { type, id, name, path } = library; + return { + type: type.slice(0, -1) as Mocks.EntityLibrary["type"], + id, + name, + links: { + alternate: { + href: "https://www.zotero.org/" + (type == "users" ? name : path), + type: "text/html" + } + } + }; +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/deleted.ts b/prototypes/zotero-roam-mcp/mocks/zotero/deleted.ts new file mode 100644 index 0000000..c6648a3 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/deleted.ts @@ -0,0 +1,106 @@ +import { http, HttpResponse } from "msw"; +import { makeCollection, makeItemMetadata, zotero } from "./common"; +import { libraries } from "./libraries"; +import { Mocks } from "Mocks"; + + +const { userLibrary, groupLibrary } = libraries; + +const data: Record = { + [userLibrary.path]: { + collections: [ + makeCollection({ + key: "CRTG512", + library: userLibrary, + name: "A deleted collection", + version: 144 + }) + ], + items: [] + }, + [groupLibrary.path]: { + collections: [], + items: [ + { + ...makeItemMetadata({ + itemType: "journalArticle", + key: "WSQ399YM", + library: groupLibrary, + title: "Lorem ipsum title", + version: 128, + data: { + abstractNote: "", + collections: [], + creators: [ + { + creatorType: "author", + firstName: "Some", + lastName: "Author" + }, + { + creatorType: "author", + firstName: "Another", + lastName: "Author" + }, + { + creatorType: "author", + firstName: "Third", + lastName: "Author" + } + ], + date: "2022-02-03", + dateAdded: "2022-04-06T11:00:00Z", + dateModified: "2022-04-06T11:00:00Z", + DOI: "", + publicationTitle: "", + url: "" + } + }), + meta: { + creatorSummary: "Author et al.", + numChildren: 0, + parsedDate: "2022-02-03" + } + } + ] + } +}; + +export const findDeleted = ({ path, since }: { path: string, since: number }) => { + const entities: Partial = {}; + + entities.collections = data[path] + .collections + .filter(cl => cl.version > since) + .map(cl => cl.key); + + entities.items = data[path] + .items + .filter(it => it.version > since) + .map(it => it.key); + + return entities; +}; + +export const handleDeleted = http.get( + zotero(":libraryType/:libraryID/deleted"), + ({ request, params }) => { + const { libraryType, libraryID } = params; + const url = new URL(request.url); + const since = Number(url.searchParams.get("since")); + const path = `${libraryType}/${libraryID}`; + + const { collections = [], items = [] } = findDeleted({ path, since }); + + return HttpResponse.json({ + collections, + items, + searches: [], + tags: [] + }); + } +); + +export { + data as deletions +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/index.ts b/prototypes/zotero-roam-mcp/mocks/zotero/index.ts new file mode 100644 index 0000000..9df89e5 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/index.ts @@ -0,0 +1,10 @@ +export * from "./annotations"; +export * from "./bib"; +export * from "./collections"; +export * from "./deleted"; +export * from "./items"; +export * from "./keys"; +export * from "./libraries"; +export * from "./notes"; +export * from "./pdfs"; +export * from "./tags"; diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/items.ts b/prototypes/zotero-roam-mcp/mocks/zotero/items.ts new file mode 100644 index 0000000..8a35d83 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/items.ts @@ -0,0 +1,275 @@ +import { http, HttpResponse } from "msw"; +import { makeItemMetadata, zotero } from "./common"; +import { libraries } from "./libraries"; +import { citoids, semanticIdentifier, Mocks } from "Mocks"; +import { ObjValues } from "Types/helpers"; + + +const { userLibrary, groupLibrary } = libraries; + +const data: Mocks.ItemTop[] = [ + { + ...makeItemMetadata({ + citekey: "blochImplementingSocialInterventions2021", + itemType: "journalArticle", + key: "PPD648N6", + library: userLibrary, + title: "Implementing social interventions in primary care", + data: { + abstractNote: "KEY POINTS\n- Primary care–based social interventions offer an important means to mitigate threats to individual and community health posed by adverse social conditions.\n- Effective interventions include those that target individual-level determinants, connections with community resources, community-focused partnerships and structures within health teams that affect equity.\n- Accumulating evidence points to the positive impacts of social interventions on broad markers of health; however, most research in this area has focused on implementation and process measures, rather than outcomes.\n- Some interventions require large, interdisciplinary health care resources to implement, but many are accessible to small group practices or individual providers.", + collections: ["ABCDEF"], + creators: [ + { + creatorType: "author", + firstName: "Gary", + lastName: "Bloch" + }, + { + creatorType: "author", + firstName: "Linda", + lastName: "Rozmovits" + } + ], + date: "2021-11-08", + dateAdded: "2021-11-12T22:58:47Z", + dateModified: "2021-11-12T23:12:25Z", + DOI: "10.1503/cmaj.210229", + publicationTitle: "CMAJ", + url: "https://www.cmaj.ca/content/193/44/E1696" + } + }), + meta: { + creatorSummary: "Bloch and Rozmovits", + numChildren: 0, + parsedDate: "2021-11-08" + } + }, + { + ...makeItemMetadata({ + citekey: "pintoExploringDifferentMethods2021", + itemType: "journalArticle", + key: "D53X926C", + library: groupLibrary, + title: "Exploring different methods to evaluate the impact of basic income interventions: a systematic review", + version: 17, + data: { + abstractNote: "Abstract \n \n Background \n Persistent income inequality, the increase in precarious employment, the inadequacy of many welfare systems, and economic impact of the COVID-19 pandemic have increased interest in Basic Income (BI) interventions. Ensuring that social interventions, such as BI, are evaluated appropriately is key to ensuring their overall effectiveness. This systematic review therefore aims to report on available methods and domains of assessment, which have been used to evaluate BI interventions. These findings will assist in informing future program and research development and implementation. \n \n \n Methods \n Studies were identified through systematic searches of the indexed and grey literature (Databases included: Scopus, Embase, Medline, CINAHL, Web of Science, ProQuest databases, EBSCOhost Research Databases, and PsycINFO), hand-searching reference lists of included studies, and recommendations from experts. Citations were independently reviewed by two study team members. We included studies that reported on methods used to evaluate the impact of BI, incorporated primary data from an observational or experimental study, or were a protocol for a future BI study. We extracted information on the BI intervention, context and evaluation method. \n \n \n Results \n 86 eligible articles reported on 10 distinct BI interventions from the last six decades. Workforce participation was the most common outcome of interest among BI evaluations in the 1960–1980 era. During the 2000s, studies of BI expanded to include outcomes related to health, educational attainment, housing and other key facets of life impacted by individuals’ income. Many BI interventions were tested in randomized controlled trials with data collected through surveys at multiple time points. \n \n \n Conclusions \n Over the last two decades, the assessment of the impact of BI interventions has evolved to include a wide array of outcomes. This shift in evaluation outcomes reflects the current hypothesis that investing in BI can result in lower spending on health and social care. Methods of evaluation ranged but emphasized the use of randomization, surveys, and existing data sources (i.e., administrative data). Our findings can inform future BI intervention studies and interventions by providing an overview of how previous BI interventions have been evaluated and commenting on the effectiveness of these methods. \n \n \n Registration \n This systematic review was registered with PROSPERO (CRD 42016051218).", + creators: [ + { + creatorType: "author", + firstName: "Andrew D.", + lastName: "Pinto" + }, + { + creatorType: "author", + firstName: "Melissa", + lastName: "Perri" + }, + { + creatorType: "author", + firstName: "Cheryl L.", + lastName: "Pedersen" + }, + { + creatorType: "author", + firstName: "Tatiana", + lastName: "Aratangy" + }, + { + creatorType: "author", + firstName: "Ayu Pinky", + lastName: "Hapsari" + }, + { + creatorType: "author", + firstName: "Stephen W.", + lastName: "Hwang" + } + ], + DOI: "10.1186/s12939-021-01479-2", + date: "2021-12", + dateAdded: "2021-06-20T03:08:30Z", + dateModified: "2021-06-20T03:08:36Z", + publicationTitle: "International Journal for Equity in Health", + tags: [ + { tag: "housing" } + ], + url: "https://equityhealthj.biomedcentral.com/articles/10.1186/s12939-021-01479-2" + } + }), + meta: { + creatorSummary: "Pinto et al.", + numChildren: 1, + parsedDate: "2021-12" + } + }, + { + ...makeItemMetadata({ + citekey: "solbergTheoryPracticePrimary2016", + itemType: citoids[semanticIdentifier].itemType, + key: "_SEMANTIC_ITEM_", + library: userLibrary, + title: citoids[semanticIdentifier].title, + version: 333, + data: { + abstractNote: citoids[semanticIdentifier].abstractNote, + creators: citoids[semanticIdentifier].creators, + date: citoids[semanticIdentifier].date, + DOI: citoids[semanticIdentifier].DOI, + publicationTitle: citoids[semanticIdentifier].publicationTitle, + relations: { + "dc:relation": userLibrary.path + "/items/" + "PPD648N6" + }, + tags: citoids[semanticIdentifier].tags, + url: citoids[semanticIdentifier].url + } + }), + meta: { + creatorSummary: "Solberg", + numChildren: 0, + parsedDate: citoids[semanticIdentifier].date + } + } +]; + +const makeBibEntry = ({ citekey, biblatex }: { citekey: string, biblatex: string }): Mocks.Bib => { + const item = data.find(it => it.key == citekey)!; + const { data: { key }, library, links, meta, version } = item; + return { + biblatex, + key, + library, + links, + meta, + version + }; +}; + +const bibEntries = { + "blochImplementingSocialInterventions2021": makeBibEntry({ + citekey: "blochImplementingSocialInterventions2021", + biblatex: "\n@article{blochImplementingSocialInterventions2021,\n\ttitle = {Implementing social interventions in primary care},\n\tvolume = {193},\n\trights = {© 2021 {CMA} Joule Inc. or its licensors. This is an Open Access article distributed in accordance with the terms of the Creative Commons Attribution ({CC} {BY}-{NC}-{ND} 4.0) licence, which permits use, distribution and reproduction in any medium, provided that the original publication is properly cited, the use is noncommercial (i.e., research or educational use), and no modifications or adaptations are made. See: https://creativecommons.org/licenses/by-nc-nd/4.0/},\n\tissn = {0820-3946, 1488-2329},\n\turl = {https://www.cmaj.ca/content/193/44/E1696},\n\tdoi = {10.1503/cmaj.210229},\n\tabstract = {{KEY} {POINTS}\n- Primary care–based social interventions offer an important means to mitigate threats to individual and community health posed by adverse social conditions.\n- Effective interventions include those that target individual-level determinants, connections with community resources, community-focused partnerships and structures within health teams that affect equity.\n- Accumulating evidence points to the positive impacts of social interventions on broad markers of health; however, most research in this area has focused on implementation and process measures, rather than outcomes.\n- Some interventions require large, interdisciplinary health care resources to implement, but many are accessible to small group practices or individual providers.},\n\tpages = {E1696--E1701},\n\tnumber = {44},\n\tjournaltitle = {{CMAJ}},\n\tauthor = {Bloch, Gary and Rozmovits, Linda},\n\turldate = {2021-11-12},\n\tdate = {2021-11-08},\n\tlangid = {english},\n\tpmid = {34750179},\n\tkeywords = {primary care, social prescribing},\n}" + }), + "pintoExploringDifferentMethods2021": makeBibEntry({ + citekey: "pintoExploringDifferentMethods2021", + biblatex: "\n@article{pintoExploringDifferentMethods2021,\n\ttitle = {Exploring different methods to evaluate the impact of basic income interventions: a systematic review},\n\tvolume = {20},\n\tissn = {1475-9276},\n\turl = {https://equityhealthj.biomedcentral.com/articles/10.1186/s12939-021-01479-2},\n\tdoi = {10.1186/s12939-021-01479-2},\n\tshorttitle = {Exploring different methods to evaluate the impact of basic income interventions},\n\tabstract = {Abstract \n \n Background \n Persistent income inequality, the increase in precarious employment, the inadequacy of many welfare systems, and economic impact of the {COVID}-19 pandemic have increased interest in Basic Income ({BI}) interventions. Ensuring that social interventions, such as {BI}, are evaluated appropriately is key to ensuring their overall effectiveness. This systematic review therefore aims to report on available methods and domains of assessment, which have been used to evaluate {BI} interventions. These findings will assist in informing future program and research development and implementation. \n \n \n Methods \n Studies were identified through systematic searches of the indexed and grey literature (Databases included: Scopus, Embase, Medline, {CINAHL}, Web of Science, {ProQuest} databases, {EBSCOhost} Research Databases, and {PsycINFO}), hand-searching reference lists of included studies, and recommendations from experts. Citations were independently reviewed by two study team members. We included studies that reported on methods used to evaluate the impact of {BI}, incorporated primary data from an observational or experimental study, or were a protocol for a future {BI} study. We extracted information on the {BI} intervention, context and evaluation method. \n \n \n Results \n 86 eligible articles reported on 10 distinct {BI} interventions from the last six decades. Workforce participation was the most common outcome of interest among {BI} evaluations in the 1960–1980 era. During the 2000s, studies of {BI} expanded to include outcomes related to health, educational attainment, housing and other key facets of life impacted by individuals’ income. Many {BI} interventions were tested in randomized controlled trials with data collected through surveys at multiple time points. \n \n \n Conclusions \n Over the last two decades, the assessment of the impact of {BI} interventions has evolved to include a wide array of outcomes. This shift in evaluation outcomes reflects the current hypothesis that investing in {BI} can result in lower spending on health and social care. Methods of evaluation ranged but emphasized the use of randomization, surveys, and existing data sources (i.e., administrative data). Our findings can inform future {BI} intervention studies and interventions by providing an overview of how previous {BI} interventions have been evaluated and commenting on the effectiveness of these methods. \n \n \n Registration \n This systematic review was registered with {PROSPERO} ({CRD} 42016051218).},\n\tpages = {142},\n\tnumber = {1},\n\tjournaltitle = {International Journal for Equity in Health},\n\tshortjournal = {Int J Equity Health},\n\tauthor = {Pinto, Andrew D. and Perri, Melissa and Pedersen, Cheryl L. and Aratangy, Tatiana and Hapsari, Ayu Pinky and Hwang, Stephen W.},\n\turldate = {2021-06-20},\n\tdate = {2021-12},\n\tlangid = {english},\n}" + }) +}; + +export const findBibEntry = ({ type, id, key }: Pick & { key: string }) => { + return Object.values(bibEntries).find(entry => entry.key == key && entry.library.type + "s" == type && entry.library.id == id); +}; + +export const findItems = ({ type, id, since }: Pick & { since: number }) => { + return data.filter(item => item.library.type + "s" == type && item.library.id == id && item.version > since); +}; + +export const handleItems = [ + http.get( + zotero(":libraryType/:libraryID/items"), + ({ request, params }) => { + const { libraryType, libraryID } = params; + const url = new URL(request.url); + const since = url.searchParams.get("since"); + const include = url.searchParams.get("include") || "json"; + + // Otherwise create success response + const { type, id, version } = Object.values(libraries).find(lib => lib.path == `${libraryType}/${libraryID}`)!; + + // Bibliography entries + if (include == "biblatex") { + const keyList = url.searchParams.get("itemKey")!.split(","); + const bibs = keyList.map(key => findBibEntry({ type, id, key })!); + return HttpResponse.json(bibs); + } + + // Items JSON + const items = findItems({ type, id, since: Number(since) }); + return HttpResponse.json( + items, + { + headers: { + "last-modified-version": `${version}`, + // We're not mocking for additional requests + "total-results": `${Math.min(items.length, 100)}` + } + } + ); + } + ), + http.post( + zotero(":libraryType/:libraryID/items"), + async ({ request, params }) => { + + const { libraryType, libraryID } = params; + const itemsData = await request.json(); + + const library = Object.values(libraries).find(lib => lib.path == `${libraryType}/${libraryID}`)!; + + type TReduce = { + [k in keyof Mocks.Responses.ItemsPost]: ObjValues[] + }; + const output = itemsData.reduce((obj, item) => { + const { key = "__NO_UNIQUE_KEY__", version = library.version, ...props } = item; + + if (!key) { + // We're not actually adding the item to the data, so no need to ensure keys are unique + obj.success.push("__NO_UNIQUE_KEY__"); + obj.successful.push({ + ...makeItemMetadata({ + key, + itemType: props.itemType, + library, + title: props.title, + version: library.version, + data: props + }), + meta: { + creatorSummary: "", + numChildren: 0, + parsedDate: "" + } + }); + } else { + const libraryCopy = data.find(it => it.library.type + "s" == library.type && it.library.id == library.id)!; + if (version < libraryCopy.version) { + obj.failed.push(libraryCopy.data.key); + obj.unchanged.push(libraryCopy.data.key); + } else { + obj.success.push(libraryCopy.data.key); + obj.successful.push({ + ...libraryCopy, + data: { + ...libraryCopy.data, + key, + version, + ...props + } + }); + } + } + + return obj; + + }, { + failed: [], + success: [], + successful: [], + unchanged: [] + }); + + const restructuredOutput: Mocks.Responses.ItemsPost = { failed: {}, success: {}, successful: {}, unchanged: {} }; + + Object.keys(output).forEach(cat => { + restructuredOutput[cat] = Object.fromEntries(output[cat].map((el, i) => [i, el])); + }); + + return HttpResponse.json(restructuredOutput); + } + ) +]; + +export { + bibEntries as entries, + data as items +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/keys.ts b/prototypes/zotero-roam-mcp/mocks/zotero/keys.ts new file mode 100644 index 0000000..474cb31 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/keys.ts @@ -0,0 +1,59 @@ +import { http, HttpResponse } from "msw"; +import { zotero } from "./common"; +import { libraries } from "./libraries"; +import { Mocks } from "Mocks"; + + +const { userLibrary, groupLibrary } = libraries; +const addUserInfo = () => ({ userID: userLibrary.id, username: userLibrary.username, displayName: "" }); + +const data = { + keyWithFullAccess: { + ...addUserInfo(), + key: "ABCD1234EFG", + access: { + user: { + library: true, + files: true, + notes: true, + write: true + }, + groups: { + [groupLibrary.id]: { + library: true, + write: true + } + } + } + } as Mocks.Responses.Permissions, + keyWithNoGroupAccess: { + ...addUserInfo(), + key: "EFGH5678IJK", + access: { + user: { + library: true, + files: true, + notes: true, + write: true + }, + groups: { + all: { + library: false, + write: false + } + } + } + } as Mocks.Responses.Permissions +}; + +export const handleAPIKey = http.get( + zotero("keys/:apikey"), + ({ params }) => { + const { apikey } = params; + return HttpResponse.json(Object.values(data).find(val => val.key == apikey)!); + } +); + +export { + data as apiKeys +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/libraries.ts b/prototypes/zotero-roam-mcp/mocks/zotero/libraries.ts new file mode 100644 index 0000000..103ce47 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/libraries.ts @@ -0,0 +1,29 @@ +import { Mocks } from "Mocks"; + + +const configLibrary = (type: Mocks.Library["type"], id: number, name: string, version: number): Mocks.Library => { + const path = [type, id].join("/"); + return { + id, + links: { + alternate: { + href: "https://www.zotero.org/" + (type == "users" ? name : path), + type: "text/html" + } + }, + name, + path, + type, + username: (type == "users" ? name : undefined), + version + }; +}; + +const data = { + "userLibrary": configLibrary("users", 123456, "username", 4310), + "groupLibrary": configLibrary("groups", 456789, "group-library", 1598) +}; + +export { + data as libraries +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/notes.ts b/prototypes/zotero-roam-mcp/mocks/zotero/notes.ts new file mode 100644 index 0000000..f36d779 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/notes.ts @@ -0,0 +1,62 @@ +import { makeEntityLinks, makeLibraryMetadata } from "./common"; +import { libraries } from "./libraries"; +import { Mocks } from "Mocks"; + + +const { userLibrary } = libraries; + +type MakeNoteArgs = { + key: string, + library: Mocks.Library, + data: Partial, + note: Mocks.ItemNote["data"]["note"], + numChildren?: number, + parentItem: string, + version?: number +}; +const makeNote = ({ key, library, data, note, numChildren = 0, parentItem, version = 1 }: MakeNoteArgs): Mocks.ItemNote => { + return { + key, + data: { + dateAdded: "2021-06-16T21:36:12Z", + dateModified: "2021-06-16T21:36:12Z", + itemType: "note", + key, + note, + parentItem, + relations: {}, + tags: [], + version, + ...data + }, + links: makeEntityLinks({ key, library, parentItem }), + library: makeLibraryMetadata(library), + meta: { + numChildren + }, + version, + has_citekey: false + }; +}; + +export const sampleNote = makeNote({ + key: "A12BCDEF", + library: userLibrary, + note: "
Annotations

\"After all, argues Balthasar, \"in a world without beauty... the good also loses its attractiveness, self-evidence why it must be carried out.\" Why not prefer evil over good? \"Why not investigate Satan's depth?\" Why desire the beatific vision? Accordingly, Balthasar seeks to rectify the given imbalance by embarking on an \"archeology of alienated beauty\" in dialogue with thinkers such as Irenaeus, Augustine, Pseudo-Dionysius, Dante, Hopkins, Solovyev, and others.\" (Smith 2003:1)

", + parentItem: "P34QRSTU", + data: { + tags: [{ tag: "toRead" }] + }, + version: 1234 +}); + +export const sampleOlderNote = makeNote({ + key: "__OLDER_NOTE__", + library: userLibrary, + note: "
an older note
", + parentItem: "P34QRSTU", + data: { + dateAdded: "1996-04-06T22:00:00Z" + }, + version: 1234 +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/pdfs.ts b/prototypes/zotero-roam-mcp/mocks/zotero/pdfs.ts new file mode 100644 index 0000000..c0c0d83 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/pdfs.ts @@ -0,0 +1,54 @@ +import { makeEntityLinks, makeLibraryMetadata } from "./common"; +import { libraries } from "./libraries"; +import { Mocks } from "Mocks"; + + +const { userLibrary } = libraries; + +type MakePDFArgs = { + key: string, + library: Mocks.Library, + numChildren?: number, + parentItem: string, + title: string, + version: number, + data?: Partial +}; +const makePDF = ({ key, library, numChildren = 0, parentItem, title, version, data = {} }: MakePDFArgs): Mocks.ItemAttachment => ({ + key, + data: { + accessDate: "", + charset: "", + contentType: "application/pdf", + dateAdded: "2022-01-01T23:00:00Z", + dateModified: "2022-03-04T11:00:00Z", + itemType: "attachment", + key, + linkMode: "linked_file", + note: "", + parentItem, + path: "attachments:" + title, + relations: {}, + tags: [], + title, + url: "", + version, + ...data + }, + library: makeLibraryMetadata(library), + links: makeEntityLinks({ key, library, parentItem }), + meta: { + numChildren + }, + version, + has_citekey: false +}); + +export const samplePDF = makePDF({ + key: "P34QRSTU", + library: userLibrary, + numChildren: 1, + parentItem: "PPD648N6", + title: "Bloch_Rozmovits_2021_Implementing social interventions in primary care.pdf", + version: 18 +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/mocks/zotero/tags.ts b/prototypes/zotero-roam-mcp/mocks/zotero/tags.ts new file mode 100644 index 0000000..4259319 --- /dev/null +++ b/prototypes/zotero-roam-mcp/mocks/zotero/tags.ts @@ -0,0 +1,97 @@ +import { http, HttpResponse } from "msw"; +import { zotero } from "./common"; +import { libraries } from "./libraries"; +import { searchEngine } from "../../src/utils"; +import { Mocks } from "Mocks"; + + +const { userLibrary, groupLibrary } = libraries; + +type MakeTagArgs = { + tag: string, + library: Mocks.Library, +} & Partial; +const makeTag = ({ tag, library, type = 1, numItems = 1 }: MakeTagArgs): Mocks.Tag => { + const { path } = library; + return { + tag, + links: { + self: { + href: `https://api.zotero.org/${path}/tags/${encodeURIComponent(tag)}`, + type: "application/json" + }, + alternate: { + href: `https://zotero.org/${path}/tags/${encodeURIComponent(tag)}`, + type: "text/html" + } + }, + meta: { + type, + numItems + } + }; +}; + +export const findTags = (path: string, token: string) => { + const tagList = data[path]; + return tagList.filter(t => searchEngine(t.tag, token, { any_case: true, match: "exact", search_compounds: true })); +}; + +const data: Record = { + [userLibrary.path]: [ + makeTag({ tag: "immigrant youth", library: userLibrary, numItems: 2 }), + makeTag({ tag: "immigration", library: userLibrary, type: 1 }), + makeTag({ tag: "immigration", library: userLibrary, type: 0 }), + makeTag({ tag: "IMMIGRATION", library: userLibrary, type: 1 }), + makeTag({ tag: "patient journeys", library: userLibrary }) + ], + [groupLibrary.path]: [ + makeTag({ tag: "Urban design", library: groupLibrary, numItems: 11 }), + makeTag({ tag: "HOUSING", library: groupLibrary, type: 0, numItems: 7 }), + makeTag({ tag: "housing", library: groupLibrary, type: 0 }), + makeTag({ tag: "housing", library: groupLibrary, type: 1, numItems: 4 }) + ] +}; + +export const handleTags = [ + http.get( + zotero(":libraryType/:libraryID/tags"), + ({ params }) => { + const { libraryType, libraryID } = params; + + const { path, version } = Object.values(libraries).find(val => val.path == `${libraryType}/${libraryID}`)!; + const tags = data[path]; + + return HttpResponse.json( + tags, + { + headers: { + "last-modified-version": `${version}`, + // We're not mocking with additional requests + "total-results": `${Math.min(tags.length, 100)}` + } + } + ); + } + ), + http.delete( + zotero(":libraryType/:libraryID/tags"), + ({ request, params }) => { + const { libraryType, libraryID } = params; + // const tag = req.url.searchParams("tag"); + const ifUnmodifiedSince = request.headers.get("If-Unmodified-Since-Version"); + + const { version } = Object.values(libraries).find(val => val.path == `${libraryType}/${libraryID}`)!; + + if(Number(ifUnmodifiedSince) < version){ + return new HttpResponse(null, { status: 412, statusText: "Precondition failed" }); + } else { + return new HttpResponse(null, { status: 204, statusText: "No content" }); + } + } + ) +]; + +export { + data as tags +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/package.json b/prototypes/zotero-roam-mcp/package.json new file mode 100644 index 0000000..5653c80 --- /dev/null +++ b/prototypes/zotero-roam-mcp/package.json @@ -0,0 +1,48 @@ +{ + "name": "zotero-roam-mcp", + "version": "0.7.29-mcp.0", + "private": true, + "description": "Fork of the zoteroRoam extension adding experimental headless AI tools for Roam MCP: search Zotero items, import item metadata, and import notes into citekey pages without the UI.", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "dev": "vite build --config dev/vite.config.mts --watch", + "build": "vite build --config dev/vite.config.mts", + "test": "vitest run --config ./dev/vite.config.mts" + }, + "dependencies": { + "@blueprintjs/core": "^3.54.0", + "@blueprintjs/datetime": "^3.24.1", + "@blueprintjs/icons": "^3.33.0", + "@blueprintjs/popover2": "^0.14.0", + "@blueprintjs/select": "^3.19.1", + "@tanstack/query-core": "^4.36.1", + "@tanstack/query-persist-client-core": "^4.36.1", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-persist-client": "^4.36.1", + "axios": "^1.8.2", + "axios-retry": "^4.5.0", + "idb": "^6.1.5", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "scite-badge": "github:scitedotai/scite-badge#v8.0.6-prod", + "tributejs": "^5.1.3" + }, + "devDependencies": { + "@storybook/test": "^8.3.2", + "@swc/core": "^1.7.26", + "@testing-library/dom": "^8.20.1", + "@testing-library/react": "^12.1.5", + "@testing-library/react-hooks": "^8.0.1", + "@vitejs/plugin-react-swc": "^3.7.0", + "fake-indexeddb": "^4.0.2", + "jsdom": "26.0.0", + "msw": "^2.4.9", + "sass": "^1.79.3", + "typescript": "^4.9.5", + "vite": "^5.4.19", + "vite-plugin-externals": "^0.6.2", + "vitest": "^2.1.1", + "vitest-mock-extended": "^2.0.2" + } +} diff --git a/prototypes/zotero-roam-mcp/src/api/helpers.test.ts b/prototypes/zotero-roam-mcp/src/api/helpers.test.ts new file mode 100644 index 0000000..9641249 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/api/helpers.test.ts @@ -0,0 +1,325 @@ +import { mock } from "vitest-mock-extended"; + +import { cleanBibliographyHTML, compareAnnotationRawIndices, formatNotes, formatZoteroAnnotations, getItemCreators, getItemDateAdded, getItemTags } from "./helpers"; + +import { setupInitialSettings } from "../setup"; +import { formatZoteroNotes, simplifyZoteroAnnotations } from "../utils"; + +import { bibs, sampleAnnot, sampleAnnotPrevPage, sampleAnnotLaterPage, sampleImageAnnot, sampleNote, sampleOlderNote } from "Mocks"; +import { existing_block_uid, existing_block_uid_with_children, uid_with_existing_block, uid_with_existing_block_with_children } from "Mocks/roam"; +import { UserSettings } from "Types/extension"; +import { ZItem, ZItemTop } from "Types/transforms"; + + +const simplifiedAnnot = simplifyZoteroAnnotations([sampleAnnot])[0]; +const { annotations: annotationsSettings, notes } = setupInitialSettings({}); + + +describe("cleanBibliographyHTML", () => { + // Necessary since jsdom does not support innerText + // It shouldn't give discrepant results here + // https://github.com/jsdom/jsdom/issues/1245#issuecomment-763535573 + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, "innerText", { + get() { + return this.textContent; + } + }); + }); + + it("formats citations with one content div (like Chicago)", () => { + expect(cleanBibliographyHTML(bibs.itemFromUserLibrary.bib)) + .toBe("Agarwal, Payal, Rick Wang, Christopher Meaney, Sakina Walji, Ali Damji, Navsheer Gill Toor, Gina Yip, et al. “Sociodemographic Differences in Patient Experience with Virtual Care during COVID-19.” medRxiv, July 22, 2021. https://www.medrxiv.org/content/10.1101/2021.07.19.21260373v1."); + }); + + it("formats citations with multiple content divs (like Vancouver)", () => { + const sample_bib_vancouver = "
\n
\n
1.
MacDonald K, Fainman-Adelman N, Anderson KK, Iyer SN. Pathways to mental health services for young people: a systematic review. Soc Psychiatry Psychiatr Epidemiol. 2018 Oct;53(10):1005–38.
\n
\n
"; + + expect(cleanBibliographyHTML(sample_bib_vancouver)) + .toBe("1. MacDonald K, Fainman-Adelman N, Anderson KK, Iyer SN. Pathways to mental health services for young people: a systematic review. Soc Psychiatry Psychiatr Epidemiol. 2018 Oct;53(10):1005–38."); + }); +}); + +test("compareAnnotationRawIndices", () => { + const indices = [ + "00003|00014|00009", + "00005|00010|00007", + "00002|00022|00007", + "00002|00022|00030", + "00002|00024|00010", + "00003|00014|00007", + "00005|00009|00009" + ]; + + expect(indices.sort(compareAnnotationRawIndices)) + .toEqual([ + "00002|00022|00007", + "00002|00022|00030", + "00002|00024|00010", + "00003|00014|00007", + "00003|00014|00009", + "00005|00009|00009", + "00005|00010|00007" + ]); +}); + +describe("formatNotes", () => { + it("sorts output", () => { + const notesSettings: UserSettings["notes"] = { + ...notes, + nest_preset: false + }; + + expect(formatNotes([sampleAnnotLaterPage, sampleAnnotPrevPage], null, { annotationsSettings, notesSettings })) + .toEqual([ + ...formatZoteroAnnotations([sampleAnnotPrevPage], annotationsSettings), + ...formatZoteroAnnotations([sampleAnnotLaterPage], annotationsSettings) + ]); + + expect(formatNotes([sampleNote, sampleOlderNote], null, { annotationsSettings, notesSettings })) + .toEqual([ + ...formatZoteroNotes([sampleOlderNote], notesSettings), + ...formatZoteroNotes([sampleNote], notesSettings) + ]); + + expect(formatNotes([sampleNote, sampleAnnotLaterPage, sampleAnnotPrevPage], null, { annotationsSettings, notesSettings })) + .toEqual([ + ...formatZoteroAnnotations([sampleAnnotPrevPage], annotationsSettings), + ...formatZoteroAnnotations([sampleAnnotLaterPage], annotationsSettings), + ...formatZoteroNotes([sampleNote], notesSettings) + ]); + }); + + it("handles nested output (with preset)", () => { + const notesSettings = { + ...notes, + nest_preset: "[[Notes]]", + nest_use: "preset" + } as const + + expect(formatNotes([sampleNote, sampleOlderNote], null, { annotationsSettings, notesSettings })) + .toEqual([ + { + string: notesSettings.nest_preset, + text: notesSettings.nest_preset, + children: [ + ...formatZoteroNotes([sampleOlderNote], notesSettings), + ...formatZoteroNotes([sampleNote], notesSettings) + ] + } + ]); + }); + + it("handles nested output (with custom string)", () => { + const custom_string = "[[My Notes]]"; + + const notesSettings = { + ...notes, + nest_char: custom_string, + nest_use: "custom" + } as const + + expect(formatNotes([sampleNote, sampleOlderNote], null, { annotationsSettings, notesSettings })) + .toEqual([ + { + string: custom_string, + text: custom_string, + children: [ + ...formatZoteroNotes([sampleOlderNote], notesSettings), + ...formatZoteroNotes([sampleNote], notesSettings) + ] + } + ]); + }); + + it("handles nested output, with block checking", () => { + const custom_string = "[[My Notes]]"; + const mockSettings = { + annotationsSettings, + notesSettings: { + ...notes, + nest_char: custom_string, + nest_position: "top", + nest_preset: false, + nest_use: "custom" + } + } as const + + const formattedOutput = formatZoteroNotes([sampleNote], mockSettings.notesSettings); + + expect(formatNotes([sampleNote], uid_with_existing_block, mockSettings)).toEqual( + formattedOutput.map(blck => ({ + string: blck, + text: blck, + order: 0, + parentUID: existing_block_uid + })) + ); + + expect(formatNotes([sampleNote], uid_with_existing_block_with_children, mockSettings)).toEqual( + formattedOutput.map(blck => ({ + string: blck, + text: blck, + order: 0, + parentUID: existing_block_uid_with_children + })) + ); + + expect(formatNotes([sampleNote], "uid without existing block", mockSettings)).toEqual([ + { + string: custom_string, + text: custom_string, + children: formattedOutput + } + ]); + + }); + + it("handles nested output, with block checking & position", () => { + const mockSettings = { + annotationsSettings, + notesSettings: { + ...notes, + nest_char: "[[My Notes]]", + nest_position: "bottom", + nest_preset: false, + nest_use: "custom" + } + } as const + + const formattedOutput = formatZoteroNotes([sampleNote], mockSettings.notesSettings); + + expect(formatNotes([sampleNote], uid_with_existing_block, mockSettings)) + .toEqual( + formattedOutput.map(blck => ({ + string: blck, + text: blck, + order: 0, + parentUID: existing_block_uid + })) + ); + + expect(formatNotes([sampleNote], uid_with_existing_block_with_children, mockSettings)) + .toEqual( + formattedOutput.map(blck => ({ + string: blck, + text: blck, + order: 2, + parentUID: existing_block_uid_with_children + })) + ); + }); +}); + +describe("formatZoteroAnnotations", () => { + it("formats with defaults", () => { + expect(formatZoteroAnnotations([sampleAnnot])) + .toEqual([ + { + string: `[[>]] ${simplifiedAnnot.text} ([p. ${simplifiedAnnot.page_label}](${simplifiedAnnot.link_page})) ${simplifiedAnnot.tags_string}`, + text: `[[>]] ${simplifiedAnnot.text} ([p. ${simplifiedAnnot.page_label}](${simplifiedAnnot.link_page})) ${simplifiedAnnot.tags_string}`, + children: [simplifiedAnnot.comment] + } + ]); + expect(formatZoteroAnnotations([sampleImageAnnot])) + .toEqual([ + // Images return null, and are filtered out + ]); + }); + + it("groups by day added", () => { + expect(formatZoteroAnnotations([sampleAnnot], { group_by: "day_added" })) + .toEqual([ + { + string: "[[March 18th, 2022]]", + text: "[[March 18th, 2022]]", + children: [ + { + string: `[[>]] ${simplifiedAnnot.text} ([p. ${simplifiedAnnot.page_label}](${simplifiedAnnot.link_page})) ${simplifiedAnnot.tags_string}`, + text: `[[>]] ${simplifiedAnnot.text} ([p. ${simplifiedAnnot.page_label}](${simplifiedAnnot.link_page})) ${simplifiedAnnot.tags_string}`, + children: [simplifiedAnnot.comment] + } + ] + } + ]); + }); +}); + +describe("getItemCreators", () => { + const mockItem = mock({ + data: { + creators: [ + { name: "Gary Bloch", creatorType: "author" }, + { name: "Linda Rozmovits", creatorType: "author" } + ] + } + }); + + const cases = [ + [ + { return_as: "identity" }, + [ + { name: "Gary Bloch", type: "author", inGraph: false }, + { name: "Linda Rozmovits", type: "author", inGraph: false } + ] + ], + [ + { return_as: "array" }, + [ + "Gary Bloch", + "Linda Rozmovits" + ] + ], + [ + { return_as: "string", brackets: true, use_type: true }, + "[[Gary Bloch]], [[Linda Rozmovits]]" + ], + [ + { return_as: "string", brackets: false, use_type: true }, + "Gary Bloch, Linda Rozmovits" + ] + ] as const; + + test.each(cases)( + "%#", + (config, expectation) => { + expect(getItemCreators(mockItem, config)) + .toEqual(expectation); + } + ); + +}); + +test("getItemDateAdded", () => { + const date = new Date(2022, 0, 1).toString(); + const mockItem = mock({ data: { dateAdded: date } }); + + expect(getItemDateAdded(mockItem)).toBe("[[January 1st, 2022]]"); + expect(getItemDateAdded(mockItem, { brackets: false })).toBe("January 1st, 2022"); + +}); + +describe("getItemTags", () => { + const mockItem = mock({ + data: { + tags: [{ tag: "housing" }] + } + }); + + const cases = [ + [{ return_as: "array", brackets: false }, ["housing"]], + [{ return_as: "array", brackets: true }, ["#[[housing]]"]], + [{ return_as: "string", brackets: false }, "housing"], + [{ return_as: "string", brackets: true }, "#[[housing]]"], + [{}, "#[[housing]]"] + ] as const; + + test.each(cases)( + "%#", + (config, expectation) => { + expect(getItemTags(mockItem, config)) + .toEqual(expectation); + } + ); + +}); diff --git a/prototypes/zotero-roam-mcp/src/api/helpers.ts b/prototypes/zotero-roam-mcp/src/api/helpers.ts new file mode 100644 index 0000000..8d3fc0a --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/api/helpers.ts @@ -0,0 +1,340 @@ +import { ZoteroAPI } from "@clients/zotero"; +import { findRoamBlock, findRoamPage, makeDNP } from "@services/roam"; + +import { cleanNewlines, compareAnnotationIndices, executeFunctionByName, extractSortIndex, formatItemAnnotations, formatZoteroNotes, getLocalLink, getPDFLink, getWebLink, simplifyZoteroAnnotations } from "../utils"; + +import { SettingsAnnotations, SettingsNotes, SettingsTypemap } from "Types/extension"; +import { AsBoolean } from "Types/helpers"; +import { RImportableElement, ZItem, ZItemAnnotation, ZItemAttachment, ZItemNote, ZItemTop, ZLinkType, ZLinkOptions, isZAnnotation, isZNote } from "Types/transforms"; + + +/** Parses the XHTML bibliography for a Zotero item into Roam formatting + * @param bib - The item's XHTML bibliography + * @returns The clean bibliography string + */ +function cleanBibliographyHTML(bib: string) { + let bibString = bib; + + // Strip divs + const richTags = ["div"]; + richTags.forEach(tag => { + // eslint-disable-next-line no-useless-escape + const tagRegex = new RegExp(`<\/?${tag}>|<${tag} .+?>`, "g"); // Covers both the simple case : or , and the case with modifiers : + bibString = bibString.replaceAll(tagRegex, ""); + }); + + bibString = cleanNewlines(bibString).trim(); + + // Use a textarea element to decode HTML + const formatter = document.createElement("textarea"); + formatter.innerHTML = bibString; + let formattedBib = formatter.innerText; + // Convert italics + formattedBib = formattedBib.replaceAll(/<\/?i>/g, "__"); + // Convert links + const linkRegex = /(.+)<\/a>/g; + formattedBib = formattedBib.replaceAll(linkRegex, "[$2]($1)"); + + return formattedBib; +} + + +/** Orders the (raw) indices of two Zotero annotations */ +function compareAnnotationRawIndices(a: string, b: string): number { + return compareAnnotationIndices( + extractSortIndex(a), + extractSortIndex(b) + ); +} + + +/* istanbul ignore next */ +/** Formats an item's and its children's metadata for import to Roam using the default template */ +function formatItemMetadata( + item: ZItemTop, pdfs: ZItemAttachment[], notes: (ZItemNote | ZItemAnnotation)[], + { annotationsSettings, notesSettings, typemap }: { annotationsSettings: SettingsAnnotations, notesSettings: SettingsNotes, typemap: SettingsTypemap } +): RImportableElement[] { + const metadata: RImportableElement[] = []; + + if (item.data.title) { metadata.push(`Title:: ${item.data.title}`); } // Title, if available + if (item.data.creators.length > 0) { metadata.push(`Author(s):: ${getItemCreators(item, { return_as: "string", brackets: true, use_type: true })}`); } // Creators list, if available + if (item.data.abstractNote) { metadata.push(`Abstract:: ${item.data.abstractNote}`); } // Abstract, if available + if (item.data.itemType) { metadata.push(`Type:: ${getItemType(item, { brackets: true }, { typemap })}`); } // Item type, according to typemap + metadata.push(`Publication:: ${getItemPublication(item, { brackets: true })}`); + const year = getItemYear(item); + if (year) { metadata.push(`Year:: ${year}`); } // Publication year, if available + if (item.data.url) { metadata.push(`URL : ${item.data.url}`); } + if (item.data.dateAdded) { metadata.push(`Date Added:: ${getItemDateAdded(item)}`); } // Date added, as Daily Notes Page reference + metadata.push(`Zotero links:: ${getItemLink(item, "local", { format: "markdown", text: "Local library" })}, ${getItemLink(item, "web", { format: "markdown", text: "Web library" })}`); // Local + Web links to the item + if (item.data.tags.length > 0) { metadata.push(`Tags:: ${getItemTags(item, { return_as: "string", brackets: true })}`); } // Tags, if any + + if (pdfs.length > 0) { + metadata.push(`PDF links : ${(formatPDFs(pdfs, "links") as string[]).join(", ")}`); + } + if (notes.length > 0) { + const formattedOutput = formatNotes(notes, null, { annotationsSettings, notesSettings }); + metadata.push(...formattedOutput); + } + + return metadata; +} + + +/** Formats Zotero notes/annotations items */ +function formatNotes( + /** The Array of Zotero notes/annotations */ + notes: (ZItemNote | ZItemAnnotation)[], + /** The UID of the parent item's Roam page, if it exists */ + pageUID: string | null = null, + /** The user's current settings */ + { annotationsSettings, notesSettings }: { annotationsSettings: SettingsAnnotations, notesSettings: SettingsNotes } +): RImportableElement[] { + if (!notes || notes.length == 0) { + return []; + } else { + const annotItems = notes + .filter(isZAnnotation) + .sort((a, b) => compareAnnotationRawIndices(a.data.annotationSortIndex, b.data.annotationSortIndex)); + const noteItems = notes + .filter(isZNote) + .sort((a, b) => a.data.dateAdded < b.data.dateAdded ? -1 : 1); + const formattedOutput = [ + ...formatZoteroAnnotations(annotItems, annotationsSettings), + ...formatZoteroNotes(noteItems, notesSettings) + ]; + + const { nest_char, nest_position, nest_preset, nest_use } = notesSettings; + + // If nesting is disabled, simply return the array of blocks + if (nest_use == "preset" && !nest_preset) { + return formattedOutput; + } + + // Else if the page UID was provided, check if the page already has a block with the same content + // If yes, set that block as the parent for all the outputted blocks + if (pageUID) { + const blockString = ((nest_use == "custom") ? nest_char : nest_preset) || ""; + const existingBlock = findRoamBlock(blockString, pageUID); + + if (existingBlock) { + const { uid, children = [] } = existingBlock; + const pos = (nest_position == "bottom") ? children.length : 0; + + return formattedOutput.map(blck => { + if (blck.constructor === String) { + return { + string: blck, + text: blck, + order: pos, + parentUID: uid + }; + } else { + return { + ...blck, + order: pos, + parentUID: uid + }; + } + }); + } + } + + const blockString = (nest_use == "custom" ? nest_char : nest_preset) || ""; + return [{ + string: blockString, + text: blockString, + children: formattedOutput + }]; + + } +} + + +type PDFFormatOption = "links" | "identity" | "string"; +type PDFAsIdentity = { key: string, link: string, title: string }; + +/** Converts Zotero PDF items into a specific format */ +function formatPDFs(pdfs: ZItemAttachment[], as: PDFFormatOption = "string"): string | string[] | PDFAsIdentity[] { + if (!pdfs) { + switch (as) { + case "identity": + case "links": + return []; + case "string": + default: + return ""; + } + } else { + const pdfsIdentity = pdfs.map(file => ({ + title: file.data.filename || file.data.title, + key: file.key, + link: getPDFLink(file, "href") + })); + switch (as) { + case "identity": + return pdfsIdentity; + case "links": + return pdfsIdentity.map(entry => `[${entry.title}](${entry.link})`); + case "string": + default: + return pdfsIdentity.map(entry => `[${entry.title}](${entry.link})`).join(", "); + } + } +} + + +type CreatorAsIdentity = { inGraph: string | boolean, name: string, type: ZoteroAPI.CreatorType }; +type CreatorOptions = { brackets: boolean | "existing", return_as: "array" | "identity" | "string", use_type: boolean, authors_or_editors: boolean }; + +/** Retrieves the creators list of a Zotero item, and returns it into a specific format */ +function getItemCreators(item: ZItemTop, { return_as = "string", brackets = true, use_type = true, authors_or_editors = false }: Partial = {}): string | string[] | CreatorAsIdentity[] { + let creatorsToUse = item.data.creators; + + // If authors_or_editors is true, filter to show only authors OR editors (if no authors) + if (authors_or_editors) { + const authors = item.data.creators.filter(c => c.creatorType === "author"); + if (authors.length > 0) { + creatorsToUse = authors; + } else { + const editors = item.data.creators.filter(c => c.creatorType === "editor"); + creatorsToUse = editors; + } + } + + const creatorsInfoList = creatorsToUse.map(creator => { + const nameTag = "name" in creator + ? creator.name + : `${[creator.firstName, creator.lastName].filter(AsBoolean).join(" ")}`; + return { + name: nameTag, + type: creator.creatorType, + inGraph: findRoamPage(nameTag) + }; + }); + switch (return_as) { + case "identity": + return creatorsInfoList; + case "array": + return creatorsInfoList.map(c => c.name); + case "string": + default: + return creatorsInfoList.map(creator => { + const creatorTag = (brackets == true || (brackets == "existing" && creator.inGraph)) + ? `[[${creator.name}]]` + : creator.name; + return (use_type == true ? creatorTag + (creator.type == "author" ? "" : ` (${creator.type})`) : creatorTag); + }).join(", "); + } +} + + +type TagOptions = { brackets: boolean, return_as: "array" | "string" }; + +/** Retrieves the tags of a Zotero item, and returns them into a specific format */ +function getItemTags(item: ZItemTop, { return_as = "string", brackets = true }: Partial = {}): string | string[] { + const tags = item.data.tags.map(t => t.tag); + const tagList = (brackets == true ? tags.map(el => `#[[${el}]]`) : tags); + + switch (return_as) { + case "array": + return tagList; + case "string": + default: + return tagList.join(" "); + } +} + + +/** Formats an array of Zotero annotations into Roam blocks, with optional configuration */ +function formatZoteroAnnotations( + annotations: ZItemAnnotation[], + { func = "", use = "default", __with = "raw", ...settings }: Partial = {} +): any { + if (use == "function" && func) { + // If the user has provided a custom function, execute it with the desired input + return executeFunctionByName(func, window, __with == "raw" ? annotations : simplifyZoteroAnnotations(annotations)); + } else { + return formatItemAnnotations(annotations, { ...settings }); + } +} + + +/** Returns the date on which an item was added to Zotero, in DNP format */ +function getItemDateAdded(item: ZItem, { brackets = true }: { brackets?: boolean } = {}): string { + return makeDNP(item.data.dateAdded, { brackets }); +} + +/** Returns a link for the item (web or local) */ +function getItemLink(item: ZItemTop, type: ZLinkType = "local", config: Partial = {}) { + return (type == "local") + ? getLocalLink(item, config) + : getWebLink(item, config); +} + + +/** Retrieves the publication details for a given item. + * The extension will check for the existence of a `publicationTitle`, then a `bookTitle`, then a `university` name. + */ +function getItemPublication(item: ZItemTop, { brackets = true }: { brackets?: boolean } = {}): string { + const maybePublication = item.data.publicationTitle || item.data.bookTitle || item.data.university; + if (maybePublication) { + return (brackets == true) + ? `[[${maybePublication}]]` + : maybePublication; + } else { + return ""; + } +} + +/** Retrieves the type of a Zotero item, according to a given typemap */ +function getItemType(item: ZItemTop, { brackets = true }: { brackets?: boolean } = {}, { typemap }: { typemap: SettingsTypemap }): string { + const type = typemap[item.data.itemType] || item.data.itemType; + return (brackets == true ? `[[${type}]]` : type); +} + +/** Retrieves the publication year for a given item */ +function getItemYear(item: ZItemTop): string { + if (!item.meta.parsedDate) { + return ""; + } + const dateObj = new Date(item.meta.parsedDate); + if (isNaN(Number(dateObj))) { + return ""; + } + return dateObj.getUTCFullYear().toString(); +} + + +/** Groups a list of citekeys by the library they belong to. */ +function groupCitekeysByLibrary(citekeys: string[], { items }: { items: ZItem[] }) { + return citekeys.reduce>((obj, citekey) => { + const libItem = items.find(it => it.key == citekey); + if (libItem) { + const location = libItem.library.type + "s/" + libItem.library.id; + if (Object.keys(obj).includes(location)) { + obj[location].push(libItem.data.key); + } else { + obj[location] = [libItem.data.key]; + } + } + return obj; + }, {}); +} + + +export { + cleanBibliographyHTML, + compareAnnotationRawIndices, + formatItemMetadata, + formatNotes, + formatPDFs, + formatZoteroAnnotations, + getItemCreators, + getItemDateAdded, + getItemLink, + getItemPublication, + getItemTags, + getItemType, + getItemYear, + groupCitekeysByLibrary +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/api/index.test.tsx b/prototypes/zotero-roam-mcp/src/api/index.test.tsx new file mode 100644 index 0000000..dc7eb61 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/api/index.test.tsx @@ -0,0 +1,382 @@ +import { QueryClient } from "@tanstack/query-core"; +import { persistQueryClientSave } from "@tanstack/query-persist-client-core"; +import { waitFor } from "@testing-library/dom"; +import "fake-indexeddb/auto"; +import { mock } from "vitest-mock-extended"; + +import IDBDatabaseService from "@services/idb"; + +import ZoteroRoam from "."; +import { cleanBibliographyHTML, formatNotes, formatPDFs, getItemCreators, getItemDateAdded, getItemTags } from "./helpers"; + +import { createPersisterWithIDB, setupInitialSettings } from "../setup"; +import { getLocalLink, getWebLink } from "../utils"; + +import { bibs, findBibliographyEntry, entries, findItems, items, apiKeys, libraries, sampleAnnot, sampleNote, samplePDF } from "Mocks"; + +import { UserRequests, UserSettings } from "Types/extension"; +import { ZItem, ZItemTop } from "Types/transforms"; + + +const { keyWithFullAccess: { key: masterKey } } = apiKeys; +const initSettings = setupInitialSettings({}); + + +describe("Formatting utils", () => { + const extension = new ZoteroRoam({ + queryClient: new QueryClient(), + requests: mock({ libraries: [] }), + settings: initSettings + }); + + test("formatNotes", () => { + expect(extension.formatNotes([sampleNote, sampleAnnot])) + .toEqual(formatNotes([sampleNote, sampleAnnot], null, { annotationsSettings: initSettings.annotations, notesSettings: initSettings.notes })); + expect(extension.formatNotes([])) + .toEqual([]); + + }); + + test("formatPDFs", () => { + expect(extension.formatPDFs([samplePDF])) + .toEqual(formatPDFs([samplePDF], "string")); + expect(extension.formatPDFs([])) + .toEqual(""); + }); + + test("getItemDateAdded", () => { + const date = new Date(2022, 0, 1).toString(); + const mockItem = mock({ data: { dateAdded: date } }); + + expect(extension.getItemDateAdded(mockItem)) + .toEqual(getItemDateAdded(mockItem, { brackets: true })); + }); + + test("getItemLink", () => { + const mockItem = mock(); + expect(extension.getItemLink(mockItem, "local")) + .toBe(getLocalLink(mockItem)); + expect(extension.getItemLink(mockItem, "web")) + .toBe(getWebLink(mockItem)); + }); + + describe("getItemPublication", () => { + const makeMock = (itemData: Partial) => mock({ data: { ...itemData } }); + const cases = [ + [{ publicationTitle: "some publication", bookTitle: undefined, university: undefined }, "some publication"], + [{ publicationTitle: undefined, bookTitle: "some book", university: undefined }, "some book"], + [{ publicationTitle: undefined, bookTitle: undefined, university: "some university" }, "some university"], + [{ publicationTitle: undefined, bookTitle: undefined, university: undefined }, ""] + ] as const; + + test.each(cases)( + "%# - no brackets", + (itemData, expectation) => { + const mockItem = makeMock(itemData); + expect(extension.getItemPublication(mockItem, { brackets: false })) + .toBe(expectation); + } + ); + + test.each(cases)( + "%# - with brackets", + (itemData, expectation) => { + const mockItem = makeMock(itemData); + expect(extension.getItemPublication(mockItem)) + .toBe(expectation ? `[[${expectation}]]` : ""); + } + ); + }); + + describe("Retrieving the formatted type for an item", () => { + const cases = [ + [{ itemType: "journalArticle" }, {}, "[[Article]]"], + [{ itemType: "bookSection" }, { brackets: false }, "Chapter"] + ] as const; + + test.each(cases)( + "%#", + (itemData, config, expectation) => { + const mockItem = mock({ data: { ...itemData } }); + expect(extension.getItemType(mockItem, config)) + .toBe(expectation); + } + ); + }); +}); + +describe("Retrieval utils", () => { + let client: QueryClient; + let extension: ZoteroRoam; + + beforeEach(() => { + client = new QueryClient(); + extension = new ZoteroRoam({ + queryClient: client, + requests: mock({ + libraries: Object.values(libraries).map(lib => ({ apikey: masterKey, path: lib.path })) + }), + settings: mock() + }); + }); + + test("Retrieving creators data for an item", () => { + const sample_item = items.find(it => it.data.creators.length > 0)!; + expect(extension.getItemCreators(sample_item, {})) + .toEqual(getItemCreators(sample_item, {})); + }); + + test("Retrieving tags data for an item", () => { + const sample_item = items.find(it => it.data.tags.length > 0)!; + expect(extension.getItemTags(sample_item, {})) + .toEqual(getItemTags(sample_item, {})); + }); + + describe("Retrieving bibliography for an item", () => { + // Necessary since jsdom does not support innerText + // It shouldn't give discrepant results here + // https://github.com/jsdom/jsdom/issues/1245#issuecomment-763535573 + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, "innerText", { + get() { + return this.textContent; + } + }); + }); + + test("It should return the clean bibliography", async() => { + const sample_bib = bibs.itemInLibrary; + const { library: { type, id } } = sample_bib; + const path = type + "s/" + id; + const itemKey = sample_bib.key; + + const sample_item = findItems({ type: `${type}s`, id, since: 0 }).find(it => it.data.key == itemKey)!; + + const res = await extension.getItemCitation(sample_item, {}); + + const { bib } = findBibliographyEntry({ key: itemKey, path }); + + expect(res) + .toEqual(cleanBibliographyHTML(bib)); + }); + + }); + + test("Retrieving bibliographic entries for a list of citekeys", async() => { + // getItems() retrieves queries data with an inclusive query, + // so no need to reproduce the exact query key that would exist in prod + client.setQueryData( + ["items"], + (_prev) => ({ + data: items, + lastUpdated: 9999 + }) + ); + + const citekeys = Object.keys(entries); + const expected = Object.values(entries).map(entry => entry.biblatex).join("\n"); + + const bibEntries = await extension.getBibEntries(citekeys); + + expect(bibEntries) + .toEqual(expected); + }); + +}); + +describe("Database connectivity", () => { + const client = new QueryClient(); + const date = new Date(2021, 4, 6); + const extensionRequests = mock({ libraries: [] }); + + let extension: ZoteroRoam; + let idbInstance: IDBDatabaseService; + + beforeEach(() => { + idbInstance = new IDBDatabaseService(); + extension = new ZoteroRoam({ + idbDatabase: idbInstance, + queryClient: client, + requests: extensionRequests, + settings: initSettings + }); + + vi.useFakeTimers() + .setSystemTime(date); + }); + + afterEach(async () => { + /* eslint-disable-next-line dot-notation */ + await waitFor(() => indexedDB.deleteDatabase(idbInstance["dbName"]), { timeout: 5000 }); + vi.useRealTimers(); + }); + + it("can manage the cache", async () => { + const infoLogger = vi.spyOn(extension, "info"); + + // Simulate behavior of the React Query provider + persistQueryClientSave({ + queryClient: client, + persister: createPersisterWithIDB(idbInstance) + }); + + let clientIsCached = await extension.isDataCached(); + expect(clientIsCached).toBe(true); + + const lastCachedTimestamp = await extension.getDataCacheUpdatedAt(); + expect(lastCachedTimestamp).toBe(date.valueOf()); + + await extension.clearDataCache(); + + expect(infoLogger).toHaveBeenCalledTimes(1); + expect(infoLogger).toHaveBeenCalledWith({ + origin: "Database", + message: "Successfully cleared data from cache", + showToaster: 1000 + }); + + clientIsCached = await extension.isDataCached(); + expect(clientIsCached).toBe(false); + + }); + + it("can delete the database", async () => { + await extension.deleteDatabase(); + + await waitFor(async () => { + const databasesList = await indexedDB.databases(); + expect(databasesList).toEqual([]); + }, { timeout: 500 }); + }); + +}); + +describe("isDataCached", () => { + const extensionRequests = mock({ libraries: [] }); + + it("returns false when no data has been persisted yet", async() => { + const idbInstance = new IDBDatabaseService(); + const client = new QueryClient(); + const extension = new ZoteroRoam({ + idbDatabase: idbInstance, + queryClient: client, + requests: extensionRequests, + settings: initSettings + }); + + const clientIsCached = await extension.isDataCached(); + expect(clientIsCached).toBe(false); + + /* eslint-disable-next-line dot-notation */ + await waitFor(() => indexedDB.deleteDatabase(idbInstance["dbName"]), { timeout: 5000 }); + }); + + it("returns true when data has been persisted", async () => { + const idbInstance = new IDBDatabaseService(); + const client = new QueryClient(); + const extension = new ZoteroRoam({ + idbDatabase: idbInstance, + queryClient: client, + requests: extensionRequests, + settings: initSettings + }); + + // Simulate behavior of the React Query provider + persistQueryClientSave({ + queryClient: client, + persister: createPersisterWithIDB(idbInstance) + }); + + const clientIsCached = await extension.isDataCached(); + expect(clientIsCached).toBe(true); + + /* eslint-disable-next-line dot-notation */ + await waitFor(() => indexedDB.deleteDatabase(idbInstance["dbName"]), { timeout: 5000 }); + }); + + it("returns false when there is no database", async() => { + const client = new QueryClient(); + const extension = new ZoteroRoam({ + queryClient: client, + requests: extensionRequests, + settings: initSettings + }); + + const clientIsCached = await extension.isDataCached(); + expect(clientIsCached).toBe(false); + }); + +}); + +describe("Error logging when the database no longer exists", () => { + const extensionRequests = mock({ libraries: [] }); + + let client: QueryClient; + let extension: ZoteroRoam; + let idbInstance: IDBDatabaseService; + + beforeEach(async () => { + client = new QueryClient(); + idbInstance = new IDBDatabaseService(); + extension = new ZoteroRoam({ + idbDatabase: idbInstance, + queryClient: client, + requests: extensionRequests, + settings: initSettings + }); + + /* eslint-disable-next-line dot-notation */ + await waitFor(async () => { + await idbInstance.close(); + await extension.deleteDatabase(); + }, { timeout: 500 }); + }); + + + it("handles attempting to clear the React Query store", async () => { + const errorLogger = vi.spyOn(extension, "error"); + + await extension.clearDataCache(); + + expect(errorLogger).toHaveBeenCalledTimes(1); + expect(errorLogger).toHaveBeenCalledWith( + expect.objectContaining({ + origin: "Database", + message: "Failed to clear data from cache" + }) + ); + + }); + + + it("handles attempting to check if there is cached data", async () => { + const errorLogger = vi.spyOn(extension, "error"); + + const res = await extension.isDataCached(); + + expect(res).toBe(false); + expect(errorLogger).toHaveBeenCalledTimes(1); + expect(errorLogger).toHaveBeenCalledWith( + expect.objectContaining({ + origin: "Database", + message: "Failed to obtain caching status" + }) + ); + + }); + + + it("handles attempting to fetch the timestamp for the last caching operation", async () => { + const errorLogger = vi.spyOn(extension, "error"); + + await extension.getDataCacheUpdatedAt(); + + expect(errorLogger).toHaveBeenCalledTimes(1); + expect(errorLogger).toHaveBeenCalledWith( + expect.objectContaining({ + origin: "Database", + message: "Failed to retrieve cache age" + }) + ); + }); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/api/index.tsx b/prototypes/zotero-roam-mcp/src/api/index.tsx new file mode 100644 index 0000000..36a3f7e --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/api/index.tsx @@ -0,0 +1,248 @@ +import { QueryClient } from "@tanstack/query-core"; + +import { ZoteroAPI, fetchBibEntries, fetchBibliography } from "@clients/zotero"; +import IDBDatabase from "@services/idb"; +import { SelectItemCollectionsOptions, SelectItemRelatedOptions, SelectItemsOption, selectCollections, selectItemChildren, selectItemCollections, selectItemRelated, selectItems, selectTags } from "@services/react-query"; + +import { cleanBibliographyHTML, formatItemMetadata, formatNotes, formatPDFs, getItemCreators, getItemDateAdded, getItemLink, getItemPublication, getItemTags, getItemType, groupCitekeysByLibrary } from "./helpers"; +import { Logger } from "./logging"; + +import { cleanError } from "../utils"; + +import { IDB_REACT_QUERY_CLIENT_KEY, IDB_REACT_QUERY_STORE_NAME } from "../constants"; +import { ZItemAnnotation, ZItemAttachment, ZItemNote, ZItemTop } from "Types/transforms"; +import { UserRequests, UserSettings } from "Types/extension"; + + +type ZoteroRoamConstructorArgs = { + idbDatabase?: IDBDatabase | null, + queryClient: QueryClient, + requests: UserRequests, + settings: UserSettings +}; + +/** + * Creates a new public API instance for the extension. This is meant to make available an interface for users as well as other plugins to consume some of the extension's data and functionalities, in a controlled manner. Updates to settings are done by the relevant widgets. + */ +export default class ZoteroRoam extends Logger { + #db: IDBDatabase | null; + #libraries: UserRequests["libraries"]; + #queryClient: QueryClient; + #settings: Pick; + + constructor({ idbDatabase = null, queryClient, requests, settings }: ZoteroRoamConstructorArgs) { + super() + + const { libraries } = requests; + const { annotations, notes, typemap } = settings; + + this.#db = idbDatabase; + this.#libraries = libraries; + this.#queryClient = queryClient; + this.#settings = { annotations, notes, typemap }; + + } + + /** Clears the contents of the React Query store from the database. */ + async clearDataCache(): Promise{ + if(this.#db !== null){ + try { + const reactQueryStore = await this.#db.selectStore(IDB_REACT_QUERY_STORE_NAME); + await reactQueryStore.clear(); + this.info({ + origin: "Database", + message: "Successfully cleared data from cache", + showToaster: 1000 + }); + } catch(e){ + this.error({ + origin: "Database", + message: "Failed to clear data from cache", + context: { + error: cleanError(e) + } + }); + } + } + } + + /** Deletes the database, if any */ + async deleteDatabase(): Promise{ + if(this.#db){ + await this.#db.deleteSelf(); + } + } + + /** Checks if there is a cached version of the React Query client */ + async isDataCached(): Promise{ + if(this.#db !== null){ + try { + const reactQueryStore = await this.#db.selectStore(IDB_REACT_QUERY_STORE_NAME); + const cachedClient = await reactQueryStore.get(IDB_REACT_QUERY_CLIENT_KEY); + return cachedClient !== undefined; + } catch(e) { + this.error({ + origin: "Database", + message: "Failed to obtain caching status", + context: { + error: cleanError(e) + } + }); + + return false; + } + } + + return false; + } + + /** Retrieves the timestamp when the React Query client was last persisted to cache. */ + async getDataCacheUpdatedAt(){ + if(this.#db){ + try { + const reactQueryStore = await this.#db.selectStore(IDB_REACT_QUERY_STORE_NAME); + const { timestamp } = await reactQueryStore.get(IDB_REACT_QUERY_CLIENT_KEY) || {}; + return timestamp; + } catch(e){ + this.error({ + origin: "Database", + message: "Failed to retrieve cache age", + context: { + error: cleanError(e) + } + }); + } + } + return null; + } + + // To be called in the RequestsWidget + /* istanbul ignore next */ + updateLibraries(val: UserRequests["libraries"]) { + this.#libraries = val; + } + + /* istanbul ignore next */ + updateSetting(op: T, val: UserSettings[T]) { + this.#settings[op] = val; + } + + formatPDFs = formatPDFs; + getItemCreators = getItemCreators; + getItemDateAdded = getItemDateAdded; + getItemLink = getItemLink; + getItemPublication = getItemPublication; + getItemTags = getItemTags; + + /** Formats Zotero notes and annotations, with current user settings */ + formatNotes(notes: (ZItemNote | ZItemAnnotation)[]) { + return formatNotes(notes, null, { + annotationsSettings: this.#settings.annotations, + notesSettings: this.#settings.notes + }); + } + + /** Retrieves the bibliographic entries for a list of items */ + async getBibEntries(citekeys: string[]) { + const items = selectItems("items", {}, { + queryClient: this.#queryClient + }); + + const groupedList = groupCitekeysByLibrary(citekeys, { items }) + const bibEntries: Promise[] = []; + Object.keys(groupedList).forEach(libPath => { + const library = this.#libraries.find(lib => lib.path == libPath); + if (library) { + bibEntries.push(fetchBibEntries(groupedList[libPath], library)); + } + }); + + const bibOutput = await Promise.all(bibEntries); + return bibOutput.join("\n"); + } + + /** Retrieves the formatted bibliography for a given item, with optional config */ + async getItemCitation(item: ZItemTop, config: Partial = {}) { + const location = item.library.type + "s/" + item.library.id; + const library = this.#libraries.find(lib => lib.path == location)!; + + const bib = await fetchBibliography(item.data.key, library, config); + return cleanBibliographyHTML(bib); + } + + /** Retrieves the list of collections for a given library */ + getCollections(path: string) { + const library = this.#libraries.find(lib => lib.path == path)!; + return selectCollections(library, { + queryClient: this.#queryClient + }); + } + + /** Retrieves the children for a given item */ + getItemChildren(item: ZItemTop) { + return selectItemChildren(item, { + queryClient: this.#queryClient + }); + } + + /** Retrieves the list of collections for a given item */ + getItemCollections( + item: ZItemTop, { return_as = "string", brackets = true }: Partial = {} + ) { + return selectItemCollections(item, { return_as, brackets }, { + libraries: this.#libraries, + queryClient: this.#queryClient + }); + } + + /* istanbul ignore next */ + /** Formats an item's metadata into Roam blocks */ + getItemMetadata(item: ZItemTop, pdfs: ZItemAttachment[], notes: (ZItemNote | ZItemAnnotation)[]) { + return formatItemMetadata(item, pdfs, notes, { + annotationsSettings: this.#settings.annotations, + notesSettings: this.#settings.notes, + typemap: this.#settings.typemap + }); + } + + /** Retrieves the in-library relations for a given item */ + getItemRelated( + item: ZItemTop, { return_as = "string", brackets = true }: Partial = {} + ) { + return selectItemRelated(item, { return_as, brackets }, { + queryClient: this.#queryClient + }) + } + + /** Retrieves an item's type */ + getItemType(item: ZItemTop, { brackets = true }: { brackets?: boolean } = {}) { + return getItemType(item, { brackets }, { + typemap: this.#settings.typemap + }); + } + + /** Retrieves all / a subset of all items currently available to the extension + * @example + * // Returns the full list of items currently loaded + * .getItems("all") + * // Returns all items currently loaded, except for annotations, notes, and attachments + * .getItems("items") + */ + getItems(select: SelectItemsOption = "all") { + return selectItems(select, {}, { + queryClient: this.#queryClient + }); + } + + /** Retrieves the map of tags for a given library + * @example + * // Returns the map of tags for the library of user ID 123456 + * .getTags("users/123456") + */ + getTags(path: string) { + return selectTags(path, { + libraries: this.#libraries, + queryClient: this.#queryClient + }); + } +} diff --git a/prototypes/zotero-roam-mcp/src/api/logging.test.tsx b/prototypes/zotero-roam-mcp/src/api/logging.test.tsx new file mode 100644 index 0000000..beb6299 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/api/logging.test.tsx @@ -0,0 +1,181 @@ +import { H5 } from "@blueprintjs/core"; + +import zrToaster from "Components/ExtensionToaster"; +import { Logger, ZoteroRoamLog } from "./logging"; + + +describe("ZoteroRoamLog", () => { + beforeAll(() => { + vi.useFakeTimers() + .setSystemTime(new Date(2022, 4, 6)); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + it("uses fallback values", () => { + const sample_log = new ZoteroRoamLog(); + + expect(sample_log) + .toEqual({ + context: {}, + detail: "", + intent: "primary", + level: "info", + message: "", + origin: "", + timestamp: new Date(2022, 4, 6) + }); + }); + + it("initializes with values provided", () => { + const sample_log = new ZoteroRoamLog( + { + origin: "API", + message: "Failed to fetch", + context: { + text: "some text" + } + }, + "error" + ); + + expect(sample_log) + .toEqual({ + context: { + text: "some text" + }, + detail: "", + intent: "danger", + level: "error", + message: "Failed to fetch", + origin: "API", + timestamp: new Date(2022, 4, 6) + }); + }); + + it("calls the toaster when showToaster is provided", () => { + const showToasterFn = vi.spyOn(zrToaster, "show"); + + const log_contents = { + origin: "Metadata", + message: "Failed to import metadata for @someCitekey", + context: { + text: "some text" + } + }; + + new ZoteroRoamLog({ ...log_contents }, "error"); + expect(showToasterFn).not.toHaveBeenCalled(); + + new ZoteroRoamLog({ ...log_contents, showToaster: 1500 }, "error"); + expect(showToasterFn).toHaveBeenCalled(); + expect(showToasterFn).toHaveBeenCalledWith({ + icon: "warning-sign", + intent: "danger", + message: log_contents.message, + timeout: 1500 + }); + + new ZoteroRoamLog({ ...log_contents, showToaster: true }, "warning"); + expect(showToasterFn).toHaveBeenCalledTimes(2); + expect(showToasterFn).toHaveBeenNthCalledWith(2, { + icon: "warning-sign", + intent: "warning", + message: log_contents.message, + timeout: 1000 + }); + }); + + it("creates the right message for the toaster", () => { + const showToasterFn = vi.spyOn(zrToaster, "show"); + + new ZoteroRoamLog({ + origin: "Metadata", + message: "Failed to import metadata for @someCitekey", + detail: "Function customFunc doesn't exist", + context: {}, + showToaster: 1000 + }, "error"); + + expect(showToasterFn).toHaveBeenCalled(); + expect(showToasterFn).toHaveBeenCalledWith({ + icon: "warning-sign", + intent: "danger", + message: ( + <> +
{"Failed to import metadata for @someCitekey"}
+

{"Function customFunc doesn't exist"}

+ + ), + timeout: 1000 + }); + }); +}); + +describe("Logger", () => { + let logger: Logger; + + beforeAll(() => { + vi.useFakeTimers() + .setSystemTime(new Date(2022, 4, 6)); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + beforeEach(() => { + logger = new Logger(); + }); + + const log_details = { + origin: "API", + message: "Some log", + detail: "", + context: { + text: "string" + } + }; + + it("logs errors", () => { + logger.error(log_details); + expect(logger.logs) + .toEqual([ + { + ...log_details, + intent: "danger", + level: "error", + timestamp: new Date(2022, 4, 6) + } + ]); + }); + + it("logs infos", () => { + logger.info(log_details); + expect(logger.logs) + .toEqual([ + { + ...log_details, + intent: "primary", + level: "info", + timestamp: new Date(2022, 4, 6) + } + ]); + }); + + it("logs warnings", () => { + logger.warn(log_details); + expect(logger.logs) + .toEqual([ + { + ...log_details, + intent: "warning", + level: "warning", + timestamp: new Date(2022, 4, 6) + } + ]); + }); + +}); diff --git a/prototypes/zotero-roam-mcp/src/api/logging.tsx b/prototypes/zotero-roam-mcp/src/api/logging.tsx new file mode 100644 index 0000000..ff8f145 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/api/logging.tsx @@ -0,0 +1,97 @@ +import { H5, IconName, Intent } from "@blueprintjs/core"; +import zrToaster from "Components/ExtensionToaster"; + + +type LogConfig = { + context?: Record, + detail?: string, + origin?: string, + message?: string, + showToaster?: number | boolean +}; + +type LogLevel = "error" | "info" | "warning"; + +/** + * Creates a log entry for the extension. This is meant to provide users with information about different events (e.g errors when fetching data), through an optional toast and more detailed logs. + */ +export class ZoteroRoamLog { + level: LogLevel; + origin: string; + message: string; + detail: string; + context: Record; + intent: Intent | undefined; + timestamp: Date; + + #LEVELS_MAPPING: Record = { + "error": "danger", + "info": "primary", + "warning": "warning" + }; + + #ICONS_MAPPING: Record = { + "error": "warning-sign", + "info": "info-sign", + "warning": "warning-sign" + }; + + constructor(obj: LogConfig = {}, level: LogLevel = "info") { + const { origin = "", message = "", detail = "", context = {}, showToaster = false } = obj; + this.level = level; + this.origin = origin; + this.message = message; + this.detail = detail; + this.context = context; + this.intent = this.#LEVELS_MAPPING[level] || undefined; + this.timestamp = new Date(); + + if (showToaster) { + zrToaster.show({ + icon: this.#ICONS_MAPPING[level] || null, + intent: this.intent, + message: ( + this.detail + ? <> +
{this.message}
+

{this.detail}

+ + : this.message + ), + timeout: showToaster.constructor === Number ? showToaster : 1000 + }); + } + } +} + +// Maximum number of log entries to retain (prevents unbounded memory growth) +const MAX_LOG_ENTRIES = 1000; + +export class Logger { + logs: ZoteroRoamLog[] = []; + + send(obj: LogConfig, level: LogLevel = "info") { + this.logs.push(new ZoteroRoamLog(obj, level)); + // Trim logs to prevent unbounded memory growth + if (this.logs.length > MAX_LOG_ENTRIES) { + this.logs = this.logs.slice(-MAX_LOG_ENTRIES); + } + } + + /** Clears all log entries */ + clearLogs() { + this.logs = []; + } + + error(obj: LogConfig) { + this.send(obj, "error"); + } + + info(obj: LogConfig) { + this.send(obj, "info"); + } + + warn(obj: LogConfig) { + this.send(obj, "warning"); + } +} diff --git a/prototypes/zotero-roam-mcp/src/clients/citoid/base.test.ts b/prototypes/zotero-roam-mcp/src/clients/citoid/base.test.ts new file mode 100644 index 0000000..6150e56 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/citoid/base.test.ts @@ -0,0 +1,48 @@ +import { fetchCitoid } from "./base"; + +import { citoids, Mocks } from "Mocks"; + + +describe("fetchCitoid", () => { + function isFailure(response: typeof citoids[keyof typeof citoids]): response is Mocks.Responses.CitoidError { + return (response as Mocks.Responses.CitoidError).status !== undefined; + } + + const { success_cases, error_cases } = Object.entries(citoids) + .reduce<{ success_cases: [string, Mocks.Responses.CitoidSuccess[number]][], error_cases: [string, Mocks.Responses.CitoidError][] }>((obj, entry) => { + const [identifier, res] = entry; + if (isFailure(res)) { + obj.error_cases.push([identifier, res]); + } else { + obj.success_cases.push([identifier, res]); + } + return obj; + }, { success_cases: [], error_cases: [] }); + + test.each(success_cases)( + "%# Successfully mocking Citoid data for %s", + async (identifier, itemData) => { + const { status, ...output } = itemData; + const citoid = await fetchCitoid(identifier); + expect(citoid).toEqual({ + item: output, + query: identifier + }); + } + ); + + test.each(error_cases)( + "%# Successfully mocking Citoid error for %s", + async (identifier, itemData) => { + const res = await fetchCitoid(identifier) + .catch((error) => { + if (error.response) { + return error.response; + } + }); + expect(res.status).toBe(itemData.status); + expect(res.data).toEqual([itemData]); + } + ); + +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/citoid/base.ts b/prototypes/zotero-roam-mcp/src/clients/citoid/base.ts new file mode 100644 index 0000000..04faa02 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/citoid/base.ts @@ -0,0 +1,44 @@ +import axios from "axios"; + +import { Queries } from "@services/react-query"; + +import { CitoidAPI } from "./types"; + +import { cleanError } from "../../utils"; + + +const citoidClient = axios.create({ + baseURL: "https://en.wikipedia.org/api/rest_v1/data/citation/zotero" +}); + + +/** Requests data from the `/data/citation/zotero` endpoint of the Wikipedia API + * @param url - The URL for which to request Zotero metadata + * @returns The metadata for the URL + */ +async function fetchCitoid(url: string): Promise { + let response: unknown; + try { + const { data, ...rest } = await citoidClient.get(encodeURIComponent(url)); + response = { data, ...rest }; + + return { + item: data[0], + query: url + }; + + } catch (error) { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch metadata from Wikipedia", + context: { + error: cleanError(error), + query: url, + response + } + }); + return Promise.reject(error); + } +} + +export { fetchCitoid }; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/citoid/hooks.test.ts b/prototypes/zotero-roam-mcp/src/clients/citoid/hooks.test.ts new file mode 100644 index 0000000..9e34d6e --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/citoid/hooks.test.ts @@ -0,0 +1,45 @@ +import { renderHook } from "@testing-library/react-hooks"; +import { wrapper } from "../query-test-wrapper"; +import { useCitoids } from "./hooks"; + +import { badIdentifier, goodIdentifier } from "Mocks/citoid"; + + +describe("useCitoids", () => { + const cases = [ + [ + "1 valid identifier", + [goodIdentifier], + {}, + ["success"] + ], + [ + "1 invalid identifier", + [badIdentifier], + {}, + ["error"] + ], + [ + "1 valid + 1 invalid", + [goodIdentifier, badIdentifier], + { retry: false }, + ["success", "error"] + ] + ]; + + test.each(cases)( + "%s", + async (_id, urls: string[], options: Record, expectation) => { + const { result, waitFor } = renderHook(() => useCitoids(urls, options), { wrapper }); + + await waitFor( + () => result.current.every(res => !res.isLoading), + { + timeout: 3000 + }); + + expect(result.current.map(res => res.status)) + .toEqual(expectation); + } + ); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/citoid/hooks.ts b/prototypes/zotero-roam-mcp/src/clients/citoid/hooks.ts new file mode 100644 index 0000000..15d0359 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/citoid/hooks.ts @@ -0,0 +1,45 @@ +import { UseQueryOptions, useQueries } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import { Queries } from "@services/react-query"; + +import { fetchCitoid } from "./base"; + + +/** React Query custom hook for retrieving Wikipedia metadata for a list of URLs. By default, `cacheTime = Infinity` and `staleTime = 10min`. + * There is no refetch scheduled, since the data should not change over the course of a session. + * Requests are retried only once (except for 404 errors, which should never be retried). + */ +const useCitoids = ( + /** The targeted URLs */ + urls: string[], + /** Optional configuration for queries */ + opts: Omit, "queryKey" | "queryFn"> = {} +) => { + // Defaults for this query + const { + cacheTime = Infinity, + retry = (failureCount: number, error: AxiosError) => { + // TODO: fix error JSON type + return (failureCount < 1 && (error.toJSON() as any).status != 404); + }, + staleTime = 1000 * 60 * 10, + ...rest } = opts; + // Factory + const queriesDefs = urls.map((url) => { + const queryKey: Queries.Key.Citoid = ["citoid", { url }]; + return { + queryKey: queryKey, + queryFn: (_queryKey) => fetchCitoid(url), + cacheTime, + retry, + staleTime, + ...rest + }; + }); + return useQueries({ + queries: queriesDefs + }); +}; + +export { useCitoids }; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/citoid/index.ts b/prototypes/zotero-roam-mcp/src/clients/citoid/index.ts new file mode 100644 index 0000000..8a4d1a6 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/citoid/index.ts @@ -0,0 +1,2 @@ +export * from "./hooks"; +export * from "./types"; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/citoid/types.ts b/prototypes/zotero-roam-mcp/src/clients/citoid/types.ts new file mode 100644 index 0000000..2ef57a8 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/citoid/types.ts @@ -0,0 +1,9 @@ +import { ZoteroAPI } from "../zotero"; + + +export namespace CitoidAPI { + export type AsZotero = Omit & { + itemKey?: string, + key?: string + }; +} diff --git a/prototypes/zotero-roam-mcp/src/clients/index.test.ts b/prototypes/zotero-roam-mcp/src/clients/index.test.ts new file mode 100644 index 0000000..d5aa600 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/index.test.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + + +describe("Mock fallback", () => { + it("is called when no matching handler exists", async () => { + const res = await axios.get("https://example.com/") + .catch((error) => { + if (error.response) { + return error.response; + } + }); + expect(res.status).toBe(404); + expect(res.statusText).toBe("You need to add a handler for https://example.com/"); + }); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/query-test-wrapper.tsx b/prototypes/zotero-roam-mcp/src/clients/query-test-wrapper.tsx new file mode 100644 index 0000000..930b438 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/query-test-wrapper.tsx @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +import { ReactChildren } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { WrapperComponent } from "@testing-library/react-hooks"; + + +// https://tkdodo.eu/blog/testing-react-query +export const wrapper: WrapperComponent<{ children: ReactChildren }> = ({ children }) => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Set global defaults here + } + }, + logger: { + log: console.log, + warn: console.warn, + error: () => { } + } + }); + return ( + { children } + ); +}; diff --git a/prototypes/zotero-roam-mcp/src/clients/semantic/base.test.ts b/prototypes/zotero-roam-mcp/src/clients/semantic/base.test.ts new file mode 100644 index 0000000..389404a --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/semantic/base.test.ts @@ -0,0 +1,22 @@ +import { fetchSemantic } from "./base"; +import { transformDOIs } from "../../utils"; +import { semantics } from "Mocks/semantic-scholar"; + + +describe("Fetching mocked Semantic data", () => { + const cases = Object.entries(semantics); + test.each(cases)( + "%# Successfully mocking Semantic data for %s", + async (doi, semanticData) => { + const { citations, references } = semanticData; + + const res = await fetchSemantic(doi); + + expect(res).toEqual({ + doi, + citations: transformDOIs(citations), + references: transformDOIs(references) + }); + } + ); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/semantic/base.ts b/prototypes/zotero-roam-mcp/src/clients/semantic/base.ts new file mode 100644 index 0000000..8d1ed75 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/semantic/base.ts @@ -0,0 +1,53 @@ +import axios from "axios"; +import axiosRetry from "axios-retry"; + +import { Queries } from "@services/react-query"; + +import { SemanticScholarAPI } from "./types"; + +import { cleanError, transformDOIs } from "../../utils"; + + +const semanticClient = axios.create({ + baseURL: "https://api.semanticscholar.org/v1/paper/", + params: { + "include_unknown_references": "true" + } +}); +axiosRetry(semanticClient, { + retries: 3 +}); + + +/** Requests data from the `/paper` endpoint of the Semantic Scholar API + * @param doi - The DOI of the targeted item, assumed to have already been checked and parsed. + * @returns Citation data for the item +**/ +async function fetchSemantic(doi: string): Promise { + let response: unknown; + + try { + const apiResponse = await semanticClient.get(`${doi}`); + const { data: { citations, references } } = apiResponse; + response = apiResponse; + + return { + doi, + citations: transformDOIs(citations), + references: transformDOIs(references) + }; + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch data from SemanticScholar", + context: { + error: cleanError(error), + response + } + }); + return Promise.reject(error); + } +} + + +export { fetchSemantic }; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/semantic/hooks.test.ts b/prototypes/zotero-roam-mcp/src/clients/semantic/hooks.test.ts new file mode 100644 index 0000000..8ff011a --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/semantic/hooks.test.ts @@ -0,0 +1,28 @@ +import { renderHook } from "@testing-library/react-hooks"; +import { wrapper } from "../query-test-wrapper"; + +import { useSemantic } from "./hooks"; +import { transformDOIs } from "../../utils"; + +import { semantics } from "Mocks/semantic-scholar"; + + +describe("useSemantic", () => { + const cases = Object.entries(semantics); + + test.each(cases)( + "returns fetch results - %s", + async (doi, expectation) => { + const { result, waitFor } = renderHook(() => useSemantic(doi, {}), { wrapper }); + + await waitFor(() => !result.current.isLoading); + + expect(result.current.data) + .toEqual({ + doi, + citations: transformDOIs(expectation.citations), + references: transformDOIs(expectation.references) + }); + } + ); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/semantic/hooks.ts b/prototypes/zotero-roam-mcp/src/clients/semantic/hooks.ts new file mode 100644 index 0000000..d31b56b --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/semantic/hooks.ts @@ -0,0 +1,33 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { Queries } from "@services/react-query"; + +import { fetchSemantic } from "./base"; + + +/** Use Semantic Scholar citation data for a DOI. By default, `cacheTime = Infinity`. + * There is no refetch scheduled, since the data should not change over the course of a session. + * @param doi - The targeted DOI + * @param opts - Optional configuration to use with the queries + * @returns The React Query for the given DOI's Semantic Scholar data + */ +const useSemantic = ( + /** The targeted DOI */ + doi: string, + /** Optional configuration to use with the queries */ + opts: Omit, "queryKey" | "queryFn"> = {} +) => { + // Defaults for this query + const { cacheTime = Infinity, ...rest } = opts; + // Factory + const queryKey: Queries.Key.Semantic = ["semantic", { doi }]; + return useQuery({ + queryKey, + queryFn: (_queryKey) => fetchSemantic(doi), + cacheTime, + ...rest + }); +}; + + +export { useSemantic }; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/semantic/index.ts b/prototypes/zotero-roam-mcp/src/clients/semantic/index.ts new file mode 100644 index 0000000..8a4d1a6 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/semantic/index.ts @@ -0,0 +1,2 @@ +export * from "./hooks"; +export * from "./types"; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/semantic/types.ts b/prototypes/zotero-roam-mcp/src/clients/semantic/types.ts new file mode 100644 index 0000000..58422bc --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/semantic/types.ts @@ -0,0 +1,36 @@ +export namespace SemanticScholarAPI { + interface BasePaper { + arxivId: string | null, + authors: { authorId: string, name: string, url?: string }[], + doi: string | false, + paperId: string, + title: string, + url: string, + venue: string, + year: number + } + + export interface Item extends BasePaper { + abstract: string, + citationVelocity: number, + citations: RelatedPaper[], + corpusId: number | null, + /** @example ["Medicine"] */ + fieldsOfStudy: string[], + influentialCitationCount: number, + isOpenAccess: boolean, + isPublisherLicensed: boolean, + is_open_access: boolean, + is_publisher_licensed: boolean, + numCitedBy: number, + numCiting: number, + references: RelatedPaper[], + s2FieldsOfStudy: { category: string, source: string }[], + topics: [] + } + + export interface RelatedPaper extends BasePaper { + intent: string[], + isInfluential: boolean, + } +} diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/base.test.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/base.test.ts new file mode 100644 index 0000000..9eeda10 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/base.test.ts @@ -0,0 +1,278 @@ +import { QueryClient } from "@tanstack/react-query"; + +import * as base from "./base"; +import { makeTagList } from "./helpers"; +import { ZoteroAPI } from "./types"; + +import { apiKeys, bibs, deletions, findBibliographyEntry, findBibEntry, findCollections, findItems, libraries, tags } from "Mocks"; + +import { isFulfilled } from "Types/helpers"; + + +const { deleteTags, fetchAdditionalData, fetchBibEntries, fetchBibliography, fetchCollections, fetchDeleted, fetchItems, fetchPermissions, fetchTags, writeItems } = base; + +const { keyWithFullAccess: { key: masterKey } } = apiKeys; + +const getLibraryPath = (library) => { + return library.type + "s/" + library.id; +}; + + +describe("Fetching mocked API Key permissions", () => { + const cases = Object.entries(apiKeys); + test.each(cases)( + "%# Fetching permissions for %s", + async (_keyName, expectation) => { + const permissions = await fetchPermissions(expectation.key); + expect(permissions).toEqual(expectation); + } + ); +}); + +describe("Fetching mocked bibliography", () => { + const cases = Object.entries(bibs); + test.each(cases)( + "%# Fetching bibliography as bib for %s", + async (_bibName, entry) => { + const path = getLibraryPath(entry.library); + + const bibliography = await fetchBibliography(entry.key, { apikey: masterKey, path }, {}); + const { bib } = findBibliographyEntry({ key: entry.key, path }); + + expect(bibliography).toEqual(bib); + } + ); +}); + +describe("Fetching mocked bibliography entries", () => { + const cases = Object.entries(libraries); + + test.each(cases)( + "%# Fetching a bibliography entry from %s", + async (_libName, libraryDetails) => { + const { type, id, path } = libraryDetails; + const sample_item = findItems({ type, id, since: 0 })[0]; + + const res = await fetchBibEntries([sample_item.data.key], { apikey: masterKey, path }); + + const sample_bib = findBibEntry({ type, id, key: sample_item.data.key })!; + + expect(res).toBe(sample_bib.biblatex); + } + ); +}); + +describe("Fetching mocked collections", () => { + const cases = Object.entries(libraries); + + test.each(cases)( + "%# There should be no items older than latest in %s", + (_libName, libraryDetails) => { + const { type, id, version } = libraryDetails; + expect(findCollections(type, id, version)).toEqual([]); + } + ); + + test.each(cases)( + "%# Fetching collections for %s", + async (_libName, libraryDetails) => { + const { id, path, type, version } = libraryDetails; + const libraryObj = { + apikey: masterKey, + path + }; + + const allCollections = findCollections(type, id, 0); + + const sinceEver = await fetchCollections( + libraryObj, + 0, + { match: [] } + ); + expect(sinceEver).toEqual({ + data: allCollections, + lastUpdated: version + }); + + const sinceLatest = await fetchCollections( + libraryObj, + version, + { match: sinceEver.data } + ); + expect(sinceLatest).toEqual({ + data: allCollections, + lastUpdated: version + }); + + // To cover fetchAdditionalData + const mockAdditional = await fetchAdditionalData( + { apikey: masterKey, dataURI: `${path}/collections`, since: 0 }, + allCollections.length + 100 + ); + expect(mockAdditional).toEqual(allCollections); + } + ); +}); + +describe("Fetching mocked deleted entities", () => { + const cases = Object.entries(libraries); + + test.each(cases)( + "%# Fetching entities deleted from %s", + async (_libName, libraryDetails) => { + const { path } = libraryDetails; + const deleted = await fetchDeleted({ apikey: masterKey, path }, 0); + expect(deleted).toEqual({ + collections: deletions[path].collections.map(del => del.key), + items: deletions[path].items.map(del => del.key), + searches: [], + tags: [] + }); + } + ); + + test.each(cases)( + "%# Checking that no entities are versioned over latest in %s", + async (_libName, libraryDetails) => { + const { path, version } = libraryDetails; + const deleted = await fetchDeleted({ apikey: masterKey, path }, version); + expect(deleted).toEqual({ + collections: [], + items: [], + searches: [], + tags: [] + }); + } + ); +}); + +describe("Fetching mocked items", () => { + const cases = Object.entries(libraries); + const queryClient = new QueryClient(); + + test.each(cases)( + "%# Fetching items for %s", + async (_libName, libraryDetails) => { + const { type, id, path, version } = libraryDetails; + + const itemData = findItems({ type, id, since: 0 }); + + const library = { + id: `${id}`, + path, + type, + uri: `${path}/items` + }; + + const sinceEver = await fetchItems( + { apikey: masterKey, dataURI: `${path}/items`, library, name: "", since: 0 }, + { match: [] }, + queryClient + ); + expect(sinceEver).toEqual({ + data: itemData, + lastUpdated: version + }); + + const sinceLatest = await fetchItems( + { apikey: masterKey, dataURI: `${path}/items`, library, name: "", since: version }, + { match: itemData }, + queryClient + ); + expect(sinceLatest).toEqual({ + data: itemData, + lastUpdated: version + }); + } + ); +}); + +describe("writeItems", () => { + const cases = Object.entries(libraries); + + test.each(cases)( + "%# Updating an item from %s", + async (_libName, libraryDetails) => { + const { type, id, path } = libraryDetails; + const sample_item = findItems({ type, id, since: 0 })[0]; + + const res = await writeItems>( + [{ key: sample_item.data.key, version: sample_item.version, tags: [{ tag: "TEST_TAG", type: 0 }] }], + { apikey: masterKey, path }); + + const data = res.filter(isFulfilled).map(rq => rq.value.data); + + expect(data).toEqual([{ + failed: {}, + unchanged: {}, + success: { + 0: sample_item.data.key + }, + successful: { + 0: { + ...sample_item, + data: { + ...sample_item.data, + tags: [{ tag: "TEST_TAG", type: 0 }] + } + } + } + }]); + + const resWithFailure = await writeItems>( + [{ key: sample_item.data.key, version: sample_item.version - 1, tags: [{ tag: "TEST_TAG", type: 0 }] }], + { apikey: masterKey, path }); + + const dataWithFailure = resWithFailure.filter(isFulfilled).map(rq => rq.value.data); + + expect(dataWithFailure).toEqual([{ + failed: { + 0: sample_item.data.key + }, + unchanged: { + 0: sample_item.data.key + }, + success: {}, + successful: {} + }]); + } + ); +}); + +describe("fetchTags", () => { + const cases = Object.entries(libraries); + + test.each(cases)( + "%# Fetching tags for %s", + async (_libName, libraryDetails) => { + const { path, version } = libraryDetails; + const tagData = await fetchTags({ apikey: masterKey, path }); + expect(tagData).toEqual({ + data: makeTagList(tags[path]), + lastUpdated: version + }); + } + ); +}); + +describe("deleteTags", () => { + const cases = Object.entries(libraries); + + test.each(cases)( + "%# Deleting tags in %s", + async (_libName, libraryDetails) => { + const { path, version } = libraryDetails; + + const deleteExpired = await deleteTags(["systems"], { apikey: masterKey, path }, version - 10) + .catch((error) => { + if (error.response) { + return error.response; + } + }); + expect(deleteExpired.status).toBe(412); + + const deleteLatest = await deleteTags(["systems"], { apikey: masterKey, path }, version); + expect(deleteLatest.status).toBe(204); + } + ); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/base.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/base.ts new file mode 100644 index 0000000..79a061b --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/base.ts @@ -0,0 +1,518 @@ +import { QueryClient } from "@tanstack/query-core"; +import axios, { AxiosResponse } from "axios"; +import axiosRetry from "axios-retry"; + +import { emitCustomEvent } from "@services/events"; +import { Queries } from "@services/react-query"; + +import { makeTagList, matchWithCurrentData } from "./helpers"; +import { ZoteroAPI } from "./types"; + +import { cleanError } from "../../utils"; + +import { DataRequest } from "Types/extension"; +import { Maybe } from "Types/helpers"; +import { ZItem, ZLibrary } from "Types/transforms"; + + +const zoteroClient = axios.create({ + baseURL: "https://api.zotero.org/", + headers: { + "Zotero-API-Version": 3 + } +}); + +axiosRetry(zoteroClient, { + retries: 2, + retryCondition: (error) => { + /* istanbul ignore else */ + if (error.response) { + const { status } = error.response; + if (status == 429 || status >= 500) { + return true; + } else { + return false; + } + } else { + return true; + } + }, + retryDelay: (retryCount, error) => { + /* istanbul ignore else */ + if (error.response) { + const { headers } = error.response; + return (Number(headers.backoff) || Number(headers["retry-after"]) || retryCount) * 1000; + } else { + return retryCount * 3000; + } + } +}); + + +/** Deletes Zotero tags through the `/[library]/tags` endpoint of the Zotero API + * @param tags - The names of the tags to be deleted + * @param library - The targeted Zotero library + * @param version - The last known version of the Zotero library + * @returns The outcome of the Axios API call + */ +async function deleteTags(tags: string[], library: ZLibrary, version: number) { + const { apikey, path } = library; + // * Only 50 tags can be deleted at once + // * Since each deletion is version-dependent, the extension won't support deleting more for now + // https://www.zotero.org/support/dev/web_api/v3/write_requests#deleting_multiple_tags + /* istanbul ignore if */ + if (tags.length > 50) { + window.zoteroRoam?.warn?.({ + origin: "API", + message: "API limits exceeded", + detail: "Only 50 Zotero tags can be deleted at once. Any additional tags selected will be ignored." + }); + } + + const tagList = tags.slice(0, 50).map(t => encodeURIComponent(t)).join(" || "); + + return await zoteroClient.delete( + `${path}/tags`, + { + headers: { + "Zotero-API-Key": apikey, + "If-Unmodified-Since-Version": version + }, + params: { + tag: tagList + } + } + ); +} + + +/** Retrieves additional data from the Zotero API, when the original results are greater than the limit of n = 100. + * A minimum of parameters are required so that the function can be used for all data types. + * @param req - The parameters of the request + * @param totalResults - The total number of results indicated by the original response + * @returns The additional results to the original request + */ +async function fetchAdditionalData( + req: { dataURI: string, apikey: string, since?: number }, + totalResults: number +) { + const { dataURI, apikey, since = null } = req; + const nbExtraCalls = Math.ceil((totalResults / 100) - 1); + + const apiCalls: Promise>[] = []; + + for (let i = 1; i <= nbExtraCalls; i++) { + const reqParams = new URLSearchParams(""); + if (since) { + reqParams.set("since", `${since}`); + } + reqParams.set("start", `${100 * i}`); + reqParams.set("limit", `${100}`); + apiCalls.push(zoteroClient.get( + `${dataURI}?${reqParams.toString()}`, + { + headers: { "Zotero-API-Key": apikey } + }) + ); + } + + let responses: unknown[] = [null]; + + try { + const apiResponses = await Promise.all(apiCalls); + responses = apiResponses; + + return apiResponses.map(res => res.data).flat(1); + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch additional data", + context: { + dataURI, + error: cleanError(error), + responses, + totalResults + } + }); + return Promise.reject(error); + } +} + + +/** Retrieves the bibliography for a list of Zotero items. */ +async function fetchBibEntries( + /* The Zotero keys of the targeted items */ + itemKeys: string[], + /* The targeted library */ + library: ZLibrary +): Promise { + const { apikey, path } = library; + + // * Only 100 entries can be retrieved at once + const apiCalls: Promise>>[] = []; + const nbCalls = Math.ceil(itemKeys.length / 100); + for (let i = 1; i <= nbCalls; i++) { + const keyList = itemKeys.slice(100 * (i - 1), 100 * i); + apiCalls.push(zoteroClient.get>(`${path}/items`, { + headers: { + "Zotero-API-Key": apikey + }, + params: { + include: "biblatex", + itemKey: keyList.join(",") + } + })); + } + const bibResults = await Promise.all(apiCalls); + return bibResults + .map(res => res.data) + .flat(1) + .map(entry => entry.biblatex) + .join("\n"); +} + + +/** Retrieves an item's formatted bibliographic entry as returned by the Zotero API + * @param itemKey - The item's Zotero key + * @param library - The item's Zotero library + * @param config - Optional parameters to use in the API call + * @returns + */ +async function fetchBibliography(itemKey: string, library: ZLibrary, config: Partial = {}) { + const { apikey, path } = library; + const dataURI = `${path}/items/${itemKey}`; + // See https://www.zotero.org/support/dev/web_api/v3/basics#parameters_for_format_bib_includecontent_bib_includecontent_citation + const { linkwrap = 0, locale = "en-US", style = "chicago-note-bibliography" } = config; + let response: unknown; + try { + const { data, ...rest } = await zoteroClient.get>( + dataURI, + { + headers: { "Zotero-API-Key": apikey }, + params: { + include: "bib", + linkwrap, + locale, + style + } + } + ); + response = { data, ...rest }; + return data.bib; + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch bibliography", + context: { + config, + dataURI, + error: cleanError(error), + response + } + }); + return Promise.reject(error); + } +} + + +/** Requests data from the `/[library]/collections` endpoint of the Zotero API + * @fires zotero-roam:update + * @returns Collections created or modified in Zotero since the specified version + */ +async function fetchCollections( + library: ZLibrary, + since = 0, + { match = [] }: { match?: ZoteroAPI.Collection[] } +): Promise { + const { apikey, path } = library; + + const defaultOutcome = { + data: null, + error: null, + library: path, + since, + success: false + }; + + let response: unknown; + let modified: Maybe; + let deleted: Maybe; + + try { + const { data, headers, ...rest } = await zoteroClient.get( + `${path}/collections`, + { + headers: { "Zotero-API-Key": apikey }, + params: { since } + }); + response = { data, headers, ...rest }; + modified = data; + + const { "last-modified-version": lastUpdated, "total-results": totalResultsStr } = headers; + const totalResults = Number(totalResultsStr); + + if (totalResults > 100) { + const additional = await fetchAdditionalData({ dataURI: `${path}/collections`, apikey, since }, totalResults); + modified.push(...additional); + } + + // DO NOT request deleted items since X if since = 0 (aka, initial data request) + // It's a waste of a call + if (since > 0 && modified.length > 0) { + const { collections = [] } = await fetchDeleted(library, since); + deleted = collections; + + emitCustomEvent({ + ...defaultOutcome, + data: modified, + success: true, + type: "collections", + _type: "update" + }); + } + + return { + data: matchWithCurrentData({ modified, deleted }, match), + lastUpdated: Number(lastUpdated) + }; + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch collections", + context: { + data: modified, + deleted, + error: cleanError(error), + response + } + }); + emitCustomEvent({ + ...defaultOutcome, + error, + success: false, + type: "collections", + _type: "update" + }); + return Promise.reject(error); + } +} + + +/** Requests data from the `/[library]/deleted` endpoint of the Zotero API + * @param library - The targeted Zotero library + * @param since - A library version + * @returns Elements deleted from Zotero since the specified version + */ +async function fetchDeleted(library: ZLibrary, since: number) { + const { apikey, path } = library; + + let response: unknown; + + try { + const { data } = await zoteroClient.get( + `${path}/deleted`, + { + headers: { "Zotero-API-Key": apikey }, + params: { since } + } + ); + response = data; + + return data; + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch deleted data", + context: { + error: cleanError(error), + response + } + }); + return Promise.reject(error); + } +} + + +/** Requests data from the Zotero API, based on a specific data URI + * @fires zotero-roam:update + * @param req - The parameters of the request + * @param config - Additional parameters + * @param queryClient - The current React Query client + * @returns + */ +async function fetchItems( + req: DataRequest & { since?: number }, + { match = [] }: { match: ZItem[] }, + queryClient: QueryClient +): Promise { + const { apikey, dataURI, library: { path }, since = 0 } = req; + const paramsQuery = new URLSearchParams(""); + paramsQuery.set("since", `${since}`); + paramsQuery.set("start", "0"); + paramsQuery.set("limit", "100"); + const defaultOutcome = { + data: null, + error: null, + library: path, + since, + success: false + }; + let response: unknown; + let modified: Maybe; + let deleted: Maybe; + try { + const { data, headers, ...rest } = await zoteroClient.get(`${dataURI}?${paramsQuery.toString()}`, + { + headers: { "Zotero-API-Key": apikey } + }); + response = { data, headers, ...rest }; + modified = data; + const { "last-modified-version": lastUpdated, "total-results": totalResultsStr } = headers; + const totalResults = Number(totalResultsStr); + if (totalResults > 100) { + const additional = await fetchAdditionalData({ dataURI, apikey, since }, totalResults); + modified.push(...additional); + } + // DO NOT request deleted items since X if since = 0 (aka, initial data request) + // It's a waste of a call + if (since > 0) { + // Retrieve deleted items, if any + const { items = [] } = await fetchDeleted({ apikey, path }, since); + deleted = items; + const tagsQueryKey: Queries.Key.Tags = ["tags", { library: path }]; + const { lastUpdated: latest_tags_version } = queryClient.getQueryData(tagsQueryKey) || {}; + if (modified.length > 0 || Number(latest_tags_version) < Number(lastUpdated)) { + // Refetch tags data + queryClient.refetchQueries(tagsQueryKey); + emitCustomEvent({ + ...defaultOutcome, + data: modified, + success: true, + type: "items", + _type: "update" + }); + } + } + return { + data: matchWithCurrentData({ modified, deleted }, match, { with_citekey: true }) as ZItem[], + lastUpdated: Number(lastUpdated) + }; + } catch (error) { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch items", + context: { + data: modified, + deleted, + error: cleanError(error), + response + } + }); + emitCustomEvent({ + ...defaultOutcome, + error, + success: false, + type: "items", + _type: "update" + }); + return Promise.reject(error); + } +} + + +/** Requests data from the `/keys` endpoint of the Zotero API + * @param apikey - The targeted API key + * @returns The API key's permissions + */ +async function fetchPermissions(apikey: string): Promise { + try { + const { data } = await zoteroClient.get(`keys/${apikey}`, { headers: { "Zotero-API-Key": apikey } }); + return data; + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch permissions" + }); + return Promise.reject(error); + } +} + + +/** Requests data from the `/[library]/tags` endpoint of the Zotero API + * @param library - The targeted Zotero library + * @returns The library's tags + */ +async function fetchTags(library: ZLibrary): Promise { + const { apikey, path } = library; + + let tags: ZoteroAPI.Tag[] = []; + + try { + const { data, headers } = await zoteroClient.get( + `${path}/tags?limit=100`, + { headers: { "Zotero-API-Key": apikey } } + ); + tags = data; + + const { "last-modified-version": lastUpdated, "total-results": totalResultsStr } = headers; + const totalResults = Number(totalResultsStr); + + if (totalResults > 100) { + const additional = await fetchAdditionalData({ dataURI: `${path}/tags`, apikey }, totalResults); + tags.push(...additional); + } + + return { + data: makeTagList(tags), + lastUpdated: Number(lastUpdated) + }; + } catch (error) /* istanbul ignore next */ { + window.zoteroRoam?.error?.({ + origin: "API", + message: "Failed to fetch tags", + context: { + error: cleanError(error), + path, + tags + } + }); + return Promise.reject(error); + } +} + + +/** Adds or modifies items in a Zotero library. Only 50 items can be manipulated per API call. + * @param dataList - The array containing the items' data + * @param library - The targeted Zotero library + * @see https://www.zotero.org/support/dev/web_api/v3/write_requests#creating_multiple_objects + * @see https://www.zotero.org/support/dev/web_api/v3/write_requests#updating_multiple_objects + */ +function writeItems(dataList: T[], library: ZLibrary) { + const { apikey, path } = library; + const nbCalls = Math.ceil(dataList.length / 50); + const apiCalls: Promise>[] = []; + + for (let i = 1; i <= nbCalls; i++) { + const itemsData = dataList.slice(50 * (i - 1), 50 * i); + apiCalls.push(zoteroClient.post( + `${path}/items`, + JSON.stringify(itemsData), + { headers: { "Zotero-API-Key": apikey } } + )); + } + return Promise.allSettled(apiCalls); +} + + +export { + deleteTags, + fetchAdditionalData, + fetchBibEntries, + fetchBibliography, + fetchCollections, + fetchDeleted, + fetchItems, + fetchPermissions, + fetchTags, + writeItems +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/helpers.test.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/helpers.test.ts new file mode 100644 index 0000000..2383a8c --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/helpers.test.ts @@ -0,0 +1,263 @@ +import { QueryClient } from "@tanstack/react-query"; +import { vi } from "vitest"; +import { mock } from "vitest-mock-extended"; + + +vi.mock("./base"); +import { fetchItems } from "./base"; +import { ZoteroAPI } from "./types"; + +import { areTagsDuplicate, extractCitekeys, makeTagList, matchWithCurrentData, updateTagMap, wrappedFetchItems } from "./helpers"; + +import { apiKeys, findTags, items, libraries, tags } from "Mocks"; +import { DataRequest } from "Types/extension"; +import { ZTagDictionary } from "Types/transforms"; + + +const { keyWithFullAccess: { key: masterKey } } = apiKeys; +const { userLibrary, groupLibrary } = libraries; +const { path: userPath } = userLibrary; + + +test("Extracting citekeys for Zotero items", () => { + const cases = [ + { key: "ABCD1234", data: { extra: "Citation Key: someCitekey1994" } }, + { key: "PQRST789", data: { extra: "" } }, + { key: "8NNHP96Y", data: { citationKey: "delandaNewPhilosophySociety2006", extra: "" } }, + { key: "ZSEIMWDM", data: { citationKey: "intensiveScienceVirtualPhilosophy2002", extra: "Citation Key: intensiveScienceVirtualPhilosophy2002" } } + ]; + + const expectations = [ + { key: "someCitekey1994", data: { extra: "Citation Key: someCitekey1994" }, has_citekey: true }, + { key: "PQRST789", data: { extra: "" }, has_citekey: false }, + { key: "delandaNewPhilosophySociety2006", data: { citationKey: "delandaNewPhilosophySociety2006", extra: "" }, has_citekey: true }, + { key: "intensiveScienceVirtualPhilosophy2002", data: { citationKey: "intensiveScienceVirtualPhilosophy2002", extra: "Citation Key: intensiveScienceVirtualPhilosophy2002" }, has_citekey: true } + ]; + + expect(extractCitekeys(cases)).toEqual(expectations); + expect(extractCitekeys(items)).toEqual(items); + +}); + +describe("Comparing tag entries", () => { + const tag1 = { tag: "some_tag", meta: { numItems: 3, type: 0 } } as ZoteroAPI.Tag; + const tag2 = { tag: "some_tag", meta: { numItems: 2, type: 0 } } as ZoteroAPI.Tag; + const tag3 = { tag: "some_tag", meta: { numItems: 6, type: 1 } } as ZoteroAPI.Tag; + const tag4 = { tag: "other_tag", meta: { numItems: 4, type: 1 } } as ZoteroAPI.Tag; + + const cases = [ + [[tag1, tag2], true], + [[tag1, tag3], false], + [[tag2, tag3], false], + [[tag1, tag4], false], + [[tag2, tag4], false], + [[tag3, tag4], false] + ]; + + test.each(cases as [[ZoteroAPI.Tag, ZoteroAPI.Tag], boolean][])( + "Tag comparison %#", + (tags_to_compare, are_duplicates) => { + expect(areTagsDuplicate(...tags_to_compare)).toBe(are_duplicates); + } + ); + + test("Inputs with incorrect format are detected", () => { + // @ts-expect-error Test checks for bad input handling + expect(() => areTagsDuplicate(tag1, "some_text")) + .toThrow("Received bad input: \"some_text\", expected a Zotero tag"); + + const tag_with_error = { tag: "some_tag", meta: { numItems: 4 } }; + // @ts-expect-error Test checks for bad input handling + expect(() => areTagsDuplicate(tag1, tag_with_error)) + .toThrow(`Received bad input: ${JSON.stringify(tag_with_error)}, expected the tag to have a type`); + }); + +}); + +describe("Building tag maps", () => { + let tagMap = new Map(); + + beforeEach(() => { + tagMap = new Map([ + ["some_tag", { tag: "some_tag", meta: { numItems: 4, type: 0 } }], + [ + "other_tag", + [ + { tag: "other_tag", meta: { numItems: 3, type: 0 } }, + { tag: "other_tag", meta: { numItems: 11, type: 1 } } + ] + ] + ]); + }); + + test("New entries are added correctly", () => { + updateTagMap(tagMap, { tag: "a_new_tag", meta: { numItems: 2, type: 1 } } as ZoteroAPI.Tag); + expect(tagMap.has("a_new_tag")) + .toBe(true); + expect(tagMap.get("a_new_tag")) + .toEqual({ tag: "a_new_tag", meta: { numItems: 2, type: 1 } }); + }); + + test("New entries are appended correctly - Object entries", () => { + updateTagMap(tagMap, { tag: "some_tag", meta: { numItems: 4, type: 1 } } as ZoteroAPI.Tag); + expect(tagMap.get("some_tag")).toBeInstanceOf(Array); + expect(tagMap.get("some_tag").length).toBe(2); + expect(tagMap.get("some_tag")) + .toEqual([ + { tag: "some_tag", meta: { numItems: 4, type: 0 } }, + { tag: "some_tag", meta: { numItems: 4, type: 1 } } + ]); + }); + + test("New entries are appended correctly - Array entries", () => { + updateTagMap(tagMap, { tag: "other_tag", meta: { numItems: 7, type: 2 } } as ZoteroAPI.Tag); + expect(tagMap.get("other_tag").length).toBe(3); + }); + + test("Duplicates are prevented - Object entries", () => { + updateTagMap(tagMap, { tag: "some_tag", meta: { numItems: 4, type: 0 } } as ZoteroAPI.Tag); + expect(tagMap.get("some_tag")) + .toEqual({ tag: "some_tag", meta: { numItems: 4, type: 0 } }); + }); + + test("Duplicates are prevented - Array entries", () => { + expect(tagMap.get("other_tag").length).toBe(2); + + updateTagMap(tagMap, { tag: "other_tag", meta: { numItems: 3, type: 0 } } as ZoteroAPI.Tag); + expect(tagMap.get("other_tag").length).toBe(2); + + updateTagMap(tagMap, { tag: "other_tag", meta: { numItems: 11, type: 1 } } as ZoteroAPI.Tag); + expect(tagMap.get("other_tag").length).toBe(2); + }); + + test("Badly constructed maps are detected", () => { + const map_with_error = new Map([ + ["some_tag", "some text"], + ["other_tag", "other_text"] + ]); + + // @ts-expect-error Test checks for bad input handling + expect(() => updateTagMap(map_with_error, { tag: "some_tag", meta: { numItems: 8, type: 1 } })) + .toThrow("Map entry is of unexpected type string, expected Array or Object"); + }); +}); + +describe("Creating formatted tag lists", () => { + const cases = Object.entries(libraries); + + function setExpectations(path: string, list: ZTagDictionary) { + const output = {}; + Object.entries(list).map(([initial, tokens]) => { + output[initial] = tokens.map(token => ({ + token, + roam: [], + zotero: findTags(path, token).reverse().sort((a, b) => a.tag < b.tag ? 1 : -1) + })); + }); + return output; + } + + const expectations = { + [userLibrary.path]: setExpectations(userLibrary.path, { + "i": ["immigrant youth", "immigration"], + "p": ["patient journeys"] + }), + [groupLibrary.path]: setExpectations(groupLibrary.path, { + "h": ["housing"], + "u": ["urban design"] + }) + }; + + test.each(cases)( + "%# Creating tag list for %s", + (_libName, libraryDetails) => { + const { path } = libraryDetails; + expect(makeTagList(tags[path])).toEqual(expectations[path]); + } + ); +}); + +test("Merging data updates", () => { + const itemsList = [ + { data: { key: "ABC" }, key: "someCitekey" }, + { data: { key: "DEF" }, key: "DEF" }, + { data: { key: "GHI" }, key: "GHI" } + ]; + + expect(matchWithCurrentData( + { + modified: [itemsList[1]], + deleted: [] + }, + [itemsList[0]], + { with_citekey: false } + )) + .toEqual([itemsList[0], itemsList[1]]); + + expect(matchWithCurrentData( + { + modified: [itemsList[2]], + deleted: [] + }, + itemsList, + { with_citekey: false } + )) + .toEqual(itemsList); + + expect(matchWithCurrentData( + { + modified: [], + deleted: [itemsList[0].data.key] + }, + [itemsList[0]], + { with_citekey: false } + )) + .toEqual([]); + + expect(matchWithCurrentData( + { + modified: [itemsList[1]], + deleted: [itemsList[2].data.key] + }, + itemsList, + { with_citekey: false } + )) + .toEqual(itemsList.slice(0, 2)); +}); + +describe("wrappedFetchItems", () => { + let client: QueryClient; + const sample_req = mock({ + library: { + path: userPath + }, + apikey: masterKey, + dataURI: userPath + "/items" + }); + const { apikey, library, ...identifiers } = sample_req; + + beforeEach(() => { + client = new QueryClient(); + }); + + test("Fetching items when query cache is empty", async () => { + await wrappedFetchItems(sample_req, client); + + expect(fetchItems).toHaveBeenCalledWith({ ...sample_req, since: 0 }, { match: [] }, client); + }); + + test("Fetching items when query cache has version data", async () => { + const cachedData = { + data: [items[0]], + lastUpdated: 13 + }; + client.setQueryData( + ["items", library.path, { ...identifiers }], + (_prev) => cachedData + ); + + await wrappedFetchItems(sample_req, client); + + expect(fetchItems).toHaveBeenCalledWith({ ...sample_req, since: cachedData.lastUpdated }, { match: cachedData.data }, client); + }); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/helpers.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/helpers.ts new file mode 100644 index 0000000..2369538 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/helpers.ts @@ -0,0 +1,220 @@ +import { QueryClient } from "@tanstack/react-query"; + +import { Queries } from "@services/react-query"; + +import { fetchItems } from "./base"; +import { ZoteroAPI } from "./types"; + +import { makeDictionary, searchEngine } from "../../utils"; + +import { DataRequest } from "Types/extension"; +import { ZTagEntry, ZTagList, ZTagMap } from "Types/transforms"; + +/** Compares two Zotero tags based on tag string and type, to determine if they are duplicates + * @param tag1 - The first tag to compare + * @param tag2 - The second tag to compare + * @returns The result of the comparison - `true` if the tags are duplicates of each other, `false` otherwise + */ +function areTagsDuplicate(tag1: ZoteroAPI.Tag, tag2: ZoteroAPI.Tag) { + [tag1, tag2].forEach(tag => { + if (tag.constructor !== Object || !tag.tag || !tag.meta) { + throw new Error(`Received bad input: ${JSON.stringify(tag)}, expected a Zotero tag`); + } + if (tag.meta.type === undefined) { + throw new Error(`Received bad input: ${JSON.stringify(tag)}, expected the tag to have a type`); + } + }); + + return tag1.meta.type == tag2.meta.type && tag1.tag == tag2.tag; +} + + +/** Categorizes Zotero tags into tokens, based on similar spellings + * @param z_data - The tags to be categorized, as Strings + * @param tagMap - The map of Zotero tags + * @returns The Array of tokenized tags, sorted alphabetically + */ +function categorizeZoteroTags(z_data: string[], tagMap: ZTagMap): ZTagEntry[] { + const output: ZTagEntry[] = []; + const zdata = Array.from(z_data).sort((a, b) => a > b ? -1 : 1); + + for (const elem of zdata) { + try { + const in_table = output.findIndex(tk => searchEngine(elem, tk.token, { any_case: true, match: "exact", search_compounds: true })); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const z_item = tagMap.get(elem)!; + if (in_table == -1) { + output.push({ + token: elem.toLowerCase(), + roam: [], + zotero: Array.isArray(z_item) ? z_item : [z_item] + }); + } else { + if (Array.isArray(z_item)) { + output[in_table].zotero.push(...z_item); + } else { + output[in_table].zotero.push(z_item); + } + } + } catch (e) { + throw new Error(`Failed to categorize ${elem}: ` + e.message); + } + } + + return output.sort((a, b) => a.token < b.token ? -1 : 1); +} + + +type WithHasCitekey = T & { has_citekey: boolean }; + +/** Extracts pinned citekeys from a dataset + * @param arr - The items to scan + * @returns The processed dataset : each item gains a `has_citekey` property, and its `key` property is assigned its citekey + */ +function extractCitekeys(arr: T[]): WithHasCitekey[] { + const itemList = [...arr]; + return itemList.map(item => { + let { key } = item; + let has_citekey = false; + + if (item.data.citationKey) { + key = item.data.citationKey; + has_citekey = true; + } else if (typeof (item.data.extra) !== "undefined") { + if (item.data.extra.includes("Citation Key: ")) { + key = item.data.extra.match("Citation Key: (.+)")?.[1] || key; + has_citekey = true; + } + } + return { + ...item, + key, + has_citekey + }; + }); +} + + +/** Converts Zotero tags data into a categorized list + * @param tags - The tags data from Zotero to categorize + * @returns The list of categorized tags + */ +function makeTagList(tags: ZoteroAPI.Tag[]): ZTagList { + try { + const tagMap = makeTagMap(tags); + const zdict = makeDictionary(Array.from(tagMap.keys())); + const zkeys = Object.keys(zdict).sort((a, b) => a < b ? -1 : 1); + + const output = {}; + zkeys.forEach(key => { + output[key] = categorizeZoteroTags(zdict[key], tagMap); + }); + return output; + } catch (e) { + throw new Error("Could not create tag list : " + e.message); + } +} + + +/** Converts Zotero tags data into a Map + * @param tags - The tags data from Zotero from which to create the Map + * @returns A Map where each entry groups together Zotero tags with the exact same spelling, but a different type + */ +function makeTagMap(tags: ZoteroAPI.Tag[]) { + return tags.reduce( + (map, tag) => updateTagMap(map, tag), + new Map() + ); +} + + +/** Merges two datasets. As the match is done on the `data.key` property, both items and collections can be matched. For items, citekeys are extracted. + */ +function matchWithCurrentData( + update: { modified?: T[], deleted?: string[] }, + arr: T[] = [], + { with_citekey = false } = {} +) { + const { modified = [], deleted = [] } = update; + // If the data has citekeys, transform before pushing + const modifiedData = with_citekey + ? extractCitekeys([...modified]) + : [...modified]; + const deletedData = [...deleted]; + + // Remove deleted items + const oldData = deletedData.length == 0 + ? arr + : arr.filter(item => !deletedData.includes(item.data.key)); + + // Update datastore + if (modifiedData.length == 0) { + return oldData; + } else if (oldData.length == 0) { + return modifiedData; + } else { + const [...datastore] = oldData; + modifiedData.forEach(item => { + const duplicateIndex = datastore.findIndex(i => i.data.key == item.data.key); + if (duplicateIndex == -1) { + datastore.push(item); + } else { + datastore[duplicateIndex] = item; + } + }); + return datastore; + } +} + + +/** Adds a new entry to a tag map, if it doesn't already exist + * @param map - The targeted tag map + * @param tagEntry - The entry to be added + * @returns The updated tag map + */ +function updateTagMap(map: ZTagMap, tagEntry: ZoteroAPI.Tag) { + const { tag } = tagEntry; + + // If the map already has an entry for the tag, try to append the new entry + if (map.has(tag)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const entry = map.get(tag)!; + + if (Array.isArray(entry)) { + // Only append if no duplicate exists + if (entry.every(el => !areTagsDuplicate(tagEntry, el))) { + map.set(tag, [...entry, tagEntry]); + } + } else if (typeof (entry) === "object") { + if (!areTagsDuplicate(tagEntry, entry)) { + map.set(tag, [entry, tagEntry]); + } + } else { + throw new Error(`Map entry is of unexpected type ${typeof (entry)}, expected Array or Object`); + } + // Else add the entry to the map + } else { + map.set(tag, tagEntry); + } + return map; +} + + +/** Wrapper for retrieving items data, based on contents of the query cache. */ +async function wrappedFetchItems(req: DataRequest, queryClient: QueryClient) { + const { apikey, library: { path }, ...identifiers } = req; + const queryKey: Queries.Key.Items = ["items", path, { ...identifiers }]; + const { data: match = [], lastUpdated: since = 0 } = queryClient.getQueryData(queryKey) || {}; + return await fetchItems({ ...req, since }, { match }, queryClient); +} + + +export { + areTagsDuplicate, + extractCitekeys, + makeTagList, + makeTagMap, + matchWithCurrentData, + updateTagMap, + wrappedFetchItems +}; diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/hooks.test.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/hooks.test.ts new file mode 100644 index 0000000..d1ed8e4 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/hooks.test.ts @@ -0,0 +1,118 @@ +import { renderHook } from "@testing-library/react-hooks"; +import { mock } from "vitest-mock-extended"; +import { wrapper } from "../query-test-wrapper"; + +import { makeTagList } from "./helpers"; +import { useCollections, useItems, useTags, useWriteableLibraries } from "./hooks"; + +import { apiKeys, findCollections, findItems, libraries, tags } from "Mocks"; +import { DataRequest } from "Types/extension"; + + +const { + keyWithFullAccess: { key: masterKey }, + keyWithNoGroupAccess: { key: userOnlyKey } +} = apiKeys; +const { + userLibrary: { path: userPath }, + groupLibrary: { path: groupPath } +} = libraries; + + +describe("useCollections", () => { + it("returns fetch results", async () => { + const libs = Object.values(libraries).map(lib => ({ + apikey: masterKey, + path: lib.path + })); + + const { result, waitFor } = renderHook(() => useCollections(libs, {}), { wrapper }); + + await waitFor(() => result.current.every(res => !res.isLoading)); + + const libData = Object.values(libraries).map(lib => ({ + data: findCollections(lib.type, lib.id, 0), + lastUpdated: lib.version + })); + + expect(result.current.map(res => res.data)) + .toEqual(libData); + }); +}); + +describe("Hook for items queries", () => { + it("returns fetch results", async () => { + const reqs = Object.values(libraries).map((lib) => mock({ + apikey: masterKey, + dataURI: `${lib.path}/items`, + library: { path: lib.path } + })); + + const { result, waitFor } = renderHook(() => useItems(reqs, {}), { wrapper }); + + await waitFor(() => result.current.every(res => !res.isLoading)); + + const itemData = Object.values(libraries).map(lib => ({ + data: findItems({ type: lib.type, id: lib.id, since: 0 }), + lastUpdated: lib.version + })); + + expect(result.current.map(res => res.data)) + .toEqual(itemData); + }); +}); + +describe("Hook for tags queries", () => { + it("returns fetch results", async () => { + const libs = Object.values(libraries).map(lib => ({ + apikey: masterKey, + path: lib.path + })); + + const { result, waitFor } = renderHook(() => useTags(libs, {}), { wrapper }); + + await waitFor(() => result.current.every(res => !res.isLoading)); + + const tagData = Object.values(libraries).map(lib => ({ + data: makeTagList(tags[lib.path]), + lastUpdated: lib.version + })); + + expect(result.current.map(res => res.data)) + .toEqual(tagData); + }); +}); + +describe("useWriteableLibraries", () => { + test("Key with full access", async () => { + const libs = [ + { apikey: masterKey, path: userPath }, + { apikey: masterKey, path: groupPath } + ]; + + const { result, waitFor } = renderHook(() => useWriteableLibraries(libs), { wrapper }); + + await waitFor(() => !result.current.isLoading); + + expect(result.current.data) + .toEqual(libs); + + }); + + test("Key with no group access", async () => { + const libs = [ + { apikey: userOnlyKey, path: userPath }, + { apikey: userOnlyKey, path: groupPath } + ]; + + const { result, waitFor } = renderHook(() => useWriteableLibraries(libs), { wrapper }); + + await waitFor(() => !result.current.isLoading); + + expect(result.current.data) + .toEqual([ + { apikey: userOnlyKey, path: userPath } + ]); + + }); +}); diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/hooks.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/hooks.ts new file mode 100644 index 0000000..c7fb963 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/hooks.ts @@ -0,0 +1,163 @@ +import { useMemo } from "react"; +import { useQueries, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; + +import { Queries } from "@services/react-query"; + +import { fetchCollections, fetchPermissions, fetchTags } from "./base"; +import { wrappedFetchItems } from "./helpers"; + +import { DataRequest } from "Types/extension"; +import { ZLibrary } from "Types/transforms"; + + +/** Use collections from one or more Zotero libraries. By default, `staleTime = 5 min` and `refetchInterval = 5 min`. */ +const useCollections = ( + /** The targeted Zotero libraries */ + libraries: ZLibrary[], + /** Optional configuration to use with the queries */ + opts: Omit, "queryKey" | "queryFn"> = {} +) => { + // Defaults for this query + const { staleTime = 1000 * 60 * 5, refetchInterval = 1000 * 60 * 5, ...rest } = opts; + // Factory + const client = useQueryClient(); + const queriesDefs = libraries.map((lib) => { + const { path/*, apikey*/ } = lib; + const queryKey: Queries.Key.Collections = ["collections", { library: path }]; + const { data: match, lastUpdated: since } = client.getQueryData(queryKey) || {}; + return { + queryKey: queryKey, + queryFn: (_queryKey) => fetchCollections(lib, since, { match }), + staleTime, + refetchInterval, + ...rest + }; + }); + return useQueries({ + queries: queriesDefs + }); +}; + +/** Use Zotero items from one or more Zotero libraries. By default, `staleTime = 1 min` and `refetchInterval = 1 min`. */ +const useItems = ( + /** The targeted data requests */ + reqs: DataRequest[], + /** Optional configuration to use with the queries */ + opts: Omit, "queryKey" | "queryFn"> = {} +) => { + const client = useQueryClient(); + const queriesDefs = useMemo(() => { + // Defaults for this query + const { staleTime = 1000 * 60, refetchInterval = 1000 * 60, ...rest } = opts; + // Factory + return reqs.map((req) => { + const { apikey, library: { path }, ...identifiers } = req; + const queryKey: Queries.Key.Items = ["items", path, { ...identifiers }]; + return { + queryKey: queryKey, + queryFn: (_queryKey) => wrappedFetchItems(req, client), + staleTime, + refetchInterval, + ...rest + }; + }); + }, [reqs, client, opts]); + return useQueries({ + queries: queriesDefs + }); +}; + +/** Use permissions for one or more Zotero API keys. By default, `staleTime = 1 hour` and `refetchInterval = 1 hour`. */ +const usePermissions = ( + /** The targeted API keys */ + keys: string[], + /** Optional configuration to use with the queries */ + opts: Omit, "queryKey" | "queryFn"> = {} +) => { + // Defaults for this query + const { staleTime = 1000 * 60 * 60, refetchInterval = 1000 * 60 * 60, ...rest } = opts; + // Factory + const queriesDefs = keys.map((apikey) => { + const queryKey: Queries.Key.Permissions = ["permissions", { apikey }]; + return { + queryKey: queryKey, + queryFn: (_queryKey) => fetchPermissions(apikey), + staleTime, + refetchInterval, + ...rest + }; + }); + return useQueries({ + queries: queriesDefs + }); +}; + +/** Use Zotero tags from one or more libraries. By default, `staleTime = 3 min`. + * Refetching is managed by {@link useItems}. + */ +const useTags = ( + /** The targeted Zotero libraries */ + libraries: ZLibrary[], + /** Optional configuration to use with the queries */ + opts: Omit, "queryKey" | "queryFn"> = {} +) => { + const queriesDefs = useMemo(() => { + // Defaults for this query + const { staleTime = 1000 * 60 * 3, ...rest } = opts; + // Factory + return libraries.map((lib) => { + const { apikey, path } = lib; + const queryKey: Queries.Key.Tags = ["tags", { library: path }]; + return { + queryKey: queryKey, + queryFn: (_queryKey) => fetchTags({ apikey, path }), + staleTime, + ...rest + }; + }); + }, [libraries, opts]); + + return useQueries({ + queries: queriesDefs + }); +}; + + +/** Custom hook for retrieving the list of Zotero libraries with `write` permissions. + * @param libraries - The targeted Zotero libraries + * @returns The operation's status and outcome + */ +const useWriteableLibraries = (libraries: ZLibrary[]) => { + const apiKeys = useMemo(() => Array.from(new Set(libraries.map(lib => lib.apikey))), [libraries]); + const permissionQueries = usePermissions(apiKeys, { + notifyOnChangeProps: ["data", "isLoading"] + }); + + const isLoading = permissionQueries.some(q => q.isLoading); + const permissions = useMemo(() => permissionQueries.map(q => q.data || []).flat(1), [permissionQueries]); + + const data = useMemo(() => { + return libraries + .filter(lib => { + const keyData = permissions.find(k => k.key == lib.apikey); + if (!keyData) { + return false; + } else { + const { access } = keyData; + const [libType, libId] = lib.path.split("/"); + const permissionsList = libType == "users" + ? access.user + : (access.groups?.[libId] || access.groups?.all); + return (permissionsList || {}).write || false; + } + }); + }, [libraries, permissions]); + + return { + data, + isLoading + }; +}; + + +export { useCollections, useItems, usePermissions, useTags, useWriteableLibraries }; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/index.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/index.ts new file mode 100644 index 0000000..605ac1f --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/index.ts @@ -0,0 +1,4 @@ +export * from "./base"; +export * from "./hooks"; +export * from "./mutations"; +export * from "./types"; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/mutations.test.tsx b/prototypes/zotero-roam-mcp/src/clients/zotero/mutations.test.tsx new file mode 100644 index 0000000..56b11e8 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/mutations.test.tsx @@ -0,0 +1,161 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { act, renderHook } from "@testing-library/react-hooks"; +import { mock } from "vitest-mock-extended"; + +import { fetchItems, fetchTags } from "./base"; +import { useDeleteTags, useImportCitoids, useModifyTags } from "./mutations"; + +import { apiKeys, citoids, goodIdentifier, libraries } from "Mocks"; +import { DataRequest } from "Types/extension"; +import { isFulfilled } from "Types/helpers"; + + +const { keyWithFullAccess: { key: masterKey } } = apiKeys; +const { groupLibrary, userLibrary } = libraries; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Set global defaults here + } + }, + logger: { + log: console.log, + warn: console.warn, + error: () => { } + } +}); + +const invalidateQueriesSpy = vi.spyOn(queryClient, "invalidateQueries"); +const dispatchEventSpy = vi.spyOn(document, "dispatchEvent"); + +// https://tkdodo.eu/blog/testing-react-query +const wrapper = ({ children }) => { + return ( + {children} + ); +}; + +describe("Mutation hooks for the Zotero API", () => { + afterEach(() => { + queryClient.clear(); + }); + + describe("useDeleteTags", () => { + beforeEach(async() => { + const mockData = await fetchTags({ apikey: masterKey, path: userLibrary.path }); + queryClient.setQueryData(["tags", { library: userLibrary.path }], () => mockData); + }); + + test("callback on success", async () => { + const { result, waitFor } = renderHook(() => useDeleteTags(), { wrapper }); + + expect(result.current.status).toBe("idle"); + act(() => { + result.current.mutate({ + library: { apikey: masterKey, path: userLibrary.path }, + tags: ["systems"] + }); + }); + await waitFor(() => result.current.status == "success"); + + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + ["items", userLibrary.path], + { refetchType: "all" }, + { throwOnError: true } + ); + + expect((dispatchEventSpy.mock.calls[0][0] as CustomEvent).detail).toEqual({ + args: { tags: ["systems"] }, + error: null, + library: userLibrary.path, + _type: "tags-deleted" + }); + }); + }); + + describe("useImportCitoids", () => { + + test("callback on success", async () => { + const { result, waitFor } = renderHook(() => useImportCitoids(), { wrapper }); + + expect(result.current.status).toBe("idle"); + act(() => { + result.current.mutate({ + collections: [], + items: [citoids[goodIdentifier]], + library: { apikey: masterKey, path: userLibrary.path }, + tags: [] + }); + }); + await waitFor(() => result.current.status == "success"); + + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + ["items", userLibrary.path], + { refetchType: "all" } + ); + + expect((dispatchEventSpy.mock.calls[0][0] as CustomEvent).detail).toEqual({ + args: { + collections: [], + items: [citoids[goodIdentifier]], + tags: [] + }, + data: { + failed: [], + successful: result.current.data?.filter(isFulfilled).map(res => res.value.data) + }, + error: null, + library: userLibrary.path, + _type: "write" + }); + }); + }); + + describe("useModifyTags", () => { + const sample_req = mock({ apikey: masterKey, dataURI: `${groupLibrary.path}/items`, library: { path: groupLibrary.path } }); + + beforeEach(async() => { + const mockData = await fetchItems( + { ...sample_req, since: 0 }, + { match: [] }, + queryClient + ); + queryClient.setQueryData(["items", groupLibrary.path, {}], () => mockData); + }); + + test("callback on success", async () => { + const { result, waitFor } = renderHook(() => useModifyTags(), { wrapper }); + + expect(result.current.status).toBe("idle"); + act(() => { + result.current.mutate({ + into: "HOUSING", + library: { apikey: masterKey, path: groupLibrary.path }, + tags: ["housing"] + }); + }); + await waitFor(() => result.current.status == "success"); + + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + ["items", groupLibrary.path], + { refetchType: "all" } + ); + + expect((dispatchEventSpy.mock.calls[0][0] as CustomEvent).detail).toEqual({ + args: { + into: "HOUSING", + tags: ["housing"] + }, + data: { + failed: [], + successful: result.current.data?.filter(isFulfilled).map(res => res.value.data) + }, + error: null, + library: groupLibrary.path, + _type: "tags-modified" + }); + }); + }); + +}); diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/mutations.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/mutations.ts new file mode 100644 index 0000000..6ea3a70 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/mutations.ts @@ -0,0 +1,208 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { CitoidAPI } from "@clients/citoid"; +import { emitCustomEvent } from "@services/events"; +import { Queries } from "@services/react-query"; + +import { deleteTags, writeItems } from "./base"; +import { ZoteroAPI } from "./types"; + +import { isFulfilled } from "Types/helpers"; +import { ZLibrary, isZItemTop } from "Types/transforms"; + + +type DeleteTagsArgs = { + library: ZLibrary, + tags: string[] +}; + +/** Delete tags from a Zotero library. + * @fires zotero-roam:write + */ +const useDeleteTags = () => { + const client = useQueryClient(); + + return useMutation((variables: DeleteTagsArgs) => { + const { library: { apikey, path }, tags } = variables; + const { lastUpdated: version = 0 } = client.getQueryData(["tags", { library: path }]) || {}; + + return deleteTags(tags, { apikey, path }, version); + }, { + onSettled: (_data, error, variables, _context) => { + const { library: { path }, tags } = variables; + + if (!error) { + // Invalidate item queries related to the library used + // Data can't be updated through cache modification because of the library version + client.invalidateQueries(["items", path], { + refetchType: "all" + }, { + throwOnError: true + }); + } + + emitCustomEvent({ + args: { + tags + }, + error, + library: path, + _type: "tags-deleted" + }); + } + }); +}; + + +type ImportCitoidsArgs = { + collections: string[], + items: CitoidAPI.AsZotero[], + library: ZLibrary, + tags: string[]; +}; + +/** Add items to a Zotero library. + * @fires zotero-roam:write + */ +const useImportCitoids = () => { + const client = useQueryClient(); + + return useMutation((variables: ImportCitoidsArgs) => { + const { collections = [], items, library, tags = [] } = variables; + + // Transform the data for import + const clean_tags = tags.map(t => { return { tag: t }; }); + const dataList = items + .map(citoid => { + // Remove key and version from the data object + const { key, version, ...item } = citoid; + + return { + ...item, + collections, + tags: clean_tags + }; + }); + + return writeItems>(dataList, library); + }, { + onSettled: (data = [], error, variables, _context) => { + const { collections, items, library: { path }, tags } = variables; + + const outcome = data.reduce<{ successful: ZoteroAPI.Responses.ItemsWrite[], failed: string[] }>((obj, res) => { + /* istanbul ignore else */ + if (isFulfilled(res)) { + obj.successful.push(res.value.data); + } else { + obj.failed.push(res.reason); + } + return obj; + }, { successful: [], failed: [] }); + + if (!error && outcome.successful.length > 0) { + // Invalidate item queries related to the library used + // Data can't be updated through cache modification because of the library version + client.invalidateQueries(["items", path], { + refetchType: "all" + }); + } + + emitCustomEvent({ + _type: "write", + args: { + collections, + items, + tags + }, + data: outcome, + error, + library: path + }); + } + }); +}; + + +type ModifyTagsArgs = { + into: string, + library: ZLibrary, + tags: string[] +}; + +/** Update tags in a Zotero library + * @fires zotero-roam:tags-modified + */ +const useModifyTags = () => { + const client = useQueryClient(); + + return useMutation((variables: ModifyTagsArgs) => { + const { into, library: { apikey, path }, tags } = variables; + const dataList: Pick[] = []; + const libItems = client.getQueriesData(["items", path]) + .map(query => (query[1] || {}).data || []).flat(1) + .filter(isZItemTop) + .filter(i => i.data.tags.length > 0); + + libItems.forEach(i => { + const itemTags = i.data.tags; + // If the item already has the target tag, with type 0 (explicit or implicit) - remove it from the array before the filtering : + const has_clean_tag = itemTags.findIndex(it => it.tag == into && (it.type == 0 || !it.type)); + if (has_clean_tag > -1) { + itemTags.splice(has_clean_tag, 1); + } + // Compare the lengths of the tag arrays, before vs. after filtering out the tags to be renamed + const cleanTags = itemTags.filter(t => !tags.includes(t.tag)); + if (cleanTags.length < itemTags.length) { + // If they do not match (aka, there are tags to be removed/renamed), insert the target tag & add to the dataList + cleanTags.push({ tag: into, type: 0 }); + dataList.push({ + key: i.data.key, + version: i.version, + tags: cleanTags + }); + } + }); + + return writeItems>(dataList, { apikey, path }); + }, { + onSettled: (data = [], error, variables, _context) => { + const { into, library: { path }, tags } = variables; + + const outcome = data.reduce<{ successful: ZoteroAPI.Responses.ItemsWrite[], failed: string[] }>((obj, res) => { + /* istanbul ignore else */ + if (isFulfilled(res)) { + obj.successful.push(res.value.data); + } else { + obj.failed.push(res.reason); + } + return obj; + }, { successful: [], failed: [] }); + + /* istanbul ignore if */ + if (outcome.successful.length > 0) { + // If any item was modified, invalidate item queries for the targeted library + // Data can't be updated through cache modification because of the library version + client.invalidateQueries(["items", path], { + refetchType: "all" + }); + } + + emitCustomEvent({ + args: { + into, + tags + }, + data: outcome, + error, + library: path, + _type: "tags-modified" + }); + } + }); +}; + +export { + useDeleteTags, + useImportCitoids, + useModifyTags +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/clients/zotero/types.ts b/prototypes/zotero-roam-mcp/src/clients/zotero/types.ts new file mode 100644 index 0000000..5ba8bd0 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/clients/zotero/types.ts @@ -0,0 +1,444 @@ + +/** + * @see https://github.com/zotero/translators/blob/master/index.d.ts + */ +export namespace ZoteroAPI { + /** A creator's role (author, editor, translator...) */ + export type CreatorType = _CreatorType; + + /** A creator of a Zotero item */ + export type Creator = ({ name: string } | { firstName: string, lastName: string }) & { + creatorType: CreatorType + }; + + /** An entity's related links */ + type EntityLinks = Record; + + /** An entity's library */ + export type EntityLibrary = { + /** The library's Zotero ID */ + id: number, + /** Links related to the library */ + links: EntityLinks, + /** The library's display name */ + name: string, + /** The library's type */ + type: LibraryType + }; + + type ExportFormat = _ExportFormat; + + export type LibraryType = "user" | "group"; + export type LibraryTypeURI = `${LibraryType}s`; + + /** Basic metadata for Zotero items and collections */ + interface Base { + /** The entity's Zotero key */ + key: string, + /** The entity's Zotero library */ + library: EntityLibrary, + /** Links related to the Zotero entity */ + links: EntityLinks, + /** Metadata about the entity */ + meta: Record, + /** The entity's current version */ + version: number + } + + export interface Collection extends Base { + data: { + /** The collection's Zotero key */ + key: Base["key"], + /** The collection's display name */ + name: string, + /** The key of the collection's parent, if any */ + parentCollection: string | false, + relations: Record, + version: Base["version"] + } & Record, + meta: { + numCollections: number, + numItems: number + } + } + + export interface Tag { + links: Base["links"], + meta: { + numItems: number, + type: 0 | 1 | number + }, + tag: string, + } + + type ItemTag = { + tag: Tag["tag"], + type?: Tag["meta"]["type"] + }; + + type ItemTopType = _ItemTopType; + + type ItemType = ItemTopType | "attachment" | "annotation" | "note"; + + /** Basic metadata for Zotero items */ + interface ItemDataBase { + /** The datetime when the item was created */ + dateAdded: string, + /** The datetime when the item was last modified */ + dateModified: string, + /** The item's type */ + itemType: T, + /** The item's Zotero key */ + key: Base["key"], + /** Zotero entities linked that are linked to the item */ + relations: Record, + /** The item's tags */ + tags: ItemTag[] + /** The item's current version */ + version: Base["version"] + } + + export interface ItemAnnotation extends Base { + data: ItemDataBase<"annotation"> & { + /** The highlight color of the annotation */ + annotationColor: string, + /** The text of the comment, if any */ + annotationComment: string, + /** The label for the page where the annotation is located */ + annotationPageLabel: string, + /** The position of the annotation in the PDF. This is a stringified JSON object, containing the page index and the rect coordinates of the annotation. + * @example "{\"pageIndex\":24,\"rects\":[[203.6,431.053,546.865,441.6],[203.6,419.056,536.829,429.603],[203.6,407.059,566.448,417.606],[203.6,395.062,564.521,405.609],[203.6,383.065,265.699,393.612]]}" + */ + annotationPosition: string, + /** The position of the annotation in the page. + * @example "00024|001317|00350" + */ + annotationSortIndex: string, + /** The text of the highlight */ + annotationText: string, + /** The type of annotation */ + annotationType: "highlight" | "image", + /** The Zotero key of the item's parent */ + parentItem: string + } + } + + export interface ItemAttachment extends Base { + data: ItemDataBase<"attachment"> & { + /** The type of attachment + * @example "application/pdf" + */ + contentType: string, + parentItem: string, + [x: string]: any + } + } + + export interface ItemNote extends Base { + data: ItemDataBase<"note"> & { + note: string, + parentItem: string + } & Record + } + + export interface ItemTop extends Base { + data: ItemDataBase & { + collections: string[], + creators: Creator[], + extra: string, + title: string + } & { [P in _ItemTopDataFields]?: any }, + meta: { + creatorSummary: string, + numChildren: number, + parsedDate: string, + [x: string]: any + } & Record + } + + /** A Zotero item (top-level, annotation, note, attachment...) */ + export type Item = ItemAnnotation | ItemAttachment | ItemNote | ItemTop; + + export namespace Requests { + /** Parameters to control bibliography output from Zotero */ + export interface BibliographyArgs { + /** The output format(s) to use (API default: `data`). Multiple formats can be requested, by passing them as a comma-separated list. + * @example "data,bib" + */ + include: string, + /** Determines if URLs and DOIs should be formatted as links (API default: `0`). */ + linkwrap: 0 | 1, + /** The locale to use for generating citations (API default: `en-US`). */ + locale: string, + /** The citation style to use (API default: `chicago-note-bibliography`). + * @example "apa" + */ + style: string + } + } + + export namespace Responses { + /** API KEYS */ + /** @see https://www.zotero.org/support/dev/web_api/v3/syncing#verify_key_access */ + export interface Permissions { + access: { + user?: { + files: boolean, + library: boolean, + notes: boolean, + write: boolean + }, + groups?: { + [P in "all" | string]?: { + library: boolean, + write: boolean + } + } + }, + key: string, + userID: number, + username?: string + } + + /** COLLECTIONS */ + + export type Collections = Collection[]; + + /** DELETIONS */ + + export interface Deleted { + collections: string[], + items: string[], + searches: string[], + tags: string[] + } + + /** ITEMS */ + type ExportFormatData = { + [Format in ExportFormat]: string + } + + type IncludeJSON = ExportFormatData & { + bib: string, + citation: string, + data: Item["data"] + } + type IncludeFormat = keyof IncludeJSON; + + type Include = K extends "data" + ? Item + : Base & Pick & { meta?: Record }; + + export type ItemGet = Include; + export type ItemsGet = Include[]; + + export interface ItemsWrite { + failed: Record, + unchanged: Record, + success: Record, + successful: Record + } + + /** TAGS */ + export type Tags = Tag[]; + // TODO: Verify return type + export type TagsDelete = null; + + } +} + +type _CreatorType = + | "artist" + | "contributor" + | "performer" + | "composer" + | "wordsBy" + | "sponsor" + | "cosponsor" + | "author" + | "commenter" + | "editor" + | "translator" + | "seriesEditor" + | "bookAuthor" + | "counsel" + | "programmer" + | "reviewedAuthor" + | "recipient" + | "director" + | "scriptwriter" + | "producer" + | "interviewee" + | "interviewer" + | "cartographer" + | "inventor" + | "attorneyAgent" + | "podcaster" + | "guest" + | "presenter" + | "castMember"; + +type _ExportFormat = + | "bibtex" + | "biblatex" + | "bookmarks" + | "coins" + | "csljson" + | "csv" + | "mods" + | "refer" + | "rdf_bibliontology" + | "rdf_dc" + | "rdf_zotero" + | "ris" + | "tei" + | "wikipedia"; + +type _ItemTopType = + | "artwork" + | "audioRecording" + | "bill" + | "blogPost" + | "book" + | "bookSection" + | "case" + | "computerProgram" + | "conferencePaper" + | "dictionaryEntry" + | "document" + | "email" + | "encyclopediaArticle" + | "film" + | "forumPost" + | "hearing" + | "instantMessage" + | "interview" + | "journalArticle" + | "letter" + | "magazineArticle" + | "manuscript" + | "map" + | "newspaperArticle" + | "patent" + | "podcast" + | "preprint" + | "presentation" + | "radioBroadcast" + | "report" + | "statute" + | "thesis" + | "tvBroadcast" + | "videoRecording" + | "webpage"; + +type _ItemTopDataFields = + | "numPages" + | "numberOfVolumes" + | "abstractNote" + | "accessDate" + | "applicationNumber" + | "archive" + | "archiveID" + | "artworkSize" + | "assignee" + | "billNumber" + | "blogTitle" + | "bookTitle" + | "callNumber" + | "caseName" + | "citationKey" + | "code" + | "codeNumber" + | "codePages" + | "codeVolume" + | "committee" + | "company" + | "conferenceName" + | "country" + | "court" + | "DOI" + | "date" + | "dateDecided" + | "dateEnacted" + | "dictionaryTitle" + | "distributor" + | "docketNumber" + | "documentNumber" + | "edition" + | "encyclopediaTitle" + | "episodeNumber" + | "extra" + | "audioFileType" + | "filingDate" + | "firstPage" + | "videoRecordingFormat" + | "audioRecordingFormat" + | "format" + | "forumTitle" + | "genre" + | "history" + | "ISBN" + | "ISSN" + | "identifier" + | "institution" + | "issue" + | "issueDate" + | "issuingAuthority" + | "journalAbbreviation" + | "label" + | "language" + | "legalStatus" + | "legislativeBody" + | "libraryCatalog" + | "archiveLocation" + | "artworkMedium" + | "interviewMedium" + | "meetingName" + | "nameOfAct" + | "network" + | "number" + | "organization" + | "pages" + | "patentNumber" + | "place" + | "postType" + | "priorityNumbers" + | "proceedingsTitle" + | "programmingLanguage" + | "programTitle" + | "publicLawNumber" + | "publicationTitle" + | "publisher" + | "references" + | "repositoryLocation" + | "reportNumber" + | "reportType" + | "reporter" + | "reporterVolume" + | "repository" + | "rights" + | "runningTime" + | "scale" + | "section" + | "series" + | "seriesNumber" + | "seriesText" + | "seriesTitle" + | "session" + | "shortTitle" + | "status" + | "studio" + | "subject" + | "system" + | "letterType" + | "manuscriptType" + | "mapType" + | "thesisType" + | "presentationType" + | "type" + | "url" + | "university" + | "versionNumber" + | "volume" + | "websiteTitle" + | "websiteType"; diff --git a/prototypes/zotero-roam-mcp/src/components/App/index.tsx b/prototypes/zotero-roam-mcp/src/components/App/index.tsx new file mode 100644 index 0000000..7f851d8 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/App/index.tsx @@ -0,0 +1,322 @@ +/* istanbul ignore file */ +import { Component, FC, createContext, useContext, useMemo } from "react"; +import { HotkeyConfig, HotkeysTarget2, UseHotkeysOptions } from "@blueprintjs/core"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; + +import Dashboard from "Components/Dashboard"; +import ExtensionIcon from "Components/ExtensionIcon"; +import GraphWatcher from "Components/GraphWatcher"; +import Logger from "Components/Logger"; +import { RoamCitekeysProvider } from "Components/RoamCitekeysContext"; +import SearchPanel from "Components/SearchPanel"; +import { SettingsDialog, useOtherSettings, useRequestsSettings, useShortcutsSettings } from "Components/UserSettings"; + +import IDBDatabase from "@services/idb"; +import { Roam, addPaletteCommand, getCurrentCursorLocation, maybeReturnCursorToPlace, removePaletteCommand } from "@services/roam"; +import { createPersisterWithIDB, shouldQueryBePersisted, validateShortcuts } from "../../setup"; + +import { AsBoolean } from "Types/helpers"; +import { ExtensionContextValue, ExtensionStatusEnum, SettingsOther, SettingsShortcuts, UserRequests } from "Types/extension"; + + +const openSearchCommand = "zoteroRoam : Open Search Panel"; +const openDashboardCommand = "zoteroRoam : Open Dashboard"; + +const ExtensionContext = createContext(null); + +const useExtensionContext = () => { + const context = useContext(ExtensionContext); + + if (!context) { + throw new Error("No context provided"); + } + + return context; +}; + +// Cache time of 30 minutes prevents unbounded memory growth while maintaining +// good performance for typical usage sessions +const CACHE_TIME_MS = 1000 * 60 * 30; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + cacheTime: CACHE_TIME_MS, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchIntervalInBackground: true, + retryOnMount: false, + staleTime: 1000 * 60 + } + } +}); + + +type QCProviderProps = { + idbDatabase: IDBDatabase +} + +const QCProvider: FC = ({ children, idbDatabase }) => { + const [{ cacheEnabled }] = useOtherSettings(); + const persister = useMemo(() => createPersisterWithIDB(idbDatabase), [idbDatabase]); + const persisterProps = useMemo(() => ({ + onSuccess: () => { + window.zoteroRoam?.info?.({ + origin: "Database", + message: "Initialization complete" + }); + }, + persistOptions: { + buster: "v1.0", + dehydrateOptions: { + shouldDehydrateQuery: shouldQueryBePersisted + }, + maxAge: 1000 * 60 * 60 * 24 * 3, + persister + } + }), [persister]); + + const Provider = useMemo(() => cacheEnabled ? PersistQueryClientProvider : QueryClientProvider, [cacheEnabled]); + + return {children}; + +}; + + +type AppWrapperProps = { + extension: ExtensionContextValue, + extensionAPI?: Roam.ExtensionAPI, + idbDatabase: IDBDatabase +}; + +// https://stackoverflow.com/questions/63431873/using-multiple-context-in-a-class-component +const AppWrapper = (props: AppWrapperProps) => { + const [{ autoload }] = useOtherSettings(); + const [requests] = useRequestsSettings(); + const [shortcuts] = useShortcutsSettings(); + + // Only pass valid hotkey combos + // TODO: move validation step upstream + const sanitizedShortcuts = useMemo(() => validateShortcuts(shortcuts), [shortcuts]); + + return ; +}; + + +type AppProps = { + autoload: SettingsOther["autoload"], + extension: ExtensionContextValue, + extensionAPI?: Roam.ExtensionAPI, + idbDatabase: IDBDatabase, + requests: UserRequests, + shortcuts: SettingsShortcuts +}; + +type AppState = { + isDashboardOpen: boolean, + isLoggerOpen: boolean, + isSearchPanelOpen: boolean, + isSettingsPanelOpen: boolean, + lastCursorLocation: ReturnType, + status: ExtensionStatusEnum +}; + +class App extends Component { + constructor(props){ + super(props); + this.state = { + isDashboardOpen: false, + isLoggerOpen: false, + isSearchPanelOpen: false, + isSettingsPanelOpen: false, + lastCursorLocation: null, + status: ( + this.props.requests.dataRequests.length == 0 + ? ExtensionStatusEnum.DISABLED + : this.props.autoload + ? ExtensionStatusEnum.ON + : ExtensionStatusEnum.OFF + ) + }; + this.toggleExtension = this.toggleExtension.bind(this); + this.closeSearchPanel = this.closeSearchPanel.bind(this); + this.openSearchPanel = this.openSearchPanel.bind(this); + this.toggleSearchPanel = this.toggleSearchPanel.bind(this); + this.closeDashboard = this.closeDashboard.bind(this); + this.openDashboard = this.openDashboard.bind(this); + this.toggleDashboard = this.toggleDashboard.bind(this); + this.closeSettings = this.closeSettings.bind(this); + this.openSettings = this.openSettings.bind(this); + this.toggleSettings = this.toggleSettings.bind(this); + this.closeLogger = this.closeLogger.bind(this); + this.openLogger = this.openLogger.bind(this); + } + + componentDidMount(){ + addPaletteCommand(openSearchCommand, this.openSearchPanel, this.props.extensionAPI); + addPaletteCommand(openDashboardCommand, this.openDashboard, this.props.extensionAPI); + } + + componentDidUpdate(prevProps){ + if(this.props.requests.dataRequests.length == 0 && prevProps.requests.dataRequests.length > 0){ + this.setState((_prev) => { + queryClient.clear(); + return { + status: ExtensionStatusEnum.DISABLED + }; + }); + } else if(prevProps.requests.dataRequests.length == 0 && this.props.requests.dataRequests.length > 0){ + this.setState((_prev) => ({ + status: ExtensionStatusEnum.ON + })); + } + // In the case of a change in requests, the old requests should become inactive & be eventually cleared + } + + componentWillUnmount(){ + removePaletteCommand(openSearchCommand, this.props.extensionAPI); + removePaletteCommand(openDashboardCommand, this.props.extensionAPI); + } + + render() { + const { status, isDashboardOpen, isLoggerOpen, isSearchPanelOpen, isSettingsPanelOpen } = this.state; + const { extension, idbDatabase } = this.props; + + const hotkeys = Object.keys(this.shortcutsConfig) + .map(cmd => { + const combo = this.props.shortcuts[cmd] || ""; + if(combo !== ""){ + return { + allowInInput: true, + combo, + global: true, + ...this.shortcutsConfig[cmd] + }; + } else { + return false; + } + }).filter(AsBoolean); + + return ( + + + + + + {status == ExtensionStatusEnum.ON ? : null} + + + + + + + + + ); + } + + shortcutsConfig: Record> = { + "toggleDashboard": { + label: "Show/hide the dashboard", + onKeyDown: () => this.toggleDashboard() + }, + "toggleSearchPanel": { + label: "Show/hide the search panel", + onKeyDown: () => this.toggleSearchPanel() + }, + "toggleSettingsPanel": { + label: "Show/hide the settings panel", + onKeyDown: () => this.toggleSettings() + } + }; + + hotkeysOptions: UseHotkeysOptions = { + showDialogKeyCombo: "shift+Z+R" + }; + + toggleExtension() { + this.setState((prevState) => { + const { status } = prevState; + if (status == ExtensionStatusEnum.ON) { + // Aka, turning off the extension + queryClient.clear(); + } + return { + status: status == ExtensionStatusEnum.OFF ? ExtensionStatusEnum.ON : ExtensionStatusEnum.OFF + }; + }); + } + + closeSearchPanel() { + this.setState((_prev) => { + maybeReturnCursorToPlace(_prev.lastCursorLocation); + return { + isSearchPanelOpen: false, + lastCursorLocation: null + }; + }); + } + + openSearchPanel() { + this.setState((_prev) => ({ lastCursorLocation: getCurrentCursorLocation(), isSearchPanelOpen: true })); + } + + toggleSearchPanel() { + if (this.state.status == ExtensionStatusEnum.ON){ + this.setState((prev) => ({ isSearchPanelOpen: !prev.isSearchPanelOpen })); + } + } + + closeDashboard() { + this.setState((_prev) => ({ isDashboardOpen: false })); + } + + openDashboard() { + this.setState((_prev) => ({ isDashboardOpen: true })); + } + + toggleDashboard(){ + if (this.state.status == ExtensionStatusEnum.ON){ + this.setState((prev) => ({ isDashboardOpen: !prev.isDashboardOpen })); + } + } + + closeSettings() { + this.setState((_prev) => ({ isSettingsPanelOpen: false })); + } + + openSettings() { + this.setState((_prev) => ({ isSettingsPanelOpen: true })); + } + + toggleSettings() { + this.setState((prev) => ({ isSettingsPanelOpen: !prev.isSettingsPanelOpen })); + } + + closeLogger() { + this.setState((_prev) => ({ isLoggerOpen: false })); + } + + openLogger() { + this.setState((_prev) => ({ isLoggerOpen: true })); + } + +} + + +export { + AppWrapper, + ExtensionContext, + useExtensionContext, + queryClient +}; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/components/Autocomplete/_index.sass b/prototypes/zotero-roam-mcp/src/components/Autocomplete/_index.sass new file mode 100644 index 0000000..4bf0a6c --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/Autocomplete/_index.sass @@ -0,0 +1,28 @@ +@use "Styles/base" + + +.zr-tribute + @include base.neutralBox($border-width: 1px) + border-radius: 3px + margin-top: 8px + max-height: 300px + max-width: 450px + overflow-y: scroll + + ul + list-style-type: none + margin-bottom: 0px + padding: 0px + + li + display: flex + justify-content: space-between + font-size: var(--zr-text-small) + line-height: 1.5 + padding: 3px 8px + + &--item-details + flex: 0 1 100% + + &--selected + background: hsla(var(--zr-accent-1-hsl), 8%) diff --git a/prototypes/zotero-roam-mcp/src/components/Autocomplete/index.tsx b/prototypes/zotero-roam-mcp/src/components/Autocomplete/index.tsx new file mode 100644 index 0000000..cf7b9aa --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/Autocomplete/index.tsx @@ -0,0 +1,196 @@ +/* istanbul ignore file */ +import { memo, useCallback, useEffect, useMemo, useRef } from "react"; +import Tribute, { TributeCollection } from "tributejs"; + +import { useAutocompleteSettings, useRequestsSettings } from "Components/UserSettings"; + +import { useItems } from "@clients/zotero"; +import { Queries } from "@services/react-query"; +import { TributeJS } from "@services/tribute"; +import { escapeRegExp, formatItemReference } from "../../utils"; + +import { CustomClasses } from "../../constants"; +import { DataRequest, ZItemReferenceFormat } from "Types/extension"; +import { isZItemTop } from "Types/transforms"; +import "./_index.sass"; + + +type Option = { + display: string, + itemType: string, + key: string, + source: "zotero", + value: string +}; + +const LOOKUP_KEY: keyof Option = "display"; + +const tributeConfig: TributeCollection
} + multiline={true} + onClick={() => triggerMerge(el)} + text={el} + />))} + {zotero.map(el => ( + triggerMerge(el)} + text={el} + />))} + + + + ); +} + + +export default MergeAsOptions; diff --git a/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Stats/_index.sass b/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Stats/_index.sass new file mode 100644 index 0000000..d2901fd --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Stats/_index.sass @@ -0,0 +1,23 @@ +.zr-tagmanager--datalist [zr-role="list-stats"] + align-items: center + background: var(--zr-box-bg) + border: 1px var(--zr-border-co) solid + display: flex + flex-direction: column + margin: 10px auto + padding: 10px 15px + width: fit-content + + [zr-role="stats-text"] + flex: 0 0 100% + + [zr-role="stats-figures"] + display: flex + + [zr-role="stats-bar"] + flex: 0 0 auto + flex-direction: column + margin: 3px 10px + + .bp3-progress-bar + max-width: 100px diff --git a/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Stats/index.tsx b/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Stats/index.tsx new file mode 100644 index 0000000..2f5eb89 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Stats/index.tsx @@ -0,0 +1,42 @@ +import { memo } from "react"; +import { ProgressBar } from "@blueprintjs/core"; + +import { CustomClasses } from "../../../../constants"; +import { ZTagStats } from "Types/transforms"; +import "./_index.sass"; + + +type OwnProps = { + stats?: ZTagStats +}; + +const Stats = memo(function Stats({ stats }){ + if(!stats){ + return null; + } else { + const { nTags, nRoam, nAuto, nTotal } = stats; + const autoPct = Math.round(nAuto/nTags*100)/100; + const roamPct = Math.round(nRoam/nTotal*100)/100; + + return ( +
+ + Zotero has {nTags} tags, matched in {nTotal} groups + + + + Automatic : {autoPct*100}% + + + + In Roam : {roamPct*100}% + + + +
+ ); + } +}); + + +export default Stats; diff --git a/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Suggest.tsx b/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Suggest.tsx new file mode 100644 index 0000000..f4d0a26 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/Dashboard/TagManager/Suggest.tsx @@ -0,0 +1,111 @@ +import { useCallback, useMemo } from "react"; +import { Button, ButtonGroup, ButtonProps, IconName, Intent, Menu } from "@blueprintjs/core"; +import { Popover2 } from "@blueprintjs/popover2"; + +import ActionsMenu from "./ActionsMenu"; +import MergeAsOptions from "./MergeAsOptions"; + +import { useModifyTags } from "@clients/zotero"; +import { makeSuggestionFor } from "./utils"; + +import { CustomClasses } from "../../../constants"; +import { ZLibrary, ZTagEntry, ZTagSuggestionAuto, ZTagSuggestionManual } from "Types/transforms"; + + +type AutoMergeProps = { + library: ZLibrary, + suggestion: ZTagSuggestionAuto +}; + +function AutoMerge({ library, suggestion }: AutoMergeProps){ + const { recommend, use } = suggestion; + const { mutate, status } = useModifyTags(); + + const triggerMerge = useCallback(() => { + mutate({ + into: recommend, + library, + tags: [...use.roam, ...use.zotero] + }); + }, [library, mutate, recommend, use]); + + const buttonProps = useMemo>(() => { + if(status == "error"){ + return { + disabled: true, + icon: "warning" as IconName, + intent: Intent.DANGER + }; + } else if(status == "success"){ + return { + disabled: true, + icon: "tick" as IconName, + intent: Intent.SUCCESS, + text: "Merged" + }; + } else { + return { + icon: "many-to-one" as IconName + }; + } + }, [status]); + + return
+ + ); +} + + +type QueriesStatusListProps = { + queries: QueriesList +}; + +function QueriesStatusList(props: QueriesStatusListProps){ + const { queries } = props; + return ( +
    + {Object.keys(queries).map((key) => { + return ( +
  • + {key} + +
  • + ); + })} +
+ ); +} + + +type ExtensionIconProps = { + openDashboard: () => void, + openLogger: () => void, + openSearchPanel: () => void, + openSettingsPanel: () => void, + status: ExtensionStatusEnum, + toggleExtension: () => void +}; + +const ExtensionIcon = memo(function ExtensionIcon(props) { + const { openDashboard, openLogger, openSearchPanel, openSettingsPanel, status, toggleExtension } = props; + const [{ apiKeys, dataRequests, libraries }] = useRequestsSettings(); + + const queryOpts = useMemo>(() => ({ + enabled: status == ExtensionStatusEnum.ON, + notifyOnChangeProps: ["dataUpdatedAt", "status", "isFetching"] + }), [status]); + + const itemQueries = useItems(dataRequests, queryOpts); + const permissionQueries = usePermissions(apiKeys, queryOpts); + const tagQueries = useTags(libraries, queryOpts); + const collectionQueries = useCollections(libraries, queryOpts); + + const isCurrentlyLoading = [...permissionQueries, ...itemQueries, ...tagQueries, ...collectionQueries].some(q => q.isLoading); + const isCurrentlyFetching = [...permissionQueries, ...itemQueries, ...tagQueries, ...collectionQueries].some(q => q.fetchStatus == "fetching"); + const hasLoadingError = [...permissionQueries, ...itemQueries, ...tagQueries, ...collectionQueries].some(q => q.isLoadingError); + const allowContext = itemQueries.some(q => q.data); + + const data_status = useMemo(() => hasLoadingError ? "error" : ((isCurrentlyLoading && isCurrentlyFetching) ? "loading" : "ready"), [isCurrentlyFetching, isCurrentlyLoading, hasLoadingError]); + const button_icon = useMemo(() => status == ExtensionStatusEnum.DISABLED ? "warning-sign" : hasLoadingError ? "issue" : "manual", [hasLoadingError, status]); + + const queries = useMemo(() => { + return { + "Permissions": permissionQueries, + "Collections": collectionQueries, + "Tags": tagQueries, + "Items": itemQueries + }; + }, [collectionQueries, itemQueries, permissionQueries, tagQueries]); + + const tooltipContent = useMemo(() => { + return <> + Status : {status} + {status == ExtensionStatusEnum.DISABLED && + + + + + + + ); +} + + +type ViewNotesProps = { + item: ZItemTop, + notes: (ZItemAnnotation | ZItemNote)[], + pageUID: string | false +}; + +function ViewNotes({ item, notes, pageUID }: ViewNotesProps){ + const [isPanelOpen, { on: openPanel, off: closePanel }] = useBool(false); + + return <> + + + + + ; +} + + +type CitekeyMenuProps = { + item: ZItemTop, + itemList: ZLibraryContents +}; + +const CitekeyMenu = memo(function CitekeyMenu({ item, itemList }) { + const [annotationsSettings] = useAnnotationsSettings(); + const [metadataSettings] = useMetadataSettings(); + const [notesSettings] = useNotesSettings(); + const [{ defaults }] = usePageMenuSettings(); + const [sciteBadgeSettings] = useSciteSettings(); + const [typemap] = useTypemapSettings(); + + const [roamCitekeys/*, updateCitekeys */] = useRoamCitekeys(); + + const doi = parseDOI(item.data.DOI); + const pageUID = findRoamPage("@" + item.key); + + const children = useMemo(() => { + const itemKey = item.data.key; + const location = item.library.type + "s/" + item.library.id; + const { pdfs, notes } = itemList; + + return identifyChildren(itemKey, location, { pdfs: pdfs, notes: notes }); + }, [itemList, item]); + + const doiHeader = useMemo(() => { + return doi + ? {doi} + : null; + }, [doi]); + + const importMetadata = useCallback(async() => { + const { pdfs, notes } = children; + return await importItemMetadata({ item, pdfs, notes }, pageUID, metadataSettings, typemap, notesSettings, annotationsSettings); + }, [annotationsSettings, children, item, metadataSettings, notesSettings, pageUID, typemap]); + + const pdfLinks = useMemo(() => { + if(children.pdfs.length == 0 || !defaults.includes("pdfLinks")) { + return null; + } else { + return ( + children.pdfs.map(pdf => { + return ( + + ); + }) + ); + } + }, [defaults, children.pdfs]); + + const notesButton = useMemo(() => { + if(children.notes.length == 0 || !defaults.includes("importNotes")){ + return null; + } else { + return ; + } + }, [defaults, children.notes, item, pageUID]); + + const open_zotero = useMemo(() => { + return ( + <> + {defaults.includes("openZoteroLocal") + ? + : null} + {defaults.includes("openZoteroWeb") + ? + : null} + + ); + },[defaults, item]); + + const sciteBadge = useMemo(() => { + return doi && defaults.includes("sciteBadge") ? : null; + }, [defaults, doi, sciteBadgeSettings]); + + const ext_links = useMemo(() => { + const connectedPapersLink = defaults.includes("connectedPapers") + ? + : null; + const semanticLink = doi && defaults.includes("semanticScholar") + ? + : null; + const googleScholarLink = defaults.includes("googleScholar") + ? + : null; + + return ( + <> + {connectedPapersLink} + {semanticLink} + {googleScholarLink} + + ); + }, [defaults, doi, item.data.title]); + + const relatedBar = useMemo(() => { + return doi && defaults.includes("citingPapers") + ? + : null; + }, [defaults, doi, item.key, item.meta.parsedDate, itemList]); + + const clean_item = useMemo(() => { + return cleanLibraryItem(item, children.pdfs, children.notes, roamCitekeys); + }, [children, item, roamCitekeys]); + + return ( + + {doiHeader} + +
+ + {defaults.includes("addMetadata") + ? + : null} + {notesButton} + {defaults.includes("viewItemInfo") + ? + : null} + {open_zotero} + {pdfLinks} + {ext_links} + + {sciteBadge} +
+ {relatedBar} +
+
+ ); +}); + + +export default CitekeyMenu; +export { Backlinks }; diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/DNPMenu.tsx b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/DNPMenu.tsx new file mode 100644 index 0000000..d28f35f --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/DNPMenu.tsx @@ -0,0 +1,45 @@ +import { useMemo } from "react"; +import { Button } from "@blueprintjs/core"; + +import { useBool } from "@hooks"; + +import RelatedPanel from "../RelatedPanel"; +import { ShowTypeRelated } from "../types"; + +import { pluralize } from "../../../utils"; + +import { SCleanRelatedItem } from "Types/transforms"; + + +type DNPMenuProps = { + added: SCleanRelatedItem[], + title: string +}; + + +function DNPMenu({ added, title }: DNPMenuProps){ + const [isDialogOpen, { on: openDialog, off: closeDialog }] = useBool(false); + + const hasAddedItems = added.length > 0; + + const buttonLabel = useMemo(() => { + return pluralize(added.length, "item", " added"); + }, [added.length]); + + return <> + {hasAddedItems + ? <> + + + + : null} + ; +} + + +export default DNPMenu; diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/TagMenu.tsx b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/TagMenu.tsx new file mode 100644 index 0000000..3ec005c --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/TagMenu.tsx @@ -0,0 +1,76 @@ +import { useCallback, useMemo, useState } from "react"; +import { Button } from "@blueprintjs/core"; + +import { useBool } from "@hooks"; + +import RelatedPanel from "../RelatedPanel"; +import { ShowPropertiesRelated, ShowTypeRelated } from "../types"; + +import { pluralize } from "../../../utils"; +import { SCleanRelatedItem } from "Types/transforms"; + + +export type TagMenuProps = { + inAbstract: SCleanRelatedItem[], + tag: string, + tagged: SCleanRelatedItem[] +}; + + +function TagMenu(props: TagMenuProps){ + const { inAbstract = [], tag, tagged = [] } = props; + const [isDialogOpen, { on: openDialog, off: closeDialog }] = useBool(false); + const [isShowing, setShowing] = useState(); + + const hasTaggedItems = tagged.length > 0; + const hasAbstracts = inAbstract.length > 0; + + const fallbackShowProps = useMemo(() => ({ + title: tag, + type: tagged.length > 0 + ? ShowTypeRelated.WITH_TAG + : ShowTypeRelated.WITH_ABSTRACT + }), [tag, tagged.length]); + + const showAbstracts = useCallback(() => { + openDialog(); + setShowing({ + title: tag, + type: ShowTypeRelated.WITH_ABSTRACT + }); + }, [tag, openDialog]); + + const showTagged = useCallback(() => { + openDialog(); + setShowing({ + title: tag, + type: ShowTypeRelated.WITH_TAG + }); + }, [tag, openDialog]); + + const abstractsLabel = useMemo(() => { + return pluralize(inAbstract.length, "abstract"); + }, [inAbstract.length]); + + const taggedLabel = useMemo(() => { + return pluralize(tagged.length, "tagged item"); + }, [tagged.length]); + + return ( + <> + {hasTaggedItems && + } + {hasAbstracts && + } + {(hasTaggedItems || hasAbstracts) && + } + + ); +} + + +export default TagMenu; diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/_index.sass b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/_index.sass new file mode 100644 index 0000000..d4e7dcd --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/_index.sass @@ -0,0 +1,119 @@ +.zr-page-menu--citekey + max-width: 850px + + +.zr-page-menu--dnp, .zr-page-menu--tag + opacity: 0.6 + position: absolute + right: 10px + top: 40px + + +/* CitekeyMenu */ + +.zr-citekey-doi + display: block + letter-spacing: 0.3mm + margin: 10px + + +.zr-citekey-menu + + @media (max-width: 700px) + .bp3-button + font-size: 0.85em + + &.bp3-card + justify-content: space-between + border-width: 0px + padding: 5px + border-radius: 5px + background-color: var(--zr-box-bg) + box-shadow: none + + .bp3-button-group + .bp3-button + &, &:hover, .bp3-icon + color: var(--zr-text-co) + + &.bp3-disabled + color: var(--zr-text-co) + opacity: 0.8 + + .zotero-roam-page-menu-citations + display: flex + padding: 5px + flex-wrap: wrap + padding-bottom: 0px + border-top: 1px var(--zr-border-co) solid + + button[class*='zr-page-menu-show--'] + flex: 1 0 33% + + &--header + display: flex + + &--actions.bp3-button-group + flex-wrap: wrap + flex: 1 1 auto + + .bp3-button + flex: 0 1 auto + + [zr-role="pdf-link"] + font-weight: 600 + + &--backlinks + display: flex + flex-direction: column + margin: 10px 0px + width: 100% + + .zr-backlinks-divider--tag.bp3-tag.bp3-minimal + color: var(--zr-text-co) + + & + .bp3-divider + border-color: var(--zr-border-co) + + +.zr-backlink-item + align-items: flex-start + display: flex + padding: 0px + + &--year + flex: 0 0 auto + + &--info + line-height: normal + flex: 1 2 90% + + [zr-role="item-title"] + color: var(--zr-text-co) + display: block + font-weight: 500 + + &--year, &--info + font-size: 0.9em + padding: 0px 0px 6px 10px + + &--year, &--info > *:not([zr-role="item-title"]) + line-height: 2em + + &--state + flex: 1 0 auto + overflow-wrap: anywhere + + @media (max-width: 700px) + flex: 1 2 auto + + &[data-backlink-type="reference"] .zr-backlink-item--year + color: var(--zr-accent-1) + + &[data-backlink-type="citation"] .zr-backlink-item--year + color: var(--zr-accent-2) + + +.zr-auxiliary-dialog--view-item-information.bp3-dialog + --zr-dialog-gutter: 2.5% + --zr-dialog-margin: 15vw diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/helpers.test.ts b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/helpers.test.ts new file mode 100644 index 0000000..c00f4b4 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/helpers.test.ts @@ -0,0 +1,195 @@ +import { cleanSemantic, compareItemsByYear, getAuthorLastName, makeAuthorsSummary } from "./helpers"; +import { semantics } from "Mocks"; + + +describe("cleanSemantic", () => { + it("simplifies SemanticScholar item metadata", () => { + const { citations, references } = Object.values(semantics)[0]; + expect(cleanSemantic({ items: [], notes: [], pdfs: [] }, { citations, references }, new Map())) + .toEqual({ + citations: [ + { + authors: "Knox et al.", + authorsLastNames: ["Knox", "Esteban", "Hernandez", "Fleming", "Safaeinilli", "Brewster"], + authorsString: "Knox Esteban Hernandez Fleming Safaeinilli Brewster", + doi: "10.1136/bmjoq-2021-001807", + intent: [], + isInfluential: false, + inGraph: false, + inLibrary: false, + links: { + "semantic-scholar": "https://www.semanticscholar.org/paper/c3a1a1c6875a03b4d32fd0a36f54840aa6ba4fb7", + "connected-papers": "https://www.connectedpapers.com/api/redirect/doi/10.1136/bmjoq-2021-001807", + "google-scholar": "https://scholar.google.com/scholar?q=10.1136/bmjoq-2021-001807" + }, + meta: "BMJ open quality", + title: "Defining case management success: a qualitative study of case manager perspectives from a large-scale health and social needs support program", + url: "https://www.semanticscholar.org/paper/c3a1a1c6875a03b4d32fd0a36f54840aa6ba4fb7", + year: "2022", + _type: "citing", + _multiField: "Knox Esteban Hernandez Fleming Safaeinilli Brewster 2022 Defining case management success: a qualitative study of case manager perspectives from a large-scale health and social needs support program" + } + ], + references: [ + { + authors: "Solberg", + authorsLastNames: ["Solberg"], + authorsString: "Solberg", + doi: "10.1370/afm.1918", + intent: [], + isInfluential: false, + inGraph: false, + inLibrary: false, + links: { + "semantic-scholar": "https://www.semanticscholar.org/paper/447a7aa39d71dda859564dc12326a43b66488c76", + "connected-papers": "https://www.connectedpapers.com/api/redirect/doi/10.1370/afm.1918", + "google-scholar": "https://scholar.google.com/scholar?q=10.1370/afm.1918" + }, + meta: "The Annals of Family Medicine", + title: "Theory vs Practice: Should Primary Care Practice Take on Social Determinants of Health Now? No.", + url: "https://www.semanticscholar.org/paper/447a7aa39d71dda859564dc12326a43b66488c76", + year: "2016", + _type: "cited", + _multiField: "Solberg 2016 Theory vs Practice: Should Primary Care Practice Take on Social Determinants of Health Now? No." + }, + { + authors: "Fagnan", + authorsLastNames: ["Fagnan"], + authorsString: "Fagnan", + doi: "10.3122/jabfm.2017.01.160355", + intent: [], + isInfluential: false, + inGraph: false, + inLibrary: false, + links: { + "semantic-scholar": "https://www.semanticscholar.org/paper/da834ae1504204c6e14e4bcf8811c4ca250d4ada", + "connected-papers": "https://www.connectedpapers.com/api/redirect/doi/10.3122/jabfm.2017.01.160355", + "google-scholar": "https://scholar.google.com/scholar?q=10.3122/jabfm.2017.01.160355" + }, + meta: "The Journal of the American Board of Family Medicine", + title: "Moving Upstream—Health Extension and Primary Care", + url: "https://www.semanticscholar.org/paper/da834ae1504204c6e14e4bcf8811c4ca250d4ada", + year: "2017", + _type: "cited", + _multiField: "Fagnan 2017 Moving Upstream—Health Extension and Primary Care" + }, + { + authors: "Andermann", + authorsLastNames: ["Andermann"], + authorsString: "Andermann", + doi: "10.1186/s40985-018-0094-7", + intent: [], + isInfluential: false, + inGraph: false, + inLibrary: false, + links: { + "semantic-scholar": "https://www.semanticscholar.org/paper/f090bb04877612278bd614b951a8d753eb6b8b5b", + "connected-papers": "https://www.connectedpapers.com/api/redirect/doi/10.1186/s40985-018-0094-7", + "google-scholar": "https://scholar.google.com/scholar?q=10.1186/s40985-018-0094-7" + }, + meta: "Public Health Reviews", + title: "Screening for social determinants of health in clinical care: moving from the margins to the mainstream", + url: "https://www.semanticscholar.org/paper/f090bb04877612278bd614b951a8d753eb6b8b5b", + year: "2018", + _type: "cited", + _multiField: "Andermann 2018 Screening for social determinants of health in clinical care: moving from the margins to the mainstream" + }, + { + authors: "Doe", + authorsLastNames: ["Doe"], + authorsString: "Doe", + doi: false, + intent: ["background"], + isInfluential: true, + inGraph: false, + inLibrary: false, + links: { + "arxiv": "https://arxiv.org/abs/arXiv:1501.00001", + "semantic-scholar": "https://www.semanticscholar.org/paper/__some_paper_id__", + "connected-papers": "https://www.connectedpapers.com/search?q=" + encodeURIComponent("DOI-less paper: an influential work"), + "google-scholar": "https://scholar.google.com/scholar?q=" + encodeURIComponent("DOI-less paper: an influential work") + }, + meta: "Nature", + title: "DOI-less paper: an influential work", + url: "", + year: "2013", + _type: "cited", + _multiField: "Doe 2013 DOI-less paper: an influential work" + } + ], + backlinks: [] + }); + }); +}); + +describe("compareItemsByYear", () => { + it("sorts Zotero items by publication year & creators", () => { + const a = { meta: { creatorSummary: "Smith et al.", parsedDate: "" } }; + const b = { meta: { creatorSummary: "Scott et al.", parsedDate: "" } }; + const c = { meta: { creatorSummary: "Smith et al.", parsedDate: "2022-01-01" } }; + const d = { meta: { creatorSummary: "Tikki and Noald", parsedDate: "2021-01-01" } }; + const e = { meta: { creatorSummary: "Chen and Talmanes", parsedDate: "2021" } }; + + expect([ + [a, b].sort(compareItemsByYear), + [a, c].sort(compareItemsByYear), + [c, a].sort(compareItemsByYear), + [b, c].sort(compareItemsByYear), + [c, d].sort(compareItemsByYear), + [d, c].sort(compareItemsByYear), + [d, e].sort(compareItemsByYear) + ]) + .toEqual([ + [b, a], + [c, a], + [c, a], + [c, b], + [d, c], + [d, c], + [e, d] + ]); + }); +}); + +describe("getAuthorLastName", () => { + it("extracts from single names", () => { + expect(getAuthorLastName("WHO")).toBe("WHO"); + }); + it("extracts from simple names", () => { + expect(getAuthorLastName("Serena Chen")).toBe("Chen"); + }); + + it("extracts from names with hyphen", () => { + expect(getAuthorLastName("Bo-yeong Kim")).toBe("Kim"); + }); + + it("extracts from names with particle", () => { + expect(getAuthorLastName("Tomas de Koon")).toBe("de Koon"); + }); + + it("extracts from names with middle name", () => { + expect(getAuthorLastName("Kelsey S. Dickson")).toBe("Dickson"); + }); +}); + +describe("makeAuthorsSummary", () => { + it("returns an empty string when given an empty input", () => { + expect(makeAuthorsSummary([])).toBe(""); + }); + + it("formats correctly with 1 author", () => { + expect(makeAuthorsSummary(["Dickson"])).toBe("Dickson"); + }); + + it("formats correctly with 2 authors", () => { + expect(makeAuthorsSummary(["Dickson", "Sklar"])).toBe("Dickson & Sklar"); + }); + + it("formats correctly with 3 authors", () => { + expect(makeAuthorsSummary(["Dickson", "Sklar", "Chen"])).toBe("Dickson, Sklar & Chen"); + }); + + it("formats correctly with 4+ authors", () => { + expect(makeAuthorsSummary(["Dickson", "Chen", "de Koon", "Sklar"])).toBe("Dickson et al."); + }); +}); \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/helpers.ts b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/helpers.ts new file mode 100644 index 0000000..525ad21 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/helpers.ts @@ -0,0 +1,185 @@ +import { SemanticScholarAPI } from "@clients/semantic"; + +import { identifyChildren, parseDOI } from "../../../utils"; + +import { AsBoolean } from "Types/helpers"; +import { isSBacklink, RCitekeyPages, SCleanItem, SEnrichedItem, SEnrichedItemCitation, SEnrichedItemReference, SEnrichedItemTypeEnum, SRelatedEntries, ZItemTop, ZLibraryContents } from "Types/transforms"; + + +/** Formats a list of Semantic Scholar entries for display */ +function cleanSemantic( + datastore: ZLibraryContents, + semantic: Pick, + roamCitekeys: RCitekeyPages +): SRelatedEntries { + const { items = [], pdfs = [], notes = [] } = datastore; + const itemsWithDOIs = items.filter(it => it.data.DOI); + // * Note: DOIs from the Semantic Scholar queries are sanitized at fetch + const { citations = [], references = [] } = semantic; + + const clean_citations: SEnrichedItemCitation[] = citations.map((cit) => { + const cleanProps = matchSemanticEntry(cit, { items: itemsWithDOIs, pdfs, notes }, roamCitekeys); + return { + ...cleanProps, + _type: SEnrichedItemTypeEnum.CITING + }; + }); + + const clean_references: SEnrichedItemReference[] = references.map((ref) => { + const cleanProps = matchSemanticEntry(ref, { items: itemsWithDOIs, pdfs, notes }, roamCitekeys); + return { + ...cleanProps, + _type: SEnrichedItemTypeEnum.CITED + }; + }); + + return { + citations: clean_citations, + references: clean_references, + backlinks: [...clean_references, ...clean_citations].filter(isSBacklink) + }; +} + + +/** Formats the metadata of a Semantic Scholar entry */ +function cleanSemanticItem(item: SemanticScholarAPI.RelatedPaper): SCleanItem { + const clean_item: SCleanItem = { + authors: "", + authorsLastNames: [], + authorsString: "", + //* Note: SemanticScholar DOIs are sanitized on fetch + doi: item.doi, + intent: item.intent, + isInfluential: item.isInfluential, + links: {}, + meta: item.venue.split(/ ?:/)[0], // If the publication has a colon, only take the portion that precedes it + title: item.title, + url: item.url || "", + year: item.year ? item.year.toString() : "", + _multiField: "" + }; + + // Parse authors data + clean_item.authorsLastNames = item.authors.map(a => getAuthorLastName(a.name)); + clean_item.authorsString = clean_item.authorsLastNames.join(" "); + clean_item.authors = makeAuthorsSummary(clean_item.authorsLastNames); + + // Parse external links + if (item.paperId) { + clean_item.links["semantic-scholar"] = `https://www.semanticscholar.org/paper/${item.paperId}`; + } + if (item.arxivId) { + clean_item.links.arxiv = `https://arxiv.org/abs/${item.arxivId}`; + } + if (item.doi || item.title) { + clean_item.links["connected-papers"] = "https://www.connectedpapers.com/" + (item.doi ? "api/redirect/doi/" + item.doi : "search?q=" + encodeURIComponent(item.title)); + clean_item.links["google-scholar"] = "https://scholar.google.com/scholar?q=" + (item.doi || encodeURIComponent(item.title)); + } + + // Set multifield property for search + clean_item._multiField = [ + clean_item.authorsString, + clean_item.year, + clean_item.title + ].filter(AsBoolean).join(" "); + + return clean_item; +} + + +/** Compares two Zotero items by publication year then alphabetically, to determine sort order + * @returns The comparison outcome + */ +function compareItemsByYear(a: ZItemTop, b: ZItemTop): (-1 | 1) { + if (!a.meta.parsedDate) { + if (!b.meta.parsedDate) { + return a.meta.creatorSummary < b.meta.creatorSummary ? -1 : 1; + } else { + return 1; + } + } else { + if (!b.meta.parsedDate) { + return -1; + } else { + const date_diff = new Date(a.meta.parsedDate).getUTCFullYear() - new Date(b.meta.parsedDate).getUTCFullYear(); + if (date_diff < 0) { + return -1; + } else if (date_diff == 0) { + return a.meta.creatorSummary < b.meta.creatorSummary ? -1 : 1; + } else { + return 1; + } + } + } +} + + +/** Extracts an author's last name */ +function getAuthorLastName(name: string): string { + const components = name.replaceAll(".", " ").split(" ").filter(AsBoolean); + if (components.length == 1) { + return components[0]; + } else { + return components.slice(1).filter(c => c.length > 1).join(" "); + } +} + + +/** Formats authoring metadata */ +function makeAuthorsSummary(names: string[]): string { + switch (names.length) { + case 0: + return ""; + case 1: + return names[0]; + case 2: + return names[0] + " & " + names[1]; + case 3: + return names[0] + ", " + names[1] + " & " + names[2]; + default: + return names[0] + " et al."; + } +} + + +/** Matches a clean Semantic Scholar entry to Zotero and Roam data */ +function matchSemanticEntry( + semanticItem: SemanticScholarAPI.RelatedPaper, + datastore: Partial, + roamCitekeys: RCitekeyPages +): SEnrichedItem { + const { items = [], pdfs = [], notes = [] } = datastore; + const cleanItem = cleanSemanticItem(semanticItem); + if (!cleanItem.doi) { + return { + ...cleanItem, + inGraph: false, + inLibrary: false + }; + } else { + const libItem = items.find(it => parseDOI(it.data.DOI) == cleanItem.doi); + if (!libItem) { + return { + ...cleanItem, + inGraph: false, + inLibrary: false + }; + } else { + const itemKey = libItem.data.key; + const location = libItem.library.type + "s/" + libItem.library.id; + const children = identifyChildren(itemKey, location, { pdfs: pdfs, notes: notes }); + + return { + ...cleanItem, + inGraph: roamCitekeys.get("@" + libItem.key) || false, + inLibrary: { + children, + raw: libItem + } + }; + } + } +} + + +export { cleanSemantic, cleanSemanticItem, compareItemsByYear, getAuthorLastName, makeAuthorsSummary }; \ No newline at end of file diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/index.tsx b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/index.tsx new file mode 100644 index 0000000..93c1ce7 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/index.tsx @@ -0,0 +1,199 @@ +import { createPortal } from "react-dom"; +import { useMemo } from "react"; + +import { useRoamCitekeys } from "Components/RoamCitekeysContext"; +import { usePageMenuSettings, useRequestsSettings } from "Components/UserSettings"; +import CitekeyMenu from "./CitekeyMenu"; +import DNPMenu from "./DNPMenu"; +import TagMenu from "./TagMenu"; + +import { useItems } from "@clients/zotero"; + +import { categorizeLibraryItems } from "../../../utils"; +import { cleanRelatedItem } from "../helpers"; + +import { SCleanRelatedItem } from "Types/transforms"; +import "./_index.sass"; + + +function CitekeyMenuFactory({ menus }: { menus: Element[] }){ + const [{ trigger }] = usePageMenuSettings(); + const [{ dataRequests }] = useRequestsSettings(); + + const itemQueries = useItems(dataRequests, { + select: (datastore) => datastore.data, + notifyOnChangeProps: ["data"] + }); + + const data = useMemo(() => itemQueries.map(q => q.data || []).flat(1), [itemQueries]); + const itemList = useMemo(() => categorizeLibraryItems(data), [data]); + + const citekeyItems = useMemo(() => itemList.items.filter(it => it.has_citekey), [itemList]); + const citekeyMenus = useMemo(() => { + if (!citekeyItems) { + return null; + } else { + return menus + .filter(_menu => { + if(trigger.constructor === Boolean){ + return trigger; + } else { + // If trigger isn't a Boolean, set default behavior + // i.e citekey menus are always shown + return true; + } + }) + .map(menu => { + const item = citekeyItems.find(it => it.key == menu.getAttribute("data-citekey")); + return { div: menu, item }; + }) + .map((menu, i) => { + if (menu.item) { + const { item, div } = menu; + return ( + createPortal(, div) + ); + } + }); + } + }, [citekeyItems, itemList, menus, trigger]); + + return <>{citekeyMenus}; +} + + +function DNPMenuFactory({ menus }: { menus: Element[] }){ + const [{ trigger }] = usePageMenuSettings(); + const [{ dataRequests }] = useRequestsSettings(); + const [roamCitekeys/*, updateCitekeys */] = useRoamCitekeys(); + + const itemQueries = useItems(dataRequests, { + notifyOnChangeProps: ["data"], + select: (datastore) => datastore.data + }); + + const data = itemQueries.map(q => q.data || []).flat(1); + const itemList = useMemo(() => categorizeLibraryItems(data), [data]); + + const dnpPortals = useMemo(() => { + const { items, pdfs, notes } = itemList; + + if(!items){ + return null; + } else { + return menus + .filter(_menu => { + if(typeof(trigger) == "boolean"){ + return trigger; + } else { + // If trigger isn't a Boolean, set default behavior + // i.e DNP menus are always shown + return true; + } + }) + .map(menu => { + const title = menu.getAttribute("data-title")!; + + // e.g. [2023,5,6] is June 6th, 2023 + // For the month to be parsed correctly, the array has to be spread - otherwise it will be off by 1 + const date_as_array = JSON.parse(menu.getAttribute("data-dnp-date")!) as [number, number, number]; + const dnp_date = new Date(...date_as_array).toDateString(); + + const added = items + .filter(it => new Date(it.data.dateAdded).toDateString() == dnp_date) + .map(it => cleanRelatedItem(it, { pdfs, notes }, roamCitekeys)); + return { div: menu, added, title }; + }) + .filter(menu => menu.added) + .map((menu, i) => { + const { added, div, title } = menu; + return ( + createPortal(, div) + ); + }); + } + }, [itemList, menus, roamCitekeys, trigger]); + + return <>{dnpPortals}; +} + + +function TagMenuFactory({ menus }: { menus: Element[] }){ + const [{ trigger }] = usePageMenuSettings(); + const [{ dataRequests }] = useRequestsSettings(); + const [roamCitekeys/*, updateCitekeys */] = useRoamCitekeys(); + + const itemQueries = useItems(dataRequests, { + notifyOnChangeProps: ["data"], + select: (datastore) => datastore.data + }); + + const data = itemQueries.map(q => q.data || []).flat(1); + const itemList = useMemo(() => categorizeLibraryItems(data), [data]); + + // Select to reduce dataset size : + // - for tag matching, only top-level items that have any tags + // - for abstract matching, only items that have an abstract + const with_tags_or_abstract = useMemo(() => { + return itemList.items + .filter(it => it.data.abstractNote || it.data.tags.length > 0) + .map(it => { + return { + itemData: it, + abstract: it.data.abstractNote || "", + tagList: it.data.tags.map(t => t.tag) + }; + }); + }, [itemList.items]); + + const tagPortals = useMemo(() => { + const { items, pdfs, notes } = itemList; + if(!items){ + return null; + } else { + return menus + .filter(menu => { + if(typeof(trigger) == "boolean"){ + return trigger; + } else { + // If trigger isn't a Boolean, set default behavior + // i.e page menus are shown only if the page title is > 5 characters + const title = menu.getAttribute("data-title")!; + return title.length > 5; + } + }) + .map(menu => { + const title = menu.getAttribute("data-title")!; + const results = with_tags_or_abstract.reduce<{ with_tags: SCleanRelatedItem[], with_abstract: SCleanRelatedItem[] }>((obj, item) => { + if(item.abstract.includes(title)){ + obj.with_abstract.push(cleanRelatedItem(item.itemData, { pdfs, notes }, roamCitekeys)); + } + if(item.tagList.includes(title)){ + obj.with_tags.push(cleanRelatedItem(item.itemData, { pdfs, notes }, roamCitekeys)); + } + return obj; + }, { with_tags: [], with_abstract: [] }); + + return { div: menu, tag: title, ...results }; + }) + .filter(menu => menu.with_tags.length > 0 || menu.with_abstract.length > 0) + .map((menu,i) => { + const { with_tags, with_abstract, div, tag } = menu; + return ( + createPortal(, div) + ); + }); + } + }, [itemList, menus, roamCitekeys, trigger, with_tags_or_abstract]); + + return <>{tagPortals}; +} + + +export { + CitekeyMenuFactory, + DNPMenuFactory, + TagMenuFactory +}; diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/utils.test.tsx b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/utils.test.tsx new file mode 100644 index 0000000..381fdc3 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/utils.test.tsx @@ -0,0 +1,137 @@ +import { act, render } from "@testing-library/react"; +import { + CitekeyPageValid, + CitekeyPageInvalid, + DnpLogWithItems, + DnpLogWithoutItems, + DnpPageWithItems, + DnpPageWithoutItems, + DnpPreviewWithItems, + NormalPageWithoutTaggingContent, + NormalPageWithTaggingContent +} from "../fixtures"; + +import { hasNodeListChanged } from "../helpers"; +import { addPageMenus, findPageMenus } from "./utils"; +import { menuClasses } from "../classes"; + + +// Necessary since jsdom does not support innerText +// It shouldn't give discrepant results here +// https://github.com/jsdom/jsdom/issues/1245#issuecomment-763535573 +beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, "innerText", { + get() { + return this.textContent; + } + }); +}); + + +describe("Citekey menu containers are inserted", () => { + const cases = [ + [CitekeyPageValid, 1, "blochImplementingSocialInterventions2021"], + [CitekeyPageInvalid, 1, "nonExistentCitekey"] + ] as const; + + test.each(cases)( + "%#", + (Component, expected_menus_count, expected_citekey) => { + const { container } = render(); + + expect(findPageMenus()).toMatchObject({ + citekeyMenus: [], + dnpMenus: [], + tagMenus: [] + }); + + act(() => addPageMenus()); + + const allMenus = findPageMenus(); + + expect(hasNodeListChanged( + allMenus.citekeyMenus, + container.querySelectorAll(`[class=${menuClasses.citekey}]`) + )).toBe(false); + + expect(allMenus.citekeyMenus).toHaveLength(expected_menus_count); + if (expected_citekey) { + expect(allMenus.citekeyMenus[0].getAttribute("data-citekey")).toBe(expected_citekey); + } + } + ); +}); + + +describe("DNP menu containers are inserted", () => { + const cases = [ + [DnpLogWithItems, 1, "[2021,10,12]"], + [DnpPageWithItems, 1, "[2021,5,19]"], + [DnpLogWithoutItems, 1, "[1999,3,6]"], + [DnpPageWithoutItems, 1, "[1999,3,6]"], + [DnpPreviewWithItems, 0, null] + ] as const; + + test.each(cases)( + "%#", + (Component, expected_menus_count, expected_dnp_date) => { + const { container } = render(); + + expect(findPageMenus()).toMatchObject({ + citekeyMenus: [], + dnpMenus: [], + tagMenus: [] + }); + + act(() => addPageMenus()); + + const allMenus = findPageMenus(); + + expect(hasNodeListChanged( + allMenus.dnpMenus, + container.querySelectorAll(`[class=${menuClasses.dnp}]`) + )).toBe(false); + + expect(allMenus.dnpMenus).toHaveLength(expected_menus_count); + if (expected_dnp_date) { + expect(allMenus.dnpMenus[0].getAttribute("data-dnp-date")).toBe(expected_dnp_date); + } + + } + ); +}); + + +describe("Tag menu containers are inserted", () => { + const cases = [ + [NormalPageWithoutTaggingContent, 1, "September"], + [NormalPageWithTaggingContent, 1, "housing"] + ] as const; + + test.each(cases)( + "%#", + (Component, expected_menus_count, expected_title) => { + const { container } = render(); + + expect(findPageMenus()).toMatchObject({ + citekeyMenus: [], + dnpMenus: [], + tagMenus: [] + }); + + act(() => addPageMenus()); + + const allMenus = findPageMenus(); + + expect(hasNodeListChanged( + allMenus.tagMenus, + container.querySelectorAll(`[class=${menuClasses.tag}]`) + )).toBe(false); + + expect(allMenus.tagMenus).toHaveLength(expected_menus_count); + if (expected_title) { + expect(allMenus.tagMenus[0].getAttribute("data-title")).toBe(expected_title); + } + } + ); +}); diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/utils.ts b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/utils.ts new file mode 100644 index 0000000..075680c --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/Menus/utils.ts @@ -0,0 +1,49 @@ +import { findRoamPage, readDNP } from "@services/roam"; +import { menuClasses, menuPrefix } from "../classes"; + + +const dnpRegex = new RegExp(/(.+) ([0-9]+).{2}, ([0-9]{4})/); + +const addPageMenus = () => { + const newPages = Array.from(document.querySelectorAll("h1.rm-title-display")) + .filter(page => !(page.parentElement?.querySelector(`[class*=${menuPrefix}]`))); + for(const page of newPages) { + const title = page.querySelector("span")?.innerText || page.innerText; + const pageUID = findRoamPage(title); + + const menu = document.createElement("div"); + menu.setAttribute("data-title", title); + if(pageUID) { menu.setAttribute("data-uid", pageUID); } + + switch(true){ + case (title.startsWith("@")): + // (ref-citekey) : on-page menu + menu.classList.add(menuClasses.citekey); + menu.setAttribute("data-citekey", title.slice(1)); + break; + case (dnpRegex.test(title)): + // (DNP) : "XX items added" + menu.classList.add(menuClasses.dnp); + menu.setAttribute("data-dnp-date", JSON.stringify(readDNP(title, { as_date: false }))); + break; + default: + // (all other pages) : "XX abstracts", "YY tagged items" + menu.classList.add(menuClasses.tag); + } + + page.insertAdjacentElement("afterend", menu); + } +}; + +const findPageMenus = () => { + return { + citekeyMenus: Array.from(document.querySelectorAll(`[class=${menuClasses.citekey}]`)), + dnpMenus: Array.from(document.querySelectorAll(`[class=${menuClasses.dnp}]`)), + tagMenus: Array.from(document.querySelectorAll(`[class=${menuClasses.tag}]`)) + }; +}; + +export { + addPageMenus, + findPageMenus +}; diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/RelatedPanel/_index.sass b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/RelatedPanel/_index.sass new file mode 100644 index 0000000..0d7d33b --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/RelatedPanel/_index.sass @@ -0,0 +1,37 @@ +.zr-auxiliary-dialog--related.bp3-dialog + --zr-dialog-gutter: 2.5% + --zr-dialog-margin: 16vw + margin-top: 10vh + margin-bottom: 10vh + + .rendered-div + margin-bottom: 15px + + [zr-role="abstract-text"] + display: block + padding: 10px + padding-top: 3px + line-height: 1.4em + text-align: justify + white-space: break-spaces + + [zr-role="toggle-abstracts"] + margin: 0px 15px + + +.zr-related-item + + &--abstract + display: inline-block + border-radius: 6px + margin-top: 5px + + &--timestamp + margin-right: 10px + padding: 0px 6px + + &--title + display: block + font-size: var(--zr-text-small) + font-weight: 500 + white-space: normal diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/RelatedPanel/index.tsx b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/RelatedPanel/index.tsx new file mode 100644 index 0000000..a833db3 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/RelatedPanel/index.tsx @@ -0,0 +1,214 @@ +import { memo, useEffect, useMemo } from "react"; +import { Button, Classes } from "@blueprintjs/core"; + +import AuxiliaryDialog from "Components/AuxiliaryDialog"; +import CitekeyPopover from "Components/CitekeyPopover"; +import { ErrorBoundary } from "Components/Errors"; + +import { useBool } from "@hooks"; + +import { sortElems } from "../helpers"; +import { ShowPropertiesRelated, ShowTypeRelated } from "../types"; + +import { CustomClasses } from "../../../constants"; +import { pluralize } from "../../../utils"; +import { SCleanRelatedItem } from "Types/transforms"; +import "./_index.sass"; + + +const PANEL_LABEL_ID = "zr-related-panel-label"; + +type AbstractProps = { + abstract: string, + allAbstractsShown: boolean +}; + +const Abstract = memo(function Abstract({ abstract, allAbstractsShown }) { + const [isVisible, { set: setVisible, toggle: toggleAbstract }] = useBool(allAbstractsShown); + + useEffect(() => { + setVisible(allAbstractsShown); + }, [allAbstractsShown, setVisible]); + + if(!abstract){ + return null; + } else { + return ( +
+ + {isVisible + ? {abstract} + : null} +
+ ); + } +}); + + +type TimestampProps = { + timestamp: string, + type: ShowTypeRelated +}; + +const Timestamp = memo(function Timestamp({ timestamp, type }){ + return type == "added_on" + ? + {timestamp} + + : null; +}); + + +type RelatedItemProps = { + allAbstractsShown: boolean, + closeDialog: () => void, + item: SCleanRelatedItem, + type: ShowTypeRelated +}; + +const RelatedItem = memo(function RelatedItem(props) { + const { allAbstractsShown, closeDialog, item, type } = props; + const { children: { pdfs, notes }, raw } = item; + + return ( +
  • +
    + +
    +
    +
    + {item.title} + {item.meta} +
    + + + +
    + +
    +
    +
  • + ); +}); + + +type RelatedListProps = { + items: SCleanRelatedItem[] +} & Omit; + +const RelatedList = memo(function RelatedList(props) { + const { allAbstractsShown, closeDialog, items, type } = props; + + const sortedItems = useMemo(() => { + const sort = type == ShowTypeRelated.ADDED_ON ? "added" : "meta"; + return sortElems(items, sort); + }, [items, type]); + + return ( +
      + {sortedItems.map(it => ( + + ))} +
    + ); +}); + + +type RelatedPanelProps = { + isOpen: boolean, + items: SCleanRelatedItem[], + onClose: () => void, + show: ShowPropertiesRelated +}; + +const RelatedPanel = memo(function RelatedPanel(props) { + const { isOpen, items, onClose, show } = props; + const [isShowingAllAbstracts, { toggle: toggleAbstracts }] = useBool(false); + + const relationship = useMemo(() => { + const { title, type } = show; + switch(type){ + case ShowTypeRelated.ADDED_ON: + return { + string: "item", + suffix: " added on " + title + }; + case ShowTypeRelated.WITH_ABSTRACT: + return { + string: "abstract", + suffix: " containing " + title + }; + case ShowTypeRelated.WITH_TAG: + default: + return { + string: "item", + suffix: " tagged with " + title + }; + } + }, [show]); + + const panelLabel = useMemo(() => { + return
    {pluralize(items.length, relationship.string, relationship.suffix)}
    ; + }, [items.length, relationship]); + + const headerRight = useMemo(() => { + return ( +
    +
    + ); + }, [onClose]); + + return ( + +
    + +
    +
    + {panelLabel} + +
    + {headerRight} +
    +
    + +
    +
    +
    +
    + ); +}); + + +export default RelatedPanel; diff --git a/prototypes/zotero-roam-mcp/src/components/GraphWatcher/SemanticPanel/SemanticItem.tsx b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/SemanticPanel/SemanticItem.tsx new file mode 100644 index 0000000..90e4c95 --- /dev/null +++ b/prototypes/zotero-roam-mcp/src/components/GraphWatcher/SemanticPanel/SemanticItem.tsx @@ -0,0 +1,116 @@ +import { memo, useCallback, useMemo } from "react"; +import { Button, Classes, Icon, Tag } from "@blueprintjs/core"; + +import CitekeyPopover from "Components/CitekeyPopover"; + +import { CustomClasses } from "../../../constants"; +import { ShowTypeSemantic } from "../types"; +import { SEnrichedItem } from "Types/transforms"; + + +type OwnProps = { + handleRemove: (value: SEnrichedItem) => void, + handleSelect: (value: SEnrichedItem) => void, + inGraph: string | false, + isSelected: boolean, + item: SEnrichedItem, + type: ShowTypeSemantic +}; + +const SemanticItem = memo(function SemanticItem(props) { + const { handleRemove, handleSelect, inGraph, isSelected, item, type } = props; + const { inLibrary } = item; + + const handleClick = useCallback(() => { + if(isSelected){ + handleRemove(item); + } else { + handleSelect(item); + } + }, [isSelected, item, handleRemove, handleSelect]); + + const itemActions = useMemo(() => { + if(!inLibrary){ + return ( + <> + {item.url + ? + {item.doi || "Semantic Scholar"} + + : null} + {item.doi + ? +