diff --git a/extensions/sunsama/.gitignore b/extensions/sunsama/.gitignore new file mode 100644 index 00000000000..f86ff84047e --- /dev/null +++ b/extensions/sunsama/.gitignore @@ -0,0 +1,22 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# Raycast specific files +raycast-env.d.ts +.raycast-swift-build +.swiftpm +compiled_raycast_swift +compiled_raycast_rust + +# misc +.DS_Store + +# local design/spec docs +/docs + +# local tooling and working notes +/.claude +CLAUDE.md +pr-description.md diff --git a/extensions/sunsama/CHANGELOG.md b/extensions/sunsama/CHANGELOG.md new file mode 100644 index 00000000000..35598165c61 --- /dev/null +++ b/extensions/sunsama/CHANGELOG.md @@ -0,0 +1,18 @@ +# Sunsama Changelog + +## [Initial Version] - {PR_MERGE_DATE} + +Manage your Sunsama tasks from Raycast, over Sunsama's official MCP server. + +- **Add Task** — create a task on any day, with notes, a channel, a time + estimate, and subtasks. Paste a link from any service Sunsama connects to + (Trello, GitHub, Todoist, ClickUp, Jira, Linear, Asana, Notion, Gmail, Slack + and more) and it's attached as a natively linked task, titled from the item + itself. No API keys to supply. +- **View Today's Tasks** — today's tasks in Sunsama's own order. Start and stop + timers on tasks and subtasks, complete, edit, snooze to another day, set + planned time, reorder, open the linked item, manage subtasks, and delete. + Actions apply immediately and roll back if the request fails. +- **Set Default Channel** — pick the channel new tasks default to. +- Sign in once through the browser with OAuth; access refreshes itself. A Sign + Out action clears it. diff --git a/extensions/sunsama/README.md b/extensions/sunsama/README.md new file mode 100644 index 00000000000..eca22854d14 --- /dev/null +++ b/extensions/sunsama/README.md @@ -0,0 +1,120 @@ +# Sunsama for Raycast + +Create, view, and manage your Sunsama tasks without leaving Raycast. + +## FYI + +This is an unofficial extension and isn't affiliated with Sunsama. It runs on +Sunsama's official MCP server, so everything it does is something Sunsama +supports on purpose. + +## Setup + +There isn't any. The first time you run a command, Raycast opens Sunsama in your +browser and asks you to authorize the extension. Approve it and you're done — +access refreshes itself after that, so you shouldn't ever have to think about it +again. + +If you do need to disconnect, run **View Today's Tasks** and pick **Sign Out of +Sunsama** from the actions. The next command you run will ask you to sign in +again. + +## Commands + +### Add Task + +Adds a task to today, or whatever day you pick. + +The Task field takes three shapes. You can type a plain title, paste a link, or +paste a link followed by a title: + +| What you type | What you get | +| -------------------------- | --------------------------------------------- | +| `Fix the contact form` | A task with that title | +| `` | A linked task, titled from the item itself | +| ` Fix the form` | A linked task titled "Fix the form" | + +Paste a link from anything Sunsama connects to — Trello, GitHub, Todoist, +ClickUp, Jira, Linear, Asana, Notion, Gmail, Slack — and it comes through as a +proper linked task with the provider's icon and a click-through to the original, +not just a URL sitting in the notes. Sunsama does the lookup on its end, so +there's no API key for you to go dig up. + +You can also set notes, a channel, a time estimate, subtasks, and whether the +task lands at the top or bottom of the day. + +### View Today's Tasks + +Today's tasks in Sunsama's own order. From here you can start and stop timers, +mark things complete, edit a task, set planned time, snooze to another day, move +tasks around, open the linked item, manage subtasks, and delete. + +Timers work on subtasks too. If a subtask timer is running — even one you started +in the Sunsama web app — hitting Stop Timer on the parent task stops the right +one. + +### Set Default Channel + +Picks the channel new tasks default to. Sunsama has no "list all channels" +endpoint, only search, so this list is search-driven — start typing and it +narrows. + +## Time input + +Anywhere you type a duration, all of these work: + +| Input | Meaning | +| -------- | ------------- | +| `90` | 90 minutes | +| `45m` | 45 minutes | +| `1h` | 1 hour | +| `1h 30m` | 90 minutes | +| `1:15` | 1 hour 15 min | + +One thing worth knowing: if a task has subtasks with their own planned times, +Sunsama adds those up to get the task's time. So setting a time on the parent +means clearing the subtask times first. The extension asks you before it does +that. + +## Development + +You'll need Node 22+ and Raycast. + +```bash +npm install +npm run dev +``` + +`npm run dev` loads the commands into Raycast and reloads them when you change a +file. They'll show up in Raycast's root search while it's running. + +Before you commit anything, run all three of these: + +```bash +npm test # unit tests +npm run lint # eslint + prettier +npm run build # the build Raycast actually publishes +``` + +The tests cover the pure logic — duration parsing, day math, and the HTML to +Markdown conversion for notes. Sunsama sends task notes as HTML but takes +Markdown back when you save them, so that conversion has to survive a round trip +and it's worth testing on its own. Everything that touches the network gets +tested by running the extension against a real account. + +## Project layout + +``` +src/ + add-task.tsx Add Task command + view-today.tsx View Today's Tasks command + set-default-channel.tsx Set Default Channel command + components/ Forms and lists pushed onto the navigation stack + lib/ + mcp.ts OAuth, MCP transport, tool and resource calls + sunsama-client.ts Tasks, subtasks, channels, timers + notes.ts Sunsama notes HTML to Markdown (tested) + time.ts Duration parsing (tested) + date.ts Day string helpers (tested) + open-integration.ts Opens a linked item, preferring the desktop app +``` diff --git a/extensions/sunsama/assets/extension-icon.png b/extensions/sunsama/assets/extension-icon.png new file mode 100644 index 00000000000..2d3178e511b Binary files /dev/null and b/extensions/sunsama/assets/extension-icon.png differ diff --git a/extensions/sunsama/eslint.config.js b/extensions/sunsama/eslint.config.js new file mode 100644 index 00000000000..543274e97ec --- /dev/null +++ b/extensions/sunsama/eslint.config.js @@ -0,0 +1,4 @@ +const { defineConfig } = require("eslint/config"); +const raycastConfig = require("@raycast/eslint-config"); + +module.exports = defineConfig([...raycastConfig]); diff --git a/extensions/sunsama/metadata/sunsama-1.png b/extensions/sunsama/metadata/sunsama-1.png new file mode 100644 index 00000000000..82658f825e1 Binary files /dev/null and b/extensions/sunsama/metadata/sunsama-1.png differ diff --git a/extensions/sunsama/metadata/sunsama-2.png b/extensions/sunsama/metadata/sunsama-2.png new file mode 100644 index 00000000000..d2d8f35cfb3 Binary files /dev/null and b/extensions/sunsama/metadata/sunsama-2.png differ diff --git a/extensions/sunsama/metadata/sunsama-3.png b/extensions/sunsama/metadata/sunsama-3.png new file mode 100644 index 00000000000..c88e91c5671 Binary files /dev/null and b/extensions/sunsama/metadata/sunsama-3.png differ diff --git a/extensions/sunsama/metadata/sunsama-4.png b/extensions/sunsama/metadata/sunsama-4.png new file mode 100644 index 00000000000..55c3054ef6b Binary files /dev/null and b/extensions/sunsama/metadata/sunsama-4.png differ diff --git a/extensions/sunsama/package-lock.json b/extensions/sunsama/package-lock.json new file mode 100644 index 00000000000..3dc4f8ec5d7 --- /dev/null +++ b/extensions/sunsama/package-lock.json @@ -0,0 +1,5316 @@ +{ + "name": "sunsama", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sunsama", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "@raycast/api": "^1.103.0", + "@raycast/utils": "^2.2.1" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.0.4", + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "eslint": "^9.22.0", + "prettier": "^3.5.3", + "typescript": "^5.8.2", + "vitest": "^4.1.8" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@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.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "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", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@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.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@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" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@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.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@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" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@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" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oclif/core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.4.tgz", + "integrity": "sha512-URwiQ5ALx/sJ2iH4vzXEd+H4K6NAI7LRs6Jag3hrgKEpGmaE6alfRC8qjO4GIgb6A3ACaJumqP9twi/M9ywdHQ==", + "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.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": "^10.2.5", + "semver": "^7.8.1", + "string-width": "^4.2.3", + "supports-color": "^8", + "tinyglobby": "^0.2.16", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "3.2.50", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.50.tgz", + "integrity": "sha512-SQRIJSYue/1tIn7X55W/97gTb8UkSoHeFAcBng2r2YMJyWj8uB1DtFl28D8BDXPQXPTiPK89hQGejoT7RdkR2w==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4", + "ansis": "^3.16.0", + "debug": "^4.4.1", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "6.2.50", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.50.tgz", + "integrity": "sha512-rNCG4hUm+kPXFbhJfAVk/fZ3OdWJYwBDASlyX8CqOLP0MssjIGl7iEgfZz7TMuZFa+KucupKU5NRSc0KWfPTQA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "3.2.87", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.87.tgz", + "integrity": "sha512-lKyZ4INrx5vB14HNWIkM6Vla/4rWVhOA2U7uCAj6gEBg36/KVmwYXxpZ9ckzZS0+jtLE84TVqS8NCYEhQiQojw==", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.10.1", + "@oclif/core": "^4.11.4", + "ansis": "^3.17.0", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@raycast/api": { + "version": "1.104.19", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.104.19.tgz", + "integrity": "sha512-SAVg56BAzxZGy/OPQ0jekUG3pJaoX5pCqleALvFo9JRE7P2tvKoglWnYRcIJArRhLlvV8FtFNMDafd+NNwXXCw==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4.8.4", + "@oclif/plugin-autocomplete": "^3.2.40", + "@oclif/plugin-help": "^6.2.37", + "@oclif/plugin-not-found": "^3.2.74", + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "esbuild": "^0.27.3", + "react": "19.0.0" + }, + "bin": { + "ray": "bin/run.js" + }, + "engines": { + "node": ">=22.22.2" + }, + "peerDependencies": { + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "react-devtools": "6.1.1" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react-devtools": { + "optional": true + } + } + }, + "node_modules/@raycast/eslint-config": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@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.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", + "prettier": ">=2", + "typescript": ">=4" + } + }, + "node_modules/@raycast/eslint-plugin": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.1.1.tgz", + "integrity": "sha512-r2gs8uIlNp6I2mLOyN/kReGlvigzEeuyQPl4yw7nwLy8Zxjfjhg8txMViaBux8juBWBxbSWq/IfW6ZA50oeOHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.26.1" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/@raycast/utils": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-2.2.6.tgz", + "integrity": "sha512-/6NUftsaNjboTOhTiadRhQ+hgfsg9AxKdYCpGGbwfUghTX9M+ljbGBesCxQ+X5GBIBM+9lH3ma7GMwZPSPzJgw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "@raycast/api": ">=1.99.4", + "react": ">=19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "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==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/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", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@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", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "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", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz", + "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/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": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jose": { + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/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", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/extensions/sunsama/package.json b/extensions/sunsama/package.json new file mode 100644 index 00000000000..d49a508fee0 --- /dev/null +++ b/extensions/sunsama/package.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "sunsama", + "description": "Create, Edit, View, and Delete Tasks in Sunsama", + "license": "MIT", + "scripts": { + "dev": "ray develop", + "lint": "ray lint", + "fix-lint": "ray lint --fix", + "test": "vitest run", + "build": "ray build", + "publish": "npx @raycast/api@latest publish", + "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" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "@raycast/api": "^1.103.0", + "@raycast/utils": "^2.2.1" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.0.4", + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "eslint": "^9.22.0", + "prettier": "^3.5.3", + "typescript": "^5.8.2", + "vitest": "^4.1.8" + }, + "icon": "extension-icon.png", + "commands": [ + { + "mode": "view", + "name": "add-task", + "title": "Add Task", + "description": "Add a task to Sunsama, optionally linked to a URL (Trello, GitHub, Todoist, ClickUp, ...)", + "subtitle": "Sunsama", + "icon": "extension-icon.png", + "keywords": [ + "create", + "create task", + "new task", + "todo", + "task" + ], + "preferences": [ + { + "name": "rememberChannelMinutes", + "type": "dropdown", + "required": false, + "default": "0", + "title": "Reuse Last Channel", + "description": "Preselect the channel you last created a task in, if you used it recently. Falls back to the default channel after that.", + "data": [ + { "title": "Off", "value": "0" }, + { "title": "For 3 minutes", "value": "3" }, + { "title": "For 5 minutes", "value": "5" }, + { "title": "For 15 minutes", "value": "15" }, + { "title": "For 1 hour", "value": "60" } + ] + }, + { + "name": "defaultPosition", + "type": "dropdown", + "required": false, + "default": "top", + "title": "Default Position", + "description": "Where new tasks are placed in the day by default.", + "data": [ + { + "title": "Top of day", + "value": "top" + }, + { + "title": "Bottom of day", + "value": "bottom" + } + ] + } + ] + }, + { + "mode": "view", + "name": "view-today", + "title": "View Today's Tasks", + "description": "View, edit, delete, and add subtasks to today's tasks", + "subtitle": "Sunsama", + "icon": "extension-icon.png", + "preferences": [ + { + "name": "showCompleted", + "type": "checkbox", + "required": false, + "default": false, + "label": "Show completed tasks", + "description": "Show today's completed tasks (at the bottom of the list)." + } + ] + }, + { + "mode": "view", + "name": "set-default-channel", + "title": "Set Default Channel", + "description": "Choose the channel new tasks default to", + "subtitle": "Sunsama", + "icon": "extension-icon.png" + } + ], + "preferences": [ + { + "name": "workspaceUrl", + "type": "textfield", + "required": false, + "title": "Workspace URL", + "placeholder": "https://app.sunsama.com/group/your-workspace", + "description": "Lets Open in Sunsama jump straight to a task. Copy it from the address bar while Sunsama is open. Without it, Open in Sunsama opens the app's home page." + } + ], + "title": "Sunsama", + "author": "artistro08", + "platforms": [ + "Windows", + "macOS" + ], + "categories": [ + "Applications", + "Productivity" + ] +} diff --git a/extensions/sunsama/src/add-task.tsx b/extensions/sunsama/src/add-task.tsx new file mode 100644 index 00000000000..cb8a02206f1 --- /dev/null +++ b/extensions/sunsama/src/add-task.tsx @@ -0,0 +1,207 @@ +import { + Action, + ActionPanel, + Form, + getPreferenceValues, + Icon, + PopToRootType, + showHUD, + showToast, + Toast, +} from "@raycast/api"; +import { FormValidation, useCachedPromise, useForm } from "@raycast/utils"; +import { useEffect, useState } from "react"; +import { + createTask, + getDefaultChannel, + getRecentChannel, + rememberLastChannel, +} from "./lib/sunsama-client"; +import { toDayString } from "./lib/date"; +import { reportError } from "./lib/errors"; +import { parseDuration, parseSubtasks } from "./lib/time"; +import { + ChannelDropdown, + RefreshChannelsAction, + useChannels, +} from "./components/channel-dropdown"; + +interface FormValues { + task: string; + notes: string; + day: Date | null; + channel: string; + position: string; + timeEstimate: string; + subtasks: string; +} + +const isUrl = (s: string) => /^https?:\/\/\S+$/i.test(s); + +const TIME_HINT = "1h 30m · 90 · 1:15 · 45m"; + +export default function AddTask() { + const [submitting, setSubmitting] = useState(false); + const { defaultPosition, rememberChannelMinutes } = + getPreferenceValues(); + const rememberFor = Number(rememberChannelMinutes) || 0; + const channels = useChannels(); + + // The channel to start on: the one just used, while it's still recent, and + // otherwise the saved default. Resolved together so the field is only set + // once and doesn't visibly change under the cursor. + const { data: startingChannel } = useCachedPromise( + async (minutes: number) => { + const recent = await getRecentChannel(minutes); + if (recent) return recent; + return (await getDefaultChannel())?.name ?? ""; + }, + [rememberFor], + ); + + // useForm keeps the fields controlled, which is what lets the saved default + // channel be applied below once it resolves — `defaultValue` is only read + // once per component lifecycle and would ignore it. + const { handleSubmit, itemProps, setValue } = useForm({ + onSubmit: submit, + initialValues: { + task: "", + notes: "", + day: new Date(), + channel: startingChannel ?? "", + position: defaultPosition, + timeEstimate: "", + subtasks: "", + }, + validation: { + task: FormValidation.Required, + // Optional, but must parse when filled in. + timeEstimate: (value) => + value?.trim() && parseDuration(value) === null + ? `Enter a time like ${TIME_HINT}` + : undefined, + }, + }); + + // On the very first run (no cache yet), apply it once it resolves. + useEffect(() => { + if (startingChannel) setValue("channel", startingChannel); + }, [startingChannel, setValue]); + + async function submit(values: FormValues) { + const entry = values.task.trim(); + + // The single field supports three shapes; the server handles the link + // natively (Trello/GitHub/Todoist/ClickUp/… or a plain web page): + // " " → link it, and use the text after the space as the title + // "<link>" → link it, title comes from the linked item + // "<text>" → a plain task title + let url: string | undefined; + let title = entry; + const leading = entry.match(/^(\S+)\s+(.+)$/); + if (leading && isUrl(leading[1])) { + url = leading[1]; + title = leading[2].trim(); + } else if (isUrl(entry)) { + url = entry; + title = ""; + } + + // Validation already rejected anything unparseable. + const timeEstimate = values.timeEstimate.trim() + ? (parseDuration(values.timeEstimate) as number) + : undefined; + + setSubmitting(true); + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Creating task…", + }); + try { + const createdTitle = await createTask({ + title, + day: toDayString(values.day ?? new Date()), + url, + notes: values.notes.trim() || undefined, + channel: values.channel || undefined, + position: values.position === "bottom" ? "bottom" : "top", + timeEstimate, + subtasks: parseSubtasks(values.subtasks), + }); + // Recorded after the task lands, so the next one can start here while + // it's still recent. + await rememberLastChannel(values.channel); + await toast.hide(); + // Close and go back to root. Without an explicit type this follows the + // user's "Pop to Root Search" preference, which can leave the filled-in + // form on the stack for the next launch. + await showHUD(`Added task: ${createdTitle}`, { + popToRootType: PopToRootType.Immediate, + clearRootSearch: true, + }); + } catch (error) { + toast.hide(); + await reportError(error, "Failed to create task"); + } finally { + setSubmitting(false); + } + } + + return ( + <Form + isLoading={submitting} + actions={ + <ActionPanel> + <Action.SubmitForm + title="Create Task" + icon={Icon.Plus} + onSubmit={handleSubmit} + /> + <RefreshChannelsAction channels={channels} /> + </ActionPanel> + } + > + <Form.TextField + {...itemProps.task} + title="Task" + placeholder="What needs doing — or paste a link" + autoFocus + /> + <Form.TextArea + {...itemProps.notes} + title="Notes" + placeholder="Optional details (Markdown supported)" + enableMarkdown + /> + <Form.DatePicker + {...itemProps.day} + title="Day" + type={Form.DatePicker.Type.Date} + /> + <Form.Separator /> + <ChannelDropdown {...itemProps.channel} channels={channels} /> + <Form.Dropdown {...itemProps.position} title="Position"> + <Form.Dropdown.Item + value="top" + title="Top of day" + icon={Icon.ArrowUp} + /> + <Form.Dropdown.Item + value="bottom" + title="Bottom of day" + icon={Icon.ArrowDown} + /> + </Form.Dropdown> + <Form.TextField + {...itemProps.timeEstimate} + title="Time Estimate" + placeholder={TIME_HINT} + /> + <Form.TextArea + {...itemProps.subtasks} + title="Subtasks" + placeholder="One subtask per line" + /> + </Form> + ); +} diff --git a/extensions/sunsama/src/components/add-subtasks-form.tsx b/extensions/sunsama/src/components/add-subtasks-form.tsx new file mode 100644 index 00000000000..4f27fb59cc3 --- /dev/null +++ b/extensions/sunsama/src/components/add-subtasks-form.tsx @@ -0,0 +1,68 @@ +import { Action, ActionPanel, Form, Icon, useNavigation } from "@raycast/api"; +import { useForm } from "@raycast/utils"; +import { useState } from "react"; +import { addSubtasks } from "../lib/sunsama-client"; +import { runWithToast } from "../lib/errors"; +import { parseSubtasks } from "../lib/time"; +import { Task } from "../lib/types"; + +interface Props { + task: Task; + onSaved: () => void; +} + +export function AddSubtasksForm({ task, onSaved }: Props) { + const { pop } = useNavigation(); + const [submitting, setSubmitting] = useState(false); + + const { handleSubmit, itemProps } = useForm<{ subtasks: string }>({ + async onSubmit(values) { + const subtasks = parseSubtasks(values.subtasks); + setSubmitting(true); + const ok = await runWithToast( + { + pending: "Adding subtasks…", + success: `Added ${subtasks.length} subtask${subtasks.length > 1 ? "s" : ""}`, + failure: "Failed to add subtasks", + }, + () => addSubtasks(task.id, subtasks), + ); + setSubmitting(false); + if (ok) { + onSaved(); + pop(); + } + }, + validation: { + subtasks: (value) => + parseSubtasks(value ?? "").length === 0 + ? "Enter at least one subtask" + : undefined, + }, + }); + + return ( + <Form + isLoading={submitting} + actions={ + <ActionPanel> + <Action.SubmitForm + title="Add Subtasks" + icon={Icon.Plus} + onSubmit={handleSubmit} + /> + </ActionPanel> + } + > + <Form.Description + text={`Adding subtasks to “${task.title}”. One per line.`} + /> + <Form.TextArea + {...itemProps.subtasks} + title="Subtasks" + placeholder={"Draft outline\nReview with team"} + autoFocus + /> + </Form> + ); +} diff --git a/extensions/sunsama/src/components/channel-dropdown.tsx b/extensions/sunsama/src/components/channel-dropdown.tsx new file mode 100644 index 00000000000..527ccf69481 --- /dev/null +++ b/extensions/sunsama/src/components/channel-dropdown.tsx @@ -0,0 +1,197 @@ +import { Action, Form, Icon, Keyboard } from "@raycast/api"; +import { useCachedPromise } from "@raycast/utils"; +import { useEffect, useMemo, useState } from "react"; +import { Channel } from "../lib/types"; +import { + loadChannels, + refreshChannels, + rememberChannels, + searchChannels, +} from "../lib/sunsama-client"; +import { matchesChannel, matchesNoChannel } from "../lib/channels"; +import { reportError, runWithToast } from "../lib/errors"; + +export interface Channels { + list: Channel[]; + isLoading: boolean; + /** Whether there is a list to render yet — see the comment below. */ + ready: boolean; + /** What's currently typed, for the picker to narrow its own items with. */ + query: string; + /** Feed the picker's search text in, so the server can be asked too. */ + onSearchTextChange: (text: string) => void; + /** Re-sweep the server and replace the stored list. */ + refresh: () => Promise<void>; +} + +/** + * The channel list, shared by every picker. + * + * Two sources, merged. The stored list is swept once and reused, so opening a + * picker is cheap. On top of that, whatever is typed is also sent to the + * server: `search_channels` ranks semantically, returns at most 25, and has no + * pagination, so no sweep can be guaranteed complete — but an exact name ranks + * first, so searching what the user typed always reaches the channel they + * mean. Anything new that turns up is added to the stored list. + */ +export function useChannels(): Channels { + const [query, setQuery] = useState(""); + // Filtering runs off `query` so it keeps up with typing; only the server + // lookup waits, which is what the delay is actually for. Throttling the + // search text itself would hold the filter back too. + const [settled, setSettled] = useState(""); + useEffect(() => { + const id = setTimeout(() => setSettled(query), 300); + return () => clearTimeout(id); + }, [query]); + + const { data, isLoading, revalidate } = useCachedPromise(loadChannels, [], { + onError: (error) => reportError(error, "Failed to load channels"), + }); + + const stored = useMemo(() => data ?? [], [data]); + + // Ask the server only when nothing stored matches what was typed. Searching + // on every keystroke means results arriving mid-type, which reshuffles the + // list under the cursor; this way the stored list answers almost everything + // and the server is a fallback for what the sweep couldn't reach. + const text = settled.trim(); + const missingLocally = useMemo( + () => text.length >= 2 && !stored.some((c) => matchesChannel(c, text)), + [stored, text], + ); + + const { data: found, isLoading: isSearching } = useCachedPromise( + searchChannels, + [text], + { + execute: missingLocally, + keepPreviousData: true, + // A failed lookup just means no extra results; the stored list still + // shows. + onError: () => undefined, + }, + ); + + // Everything the server returned is a real channel, so it all goes into + // storage and widens the list for next time. + useEffect(() => { + if (found?.length) void rememberChannels(found); + }, [found]); + + // Merged as-is; the pickers narrow it themselves with `matchesChannel`. + const list = useMemo(() => { + const byId = new Map(stored.map((c) => [c.id, c])); + for (const channel of found ?? []) byId.set(channel.id, channel); + return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name)); + }, [stored, found]); + + return { + list, + // Only when there is genuinely nothing to show yet — the first sweep — or + // a server lookup is in flight. Re-reading stored channels on each launch + // also flips `isLoading`, and flagging that would show a loading state + // over a list that is already complete. + isLoading: (isLoading && data === undefined) || isSearching, + // `useCachedPromise` hands back cached data on the first render while it + // revalidates, so `data` being set — not `isLoading` being false — is the + // signal that there's a list to render. Checking `data` rather than its + // length also keeps this right for an account with no channels at all. + ready: data !== undefined || !isLoading, + query, + onSearchTextChange: setQuery, + refresh: async () => { + const ok = await runWithToast( + { + pending: "Refreshing channels…", + success: "Channels refreshed", + failure: "Failed to refresh channels", + }, + refreshChannels, + ); + if (ok) revalidate(); + }, + }; +} + +/** The action that re-sweeps the channel list, for a command's action panel. */ +export function RefreshChannelsAction({ channels }: { channels: Channels }) { + return ( + <Action + title="Refresh Channels" + icon={Icon.ArrowClockwise} + shortcut={Keyboard.Shortcut.Common.Refresh} + onAction={channels.refresh} + /> + ); +} + +/** Accepts every Form item prop, so `useForm`'s itemProps can be spread in. */ +interface Props extends Partial<Form.ItemProps<string>> { + id: string; + channels: Channels; + /** + * Whether "No channel" can be picked. The server has no way to take a task + * out of a channel — `add_task_to_channel` rejects an empty name and there is + * no remove tool — so a task that already has one must not be offered it. + */ + allowNone?: boolean; +} + +/** + * A channel picker. + * + * Filtering is done here rather than by Raycast. Reaching a channel the stored + * list is missing means sending the query to the server, and handling the + * search text switches Raycast's own filtering off — so `filtering` is false + * and the list is narrowed with `matchesChannel`. + */ +export function ChannelDropdown({ + channels, + allowNone = true, + title, + ...dropdownProps +}: Props) { + const { list, isLoading, ready, query, onSearchTextChange } = channels; + + const visible = list.filter((ch) => matchesChannel(ch, query)); + + // A task can sit in a channel the sweep never surfaced, so the selected one + // gets an item of its own when the list has none — otherwise opening the + // form would silently reassign it. It is filtered like any other item; a + // selection the query excludes just isn't shown, same as Raycast's own + // filtering, and the value survives. + const selected = dropdownProps.value ?? dropdownProps.defaultValue ?? ""; + const missing = + selected && !list.some((ch) => ch.name === selected) ? selected : null; + + return ( + <Form.Dropdown + {...dropdownProps} + title={title ?? "Channel"} + isLoading={isLoading} + // Says why the list is short instead of leaving it looking empty. + placeholder={isLoading ? "Loading channels…" : "Search channels"} + onSearchTextChange={onSearchTextChange} + filtering={false} + > + {/* Shown as soon as there is a list to sit on top of, so the dropdown + never opens on a lone "No channel" that the rest then shifts. */} + {ready && allowNone && matchesNoChannel(query) && ( + <Form.Dropdown.Item value="" title="No channel" /> + )} + {missing && matchesChannel({ id: missing, name: missing }, query) && ( + <Form.Dropdown.Item value={missing} title={missing} /> + )} + {visible.map((ch) => ( + <Form.Dropdown.Item + key={ch.id} + value={ch.name} + // A category can hold tasks of its own, so it appears alongside the + // channels inside it. + title={ch.isCategory ? `${ch.name} (category)` : ch.name} + /> + ))} + </Form.Dropdown> + ); +} diff --git a/extensions/sunsama/src/components/edit-subtask-form.tsx b/extensions/sunsama/src/components/edit-subtask-form.tsx new file mode 100644 index 00000000000..289f90abf18 --- /dev/null +++ b/extensions/sunsama/src/components/edit-subtask-form.tsx @@ -0,0 +1,65 @@ +import { Action, ActionPanel, Form, Icon, useNavigation } from "@raycast/api"; +import { FormValidation, useForm } from "@raycast/utils"; +import { useState } from "react"; +import { editSubtaskTitle } from "../lib/sunsama-client"; +import { runWithToast } from "../lib/errors"; +import { Subtask } from "../lib/types"; + +interface Props { + taskId: string; + subtask: Subtask; + onSaved: () => void; +} + +export function EditSubtaskForm({ taskId, subtask, onSaved }: Props) { + const { pop } = useNavigation(); + const [saving, setSaving] = useState(false); + + const { handleSubmit, itemProps } = useForm<{ title: string }>({ + async onSubmit(values) { + const title = values.title.trim(); + if (title === subtask.title) { + pop(); + return; + } + + setSaving(true); + const ok = await runWithToast( + { + pending: "Saving…", + success: "Subtask updated", + failure: "Failed to update subtask", + }, + () => editSubtaskTitle(taskId, subtask.id, title), + ); + setSaving(false); + if (ok) { + onSaved(); + pop(); + } + }, + initialValues: { title: subtask.title }, + validation: { title: FormValidation.Required }, + }); + + return ( + <Form + isLoading={saving} + actions={ + <ActionPanel> + <Action.SubmitForm + title="Save" + icon={Icon.Check} + onSubmit={handleSubmit} + /> + </ActionPanel> + } + > + <Form.TextField + {...itemProps.title} + title="Subtask" + placeholder="What this step involves" + /> + </Form> + ); +} diff --git a/extensions/sunsama/src/components/edit-task-form.tsx b/extensions/sunsama/src/components/edit-task-form.tsx new file mode 100644 index 00000000000..01254c14b91 --- /dev/null +++ b/extensions/sunsama/src/components/edit-task-form.tsx @@ -0,0 +1,273 @@ +import { + Action, + ActionPanel, + Alert, + Color, + confirmAlert, + Form, + Icon, + useNavigation, +} from "@raycast/api"; +import { FormValidation, useForm } from "@raycast/utils"; +import { useRef, useState } from "react"; +import { + editNotes, + editTitle, + plannedTimeSteps, + rescheduleTask, + setChannel, + subtasksWithPlannedTime, +} from "../lib/sunsama-client"; +import { runWithToast } from "../lib/errors"; +import { toDayString } from "../lib/date"; +import { parseDuration } from "../lib/time"; +import { Task } from "../lib/types"; +import { + ChannelDropdown, + RefreshChannelsAction, + useChannels, +} from "./channel-dropdown"; + +const TIME_HINT = "1h 30m · 90 · 1:15 · 45m"; + +interface Props { + task: Task; + /** The day the task is currently on (YYYY-MM-DD), used to detect a move. */ + day: string; + onSaved: () => void; +} + +interface FormValues { + title: string; + notes: string; + day: Date | null; + channel: string; + timeEstimate: string; +} + +export function EditTaskForm({ task, day, onSaved }: Props) { + const { pop } = useNavigation(); + const [submitting, setSubmitting] = useState(false); + + const currentChannel = task.channelName ?? ""; + const channels = useChannels(); + + // What the server is believed to hold. Each field is saved by its own + // request, so a failure part-way through leaves some already applied; this + // advances as they succeed, and the form diffs against it rather than the + // values it opened with. Retrying after a partial save then sends exactly + // what is still outstanding — including a field changed back to what it + // originally was, which a diff against the untouched task would miss. + const saved = useRef({ + title: task.title, + notes: (task.notes ?? "").trim(), + channel: currentChannel, + day, + timeEstimate: task.timeEstimate, + subtasksWithTime: subtasksWithPlannedTime(task), + }); + + const { handleSubmit, itemProps } = useForm<FormValues>({ + async onSubmit(values) { + const title = values.title.trim(); + const notes = values.notes.trim(); + const newDay = values.day ? toDayString(values.day) : null; + const newEstimate = values.timeEstimate.trim() + ? (parseDuration(values.timeEstimate) as number) + : undefined; + + // Only send fields that differ from what the server holds. `commit` + // records the new value once its request succeeds. + const changes: Array<{ run: () => Promise<void>; commit: () => void }> = + []; + if (title !== saved.current.title) { + changes.push({ + run: () => editTitle(task.id, title), + commit: () => (saved.current.title = title), + }); + } + + // A task's estimate is derived from its subtasks whenever they carry one, + // and the server rejects a task-level estimate until those are cleared. + const blockingSubtasks = [...saved.current.subtasksWithTime]; + if ( + newEstimate !== undefined && + newEstimate !== saved.current.timeEstimate + ) { + if (blockingSubtasks.length > 0) { + const ok = await confirmAlert({ + title: "Clear subtask planned times?", + message: + "This task's planned time is the sum of its subtasks. Changing it will clear all subtask planned times.", + icon: { source: Icon.Clock }, + primaryAction: { + title: "Set & Clear Subtasks", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!ok) return; + } + // Added as individual steps because each clears one subtask and + // persists on its own; the step without `clears` sets the estimate. + for (const step of plannedTimeSteps( + task.id, + newEstimate, + blockingSubtasks, + )) { + changes.push({ + run: step.run, + commit: () => { + // Record only the subtask this step cleared. Marking them all + // cleared would make a retry skip the ones that never ran, and + // the server rejects the estimate while any of them still has + // planned time. + if (step.clears) { + saved.current.subtasksWithTime = + saved.current.subtasksWithTime.filter( + (id) => id !== step.clears, + ); + } else { + saved.current.timeEstimate = newEstimate; + } + }, + }); + } + } + + if (values.channel && values.channel !== saved.current.channel) { + changes.push({ + run: () => setChannel(task.id, values.channel), + commit: () => (saved.current.channel = values.channel), + }); + } + if (notes !== saved.current.notes) { + changes.push({ + run: () => editNotes(task.id, notes), + commit: () => (saved.current.notes = notes), + }); + } + if (newDay !== saved.current.day) { + changes.push({ + run: () => rescheduleTask(task.id, newDay), + commit: () => (saved.current.day = newDay ?? day), + }); + } + + if (changes.length === 0) { + pop(); + return; + } + + // Clearing the day moves the task off every day into the backlog — + // confirm, since it then disappears from this list. + if (newDay === null) { + const confirmed = await confirmAlert({ + title: "Move to backlog?", + message: `"${task.title}" will be removed from this day and moved to your backlog.`, + icon: { source: Icon.Tray, tintColor: Color.Orange }, + primaryAction: { title: "Move to Backlog" }, + }); + if (!confirmed) return; + } + + setSubmitting(true); + // Each field is its own request and the server has no way to apply them + // together, so a failure part-way through leaves the earlier ones saved. + // Report how far it got rather than a bare failure, and refresh either + // way so the list shows what actually landed. + let applied = 0; + const ok = await runWithToast( + { + pending: "Saving…", + success: "Task updated", + failure: "Failed to update task", + }, + async () => { + try { + for (const change of changes) { + await change.run(); + change.commit(); + applied++; + } + } catch (error) { + const detail = + error instanceof Error ? error.message : String(error); + throw applied > 0 + ? new Error( + `Saved ${applied} of ${changes.length} changes, then: ${detail}`, + ) + : error; + } + }, + ); + setSubmitting(false); + + // Refresh the list for whatever did land. + if (applied > 0) onSaved(); + // Close only when everything saved. On a partial failure the form stays + // open with the values still in it, so the edit isn't lost and can be + // retried against what actually landed. + if (ok) pop(); + }, + initialValues: { + title: task.title, + notes: task.notes ?? "", + day: new Date(`${day}T00:00:00`), + channel: currentChannel, + timeEstimate: task.timeEstimate != null ? String(task.timeEstimate) : "", + }, + validation: { + title: FormValidation.Required, + // Optional, but must parse when filled in. + timeEstimate: (value) => + value?.trim() && parseDuration(value) === null + ? `Enter a time like ${TIME_HINT}` + : undefined, + }, + }); + + return ( + <Form + isLoading={submitting} + actions={ + <ActionPanel> + <Action.SubmitForm + title="Save Changes" + icon={Icon.Check} + onSubmit={handleSubmit} + /> + <RefreshChannelsAction channels={channels} /> + </ActionPanel> + } + > + <Form.TextField + {...itemProps.title} + title="Title" + placeholder="What needs doing" + /> + <Form.TextArea + {...itemProps.notes} + title="Notes" + placeholder="Optional details (Markdown supported)" + enableMarkdown + /> + <Form.DatePicker + {...itemProps.day} + title="Day" + type={Form.DatePicker.Type.Date} + /> + <ChannelDropdown + {...itemProps.channel} + channels={channels} + // A task already in a channel can't be taken out of one, so don't + // offer an option that would silently do nothing. + allowNone={!currentChannel} + /> + <Form.TextField + {...itemProps.timeEstimate} + title="Time Estimate" + placeholder={TIME_HINT} + /> + </Form> + ); +} diff --git a/extensions/sunsama/src/components/set-time-form.tsx b/extensions/sunsama/src/components/set-time-form.tsx new file mode 100644 index 00000000000..60ee613502e --- /dev/null +++ b/extensions/sunsama/src/components/set-time-form.tsx @@ -0,0 +1,142 @@ +import { + Action, + ActionPanel, + Alert, + confirmAlert, + Form, + Icon, + useNavigation, +} from "@raycast/api"; +import { useForm } from "@raycast/utils"; +import { useState } from "react"; +import { plannedTimeSteps, setPlannedTime } from "../lib/sunsama-client"; +import { runWithToast } from "../lib/errors"; +import { parseDuration } from "../lib/time"; + +const TIME_HINT = "1h 30m · 90 · 1:15 · 45m"; + +interface Props { + taskId: string; + /** When set, edits a subtask's planned time instead of the task's. */ + subtaskId?: string; + title: string; + /** Current value in minutes, used to prefill the field. */ + currentMinutes: number; + /** + * Subtask ids that carry their own planned time. A task's planned time is the + * sum of its subtasks, so the server refuses a task-level estimate until + * these are cleared — we confirm and clear them first. + */ + clearSubtaskIds?: string[]; + onSaved: () => void; +} + +export function SetTimeForm({ + taskId, + subtaskId, + title, + currentMinutes, + clearSubtaskIds, + onSaved, +}: Props) { + const { pop } = useNavigation(); + const [saving, setSaving] = useState(false); + + // A task's planned total is driven by its subtasks; clearing them is required + // for the new value to take effect. + const mustClearSubtasks = !subtaskId && (clearSubtaskIds?.length ?? 0) > 0; + + const { handleSubmit, itemProps } = useForm<{ value: string }>({ + async onSubmit(values) { + // Validation already rejected anything unparseable. + const minutes = parseDuration(values.value) as number; + + if (mustClearSubtasks) { + const ok = await confirmAlert({ + title: "Clear subtask planned times?", + message: + "This task's planned time is the sum of its subtasks. Setting it will clear all subtask planned times.", + icon: { source: Icon.Clock }, + primaryAction: { + title: "Set & Clear Subtasks", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!ok) return; + } + + // Setting a task's own time can take several requests, since each + // subtask estimate has to be cleared first and each one persists + // separately. Track them individually so a failure part-way through + // still refreshes what landed. + const steps = subtaskId + ? [{ run: () => setPlannedTime(taskId, minutes, subtaskId) }] + : plannedTimeSteps(taskId, minutes, clearSubtaskIds ?? []); + + setSaving(true); + let applied = 0; + const ok = await runWithToast( + { + pending: "Saving…", + success: "Planned time set", + failure: "Failed to set planned time", + }, + async () => { + try { + for (const step of steps) { + await step.run(); + applied++; + } + } catch (error) { + const detail = + error instanceof Error ? error.message : String(error); + throw applied > 0 + ? new Error( + `Applied ${applied} of ${steps.length} changes, then: ${detail}`, + ) + : error; + } + }, + ); + setSaving(false); + + // Refresh the list for whatever did land. + if (applied > 0) onSaved(); + // Close only when everything saved, so a partial failure keeps the + // entered time on screen to retry rather than discarding it. + if (ok) pop(); + }, + initialValues: { + value: currentMinutes > 0 ? String(currentMinutes) : "", + }, + validation: { + value: (value) => + parseDuration(value ?? "") === null + ? `Enter a time like ${TIME_HINT}` + : undefined, + }, + }); + + return ( + <Form + isLoading={saving} + actions={ + <ActionPanel> + <Action.SubmitForm + title="Set Planned Time" + icon={Icon.Clock} + onSubmit={handleSubmit} + /> + </ActionPanel> + } + > + <Form.Description text={`Set planned time for “${title}”.`} /> + <Form.TextField + {...itemProps.value} + title="Planned Time" + placeholder={TIME_HINT} + autoFocus + /> + </Form> + ); +} diff --git a/extensions/sunsama/src/components/subtasks-list.tsx b/extensions/sunsama/src/components/subtasks-list.tsx new file mode 100644 index 00000000000..b53dff57680 --- /dev/null +++ b/extensions/sunsama/src/components/subtasks-list.tsx @@ -0,0 +1,226 @@ +import { Action, ActionPanel, Color, Icon, Keyboard, List } from "@raycast/api"; +import { useCachedPromise } from "@raycast/utils"; +import { useEffect, useState } from "react"; +import { + completeSubtask, + getActiveTimer, + getTask, + startTimer, + stopTimer, + uncompleteSubtask, + withActiveTimer, +} from "../lib/sunsama-client"; +import { reportError, runWithToast } from "../lib/errors"; +import { formatElapsed } from "../lib/time"; +import { xShortcut } from "../lib/shortcuts"; +import { Subtask, Task } from "../lib/types"; +import { EditSubtaskForm } from "./edit-subtask-form"; +import { AddSubtasksForm } from "./add-subtasks-form"; +import { SetTimeForm } from "./set-time-form"; + +interface Props { + task: Task; + /** Refresh the parent list when subtasks change. */ + onChanged: () => void; +} + +function accessories(subtask: Subtask, now: number): List.Item.Accessory[] { + const items: List.Item.Accessory[] = []; + if (subtask.isRunning) { + const current = subtask.timerStart + ? Math.floor((now - Date.parse(subtask.timerStart)) / 1000) + : undefined; + items.push({ + tag: { + value: current === undefined ? "running" : formatElapsed(current), + color: Color.Green, + }, + icon: { source: Icon.Stopwatch, tintColor: Color.Green }, + tooltip: "Timer running", + }); + } + if (typeof subtask.timeEstimate === "number" && subtask.timeEstimate > 0) { + items.push({ icon: Icon.Clock, text: `${subtask.timeEstimate}m` }); + } + return items; +} + +export function SubtasksList({ task, onChanged }: Props) { + const { data, isLoading, revalidate } = useCachedPromise( + (id: string) => getTask(id), + [task.id], + { + initialData: task, + onError: (error) => reportError(error, "Failed to load subtasks"), + }, + ); + // Fetched separately so the subtask list isn't gated on it. + const { data: timer, revalidate: revalidateTimer } = useCachedPromise( + getActiveTimer, + [], + { onError: () => undefined }, + ); + + const current = withActiveTimer([data ?? task], timer ?? null)[0]; + const subtasks = current.subtasks; + + const [now, setNow] = useState(() => Date.now()); + const hasRunningTimer = subtasks.some((s) => s.timerStart); + useEffect(() => { + if (!hasRunningTimer) return; + const id = setInterval(() => setNow(Date.now()), 1000); + return () => clearInterval(id); + }, [hasRunningTimer]); + + function refresh() { + revalidate(); + revalidateTimer(); + onChanged(); + } + + /** Run a mutation behind a toast and refresh both views when it succeeds. */ + async function run( + labels: { pending: string; success: string; failure: string }, + action: () => Promise<unknown>, + ) { + if (await runWithToast(labels, action)) refresh(); + } + + async function onToggle(subtask: Subtask) { + await run( + { + pending: subtask.completed ? "Marking incomplete…" : "Completing…", + success: subtask.completed ? "Marked incomplete" : "Subtask completed", + failure: "Failed to update subtask", + }, + () => + subtask.completed + ? uncompleteSubtask(task.id, subtask.id) + : completeSubtask(task.id, subtask.id), + ); + } + + async function onStartTimer(subtask: Subtask) { + await run( + { + pending: "Starting timer…", + success: "Timer started", + failure: "Failed to start timer", + }, + () => startTimer(task.id, subtask.id), + ); + } + + async function onStopTimer(subtask: Subtask) { + await run( + { + pending: "Stopping timer…", + success: "Timer stopped", + failure: "Failed to stop timer", + }, + () => stopTimer(task.id, subtask.id), + ); + } + + const doneCount = subtasks.filter((s) => s.completed).length; + + return ( + <List + isLoading={isLoading} + navigationTitle={current.title} + searchBarPlaceholder={`${doneCount}/${subtasks.length} subtasks complete`} + > + <List.EmptyView + icon={Icon.PlusCircle} + title="No subtasks" + description="Add subtasks with ⌘N." + actions={ + <ActionPanel> + <Action.Push + title="Add Subtasks" + icon={Icon.PlusCircle} + target={<AddSubtasksForm task={current} onSaved={refresh} />} + /> + </ActionPanel> + } + /> + {subtasks.map((subtask) => ( + <List.Item + key={subtask.id} + icon={ + subtask.isRunning + ? { source: Icon.Stopwatch, tintColor: Color.Green } + : subtask.completed + ? { source: Icon.CheckCircle, tintColor: Color.Green } + : { source: Icon.Circle, tintColor: Color.SecondaryText } + } + title={subtask.title} + accessories={accessories(subtask, now)} + actions={ + <ActionPanel> + <ActionPanel.Section> + {subtask.isRunning ? ( + <Action + title="Stop Timer" + icon={Icon.Stop} + onAction={() => onStopTimer(subtask)} + /> + ) : ( + <Action + title="Start Timer" + icon={Icon.Stopwatch} + onAction={() => onStartTimer(subtask)} + /> + )} + <Action + title={ + subtask.completed + ? "Mark as Incomplete" + : "Mark as Completed" + } + icon={subtask.completed ? Icon.Circle : Icon.Check} + shortcut={xShortcut("enter")} + onAction={() => onToggle(subtask)} + /> + </ActionPanel.Section> + <ActionPanel.Section> + <Action.Push + title="Edit Subtask" + icon={Icon.Pencil} + shortcut={Keyboard.Shortcut.Common.Edit} + target={ + <EditSubtaskForm + taskId={task.id} + subtask={subtask} + onSaved={refresh} + /> + } + /> + <Action.Push + title="Add Subtasks" + icon={Icon.PlusCircle} + shortcut={Keyboard.Shortcut.Common.New} + target={<AddSubtasksForm task={current} onSaved={refresh} />} + /> + <Action.Push + title="Set Planned Time" + icon={Icon.Clock} + shortcut={xShortcut("p", "shift")} + target={ + <SetTimeForm + taskId={task.id} + subtaskId={subtask.id} + title={subtask.title} + currentMinutes={subtask.timeEstimate ?? 0} + onSaved={refresh} + /> + } + /> + </ActionPanel.Section> + </ActionPanel> + } + /> + ))} + </List> + ); +} diff --git a/extensions/sunsama/src/lib/channels.test.ts b/extensions/sunsama/src/lib/channels.test.ts new file mode 100644 index 00000000000..00336f9d758 --- /dev/null +++ b/extensions/sunsama/src/lib/channels.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { matchesChannel, matchesNoChannel } from "./channels"; +import { Channel } from "./types"; + +const channel = (name: string, categoryName?: string): Channel => ({ + id: name, + name, + categoryName, +}); + +describe("matchesChannel", () => { + it("matches on a plain substring", () => { + expect(matchesChannel(channel("Bark & Stay Retreat"), "bark")).toBe(true); + expect(matchesChannel(channel("Bark & Stay Retreat"), "retreat")).toBe( + true, + ); + }); + + it("ignores word order and punctuation the user leaves out", () => { + expect(matchesChannel(channel("Bark & Stay Retreat"), "bark stay")).toBe( + true, + ); + expect(matchesChannel(channel("Bark & Stay Retreat"), "bark & stay")).toBe( + true, + ); + expect(matchesChannel(channel("Bark & Stay Retreat"), "stay bark")).toBe( + true, + ); + }); + + it("is case-insensitive", () => { + expect(matchesChannel(channel("Bark & Stay Retreat"), "BARK")).toBe(true); + }); + + it("matches on the category too", () => { + expect(matchesChannel(channel("The Lab", "Work"), "work")).toBe(true); + }); + + it("rejects a channel missing any part of the query", () => { + expect(matchesChannel(channel("Bark & Stay Retreat"), "bark cafe")).toBe( + false, + ); + expect(matchesChannel(channel("The Lab"), "bark")).toBe(false); + }); + + it("matches everything on an empty query", () => { + expect(matchesChannel(channel("Anything"), "")).toBe(true); + expect(matchesChannel(channel("Anything"), " ")).toBe(true); + }); +}); + +describe("matchesNoChannel", () => { + it("matches while the query is a piece of the label", () => { + expect(matchesNoChannel("")).toBe(true); + expect(matchesNoChannel("no")).toBe(true); + expect(matchesNoChannel("chan")).toBe(true); + expect(matchesNoChannel("no channel")).toBe(true); + }); + + it("stops matching once the query names something else", () => { + expect(matchesNoChannel("bark")).toBe(false); + }); +}); diff --git a/extensions/sunsama/src/lib/channels.ts b/extensions/sunsama/src/lib/channels.ts new file mode 100644 index 00000000000..760bdbc89aa --- /dev/null +++ b/extensions/sunsama/src/lib/channels.ts @@ -0,0 +1,27 @@ +import { Channel } from "./types"; + +/** + * Whether a channel matches what was typed. + * + * Every whitespace-separated piece of the query has to appear somewhere in the + * channel's name or its category, so word order doesn't matter and "bark stay" + * finds "Bark & Stay Retreat" just as "bark & stay" does. + * + * The pickers filter with this rather than leaving it to Raycast: reaching a + * channel the stored list is missing means sending the query to the server, + * and once a component handles the search text Raycast stops filtering for it. + */ +export function matchesChannel(channel: Channel, query: string): boolean { + const parts = query.toLowerCase().split(/\s+/).filter(Boolean); + if (parts.length === 0) return true; + + const haystack = + `${channel.name} ${channel.categoryName ?? ""}`.toLowerCase(); + return parts.every((part) => haystack.includes(part)); +} + +/** Whether the literal "No channel" option matches what was typed. */ +export function matchesNoChannel(query: string): boolean { + const parts = query.toLowerCase().split(/\s+/).filter(Boolean); + return parts.every((part) => "no channel".includes(part)); +} diff --git a/extensions/sunsama/src/lib/date.test.ts b/extensions/sunsama/src/lib/date.test.ts new file mode 100644 index 00000000000..75555d7ffa2 --- /dev/null +++ b/extensions/sunsama/src/lib/date.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { addDays, isAfterDay, nextMonday } from "./date"; + +describe("addDays", () => { + it("advances within a month", () => { + expect(addDays("2026-08-11", 1)).toBe("2026-08-12"); + }); + + it("rolls over month and year boundaries", () => { + expect(addDays("2026-08-31", 1)).toBe("2026-09-01"); + expect(addDays("2026-12-31", 1)).toBe("2027-01-01"); + }); + + it("goes backwards for negative counts", () => { + expect(addDays("2026-03-01", -1)).toBe("2026-02-28"); + }); +}); + +describe("isAfterDay", () => { + it("is true only for a later day", () => { + expect(isAfterDay("2026-08-28", "2026-08-27")).toBe(true); + expect(isAfterDay("2026-08-27", "2026-08-27")).toBe(false); + // A rolled-over task sits before the viewed day and must be kept. + expect(isAfterDay("2026-08-24", "2026-08-27")).toBe(false); + }); + + it("compares across month and year boundaries", () => { + expect(isAfterDay("2026-09-01", "2026-08-31")).toBe(true); + expect(isAfterDay("2027-01-01", "2026-12-31")).toBe(true); + expect(isAfterDay("2026-08-31", "2026-09-01")).toBe(false); + }); +}); + +describe("nextMonday", () => { + it("returns the following Monday, never the same day", () => { + // 2026-08-10 is a Monday. + expect(nextMonday("2026-08-10")).toBe("2026-08-17"); + expect(nextMonday("2026-08-11")).toBe("2026-08-17"); // Tuesday + expect(nextMonday("2026-08-15")).toBe("2026-08-17"); // Saturday + expect(nextMonday("2026-08-16")).toBe("2026-08-17"); // Sunday + }); +}); diff --git a/extensions/sunsama/src/lib/date.ts b/extensions/sunsama/src/lib/date.ts new file mode 100644 index 00000000000..5a6e045b424 --- /dev/null +++ b/extensions/sunsama/src/lib/date.ts @@ -0,0 +1,34 @@ +/** Format a Date as a local YYYY-MM-DD string (no UTC shift). */ +export function toDayString(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +/** Today in the user's local timezone as YYYY-MM-DD. */ +export function todayString(): string { + return toDayString(new Date()); +} + +/** The YYYY-MM-DD string `n` days after the given day. */ +export function addDays(day: string, n: number): string { + // No trailing Z — parsed as local midnight, so DST shifts don't move the date. + const d = new Date(`${day}T00:00:00`); + d.setDate(d.getDate() + n); + return toDayString(d); +} + +/** + * Whether `day` falls after `reference`. Both are YYYY-MM-DD, which is + * zero-padded and big-endian, so comparing the strings compares the dates. + */ +export function isAfterDay(day: string, reference: string): boolean { + return day > reference; +} + +/** The first Monday strictly after the given day (YYYY-MM-DD). */ +export function nextMonday(day: string): string { + const d = new Date(`${day}T00:00:00`); + return addDays(day, (8 - d.getDay()) % 7 || 7); +} diff --git a/extensions/sunsama/src/lib/errors.ts b/extensions/sunsama/src/lib/errors.ts new file mode 100644 index 00000000000..b684fb68c76 --- /dev/null +++ b/extensions/sunsama/src/lib/errors.ts @@ -0,0 +1,74 @@ +import { Toast, showToast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; + +/** Raised when Sunsama sign-in is required (no tokens, or refresh failed). */ +export class AuthRequiredError extends Error { + constructor() { + super("Sunsama sign-in required. Run the command again to sign in."); + this.name = "AuthRequiredError"; + } +} + +/** Raised when Sunsama rate-limits requests (HTTP 429). */ +export class RateLimitError extends Error { + constructor() { + super( + "Sunsama is rate-limiting requests. Wait a few minutes and try again.", + ); + this.name = "RateLimitError"; + } +} + +/** + * Surface an error as a toast. Known errors get tailored messaging; everything + * else shows the underlying message verbatim so server errors are not hidden. + */ +export async function reportError( + error: unknown, + title = "Something went wrong", +): Promise<void> { + if (error instanceof AuthRequiredError) { + await showToast({ + style: Toast.Style.Failure, + title: "Sign in to Sunsama", + message: "Authorization expired or missing — run the command again.", + }); + return; + } + + if (error instanceof RateLimitError) { + await showToast({ + style: Toast.Style.Failure, + title: "Rate limited by Sunsama", + message: "Too many requests. Wait a few minutes and try again.", + }); + return; + } + + await showFailureToast(error, { title }); +} + +/** + * Run an action behind a progress toast: animated while it runs, success when + * it lands, and the shared error reporting when it throws. Returns whether the + * action succeeded, so callers can refresh or navigate only on success. + */ +export async function runWithToast( + labels: { pending: string; success: string; failure: string }, + action: () => Promise<unknown>, +): Promise<boolean> { + const toast = await showToast({ + style: Toast.Style.Animated, + title: labels.pending, + }); + try { + await action(); + toast.style = Toast.Style.Success; + toast.title = labels.success; + return true; + } catch (error) { + await toast.hide(); + await reportError(error, labels.failure); + return false; + } +} diff --git a/extensions/sunsama/src/lib/mcp.ts b/extensions/sunsama/src/lib/mcp.ts new file mode 100644 index 00000000000..ef1718b46aa --- /dev/null +++ b/extensions/sunsama/src/lib/mcp.ts @@ -0,0 +1,290 @@ +/** + * MCP transport + OAuth for the official Sunsama MCP server. + * + * Auth pattern (the one that works in Raycast): the MCP SDK's auth machinery + * drives discovery, Dynamic Client Registration, PKCE, resource binding, and + * token refresh via an OAuthClientProvider; only the browser hop is delegated + * to Raycast's OAuth.PKCEClient (`authorize({ url })`). Raycast requires a + * `state` param in the authorize URL, hence `state()`. + */ +import { LocalStorage, OAuth } from "@raycast/api"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; +import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; +import type { + OAuthClientInformationMixed, + OAuthClientMetadata, + OAuthTokens, +} from "@modelcontextprotocol/sdk/shared/auth.js"; +import { AuthRequiredError, RateLimitError } from "./errors"; + +const MCP_URL = "https://api.sunsama.com/mcp"; +const REDIRECT_URL = "https://raycast.com/redirect?packageName=Extension"; +const CLIENT_INFO_KEY = "sunsama-mcp-client-info"; +const CODE_VERIFIER_KEY = "sunsama-mcp-code-verifier"; + +const oauthClient = new OAuth.PKCEClient({ + redirectMethod: OAuth.RedirectMethod.Web, + providerName: "Sunsama", + providerIcon: "extension-icon.png", + providerId: "sunsama", + description: "Connect your Sunsama account to Raycast.", +}); + +class RaycastOAuthProvider implements OAuthClientProvider { + /** Resolves with the authorization code once the user finishes the browser hop. */ + private authCode: Promise<string> | null = null; + /** + * One interactive sign-in per command run. Without this the SDK re-enters + * authorization on the retry connect and the user gets a second browser hop + * for a flow they already completed. + */ + private prompted = false; + + get redirectUrl(): string { + return REDIRECT_URL; + } + + get clientMetadata(): OAuthClientMetadata { + return { + client_name: "Raycast Sunsama", + redirect_uris: [REDIRECT_URL], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + token_endpoint_auth_method: "none", + scope: "read execute offline_access", + }; + } + + state(): string { + // Raycast rejects authorize URLs without a state param. + return crypto.randomUUID(); + } + + async clientInformation(): Promise<OAuthClientInformationMixed | undefined> { + const raw = await LocalStorage.getItem<string>(CLIENT_INFO_KEY); + return raw ? (JSON.parse(raw) as OAuthClientInformationMixed) : undefined; + } + + async saveClientInformation( + info: OAuthClientInformationMixed, + ): Promise<void> { + await LocalStorage.setItem(CLIENT_INFO_KEY, JSON.stringify(info)); + } + + async tokens(): Promise<OAuthTokens | undefined> { + const set = await oauthClient.getTokens(); + if (!set?.accessToken) return undefined; + return { + access_token: set.accessToken, + token_type: "Bearer", + refresh_token: set.refreshToken, + scope: set.scope, + }; + } + + async saveTokens(tokens: OAuthTokens): Promise<void> { + await oauthClient.setTokens({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + scope: tokens.scope, + }); + } + + async redirectToAuthorization(authorizationUrl: URL): Promise<void> { + if (this.prompted) throw new AuthRequiredError(); + this.prompted = true; + + // Rebuild the SDK's authorize URL as a native Raycast authorization + // request — passing a pre-built URL to `authorize({ url })` fails with + // "No OAuth session found", because Raycast only registers a session for + // requests it created itself. Raycast generates its own PKCE pair and + // state; carry over every non-standard param the SDK added (notably the + // `resource` binding, which the token exchange depends on). + const params = authorizationUrl.searchParams; + const standard = new Set([ + "response_type", + "client_id", + "redirect_uri", + "scope", + "state", + "code_challenge", + "code_challenge_method", + ]); + const extraParameters: Record<string, string> = {}; + for (const [key, value] of params) { + if (!standard.has(key)) extraParameters[key] = value; + } + const request = await oauthClient.authorizationRequest({ + endpoint: `${authorizationUrl.origin}${authorizationUrl.pathname}`, + clientId: params.get("client_id") ?? "", + scope: params.get("scope") ?? "read execute offline_access", + extraParameters, + }); + // The token exchange must use Raycast's verifier, not the one the SDK + // generated for the URL we discarded. + await this.saveCodeVerifier(request.codeVerifier); + // Kick off the browser hop. The code is consumed via takeAuthCode() after + // the transport surfaces UnauthorizedError; the rejection is handled there, + // so swallow it here only to avoid an unhandled rejection warning. + this.authCode = oauthClient + .authorize(request) + .then((r) => r.authorizationCode); + await this.authCode.catch(() => undefined); + } + + takeAuthCode(): Promise<string> | null { + const code = this.authCode; + this.authCode = null; + return code; + } + + async saveCodeVerifier(codeVerifier: string): Promise<void> { + await LocalStorage.setItem(CODE_VERIFIER_KEY, codeVerifier); + } + + async codeVerifier(): Promise<string> { + const raw = await LocalStorage.getItem<string>(CODE_VERIFIER_KEY); + if (!raw) throw new AuthRequiredError(); + return raw; + } + + async invalidateCredentials( + scope: "all" | "client" | "tokens" | "verifier", + ): Promise<void> { + if (scope === "all" || scope === "tokens") await oauthClient.removeTokens(); + if (scope === "all" || scope === "client") + await LocalStorage.removeItem(CLIENT_INFO_KEY); + if (scope === "all" || scope === "verifier") + await LocalStorage.removeItem(CODE_VERIFIER_KEY); + } +} + +let clientPromise: Promise<Client> | null = null; + +async function connect(): Promise<Client> { + const provider = new RaycastOAuthProvider(); + // First attempt uses stored tokens (refreshing if needed); on Unauthorized + // the SDK has already sent the user through the browser via the provider, so + // finish the code exchange and connect once more. + for (let attempt = 0; ; attempt++) { + const client = new Client({ name: "raycast-sunsama", version: "1.0.0" }); + const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), { + authProvider: provider, + }); + try { + await client.connect(transport); + return client; + } catch (error) { + const pending = provider.takeAuthCode(); + if (error instanceof UnauthorizedError && pending && attempt === 0) { + // The user just completed (or dismissed) the browser hop. A dismissal + // rejects `pending`, which means sign-in didn't happen — surface that + // as an auth error rather than the raw Raycast rejection. + let code: string; + try { + code = await pending; + } catch { + throw new AuthRequiredError(); + } + await transport.finishAuth(code); + continue; + } + if (error instanceof UnauthorizedError) throw new AuthRequiredError(); + throw error; + } + } +} + +async function getClient(): Promise<Client> { + if (!clientPromise) { + clientPromise = connect().catch((error) => { + clientPromise = null; // don't cache a failed connection + throw error; + }); + } + return clientPromise; +} + +function mapError(error: unknown): never { + const text = error instanceof Error ? error.message : String(error); + if (error instanceof UnauthorizedError || /401|unauthorized/i.test(text)) { + throw new AuthRequiredError(); + } + if (/429|too many requests/i.test(text)) throw new RateLimitError(); + throw error; +} + +/** + * Parse a server payload as JSON. The MCP server answers in JSON, but an + * unexpected plain-text reply would otherwise surface as a bare SyntaxError + * with no hint of where it came from. + */ +function parseJson<T>(text: string, source: string): T { + try { + return JSON.parse(text) as T; + } catch { + const preview = text.trim().slice(0, 200); + throw new Error( + preview + ? `Unexpected response from Sunsama (${source}): ${preview}` + : `Empty response from Sunsama (${source})`, + ); + } +} + +/** Call an MCP tool; returns the text content (usually JSON) of the result. */ +export async function callTool( + name: string, + args: Record<string, unknown> = {}, +): Promise<string> { + const client = await getClient(); + let result; + try { + result = await client.callTool({ name, arguments: args }); + } catch (error) { + mapError(error); + } + const content = Array.isArray(result.content) + ? (result.content as Array<{ type: string; text?: string }>) + : []; + const text = content + .filter((c) => c.type === "text") + .map((c) => c.text ?? "") + .join("\n"); + // A tool-level failure comes back as a normal result with isError set, not + // as a thrown transport error — so it has to be checked explicitly. + if (result.isError) throw new Error(text || `${name} failed`); + return text; +} + +/** Call an MCP tool and parse its JSON result. */ +export async function callToolJson<T>( + name: string, + args: Record<string, unknown> = {}, +): Promise<T> { + return parseJson<T>(await callTool(name, args), name); +} + +/** Read an MCP resource and parse its JSON content. */ +export async function readResourceJson<T>(uri: string): Promise<T> { + const client = await getClient(); + let result; + try { + result = await client.readResource({ uri }); + } catch (error) { + mapError(error); + } + const text = (result.contents ?? []) + .map((c) => ("text" in c ? String(c.text) : "")) + .join("\n"); + return parseJson<T>(text, uri); +} + +/** Drop all stored credentials (used when auth is irrecoverably broken). */ +export async function signOut(): Promise<void> { + clientPromise = null; + await new RaycastOAuthProvider().invalidateCredentials("all"); +} diff --git a/extensions/sunsama/src/lib/notes.test.ts b/extensions/sunsama/src/lib/notes.test.ts new file mode 100644 index 00000000000..4cc09cd69e2 --- /dev/null +++ b/extensions/sunsama/src/lib/notes.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { htmlToMarkdown } from "./notes"; + +describe("htmlToMarkdown", () => { + it("returns empty for empty/placeholder notes", () => { + expect(htmlToMarkdown("")).toBe(""); + expect(htmlToMarkdown(undefined)).toBe(""); + expect(htmlToMarkdown("<p></p>")).toBe(""); + }); + + it("converts paragraphs and headings", () => { + expect(htmlToMarkdown("<h3>Title</h3><p>Body text</p>")).toBe( + "### Title\n\nBody text", + ); + }); + + it("converts inline marks and links", () => { + expect( + htmlToMarkdown( + '<p><strong>bold</strong> and <a href="https://x.com">x</a></p>', + ), + ).toBe("**bold** and [x](https://x.com)"); + }); + + it("converts checkbox task lists (Sunsama taskItem markup)", () => { + const html = + '<ul data-type="taskList">' + + '<li data-type="taskItem" class="editor-todo-item"><label><input type="checkbox"></label><div><p>Brush Teeth</p></div></li>' + + '<li data-type="taskItem"><label><input type="checkbox" checked></label><div><p>Done thing</p></div></li>' + + "</ul>"; + expect(htmlToMarkdown(html)).toBe("- [ ] Brush Teeth\n- [x] Done thing"); + }); + + it("converts bullet and ordered lists", () => { + expect(htmlToMarkdown("<ul><li>a</li><li>b</li></ul>")).toBe("- a\n- b"); + expect(htmlToMarkdown("<ol><li>a</li><li>b</li></ol>")).toBe("1. a\n1. b"); + }); + + it("decodes entities and strips unknown tags", () => { + expect(htmlToMarkdown("<p><span>a & b</span></p>")).toBe("a & b"); + }); +}); diff --git a/extensions/sunsama/src/lib/notes.ts b/extensions/sunsama/src/lib/notes.ts new file mode 100644 index 00000000000..17c4052c420 --- /dev/null +++ b/extensions/sunsama/src/lib/notes.ts @@ -0,0 +1,80 @@ +/** + * Convert Sunsama's task-notes HTML (a TipTap document) to Markdown so notes + * can be edited in a plain text area and written back via edit_task_notes + * (which accepts Markdown). Covers what the Sunsama editor emits: paragraphs, + * headings, bold/italic/code, links, bullet/ordered lists, and checkbox + * ("taskItem") lists. Unknown tags are stripped, entities decoded. + */ + +function decodeEntities(s: string): string { + return s + .replace(/ /g, " ") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&"); +} + +/** Strip any remaining tags from an inline fragment. */ +function stripTags(s: string): string { + return s.replace(/<[^>]+>/g, ""); +} + +export function htmlToMarkdown(html: string | undefined | null): string { + if (!html) return ""; + let s = html; + + // Inline marks first, while their tags are still present. + s = s.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, "**$2**"); + s = s.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, "*$2*"); + s = s.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`"); + s = s.replace( + /<a\b[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, + (_m, href, text) => `[${stripTags(text).trim()}](${href})`, + ); + + // Checkbox task items (before generic list items). + s = s.replace( + /<li[^>]*data-type="taskItem"[^>]*>([\s\S]*?)<\/li>/gi, + (_m, inner: string) => { + const checked = /<input[^>]*\bchecked\b/i.test(inner); + return `- [${checked ? "x" : " "}] ${stripTags(inner).trim()}\n`; + }, + ); + + // Ordered list items become "1." (Markdown renumbers), bullets become "-". + s = s.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_m, inner: string) => + inner.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m2, li: string) => { + return `1. ${stripTags(li).trim()}\n`; + }), + ); + s = s.replace( + /<li[^>]*>([\s\S]*?)<\/li>/gi, + (_m, li: string) => `- ${stripTags(li).trim()}\n`, + ); + + // Block elements. + s = s.replace( + /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, + (_m, level: string, text: string) => + `${"#".repeat(Number(level))} ${stripTags(text).trim()}\n\n`, + ); + s = s.replace(/<br\s*\/?>/gi, "\n"); + s = s.replace( + /<p[^>]*>([\s\S]*?)<\/p>/gi, + (_m, text: string) => `${text}\n\n`, + ); + + // Drop whatever tags remain (ul wrappers, spans, labels, inputs, divs, …). + s = stripTags(s); + s = decodeEntities(s); + + // Tidy whitespace: no trailing spaces, max one blank line, trimmed ends. + return s + .split("\n") + .map((line) => line.trimEnd()) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} diff --git a/extensions/sunsama/src/lib/open-integration.ts b/extensions/sunsama/src/lib/open-integration.ts new file mode 100644 index 00000000000..c5553033e6b --- /dev/null +++ b/extensions/sunsama/src/lib/open-integration.ts @@ -0,0 +1,55 @@ +import { getApplications, open } from "@raycast/api"; + +// Cache the promise (not the resolved value) so concurrent calls share one +// getApplications() lookup instead of racing to populate it. +let appNamesPromise: Promise<Set<string>> | null = null; +async function isAppInstalled(name: string): Promise<boolean> { + if (!appNamesPromise) { + appNamesPromise = getApplications().then( + (apps) => new Set(apps.map((a) => a.name.toLowerCase())), + ); + } + return (await appNamesPromise).has(name.toLowerCase()); +} + +// Not unit-tested: this module imports @raycast/api, which can't be loaded +// outside Raycast. Verified by hand against the app, bare, slugged, and +// query-string URL forms. +/** The Todoist task id from a task URL (last path segment, minus any slug). */ +function todoistTaskId(url: string): string | null { + const match = url.match(/todoist\.com\/(?:app\/)?task\/([^/?#]+)/i); + if (!match) return null; + const segment = match[1]; + // URLs may carry a slug prefix ("buy-milk-6gwW6RxjHwp8hqxr"). + return segment.split("-").pop() ?? null; +} + +/** + * Open an integration link, preferring the native desktop app at the exact item. + * + * - Trello desktop registers the `trello://` scheme — swap the protocol so it + * deep-links to the specific card instead of just launching the app. + * - Todoist desktop registers `todoist://task?id=` — deep-link to the task. + * - Slack/Linear/etc.: open the `https` URL with the default handler. When the + * desktop app is installed it claims its own URLs and navigates to the exact + * item; forcing the app via `open(url, app)` only lands on its home screen. + * (Slack's `slack://` scheme has no message-level form and needs a team ID the + * permalink doesn't carry, so the https permalink is the best we can do.) + */ +export async function openIntegration( + url: string, + service?: string, +): Promise<void> { + if (service === "trello" && (await isAppInstalled("Trello"))) { + await open(url.replace(/^https?:\/\//i, "trello://")); + return; + } + if (service === "todoist" && (await isAppInstalled("Todoist"))) { + const id = todoistTaskId(url); + if (id) { + await open(`todoist://task?id=${id}`); + return; + } + } + await open(url); +} diff --git a/extensions/sunsama/src/lib/shortcuts.ts b/extensions/sunsama/src/lib/shortcuts.ts new file mode 100644 index 00000000000..539b9ff92cb --- /dev/null +++ b/extensions/sunsama/src/lib/shortcuts.ts @@ -0,0 +1,16 @@ +import { Keyboard } from "@raycast/api"; + +/** + * A cross-platform shortcut: `cmd` on macOS, `ctrl` on Windows. Raycast does NOT + * auto-map `cmd` to `ctrl` on Windows, so cmd-only shortcuts silently do nothing + * there — these must be specified per platform. + */ +export function xShortcut( + key: Keyboard.KeyEquivalent, + ...extra: Keyboard.KeyModifier[] +): Keyboard.Shortcut { + return { + macOS: { modifiers: ["cmd", ...extra], key }, + Windows: { modifiers: ["ctrl", ...extra], key }, + }; +} diff --git a/extensions/sunsama/src/lib/sunsama-client.ts b/extensions/sunsama/src/lib/sunsama-client.ts new file mode 100644 index 00000000000..d9742329650 --- /dev/null +++ b/extensions/sunsama/src/lib/sunsama-client.ts @@ -0,0 +1,610 @@ +/** + * Sunsama operations over the official MCP server (tools + resources). + * Auth/transport live in `mcp.ts`; this module maps MCP JSON to UI types. + */ +import { LocalStorage } from "@raycast/api"; +import { callTool, callToolJson, readResourceJson } from "./mcp"; +import { parseDuration } from "./time"; +import { htmlToMarkdown } from "./notes"; +import { Channel, CreateTaskInput, Subtask, SubtaskInput, Task } from "./types"; +import { isAfterDay, todayString } from "./date"; + +// --------------------------------------------------------------------------- +// MCP wire shapes +// --------------------------------------------------------------------------- + +interface McpSubtask { + _id: string; + title: string; + completed: boolean; + timeEstimate?: string; // human string, e.g. "1 hours and 30 minutes" +} + +interface McpTask { + _id: string; + title: string; + notes?: string; // HTML + completed: boolean; + timeEstimate?: string; // human string + sortOrder?: number; + /** The day the task is scheduled to, YYYY-MM-DD. */ + scheduledDate?: string; + channel?: string; + subtasks?: McpSubtask[]; + integrationDetails?: { service?: string; url?: string }; + actualTimeSpent?: { total?: string }; + projectedTimeEntries?: Array<{ startTime?: string; startDate?: string }>; + /** Present on calendar-imported tasks; false = anchored to another day. */ + isScheduledOnPanelDate?: boolean; +} + +interface ActiveTimer { + taskId?: string; + subtaskId?: string; + /** ISO start of the running session, when the server exposes one. */ + start?: string; +} + +// --------------------------------------------------------------------------- +// Channels +// --------------------------------------------------------------------------- + +async function searchChannelsRaw( + searchText: string, + extra: Record<string, unknown> = {}, +): Promise<Channel[]> { + const data = await callToolJson<{ channels?: Channel[] }>("search_channels", { + searchText, + numResults: 25, + ...extra, + }); + return data.channels ?? []; +} + +const LETTERS = "abcdefghijklmnopqrstuvwxyz".split(""); + +/** Run tasks a few at a time so a full sweep doesn't fire dozens at once. */ +async function inBatches<T>( + tasks: Array<() => Promise<T[]>>, + size = 6, +): Promise<T[][]> { + const out: T[][] = []; + for (let i = 0; i < tasks.length; i += size) { + // A single failed lookup shouldn't empty the picker. + const batch = tasks.slice(i, i + size).map((run) => run().catch(() => [])); + out.push(...(await Promise.all(batch))); + } + return out; +} + +/** + * Every channel and category, sorted by name. + * + * There is no list-all endpoint — `search_channels` is the only way in, it + * ranks semantically, and it caps every answer at 25. No single query can + * return a longer list, so this sweeps one query per letter: each returns a + * different 25-item window, and merging them covers the whole set. Categories + * are swept separately, and included in the result — Sunsama lets a task be + * assigned straight to one ("Work", "My Stuff"), which is verified behaviour. + */ +async function sweepAllChannels(): Promise<Channel[]> { + const categories = await searchChannelsRaw("category", { isCategory: true }); + + // Never send `isCategory: false` — pairing it with categoryStreamId makes + // the server return an empty set for a query that otherwise matches fine. + const results = await inBatches([ + // Unscoped, so each window can span every category. + ...LETTERS.map((letter) => () => searchChannelsRaw(letter)), + // Scoped as well, so a large category still gets windows of its own + // rather than competing with the rest for the same 25 slots. + ...categories.flatMap((c) => + LETTERS.filter((_, i) => i % 3 === 0).map( + (letter) => () => searchChannelsRaw(letter, { categoryStreamId: c.id }), + ), + ), + ]); + + const byId = new Map<string, Channel>(); + for (const channel of [...categories, ...results.flat()]) { + byId.set(channel.id, channel); + } + return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name)); +} + +const CHANNELS_KEY = "sunsama-channels"; + +/** + * The channel list. + * + * Building it costs dozens of requests, and channels rarely change, so the + * sweep runs once and the result is stored. Every later read comes from + * storage until `refreshChannels` is called. + */ +export async function loadChannels(): Promise<Channel[]> { + const raw = await LocalStorage.getItem<string>(CHANNELS_KEY); + if (raw) { + try { + const stored = JSON.parse(raw) as Channel[]; + if (stored.length > 0) return stored; + } catch { + // Unreadable — sweep again below. + } + } + return refreshChannels(); +} + +/** Re-sweep the server and replace the stored list. */ +export async function refreshChannels(): Promise<Channel[]> { + const channels = await sweepAllChannels(); + await LocalStorage.setItem(CHANNELS_KEY, JSON.stringify(channels)); + return channels; +} + +/** Drop the stored list, so the next read sweeps again. */ +export async function forgetChannels(): Promise<void> { + await LocalStorage.removeItem(CHANNELS_KEY); +} + +/** + * Ask the server about a specific query. + * + * The sweep is best-effort by nature: the search ranks semantically and only + * ever returns 25, so a channel can fail to appear in any of the windows it + * builds from. Searching what the user actually typed is the reliable way to + * reach one — an exact name ranks first — so the pickers call this as you type + * and merge whatever comes back. + */ +export async function searchChannels(query: string): Promise<Channel[]> { + const text = query.trim(); + if (text.length < 2) return []; + return searchChannelsRaw(text); +} + +/** Add newly-found channels to the stored list, so they stay available. */ +export async function rememberChannels(found: Channel[]): Promise<void> { + if (found.length === 0) return; + const raw = await LocalStorage.getItem<string>(CHANNELS_KEY); + if (!raw) return; // Nothing swept yet; the first sweep will cover it. + + let stored: Channel[]; + try { + stored = JSON.parse(raw) as Channel[]; + } catch { + return; + } + + const byId = new Map(stored.map((c) => [c.id, c])); + const before = byId.size; + for (const channel of found) byId.set(channel.id, channel); + if (byId.size === before) return; // Nothing new — leave storage alone. + + const merged = [...byId.values()].sort((a, b) => + a.name.localeCompare(b.name), + ); + await LocalStorage.setItem(CHANNELS_KEY, JSON.stringify(merged)); +} + +const DEFAULT_CHANNEL_KEY = "sunsama-default-channel"; + +export interface DefaultChannel { + id: string; + name: string; +} + +/** + * The channel new tasks default to, chosen via the Set Default Channel command. + * + * This lives in LocalStorage rather than an extension preference because + * Raycast preference dropdowns are declared statically in the manifest and + * can't be populated from the channel list at runtime. + */ +export async function getDefaultChannel(): Promise<DefaultChannel | null> { + const raw = await LocalStorage.getItem<string>(DEFAULT_CHANNEL_KEY); + if (!raw) return null; + try { + return JSON.parse(raw) as DefaultChannel; + } catch { + return null; + } +} + +export async function setDefaultChannel( + channel: DefaultChannel | null, +): Promise<void> { + if (channel) + await LocalStorage.setItem(DEFAULT_CHANNEL_KEY, JSON.stringify(channel)); + else await LocalStorage.removeItem(DEFAULT_CHANNEL_KEY); +} + +const LAST_CHANNEL_KEY = "sunsama-last-channel"; + +/** Record the channel a task was just created in, with when it happened. */ +export async function rememberLastChannel(name: string): Promise<void> { + if (!name) return; + await LocalStorage.setItem( + LAST_CHANNEL_KEY, + JSON.stringify({ name, at: Date.now() }), + ); +} + +/** + * The channel last used to create a task, if that was within `withinMinutes`. + * A window of 0 disables it entirely. + */ +export async function getRecentChannel( + withinMinutes: number, +): Promise<string | null> { + if (withinMinutes <= 0) return null; + const raw = await LocalStorage.getItem<string>(LAST_CHANNEL_KEY); + if (!raw) return null; + try { + const { name, at } = JSON.parse(raw) as { name: string; at: number }; + const freshFor = withinMinutes * 60_000; + return name && Date.now() - at < freshFor ? name : null; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// Projection +// --------------------------------------------------------------------------- + +function minutes(human: string | undefined): number | undefined { + const parsed = human ? parseDuration(human) : null; + return parsed && parsed > 0 ? parsed : undefined; +} + +/** + * Extract the running timer from the active-timer resource. Observed shape: + * + * {"hasActiveTimer": true, "activeTimer": { + * "taskId": "...", "taskTitle": "...", "startTime": "<ISO>", + * "subtaskId": "...", "subtaskTitle": "...", + * "theSubtaskNotTheTaskIsBeingTimed": true }} + * + * `subtaskId` is present only while a subtask is the thing being timed. + */ +function normalizeActiveTimer(raw: unknown): ActiveTimer | null { + if (!raw || typeof raw !== "object") return null; + const r = raw as Record<string, unknown>; + const str = (k: string) => + typeof r[k] === "string" ? (r[k] as string) : undefined; + const taskId = str("taskId"); + if (!taskId) return null; + const startTime = str("startTime"); + return { + taskId, + subtaskId: str("subtaskId"), + start: + startTime && Number.isFinite(Date.parse(startTime)) + ? startTime + : undefined, + }; +} + +/** + * The running timer, if any. Fetched separately from the day's tasks so the + * list never waits on it: it only decorates rows, and the day resource is by + * far the slower of the two calls. + */ +export async function getActiveTimer(): Promise<ActiveTimer | null> { + const data = await readResourceJson<{ activeTimer?: unknown }>( + "sunsama://active-timer", + ); + return normalizeActiveTimer(data.activeTimer); +} + +/** + * Fold the running timer into already-projected tasks. Kept out of the fetch + * so the two requests can land independently. + */ +export function withActiveTimer(tasks: Task[], timer: ActiveTimer | null) { + if (!timer) return tasks; + return tasks.map((task) => { + if (timer.taskId !== task.id) return task; + // Only tick when the server actually reports a session start. Substituting + // "now" would restart the counter on every refetch and could double-count + // against a tracked total that already includes the running session. + return { + ...task, + isRunning: true, + timerStart: timer.start, + ownTimerRunning: !timer.subtaskId, + subtasks: task.subtasks.map((s) => + s.id === timer.subtaskId + ? { ...s, isRunning: true, timerStart: timer.start } + : s, + ), + }; + }); +} + +function projectTask(t: McpTask): Task { + const totalSeconds = + (parseDuration(t.actualTimeSpent?.total ?? "") ?? 0) * 60; + + // Earliest calendar slot start, shown as Sunsama formats it (e.g. "9:30 AM"). + const startTime = (t.projectedTimeEntries ?? []) + .filter((e) => e.startTime) + .sort( + (a, b) => Date.parse(a.startDate ?? "") - Date.parse(b.startDate ?? ""), + )[0]?.startTime; + + // Timer state is folded in later by `withActiveTimer`, once that separate + // request lands. + const subtasks: Subtask[] = (t.subtasks ?? []).map((s) => ({ + id: s._id, + title: s.title, + completed: s.completed, + timeEstimate: minutes(s.timeEstimate), + isRunning: false, + })); + + return { + id: t._id, + title: t.title, + notes: htmlToMarkdown(t.notes) || undefined, + completed: t.completed, + timeEstimate: minutes(t.timeEstimate), + channelName: t.channel || undefined, + integrationUrl: t.integrationDetails?.url, + integrationService: t.integrationDetails?.service, + subtasks, + isRunning: false, + trackedSeconds: totalSeconds, + ownTimerRunning: false, + startTime, + }; +} + +// --------------------------------------------------------------------------- +// Tasks +// --------------------------------------------------------------------------- + +export interface DayTasks { + /** The tasks to display, in the day's order. */ + tasks: Task[]; + /** + * Every task id on the day in order, including the ones filtered out of + * `tasks`. Reordering has to send the complete set or the omitted tasks get + * relocated — see `reorderDay`. + */ + allIds: string[]; +} + +export async function getTasksForDay(day: string): Promise<DayTasks> { + const data = await readResourceJson<{ tasks?: McpTask[] }>( + `sunsama://tasks/${day}`, + ); + const ordered = (data.tasks ?? []) + .slice() + .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)); + + return { + // Two kinds of task come back that don't belong on this day, and both are + // hidden rather than dropped — they stay in `allIds` so reordering doesn't + // relocate them: + // - Calendar imports anchored elsewhere (the server rolls incomplete past + // events forward, but Sunsama keeps an event on its own day). + // - Tasks already moved to a later day. The day resource keeps returning + // those, so without this a task snoozed to tomorrow stays on today. + // Earlier days are kept on purpose: that's a rolled-over task. + tasks: ordered + .filter((t) => t.isScheduledOnPanelDate !== false) + .filter((t) => !(t.scheduledDate && isAfterDay(t.scheduledDate, day))) + .map(projectTask), + allIds: ordered.map((t) => t._id), + }; +} + +/** Fetch a single task fresh (used by the subtasks view to stay in sync). */ +export async function getTask(taskId: string): Promise<Task | null> { + const data = await callToolJson<{ task?: McpTask } & McpTask>( + "get_task_by_id", + { taskId }, + ); + const t = data.task ?? (data._id ? data : undefined); + return t ? projectTask(t) : null; +} + +/** Creates the task and returns its final title, which the server sets from a + * linked item when no title was given. */ +export async function createTask(input: CreateTaskInput): Promise<string> { + const args: Record<string, unknown> = { + day: input.day, + position: input.position ?? "top", + }; + // With a URL and no explicit title, Sunsama titles the task from the item. + if (input.title?.trim()) args.title = input.title.trim(); + if (input.url) args.integrationUrl = input.url; + if (input.notes) args.notes = input.notes; + if (input.channel) args.channel = input.channel; + if (typeof input.timeEstimate === "number") + args.timeEstimate = input.timeEstimate; + if (input.subtasks?.length) + args.subtasks = input.subtasks.map((s) => ({ title: s.title })); + + const text = await callTool("create_task", args); + // Best-effort: pull the created title out of the JSON reply for the HUD. + try { + const parsed = JSON.parse(text) as { + task?: { title?: string }; + title?: string; + }; + const title = parsed.task?.title ?? parsed.title; + if (title) return title; + } catch { + // non-JSON reply — fall through + } + return input.title?.trim() || input.url || "New task"; +} + +export async function completeTask(taskId: string): Promise<void> { + await callTool("mark_task_as_completed", { + taskId, + finishedDay: todayString(), + }); +} + +export async function deleteTask(taskId: string): Promise<void> { + await callTool("delete_task", { taskId }); +} + +/** Move a task to another day (YYYY-MM-DD), or to the backlog when null. */ +export async function rescheduleTask( + taskId: string, + day: string | null, +): Promise<void> { + if (day) await callTool("move_task_to_day", { taskId, calendarDay: day }); + else await callTool("move_task_to_backlog", { taskId }); +} + +/** + * Apply a day's order. + * + * `taskIds` must list **every** task on the day, not just the visible ones. + * The server rewrites the whole day's sort orders from this list: ids that are + * passed are laid out in the given order, and any task left out is pushed after + * them. Sending a partial list therefore relocates the tasks it omits — a + * one-id call was observed moving an unrelated task from first to last. + */ +export async function reorderDay( + day: string, + taskIds: string[], +): Promise<void> { + await callTool("reorder_tasks", { calendarDay: day, taskIds }); +} + +// --------------------------------------------------------------------------- +// Edits +// --------------------------------------------------------------------------- + +export async function editTitle(taskId: string, title: string): Promise<void> { + await callTool("edit_task_title", { taskId, title }); +} + +/** Edit a task's notes/description as Markdown (replaces the whole body). */ +export async function editNotes( + taskId: string, + markdown: string, +): Promise<void> { + await callTool("edit_task_notes", { taskId, notes: markdown }); +} + +/** Set planned time in minutes for a task, or one of its subtasks. */ +export async function setPlannedTime( + taskId: string, + minutes: number, + subtaskId?: string, +): Promise<void> { + const args: Record<string, unknown> = { taskId, timeEstimate: minutes }; + if (subtaskId) args.subtaskId = subtaskId; + await callTool("edit_task_time_estimate", args); +} + +/** + * The requests that set a task's own planned time, in order. Sunsama derives + * the task total from its subtasks whenever any of them carry an estimate, and + * rejects a task-level estimate in that case, so those are cleared first. + * + * Returned as separate steps rather than run together: each is its own request + * that persists on its own, and callers need to know how many landed if a + * later one fails. + */ +export function plannedTimeSteps( + taskId: string, + minutes: number, + subtaskIdsToClear: string[] = [], +): PlannedTimeStep[] { + return [ + ...subtaskIdsToClear.map((id) => ({ + run: () => setPlannedTime(taskId, 0, id), + clears: id, + })), + { run: () => setPlannedTime(taskId, minutes) }, + ]; +} + +/** + * One request in a planned-time update. `clears` names the subtask this step + * clears, so a caller tracking what has been applied can say which ones + * actually went through rather than inferring it from position. + */ +export interface PlannedTimeStep { + run: () => Promise<void>; + clears?: string; +} + +/** Subtask ids that carry their own planned time (these block a task-level estimate). */ +export function subtasksWithPlannedTime(task: Task): string[] { + return task.subtasks + .filter((s) => (s.timeEstimate ?? 0) > 0) + .map((s) => s.id); +} + +/** Assign the task to a channel by name (closest match wins). */ +export async function setChannel( + taskId: string, + channelName: string, +): Promise<void> { + await callTool("add_task_to_channel", { taskId, channel: channelName }); +} + +// --------------------------------------------------------------------------- +// Timers +// --------------------------------------------------------------------------- + +export async function startTimer( + taskId: string, + subtaskId?: string, +): Promise<void> { + const args: Record<string, unknown> = { taskId }; + if (subtaskId) args.subtaskId = subtaskId; + await callTool("start_task_timer", args); +} + +export async function stopTimer( + taskId: string, + subtaskId?: string, +): Promise<void> { + const args: Record<string, unknown> = { taskId }; + if (subtaskId) args.subtaskId = subtaskId; + await callTool("stop_task_timer", args); +} + +// --------------------------------------------------------------------------- +// Subtasks +// --------------------------------------------------------------------------- + +export async function addSubtasks( + taskId: string, + subtasks: SubtaskInput[], +): Promise<void> { + await callTool("add_subtasks_to_task", { + taskId, + subtasks: subtasks.map((s) => ({ title: s.title })), + }); +} + +export async function editSubtaskTitle( + taskId: string, + subtaskId: string, + title: string, +): Promise<void> { + await callTool("edit_subtask_title", { taskId, subtaskId, newTitle: title }); +} + +export async function completeSubtask( + taskId: string, + subtaskId: string, +): Promise<void> { + await callTool("mark_subtask_as_completed", { taskId, subtaskId }); +} + +export async function uncompleteSubtask( + taskId: string, + subtaskId: string, +): Promise<void> { + await callTool("mark_subtask_as_incomplete", { taskId, subtaskId }); +} diff --git a/extensions/sunsama/src/lib/time.test.ts b/extensions/sunsama/src/lib/time.test.ts new file mode 100644 index 00000000000..0c33cb3aa23 --- /dev/null +++ b/extensions/sunsama/src/lib/time.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { + formatDuration, + formatElapsed, + parseDuration, + parseSubtasks, +} from "./time"; + +describe("parseDuration", () => { + it("parses a bare number as minutes", () => { + expect(parseDuration("90")).toBe(90); + }); + it("parses h:mm", () => { + expect(parseDuration("1:15")).toBe(75); + expect(parseDuration("0:45")).toBe(45); + }); + it("parses hour units", () => { + expect(parseDuration("1hr")).toBe(60); + expect(parseDuration("2 hours")).toBe(120); + expect(parseDuration("1.5h")).toBe(90); + }); + it("parses minute units", () => { + expect(parseDuration("30m")).toBe(30); + expect(parseDuration("45 minutes")).toBe(45); + }); + it("parses combined hours and minutes", () => { + expect(parseDuration("1h 30m")).toBe(90); + expect(parseDuration("1hr30min")).toBe(90); + }); + it("parses Sunsama MCP duration strings", () => { + expect(parseDuration("1 hours and 55 minutes")).toBe(115); + expect(parseDuration("12 hours and 7 minutes")).toBe(727); + expect(parseDuration("0 minutes")).toBe(0); + expect(parseDuration("2 hours")).toBe(120); + }); + it("returns null for empty or unrecognized input", () => { + expect(parseDuration("")).toBeNull(); + expect(parseDuration("soon")).toBeNull(); + }); +}); + +describe("formatDuration", () => { + it("formats minutes, hours, and both", () => { + expect(formatDuration(45)).toBe("45m"); + expect(formatDuration(60)).toBe("1h"); + expect(formatDuration(90)).toBe("1h 30m"); + }); +}); + +describe("formatElapsed", () => { + it("always shows hours", () => { + expect(formatElapsed(0)).toBe("0:00:00"); + expect(formatElapsed(65)).toBe("0:01:05"); + expect(formatElapsed(3661)).toBe("1:01:01"); + }); + + it("clamps negatives to zero", () => { + expect(formatElapsed(-10)).toBe("0:00:00"); + }); +}); + +describe("parseSubtasks", () => { + it("takes one subtask per non-empty line, trimmed", () => { + expect(parseSubtasks(" one \n\n two \n")).toEqual([ + { title: "one" }, + { title: "two" }, + ]); + }); + + it("returns nothing for blank input", () => { + expect(parseSubtasks(" \n\n")).toEqual([]); + }); +}); diff --git a/extensions/sunsama/src/lib/time.ts b/extensions/sunsama/src/lib/time.ts new file mode 100644 index 00000000000..2428a9fb49c --- /dev/null +++ b/extensions/sunsama/src/lib/time.ts @@ -0,0 +1,61 @@ +/** + * Pure parsing and formatting helpers. Kept free of any Raycast / MCP imports + * so they are trivially unit-testable. + */ +import { SubtaskInput } from "./types"; + +/** + * Parse a human duration into minutes. Accepts: + * - a plain number → minutes ("90" → 90) + * - h:mm clock form ("1:15" → 75) + * - unit forms, combinable ("1h", "1hr", "30m", "30 min", "1h 30m", "1.5h") + * - Sunsama's MCP strings ("1 hours and 55 minutes", "0 minutes") + * Returns null when the input can't be understood (empty or unrecognized). + */ +export function parseDuration(input: string): number | null { + const s = input.trim().toLowerCase(); + if (!s) return null; + + // h:mm (e.g. 1:15 → 75) + const clock = s.match(/^(\d+):([0-5]?\d)$/); + if (clock) return Number(clock[1]) * 60 + Number(clock[2]); + + // bare number → minutes + if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s)); + + // unit form: optional hours and/or minutes, in any order. The (?![a-z]) + // lookahead (instead of \b) lets concatenated units like "1hr30min" parse. + const hours = s.match(/(\d+(?:\.\d+)?)\s*(?:hours|hour|hrs|hr|h)(?![a-z])/); + const minutes = s.match( + /(\d+(?:\.\d+)?)\s*(?:minutes|minute|mins|min|m)(?![a-z])/, + ); + if (!hours && !minutes) return null; + + const total = + (hours ? Number(hours[1]) * 60 : 0) + (minutes ? Number(minutes[1]) : 0); + return Math.round(total); +} + +/** Format a minutes value as "45m", "1h", or "1h 30m". */ +export function formatDuration(minutes: number): string { + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + const rest = minutes % 60; + return rest ? `${hours}h ${rest}m` : `${hours}h`; +} + +/** Format elapsed seconds as "h:mm:ss". */ +export function formatElapsed(totalSeconds: number): string { + const s = Math.max(0, totalSeconds); + const pad = (n: number) => String(n).padStart(2, "0"); + return `${Math.floor(s / 3600)}:${pad(Math.floor((s % 3600) / 60))}:${pad(s % 60)}`; +} + +/** Split a textarea into one subtask per non-empty line. */ +export function parseSubtasks(raw: string): SubtaskInput[] { + return raw + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((title) => ({ title })); +} diff --git a/extensions/sunsama/src/lib/types.ts b/extensions/sunsama/src/lib/types.ts new file mode 100644 index 00000000000..a2ff476e25a --- /dev/null +++ b/extensions/sunsama/src/lib/types.ts @@ -0,0 +1,73 @@ +/** Shared domain types for the Sunsama extension (MCP backed). */ + +/** A subtask as entered in a form, before it is sent to Sunsama. */ +export interface SubtaskInput { + title: string; +} + +/** A channel from the MCP channel search. */ +export interface Channel { + id: string; + name: string; + isCategory?: boolean; + categoryName?: string | null; +} + +/** Arguments for creating a task. */ +export interface CreateTaskInput { + /** Optional when a URL is given — Sunsama titles the task from the linked item. */ + title?: string; + day: string; // YYYY-MM-DD + notes?: string; // Markdown + /** Channel name; the server assigns the closest match. */ + channel?: string; + timeEstimate?: number; // minutes + subtasks?: SubtaskInput[]; + /** A link to natively attach (Trello/GitHub/Todoist/ClickUp/… or any web page). */ + url?: string; + position?: "top" | "bottom"; +} + +/** A subtask in the UI. */ +export interface Subtask { + id: string; + title: string; + completed: boolean; + timeEstimate?: number; // minutes + /** Whether this subtask is the one the active timer is running on. */ + isRunning: boolean; + /** + * ISO start of the running session — only set when the server actually + * reports one, so the elapsed display is never invented. + */ + timerStart?: string; +} + +/** A task in the UI, projected from the MCP task JSON. */ +export interface Task { + id: string; + title: string; + /** Notes converted from Sunsama's HTML to Markdown. */ + notes?: string; + completed: boolean; + timeEstimate?: number; // minutes + channelName?: string; + /** Openable URL of the task's integration (Trello/GitHub/ClickUp/…), if any. */ + integrationUrl?: string; + /** The integration's service name (e.g. "trello", "github"), if any. */ + integrationService?: string; + subtasks: Subtask[]; + /** Whether a timer is running on this task or any of its subtasks. */ + isRunning: boolean; + /** + * ISO start of the running session, when the server reports one. Drives the + * live ticking display; absent means "running, but elapsed is unknown". + */ + timerStart?: string; + /** Seconds already tracked on the task (from Sunsama's reported total). */ + trackedSeconds: number; + /** Whether the task's *own* timer is running (drives the Start/Stop action). */ + ownTimerRunning: boolean; + /** Display start time of the task's earliest calendar slot, e.g. "9:30 AM". */ + startTime?: string; +} diff --git a/extensions/sunsama/src/lib/urls.test.ts b/extensions/sunsama/src/lib/urls.test.ts new file mode 100644 index 00000000000..52531450a3f --- /dev/null +++ b/extensions/sunsama/src/lib/urls.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { taskWebUrl, workspaceSlug } from "./urls"; + +describe("workspaceSlug", () => { + it("reads the slug out of a full task URL", () => { + expect( + workspaceSlug("https://app.sunsama.com/group/devin_green?taid=abc123"), + ).toBe("devin_green"); + }); + + it("reads it out of a workspace URL, with or without a trailing slash", () => { + expect(workspaceSlug("https://app.sunsama.com/group/devin_green")).toBe( + "devin_green", + ); + expect(workspaceSlug("https://app.sunsama.com/group/devin_green/")).toBe( + "devin_green", + ); + }); + + it("accepts a bare slug", () => { + expect(workspaceSlug("devin_green")).toBe("devin_green"); + expect(workspaceSlug(" devin_green ")).toBe("devin_green"); + }); + + it("returns null when there is nothing usable", () => { + expect(workspaceSlug("")).toBeNull(); + expect(workspaceSlug(undefined)).toBeNull(); + expect(workspaceSlug(" ")).toBeNull(); + // A URL that isn't a workspace link has no slug to take. + expect(workspaceSlug("https://app.sunsama.com/")).toBeNull(); + }); +}); + +describe("taskWebUrl", () => { + it("points at the task in its workspace", () => { + expect(taskWebUrl("devin_green", "6a9197ae879635000195c73f")).toBe( + "https://app.sunsama.com/group/devin_green?taid=6a9197ae879635000195c73f", + ); + }); +}); diff --git a/extensions/sunsama/src/lib/urls.ts b/extensions/sunsama/src/lib/urls.ts new file mode 100644 index 00000000000..d8fee0f1017 --- /dev/null +++ b/extensions/sunsama/src/lib/urls.ts @@ -0,0 +1,29 @@ +/** + * Sunsama web links. Pure, so they're unit-testable — no Raycast or MCP + * imports here. + */ + +/** + * The workspace slug from whatever the user pasted: a full task URL, the + * workspace URL, or the bare slug. Returns null when there's nothing usable. + */ +export function workspaceSlug(input: string | undefined): string | null { + const trimmed = (input ?? "").trim(); + if (!trimmed) return null; + + // Prefer the slug out of a URL, so pasting a full task link works. + const fromUrl = trimmed.match(/\/group\/([^/?#\s]+)/); + const slug = fromUrl ? fromUrl[1] : trimmed.replace(/^\/+|\/+$/g, ""); + + // Anything with a slash, space, or protocol left in it isn't a slug. + return /^[\w.-]+$/.test(slug) ? slug : null; +} + +/** + * The Sunsama web link for a task. Sunsama scopes tasks to a workspace, and + * the MCP server doesn't expose the slug, so it has to come from the setting — + * callers resolve it with `workspaceSlug` first and skip the link without one. + */ +export function taskWebUrl(slug: string, taskId: string): string { + return `https://app.sunsama.com/group/${slug}?taid=${encodeURIComponent(taskId)}`; +} diff --git a/extensions/sunsama/src/set-default-channel.tsx b/extensions/sunsama/src/set-default-channel.tsx new file mode 100644 index 00000000000..feb127eb08a --- /dev/null +++ b/extensions/sunsama/src/set-default-channel.tsx @@ -0,0 +1,97 @@ +import { + Action, + ActionPanel, + Color, + Icon, + List, + PopToRootType, + showHUD, +} from "@raycast/api"; +import { useCachedPromise } from "@raycast/utils"; +import { getDefaultChannel, setDefaultChannel } from "./lib/sunsama-client"; +import { + RefreshChannelsAction, + useChannels, +} from "./components/channel-dropdown"; +import { matchesChannel, matchesNoChannel } from "./lib/channels"; +import { reportError } from "./lib/errors"; + +export default function SetDefaultChannel() { + const channels = useChannels(); + const { list, isLoading, ready, query, onSearchTextChange } = channels; + // Filtered here, not by Raycast: handling the search text so the server can + // be asked about channels the stored list is missing turns Raycast's own + // filtering off. + const visible = list.filter((ch) => matchesChannel(ch, query)); + const { data: current } = useCachedPromise(getDefaultChannel, []); + + async function choose(id: string, name: string) { + try { + await setDefaultChannel(id ? { id, name } : null); + // Close and go back to root, rather than following the user's "Pop to + // Root Search" preference and leaving this list on the stack. + await showHUD( + id ? `Default channel: ${name}` : "Default channel cleared", + { + popToRootType: PopToRootType.Immediate, + clearRootSearch: true, + }, + ); + } catch (error) { + await reportError(error, "Failed to save default channel"); + } + } + + function itemActions(id: string, name: string) { + return ( + <ActionPanel> + <Action + title="Set as Default" + icon={Icon.Check} + onAction={() => choose(id, name)} + /> + <RefreshChannelsAction channels={channels} /> + </ActionPanel> + ); + } + + const selected = (id: string): List.Item.Accessory[] => + (current?.id ?? "") === id + ? [{ icon: { source: Icon.Checkmark, tintColor: Color.Green } }] + : []; + + return ( + <List + isLoading={isLoading} + searchBarPlaceholder={isLoading ? "Loading channels…" : "Search channels"} + // Off deliberately: the typed text has to reach the server to find a + // channel the stored list is missing, and handling it means filtering + // here instead — see `visible` above. + onSearchTextChange={onSearchTextChange} + filtering={false} + > + {/* Shown as soon as there is a list to sit on top of. useCachedPromise + returns the cached channels on the first render, so normally this + appears with them and nothing shifts; on a cold start the whole set + arrives at once instead of a lone "No channel" row. */} + {ready && matchesNoChannel(query) && ( + <List.Item + title="No channel" + subtitle="New tasks start without a channel" + accessories={selected("")} + actions={itemActions("", "No channel")} + /> + )} + {visible.map((ch) => ( + <List.Item + key={ch.id} + title={ch.name} + // Categories hold tasks of their own, so they're listed too. + subtitle={ch.isCategory ? "Category" : (ch.categoryName ?? undefined)} + accessories={selected(ch.id)} + actions={itemActions(ch.id, ch.name)} + /> + ))} + </List> + ); +} diff --git a/extensions/sunsama/src/view-today.tsx b/extensions/sunsama/src/view-today.tsx new file mode 100644 index 00000000000..56f5100da76 --- /dev/null +++ b/extensions/sunsama/src/view-today.tsx @@ -0,0 +1,548 @@ +import { + Action, + ActionPanel, + Alert, + confirmAlert, + Color, + getPreferenceValues, + Icon, + Keyboard, + List, +} from "@raycast/api"; +import { xShortcut } from "./lib/shortcuts"; +import { useCachedPromise } from "@raycast/utils"; +import { useEffect, useState } from "react"; +import { + completeTask, + DayTasks, + deleteTask, + forgetChannels, + getActiveTimer, + getTasksForDay, + withActiveTimer, + reorderDay, + rescheduleTask, + startTimer, + stopTimer, + subtasksWithPlannedTime, +} from "./lib/sunsama-client"; +import { signOut } from "./lib/mcp"; +import { addDays, nextMonday, todayString, toDayString } from "./lib/date"; +import { reportError, runWithToast } from "./lib/errors"; +import { formatDuration, formatElapsed } from "./lib/time"; +import { taskWebUrl, workspaceSlug } from "./lib/urls"; +import { Task } from "./lib/types"; +import { EditTaskForm } from "./components/edit-task-form"; +import { AddSubtasksForm } from "./components/add-subtasks-form"; +import { SubtasksList } from "./components/subtasks-list"; +import { SetTimeForm } from "./components/set-time-form"; +import { openIntegration } from "./lib/open-integration"; + +// A dimmer, semi-transparent grey for the "nothing left to do" sun. Raycast +// boosts low-contrast tints back up by default, which would undo the +// transparency, so adjustContrast is turned off. +const DONE_TINT = { + light: "rgba(0, 0, 0, 0.35)", + dark: "rgba(255, 255, 255, 0.3)", + adjustContrast: false, +}; + +/** Action label for a task's integration link, e.g. "Open in Trello". */ +function integrationLabel(service?: string): string { + const names: Record<string, string> = { + trello: "Trello", + github: "GitHub", + slack: "Slack", + gmail: "Gmail", + linear: "Linear", + clickup: "ClickUp", + todoist: "Todoist", + website: "Browser", + }; + const name = service ? names[service] : undefined; + return name ? `Open in ${name}` : "Open Link"; +} + +function accessories(task: Task, now: number): List.Item.Accessory[] { + const items: List.Item.Accessory[] = []; + if (task.channelName) { + items.push({ + tag: { value: task.channelName, color: Color.SecondaryText }, + tooltip: "Channel", + }); + } + if (task.startTime) { + items.push({ + icon: Icon.Calendar, + text: task.startTime, + tooltip: "Scheduled start", + }); + } + if (task.isRunning) { + // Tracked total plus the live session, when the server reports its start. + // Without a start we can only show the total — still green, to signal that + // the timer is running. + const current = task.timerStart + ? Math.floor((now - Date.parse(task.timerStart)) / 1000) + : 0; + items.push({ + tag: { + value: formatElapsed(task.trackedSeconds + current), + color: Color.Green, + }, + icon: { source: Icon.Stopwatch, tintColor: Color.Green }, + tooltip: "Timer running", + }); + } else if (task.trackedSeconds > 0) { + items.push({ + tag: { + value: formatElapsed(task.trackedSeconds), + color: Color.SecondaryText, + }, + icon: { source: Icon.Stopwatch, tintColor: Color.SecondaryText }, + }); + } + if (task.subtasks.length) { + const done = task.subtasks.filter((s) => s.completed).length; + items.push({ + icon: Icon.CheckCircle, + text: `${done}/${task.subtasks.length}`, + }); + } + if (typeof task.timeEstimate === "number" && task.timeEstimate > 0) { + items.push({ icon: Icon.Clock, text: formatDuration(task.timeEstimate) }); + } + return items; +} + +export default function ViewToday() { + const day = todayString(); + // useCachedPromise paints the last-seen tasks instantly on open, then + // revalidates in the background (top loading bar) — no waiting for a fetch. + const { data, isLoading, revalidate, mutate } = useCachedPromise( + getTasksForDay, + [day], + { onError: (error) => reportError(error, "Failed to load tasks") }, + ); + // Separate request so the list paints as soon as the tasks land; the timer + // only decorates rows. Failing to read it must not empty the list. + const { data: timer, revalidate: revalidateTimer } = useCachedPromise( + getActiveTimer, + [], + { onError: () => undefined }, + ); + + const { showCompleted, workspaceUrl } = + getPreferenceValues<Preferences.ViewToday>(); + const workspace = workspaceSlug(workspaceUrl); + // getTasksForDay already returns Sunsama's day order; just optionally hide + // completed tasks. `allIds` covers every task on the day — including ones + // hidden from the list — and every reorder has to send that whole set. + const ordered = withActiveTimer(data?.tasks ?? [], timer ?? null); + const allIds = data?.allIds ?? []; + const tasks = ordered.filter((t) => showCompleted || !t.completed); + + // Tick every second while any task has a running timer, so the elapsed + // indicator stays live without refetching. + const [searchText, setSearchText] = useState(""); + const [now, setNow] = useState(() => Date.now()); + // Only tick when there's a session start to count from; a running timer with + // no known start has nothing to animate. + const hasRunningTimer = tasks.some((t) => t.timerStart); + useEffect(() => { + if (!hasRunningTimer) return; + const id = setInterval(() => setNow(Date.now()), 1000); + return () => clearInterval(id); + }, [hasRunningTimer]); + + /** + * Run a mutation behind a toast. `optimistic` applies the change to the + * cached day straight away so the list reacts immediately instead of waiting + * on the refetch, which takes a couple of seconds. `mutate` rolls the change + * back automatically if the request fails, and revalidates once it lands. + */ + async function run( + labels: { pending: string; success: string; failure: string }, + action: () => Promise<unknown>, + optimistic?: (day: DayTasks) => DayTasks, + ) { + const ok = await runWithToast(labels, () => + mutate( + action(), + optimistic + ? { optimisticUpdate: (d) => (d ? optimistic(d) : d) } + : undefined, + ), + ); + if (ok) revalidateTimer(); + } + + /** Replace one task in the cached day. */ + const patchTask = + (id: string, change: (task: Task) => Task) => (d: DayTasks) => ({ + ...d, + tasks: d.tasks.map((t) => (t.id === id ? change(t) : t)), + }); + + /** Drop a task from the cached day entirely. */ + const dropTask = (id: string) => (d: DayTasks) => ({ + tasks: d.tasks.filter((t) => t.id !== id), + allIds: d.allIds.filter((taskId) => taskId !== id), + }); + + async function onComplete(task: Task) { + await run( + { + pending: "Completing…", + success: "Task completed", + failure: "Failed to complete task", + }, + () => completeTask(task.id), + patchTask(task.id, (t) => ({ ...t, completed: true })), + ); + } + + async function onStartTimer(task: Task) { + await run( + { + pending: "Starting timer…", + success: "Timer started", + failure: "Failed to start timer", + }, + () => startTimer(task.id), + ); + } + + async function onStopTimer(task: Task) { + // The running timer may be the task's own or one of its subtasks (e.g. + // started from the Sunsama web app). Stop whichever is actually running. + const runningSubtask = task.ownTimerRunning + ? undefined + : task.subtasks.find((s) => s.isRunning); + await run( + { + pending: "Stopping timer…", + success: "Timer stopped", + failure: "Failed to stop timer", + }, + () => stopTimer(task.id, runningSubtask?.id), + ); + } + + /** Apply a reordered id list for the day (ids in display order). */ + async function applyOrder(ids: string[], success: string) { + await run( + { pending: "Moving…", success, failure: "Failed to move task" }, + () => reorderDay(day, ids), + (d) => ({ + allIds: ids, + tasks: [...d.tasks].sort( + (a, b) => ids.indexOf(a.id) - ids.indexOf(b.id), + ), + }), + ); + } + + async function onMove(task: Task, direction: -1 | 1) { + // Take the adjacent visible task's slot, positioned within the full day + // order so hidden tasks keep their places. + const visibleIndex = tasks.findIndex((t) => t.id === task.id); + const neighbor = tasks[visibleIndex + direction]; + if (!neighbor) return; + + const ids = [...allIds]; + const from = ids.indexOf(task.id); + const to = ids.indexOf(neighbor.id); + if (from < 0 || to < 0) return; + ids.splice(from, 1); + ids.splice(to, 0, task.id); + await applyOrder(ids, "Task moved"); + } + + async function onMoveTo(task: Task, edge: "top" | "bottom") { + const ids = allIds.filter((id) => id !== task.id); + if (edge === "top") ids.unshift(task.id); + else ids.push(task.id); + await applyOrder(ids, edge === "top" ? "Moved to top" : "Moved to bottom"); + } + + /** Snooze a task to another day. `target` is YYYY-MM-DD. */ + async function onSnooze(task: Task, target: string, label: string) { + await run( + { + pending: "Snoozing…", + success: `Snoozed to ${label}`, + failure: "Failed to snooze task", + }, + () => rescheduleTask(task.id, target), + // It belongs to another day now, so it leaves this list. + dropTask(task.id), + ); + } + + /** Clear stored credentials so the next run re-runs the sign-in flow. */ + async function onSignOut() { + const confirmed = await confirmAlert({ + title: "Sign out of Sunsama?", + message: + "Stored credentials are removed. The next command run will ask you to sign in again.", + icon: { source: Icon.Logout, tintColor: Color.Red }, + primaryAction: { + title: "Sign Out", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!confirmed) return; + + await run( + { + pending: "Signing out…", + success: "Signed out", + failure: "Failed to sign out", + }, + // Drop the stored channels too, so signing in as someone else doesn't + // inherit this account's list. + async () => { + await signOut(); + await forgetChannels(); + }, + ); + } + + async function onDelete(task: Task) { + const confirmed = await confirmAlert({ + title: "Delete task?", + message: task.title, + icon: { source: Icon.Trash, tintColor: Color.Red }, + primaryAction: { title: "Delete", style: Alert.ActionStyle.Destructive }, + }); + if (!confirmed) return; + + await run( + { + pending: "Deleting…", + success: "Task deleted", + failure: "Failed to delete task", + }, + () => deleteTask(task.id), + dropTask(task.id), + ); + } + + const isSearching = searchText.trim().length > 0; + // Everything on the day is finished. With completed tasks hidden the list is + // simply empty and the empty view says so; with them shown, the items are + // still there, so the news goes in a section header above them. + const allComplete = ordered.length > 0 && ordered.every((t) => t.completed); + + const items = tasks.map((task) => ( + <List.Item + key={task.id} + icon={ + task.isRunning + ? { source: Icon.Stopwatch, tintColor: Color.Green } + : task.completed + ? { source: Icon.CheckCircle, tintColor: Color.Green } + : { source: Icon.Circle, tintColor: Color.SecondaryText } + } + title={task.title} + accessories={accessories(task, now)} + actions={ + <ActionPanel> + <ActionPanel.Section> + {task.isRunning ? ( + <Action + title="Stop Timer" + icon={Icon.Stop} + onAction={() => onStopTimer(task)} + /> + ) : ( + <Action + title="Start Timer" + icon={Icon.Stopwatch} + onAction={() => onStartTimer(task)} + /> + )} + {!task.completed && ( + <Action + title="Mark as Completed" + icon={Icon.Check} + shortcut={xShortcut("enter")} + onAction={() => onComplete(task)} + /> + )} + {task.subtasks.length > 0 && ( + <Action.Push + title="View Subtasks" + icon={Icon.List} + shortcut={xShortcut("u", "shift")} + target={<SubtasksList task={task} onChanged={revalidate} />} + /> + )} + <Action.Push + title="Add Subtasks" + icon={Icon.PlusCircle} + shortcut={Keyboard.Shortcut.Common.New} + target={<AddSubtasksForm task={task} onSaved={revalidate} />} + /> + <Action.Push + title="Edit Task" + icon={Icon.Pencil} + shortcut={Keyboard.Shortcut.Common.Edit} + target={ + <EditTaskForm task={task} day={day} onSaved={revalidate} /> + } + /> + {/* Only shown when the workspace is known — the link can't be + built without it, and a Sunsama extension pointing at Sunsama's + home page isn't worth an action. */} + {workspace && ( + <Action.OpenInBrowser + title="Open in Sunsama" + icon={Icon.CloudSun} + url={taskWebUrl(workspace, task.id)} + shortcut={Keyboard.Shortcut.Common.Open} + /> + )} + {task.integrationUrl && ( + <Action + title={integrationLabel(task.integrationService)} + icon={Icon.Link} + // The task's own page takes Open; the linked item in another + // service is the "open with" of the pair. + shortcut={Keyboard.Shortcut.Common.OpenWith} + onAction={() => + openIntegration( + task.integrationUrl as string, + task.integrationService, + ) + } + /> + )} + </ActionPanel.Section> + <ActionPanel.Section> + <ActionPanel.Submenu + title="Snooze…" + icon={Icon.Moon} + shortcut={xShortcut("s", "shift")} + > + <Action + title="Tomorrow" + icon={Icon.Sun} + onAction={() => onSnooze(task, addDays(day, 1), "tomorrow")} + /> + <Action + title="Next Week" + icon={Icon.Calendar} + onAction={() => onSnooze(task, nextMonday(day), "next week")} + /> + <Action.PickDate + title="Pick a Day…" + icon={Icon.Calendar} + type={Action.PickDate.Type.Date} + min={new Date()} + onChange={(date) => { + if (!date) return; + const target = toDayString(date); + onSnooze(task, target, target); + }} + /> + </ActionPanel.Submenu> + <Action + title="Move up" + icon={Icon.ArrowUp} + shortcut={Keyboard.Shortcut.Common.MoveUp} + onAction={() => onMove(task, -1)} + /> + <Action + title="Move Down" + icon={Icon.ArrowDown} + shortcut={Keyboard.Shortcut.Common.MoveDown} + onAction={() => onMove(task, 1)} + /> + <Action + title="Move to Top" + icon={Icon.ArrowUpCircle} + shortcut={xShortcut("arrowUp", "opt", "shift")} + onAction={() => onMoveTo(task, "top")} + /> + <Action + title="Move to Bottom" + icon={Icon.ArrowDownCircle} + shortcut={xShortcut("arrowDown", "opt", "shift")} + onAction={() => onMoveTo(task, "bottom")} + /> + </ActionPanel.Section> + <ActionPanel.Section> + <Action.Push + title="Set Planned Time" + icon={Icon.Clock} + shortcut={xShortcut("p", "shift")} + target={ + <SetTimeForm + taskId={task.id} + title={task.title} + currentMinutes={task.timeEstimate ?? 0} + clearSubtaskIds={subtasksWithPlannedTime(task)} + onSaved={revalidate} + /> + } + /> + <Action + title="Delete Task" + icon={Icon.Trash} + style={Action.Style.Destructive} + shortcut={Keyboard.Shortcut.Common.Remove} + onAction={() => onDelete(task)} + /> + </ActionPanel.Section> + <ActionPanel.Section> + <Action + title="Refresh" + icon={Icon.ArrowClockwise} + shortcut={Keyboard.Shortcut.Common.Refresh} + onAction={revalidate} + /> + <Action + title="Sign out of Sunsama" + icon={Icon.Logout} + onAction={onSignOut} + /> + </ActionPanel.Section> + </ActionPanel> + } + /> + )); + + return ( + <List + isLoading={isLoading || data === undefined} + searchBarPlaceholder="Filter today's tasks" + onSearchTextChange={setSearchText} + filtering + > + {isSearching ? ( + <List.EmptyView + icon={Icon.Sun} + title="No tasks found" + description="Maybe it's hiding in another day, or the backlog." + /> + ) : ( + <List.EmptyView + icon={{ source: Icon.Sun, tintColor: DONE_TINT }} + title="Done for the day" + /> + )} + {allComplete ? ( + <List.Section + title="Done for the day" + subtitle={`${ordered.length} completed`} + > + {items} + </List.Section> + ) : ( + items + )} + </List> + ); +} diff --git a/extensions/sunsama/tsconfig.json b/extensions/sunsama/tsconfig.json new file mode 100644 index 00000000000..d33dd46c481 --- /dev/null +++ b/extensions/sunsama/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*", "raycast-env.d.ts"], + "compilerOptions": { + "lib": ["ES2023"], + "module": "commonjs", + "target": "ES2023", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true + } +}