diff --git a/README.md b/README.md index fa2b494..1e51d09 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,49 @@ Once enabled, you can use the Raycast extension to interact with your Beeper cha ## Setup See the [Beeper Desktop API Getting Started guide](https://developers.beeper.com/desktop-api/#get-started) for additional setup instructions. + +## Available Commands + +The extension provides the following commands: + +### List Accounts + +- **Description**: Display a list of all connected Beeper accounts +- **Usage**: Activate from Raycast and select "List Accounts" + +### List Chats + +- **Description**: Display a list of all your Beeper chats +- **Usage**: Activate from Raycast and select "List Chats" + +### Search Chats + +- **Description**: Search through your Beeper chats +- **Usage**: Activate from Raycast and select "Search Chats", then enter search text + +### Unread Chats + +- **Description**: Display all chats with unread messages +- **Usage**: Activate from Raycast and select "Unread Chats" + +### Send Message + +- **Description**: Quickly send a message to any chat +- **Usage**: Activate from Raycast and select "Send Message" + +### Focus Beeper Desktop + +- **Description**: Bring Beeper Desktop to the foreground +- **Usage**: Activate from Raycast and select "Focus Beeper Desktop" + +## NPM Commands + +The project uses the following npm scripts: + +| Command | Description | +|---------|-------------| +| `npm run dev` | Runs the extension in development mode | +| `npm run build` | Compiles the extension for distribution | +| `npm run lint` | Validates code using ESLint | +| `npm run fix-lint` | Automatically fixes linting issues | +| `npm run publish` | Publishes the extension to the Raycast Store | diff --git a/assets/discord.svg b/assets/discord.svg new file mode 100644 index 0000000..ce3b4de --- /dev/null +++ b/assets/discord.svg @@ -0,0 +1,4 @@ + + Discord + + \ No newline at end of file diff --git a/assets/email.svg b/assets/email.svg new file mode 100644 index 0000000..a108390 --- /dev/null +++ b/assets/email.svg @@ -0,0 +1,4 @@ + + Gmail + + \ No newline at end of file diff --git a/assets/facebook.svg b/assets/facebook.svg new file mode 100644 index 0000000..976bd6b --- /dev/null +++ b/assets/facebook.svg @@ -0,0 +1,4 @@ + + Facebook + + \ No newline at end of file diff --git a/assets/google-messages.svg b/assets/google-messages.svg new file mode 100644 index 0000000..59155bc --- /dev/null +++ b/assets/google-messages.svg @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/assets/imessage.svg b/assets/imessage.svg new file mode 100644 index 0000000..201b9af --- /dev/null +++ b/assets/imessage.svg @@ -0,0 +1,4 @@ + + iMessage + + \ No newline at end of file diff --git a/assets/instagram.svg b/assets/instagram.svg new file mode 100644 index 0000000..be77209 --- /dev/null +++ b/assets/instagram.svg @@ -0,0 +1,4 @@ + + Instagram + + \ No newline at end of file diff --git a/assets/messenger.svg b/assets/messenger.svg new file mode 100644 index 0000000..17476a1 --- /dev/null +++ b/assets/messenger.svg @@ -0,0 +1,4 @@ + + Messenger + + \ No newline at end of file diff --git a/assets/signal.svg b/assets/signal.svg new file mode 100644 index 0000000..2f01d47 --- /dev/null +++ b/assets/signal.svg @@ -0,0 +1,4 @@ + + Signal + + \ No newline at end of file diff --git a/assets/slack.svg b/assets/slack.svg new file mode 100644 index 0000000..bb0b1f1 --- /dev/null +++ b/assets/slack.svg @@ -0,0 +1,15 @@ + + Slack + + + + + \ No newline at end of file diff --git a/assets/telegram.svg b/assets/telegram.svg new file mode 100644 index 0000000..ee61f29 --- /dev/null +++ b/assets/telegram.svg @@ -0,0 +1,4 @@ + + Telegram + + \ No newline at end of file diff --git a/assets/twitter.svg b/assets/twitter.svg new file mode 100644 index 0000000..052dde9 --- /dev/null +++ b/assets/twitter.svg @@ -0,0 +1,4 @@ + + X + + \ No newline at end of file diff --git a/assets/whatsapp.svg b/assets/whatsapp.svg new file mode 100644 index 0000000..d0f7ec5 --- /dev/null +++ b/assets/whatsapp.svg @@ -0,0 +1,4 @@ + + WhatsApp + + \ No newline at end of file diff --git a/metadata/beeper-1.png b/metadata/beeper-1.png new file mode 100644 index 0000000..d13d27d Binary files /dev/null and b/metadata/beeper-1.png differ diff --git a/metadata/beeper-2.png b/metadata/beeper-2.png new file mode 100644 index 0000000..86a0a39 Binary files /dev/null and b/metadata/beeper-2.png differ diff --git a/package-lock.json b/package-lock.json index 35f1ed9..cdb5e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,17 +7,19 @@ "name": "beeper", "license": "MIT", "dependencies": { - "@beeper/desktop-api": "^0.1.4", - "@raycast/api": "^1.102.6", - "@raycast/utils": "^1.17.0" + "@beeper/desktop-api": "latest", + "@raycast/api": "^1.104.1", + "@raycast/utils": "^2.2.2", + "fuse.js": "^7.1.0" }, "devDependencies": { - "@raycast/eslint-config": "^2.0.4", - "@types/node": "22.13.10", - "@types/react": "19.0.10", - "eslint": "^9.22.0", - "prettier": "^3.5.3", - "typescript": "^5.8.2" + "@raycast/eslint-config": "^2.1.1", + "@types/node": "22.19.3", + "@types/react": "19.2.7", + "eslint": "^9.39.2", + "prettier": "^3.7.4", + "simple-icons": "^15.22.0", + "typescript": "^5.9.3" } }, "node_modules/@beeper/desktop-api": { @@ -27,9 +29,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -43,9 +45,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -59,9 +61,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -75,9 +77,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -91,9 +93,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -107,9 +109,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -123,9 +125,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -139,9 +141,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -155,9 +157,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -171,9 +173,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -187,9 +189,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -203,9 +205,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -219,9 +221,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -235,9 +237,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -251,9 +253,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -267,9 +269,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -283,9 +285,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -299,9 +301,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -315,9 +317,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -331,9 +333,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -347,9 +349,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -363,9 +365,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -379,9 +381,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -395,9 +397,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -411,9 +413,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -427,9 +429,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -443,9 +445,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", - "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", + "version": "4.9.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -462,9 +464,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -472,13 +474,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -486,44 +488,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.17.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -557,17 +538,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -581,23 +551,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.39.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -608,9 +565,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -618,13 +575,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -683,17 +640,26 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.2.tgz", - "integrity": "sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==", + "version": "4.3.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -708,13 +674,13 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.16", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.16.tgz", - "integrity": "sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==", + "version": "5.1.21", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -729,19 +695,19 @@ } }, "node_modules/@inquirer/core": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.0.tgz", - "integrity": "sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==", + "version": "10.3.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -757,7 +723,7 @@ }, "node_modules/@inquirer/core/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { @@ -770,14 +736,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.18.tgz", - "integrity": "sha512-yeQN3AXjCm7+Hmq5L6Dm2wEDeBRdAZuyZ4I7tWSSanbxDzqM0KqzoDbKM7p4ebllAYdoQuPJS6N71/3L281i6w==", + "version": "4.2.23", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/external-editor": "^1.0.1", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -792,14 +758,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.18.tgz", - "integrity": "sha512-xUjteYtavH7HwDMzq4Cn2X4Qsh5NozoDHCJTdoXg9HfZ4w3R6mxV1B9tL7DGJX2eq/zqtsFjhm0/RJIMGlh3ag==", + "version": "4.0.23", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -814,13 +780,13 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", - "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "version": "1.0.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "license": "MIT", "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.6.3" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { "node": ">=18" @@ -835,22 +801,22 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "version": "1.0.15", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@inquirer/input": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.2.tgz", - "integrity": "sha512-hqOvBZj/MhQCpHUuD3MVq18SSoDNHy7wEnQ8mtvs71K8OPZVXJinOzcvQna33dNYLYE4LkA9BlhAhK6MJcsVbw==", + "version": "4.3.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -865,13 +831,13 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.18.tgz", - "integrity": "sha512-7exgBm52WXZRczsydCVftozFTrrwbG5ySE0GqUd2zLNSBXyIucs2Wnm7ZKLe/aUu6NUg9dg7Q80QIHCdZJiY4A==", + "version": "3.0.23", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -886,14 +852,14 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.18.tgz", - "integrity": "sha512-zXvzAGxPQTNk/SbT3carAD4Iqi6A2JS2qtcqQjsL22uvD+JfQzUrDEtPjLL7PLn8zlSNyPdY02IiQjzoL9TStA==", + "version": "4.0.23", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -908,21 +874,21 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.4.tgz", - "integrity": "sha512-MuxVZ1en1g5oGamXV3DWP89GEkdD54alcfhHd7InUW5BifAdKQEK9SLFa/5hlWbvuhMPlobF0WAx7Okq988Jxg==", + "version": "7.10.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.2.2", - "@inquirer/confirm": "^5.1.16", - "@inquirer/editor": "^4.2.18", - "@inquirer/expand": "^4.0.18", - "@inquirer/input": "^4.2.2", - "@inquirer/number": "^3.0.18", - "@inquirer/password": "^4.0.18", - "@inquirer/rawlist": "^4.1.6", - "@inquirer/search": "^3.1.1", - "@inquirer/select": "^4.3.2" + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" }, "engines": { "node": ">=18" @@ -937,14 +903,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.6.tgz", - "integrity": "sha512-KOZqa3QNr3f0pMnufzL7K+nweFFCCBs6LCXZzXDrVGTyssjLeudn5ySktZYv1XiSqobyHRYYK0c6QsOxJEhXKA==", + "version": "4.1.11", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -959,15 +925,15 @@ } }, "node_modules/@inquirer/search": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.1.tgz", - "integrity": "sha512-TkMUY+A2p2EYVY3GCTItYGvqT6LiLzHBnqsU1rJbrpXUijFfM6zvUx0R4civofVwFCmJZcKqOVwwWAjplKkhxA==", + "version": "3.2.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -982,16 +948,16 @@ } }, "node_modules/@inquirer/select": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.2.tgz", - "integrity": "sha512-nwous24r31M+WyDEHV+qckXkepvihxhnyIaod2MG7eCE6G0Zm/HUF6jgN8GXgf4U7AU6SLseKdanY195cwvU6w==", + "version": "4.4.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.0", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1006,9 +972,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "version": "3.0.10", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "license": "MIT", "engines": { "node": ">=18" @@ -1022,62 +988,24 @@ } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@oclif/core": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.5.2.tgz", - "integrity": "sha512-eQcKyrEcDYeZJKu4vUWiu0ii/1Gfev6GF4FsLSgNez5/+aQyAUCjg3ZWlurf491WiYZTXCWyKAxyPWk8DKv2MA==", + "version": "4.8.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@oclif/core/-/core-4.8.1.tgz", + "integrity": "sha512-07mq0vKCWNsB85ZHeBMlTAiO0KLFqHyAeRK3bD2K8CI1tX3tiwkWw1lZQZkiw8MUBrhxdROhMkYMY4Q0l7JHqA==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", - "debug": "^4.4.0", + "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", - "minimatch": "^9.0.5", - "semver": "^7.6.3", + "minimatch": "^10.2.1", + "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", @@ -1090,9 +1018,9 @@ } }, "node_modules/@oclif/plugin-autocomplete": { - "version": "3.2.34", - "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.34.tgz", - "integrity": "sha512-KhbPcNjitAU7jUojMXJ3l7duWVub0L0pEr3r3bLrpJBNuIJhoIJ7p56Ropcb7OMH2xcaz5B8HGq56cTOe1FHEg==", + "version": "3.2.40", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.40.tgz", + "integrity": "sha512-HCfDuUV3l5F5Wz7SKkaoFb+OMQ5vKul8zvsPNgI0QbZcQuGHmn3svk+392wSfXboyA1gq8kzEmKPAoQK6r6UNw==", "license": "MIT", "dependencies": { "@oclif/core": "^4", @@ -1105,9 +1033,9 @@ } }, "node_modules/@oclif/plugin-help": { - "version": "6.2.32", - "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.32.tgz", - "integrity": "sha512-LrmMdo9EMJciOvF8UurdoTcTMymv5npKtxMAyonZvhSvGR8YwCKnuHIh00+SO2mNtGOYam7f4xHnUmj2qmanyA==", + "version": "6.2.37", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@oclif/plugin-help/-/plugin-help-6.2.37.tgz", + "integrity": "sha512-5N/X/FzlJaYfpaHwDC0YHzOzKDWa41s9t+4FpCDu4f9OMReds4JeNBaaWk9rlIzdKjh2M6AC5Q18ORfECRkHGA==", "license": "MIT", "dependencies": { "@oclif/core": "^4" @@ -1117,13 +1045,13 @@ } }, "node_modules/@oclif/plugin-not-found": { - "version": "3.2.67", - "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.67.tgz", - "integrity": "sha512-Q2VluSwTrh7Sk0ey88Lk5WSATn9AZ6TjYQIyt2QrQolOBErAgpDoDSMVRYuVNtjxPBTDBzz4MM54QRFa/nN4IQ==", + "version": "3.2.74", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@oclif/plugin-not-found/-/plugin-not-found-3.2.74.tgz", + "integrity": "sha512-6RD/EuIUGxAYR45nMQg+nw+PqwCXUxkR6Eyn+1fvbVjtb9d+60OPwB77LCRUI4zKNI+n0LOFaMniEdSpb+A7kQ==", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^7.8.4", - "@oclif/core": "^4.5.2", + "@inquirer/prompts": "^7.10.1", + "@oclif/core": "^4.8.0", "ansis": "^3.17.0", "fast-levenshtein": "^3.0.0" }, @@ -1132,18 +1060,18 @@ } }, "node_modules/@raycast/api": { - "version": "1.102.6", - "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.102.6.tgz", - "integrity": "sha512-P6j4nSOJe8wlsaf8gU/fsEnB4/oO8AXywcX3NiVQGiG9PWb/OyCCWQA8SFNdARsYyzNsJRlR+XRfMVzCBNv8Rw==", + "version": "1.104.6", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@raycast/api/-/api-1.104.6.tgz", + "integrity": "sha512-W3UnZrfh1EH9mteQQOibeUvniDnVsbRgcfSUgVWFTGuI6mzKLFzoM8mD6n5amYQM51JVvbuJbaqtfMqsTmnQCQ==", "license": "MIT", "dependencies": { - "@oclif/core": "^4.4.1", - "@oclif/plugin-autocomplete": "^3.2.31", - "@oclif/plugin-help": "^6.2.29", - "@oclif/plugin-not-found": "^3.2.57", + "@oclif/core": "^4.5.4", + "@oclif/plugin-autocomplete": "^3.2.35", + "@oclif/plugin-help": "^6.2.33", + "@oclif/plugin-not-found": "^3.2.68", "@types/node": "22.13.10", "@types/react": "19.0.10", - "esbuild": "^0.25.5", + "esbuild": "^0.25.10", "react": "19.0.0" }, "bin": { @@ -1169,18 +1097,42 @@ } } }, + "node_modules/@raycast/api/node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@raycast/api/node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@raycast/api/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/@raycast/eslint-config": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-2.0.4.tgz", - "integrity": "sha512-Sz5Q0HVY8pW21Sv7CDcJw882OGubSM3zx5uiDRQMkgt7Hui3I+IZOqxDInnhLsGG52prSdU7sg3R097hMJkkQw==", + "version": "2.1.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@raycast/eslint-config/-/eslint-config-2.1.1.tgz", + "integrity": "sha512-W0kxF+FJ+BYQn0EKIV739j2ZrHEtjo/LclsoZgUWg3t364Dq75XKcjqYFYx+59/DBaamY0amdajlfuDAf6veAg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint/js": "^9.22.0", - "@raycast/eslint-plugin": "^2.0.4", - "eslint-config-prettier": "^10.1.1", - "globals": "^16.0.0", - "typescript-eslint": "^8.26.1" + "@eslint/js": "^9.36.0", + "@raycast/eslint-plugin": "^2.1.1", + "eslint-config-prettier": "^10.1.8", + "globals": "^16.4.0", + "typescript-eslint": "^8.45.0" }, "peerDependencies": { "eslint": ">=8.23.0", @@ -1189,9 +1141,9 @@ } }, "node_modules/@raycast/eslint-plugin": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.0.7.tgz", - "integrity": "sha512-4dAdua+TFQu7ay3EuDmg5nT7boIAj3Rjf0V1aQ3w5uAx/j98DvI91h/zbbkVb4mIXCom1w7SfRrZQzYIOE+tZw==", + "version": "2.1.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@raycast/eslint-plugin/-/eslint-plugin-2.1.1.tgz", + "integrity": "sha512-r2gs8uIlNp6I2mLOyN/kReGlvigzEeuyQPl4yw7nwLy8Zxjfjhg8txMViaBux8juBWBxbSWq/IfW6ZA50oeOHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1202,20 +1154,21 @@ } }, "node_modules/@raycast/utils": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-1.19.1.tgz", - "integrity": "sha512-/udUGcTZCgZZwzesmjBkqG5naQZTD/ZLHbqRwkWcF+W97vf9tr9raxKyQjKsdZ17OVllw2T3sHBQsVUdEmCm2g==", + "version": "2.2.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@raycast/utils/-/utils-2.2.2.tgz", + "integrity": "sha512-tZcyWCHZvz4L/i1CGEnSZkBoK6wwX1pzlTKjcWWugbrQyG0QCMOxjKJfRC/iNkD+hHaqhMWUj4Y0LNo/NknvFw==", "license": "MIT", "dependencies": { - "cross-fetch": "^3.1.6", - "dequal": "^2.0.3", - "object-hash": "^3.0.0", - "signal-exit": "^4.0.2", - "stream-chain": "^2.2.5", - "stream-json": "^1.8.0" + "dequal": "^2.0.3" }, "peerDependencies": { - "@raycast/api": ">=1.69.0" + "@raycast/api": ">=1.99.4", + "react": ">=19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } } }, "node_modules/@types/estree": { @@ -1227,45 +1180,46 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.19.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "version": "19.2.7", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz", - "integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.42.0", - "@typescript-eslint/type-utils": "8.42.0", - "@typescript-eslint/utils": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1275,14 +1229,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.42.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", @@ -1291,17 +1245,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz", - "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.42.0", - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/typescript-estree": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1311,20 +1265,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz", - "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.42.0", - "@typescript-eslint/types": "^8.42.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1338,14 +1292,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz", - "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1356,9 +1310,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz", - "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -1373,17 +1327,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz", - "integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/typescript-estree": "8.42.0", - "@typescript-eslint/utils": "8.42.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1393,14 +1347,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz", - "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -1412,22 +1366,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz", - "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.42.0", - "@typescript-eslint/tsconfig-utils": "8.42.0", - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1441,16 +1394,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz", - "integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.42.0", - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/typescript-estree": "8.42.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1460,19 +1413,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", - "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.42.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1483,13 +1436,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1537,7 +1490,7 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "license": "MIT", "dependencies": { @@ -1552,7 +1505,7 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { @@ -1576,7 +1529,7 @@ }, "node_modules/ansis": { "version": "3.17.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/ansis/-/ansis-3.17.0.tgz", "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", "license": "ISC", "engines": { @@ -1592,36 +1545,29 @@ }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "4.0.4", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/callsites": { @@ -1665,14 +1611,14 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "version": "2.1.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, "node_modules/clean-stack": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/clean-stack/-/clean-stack-3.0.1.tgz", "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", "license": "MIT", "dependencies": { @@ -1687,7 +1633,7 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", "engines": { @@ -1699,7 +1645,7 @@ }, "node_modules/cli-width": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "license": "ISC", "engines": { @@ -1724,22 +1670,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1756,15 +1686,15 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1796,7 +1726,7 @@ }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "license": "Apache-2.0", "dependencies": { @@ -1811,14 +1741,14 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.25.12", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1828,32 +1758,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escape-string-regexp": { @@ -1869,25 +1799,24 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.39.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -1975,17 +1904,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -1999,19 +1917,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -2096,36 +2001,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2135,7 +2010,7 @@ }, "node_modules/fast-levenshtein": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "license": "MIT", "dependencies": { @@ -2144,21 +2019,28 @@ }, "node_modules/fastest-levenshtein": { "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "license": "MIT", "engines": { "node": ">= 4.9.1" } }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -2175,37 +2057,15 @@ } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.5", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/filelist/-/filelist-1.0.5.tgz", + "integrity": "sha512-ct/ckWBV/9Dg3MlvCXsLcSUyoWwv9mCKqlhLNB2DAuXR/NZolSXlQqP5dyy6guWlPXBhodZyZ5lGPQcbQDxrEQ==", "license": "Apache-2.0", "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" + "minimatch": "^10.2.1" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, "node_modules/find-up": { @@ -2246,9 +2106,18 @@ "dev": true, "license": "ISC" }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/get-package-type": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "license": "MIT", "engines": { @@ -2269,9 +2138,9 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.5.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -2281,13 +2150,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2298,15 +2160,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -2348,7 +2214,7 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "license": "MIT", "engines": { @@ -2357,7 +2223,7 @@ }, "node_modules/is-docker": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", "bin": { @@ -2382,7 +2248,7 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { @@ -2402,19 +2268,9 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-wsl": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { @@ -2433,7 +2289,7 @@ }, "node_modules/jake": { "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/jake/-/jake-10.9.4.tgz", "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "license": "Apache-2.0", "dependencies": { @@ -2508,7 +2364,7 @@ }, "node_modules/lilconfig": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { @@ -2541,40 +2397,16 @@ "dev": true, "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", + "version": "10.2.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2588,7 +2420,7 @@ }, "node_modules/mute-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "license": "ISC", "engines": { @@ -2602,35 +2434,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2723,18 +2526,17 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "version": "4.0.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -2751,9 +2553,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -2776,27 +2578,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -2816,51 +2597,16 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2894,7 +2640,7 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { @@ -2904,24 +2650,29 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" + "node_modules/simple-icons": { + "version": "15.22.0", + "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-15.22.0.tgz", + "integrity": "sha512-i/w5Ie4tENfGYbdCo2iJ+oies0vOFd8QXWHopKOUzudfLCvnmeheF2PpHp89Z2azpc+c2su3lMiWO/SpP+429A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/simple-icons" + }, + { + "type": "github", + "url": "https://github.com/sponsors/simple-icons" + } + ], + "license": "CC0-1.0", + "engines": { + "node": ">=0.12.18" } }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { @@ -2935,7 +2686,7 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { @@ -2960,7 +2711,7 @@ }, "node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", "dependencies": { @@ -2974,13 +2725,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -2989,58 +2740,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -3065,7 +2768,7 @@ }, "node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", "engines": { @@ -3076,9 +2779,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3090,16 +2793,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz", - "integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==", + "version": "8.56.1", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.42.0", - "@typescript-eslint/parser": "8.42.0", - "@typescript-eslint/typescript-estree": "8.42.0", - "@typescript-eslint/utils": "8.42.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3109,14 +2812,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, "license": "MIT" }, "node_modules/uri-js": { @@ -3129,22 +2833,6 @@ "punycode": "^2.1.0" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3163,7 +2851,7 @@ }, "node_modules/widest-line": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/widest-line/-/widest-line-3.1.0.tgz", "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", "license": "MIT", "dependencies": { @@ -3185,13 +2873,13 @@ }, "node_modules/wordwrap": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { @@ -3221,7 +2909,7 @@ }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "resolved": "https://artifactory.skoda.vwgroup.com/artifactory/api/npm/front-npm-virtual/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "license": "MIT", "engines": { diff --git a/package.json b/package.json index 5cd4378..f3c0781 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,45 @@ "mode": "no-view" }, { - "name": "find-chat", - "title": "Find Chat", - "description": "Find chats from Beeper Desktop", + "name": "list-chats", + "title": "List Chats", + "description": "List all your Beeper chats", + "mode": "view" + }, + { + "name": "search-chats", + "title": "Search Chats", + "description": "Search through your Beeper chats", + "mode": "view" + }, + { + "name": "unread-chats", + "title": "Unread Chats", + "description": "View all chats with unread messages", + "mode": "view" + }, + { + "name": "send-message", + "title": "Send Message", + "description": "Quickly send a message to any chat", + "mode": "view" + }, + { + "name": "search-messages", + "title": "Search Messages", + "description": "Search through your Beeper messages", + "mode": "view" + }, + { + "name": "contacts", + "title": "Contacts", + "description": "Search contacts across connected Beeper accounts", + "mode": "view" + }, + { + "name": "search-all", + "title": "Search Everything", + "description": "Search across all Beeper chats and messages", "mode": "view" } ], @@ -34,20 +70,43 @@ "type": "textfield", "required": false, "default": "http://localhost:23373" + }, + { + "name": "language", + "title": "Language", + "description": "Select the extension language", + "type": "dropdown", + "required": false, + "default": "en", + "data": [ + { + "title": "English", + "value": "en" + }, + { + "title": "Čeština", + "value": "cs" + } + ] } ], "dependencies": { - "@beeper/desktop-api": "^0.1.4", - "@raycast/api": "^1.102.6", - "@raycast/utils": "^1.17.0" + "@beeper/desktop-api": "latest", + "@raycast/api": "^1.104.1", + "@raycast/utils": "^2.2.2", + "fuse.js": "^7.1.0" }, "devDependencies": { - "@raycast/eslint-config": "^2.0.4", - "@types/node": "22.13.10", - "@types/react": "19.0.10", - "eslint": "^9.22.0", - "prettier": "^3.5.3", - "typescript": "^5.8.2" + "@raycast/eslint-config": "^2.1.1", + "@types/node": "22.19.3", + "@types/react": "19.2.7", + "eslint": "^9.39.2", + "prettier": "^3.7.4", + "simple-icons": "^15.22.0", + "typescript": "^5.9.3" + }, + "overrides": { + "minimatch": ">=10.2.1" }, "scripts": { "build": "ray build", @@ -57,4 +116,4 @@ "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", "publish": "npx @raycast/api@latest publish" } -} +} \ No newline at end of file diff --git a/scripts/generate-icons.js b/scripts/generate-icons.js new file mode 100644 index 0000000..af95b3a --- /dev/null +++ b/scripts/generate-icons.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +/** + * Script to generate network icons from simple-icons + * Run: node scripts/generate-icons.js + */ + +const fs = require('fs'); +const path = require('path'); +const simpleIcons = require('simple-icons'); + +// Get icons from simple-icons +const icons = { + slack: simpleIcons.siSlack, + whatsapp: simpleIcons.siWhatsapp, + telegram: simpleIcons.siTelegram, + discord: simpleIcons.siDiscord, + instagram: simpleIcons.siInstagram, + facebook: simpleIcons.siFacebook, + messenger: simpleIcons.siMessenger, + signal: simpleIcons.siSignal, + linkedin: simpleIcons.siLinkedin, + twitter: simpleIcons.siX, + imessage: simpleIcons.siImessage, + sms: simpleIcons.siAndroidmessages, + email: simpleIcons.siGmail, +}; + +const outputDir = path.join(__dirname, '..', 'assets', 'icons'); + +// Ensure output directory exists +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// Generate SVG files with proper styling for Raycast +// Use a dark color that Raycast can tint for light/dark mode +Object.entries(icons).forEach(([name, icon]) => { + if (!icon) { + console.log(`Skipping ${name} - icon not found`); + return; + } + + const svg = ` + ${icon.title} + +`; + + const filename = `${name}.png`; + fs.writeFileSync(path.join(outputDir, filename.replace('.png', '.svg')), svg); + console.log(`Generated ${filename.replace('.png', '.svg')}`); +}); + +console.log('\nAll icons generated successfully!'); diff --git a/src/api.ts b/src/api.ts index 5f4f2b0..907097f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,10 @@ import BeeperDesktop from "@beeper/desktop-api"; -import type { AppFocusParams } from "@beeper/desktop-api/resources/app"; import { closeMainWindow, getPreferenceValues, OAuth, showHUD } from "@raycast/api"; import { OAuthService, usePromise, getAccessToken } from "@raycast/utils"; +import { readFile } from "node:fs/promises"; +import { basename } from "node:path"; +import { fileURLToPath } from "node:url"; +import { t } from "./locales"; interface Preferences { baseURL?: string; @@ -39,13 +42,15 @@ export function createBeeperOAuth() { refreshTokenUrl: `${baseURL}/oauth/token`, bodyEncoding: "url-encoded", onAuthorize: ({ token }) => { - // Reset client when new token is obtained clientInstance = null; lastAccessToken = token; }, }); } +/** + * Returns a cached BeeperDesktop client, creating a new instance when the configured base URL or access token has changed. + */ export function getBeeperDesktop(): BeeperDesktop { const baseURL = getBaseURL(); const { token: accessToken } = getAccessToken(); @@ -63,17 +68,330 @@ export function getBeeperDesktop(): BeeperDesktop { return clientInstance; } -export function useBeeperDesktop(fn: (client: BeeperDesktop) => Promise) { - return usePromise(async () => fn(getBeeperDesktop())); +/** + * Execute an asynchronous operation using the current BeeperDesktop client and return its managed result. + * The operation will be re-executed whenever any value in the args array changes. + */ +export function useBeeperDesktop( + fn: (client: BeeperDesktop, ...args: A) => Promise, + args?: A, +) { + return usePromise((...a: A) => fn(getBeeperDesktop(), ...a), (args ?? []) as A); } -export const focusApp = async (params: AppFocusParams = {}) => { +export const focusApp = async ( + params: { + chatID?: string; + draftText?: string; + draftAttachmentPath?: string; + messageID?: string; + } = {}, +) => { + const translations = t(); try { - await getBeeperDesktop().app.focus(params); + await getBeeperDesktop().post("/v1/focus", { body: params }); await closeMainWindow(); - await showHUD("Beeper Desktop focused"); + await showHUD(translations.commands.focusApp.successMessage); } catch (error) { console.error("Failed to focus Beeper Desktop:", error); - await showHUD("Failed to focus Beeper Desktop"); + await showHUD(translations.commands.focusApp.errorMessage); } }; + +// Deep-link constants +const RAYCAST_EXTENSION_AUTHOR = "batuhan"; +const RAYCAST_EXTENSION_NAME = "beeper"; +const RAYCAST_FOCUS_COMMAND = "focus-app"; + +export const getRaycastFocusLink = ( + params: { + chatID?: string; + draftText?: string; + draftAttachmentPath?: string; + messageID?: string; + } = {}, +) => { + const args = Object.keys(params).length > 0 ? `?arguments=${encodeURIComponent(JSON.stringify(params))}` : ""; + return `raycast://extensions/${RAYCAST_EXTENSION_AUTHOR}/${RAYCAST_EXTENSION_NAME}/${RAYCAST_FOCUS_COMMAND}${args}`; +}; + +// Types +export type MessageAttachmentInput = { + uploadID: string; + mimeType?: string; + fileName?: string; + size?: { width?: number; height?: number }; + duration?: number; + type?: "gif" | "voiceNote" | "sticker"; +}; + +export type MessageEditInput = { + text: string; +}; + +export type AssetUploadResponse = { + uploadID: string; + mimeType?: string; + fileName?: string; + fileSize?: number; + width?: number; + height?: number; + duration?: number; + srcURL?: string; +}; + +export type CursorResponse = { + items: T[]; + hasMore?: boolean; + newestCursor?: string | null; + oldestCursor?: string | null; + cursor?: string | null; + nextCursor?: string | null; +}; + +export type UnifiedSearchMessages = { + items?: BeeperDesktop.Message[]; + chats?: Record; + hasMore?: boolean; + newestCursor?: string | null; + oldestCursor?: string | null; +}; + +export type GlobalSearchResponse = { + results?: { + chats?: BeeperDesktop.Chat[]; + in_groups?: BeeperDesktop.Chat[]; + messages?: UnifiedSearchMessages; + }; +}; + +const normalizeCursorResponse = (result: { + items?: T[]; + hasMore?: boolean; + newestCursor?: string | null; + oldestCursor?: string | null; + cursor?: string | null; + nextCursor?: string | null; +}): CursorResponse => ({ + items: result.items ?? [], + hasMore: result.hasMore, + newestCursor: result.newestCursor, + oldestCursor: result.oldestCursor, + cursor: result.cursor, + nextCursor: result.nextCursor, +}); + +// Asset helpers +const getAccessTokenValue = () => getAccessToken().token; +const getAuthHeaders = () => ({ Authorization: `Bearer ${getAccessTokenValue()}` }); + +const requestJSON = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const response = await fetch(input, init); + if (!response.ok) { + const text = await response.text(); + throw new Error(text || response.statusText); + } + return (await response.json()) as T; +}; + +export const uploadAssetFromFile = async (filePath: string): Promise => { + const body = new FormData(); + const fileName = basename(filePath); + const buffer = await readFile(filePath); + body.append("file", new File([buffer], fileName)); + return requestJSON(`${getBaseURL()}/v1/assets/upload`, { + method: "POST", + headers: getAuthHeaders(), + body, + }); +}; + +export const uploadAssetFromBase64 = async (params: { + content: string; + fileName?: string; + mimeType?: string; +}): Promise => { + return requestJSON(`${getBaseURL()}/v1/assets/upload/base64`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...getAuthHeaders(), + }, + body: JSON.stringify({ + content: params.content, + fileName: params.fileName, + mimeType: params.mimeType, + }), + }); +}; + +export const downloadAsset = async (url: string): Promise<{ srcURL: string }> => { + return requestJSON<{ srcURL: string }>(`${getBaseURL()}/v1/assets/download`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...getAuthHeaders(), + }, + body: JSON.stringify({ url }), + }); +}; + +export const resolveFilePathFromSrcURL = (srcURL?: string) => { + if (!srcURL) return undefined; + if (srcURL.startsWith("file://")) { + return fileURLToPath(srcURL); + } + return undefined; +}; + +export const getServeAssetURL = (url: string) => { + const encoded = encodeURIComponent(url); + return `${getBaseURL()}/v1/assets/serve?url=${encoded}`; +}; + +// Chat & Account API functions + +export const listAccounts = async (): Promise => { + const response = await getBeeperDesktop().get("/v1/accounts"); + if (Array.isArray(response)) { + return response as BeeperDesktop.Account[]; + } + if (response?.items && Array.isArray(response.items)) { + return response.items as BeeperDesktop.Account[]; + } + return []; +}; + +export const searchContacts = async (accountID: string, query: string) => { + const response = await getBeeperDesktop().get(`/v1/accounts/${encodeURIComponent(accountID)}/contacts`, { + query: { query }, + }); + return response?.items && Array.isArray(response.items) ? (response.items as BeeperDesktop.User[]) : []; +}; + +export const listChats = async (params?: { + accountIDs?: string[]; + cursor?: string | null; + direction?: "after" | "before"; +}): Promise> => { + const response = await getBeeperDesktop().get("/v1/chats", { query: params }); + return normalizeCursorResponse(response); +}; + +export const searchChats = async (params: { + accountIDs?: string[]; + cursor?: string | null; + direction?: "after" | "before"; + inbox?: "primary" | "low-priority" | "archive"; + includeMuted?: boolean; + lastActivityAfter?: string; + lastActivityBefore?: string; + participantQuery?: string; + query?: string; + type?: "single" | "group" | "channel" | "any"; + unreadOnly?: boolean; +}) => { + const response = await getBeeperDesktop().get("/v1/chats/search", { query: params }); + return normalizeCursorResponse(response); +}; + +export const createChat = async (body: { + accountID: string; + participantIDs: string[]; + type: "single" | "group"; + title?: string; + messageText?: string; +}) => { + return getBeeperDesktop().post("/v1/chats", { body }); +}; + +export const retrieveChat = async (chatID: string, options?: { maxParticipantCount?: number | null }) => { + return getBeeperDesktop().get(`/v1/chats/${encodeURIComponent(chatID)}`, { + query: options, + }); +}; + +export const archiveChat = async (chatID: string, archived?: boolean) => { + return getBeeperDesktop().post(`/v1/chats/${encodeURIComponent(chatID)}/archive`, { + body: { archived }, + }); +}; + +export const createChatReminder = async ( + chatID: string, + reminder: { remindAtMs: number; dismissOnIncomingMessage?: boolean }, +) => { + return getBeeperDesktop().post(`/v1/chats/${encodeURIComponent(chatID)}/reminders`, { + body: reminder, + }); +}; + +export const deleteChatReminder = async (chatID: string) => { + return getBeeperDesktop().delete(`/v1/chats/${encodeURIComponent(chatID)}/reminders`); +}; + +export const listChatMessages = async ( + chatID: string, + params?: { cursor?: string | null; direction?: "after" | "before"; limit?: number }, +): Promise> => { + const response = await getBeeperDesktop().get(`/v1/chats/${encodeURIComponent(chatID)}/messages`, { + query: params, + }); + return normalizeCursorResponse(response); +}; + +export const searchMessages = async (params: { + query?: string; + chatIDs?: string[]; + sender?: "me" | "others" | string; + accountIDs?: string[]; + chatType?: "group" | "single"; + includeMuted?: boolean; + excludeLowPriority?: boolean | null; + mediaTypes?: Array<"any" | "video" | "image" | "link" | "file">; + dateAfter?: string; + dateBefore?: string; + cursor?: string | null; + direction?: "after" | "before"; + limit?: number; +}): Promise> => { + const response = await getBeeperDesktop().get("/v1/messages/search", { query: params }); + return normalizeCursorResponse(response); +}; + +export const searchAll = async (params: { query: string }): Promise => { + return getBeeperDesktop().get("/v1/search", { query: params }); +}; + +export const sendMessage = async ( + chatID: string, + message: { text?: string; replyToMessageID?: string; attachment?: MessageAttachmentInput }, +) => { + return getBeeperDesktop().post(`/v1/chats/${encodeURIComponent(chatID)}/messages`, { body: message }); +}; + +export const updateMessage = async (chatID: string, messageID: string, update: MessageEditInput) => { + return getBeeperDesktop().put(`/v1/chats/${encodeURIComponent(chatID)}/messages/${encodeURIComponent(messageID)}`, { + body: update, + }); +}; + +export const downloadMessageAttachments = async (params: { + chatID: string; + messageID: string; + url?: string; +}): Promise<{ success: boolean; filePath?: string; error?: string }> => { + if (params.url) { + try { + const response = await downloadAsset(params.url); + const filePath = resolveFilePathFromSrcURL(response.srcURL); + if (!filePath) { + return { success: false, error: "Downloaded asset did not return a local file path" }; + } + return { success: true, filePath }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + throw new Error("Attachment download not supported by this SDK version"); +}; diff --git a/src/chat.tsx b/src/chat.tsx new file mode 100644 index 0000000..a5598c2 --- /dev/null +++ b/src/chat.tsx @@ -0,0 +1,1438 @@ +import { + Action, + ActionPanel, + Alert, + Color, + Detail, + Form, + Icon, + Keyboard, + List, + Toast, + confirmAlert, + useNavigation, + showHUD, + showToast, +} from "@raycast/api"; +import { useCachedState, useFrecencySorting, useForm, useLocalStorage, withAccessToken } from "@raycast/utils"; +import BeeperDesktop from "@beeper/desktop-api"; +import Fuse from "fuse.js"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + archiveChat, + createBeeperOAuth, + createChatReminder, + deleteChatReminder, + focusApp, + listChatMessages, + retrieveChat, + searchChats, + sendMessage, + updateMessage, + getRaycastFocusLink, + useBeeperDesktop, +} from "./api"; +import { formatReactionsShort, formatReactionsDetailed } from "./reactions"; +import { parseDate, getMessageID } from "./utils"; + +export type InboxFilter = "all" | "inbox" | "low-priority" | "archive"; +export type ChatTypeFilter = "any" | "single" | "group"; + +/** Maps the UI inbox filter value to the API's inbox parameter. */ +export const toApiInbox = (inbox: InboxFilter): "primary" | "low-priority" | "archive" | undefined => + inbox === "all" ? undefined : inbox === "inbox" ? "primary" : inbox; + +export interface ChatFilters { + inbox: InboxFilter; + type: ChatTypeFilter; + unreadOnly: boolean; + includeMuted: boolean; +} + +const recentDefaultFilters: ChatFilters = { + inbox: "inbox", + type: "any", + unreadOnly: false, + includeMuted: true, +}; + +const getErrorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error)); + +/** Build a preview string for a message, considering text, attachments, and reactions. */ +const getMessagePreview = (message: BeeperDesktop.Message): string => { + const text = message.text?.trim(); + if (text && text.length > 0) return text; + const reactions = formatReactionsShort(message.reactions); + if (reactions) return `Reacted ${reactions}`; + if (message.attachments && message.attachments.length > 0) return "Attachment"; + return "Message"; +}; + +export type ChatInbox = "inbox" | "low-priority" | "archive"; +export type IndexedChat = { chat: BeeperDesktop.Chat; inbox: ChatInbox; searchFields: ChatSearchFields }; +export type ChatIndexState = { + items: IndexedChat[]; + cursors: Record; + updatedAt: number; +}; + +const INDEXED_INBOXES: ChatInbox[] = ["inbox", "low-priority", "archive"]; +const MAX_INDEXD_CHATS_TARGET = 3000; +const INDEX_PAGE_LIMIT = 50; +const INDEX_MAX_PAGES = 10; +const INDEX_MAX_PAGES_INCREMENTAL = 2; +const INDEX_REFRESH_MAX_AGE_MS = 6 * 60 * 60 * 1000; +const INDEX_INCREMENTAL_MIN_AGE_MS = 30 * 1000; +const MAX_PARTICIPANTS_INDEXED = 10; +const MAX_PARTICIPANTS_STORED = 0; +const MAX_INDEXED_CHATS_PER_INBOX = Math.ceil(MAX_INDEXD_CHATS_TARGET / INDEXED_INBOXES.length); + +const emptyCursors: ChatIndexState["cursors"] = { + inbox: { newestCursor: null, oldestCursor: null }, + "low-priority": { newestCursor: null, oldestCursor: null }, + archive: { newestCursor: null, oldestCursor: null }, +}; + +export const defaultIndexState: ChatIndexState = { + items: [], + cursors: emptyCursors, + updatedAt: 0, +}; + +const getChatTimestamp = (chat: BeeperDesktop.Chat) => { + if (!chat.lastActivity) return 0; + const ts = Date.parse(chat.lastActivity); + return Number.isNaN(ts) ? 0 : ts; +}; + +const sortIndexedChatsByActivity = (items: IndexedChat[]) => + [...items].sort((a, b) => getChatTimestamp(b.chat) - getChatTimestamp(a.chat)); + +const sortChatsByActivity = (items: BeeperDesktop.Chat[]) => + [...items].sort((a, b) => getChatTimestamp(b) - getChatTimestamp(a)); + +const mergeIndexedChats = (base: IndexedChat[], updates: IndexedChat[]) => { + const map = new Map(); + for (const item of base) { + map.set(item.chat.id, item); + } + for (const item of updates) { + map.set(item.chat.id, item); + } + return Array.from(map.values()); +}; + +const indexItemsChanged = (before: IndexedChat[], after: IndexedChat[]): boolean => { + if (before.length !== after.length) return true; + const beforeMap = new Map(before.map((i) => [i.chat.id, i])); + for (const item of after) { + const prev = beforeMap.get(item.chat.id); + if (!prev) return true; + if ( + prev.chat.lastActivity !== item.chat.lastActivity || + prev.chat.unreadCount !== item.chat.unreadCount || + prev.chat.title !== item.chat.title || + prev.chat.isPinned !== item.chat.isPinned || + prev.chat.isMuted !== item.chat.isMuted || + prev.chat.isArchived !== item.chat.isArchived || + prev.inbox !== item.inbox + ) + return true; + } + return false; +}; + +export const summarizeChatForIndex = (chat: BeeperDesktop.Chat): BeeperDesktop.Chat => ({ + id: chat.id, + accountID: chat.accountID, + network: chat.network ?? "", + participants: { + hasMore: false, + items: + MAX_PARTICIPANTS_STORED > 0 ? (chat.participants?.items ?? []).slice(0, MAX_PARTICIPANTS_STORED) : [], + total: chat.participants?.total ?? 0, + }, + type: chat.type, + unreadCount: chat.unreadCount ?? 0, + description: chat.description ?? undefined, + isArchived: chat.isArchived ?? false, + isMuted: chat.isMuted ?? false, + isPinned: chat.isPinned ?? false, + lastActivity: chat.lastActivity ?? undefined, + lastReadMessageSortKey: chat.lastReadMessageSortKey ?? undefined, + localChatID: chat.localChatID ?? null, + title: chat.title ?? null, +}); + +const normalizeIndexState = (state: ChatIndexState): ChatIndexState => { + let changed = false; + let items = state.items.map((item) => { + if (item.searchFields) return item; + changed = true; + return { ...item, searchFields: buildSearchFields(item.chat) }; + }); + if (items.length > MAX_INDEXD_CHATS_TARGET) { + items = sortIndexedChatsByActivity(items).slice(0, MAX_INDEXD_CHATS_TARGET); + changed = true; + } + return changed ? { ...state, items } : state; +}; + +const STOP_WORDS = new Set(["and"]); + +const normalizeSearchValue = (value: string) => + value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[|!'^=]/g, " ") + .replace(/[,;!?(){}[\]"'`~#$%^&*+=<>\\]/g, " ") + .replace(/\s+/g, " ") + .trim(); + +const parseSearchTerms = (query: string) => + normalizeSearchValue(query) + .split(" ") + .map((term) => term.trim()) + .filter((term) => term.length > 0); + +type ChatSearchFields = { + title: string; + network: string; + participants: string[]; +}; + +interface PropertyScore { + minScore: number; + hits: number; +} + +interface SearchableScore { + title: PropertyScore; + network: PropertyScore; + participants: PropertyScore; +} + +interface SearchIndexResult { + id: string; + score: SearchableScore; + matchSearchTerms: Set; +} + +interface SearchIndexItem { + id: string; + searchFields: ChatSearchFields; +} + +interface SearchableMatch { + id: string; + title: number[]; + network: number[]; + participants: number[]; + matchedSearchTerms: Set; +} + +const computeScore = (scores: number[]): PropertyScore => { + if (!scores || scores.length === 0) { + return { minScore: Number.MAX_SAFE_INTEGER, hits: 0 }; + } + return { minScore: Math.min(...scores), hits: scores.length }; +}; + +const computeScoreFromMatch = (match: SearchableMatch): SearchableScore => ({ + title: computeScore(match.title), + network: computeScore(match.network), + participants: computeScore(match.participants), +}); + +type SearchProperty = keyof ChatSearchFields; + +class ThreadSearchIndex { + private fuse: Fuse; + + constructor(collection: SearchIndexItem[]) { + this.fuse = new Fuse(collection, { + ignoreDiacritics: true, + includeScore: true, + ignoreLocation: true, + ignoreFieldNorm: true, + threshold: 0, + keys: ["searchFields.title", "searchFields.network", "searchFields.participants"], + }); + } + + search(query: string, properties: SearchProperty[]): SearchIndexResult[] { + if (!query?.trim()) return []; + const searchTerms = parseSearchTerms(query); + const matches = new Map(); + + const getOrCreateMatch = (id: string): SearchableMatch => { + const existing = matches.get(id); + if (existing) return existing; + const fresh: SearchableMatch = { + id, + title: [], + network: [], + participants: [], + matchedSearchTerms: new Set(), + }; + matches.set(id, fresh); + return fresh; + }; + + for (const searchTerm of searchTerms) { + for (const property of properties) { + const results = this.fuse.search({ [`searchFields.${property}`]: searchTerm } as Fuse.Expression); + for (const result of results) { + if (result.score == null) continue; + const match = getOrCreateMatch(result.item.id); + match[property].push(result.score); + match.matchedSearchTerms.add(searchTerm); + } + } + } + + const requiredTerms = searchTerms.filter((term) => !STOP_WORDS.has(term)); + const resp: SearchIndexResult[] = []; + for (const match of matches.values()) { + if (requiredTerms.length > 0 && !requiredTerms.every((term) => match.matchedSearchTerms.has(term))) { + continue; + } + resp.push({ + id: match.id, + score: computeScoreFromMatch(match), + matchSearchTerms: match.matchedSearchTerms, + }); + } + return resp; + } +} + +export const buildSearchFields = (chat: BeeperDesktop.Chat): ChatSearchFields => { + const title = normalizeSearchValue(chat.title || ""); + const network = normalizeSearchValue(chat.network || ""); + const participants = + chat.participants?.items + ?.slice(0, MAX_PARTICIPANTS_INDEXED) + .map((participant) => { + const values = [ + participant.fullName, + participant.username, + participant.email, + participant.phoneNumber ? String(participant.phoneNumber).replace(/\D/g, "") : undefined, + ] + .filter(Boolean) + .map((value) => normalizeSearchValue(String(value))); + return values.join(" ").trim(); + }) + .filter(Boolean) ?? []; + + return { title, network, participants }; +}; + +type ChatListViewProps = { + stateKey: string; + navigationTitle: string; + searchPlaceholder: string; + defaultFilters: ChatFilters; + showSmartSections?: boolean; + showUnreadSection?: boolean; + showPinnedSection?: boolean; +}; + +export function ChatListView({ + stateKey, + navigationTitle, + searchPlaceholder, + defaultFilters, + showSmartSections = false, + showUnreadSection = true, + showPinnedSection = true, +}: ChatListViewProps) { + const [searchText, setSearchText] = useState(""); + const [filters, setFilters] = useCachedState(`${stateKey}:filters`, defaultFilters); + const [isShowingDetail, setIsShowingDetail] = useCachedState(`${stateKey}:showing-detail`, false); + const { value: recentChatIDs = [], setValue: setRecentChatIDs } = useLocalStorage( + `${stateKey}:recent-ids`, + [], + ); + const { setValue: setLastChatID } = useLocalStorage(`${stateKey}:last-id`, null); + const { value: indexStateRaw = defaultIndexState, setValue: setIndexState, isLoading: isIndexLoading } = + useLocalStorage(`${stateKey}:index:v2`, defaultIndexState); + + const trimmedQuery = searchText.trim(); + const normalizedType = (filters.type as string) === "channel" ? "any" : filters.type; + const indexState = useMemo(() => normalizeIndexState(indexStateRaw ?? defaultIndexState), [indexStateRaw]); + const indexRef = useRef(indexState); + const refreshInFlight = useRef(false); + const initialRefreshDone = useRef(false); + const [isIndexRefreshing, setIsIndexRefreshing] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + indexRef.current = indexState; + }, [indexState]); + + const fetchInbox = async ( + inbox: ChatInbox | undefined, + mode: "full" | "incremental", + cursors: ChatIndexState["cursors"][ChatInbox], + onPage?: (payload: { + items: IndexedChat[]; + newestCursor?: string | null; + oldestCursor?: string | null; + done: boolean; + }) => void | Promise, + ) => { + const maxPages = mode === "incremental" ? INDEX_MAX_PAGES_INCREMENTAL : INDEX_MAX_PAGES; + const items: IndexedChat[] = []; + let itemsCount = 0; + let cursor: string | null | undefined = undefined; + let newestCursor = cursors?.newestCursor ?? null; + let oldestCursor = cursors?.oldestCursor ?? null; + + for (let page = 0; page < maxPages; page += 1) { + const response = await searchChats({ + inbox: inbox ? toApiInbox(inbox) : undefined, + includeMuted: true, + type: "any", + limit: INDEX_PAGE_LIMIT, + cursor: cursor ?? undefined, + direction: "before", + }); + const pageItems = response.items ?? []; + if (page === 0 && response.newestCursor) { + newestCursor = response.newestCursor; + } + if (response.oldestCursor) { + oldestCursor = response.oldestCursor; + } + if (pageItems.length === 0) break; + const mapped = pageItems.map((chat) => ({ + chat: summarizeChatForIndex(chat), + inbox: inbox ?? (chat.isArchived ? "archive" as ChatInbox : "inbox" as ChatInbox), + searchFields: buildSearchFields(chat), + })); + itemsCount += mapped.length; + if (!onPage) { + items.push(...mapped); + } + + const reachedLimit = itemsCount >= MAX_INDEXED_CHATS_PER_INBOX; + const done = reachedLimit || mode === "incremental" || !response.hasMore; + if (onPage) { + await onPage({ + items: mapped, + newestCursor, + oldestCursor, + done, + }); + } + + if (done) break; + const nextCursor = response.oldestCursor ?? response.nextCursor ?? response.cursor; + if (!nextCursor) break; + cursor = nextCursor; + } + + return { items, newestCursor, oldestCursor }; + }; + + const refreshIndex = async (mode: "full" | "incremental" = "incremental") => { + if (refreshInFlight.current) return; + refreshInFlight.current = true; + const showRefreshState = mode === "full" || indexRef.current.items.length === 0; + if (showRefreshState) setIsIndexRefreshing(true); + setError(undefined); + + const base = mode === "full" ? defaultIndexState : (indexRef.current ?? defaultIndexState); + const nextState: ChatIndexState = { + items: mode === "full" ? [] : [...base.items], + cursors: { + inbox: { ...base.cursors.inbox }, + "low-priority": { ...base.cursors["low-priority"] }, + archive: { ...base.cursors.archive }, + }, + updatedAt: base.updatedAt, + }; + + try { + let lastPersist = 0; + const persistIfNeeded = async (force = false) => { + const now = Date.now(); + if (!force && now - lastPersist < 250) return; + lastPersist = now; + await setIndexState({ ...nextState, updatedAt: now }); + }; + + for (const inbox of INDEXED_INBOXES) { + const result = await fetchInbox(inbox, mode, base.cursors[inbox], async (page) => { + nextState.items = mergeIndexedChats(nextState.items, page.items); + nextState.cursors[inbox] = { + newestCursor: page.newestCursor ?? nextState.cursors[inbox].newestCursor ?? null, + oldestCursor: page.oldestCursor ?? nextState.cursors[inbox].oldestCursor ?? null, + }; + if (nextState.items.length > MAX_INDEXD_CHATS_TARGET * 2) { + nextState.items = sortIndexedChatsByActivity(nextState.items).slice(0, MAX_INDEXD_CHATS_TARGET); + } + if (mode === "full") await persistIfNeeded(page.done); + }); + nextState.cursors[inbox] = { + newestCursor: result.newestCursor ?? nextState.cursors[inbox].newestCursor ?? null, + oldestCursor: result.oldestCursor ?? nextState.cursors[inbox].oldestCursor ?? null, + }; + } + + nextState.items = sortIndexedChatsByActivity(nextState.items).slice(0, MAX_INDEXD_CHATS_TARGET); + if (mode === "full" || indexItemsChanged(base.items, nextState.items)) { + nextState.updatedAt = Date.now(); + await setIndexState(nextState); + } + } catch (err) { + setError(err); + } finally { + refreshInFlight.current = false; + if (showRefreshState) setIsIndexRefreshing(false); + } + }; + + useEffect(() => { + if (isIndexLoading) return; + if (initialRefreshDone.current) return; + initialRefreshDone.current = true; + const age = Date.now() - indexState.updatedAt; + if (indexState.items.length === 0 || age > INDEX_REFRESH_MAX_AGE_MS) { + void refreshIndex("full"); + return; + } + if (age > INDEX_INCREMENTAL_MIN_AGE_MS) { + void refreshIndex("incremental"); + } + }, [isIndexLoading, indexState.items.length]); + + const tokens = useMemo(() => parseSearchTerms(trimmedQuery), [trimmedQuery]); + const normalizedQuery = useMemo(() => normalizeSearchValue(trimmedQuery), [trimmedQuery]); + const searchIndex = useMemo(() => { + if (tokens.length === 0) return null; + const collection = indexState.items.map((item) => ({ id: item.chat.id, searchFields: item.searchFields })); + return new ThreadSearchIndex(collection); + }, [indexState.items, tokens.length]); + const chats = useMemo(() => { + const filtered = indexState.items.filter((item) => { + if (filters.inbox !== "all" && item.inbox !== filters.inbox) return false; + if (!filters.includeMuted && item.chat.isMuted) return false; + if (filters.unreadOnly && (item.chat.unreadCount ?? 0) === 0) return false; + if (normalizedType !== "any" && item.chat.type !== normalizedType) return false; + return true; + }); + if (tokens.length === 0) { + return sortChatsByActivity(filtered.map((item) => item.chat)); + } + + const now = Date.now(); + const filteredById = new Map(filtered.map((item) => [item.chat.id, item])); + const results = searchIndex ? searchIndex.search(trimmedQuery, ["title", "network", "participants"]) : []; + const scored = results + .map((result) => { + const indexed = filteredById.get(result.id); + if (!indexed) return null; + const title = indexed.searchFields.title; + return { + chat: indexed.chat, + exactTitle: normalizedQuery.length > 0 && title === normalizedQuery, + prefixTitle: normalizedQuery.length > 0 && title.startsWith(normalizedQuery), + titleHits: result.score.title.hits, + participantHits: result.score.participants.hits, + networkHits: result.score.network.hits, + isSingle: indexed.chat.type === "single", + timestamp: getChatTimestamp(indexed.chat), + }; + }) + .filter( + ( + item, + ): item is { + chat: BeeperDesktop.Chat; + exactTitle: boolean; + prefixTitle: boolean; + titleHits: number; + participantHits: number; + networkHits: number; + isSingle: boolean; + timestamp: number; + } => Boolean(item), + ); + + const recencyBoost = (timestamp: number) => Math.max(0, 30 - (now - timestamp) / (24 * 60 * 60 * 1000)); + + scored.sort((a, b) => { + if (a.exactTitle !== b.exactTitle) return a.exactTitle ? -1 : 1; + if (a.prefixTitle !== b.prefixTitle) return a.prefixTitle ? -1 : 1; + + const aRecency = recencyBoost(a.timestamp); + const bRecency = recencyBoost(b.timestamp); + if (aRecency !== bRecency) return bRecency - aRecency; + + if (a.titleHits !== b.titleHits) return b.titleHits - a.titleHits; + if (a.participantHits !== b.participantHits) return b.participantHits - a.participantHits; + if (a.isSingle !== b.isSingle) return a.isSingle ? -1 : 1; + if (a.networkHits !== b.networkHits) return b.networkHits - a.networkHits; + return b.timestamp - a.timestamp; + }); + + return scored.map((item) => item.chat); + }, [ + filters.includeMuted, + filters.inbox, + filters.type, + filters.unreadOnly, + indexState.items, + normalizedQuery, + normalizedType, + searchIndex, + tokens, + trimmedQuery, + ]); + + const frecencyInput = useMemo(() => [...chats], [chats]); + const { + data: frecencyChats = [], + visitItem, + resetRanking, + } = useFrecencySorting(frecencyInput, { + key: (chat) => chat.id, + }); + + const inboxDropdown = ( + setFilters((prev) => ({ ...prev, inbox: value as InboxFilter }))} + > + + + + + + ); + + const toggleFilter = (key: "unreadOnly" | "includeMuted") => setFilters((prev) => ({ ...prev, [key]: !prev[key] })); + + const setChatType = (type: ChatTypeFilter) => setFilters((prev) => ({ ...prev, type })); + + const isLoading = isIndexLoading || (isIndexRefreshing && indexState.items.length === 0); + + const markChatVisited = (chat: BeeperDesktop.Chat) => { + if (!showSmartSections) return; + visitItem(chat); + void setLastChatID(chat.id); + const next = [chat.id, ...(recentChatIDs ?? []).filter((id) => id !== chat.id)].slice(0, 12); + void setRecentChatIDs(next); + }; + + const handleArchiveChat = async (chat: BeeperDesktop.Chat, archived: boolean) => { + const confirmed = await confirmAlert({ + title: archived ? "Archive?" : "Unarchive?", + message: archived ? "This will move the chat to Archive." : "This will move the chat back to your Inbox.", + primaryAction: { title: archived ? "Archive" : "Unarchive", style: Alert.ActionStyle.Destructive }, + }); + if (!confirmed) return; + + const toast = await showToast({ + style: Toast.Style.Animated, + title: archived ? "Archiving chat" : "Unarchiving chat", + }); + try { + await archiveChat(chat.id, archived); + toast.style = Toast.Style.Success; + toast.title = archived ? "Chat archived" : "Chat restored"; + void refreshIndex("full"); + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Chat update failed"; + toast.message = getErrorMessage(err); + } + }; + + const clearReminder = async (chat: BeeperDesktop.Chat) => { + const confirmed = await confirmAlert({ + title: "Dismiss Reminder?", + message: "This removes the reminder for this chat.", + primaryAction: { title: "Dismiss Reminder", style: Alert.ActionStyle.Destructive }, + }); + if (!confirmed) return; + + const toast = await showToast({ style: Toast.Style.Animated, title: "Dismissing reminder" }); + try { + await deleteChatReminder(chat.id); + toast.style = Toast.Style.Success; + toast.title = "Reminder dismissed"; + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to dismiss reminder"; + toast.message = getErrorMessage(err); + } + }; + + const setQuickReminder = async (chat: BeeperDesktop.Chat, remindAt: Date, label: string) => { + const toast = await showToast({ style: Toast.Style.Animated, title: `Setting reminder (${label})` }); + try { + await createChatReminder(chat.id, { + remindAtMs: remindAt.getTime(), + dismissOnIncomingMessage: false, + }); + toast.style = Toast.Style.Success; + toast.title = "Reminder set"; + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Reminder failed"; + toast.message = getErrorMessage(err); + } + }; + + const pinnedChats = showPinnedSection ? chats.filter((chat) => chat.isPinned) : []; + const unreadChats = chats.filter((chat) => chat.unreadCount > 0 && !(showPinnedSection && chat.isPinned)); + + + const pinnedIDs = new Set(pinnedChats.map((chat) => chat.id)); + const unreadIDs = showUnreadSection ? new Set(unreadChats.map((chat) => chat.id)) : new Set(); + const recentIDs = new Set(recentChatIDs ?? []); + + const chatById = useMemo(() => new Map(chats.map((chat) => [chat.id, chat])), [chats]); + const recentChats = useMemo( + () => + (recentChatIDs ?? []) + .map((id) => chatById.get(id)) + .filter((chat): chat is BeeperDesktop.Chat => Boolean(chat)) + .filter((chat) => !pinnedIDs.has(chat.id) && !unreadIDs.has(chat.id)), + [chatById, recentChatIDs, pinnedIDs, unreadIDs], + ); + + const frequentChats = useMemo( + () => + frecencyChats + .filter((chat) => !pinnedIDs.has(chat.id) && !unreadIDs.has(chat.id) && !recentIDs.has(chat.id)) + .slice(0, 8), + [frecencyChats, pinnedIDs, unreadIDs, recentIDs], + ); + + const showSections = showSmartSections && trimmedQuery.length === 0; + + const otherChats = chats.filter( + (chat) => + !pinnedIDs.has(chat.id) && + !unreadIDs.has(chat.id) && + (!showSections || (!recentIDs.has(chat.id) && !frequentChats.some((item) => item.id === chat.id))), + ); + const showUnread = showUnreadSection && unreadChats.length > 0; + + const renderChatItem = (chat: BeeperDesktop.Chat) => { + const lastActivity = parseDate(chat.lastActivity); + const openLink = getRaycastFocusLink({ chatID: chat.id }); + const accessories = [ + ...(chat.unreadCount > 0 ? [{ text: `${chat.unreadCount} unread` }] : []), + ...(chat.isPinned ? [{ icon: Icon.Pin }] : []), + ...(chat.isMuted ? [{ icon: Icon.SpeakerOff }] : []), + ...(chat.isArchived ? [{ icon: Icon.Archive }] : []), + ...(lastActivity ? [{ date: lastActivity }] : []), + ]; + + return ( + + + + + + + + {chat.isPinned && } + {chat.isMuted && ( + + )} + {chat.isArchived && ( + + )} + + {chat.lastActivity && ( + + )} + + } + /> + ) : null + } + actions={ + + + { + markChatVisited(chat); + return focusApp({ chatID: chat.id }); + }} + /> + } + onPush={() => markChatVisited(chat)} + /> + } + onPush={() => markChatVisited(chat)} + /> + setIsShowingDetail((prev) => !prev)} + /> + + + {openLink && ( + + )} + + + + setQuickReminder(chat, new Date(Date.now() + 60 * 60 * 1000), "1 hour")} + /> + setQuickReminder(chat, new Date(Date.now() + 2 * 60 * 60 * 1000), "2 hours")} + /> + { + const date = new Date(); + date.setDate(date.getDate() + 1); + date.setHours(9, 0, 0, 0); + return setQuickReminder(chat, date, "tomorrow"); + }} + /> + { + const date = new Date(); + date.setDate(date.getDate() + 7); + date.setHours(9, 0, 0, 0); + return setQuickReminder(chat, date, "next week"); + }} + /> + } /> + + clearReminder(chat)} /> + + + } /> + handleArchiveChat(chat, !chat.isArchived)} + /> + + + + {chat.title && } + + + refreshIndex("full")} + /> + {showSmartSections && ( + resetRanking()} /> + )} + {showSmartSections && ( + setRecentChatIDs([])} /> + )} + + toggleFilter("unreadOnly")} + /> + toggleFilter("includeMuted")} + /> + setChatType("any")} /> + setChatType("single")} /> + setChatType("group")} /> + + + + } + /> + ); + }; + + return ( + + {pinnedChats.length > 0 && {pinnedChats.map(renderChatItem)}} + {showUnread && {unreadChats.map(renderChatItem)}} + {showSections && recentChats.length > 0 && ( + {recentChats.map(renderChatItem)} + )} + {showSections && frequentChats.length > 0 && ( + {frequentChats.map(renderChatItem)} + )} + {otherChats.length > 0 && {otherChats.map(renderChatItem)}} + {!isLoading && chats.length === 0 && ( + + )} + + ); +} + +export function ChatThread({ chat }: { chat: BeeperDesktop.Chat }) { + const [query, setQuery] = useState(""); + const [messages, setMessages] = useState([]); + const [cursor, setCursor] = useState(null); + const [hasMore, setHasMore] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [error, setError] = useState(); + const [isShowingDetail, setIsShowingDetail] = useCachedState("chat-thread:showing-detail", false); + + const trimmedQuery = query.trim(); + + const PAGE_LIMIT = 50; + + /** Derive the pagination cursor from the last message's sortKey. */ + const getLastSortKey = (items: BeeperDesktop.Message[]): string | null => { + if (items.length === 0) return null; + return items[items.length - 1].sortKey ?? null; + }; + + const loadFirstPage = async (isCancelled?: () => boolean) => { + setIsLoading(true); + setError(undefined); + + try { + const result = await listChatMessages(chat.id, { limit: PAGE_LIMIT }); + if (isCancelled?.()) return; + const items = result.items ?? []; + setMessages(items); + setCursor(getLastSortKey(items)); + setHasMore(Boolean(result.hasMore)); + } catch (err) { + if (!isCancelled?.()) { + setError(getErrorMessage(err)); + } + } finally { + if (!isCancelled?.()) { + setIsLoading(false); + } + } + }; + + const loadMore = async () => { + if (!cursor) { + await showHUD("No more messages to load"); + return; + } + setIsLoadingMore(true); + try { + const nextPage = await listChatMessages(chat.id, { cursor, direction: "before", limit: PAGE_LIMIT }); + const items = nextPage.items ?? []; + setMessages((prev) => [...prev, ...items]); + setCursor(getLastSortKey(items) ?? cursor); + setHasMore(Boolean(nextPage.hasMore)); + if (items.length === 0) { + setHasMore(false); + await showHUD("No older messages found"); + } + } catch (err) { + await showHUD(`Failed to load more: ${getErrorMessage(err)}`); + } finally { + setIsLoadingMore(false); + } + }; + + useEffect(() => { + let cancelled = false; + loadFirstPage(() => cancelled); + return () => { + cancelled = true; + }; + }, [chat.id]); + + const filteredMessages = useMemo(() => { + const nonEmpty = messages.filter((m) => !!(m.text?.trim()) || (m.attachments && m.attachments.length > 0)); + if (!trimmedQuery) return nonEmpty; + const lower = trimmedQuery.toLowerCase(); + return nonEmpty.filter((m) => m.text?.toLowerCase().includes(lower)); + }, [messages, trimmedQuery]); + + const showLoadMore = !isLoading && hasMore; + + // Build a participant name lookup from message senders + chat participants + const nameMap = useMemo(() => { + const map = new Map(); + for (const msg of messages) { + if (msg.senderName) map.set(msg.senderID, msg.senderName); + } + if (chat.participants?.items) { + for (const user of chat.participants.items) { + if (user.fullName) map.set(user.id, user.fullName); + } + } + return map; + }, [messages, chat.participants]); + + return ( + + {filteredMessages.map((message) => { + const preview = getMessagePreview(message); + const reactionsShort = formatReactionsShort(message.reactions); + const reactionsDetailed = formatReactionsDetailed(message.reactions, nameMap); + const timestamp = parseDate(message.timestamp); + const sender = message.senderName || (message.isSender ? "You" : "Unknown"); + const messageID = getMessageID(message); + + return ( + + {reactionsDetailed && ( + + {reactionsDetailed.entries.map((e) => ( + + ))} + + )} + + + + {message.isSender && ( + + + + )} + + } + /> + ) : null + } + accessories={[ + ...(reactionsShort ? [{ tag: { value: reactionsShort, color: Color.SecondaryText } }] : []), + ...(timestamp ? [{ date: timestamp }] : []), + ]} + actions={ + setIsShowingDetail((prev) => !prev)} + isShowingDetail={isShowingDetail} + /> + } + /> + ); + })} + {showLoadMore && ( + + + + } + /> + )} + {!isLoading && filteredMessages.length === 0 && ( + + )} + + ); +} + +function MessageActions({ + chat, + message, + onRefresh, + onLoadMore, + onToggleDetail, + isShowingDetail, +}: { + chat: BeeperDesktop.Chat; + message: BeeperDesktop.Message; + onRefresh: () => Promise; + onLoadMore: () => Promise; + onToggleDetail: () => void; + isShowingDetail: boolean; +}) { + const messageID = getMessageID(message); + const messageLink = getRaycastFocusLink({ chatID: chat.id, messageID }); + + return ( + + + focusApp({ chatID: chat.id, messageID: messageID })} + /> + focusApp({ chatID: chat.id })} /> + {messageLink && ( + + )} + + + } + /> + } /> + {message.isSender && message.text && ( + } + /> + )} + + + + {message.text && } + {message.text && } + {message.text && ( + + )} + + + + } /> + + + + + ); +} + +function MessageDetail({ chat, message }: { chat: BeeperDesktop.Chat; message: BeeperDesktop.Message }) { + const messageID = getMessageID(message); + return ( + + ); +} + +export function ComposeMessageForm({ + chat, + replyToMessageID, + initialText, +}: { + chat: BeeperDesktop.Chat; + replyToMessageID?: string; + initialText?: string; +}) { + const { pop } = useNavigation(); + const { + value: draftText, + setValue: setDraftText, + removeValue: removeDraft, + } = useLocalStorage(`chat:draft:${chat.id}`, ""); + + const draftApplied = useRef(false); + const setDraftRef = useRef(setDraftText); + setDraftRef.current = setDraftText; + + const { handleSubmit, itemProps, values, setValue } = useForm<{ + text: string; + }>({ + initialValues: { + text: initialText ?? draftText ?? "", + }, + onSubmit: async (values) => { + const text = values.text?.trim(); + if (!text) { + await showHUD("Add text to send"); + return; + } + + const toast = await showToast({ style: Toast.Style.Animated, title: "Sending message" }); + try { + await sendMessage(chat.id, { text, replyToMessageID }); + toast.style = Toast.Style.Success; + toast.title = "Message sent"; + await removeDraft(); + pop(); + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Message failed"; + toast.message = getErrorMessage(err); + } + }, + }); + + // Apply saved draft to form once when localStorage value loads + useEffect(() => { + if (draftApplied.current || initialText) return; + if (draftText && values.text !== draftText) { + draftApplied.current = true; + setValue("text", draftText); + } + }, [draftText, initialText, setValue, values.text]); + + // Persist form changes to localStorage (ref breaks the cycle) + useEffect(() => { + if (values.text !== undefined) { + void setDraftRef.current(values.text); + } + }, [values.text]); + + return ( +
+ + {values.text?.length ? ( + { + setValue("text", ""); + await removeDraft(); + }} + /> + ) : null} + + } + > + + + + ); +} + +function EditMessageForm({ chat, message }: { chat: BeeperDesktop.Chat; message: BeeperDesktop.Message }) { + const { pop } = useNavigation(); + const messageID = getMessageID(message); + const { handleSubmit, itemProps } = useForm<{ text: string }>({ + initialValues: { text: message.text || "" }, + onSubmit: async (values) => { + const text = values.text?.trim(); + if (!text) { + await showHUD("Message can't be empty"); + return; + } + + const toast = await showToast({ style: Toast.Style.Animated, title: "Updating message" }); + try { + await updateMessage(chat.id, messageID, { text }); + toast.style = Toast.Style.Success; + toast.title = "Message updated"; + pop(); + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Update failed"; + toast.message = getErrorMessage(err); + } + }, + }); + + return ( +
+ + + } + > + + + ); +} + +export function ReminderForm({ chat }: { chat: BeeperDesktop.Chat }) { + const { pop } = useNavigation(); + const { handleSubmit, itemProps } = useForm<{ remindAt: Date; dismissOnIncoming: boolean }>({ + initialValues: { + remindAt: new Date(Date.now() + 60 * 60 * 1000), + dismissOnIncoming: false, + }, + onSubmit: async (values) => { + const remindAtMs = values.remindAt.getTime(); + const toast = await showToast({ style: Toast.Style.Animated, title: "Setting reminder" }); + try { + await createChatReminder(chat.id, { + remindAtMs, + dismissOnIncomingMessage: values.dismissOnIncoming, + }); + toast.style = Toast.Style.Success; + toast.title = "Reminder set"; + pop(); + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Reminder failed"; + toast.message = getErrorMessage(err); + } + }, + }); + + return ( +
+ + + } + > + + + + ); +} + +export function ChatDetails({ chat }: { chat: BeeperDesktop.Chat }) { + const { data, isLoading } = useBeeperDesktop(async () => { + return retrieveChat(chat.id, { maxParticipantCount: 50 }); + }); + + const participantLines = useMemo(() => { + const participants = data?.participants.items ?? []; + if (participants.length === 0) return "No participants available."; + return participants + .map((participant) => { + const name = participant.fullName || participant.username || participant.id; + return `- ${name}`; + }) + .join("\n"); + }, [data?.participants.items]); + + return ( + + ); +} + +function RecentChatsCommand() { + return ( + + ); +} + +export default withAccessToken(createBeeperOAuth())(RecentChatsCommand); diff --git a/src/components/ChatListItem.tsx b/src/components/ChatListItem.tsx new file mode 100644 index 0000000..068b2b1 --- /dev/null +++ b/src/components/ChatListItem.tsx @@ -0,0 +1,45 @@ +import { List, ActionPanel, Action, Icon } from "@raycast/api"; +import type { ReactNode } from "react"; +import { Translations } from "../locales/en"; +import { getChatIcon } from "../utils/chatIcon"; + +interface Chat { + id: string; + network: string; + type?: string; + participants?: { + items?: Array<{ isSelf?: boolean; imgURL?: string }>; + }; + title?: string; + avatarUrl?: string; + onOpen?: () => void; + detailsTarget?: ReactNode; +} + +interface ChatListItemProps { + chat: Chat; + translations: Translations; + accessories?: List.Item.Props["accessories"]; + showDetails?: boolean; +} + +export function ChatListItem({ chat, translations, accessories = [], showDetails = false }: ChatListItemProps) { + return ( + + chat.onOpen?.()} /> + {showDetails && chat.detailsTarget && ( + + )} + + + } + /> + ); +} diff --git a/src/contacts.tsx b/src/contacts.tsx new file mode 100644 index 0000000..9c3c72e --- /dev/null +++ b/src/contacts.tsx @@ -0,0 +1,272 @@ +import { Action, ActionPanel, Icon, List, Toast, showToast, useNavigation } from "@raycast/api"; +import { useCachedPromise, withAccessToken } from "@raycast/utils"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + createBeeperOAuth, + createChat, + focusApp, + listAccounts, + retrieveChat, + searchChats, + searchContacts, +} from "./api"; +import { ChatThread } from "./chat"; +import { t } from "./locales"; +import { getBeeperAppPath } from "./utils"; + +const getContactSortName = (contact: { + fullName?: string; + username?: string; + phoneNumber?: string; + email?: string; + id: string; +}) => (contact.fullName || contact.username || contact.phoneNumber || contact.email || contact.id || "").toLowerCase(); + +const sortContacts = < + T extends { fullName?: string; username?: string; phoneNumber?: string; email?: string; id: string }, +>( + items: T[], +) => + [...items].sort((a, b) => { + const aName = getContactSortName(a); + const bName = getContactSortName(b); + const aEmpty = !aName; + const bEmpty = !bName; + if (aEmpty && !bEmpty) return 1; + if (!aEmpty && bEmpty) return -1; + return aName.localeCompare(bName); + }); + +export function ContactsView() { + const translations = t(); + const c = translations.commands.contacts; + + const [query, setQuery] = useState(""); + const { data: accounts = [], isLoading: isLoadingAccounts } = useCachedPromise(listAccounts, [], { + keepPreviousData: true, + }); + const [accountFilter, setAccountFilter] = useState("all"); + const { push } = useNavigation(); + const beeperAppPath = getBeeperAppPath(); + + useEffect(() => { + if (accountFilter === "all" && accounts.length === 1) { + setAccountFilter(accounts[0].accountID); + } + }, [accountFilter, accounts]); + + const shouldSearch = query.trim().length > 0 && accounts.length > 0; + const accountMap = useMemo(() => new Map(accounts.map((account) => [account.accountID, account])), [accounts]); + const accountsKey = useMemo(() => accounts.map((account) => account.accountID).join("|"), [accounts]); + const lastPartialErrorKey = useRef(null); + + const { + data: contacts = [], + isLoading, + error, + } = useCachedPromise( + async (termInput: string, filter: string, accountsSnapshotKey: string) => { + if (!shouldSearch) return []; + const term = termInput.trim(); + if (!term) return []; + + if (filter === "all") { + const results = await Promise.allSettled( + accounts.map(async (account) => { + const items = await searchContacts(account.accountID, term); + return items.map((contact) => ({ ...contact, accountID: account.accountID })); + }), + ); + + const fulfilled = results.flatMap((result) => (result.status === "fulfilled" ? result.value : [])); + const rejected = results.filter((result) => result.status === "rejected"); + + if (rejected.length === results.length && results.length > 0) { + const reason = rejected[0].reason; + throw reason instanceof Error ? reason : new Error(String(reason)); + } + + if (rejected.length > 0) { + const toastKey = `${term}-${rejected.length}-${accountsSnapshotKey}`; + if (lastPartialErrorKey.current !== toastKey) { + lastPartialErrorKey.current = toastKey; + await showToast({ + style: Toast.Style.Failure, + title: c.partialErrorTitle, + message: c.partialErrorMessage, + }); + } + } + + return sortContacts(fulfilled); + } + + const items = await searchContacts(filter, term); + return sortContacts(items.map((contact) => ({ ...contact, accountID: filter }))); + }, + [query, accountFilter, accountsKey], + { keepPreviousData: true }, + ); + + const dropdown = ( + setAccountFilter(value)} + isLoading={isLoadingAccounts} + > + + {accounts.map((account) => ( + + ))} + + ); + + const emptyView = (() => { + if (error && shouldSearch && !isLoading) { + return ( + + + + ) : null + } + /> + ); + } + if (!isLoading && contacts.length === 0) { + return ( + + ); + } + return null; + })(); + + return ( + + {contacts.map((contact) => { + const account = accountMap.get((contact as { accountID?: string }).accountID || ""); + const title = contact.fullName || contact.username || contact.id; + const subtitle = contact.username && contact.fullName ? contact.username : contact.email || contact.phoneNumber; + const accountLabel = account + ? `${account.network || c.accountFallback} • ${ + account.user?.fullName || + account.user?.username || + account.user?.email || + account.user?.phoneNumber || + account.accountID + }` + : undefined; + return ( + + { + const selectedAccountID = (contact as { accountID?: string }).accountID; + if (!selectedAccountID) return; + const toast = await showToast({ style: Toast.Style.Animated, title: c.openingChatToast }); + try { + const contactName = contact.fullName || contact.username || contact.id; + const existing = await searchChats({ + accountIDs: [selectedAccountID], + type: "single", + participantQuery: contactName, + includeMuted: true, + }); + + let matchedChat: typeof existing.items[0] | undefined; + for (const candidate of existing.items ?? []) { + const full = await retrieveChat(candidate.id, { maxParticipantCount: 10 }); + const hasContact = full.participants?.items?.some( + (p) => + p.id === contact.id || + (contact.username && p.username && p.username === contact.username), + ); + if (hasContact) { + matchedChat = full; + break; + } + } + + if (matchedChat) { + toast.style = Toast.Style.Success; + toast.title = c.chatFoundToast; + push(); + } else { + toast.title = c.creatingChatToast; + const response = await createChat({ + accountID: selectedAccountID, + participantIDs: [contact.id], + type: "single", + }); + toast.style = Toast.Style.Success; + toast.title = c.chatCreatedToast; + const newChatID = + (response as { chatID?: string }).chatID || (response as { id?: string }).id || undefined; + if (newChatID) { + try { + const chat = await retrieveChat(newChatID, { maxParticipantCount: 0 }); + push(); + } catch { + // fallback: keep the list visible if chat load fails + } + } + } + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = c.openChatFailedToast; + toast.message = error instanceof Error ? error.message : translations.common.unknownError; + } + }} + /> + focusApp()} /> + + + } + /> + ); + })} + {emptyView} + + ); +} + +function ContactsCommand() { + return ; +} + +export default withAccessToken(createBeeperOAuth())(ContactsCommand); diff --git a/src/find-chat.tsx b/src/find-chat.tsx deleted file mode 100644 index c2a368e..0000000 --- a/src/find-chat.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ActionPanel, Action, List, Icon } from "@raycast/api"; -import { withAccessToken } from "@raycast/utils"; -import { useState } from "react"; -import { useBeeperDesktop, createBeeperOAuth, focusApp } from "./api"; - -function FindChatCommand() { - const [searchText, setSearchText] = useState(""); - const { data: chats = [], isLoading } = useBeeperDesktop(async (client) => { - const result = await client.chats.search({ query: searchText }); - return result.data; - }); - - return ( - - {chats.map((chat) => ( - 0 ? [{ text: `${chat.unreadCount} unread` }] : []), - ...(chat.isPinned ? [{ icon: Icon.Pin }] : []), - ...(chat.isMuted ? [{ icon: Icon.SpeakerOff }] : []), - ]} - actions={ - - focusApp({ chatID: chat.chatID })} - /> - - - } - /> - ))} - {!isLoading && chats.length === 0 && ( - - )} - - ); -} - -export default withAccessToken(createBeeperOAuth())(FindChatCommand); diff --git a/src/focus-app.tsx b/src/focus-app.tsx index 4dba9b0..23e1ba3 100644 --- a/src/focus-app.tsx +++ b/src/focus-app.tsx @@ -1,8 +1,16 @@ +import { LaunchProps } from "@raycast/api"; import { withAccessToken } from "@raycast/utils"; import { focusApp, createBeeperOAuth } from "./api"; -async function FocusAppCommand() { - await focusApp(); +type FocusAppArguments = { + chatID?: string; + messageID?: string; + draftText?: string; + draftAttachmentPath?: string; +}; + +async function FocusAppCommand(props: LaunchProps<{ arguments?: FocusAppArguments }>) { + await focusApp(props.arguments ?? {}); } export default withAccessToken(createBeeperOAuth())(FocusAppCommand); diff --git a/src/hooks/useChatSearch.ts b/src/hooks/useChatSearch.ts new file mode 100644 index 0000000..c9dc971 --- /dev/null +++ b/src/hooks/useChatSearch.ts @@ -0,0 +1,36 @@ +import { useBeeperDesktop } from "../api"; + +export function useChatSearch(searchText: string, includeEmpty = false) { + return useBeeperDesktop( + async (client, query, includeEmptyResults) => { + if (!query && !includeEmptyResults) return []; + + const allChats = []; + let cursor: string | null = null; + let hasMore = true; + const MAX_PAGES = 10; // Safety limit: max 10 pages * 100 = 1000 results + let pageCount = 0; + + // For search, load more results initially (100 per batch) + // since users expect comprehensive results when searching + while (hasMore && pageCount < MAX_PAGES) { + const params = query + ? { query, limit: 100, ...(cursor ? { cursor, direction: "older" as const } : {}) } + : { limit: 100, ...(cursor ? { cursor, direction: "older" as const } : {}) }; + + const page = await client.chats.search(params); + allChats.push(...page.items); + + cursor = page.oldestCursor; + hasMore = page.hasMore; + pageCount++; + + // Early exit if we have enough results + if (allChats.length >= 200) break; + } + + return allChats; + }, + [searchText, includeEmpty] as [string, boolean], + ); +} diff --git a/src/hooks/useMessageSearch.ts b/src/hooks/useMessageSearch.ts new file mode 100644 index 0000000..a6a4e26 --- /dev/null +++ b/src/hooks/useMessageSearch.ts @@ -0,0 +1,46 @@ +import { useBeeperDesktop } from "../api"; + +export function useMessageSearch(searchText: string) { + return useBeeperDesktop( + async (client, query) => { + if (!query) return []; + const allMessages = []; + // Search for messages matching the query + // We limit to a reasonable number or let the iterator handle it, + // but for a simple list view, fetching the first batch is usually enough. + // The API returns a PagePromise, so we can iterate. + for await (const message of client.messages.search({ query })) { + allMessages.push(message); + if (allMessages.length >= 50) break; + } + + // Deduplicate chat IDs + const uniqueChatIDs = Array.from(new Set(allMessages.map((m) => m.chatID))); + + // Fetch chats with simple concurrency control (batch size of 5) + const chatMap = new Map(); + const BATCH_SIZE = 5; + + for (let i = 0; i < uniqueChatIDs.length; i += BATCH_SIZE) { + const batch = uniqueChatIDs.slice(i, i + BATCH_SIZE); + await Promise.all( + batch.map(async (chatID) => { + try { + const chat = await client.chats.retrieve({ chatID }); + chatMap.set(chatID, chat); + } catch (e) { + console.warn(`Failed to load chat ${chatID}`, e); + } + }), + ); + } + + // Map messages to include chat details + return allMessages.map((message) => ({ + ...message, + chat: chatMap.get(message.chatID) || null, + })); + }, + [searchText] as [string], + ); +} diff --git a/src/list-accounts.tsx b/src/list-accounts.tsx index 70adbec..d2b8068 100644 --- a/src/list-accounts.tsx +++ b/src/list-accounts.tsx @@ -1,16 +1,24 @@ import { ActionPanel, Detail, List, Action, Icon, Keyboard, Color } from "@raycast/api"; import { useCachedState, withAccessToken } from "@raycast/utils"; -import type { BeeperDesktop } from "@beeper/desktop-api"; import { useBeeperDesktop, createBeeperOAuth, focusApp } from "./api"; +import { t } from "./locales"; +/** + * Render a searchable list of Beeper Desktop accounts with an optional inline detail pane. + * + * Displays accounts fetched from Beeper Desktop and provides actions to focus the app, toggle the inline details pane, open a legacy detail view, and refresh the list. When no accounts are available an appropriate empty view is shown. + * + * @returns A Raycast List component populated with account items and an empty-state view when no accounts exist + */ function ListAccountsCommand() { + const translations = t(); const [isShowingDetail, setIsShowingDetail] = useCachedState("list-accounts:isShowingDetail", false); const { data: accounts, isLoading, revalidate, error, - } = useBeeperDesktop(async (client) => { + } = useBeeperDesktop(async (client) => { const result = await client.accounts.list(); return result; }); @@ -21,7 +29,7 @@ function ListAccountsCommand() { )} diff --git a/src/list-chats.tsx b/src/list-chats.tsx index 76dd7d2..1cbd027 100644 --- a/src/list-chats.tsx +++ b/src/list-chats.tsx @@ -1,74 +1,152 @@ -import { ActionPanel, Detail, List, Action, Icon } from "@raycast/api"; -import { withAccessToken } from "@raycast/utils"; -import { useBeeperDesktop, createBeeperOAuth, focusApp } from "./api"; +import { useState, useMemo } from "react"; +import { ActionPanel, Detail, List, Action, Icon, Image, showToast, Toast } from "@raycast/api"; +import { useCachedPromise, withAccessToken } from "@raycast/utils"; +import { useBeeperDesktop, getBeeperDesktop, createBeeperOAuth, focusApp, listAccounts } from "./api"; +import { t } from "./locales"; +import { getChatIcon } from "./utils/chatIcon"; +/** + * Render a searchable list of Beeper chats with actions to open the chat in Beeper, view details, and copy the chat ID. + * + * The list is populated from Beeper Desktop search results filtered by the search bar; each item shows network-specific icon, title (or a localized unnamed fallback), type, and last activity when available. + * + * @returns A Raycast List component populated with chat items and an empty state when no chats are found. + */ function ListChatsCommand() { - const { - data: chats, - isLoading, - revalidate, - } = useBeeperDesktop(async (client) => { - const result = await client.chats.search(); - console.log("Fetched accounts:", JSON.stringify(result, null, 2)); - return result.data; - }); + const translations = t(); + const [searchText, setSearchText] = useState(""); + const [chats, setChats] = useState([]); + const [cursor, setCursor] = useState(null); + const [hasMore, setHasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + + const { data: accounts = [] } = useCachedPromise(listAccounts, [], { keepPreviousData: true }); + const accountMap = useMemo(() => new Map(accounts.map((a) => [a.accountID, a])), [accounts]); + + const { isLoading } = useBeeperDesktop( + async (client, query) => { + // Reset state when search changes + setChats([]); + setCursor(null); + setHasMore(true); + + const searchParams = query ? { query, limit: 50 } : { limit: 50 }; + const page = await client.chats.search(searchParams); + + setChats(page.items); + setCursor(page.oldestCursor); + setHasMore(page.hasMore); + + return page.items; + }, + [searchText], + ); + + const loadMore = async () => { + if (!hasMore || isLoadingMore || !cursor) return; + + setIsLoadingMore(true); + try { + const client = getBeeperDesktop(); + const searchParams = searchText + ? { query: searchText, limit: 50, cursor, direction: "older" as const } + : { limit: 50, cursor, direction: "older" as const }; + + const page = await client.chats.search(searchParams); + + setChats((prev) => [...prev, ...page.items]); + setCursor(page.oldestCursor); + setHasMore(page.hasMore); + } catch (error) { + console.error("Failed to load more chats:", error); + await showToast({ + style: Toast.Style.Failure, + title: translations.commands.listChats.loadMoreError || "Failed to Load More Chats", + message: translations.commands.listChats.loadMoreErrorMessage || "Please try again", + }); + } finally { + setIsLoadingMore(false); + } + }; return ( - - {(chats || []).map((chat) => ( + + {chats.map((chat) => { + const account = accountMap.get(chat.accountID); + const accountLabel = account + ? `${account.network || translations.commands.contacts.accountFallback} • ${ + account.user?.fullName || + account.user?.username || + account.user?.email || + account.user?.phoneNumber || + account.accountID + }` + : undefined; + return ( 0 ? [{ text: `${chat.unreadCount} unread` }] : []), - ...(chat.isPinned ? [{ icon: Icon.Pin }] : []), - ...(chat.isMuted ? [{ icon: Icon.SpeakerOff }] : []), - ...(chat.isArchived ? [{ icon: Icon.AddPerson }] : []), + ...(accountLabel ? [{ tag: accountLabel }] : []), + { text: chat.type }, + ...(chat.lastActivity ? [{ date: new Date(chat.lastActivity) }] : []), ]} actions={ + focusApp({ chatID: chat.id })} + /> } /> - revalidate()} - /> - focusApp()} - /> + } /> - ))} - {!isLoading && (!chats || chats.length === 0) && ( + ); + })} + {!isLoading && hasMore && chats.length > 0 && ( + + + + } + /> + )} + {!isLoading && chats.length === 0 && ( )} diff --git a/src/locales/cs.ts b/src/locales/cs.ts new file mode 100644 index 0000000..aa1b2ad --- /dev/null +++ b/src/locales/cs.ts @@ -0,0 +1,141 @@ +import { Translations } from "./en"; + +export const cs: Translations = { + // Commands + commands: { + listAccounts: { + title: "Seznam účtů", + description: "Zobrazit všechny připojené Beeper účty", + navigationTitle: "Beeper účty", + emptyTitle: "Žádné účty nenalezeny", + emptyDescription: "Nejsou připojeny žádné Beeper účty", + }, + listChats: { + title: "Seznam chatů", + description: "Zobrazit všechny vaše Beeper chaty", + searchPlaceholder: "Filtrovat chaty...", + emptyTitle: "Žádné chaty nenalezeny", + emptyDescription: "Zkuste upravit hledání nebo se ujistěte, že Beeper Desktop běží", + loadMoreError: "Nepodařilo se načíst další chaty", + loadMoreErrorMessage: "Zkuste to prosím znovu", + loading: "Načítání...", + loadMoreChats: "Načíst další chaty", + loadMoreAction: "Načíst další", + }, + searchChats: { + title: "Hledat chaty", + description: "Prohledat vaše Beeper chaty", + searchPlaceholder: "Hledat chaty...", + emptyTitle: "Vyhledat chaty", + emptyDescription: "Začněte psát pro vyhledávání v Beeper chatech", + noResultsTitle: "Žádné chaty nenalezeny", + noResultsDescription: "Zkuste změnit hledání nebo se ujistěte, že Beeper Desktop běží", + }, + unreadChats: { + title: "Nepřečtené chaty", + description: "Zobrazit všechny chaty s nepřečtenými zprávami", + navigationTitle: "Nepřečtené chaty", + searchPlaceholder: "Filtrovat nepřečtené chaty...", + emptyTitle: "Žádné nepřečtené zprávy", + emptyDescription: "Vše vyřízeno! Nemáte žádné nepřečtené chaty.", + errorTitle: "Nepodařilo se načíst chaty", + errorDescription: "Nelze se připojit k Beeper Desktop. Ujistěte se, že běží a zkuste to znovu.", + unreadCount: (count: number) => `${count} nepřečtených`, + totalCount: (count: number) => ` (${count} celkem)`, + quickReplyAction: "Rychlá odpověď", + archiveAction: "Archivovat chat", + archiveSuccess: "Chat archivován", + archiveError: "Nepodařilo se archivovat chat", + }, + sendMessage: { + title: "Odeslat zprávu", + description: "Rychle odeslat zprávu do libovolného chatu", + navigationTitle: "Odeslat zprávu", + searchPlaceholder: "Hledat chat…", + composeAction: "Napsat zprávu", + noChatsFound: "Žádné chaty nenalezeny", + noChatsDescription: "Zkuste jiný vyhledávací dotaz.", + }, + searchAll: { + title: "Hledat vše", + description: "Prohledat všechny Beeper chaty a zprávy", + navigationTitle: "Hledat vše", + searchPlaceholder: "Hledat chaty a zprávy…", + emptyTitle: "Prohledat Beeper", + emptyDescription: "Začněte psát pro vyhledávání napříč chaty a zprávami", + noResultsTitle: "Žádné výsledky", + noResultsDescription: "Zkuste jiný vyhledávací dotaz.", + chatsSection: "Chaty", + messagesSection: "Zprávy", + }, + focusApp: { + title: "Zaměřit Beeper Desktop", + description: "Přenést Beeper Desktop do popředí", + successMessage: "Beeper Desktop zaměřen", + errorMessage: "Nepodařilo se zaměřit Beeper Desktop", + }, + contacts: { + title: "Kontakty", + description: "Hledat kontakty napříč připojenými Beeper účty", + navigationTitle: "Hledat kontakty", + searchPlaceholder: "Hledat kontakty podle jména", + accountDropdownTooltip: "Účet", + allAccounts: "Všechny účty", + accountFallback: "Účet", + partialErrorTitle: "Část účtů selhala", + partialErrorMessage: "Výsledky kontaktů mohou být neúplné.", + searchFailedTitle: "Hledání selhalo", + searchFailedDescription: "Ujistěte se, že Beeper běží, a zkuste to znovu.", + noResultsTitle: "Žádné kontakty nenalezeny", + typeToSearchTitle: "Začněte psát", + noResultsDescription: "Zkuste jiný dotaz.", + typeToSearchDescription: "Zadejte jméno pro vyhledání kontaktů.", + openChatAction: "Otevřít chat", + openingChatToast: "Otevírám chat", + chatFoundToast: "Chat nalezen", + creatingChatToast: "Vytvářím chat", + chatCreatedToast: "Chat vytvořen", + openChatFailedToast: "Otevření chatu selhalo", + copyParticipantId: "Kopírovat ID účastníka", + }, + searchMessages: { + title: "Hledat zprávy", + description: "Prohledat vaše Beeper zprávy", + navigationTitle: "Nedávné zprávy", + searchPlaceholder: "Hledat zprávy...", + emptyTitle: "Vyhledat zprávy", + emptyDescription: "Začněte psát pro vyhledávání v Beeper zprávách", + noResultsTitle: "Žádné zprávy nenalezeny", + noResultsDescription: "Zkuste změnit hledání nebo se ujistěte, že Beeper Desktop běží", + }, + }, + + // Common + common: { + unnamedChat: "Nepojmenovaný chat", + unknownMessage: "Neznámá zpráva", + unreadCount: (count: number) => `${count} nepřečtených`, + unknownError: "Došlo k neznámé chybě", + openInBeeper: "Otevřít chat v Beeper", + copyChatId: "Kopírovat ID chatu", + copyMessageText: "Kopírovat text zprávy", + showDetails: "Zobrazit detaily", + pinned: "Připnuto", + muted: "Ztišeno", + archived: "Archivováno", + yes: "Ano", + no: "Ne", + details: { + id: "ID", + accountId: "ID účtu", + network: "Síť", + type: "Typ", + unreadCount: "Počet nepřečtených", + isPinned: "Připnuto", + isMuted: "Ztišeno", + isArchived: "Archivováno", + lastActivity: "Poslední aktivita", + na: "N/A", + }, + }, +}; diff --git a/src/locales/en.ts b/src/locales/en.ts new file mode 100644 index 0000000..52bb41b --- /dev/null +++ b/src/locales/en.ts @@ -0,0 +1,141 @@ +export const en = { + // Commands + commands: { + listAccounts: { + title: "List Accounts", + description: "List all connected Beeper accounts", + navigationTitle: "Beeper Accounts", + emptyTitle: "No Accounts Found", + emptyDescription: "No Beeper accounts are connected", + }, + listChats: { + title: "List Chats", + description: "List all your Beeper chats", + searchPlaceholder: "Filter chats...", + emptyTitle: "No chats found", + emptyDescription: "Try adjusting your search or make sure Beeper Desktop is running", + loadMoreError: "Failed to Load More Chats", + loadMoreErrorMessage: "Please try again", + loading: "Loading...", + loadMoreChats: "Load More Chats", + loadMoreAction: "Load More", + }, + searchChats: { + title: "Search Chats", + description: "Search through your Beeper chats", + searchPlaceholder: "Search chats...", + emptyTitle: "Search for chats", + emptyDescription: "Start typing to search through your Beeper chats", + noResultsTitle: "No chats found", + noResultsDescription: "Try changing your search or ensure Beeper Desktop is running", + }, + unreadChats: { + title: "Unread Chats", + description: "View all chats with unread messages", + navigationTitle: "Unread Chats", + searchPlaceholder: "Filter unread chats...", + emptyTitle: "No Unread Messages", + emptyDescription: "All caught up! You have no unread chats.", + errorTitle: "Failed to Load Chats", + errorDescription: "Could not connect to Beeper Desktop. Make sure it's running and try again.", + unreadCount: (count: number) => `${count} unread`, + totalCount: (count: number) => ` (${count} total)`, + quickReplyAction: "Quick Reply", + archiveAction: "Archive Chat", + archiveSuccess: "Chat archived", + archiveError: "Failed to archive chat", + }, + sendMessage: { + title: "Send Message", + description: "Quickly send a message to any chat", + navigationTitle: "Send Message", + searchPlaceholder: "Search for a chat…", + composeAction: "Compose Message", + noChatsFound: "No Chats Found", + noChatsDescription: "Try a different search query.", + }, + searchAll: { + title: "Search Everything", + description: "Search across all Beeper chats and messages", + navigationTitle: "Search Everything", + searchPlaceholder: "Search chats and messages…", + emptyTitle: "Search Beeper", + emptyDescription: "Type to search across all chats and messages", + noResultsTitle: "No Results", + noResultsDescription: "Try a different search query.", + chatsSection: "Chats", + messagesSection: "Messages", + }, + focusApp: { + title: "Focus Beeper Desktop", + description: "Bring Beeper Desktop to the foreground", + successMessage: "Beeper Desktop focused", + errorMessage: "Failed to focus Beeper Desktop", + }, + contacts: { + title: "Contacts", + description: "Search contacts across connected Beeper accounts", + navigationTitle: "Search Contacts", + searchPlaceholder: "Search contacts by name", + accountDropdownTooltip: "Account", + allAccounts: "All Accounts", + accountFallback: "Account", + partialErrorTitle: "Some accounts failed", + partialErrorMessage: "Contacts may be incomplete for this search.", + searchFailedTitle: "Search Failed", + searchFailedDescription: "Make sure Beeper is running and try again.", + noResultsTitle: "No Contacts Found", + typeToSearchTitle: "Type to Search", + noResultsDescription: "Try a different query.", + typeToSearchDescription: "Enter a name to search contacts.", + openChatAction: "Open Chat", + openingChatToast: "Opening chat", + chatFoundToast: "Chat found", + creatingChatToast: "Creating chat", + chatCreatedToast: "Chat created", + openChatFailedToast: "Open chat failed", + copyParticipantId: "Copy Participant ID", + }, + searchMessages: { + title: "Search Messages", + description: "Search through your Beeper messages", + navigationTitle: "Recent Messages", + searchPlaceholder: "Search messages...", + emptyTitle: "Search for messages", + emptyDescription: "Start typing to search through your Beeper messages", + noResultsTitle: "No messages found", + noResultsDescription: "Try changing your search or ensure Beeper Desktop is running", + }, + }, + + // Common + common: { + unnamedChat: "Unnamed Chat", + unknownMessage: "Unknown Message", + unreadCount: (count: number) => `${count} unread`, + unknownError: "Unknown error occurred", + openInBeeper: "Open Chat in Beeper", + copyChatId: "Copy Chat ID", + copyMessageText: "Copy Message Text", + showDetails: "Show Details", + pinned: "Pinned", + muted: "Muted", + archived: "Archived", + yes: "Yes", + no: "No", + details: { + id: "ID", + accountId: "Account ID", + network: "Network", + type: "Type", + unreadCount: "Unread Count", + isPinned: "Pinned", + isMuted: "Muted", + isArchived: "Archived", + lastActivity: "Last Activity", + na: "N/A", + }, + }, +}; + +export type Translations = typeof en; diff --git a/src/locales/index.ts b/src/locales/index.ts new file mode 100644 index 0000000..dfce28e --- /dev/null +++ b/src/locales/index.ts @@ -0,0 +1,67 @@ +import { environment, getPreferenceValues } from "@raycast/api"; +import { en, Translations } from "./en"; +import { cs } from "./cs"; + +const translations: Record = { + en, + cs, +}; + +/** + * Determine the active locale code to use for translations. + * + * Prefers the user's Raycast language preference when set and supported, otherwise derives the language from the Raycast environment locale, and defaults to `en` if no supported locale is found. + * + * @returns The locale code to use for translations (e.g., `en`, `cs`). `en` if no supported locale is found. + */ +function getLocale(): string { + // Try to get language from Raycast preferences first + const prefs = getPreferenceValues(); + if (typeof prefs.language === "string" && translations[prefs.language]) { + return prefs.language; + } + + // Fallback to Raycast environment locale + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const raycastLocale = (environment as any).locale as string | undefined; + if (!raycastLocale) { + return "en"; + } + const languageCode = raycastLocale.split(/[-_]/)[0].toLowerCase(); + return translations[languageCode] ? languageCode : "en"; +} + +let currentLocale = getLocale(); +let currentTranslations = translations[currentLocale]; + +/** + * Get the translations for the currently active locale. + * + * @returns The `Translations` object for the active locale. + */ +export function t(): Translations { + return currentTranslations; +} + +/** + * Switches the active locale and its translations when the given locale is supported. + * + * @param locale - Locale code (e.g., "en", "cs"); if this locale is present in the translations map the active locale and translations are updated, otherwise no change occurs. + */ +export function setLocale(locale: string): void { + if (translations[locale]) { + currentLocale = locale; + currentTranslations = translations[locale]; + } +} + +/** + * Get the currently active locale code. + * + * @returns The active locale code (for example `en` or `cs`). + */ +export function getCurrentLocale(): string { + return currentLocale; +} + +export type { Translations }; diff --git a/src/reactions.ts b/src/reactions.ts new file mode 100644 index 0000000..3122131 --- /dev/null +++ b/src/reactions.ts @@ -0,0 +1,31 @@ +import BeeperDesktop from "@beeper/desktop-api"; + +/** Compact summary like "👍2 ❤️". */ +export const formatReactionsShort = (reactions?: BeeperDesktop.Reaction[]): string | undefined => { + if (!reactions || reactions.length === 0) return undefined; + const counts = new Map(); + for (const r of reactions) { + counts.set(r.reactionKey, (counts.get(r.reactionKey) ?? 0) + 1); + } + return [...counts.entries()].map(([key, n]) => (n > 1 ? `${key}${n}` : key)).join(" "); +}; + +/** Detailed summary grouped by sender like "Alice 👍❤️, Bob 😂". */ +export const formatReactionsDetailed = ( + reactions?: BeeperDesktop.Reaction[], + nameMap?: Map, +): { entries: { name: string; emojis: string }[] } | undefined => { + if (!reactions || reactions.length === 0) return undefined; + const bySender = new Map(); + for (const r of reactions) { + const key = r.participantID; + const list = bySender.get(key) ?? []; + list.push(r.reactionKey); + bySender.set(key, list); + } + const entries = [...bySender.entries()].map(([id, emojis]) => ({ + name: nameMap?.get(id) ?? id, + emojis: emojis.join(""), + })); + return { entries }; +}; diff --git a/src/search-all.tsx b/src/search-all.tsx new file mode 100644 index 0000000..c6c0cf8 --- /dev/null +++ b/src/search-all.tsx @@ -0,0 +1,118 @@ +import { Action, ActionPanel, Color, Icon, List } from "@raycast/api"; +import { useCachedPromise, withAccessToken } from "@raycast/utils"; +import { useState } from "react"; +import { createBeeperOAuth, focusApp, searchAll } from "./api"; +import { t } from "./locales"; +import { getChatIcon } from "./utils/chatIcon"; +import { getMessageID, parseDate } from "./utils"; + +function SearchAllCommand() { + const translations = t(); + const sa = translations.commands.searchAll; + + const [searchText, setSearchText] = useState(""); + const trimmedQuery = searchText.trim(); + + const { data, isLoading } = useCachedPromise( + async (query: string) => { + if (!query) return null; + return searchAll({ query }); + }, + [trimmedQuery], + { keepPreviousData: true }, + ); + + // Merge chats + in_groups, deduplicate by id + const chatMap = new Map(); + const chats = [...(data?.results?.chats ?? []), ...(data?.results?.in_groups ?? [])]; + for (const chat of chats) chatMap.set(chat.id, chat); + const uniqueChats = Array.from(chatMap.values()); + + const messages = data?.results?.messages?.items ?? []; + + const hasResults = uniqueChats.length > 0 || messages.length > 0; + + return ( + + {!trimmedQuery && ( + + )} + + {trimmedQuery && !isLoading && !hasResults && ( + + )} + + {uniqueChats.length > 0 && ( + + {uniqueChats.map((chat) => ( + 0 ? [{ text: translations.common.unreadCount(chat.unreadCount) }] : []), + ...(chat.lastActivity ? [{ date: new Date(chat.lastActivity) }] : []), + ]} + actions={ + + focusApp({ chatID: chat.id })} + /> + + + } + /> + ))} + + )} + + {messages.length > 0 && ( + + {messages.map((message) => { + const timestamp = parseDate(message.timestamp); + const messageID = getMessageID(message); + const text = message.text?.trim(); + const preview = text ? text : "Attachment"; + const sender = message.senderName || (message.isSender ? "You" : "Unknown"); + + return ( + + focusApp({ chatID: message.chatID, messageID })} + /> + {message.text && ( + + )} + + } + /> + ); + })} + + )} + + ); +} + +export default withAccessToken(createBeeperOAuth())(SearchAllCommand); diff --git a/src/search-chats.tsx b/src/search-chats.tsx index 965eab9..ec3950a 100644 --- a/src/search-chats.tsx +++ b/src/search-chats.tsx @@ -1,58 +1,92 @@ -import { useState } from "react"; -import { ActionPanel, Detail, List, Action, Icon } from "@raycast/api"; -import { withAccessToken } from "@raycast/utils"; -import { useBeeperDesktop, createBeeperOAuth } from "./api"; +import { List, Icon } from "@raycast/api"; +import { useCachedPromise, withAccessToken } from "@raycast/utils"; +import { useState, useMemo } from "react"; +import { createBeeperOAuth, focusApp, listAccounts } from "./api"; +import { t } from "./locales"; +import { ChatListItem } from "./components/ChatListItem"; +import { useChatSearch } from "./hooks/useChatSearch"; +/** + * Returns raw avatar URL for 1:1 chats, undefined for groups. + * Note: The URL is not sanitized here - ChatListItem handles sanitization via safeAvatarPath. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getAvatarUrl(chat: any): string | undefined { + // Only show avatar for 1:1 chats, not groups + if (chat.type !== "group" && chat.participants?.items && Array.isArray(chat.participants.items)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const otherParticipant = chat.participants.items.find((p: any) => !p.isSelf); + return otherParticipant?.imgURL; + } + return undefined; +} + +/** + * Render a search interface that finds and displays Beeper chats matching the user's query. + * + * Shows an initial empty view when the search box is empty, performs a client-side search as the + * user types, and renders matching chats with network icons, unread/pinned/muted accessories, and + * actions to open the chat in Beeper or copy its ID. + * + * @returns The List JSX element presenting the search bar, results, and appropriate empty states. + */ function SearchChatsCommand() { + const translations = t(); const [searchText, setSearchText] = useState(""); - const { data: chats = [], isLoading } = useBeeperDesktop(async (client) => { - const result = await client.chats.search({ query: searchText }); - return result.data; - }); + const { data: chats = [], isLoading } = useChatSearch(searchText); + const { data: accounts = [] } = useCachedPromise(listAccounts, [], { keepPreviousData: true }); + const accountMap = useMemo(() => new Map(accounts.map((a) => [a.accountID, a])), [accounts]); return ( - - {chats.map((chat) => ( - - - } - /> - - } + + {searchText === "" ? ( + - ))} - {!isLoading && chats.length === 0 && ( + ) : !isLoading && chats.length === 0 ? ( + ) : ( + chats.map((chat) => { + const account = accountMap.get(chat.accountID); + const accountLabel = account + ? `${account.network || translations.commands.contacts.accountFallback} • ${ + account.user?.fullName || + account.user?.username || + account.user?.email || + account.user?.phoneNumber || + account.accountID + }` + : undefined; + return ( + focusApp({ chatID: chat.id }), + }} + translations={translations} + accessories={[ + ...(accountLabel ? [{ tag: accountLabel }] : []), + ...(chat.unreadCount > 0 ? [{ text: translations.common.unreadCount(chat.unreadCount) }] : []), + ...(chat.isPinned ? [{ icon: Icon.Pin }] : []), + ...(chat.isMuted ? [{ icon: Icon.SpeakerOff }] : []), + ]} + showDetails={false} + /> + ); + }) )} ); diff --git a/src/search-messages.tsx b/src/search-messages.tsx new file mode 100644 index 0000000..ca4bdf1 --- /dev/null +++ b/src/search-messages.tsx @@ -0,0 +1,525 @@ +import { + Action, + ActionPanel, + Color, + Detail, + Icon, + Keyboard, + LaunchProps, + List, + openExtensionPreferences, +} from "@raycast/api"; +import { useCachedPromise, useCachedState, withAccessToken } from "@raycast/utils"; +import BeeperDesktop from "@beeper/desktop-api"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + createBeeperOAuth, + focusApp, + listChatMessages, + retrieveChat, + getRaycastFocusLink, + searchChats, + searchMessages, + useBeeperDesktop, +} from "./api"; +import { ChatThread, ComposeMessageForm, toApiInbox, type ChatFilters, type InboxFilter } from "./chat"; +import { formatReactionsShort, formatReactionsDetailed } from "./reactions"; +import { parseDate, getMessageID, getBeeperAppPath } from "./utils"; +import { t } from "./locales"; + +type SenderFilter = "any" | "me" | "others"; + +interface MessageFilters extends ChatFilters { + sender: SenderFilter; +} + +type SearchMessagesLaunchContext = { + chatID?: string; + query?: string; + sender?: SenderFilter; +}; + +const defaultFilters: MessageFilters = { + inbox: "inbox", + type: "any", + unreadOnly: false, + sender: "any", + includeMuted: true, +}; + +function SearchMessagesCommand(props: LaunchProps<{ launchContext?: SearchMessagesLaunchContext }>) { + const translations = t(); + const initialQuery = props.launchContext?.query ?? ""; + const [searchText, setSearchText] = useState(initialQuery); + const [rawFilters, setFilters] = useCachedState("messages:filters:v2", defaultFilters); + const filters = { ...defaultFilters, ...rawFilters }; + const [isShowingDetail, setIsShowingDetail] = useCachedState("messages:showing-detail", false); + const [dateAfter, setDateAfter] = useCachedState("messages:date-after", undefined); + const [dateBefore, setDateBefore] = useCachedState("messages:date-before", undefined); + const chatIDFilter = props.launchContext?.chatID; + const beeperAppPath = getBeeperAppPath(); + const appliedContext = useRef(false); + + useEffect(() => { + if (appliedContext.current) return; + if (!props.launchContext) return; + appliedContext.current = true; + setFilters((prev) => ({ + ...prev, + sender: props.launchContext?.sender ?? prev.sender, + })); + if (props.launchContext.query) { + setSearchText(props.launchContext.query); + } + }, [props.launchContext, setFilters]); + + const trimmedQuery = searchText.trim(); + + const chatFilterParams = useMemo( + () => ({ + inbox: toApiInbox(filters.inbox), + type: filters.type !== "any" ? (filters.type as "single" | "group") : undefined, + includeMuted: filters.includeMuted, + unreadOnly: filters.unreadOnly || undefined, + }), + [filters.inbox, filters.type, filters.includeMuted, filters.unreadOnly], + ); + + const { data: inboxChatIDs, isLoading: isLoadingChats } = useCachedPromise( + async (params: typeof chatFilterParams) => { + const ids: string[] = []; + let cursor: string | null | undefined; + for (let page = 0; page < 5 && ids.length < 200; page++) { + const result = await searchChats({ + ...params, + cursor, + direction: cursor ? "before" : undefined, + }); + ids.push(...result.items.map((c) => c.id)); + if (!result.hasMore || !result.oldestCursor || result.items.length === 0) break; + cursor = result.oldestCursor; + } + return ids; + }, + [chatFilterParams], + { keepPreviousData: true }, + ); + + const resolvedChatIDs = chatIDFilter ? [chatIDFilter] : inboxChatIDs; + + const messageParams = useMemo(() => { + const next: Parameters[0] = { + includeMuted: filters.includeMuted, + chatType: filters.type !== "any" ? filters.type : undefined, + sender: filters.sender !== "any" ? filters.sender : undefined, + query: trimmedQuery.length > 0 ? trimmedQuery : undefined, + chatIDs: resolvedChatIDs, + dateAfter, + dateBefore, + limit: 20, + }; + return next; + }, [resolvedChatIDs, dateAfter, dateBefore, filters.includeMuted, filters.sender, filters.type, trimmedQuery]); + + const canFetchMessages = chatIDFilter ? true : inboxChatIDs !== undefined && inboxChatIDs.length > 0; + + const { + data: rawMessages = [], + isLoading: isLoadingMessages, + revalidate, + error, + } = useCachedPromise( + async (input: Parameters[0]) => { + const maxMessages = 60; + const allItems: BeeperDesktop.Message[] = []; + let cursor: string | null | undefined; + + while (allItems.length < maxMessages) { + const result = await searchMessages({ ...input, cursor, direction: cursor ? "before" : undefined }); + const items = result.items ?? []; + allItems.push(...items); + if (!result.hasMore || !result.oldestCursor || items.length === 0) break; + cursor = result.oldestCursor; + } + + return allItems; + }, + [messageParams], + { execute: canFetchMessages, keepPreviousData: true }, + ); + + const messages = !canFetchMessages && inboxChatIDs !== undefined ? [] : rawMessages; + const isLoading = isLoadingChats || isLoadingMessages; + + const chatIDs = useMemo(() => Array.from(new Set(messages.map((message) => message.chatID))), [messages]); + const { data: chatMeta = {} } = useCachedPromise( + async (ids: string[]) => { + if (ids.length === 0) return {}; + const entries = await Promise.all( + ids.slice(0, 20).map(async (chatID) => { + try { + const chat = await retrieveChat(chatID, { maxParticipantCount: 0 }); + return [chatID, { title: chat.title || chatID, localChatID: chat.localChatID }] as const; + } catch { + return [chatID, { title: chatID }] as const; + } + }), + ); + return Object.fromEntries(entries); + }, + [chatIDs], + { keepPreviousData: true, execute: chatIDs.length > 0 }, + ); + + const messageIDs = useMemo(() => new Set(messages.map((m) => m.id)), [messages]); + const eagerChatIDs = useMemo(() => chatIDs.slice(0, 10), [chatIDs]); + const lazyChatIDs = useMemo(() => chatIDs.slice(10), [chatIDs]); + + const fetchReactions = async (ids: string[], msgIDs: Set) => { + if (ids.length === 0) return {}; + const map: Record = {}; + await Promise.all( + ids.map(async (chatID) => { + try { + const result = await listChatMessages(chatID); + for (const msg of result.items ?? []) { + if (msg.reactions?.length && msgIDs.has(msg.id)) { + map[msg.id] = msg.reactions; + } + } + } catch { + // ignore – reactions are best-effort + } + }), + ); + return map; + }; + + const { data: eagerReactionsMap = {} } = useCachedPromise(fetchReactions, [eagerChatIDs, messageIDs], { + keepPreviousData: true, + execute: eagerChatIDs.length > 0, + }); + + const { data: lazyReactionsMap = {} } = useCachedPromise(fetchReactions, [lazyChatIDs, messageIDs], { + keepPreviousData: true, + execute: isShowingDetail && lazyChatIDs.length > 0, + }); + + const reactionsMap = useMemo( + () => ({ ...eagerReactionsMap, ...lazyReactionsMap }), + [eagerReactionsMap, lazyReactionsMap], + ); + + const updateFilters = (partial: Partial) => + setFilters((prev) => ({ ...prev, ...partial })); + + const inboxDropdown = ( + setFilters((prev) => ({ ...prev, inbox: value as InboxFilter }))} + > + + + + + + ); + + const nameMap = useMemo(() => { + const map = new Map(); + for (const msg of messages) { + if (msg.senderName) map.set(msg.senderID, msg.senderName); + } + return map; + }, [messages]); + + return ( + + {messages + .filter((m) => !!(m.text?.trim()) || (m.attachments && m.attachments.length > 0)) + .map((message) => { + const text = message.text?.trim(); + const preview = text && text.length > 0 ? text : "Attachment"; + const enrichedReactions = reactionsMap[message.id] ?? message.reactions; + const reactionsShort = formatReactionsShort(enrichedReactions); + const reactionsDetailed = formatReactionsDetailed(enrichedReactions, nameMap); + const timestamp = parseDate(message.timestamp); + const chatInfo = (chatMeta as Record)[message.chatID]; + const chatTitle = chatInfo?.title; + const sender = message.senderName || (message.isSender ? "You" : "Unknown"); + const subtitle = chatTitle ? `${chatTitle} • ${sender}` : sender; + const messageID = getMessageID(message); + const messageLink = getRaycastFocusLink({ chatID: message.chatID, messageID }); + + return ( + + {reactionsDetailed && ( + + {reactionsDetailed.entries.map((e) => ( + + ))} + + )} + + + + {message.isSender && ( + + + + )} + + } + /> + ) : null + } + accessories={[ + ...(reactionsShort ? [{ tag: { value: reactionsShort, color: Color.SecondaryText } }] : []), + ...(timestamp ? [{ date: timestamp }] : []), + ]} + actions={ + setIsShowingDetail((prev) => !prev)} + dateAfter={dateAfter} + dateBefore={dateBefore} + setDateAfter={setDateAfter} + setDateBefore={setDateBefore} + /> + } + /> + ); + })} + {!isLoading && messages.length === 0 && ( + + {beeperAppPath && } + openExtensionPreferences()} + /> + + ) : null + } + /> + )} + + ); +} + +function MessageSearchActions({ + message, + messageID, + messageLink, + chatTitle, + onRefresh, + filters, + updateFilters, + isShowingDetail, + onToggleDetail, + dateAfter, + dateBefore, + setDateAfter, + setDateBefore, +}: { + message: BeeperDesktop.Message; + messageID: string; + messageLink?: string; + chatTitle?: string; + onRefresh: () => void; + filters: MessageFilters; + updateFilters: (partial: Partial) => void; + isShowingDetail: boolean; + onToggleDetail: () => void; + dateAfter?: string; + dateBefore?: string; + setDateAfter: (value?: string) => void; + setDateBefore: (value?: string) => void; +}) { + const translations = t(); + + return ( + + + focusApp({ chatID: message.chatID, messageID: messageID })} + /> + focusApp({ chatID: message.chatID })} /> + {messageLink && ( + + )} + + + } + /> + } + /> + + + + {message.text && } + {message.text && } + {message.text && ( + + )} + + + + } /> + + + + updateFilters({ unreadOnly: !filters.unreadOnly })} + /> + updateFilters({ includeMuted: !filters.includeMuted })} + /> + updateFilters({ sender: "any" })} /> + updateFilters({ sender: "me" })} /> + updateFilters({ sender: "others" })} /> + updateFilters({ type: "any" })} /> + updateFilters({ type: "single" })} /> + updateFilters({ type: "group" })} /> + setDateAfter(date ? date.toISOString() : undefined)} + /> + setDateBefore(date ? date.toISOString() : undefined)} + /> + {(dateAfter || dateBefore) && ( + { + setDateAfter(undefined); + setDateBefore(undefined); + }} + /> + )} + + + ); +} + +function ChatThreadById({ chatID, fallbackTitle }: { chatID: string; fallbackTitle?: string }) { + const { + data: chat, + isLoading, + error, + } = useBeeperDesktop(async () => { + return retrieveChat(chatID, { maxParticipantCount: 0 }); + }); + + if (isLoading) { + return ; + } + + if (!chat || error) { + return ; + } + + return ; +} + +function ComposeMessageById({ chatID, replyToMessageID }: { chatID: string; replyToMessageID?: string }) { + const { + data: chat, + isLoading, + error, + } = useBeeperDesktop(async () => { + return retrieveChat(chatID, { maxParticipantCount: 0 }); + }); + + if (isLoading) { + return ; + } + + if (!chat || error) { + return ; + } + + return ; +} + +function MessageDetail({ message }: { message: BeeperDesktop.Message }) { + const messageID = getMessageID(message); + return ( + + ); +} + +export default withAccessToken(createBeeperOAuth())(SearchMessagesCommand); diff --git a/src/send-message.tsx b/src/send-message.tsx new file mode 100644 index 0000000..7a3cac0 --- /dev/null +++ b/src/send-message.tsx @@ -0,0 +1,116 @@ +import { Action, ActionPanel, Icon, List } from "@raycast/api"; +import { useLocalStorage, withAccessToken } from "@raycast/utils"; +import Fuse from "fuse.js"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + buildSearchFields, + ChatIndexState, + ComposeMessageForm, + defaultIndexState, + IndexedChat, + summarizeChatForIndex, +} from "./chat"; +import { createBeeperOAuth, searchChats } from "./api"; +import { t } from "./locales"; +import { parseDate } from "./utils"; + +const CHAT_INDEX_KEY = "chat:index:v2"; + +function SendMessageCommand() { + const translations = t(); + const sm = translations.commands.sendMessage; + + const [searchText, setSearchText] = useState(""); + const { value: indexState = defaultIndexState, setValue: setIndexState } = useLocalStorage( + CHAT_INDEX_KEY, + defaultIndexState, + ); + const refreshDone = useRef(false); + const indexRef = useRef(indexState); + indexRef.current = indexState; + + // Background refresh on mount — updates the index without blocking the UI + useEffect(() => { + if (refreshDone.current) return; + refreshDone.current = true; + + searchChats({ includeMuted: true, type: "any" }) + .then((result) => { + if (result.items.length === 0) return; + const newItems: IndexedChat[] = result.items.map((chat) => ({ + chat: summarizeChatForIndex(chat), + inbox: chat.isArchived ? ("archive" as const) : ("inbox" as const), + searchFields: buildSearchFields(chat), + })); + const base = indexRef.current ?? defaultIndexState; + const map = new Map(); + for (const item of base.items) map.set(item.chat.id, item); + for (const item of newItems) map.set(item.chat.id, item); + void setIndexState({ ...base, items: Array.from(map.values()), updatedAt: Date.now() }); + }) + .catch(() => { + // Silently ignore — existing index remains usable + }); + }, []); + + const allChats = indexState.items; + + const fuse = useMemo( + () => + new Fuse(allChats, { + keys: ["searchFields.title", "searchFields.network", "searchFields.participants"], + ignoreDiacritics: true, + includeScore: true, + ignoreLocation: true, + threshold: 0.3, + }), + [allChats], + ); + + const trimmedQuery = searchText.trim(); + + const chats = useMemo(() => { + if (!trimmedQuery) return allChats.map((item) => item.chat); + return fuse.search(trimmedQuery).map((result) => result.item.chat); + }, [allChats, fuse, trimmedQuery]); + + return ( + + {chats.map((chat) => { + const lastActivity = parseDate(chat.lastActivity); + return ( + 0 ? [{ text: translations.common.unreadCount(chat.unreadCount) }] : []), + ...(lastActivity ? [{ date: lastActivity }] : []), + ]} + actions={ + + } + /> + + } + /> + ); + })} + {allChats.length > 0 && chats.length === 0 && ( + + )} + + ); +} + +export default withAccessToken(createBeeperOAuth())(SendMessageCommand); diff --git a/src/unread-chats.tsx b/src/unread-chats.tsx new file mode 100644 index 0000000..8a8f72f --- /dev/null +++ b/src/unread-chats.tsx @@ -0,0 +1,114 @@ +import { ActionPanel, Action, List, Icon, showToast, Toast } from "@raycast/api"; +import { withAccessToken } from "@raycast/utils"; +import { archiveChat, createBeeperOAuth, focusApp, useBeeperDesktop } from "./api"; +import { ComposeMessageForm } from "./chat"; +import { t } from "./locales"; +import { getChatIcon } from "./utils/chatIcon"; + +function UnreadChatsCommand() { + const translations = t(); + const u = translations.commands.unreadChats; + + const { + data: chats = [], + isLoading, + error, + revalidate, + } = useBeeperDesktop(async (client) => { + const allChats = []; + let cursor: string | null = null; + let hasMore = true; + const MAX_PAGES = 20; + let pageCount = 0; + + while (hasMore && pageCount < MAX_PAGES) { + const searchParams = cursor + ? { unreadOnly: true, limit: 50, cursor, direction: "older" as const } + : { unreadOnly: true, limit: 50 }; + + const page = await client.chats.search(searchParams); + allChats.push(...page.items); + + cursor = page.oldestCursor; + hasMore = page.hasMore; + pageCount++; + } + + return allChats.sort((a, b) => b.unreadCount - a.unreadCount); + }); + + const totalUnread = chats.reduce((sum, chat) => sum + chat.unreadCount, 0); + + const handleArchive = async (chatID: string) => { + const toast = await showToast({ style: Toast.Style.Animated, title: u.archiveAction }); + try { + await archiveChat(chatID, true); + toast.style = Toast.Style.Success; + toast.title = u.archiveSuccess; + revalidate(); + } catch { + toast.style = Toast.Style.Failure; + toast.title = u.archiveError; + } + }; + + return ( + 0 ? u.totalCount(totalUnread) : ""}`} + > + {error ? ( + + ) : !isLoading && chats.length === 0 ? ( + + ) : ( + chats.map((chat) => ( + + focusApp({ chatID: chat.id })} + /> + } + /> + handleArchive(chat.id)} + /> + + + } + /> + )) + )} + + ); +} + +export default withAccessToken(createBeeperOAuth())(UnreadChatsCommand); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..115bbac --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +import BeeperDesktop from "@beeper/desktop-api"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export const parseDate = (value?: string) => { + if (!value) return undefined; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? undefined : date; +}; + +export const getMessageID = (message: BeeperDesktop.Message & { messageID?: string }) => + message.messageID ?? message.id; + +export const getBeeperAppPath = () => { + const candidates = ["/Applications/Beeper Desktop.app", join(homedir(), "Applications", "Beeper Desktop.app")]; + return candidates.find((path) => existsSync(path)); +}; diff --git a/src/utils/avatar.ts b/src/utils/avatar.ts new file mode 100644 index 0000000..ad323e2 --- /dev/null +++ b/src/utils/avatar.ts @@ -0,0 +1,98 @@ +import { homedir } from "os"; +import { resolve, normalize, sep } from "path"; + +/** + * Returns the platform-specific allowed base directories for avatar files. + * Supports macOS, Windows, and Linux. + */ +function getAllowedAvatarBases(): string[] { + const home = homedir(); + const platform = process.platform; + + switch (platform) { + case "darwin": + // macOS: ~/Library/Application Support/BeeperTexts/media + return [normalize(resolve(home, "Library/Application Support/BeeperTexts/media"))]; + + case "win32": + // Windows: %APPDATA%/BeeperTexts/media (typically ~/AppData/Roaming/BeeperTexts/media) + return [normalize(resolve(home, "AppData", "Roaming", "BeeperTexts", "media"))]; + + case "linux": + default: { + // Linux: $XDG_DATA_HOME/BeeperTexts/media or ~/.local/share/BeeperTexts/media + const bases: string[] = []; + const xdgDataHome = process.env.XDG_DATA_HOME; + if (xdgDataHome) { + bases.push(normalize(resolve(xdgDataHome, "BeeperTexts", "media"))); + } + // Always include the fallback path + bases.push(normalize(resolve(home, ".local", "share", "BeeperTexts", "media"))); + return bases; + } + } +} + +// Allowed base directories for avatar files (platform-specific) +const ALLOWED_AVATAR_BASES = getAllowedAvatarBases(); + +/** + * Checks if a normalized path is within any of the allowed avatar directories. + * Prevents prefix attacks by requiring exact match or path separator after base. + */ +function isPathAllowed(normalizedPath: string): boolean { + return ALLOWED_AVATAR_BASES.some((base) => { + const isExactMatch = normalizedPath === base; + const isSubpath = normalizedPath.startsWith(base + sep); + return isExactMatch || isSubpath; + }); +} + +/** + * Safely converts a file:// URL to a validated filesystem path. + * Returns undefined if the URL is invalid or points outside the allowed directory. + * + * Security measures: + * - Only accepts file:// URLs, rejects all other schemes + * - Wraps decodeURIComponent in try/catch for malformed URIs + * - Normalizes and resolves path to prevent path traversal + * - Validates path is exactly the allowed directory or a true subpath (prevents prefix attacks) + * - Rejects paths with null bytes + */ +export function safeAvatarPath(url: string): string | undefined { + try { + // Only accept file:// URLs - explicitly reject other schemes + if (!url.startsWith("file://")) { + return undefined; + } + + // Strip the file:// prefix + let avatarPath = url.slice(7); // "file://".length === 7 + + // Decode URL encoding safely + try { + avatarPath = decodeURIComponent(avatarPath); + } catch { + // URIError: malformed URI - return undefined to fall back to network icon + return undefined; + } + + // Reject paths with null bytes (could bypass string checks) + if (avatarPath.includes("\0")) { + return undefined; + } + + // Normalize and resolve the path to prevent path traversal + const normalizedPath = normalize(resolve(avatarPath)); + + // Validate that the path is within one of the allowed avatar directories + if (!isPathAllowed(normalizedPath)) { + return undefined; + } + + return normalizedPath; + } catch { + // Any unexpected error - fall back to network icon + return undefined; + } +} diff --git a/src/utils/chatIcon.ts b/src/utils/chatIcon.ts new file mode 100644 index 0000000..0fb0b3e --- /dev/null +++ b/src/utils/chatIcon.ts @@ -0,0 +1,24 @@ +import { Image, Icon } from "@raycast/api"; +import { safeAvatarPath } from "./avatar"; +import { getNetworkIcon } from "./networkIcons"; + +/** + * Returns chat icon - contact avatar for DMs, network icon for groups. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getChatIcon(chat: any): Image.ImageLike { + if (!chat) return Icon.Bubble; + + // For 1:1 chats, try to get the other person's avatar + if (chat.type !== "group" && chat.participants?.items && Array.isArray(chat.participants.items)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const otherParticipant = chat.participants.items.find((p: any) => !p.isSelf); + if (otherParticipant?.imgURL) { + const validatedPath = safeAvatarPath(otherParticipant.imgURL); + if (validatedPath) { + return { source: validatedPath, mask: Image.Mask.Circle }; + } + } + } + return getNetworkIcon(chat.network); +} diff --git a/src/utils/networkIcons.ts b/src/utils/networkIcons.ts new file mode 100644 index 0000000..fb907e6 --- /dev/null +++ b/src/utils/networkIcons.ts @@ -0,0 +1,41 @@ +import { Icon, Image } from "@raycast/api"; + +/** + * Maps network identifiers to their corresponding icon asset filenames. + * These filenames are relative to the extension's assets directory. + */ +const NETWORK_ICON_MAP: Record = { + slack: "slack.svg", + whatsapp: "whatsapp.svg", + telegram: "telegram.svg", + discord: "discord.svg", + instagram: "instagram.svg", + facebook: "facebook.svg", + facebookmessenger: "messenger.svg", + messenger: "messenger.svg", + signal: "signal.svg", + imessage: "imessage.svg", + twitter: "twitter.svg", + email: "email.svg", + googlemessages: "google-messages.svg", +}; + +/** + * Returns the icon for a messaging network. + * Falls back to a generic message icon if the network is not recognized. + * + * @param network - The network name (case-insensitive, spaces/slashes/dashes are ignored) + * @returns The corresponding network icon image + */ +export function getNetworkIcon(network: string | undefined | null): Image.ImageLike { + if (!network) return Icon.Message; + const networkLower = network.toLowerCase().replace(/[/\s-]/g, ""); + const iconFilename = NETWORK_ICON_MAP[networkLower]; + + if (iconFilename) { + // Return an Image.Source object pointing to the asset file + return { source: iconFilename }; + } + + return Icon.Message; +}