diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7791ee9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,48 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "discord/**"
+ pull_request:
+ branches: [main]
+ paths:
+ - "discord/**"
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ defaults:
+ run:
+ working-directory: discord
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: 24
+
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Typecheck
+ run: pnpm typecheck
+
+ - name: Lint
+ run: pnpm lint
+
+ - name: Test
+ run: pnpm test
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 0df1655..e41918c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -1,24 +1,12 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
+name: CodeQL
on:
push:
- branches: [ main ]
+ branches: [main]
pull_request:
- # The branches below must be a subset of the branches above
- branches: [ main ]
+ branches: [main]
schedule:
- - cron: '28 0 * * 0'
+ - cron: "28 0 * * 0"
jobs:
analyze:
@@ -32,39 +20,19 @@ jobs:
strategy:
fail-fast: false
matrix:
- language: [ 'python' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Learn more about CodeQL language support at https://git.io/codeql-language-support
+ language: ['javascript-typescript']
steps:
- - name: Checkout repository
- uses: actions/checkout@v3
+ - name: Checkout repository
+ uses: actions/checkout@v7
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v1
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v1
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v4
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/container-images.yml b/.github/workflows/container-images.yml
index 0d8827c..aff6be3 100644
--- a/.github/workflows/container-images.yml
+++ b/.github/workflows/container-images.yml
@@ -7,7 +7,6 @@ on:
paths:
- ".github/workflows/container-images.yml"
- "discord/**"
- - "tex/**"
- "README.md"
- "coderunbot.env.example"
pull_request:
@@ -16,7 +15,6 @@ on:
paths:
- ".github/workflows/container-images.yml"
- "discord/**"
- - "tex/**"
- "README.md"
- "coderunbot.env.example"
workflow_dispatch:
@@ -39,23 +37,19 @@ jobs:
- image_name: coderunbot-discord
context: ./discord
file: ./discord/Containerfile
- - image_name: coderunbot-tex
- context: ./tex
- file: ./tex/Dockerfile
-
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: ghcr.io
username: gaato
@@ -63,7 +57,7 @@ jobs:
- name: Extract image metadata
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ghcr.io/gaato/${{ matrix.image_name }}
tags: |
@@ -73,7 +67,7 @@ jobs:
type=sha,prefix=sha-
- name: Build and push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
diff --git a/LICENSE b/LICENSE
index 261eeb9..be3f7b2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,201 +1,661 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
index 656024c..bdff352 100644
--- a/README.md
+++ b/README.md
@@ -12,54 +12,68 @@ This is a bot for internal use that adds other functions to CodeRunBot. Also, th
## Links
-- [Privacy Policy](bots/config/privacy-policy.md)
+- [Privacy Policy](discord/config/privacy-policy.md)
- [Invite Link](https://discord.com/api/oauth2/authorize?client_id=761428259241328680&permissions=0&scope=bot)
- [Discord Server](https://discord.gg/qRpYRTgvXM)
## Requirements
-- Python >= 3.14
-- [tex.gaato.net](https://github.com/gaato/tex.gaato.net)
+- Node.js >= 24
+- [pnpm](https://pnpm.io/)
## How to run (CodeRunBot)
Set `CODERUNBOT_TOKEN` in the environment file.
-### with uv (recommended)
+### with pnpm
```bash
-$ cd discord
-$ uv sync --locked
-$ uv run python -m bots
+cd discord
+pnpm install
+pnpm build
+pnpm start
```
-### without uv
+### with Podman (or Docker)
```bash
-$ python -m venv .venv
-$ . .venv/bin/activate
-$ pip install aiohttp google-api-python-client google-auth-oauthlib iso639-lang openai "py-cord[voice]>=2.6" python-dotenv
-$ python -m bots
+podman build -t coderunbot discord
+podman run --rm --env-file coderunbot.env coderunbot
```
+Add `-v ./discord/data:/app/data` to persist the opt-out list across runs
+when using the default local state backend.
+
## How to run (gaato bot)
-Basically the same as CodeRunBot, but set the environment file to include `GAATO_BOT_TOKEN`, `GOOGLE_API_KEY` and `WOLFRAM_APPID` and the execution command is as follows. You also need ffmpeg.
+Basically the same as CodeRunBot, but set the environment file to include `GAATO_BOT_TOKEN` and `WOLFRAM_APPID`, then run:
```bash
-$ GAATO_BOT=1 uv run python -m bots
+cd discord
+GAATO_BOT=1 pnpm start
```
+With Podman, add `-e GAATO_BOT=1` to the `podman run` command instead.
+
## For developer
Pull requests are welcome.
-Please use [uv](https://docs.astral.sh/uv/) to manage dependencies.
-Do not rewrite `uv.lock` by hand.
+Please use [pnpm](https://pnpm.io/) to manage dependencies.
+Do not rewrite `pnpm-lock.yaml` by hand.
+TypeScript is pinned to the 6.x line because dependency-cruiser 18 does not
+support TypeScript 7 yet.
## Environment
For containerized or service-manager based deployment, keep secrets outside the repo and point the process at an environment file.
- Example file: [`coderunbot.env.example`](coderunbot.env.example)
-- `TEX_BASE_URL` defaults to `http://127.0.0.1:8080`
-- The TeX renderer can be started with `HOST` and `PORT` overrides
+
+## License
+
+Copyright (C) Gakuto Furuya
+
+This program is free software, licensed under the
+[GNU Affero General Public License v3.0 or later](LICENSE) (AGPL-3.0-or-later).
+If you run a modified version of this bot as a network service, the AGPL
+requires you to offer its source code to the users interacting with it.
diff --git a/coderunbot.env.example b/coderunbot.env.example
index c21b9f3..4c33527 100644
--- a/coderunbot.env.example
+++ b/coderunbot.env.example
@@ -9,8 +9,3 @@ GAATO_BOT_TOKEN=
# ---- Optional features ----
OPENAI_API_KEY=
WOLFRAM_APPID=
-# GOOGLE_API_KEY= # mentioned in README; may be unused depending on enabled cogs
-
-# ---- TeX renderer endpoint ----
-# TEX_BASE_URL defaults to http://127.0.0.1:8080 when unset
-TEX_BASE_URL=http://127.0.0.1:8080
diff --git a/compose.yaml b/compose.yaml
deleted file mode 100644
index 10b44f6..0000000
--- a/compose.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-version: '3.8'
-
-services:
- coderunbot:
- build: discord
- env_file: .env
- restart: unless-stopped
- gaato-bot:
- build: discord
- env_file: .env
- environment:
- - GAATO_BOT=1
- restart: unless-stopped
- tex:
- build: tex
- restart: unless-stopped
diff --git a/discord/.dependency-cruiser.cjs b/discord/.dependency-cruiser.cjs
new file mode 100644
index 0000000..e38a407
--- /dev/null
+++ b/discord/.dependency-cruiser.cjs
@@ -0,0 +1,32 @@
+/** @type {import('dependency-cruiser').IConfiguration} */
+module.exports = {
+ forbidden: [
+ {
+ name: "no-cross-feature-imports",
+ comment: "Features are isolated and may only depend on platform/shared code.",
+ severity: "error",
+ scope: "folder",
+ from: { path: "^src/features/([^/]+)$" },
+ to: {
+ path: "^src/features/[^/]+$",
+ pathNot: "^src/features/$1$",
+ },
+ },
+ {
+ name: "platform-shared-do-not-know-features",
+ severity: "error",
+ from: { path: "^src/(platform|shared)/" },
+ to: { path: "^src/features/" },
+ },
+ {
+ name: "config-does-not-know-features",
+ severity: "error",
+ from: { path: "^src/config\\.ts$" },
+ to: { path: "^src/features/" },
+ },
+ ],
+ options: {
+ doNotFollow: { path: "node_modules" },
+ tsConfig: { fileName: "tsconfig.json" },
+ },
+};
diff --git a/discord/.dockerignore b/discord/.dockerignore
index da0ab4e..ddc3c2a 100644
--- a/discord/.dockerignore
+++ b/discord/.dockerignore
@@ -1,6 +1,3 @@
.env
-.venv
-__pycache__/
-*.pyc
-*.pyo
-*.pyd
+node_modules/
+dist/
diff --git a/discord/.gitignore b/discord/.gitignore
new file mode 100644
index 0000000..b947077
--- /dev/null
+++ b/discord/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+dist/
diff --git a/discord/Containerfile b/discord/Containerfile
index 76ef6ac..0b3de2a 100644
--- a/discord/Containerfile
+++ b/discord/Containerfile
@@ -1,23 +1,36 @@
-FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim
+FROM node:24-slim AS build
WORKDIR /app
-ENV PYTHONDONTWRITEBYTECODE=1 \
- PYTHONUNBUFFERED=1 \
- PYTHONPATH=/app \
- UV_LINK_MODE=copy \
- UV_NO_DEV=1 \
- UV_PROJECT_ENVIRONMENT=/app/.venv
+RUN corepack enable
-RUN groupadd --gid 10001 app \
- && useradd --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin --no-create-home app
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN pnpm install --frozen-lockfile
-COPY pyproject.toml uv.lock /app/
-RUN uv sync --locked --no-install-project \
- && chown -R app:app /app
+COPY tsconfig.json ./
+COPY src ./src
+RUN pnpm build \
+ && pnpm prune --prod
-COPY --chown=app:app bots /app/bots
+FROM node:24-slim AS runtime
+
+WORKDIR /app
+
+ENV NODE_ENV=production
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends fonts-noto-cjk fonts-noto-cjk-extra \
+ && rm -rf /var/lib/apt/lists/* \
+ && groupadd --gid 10001 app \
+ && useradd --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin --no-create-home app \
+ && mkdir -p /app/data \
+ && chown app:app /app/data
+
+COPY --from=build --chown=app:app /app/node_modules ./node_modules
+COPY --from=build --chown=app:app /app/dist ./dist
+COPY --chown=app:app package.json ./package.json
+COPY --chown=app:app config ./config
USER app
-CMD ["/app/.venv/bin/python", "-m", "bots"]
+CMD ["node", "dist/index.js"]
diff --git a/discord/Dockerfile b/discord/Dockerfile
deleted file mode 100644
index 76ef6ac..0000000
--- a/discord/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim
-
-WORKDIR /app
-
-ENV PYTHONDONTWRITEBYTECODE=1 \
- PYTHONUNBUFFERED=1 \
- PYTHONPATH=/app \
- UV_LINK_MODE=copy \
- UV_NO_DEV=1 \
- UV_PROJECT_ENVIRONMENT=/app/.venv
-
-RUN groupadd --gid 10001 app \
- && useradd --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin --no-create-home app
-
-COPY pyproject.toml uv.lock /app/
-RUN uv sync --locked --no-install-project \
- && chown -R app:app /app
-
-COPY --chown=app:app bots /app/bots
-
-USER app
-
-CMD ["/app/.venv/bin/python", "-m", "bots"]
diff --git a/discord/biome.json b/discord/biome.json
new file mode 100644
index 0000000..e9ec6b8
--- /dev/null
+++ b/discord/biome.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json",
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space"
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "style": {
+ "noProcessEnv": "error"
+ }
+ }
+ },
+ "overrides": [
+ {
+ "includes": ["src/env.ts"],
+ "linter": {
+ "rules": {
+ "style": {
+ "noProcessEnv": "off"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/discord/bots/__init__.py b/discord/bots/__init__.py
deleted file mode 100644
index 1d33c33..0000000
--- a/discord/bots/__init__.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from collections import OrderedDict
-from typing import Any
-
-import discord
-
-SUPPORT_SERVER_LINK = 'discord.gg/qRpYRTgvXM'
-LOG_CHANNEL_ID = 1118867011448078417
-DEVELOPER_ID = 572432137035317249
-
-
-class DeleteButton(discord.ui.Button):
- def __init__(
- self,
- user: discord.abc.User,
- label='Delete',
- style=discord.ButtonStyle.danger,
- *args: Any,
- **kwargs: Any,
- ):
- self.user_id = user.id
- super().__init__(label=label, style=style, *args, **kwargs)
-
- async def callback(self, interaction: discord.Interaction) -> None:
- if interaction.user is None or interaction.message is None:
- return
- if self.user_id == interaction.user.id:
- await interaction.message.delete()
-
-
-class LimitedSizeDict(OrderedDict):
-
- def __init__(self, size_limit=None, *args: Any, **kwds: Any):
- self.size_limit = size_limit
- super().__init__(*args, **kwds)
- self._check_size_limit()
-
- def __setitem__(self, key: Any, value: Any) -> None:
- super().__setitem__(key, value)
- self._check_size_limit()
-
- def _check_size_limit(self) -> None:
- if self.size_limit is not None:
- while len(self) > self.size_limit:
- self.popitem(last=False)
diff --git a/discord/bots/__main__.py b/discord/bots/__main__.py
deleted file mode 100644
index 342be40..0000000
--- a/discord/bots/__main__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import os
-
-from bots.core.bot import Bot
-from dotenv import load_dotenv
-
-CODERUNBOT_COGS = ["bots.cogs.TeX", "bots.cogs.Code", "bots.cogs.Privacy"]
-GAATO_BOT_COGS = [
- "bots.cogs.TeX",
- "bots.cogs.Code",
- "bots.cogs.Privacy",
- "bots.cogs.Wolfram",
- "bots.cogs.Misc",
- "bots.cogs.Translate",
-]
-
-
-def main() -> None:
- load_dotenv(verbose=True)
-
- coderunbot_token = os.environ.get("CODERUNBOT_TOKEN")
- gaato_bot_token = os.environ.get("GAATO_BOT_TOKEN")
-
- if os.environ.get("GAATO_BOT"):
- Bot(gaato_bot_token, GAATO_BOT_COGS, ")").run()
- return
-
- Bot(coderunbot_token, CODERUNBOT_COGS, "]").run()
-
-
-if __name__ == "__main__":
- main()
diff --git a/discord/bots/cogs/Code.py b/discord/bots/cogs/Code.py
deleted file mode 100644
index 210761e..0000000
--- a/discord/bots/cogs/Code.py
+++ /dev/null
@@ -1,333 +0,0 @@
-import io
-import pathlib
-import re
-from typing import cast
-
-import aiohttp
-import discord
-import requests
-from discord.ext import commands
-from discord.interactions import Interaction
-
-from .. import DeleteButton, LimitedSizeDict
-
-URL = "https://wandbox.org/api/"
-BASE_DIR = pathlib.Path(__file__).parent.parent
-
-
-# dbname = BASE_DIR.parent / "db.sqlite3"
-# conn = sqlite3.connect(dbname, check_same_thread=False)
-# c = conn.cursor()
-# c.execute(
-# "CREATE TABLE IF NOT EXISTS code (message_id INTEGER, author_id INTEGER, language TEXT, code TEXT, stdin TEXT)"
-# )
-
-
-def get_autocomplete_languages() -> list[str]:
- with requests.get(URL + "list.json") as r:
- r.raise_for_status()
- result = r.json()
- language_names = set(
- map(lambda data: data["language"].lower().replace(" ", ""), result)
- )
- print(list(language_names))
- return list(language_names)
-
-
-autocomplete_languages = get_autocomplete_languages()
-
-
-def auto_complete_language(ctx: discord.AutocompleteContext) -> list[str]:
- return list(
- filter(
- lambda language_code: language_code.startswith(ctx.value.lower()),
- autocomplete_languages,
- )
- )
-
-
-async def get_languages() -> dict[str, str]:
- async with aiohttp.ClientSession() as session:
- async with session.get(URL + "list.json") as r:
- if r.status == 200:
- result = await r.json()
- language_names = set(map(lambda data: data["language"], result))
- languages_dict = {}
- for language_name in language_names:
- # Find all compilers for this language
- compilers = [
- item["name"]
- for item in result
- if item["language"] == language_name
- ]
- # Prefer stable versions: filter out HEAD, devel, or git versions
- stable_compilers = [
- c for c in compilers
- if "head" not in c.lower() and "devel" not in c.lower() and "git" not in c.lower()
- ]
- # Use stable version if available, otherwise use the first available
- selected = stable_compilers[0] if stable_compilers else compilers[0]
- languages_dict[language_name.lower().replace(" ", "")] = selected
- return languages_dict
-
-
-async def run_core(
- author: discord.abc.User,
- language: str,
- code: str,
- stdin: str = "",
-) -> tuple[discord.Embed, list[discord.File]]:
- language_dict = await get_languages()
- if language not in language_dict.keys():
- embed = discord.Embed(
- title="The following languages are supported",
- description=", ".join(language_dict.keys()),
- color=0xFF0000,
- )
- embed.set_author(name=author.name)
- return embed, []
- if language == "nim":
- compiler_option = (
- "--hint[Processing]:off\n"
- "--hint[Conf]:off\n"
- "--hint[Link]:off\n"
- "--hint[SuccessX]:off"
- )
- else:
- compiler_option = ""
- url = URL + "compile.json"
- params = {
- "compiler": language_dict[language],
- "code": code,
- "stdin": stdin,
- "compiler-option-raw": compiler_option,
- }
- async with aiohttp.ClientSession() as session:
- try:
- async with session.post(url, json=params) as r:
- if r.status == 200:
- try:
- # Some responses from Wandbox may omit the Content-Type header.
- # Allow parsing JSON even when content-type is missing by setting content_type=None.
- result = await r.json(content_type=None)
- except ValueError:
- # Not JSON — read text for debugging and return error embed.
- text = await r.text()
- embed = discord.Embed(
- title="Wandbox Error",
- description=(
- "Wandbox returned a non-JSON response. "
- "Check the service status or your request."
- ),
- color=0xFF0000,
- )
- max_preview = 750
- preview = text[:max_preview] + ("..." if len(text) > max_preview else "")
- embed.add_field(name="Response Preview", value=f"``\n{preview}\n```")
- embed.set_author(name=author.name)
- return embed, []
- else:
- embed = discord.Embed(
- title="Connection Error", description=f"{r.status}", color=0xFF0000
- )
- embed.set_author(name=author.name)
- return embed, []
- except aiohttp.ClientError as e:
- embed = discord.Embed(
- title="Connection Error",
- description=str(e),
- color=0xFF0000,
- )
- embed.set_author(name=author.name)
- return embed, []
- embed = discord.Embed(title=f"Result ({language_dict[language]}):")
- embed_color = 0xFF0000
- files = []
- for k, v in result.items():
- if k in ("program_message", "compiler_message"):
- continue
- if v == "":
- continue
- if k == "status" and v == "0":
- embed_color = 0x007000
- if language == "nim" and k == "compiler_error":
- v = re.sub(r"CC: \S+\n", "", v)
- if v == "":
- continue
- if len(v) > 1000 or len(v.split("\n")) > 100:
- files.append(discord.File(io.BytesIO(v.encode()), k + ".txt"))
- else:
- embed.add_field(
- name=k,
- value="```\n" + v + "\n```",
- )
- embed.color = embed_color
- embed.set_author(name=author.name)
- return embed, files
-
-
-# class EditButton(discord.ui.Button):
-# def __init__(self, label="Edit", style=discord.ButtonStyle.primary, **kwargs):
-# super().__init__(label=label, style=style, **kwargs)
-
-# async def callback(self, interaction: discord.Interaction):
-# c = conn.cursor()
-# c.execute("SELECT * FROM code WHERE message_id = ?", (interaction.message.id,))
-# result = c.fetchone()
-# if result is None:
-# embed = discord.Embed(
-# title="Error", description="Not found.", color=0xFF0000
-# )
-# await interaction.response.send_message(embed=embed, ephemeral=True)
-# return
-# await interaction.response.send_modal(
-# RunModal(result[2], code=result[3], stdin=result[4])
-# )
-
-
-# class ViewCodeButton(discord.ui.Button):
-# def __init__(
-# self, label="View Code", style=discord.ButtonStyle.secondary, **kwargs
-# ):
-# super().__init__(label=label, style=style, **kwargs)
-
-# async def callback(self, interaction: discord.Interaction):
-# c = conn.cursor()
-# c.execute("SELECT * FROM code WHERE message_id = ?", (interaction.message.id,))
-# result = c.fetchone()
-# if result is None:
-# embed = discord.Embed(
-# title="Error", description="Not found.", color=0xFF0000
-# )
-# await interaction.response.send_message(embed=embed, ephemeral=True)
-# return
-# embed = discord.Embed(
-# title=result[2],
-# description=f"```{result[2]}\n{result[3]}```",
-# color=0x007000,
-# )
-# if result[4] != "":
-# embed.add_field(
-# name="Standard Input",
-# value=f"```\n{result[4]}\n```",
-# )
-# embed.set_author(
-# name=interaction.guild.get_member(result[1]).name,
-# icon_url=interaction.guild.get_member(result[1]).display_avatar.url,
-# )
-# embed.set_footer(
-# text=f"Requested by {interaction.user.name}",
-# icon_url=interaction.user.display_avatar.url,
-# )
-# view = discord.ui.View(DeleteButton(interaction.user), timeout=None)
-# await interaction.response.send_message(embed=embed, view=view)
-
-
-class RunModal(discord.ui.Modal):
- def __init__(
- self, language: str, title="Run code", code="", stdin="", *args, **kwargs
- ):
- super().__init__(title=title, *args, **kwargs)
- self.language = (
- language.lower()
- .replace("pp", "++")
- .replace("sharp", "#")
- .replace("clisp", "lisp")
- )
- self.add_item(
- discord.ui.InputText(
- label="Code",
- placeholder="Write code here",
- style=discord.InputTextStyle.long,
- value=code,
- )
- )
- self.add_item(
- discord.ui.InputText(
- label="Standard Input",
- required=False,
- style=discord.InputTextStyle.long,
- value=stdin,
- )
- )
-
- async def callback(self, interaction: Interaction) -> None:
- author = interaction.user
- if author is None:
- return
- await interaction.response.defer(invisible=False)
- code_value = cast(str, self.children[0].value or "")
- stdin_value = cast(str, self.children[1].value or "")
- embed, files = await run_core(
- author,
- self.language,
- code_value,
- stdin_value,
- )
- embed.add_field(
- name="Code",
- value=f"```{self.language}\n{code_value}\n```",
- )
- view = discord.ui.View(DeleteButton(author), timeout=None)
- await interaction.followup.send(
- embed=embed, files=files or None, view=view, wait=True
- )
- # c.execute(
- # "INSERT INTO code VALUES (?, ?, ?, ?, ?)",
- # (
- # m.id,
- # interaction.user.id,
- # self.language,
- # self.children[0].value,
- # self.children[1].value,
- # ),
- # )
-
-
-class Code(commands.Cog):
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.user_message_id_to_bot_message = LimitedSizeDict(size_limit=100)
-
- @commands.Cog.listener()
- async def on_message_edit(self, before: discord.Message, after: discord.Message):
- if before.content != after.content:
- if before.id in self.user_message_id_to_bot_message:
- await self.user_message_id_to_bot_message[before.id].delete()
-
- @commands.command()
- async def run(self, ctx: commands.Context, language: str, *, code: str):
- """Run code"""
- code = re.sub(r"^```.*$", "", code, flags=re.MULTILINE)
- view = discord.ui.View(DeleteButton(ctx.author), timeout=None)
- embed, files = await run_core(ctx.author, language, code)
- m = await ctx.reply(embed=embed, files=files or None, view=view)
- self.user_message_id_to_bot_message[ctx.message.id] = m
-
- @discord.message_command()
- async def escape(self, ctx: discord.ApplicationContext, message: discord.Message):
- await ctx.respond(
- discord.utils.escape_markdown(
- discord.utils.escape_mentions(message.content)
- ),
- ephemeral=True,
- )
-
- @discord.slash_command(
- name="run",
- description="Run code",
- options=[
- discord.Option(
- name="language",
- description="Language",
- required=True,
- autocomplete=auto_complete_language,
- )
- ],
- )
- async def run_slash(self, ctx: discord.ApplicationContext, language: str):
- await ctx.send_modal(RunModal(language, title="Run code"))
-
-
-def setup(bot):
- return bot.add_cog(Code(bot))
diff --git a/discord/bots/cogs/Misc.py b/discord/bots/cogs/Misc.py
deleted file mode 100644
index 1e7e60c..0000000
--- a/discord/bots/cogs/Misc.py
+++ /dev/null
@@ -1,138 +0,0 @@
-import os
-import pathlib
-from collections import defaultdict
-from datetime import datetime, timedelta
-from typing import DefaultDict, cast
-
-import discord
-from discord.ext import commands
-from openai import AsyncOpenAI
-from openai.types.chat import ChatCompletionMessageParam
-
-
-# from sudachipy import tokenizer, dictionary
-
-
-
-BASE_DIR = pathlib.Path(__file__).parent.parent
-
-client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
-
-
-class Misc(commands.Cog):
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.mention_times: DefaultDict[int, list[datetime]] = defaultdict(list)
- # self.tokenizer_obj = dictionary.Dictionary().create()
-
- async def fetch_message_history(
- self,
- channel: discord.TextChannel | discord.Thread | discord.DMChannel,
- limit: int = 10,
- ) -> list[ChatCompletionMessageParam]:
- history = await channel.history(limit=limit).flatten()
- messages = []
- bot_user = self.bot.user
- for message in history:
- messages.append(
- cast(ChatCompletionMessageParam, {
- "role": "user" if message.author != bot_user else "assistant",
- "content": message.content,
- })
- )
- messages.reverse()
- return messages
-
- def is_mention_limit_exceeded(
- self, user_id: int, time_limit: int = 60, max_mentions: int = 3
- ) -> bool:
- now = datetime.now()
- mention_times = self.mention_times[user_id]
- # Remove mentions older than time_limit seconds
- self.mention_times[user_id] = [time for time in mention_times if now - time < timedelta(seconds=time_limit)]
- # Check if mention limit is exceeded
- print(self.mention_times)
- return len(self.mention_times[user_id]) >= max_mentions
-
- @commands.Cog.listener("on_message")
- async def on_mentioned(self, message: discord.Message) -> None:
- if message.author.bot:
- return
- bot_user = self.bot.user
- if bot_user is None:
- return
- if str(bot_user.id) in message.content:
- if self.is_mention_limit_exceeded(message.author.id):
- return
- self.mention_times[message.author.id].append(datetime.now())
- if not isinstance(
- message.channel, (discord.TextChannel, discord.Thread, discord.DMChannel)
- ):
- return
- async with message.channel.typing():
- history = await self.fetch_message_history(message.channel, limit=10)
- history.append(
- cast(ChatCompletionMessageParam, {
- "role": "assistant" if message.author.id == bot_user.id else "user",
- "content": message.content,
- })
- )
- gaato_messages: list[ChatCompletionMessageParam] = [
- cast(
- ChatCompletionMessageParam,
- {
- "role": "system",
- "content": "これはDiscordでのチャットです。"
- "以下の様々なユーザーによる直近のメッセージ履歴を参考に、"
- "あなたがメンションされている最後のメッセージに返信してください。",
- },
- ),
- *history,
- ]
- short_reply_messages: list[ChatCompletionMessageParam] = [
- cast(
- ChatCompletionMessageParam,
- {
- "role": "system",
- "content": "これはDiscordのチャットです。"
- "以下は直近のメッセージ履歴です。"
- "一言で返信してください。",
- },
- ),
- *history,
- ]
- if message.author.id == 572432137035317249: # gaato.
- response = await client.chat.completions.create(
- model= "gpt-4-turbo",
- messages=gaato_messages,
- )
- else:
- response = await client.chat.completions.create(
- model= "gpt-3.5-turbo",
- messages=short_reply_messages,
- )
- allowed_mentions = discord.AllowedMentions.none()
- allowed_mentions.replied_user = True
- await message.reply(
- response.choices[0].message.content or "",
- allowed_mentions=allowed_mentions,
- )
- await self.bot.process_commands(message)
-
- # @discord.slash_command(
- # name='sudachi',
- # description='形態素解析',
- # )
- # async def sudachi(self, ctx: discord.ApplicationContext, text: str):
- # """形態素解析"""
- # mode = tokenizer.Tokenizer.SplitMode.C
- # tokens = self.tokenizer_obj.tokenize(text, mode)
- # outputs = [f'{text}']
- # for t in tokens:
- # outputs.append(f'{t.surface()}\t{",".join(t.part_of_speech())}\t{t.reading_form()}\t{t.normalized_form()}')
- # view = discord.ui.View(DeleteButton(ctx.author), timeout=None)
- # await ctx.respond(f'```\n' + '\n'.join(outputs) + '\n```', view=view)
-
-
-def setup(bot: commands.Bot):
- return bot.add_cog(Misc(bot))
diff --git a/discord/bots/cogs/Privacy.py b/discord/bots/cogs/Privacy.py
deleted file mode 100644
index ff24090..0000000
--- a/discord/bots/cogs/Privacy.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import pathlib
-
-import discord
-from discord.commands import slash_command
-from discord.ext import commands
-
-from bots.core.bot import OPT_OUT_USERS
-
-BASE_DIR = pathlib.Path(__file__).parent.parent
-
-
-class Privacy(commands.Cog):
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @slash_command(name='privacy-policy')
- async def privacy_policy(self, ctx: discord.ApplicationContext):
- """Show the privacy policy"""
- with open(BASE_DIR / 'config' / 'privacy-policy.md', 'r') as f:
- await ctx.respond(f.read())
-
- @slash_command(name='opt-out')
- async def opt_out(self, ctx: discord.ApplicationContext):
- """Opt out of your message content data to be tracked"""
- if ctx.author.id in OPT_OUT_USERS:
- await ctx.respond('Your message content is already off-track. To use other commands, please use the /opt-in command.')
- else:
- OPT_OUT_USERS.add(ctx.author.id)
- await ctx.respond('This bot will not track your message content from now on. Most commands will no longer respond.')
-
- @slash_command(name='opt-in')
- async def opt_in(self, ctx: discord.ApplicationContext):
- """Opt out of your message content data to be tracked"""
- if ctx.author.id in OPT_OUT_USERS:
- OPT_OUT_USERS.remove(ctx.author.id)
- await ctx.respond('This bot will now track the content of your messages. It will only be used to provide commands. Use the /privacy-policy command to view the privacy policy.')
- else:
- await ctx.respond('This bot is already tracking your message content.')
-
-
-def setup(bot):
- return bot.add_cog(Privacy(bot))
diff --git a/discord/bots/cogs/TeX.py b/discord/bots/cogs/TeX.py
deleted file mode 100644
index 0da5346..0000000
--- a/discord/bots/cogs/TeX.py
+++ /dev/null
@@ -1,161 +0,0 @@
-import io
-import os
-import pathlib
-from typing import Optional, cast
-
-import aiohttp
-import discord
-from discord.ext import commands
-
-from .. import DeleteButton, LimitedSizeDict
-
-BASE_DIR = pathlib.Path(__file__).parent.parent
-
-
-async def respond_core(
- author: discord.abc.User, code: str, spoiler: bool
-) -> tuple[str, discord.Embed, Optional[discord.File]]:
- base_url = os.environ.get("TEX_BASE_URL", "http://127.0.0.1:8080").rstrip("/")
- url = f"{base_url}/render/png"
- params = {"latex": code}
- headers = {"Content-Type": "application/json"}
- async with aiohttp.ClientSession() as session:
- async with session.post(url, json=params, headers=headers) as r:
- if r.status != 200:
- error_message = await r.text()
- embed = discord.Embed(
- title="Rendering Error",
- description=f"```\n{error_message}\n```",
- color=0xFF0000,
- )
- embed.set_author(
- name=author.name,
- )
- return "", embed, None
-
- result = await r.read()
- file = discord.File(io.BytesIO(result), filename="tex.png", spoiler=spoiler)
- embed = discord.Embed(color=0x008000)
- embed.set_author(name=author.name)
- if not spoiler:
- embed.set_image(url="attachment://tex.png")
- if "\\\\" in code and "\\begin" not in code and "\\end" not in code:
- embed.add_field(
- name="Hint", value="You can use gather or align environment."
- )
- return "", embed, file
-
-
-class TeXModal(discord.ui.Modal):
- def __init__(
- self, spoiler, env=None, value="", title="LaTeX to Image", *arg, **kwargs
- ):
- self.spoiler = spoiler
- if env:
- value = f"\\begin{{{env}}}\n{value}\n\\end{{{env}}}"
- super().__init__(title=title, *arg, **kwargs)
- self.add_item(
- discord.ui.InputText(
- label="Code",
- placeholder="Input TeX code here",
- style=discord.InputTextStyle.long,
- value=value,
- )
- )
-
- async def callback(self, interaction: discord.Interaction) -> None:
- author = interaction.user
- if author is None:
- return
- await interaction.response.defer(invisible=False)
- code_value = cast(str, self.children[0].value or "")
- content, embed, file = await respond_core(
- author,
- code_value,
- self.spoiler,
- )
- embed.add_field(
- name="Code",
- value=f"```tex\n{code_value}\n```",
- )
- view = discord.ui.View(DeleteButton(author), timeout=None)
- if file is None:
- await interaction.followup.send(
- content=content, embed=embed, view=view, wait=True
- )
- else:
- await interaction.followup.send(
- content=content, embed=embed, file=file, view=view, wait=True
- )
-
-
-class TeX(commands.Cog):
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.user_message_id_to_bot_message = LimitedSizeDict(size_limit=100)
-
- @commands.Cog.listener()
- async def on_message_edit(self, before: discord.Message, after: discord.Message):
- if before.content != after.content:
- if before.id in self.user_message_id_to_bot_message:
- await self.user_message_id_to_bot_message[before.id].delete()
-
- async def respond(self, ctx: commands.Context, code: str, spoiler: bool):
- async with ctx.channel.typing():
- view = discord.ui.View(DeleteButton(ctx.author), timeout=None)
- code = code.replace("```tex", "").replace("```", "").strip()
- content, embed, file = await respond_core(ctx.author, code, spoiler)
- if file is None:
- m = await ctx.reply(content=content, embed=embed, view=view)
- else:
- m = await ctx.reply(content=content, embed=embed, file=file, view=view)
- return m
-
- @commands.command()
- async def tex(self, ctx: commands.Context, *, code: str):
- """LaTeX to image (in math mode)"""
- m = await self.respond(ctx, code, False)
- self.user_message_id_to_bot_message[ctx.message.id] = m
-
- @commands.command()
- async def stex(self, ctx: commands.Context, *, code: str):
- """LaTeX to spoiler image (in math mode)"""
- m = await self.respond(ctx, code, True)
- self.user_message_id_to_bot_message[ctx.message.id] = m
-
- @discord.slash_command(
- name="tex",
- description="TeX to image",
- options=[
- discord.Option(
- type=str,
- name="env",
- description="The environment to use",
- required=False,
- choices=[
- discord.OptionChoice(name="align", value="align"),
- discord.OptionChoice(name="gather", value="gather"),
- ],
- ),
- discord.Option(
- type=bool,
- name="spoiler",
- description="Whether to mark the image as a spoiler",
- required=False,
- default=False,
- ),
- ],
- )
- async def tex_slash(
- self,
- ctx: discord.ApplicationContext,
- env: Optional[str] = None,
- spoiler: bool = False,
- ):
- # modal = TeXModal(plain, spoiler)
- modal = TeXModal(spoiler, env)
- await ctx.send_modal(modal)
-
-
-def setup(bot):
- return bot.add_cog(TeX(bot))
diff --git a/discord/bots/cogs/Translate.py b/discord/bots/cogs/Translate.py
deleted file mode 100644
index 1d88c2d..0000000
--- a/discord/bots/cogs/Translate.py
+++ /dev/null
@@ -1,121 +0,0 @@
-import os
-
-import discord
-import dotenv
-import iso639
-from discord.ext import commands
-from iso639.exceptions import InvalidLanguageValue
-from openai import AsyncOpenAI
-
-from .. import DeleteButton
-
-dotenv.load_dotenv(verbose=True)
-
-
-client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
-
-two_letter_codes: dict[str, list[str]] = {}
-three_letter_codes: dict[str, list[str]] = {}
-language_names: list[str] = []
-for lang in iso639.iter_langs():
- if not lang.pt1:
- continue
- if lang.pt1:
- if lang.pt1 in two_letter_codes:
- two_letter_codes[lang.pt1].append(lang.name)
- else:
- two_letter_codes[lang.pt1] = [lang.name]
- language_names.append(f"{lang.name}")
-
-
-def autocomplete_language(ctx: discord.AutocompleteContext) -> list[str]:
- input_value = ctx.value.lower()
- if len(input_value) <= 1:
- return []
- elif len(input_value) == 2:
- return two_letter_codes.get(input_value, [])
- elif len(input_value) == 3:
- return three_letter_codes.get(input_value, [])
- else:
- matching_languages = [
- name for name in language_names if name.lower().startswith(input_value)
- ]
- matching_languages.sort(key=lambda name: len(name))
- return matching_languages[:30]
-
-
-class Translate(commands.Cog):
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @discord.slash_command(
- name="translate",
- description="Translate text",
- description_localizaitons={
- "ja_JP": "テキストを翻訳します",
- },
- options=[
- discord.Option(
- name="text",
- description="Text to translate",
- description_localizaitons={
- "ja_JP": "翻訳するテキスト",
- },
- required=True,
- ),
- discord.Option(
- name="to",
- description="Language to translate to",
- description_localizaitons={
- "ja_JP": "翻訳先の言語",
- },
- autocomplete=autocomplete_language,
- required=True,
- ),
- ],
- )
- async def translate(self, ctx: discord.ApplicationContext, text: str, to: str):
- """Translate text"""
- await ctx.defer()
- try:
- lang = iso639.Lang(to)
- except InvalidLanguageValue:
- return await ctx.followup.send("Invalid language", ephemeral=True)
- if not lang.pt1:
- return await ctx.followup.send("Invalid language", ephemeral=True)
- response = await client.chat.completions.create(
- model="gpt-4",
- messages=[
- {
- "role": "system",
- "content": "This is a direct translation task. "
- f"Translate the following text to {lang.name}. "
- "Do not add any additional comments or language indicators.",
- },
- {
- "role": "user",
- "content": text,
- },
- ],
- max_tokens=2000,
- )
- embed = discord.Embed(
- title="Translate",
- color=discord.Color.blurple(),
- )
- embed.add_field(
- name="Original text",
- value=text,
- inline=False,
- )
- embed.add_field(
- name=f"Translated to {to}",
- value=response.choices[0].message.content or "",
- inline=False,
- )
- view = discord.ui.View(DeleteButton(ctx.user))
- await ctx.followup.send(embed=embed, view=view)
-
-
-def setup(bot):
- return bot.add_cog(Translate(bot))
diff --git a/discord/bots/cogs/Wolfram.py b/discord/bots/cogs/Wolfram.py
deleted file mode 100644
index 3a18082..0000000
--- a/discord/bots/cogs/Wolfram.py
+++ /dev/null
@@ -1,105 +0,0 @@
-import os
-import pathlib
-from collections import OrderedDict
-
-import aiohttp
-import discord
-import dotenv
-from discord.ext import commands, pages
-
-from .. import SUPPORT_SERVER_LINK, DeleteButton
-
-dotenv.load_dotenv(verbose=True)
-URL = 'http://api.wolframalpha.com/v2/query'
-BASE_DIR = pathlib.Path(__file__).parent.parent
-
-
-class LimitedSizeDict(OrderedDict):
-
- def __init__(self, size_limit=None, *args, **kwds):
- self.size_limit = size_limit
- super().__init__(*args, **kwds)
- self._check_size_limit()
-
- def __setitem__(self, key, value):
- super().__setitem__(key, value)
- self._check_size_limit()
-
- def _check_size_limit(self):
- if self.size_limit is not None:
- while len(self) > self.size_limit:
- self.popitem(last=False)
-
-
-class Wolfram(commands.Cog):
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.user_message_id_to_bot_message = LimitedSizeDict(size_limit=100)
-
- @commands.Cog.listener()
- async def on_message_edit(self, before: discord.Message, after: discord.Message):
- if before.content != after.content:
- if before.id in self.user_message_id_to_bot_message:
- await self.user_message_id_to_bot_message[before.id].delete()
-
- @commands.Cog.listener()
- async def on_message_delete(self, message: discord.Message):
- if message.id in self.user_message_id_to_bot_message:
- await self.user_message_id_to_bot_message[message.id].delete()
-
-
- @commands.command(aliases=['wolfram'])
- async def wolf(self, ctx: commands.Context, *, query: str):
-
- async with ctx.channel.typing():
- view = discord.ui.View(DeleteButton(ctx.author), timeout=None)
-
- async with aiohttp.ClientSession() as session:
- async with session.get(URL, params={'input': query, 'format': 'image,plaintext', 'output': 'JSON', 'appid': os.environ.get('WOLFRAM_APPID')}) as resp:
- if resp.status == 200:
- data = await resp.json()
- else:
- embed = discord.Embed(
- title='Connection Error',
- description=f'{resp.status}',
- color=0xff0000
- )
- embed.set_author(
- name=ctx.author.name,
- icon_url=ctx.author.display_avatar.url
- )
- self.user_message_id_to_bot_message[ctx.message.id] = await ctx.reply(content=f'Please Report us!\n{SUPPORT_SERVER_LINK}', embed=embed, view=view)
- return
-
- page_list = []
- if data['queryresult']['success']:
- for pod in data['queryresult']['pods']:
- for subpod in pod['subpods']:
- embed = discord.Embed(
- title=pod['title'],
- description=subpod['plaintext'],
- color=0x00ff00,
- )
- if 'img' in subpod:
- embed.set_image(url=subpod['img']['src'])
- embed.set_author(name=ctx.author.name, icon_url=ctx.author.display_avatar.url)
- page_list.append(embed)
- paginator = pages.Paginator(pages=page_list)
- # paginator.add_button(DeleteButton(self.bot))
- m = await paginator.send(ctx)
- else:
- embed = discord.Embed(
- title='Error',
- description='Wolfram|Alpha は、その入力を理解できませんでした。',
- color=0xff0000,
- )
- embed.set_author(
- name=ctx.author.name,
- icon_url=ctx.author.display_avatar.url
- )
- m = await ctx.reply(embed=embed, view=view)
- self.user_message_id_to_bot_message[ctx.message.id] = m
-
-def setup(bot):
- return bot.add_cog(Wolfram(bot))
diff --git a/discord/bots/cogs/__init__.py b/discord/bots/cogs/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/discord/bots/config/privacy-policy.md b/discord/bots/config/privacy-policy.md
deleted file mode 100644
index 1724d17..0000000
--- a/discord/bots/config/privacy-policy.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Privacy Policy
-
-## Owner
-
-Gakuto Furuya (gaato.)
-
-## Bot
-
-- **CodeRunBot**#4348
-
-## What data we collect
-
-- Message content of the channel where the bot can be seen
-- Internal error logs (not related to the user)
-
-## How we use it
-
-### Message content
-
-- The bot uses to determine if a command is being called.
-- If so, the bot use it to create a response.
-
-It is not used for any other purpose and is discarded immediately.
-
-### Internal error logs
-
-- For debugging purposes.
-
-## How to opt-out of providing us that data
-
-The /opt-out command will prevent the bot from tracking the content of your messages. Note that this command will disable most of the features of the bot.
-
-## An accessible way for users to request to and deletion of their data
-
-Originally, users data is not saved because it is immediately discarded.
diff --git a/discord/bots/core/__init__.py b/discord/bots/core/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/discord/bots/core/bot.py b/discord/bots/core/bot.py
deleted file mode 100644
index bda8709..0000000
--- a/discord/bots/core/bot.py
+++ /dev/null
@@ -1,167 +0,0 @@
-import io
-import asyncio
-import pathlib
-import traceback
-from typing import TypeAlias, TypeGuard
-
-import discord
-from discord.ext import commands
-
-from .. import DEVELOPER_ID, LOG_CHANNEL_ID, SUPPORT_SERVER_LINK, DeleteButton
-from .state import OptOutUsers, TextState
-
-BASE_DIR = pathlib.Path(__file__).parent.parent
-OPT_OUT_USERS = OptOutUsers(
- TextState(BASE_DIR / "data" / "opt-out-users.txt", "opt-out-users.txt")
-)
-SendableChannel: TypeAlias = (
- discord.TextChannel | discord.Thread | discord.DMChannel | discord.GroupChannel
-)
-
-
-def is_sendable_channel(channel: object) -> TypeGuard[SendableChannel]:
- return isinstance(
- channel,
- (discord.TextChannel, discord.Thread, discord.DMChannel, discord.GroupChannel),
- )
-
-
-def ensure_event_loop() -> asyncio.AbstractEventLoop:
- try:
- return asyncio.get_event_loop()
- except RuntimeError:
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- return loop
-
-
-class Bot(commands.Bot):
- def __init__(self, token: str | None, cogs: list[str], prefix: str):
- self.token = token
- self.logging_channel: SendableChannel | None = None
- self.developer: discord.User | None = None
- intents = discord.Intents.default()
- intents.message_content = True
- ensure_event_loop()
- super().__init__(command_prefix=prefix, intents=intents)
- self.load_cogs(cogs)
-
- def load_cogs(self, cogs: list[str]) -> None:
- for cog in cogs:
- self.load_extension(cog)
- print('Loaded ' + cog)
-
- async def on_ready(self) -> None:
- user = self.user
- if user is None:
- return
- print(f'Logged in as {user} (ID: {user.id})')
- print(f'Pycord Version: {discord.__version__}')
- channel = self.get_channel(LOG_CHANNEL_ID)
- self.logging_channel = channel if is_sendable_channel(channel) else None
- self.developer = self.get_user(DEVELOPER_ID)
-
-
- async def on_message(self, message: discord.Message) -> None:
- if message.author.id in OPT_OUT_USERS:
- return
- await super().on_message(message)
-
- async def on_message_edit(
- self, before: discord.Message, after: discord.Message
- ) -> None:
- if before.content == after.content:
- return
- await self.on_message(after)
-
- async def on_command_error(
- self, context: commands.Context, exception: commands.CommandError
- ) -> None:
- if isinstance(exception, commands.CommandNotFound):
- return
- if isinstance(exception, commands.UserInputError):
- view = discord.ui.View(DeleteButton(context.author), timeout=None)
- embed = discord.Embed(
- title='Invalid Input',
- description=f'```\n{exception}\n```',
- color=0xff0000,
- )
- embed.set_author(
- name=context.author.name,
- icon_url=context.author.display_avatar.url,
- )
- await context.reply(embed=embed, view=view)
- return
- view = discord.ui.View(DeleteButton(context.author), timeout=None)
- embed = discord.Embed(
- title='Unhandled Error',
- color=0xff0000,
- )
- embed.set_author(
- name=context.author.name,
- icon_url=context.author.display_avatar.url,
- )
- await context.reply(
- content=f'Please Report us!\n{SUPPORT_SERVER_LINK}',
- embed=embed,
- view=view,
- )
- await self.log_error(context, exception)
- await super().on_command_error(context, exception)
-
- async def on_slash_command_error(
- self, ctx: discord.ApplicationContext, exception: Exception
- ) -> None:
- await self.log_error(ctx, exception)
-
- async def log_error(
- self, ctx: commands.Context | discord.ApplicationContext, exception: Exception
- ) -> None:
- if isinstance(ctx, commands.Context):
- content = ctx.message.content
- else:
- command_name = ctx.command.qualified_name if ctx.command else "unknown"
- option_values = " ".join(str(arg) for arg in (ctx.options or {}).values())
- content = f'/{command_name} {option_values}'.strip()
- exception_text = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__))
- content_formatted = "```\n" + content + "\n```"
- # attempt to resolve the channel if not cached
- if self.logging_channel is None:
- try:
- channel = await self.fetch_channel(LOG_CHANNEL_ID)
- except Exception:
- print("Unable to fetch log channel", LOG_CHANNEL_ID)
- print(exception_text)
- return
- self.logging_channel = channel if is_sendable_channel(channel) else None
- if self.logging_channel is None:
- print("Log channel is not sendable", LOG_CHANNEL_ID)
- print(exception_text)
- return
- error_file = discord.File(
- io.BytesIO(exception_text.encode()),
- filename='error.txt',
- )
- # Try to send to Discord log channel, but don't raise if it fails; fallback to stdout
- try:
- await self.logging_channel.send(content=content_formatted, file=error_file)
- except discord.Forbidden:
- print('Bot missing access to log channel:', LOG_CHANNEL_ID)
- print(exception_text)
- except Exception:
- print('Failed to send log to channel:', LOG_CHANNEL_ID)
- traceback.print_exc()
-
- def run(self) -> None:
- if self.token is None:
- print('Missing Discord token')
- return
- try:
- self.loop.run_until_complete(self.start(self.token))
- except discord.LoginFailure:
- print('Invalid Discord Token')
- except KeyboardInterrupt:
- print('Shutdown')
- self.loop.run_until_complete(self.close())
- except Exception:
- traceback.print_exc()
diff --git a/discord/bots/core/state.py b/discord/bots/core/state.py
deleted file mode 100644
index 10b29e1..0000000
--- a/discord/bots/core/state.py
+++ /dev/null
@@ -1,126 +0,0 @@
-import os
-import pathlib
-
-import oci
-from oci.exceptions import ServiceError
-
-
-class TextState:
- def __init__(self, local_path: pathlib.Path, object_name: str) -> None:
- self.local_path = local_path
- self.object_name = object_name
- self.backend = os.environ.get("BOT_STATE_BACKEND", "local")
- self.bucket = os.environ.get("BOT_STATE_BUCKET")
- self.namespace = os.environ.get("BOT_STATE_NAMESPACE")
- self.prefix = os.environ.get("BOT_STATE_PREFIX", "").strip("/")
- self._client = None
-
- @property
- def key(self) -> str:
- if self.prefix:
- return f"{self.prefix}/{self.object_name}"
- return self.object_name
-
- @property
- def client(self):
- if self._client is None:
- self._client = oci.object_storage.ObjectStorageClient(self._oci_config())
- return self._client
-
- def _oci_config(self) -> dict[str, str]:
- key_file = os.environ.get("OCI_PRIVATE_KEY_FILE")
- if key_file:
- return {
- "user": os.environ["OCI_USER_OCID"],
- "fingerprint": os.environ["OCI_FINGERPRINT"],
- "tenancy": os.environ["OCI_TENANCY_OCID"],
- "region": os.environ["OCI_REGION"],
- "key_file": key_file,
- **(
- {"pass_phrase": os.environ["OCI_PRIVATE_KEY_PASSPHRASE"]}
- if os.environ.get("OCI_PRIVATE_KEY_PASSPHRASE")
- else {}
- ),
- }
-
- key_content = os.environ.get("OCI_PRIVATE_KEY")
- if key_content:
- return {
- "user": os.environ["OCI_USER_OCID"],
- "fingerprint": os.environ["OCI_FINGERPRINT"],
- "tenancy": os.environ["OCI_TENANCY_OCID"],
- "region": os.environ["OCI_REGION"],
- "key_content": key_content,
- **(
- {"pass_phrase": os.environ["OCI_PRIVATE_KEY_PASSPHRASE"]}
- if os.environ.get("OCI_PRIVATE_KEY_PASSPHRASE")
- else {}
- ),
- }
- return oci.config.from_file()
-
- def read(self) -> str:
- if self.backend != "oci":
- return self._read_local()
- if not self.bucket or not self.namespace:
- raise RuntimeError(
- "BOT_STATE_BUCKET and BOT_STATE_NAMESPACE are required when BOT_STATE_BACKEND=oci"
- )
- try:
- response = self.client.get_object(self.namespace, self.bucket, self.key)
- except ServiceError as error:
- if error.status != 404:
- raise
- self.write("")
- return ""
- return response.data.content.decode("utf-8")
-
- def write(self, value: str) -> None:
- if self.backend != "oci":
- self._write_local(value)
- return
- if not self.bucket or not self.namespace:
- raise RuntimeError(
- "BOT_STATE_BUCKET and BOT_STATE_NAMESPACE are required when BOT_STATE_BACKEND=oci"
- )
- self.client.put_object(
- self.namespace,
- self.bucket,
- self.key,
- value.encode("utf-8"),
- content_type="text/plain; charset=utf-8",
- )
-
- def _read_local(self) -> str:
- if not self.local_path.exists():
- return ""
- return self.local_path.read_text()
-
- def _write_local(self, value: str) -> None:
- self.local_path.parent.mkdir(parents=True, exist_ok=True)
- self.local_path.write_text(value)
-
-
-class OptOutUsers:
- def __init__(self, state: TextState) -> None:
- self.state = state
- self.users = self._parse(state.read())
-
- def __contains__(self, user_id: int) -> bool:
- return user_id in self.users
-
- def add(self, user_id: int) -> None:
- self.users.add(user_id)
- self.save()
-
- def remove(self, user_id: int) -> None:
- self.users.remove(user_id)
- self.save()
-
- def save(self) -> None:
- value = "".join(f"{user_id}\n" for user_id in sorted(self.users))
- self.state.write(value)
-
- @staticmethod
- def _parse(value: str) -> set[int]:
- return {int(line) for line in value.splitlines() if line.strip()}
diff --git a/discord/config/privacy-policy.md b/discord/config/privacy-policy.md
new file mode 100644
index 0000000..483c073
--- /dev/null
+++ b/discord/config/privacy-policy.md
@@ -0,0 +1,65 @@
+# Privacy Policy
+
+## Owner
+
+Gakuto Furuya (gaato.)
+
+## Bots
+
+- **CodeRunBot**#4348
+- **gaato bot** (private)
+
+## What data we process
+
+- **Message content** in channels the bot can read, used to detect and execute
+ commands (e.g. `]run`, `]tex`) and, for gaato bot, to reply when mentioned.
+ Message content is processed transiently and is not stored by us, except as
+ described under "Error logs" below.
+- **Opt-out list**: if you use `/opt-out`, your Discord user ID is stored
+ persistently in cloud object storage so that the setting survives restarts.
+ `/opt-in` removes it.
+- **Error logs**: when a command fails unexpectedly, the invoking command text
+ and the internal error trace are posted to a private developer-only Discord
+ channel for debugging.
+- **Usage statistics**: anonymous aggregate counters — which command was used,
+ whether it succeeded, the language chosen for `/run`, and the number of servers
+ the bot is in — are stored in cloud object storage. These counters contain no
+ user IDs, no server IDs, and no message content.
+
+## Third-party services
+
+Some commands send your input to external services to do their work:
+
+- `/run`, `]run`: your code and standard input are sent to
+ [Wandbox](https://wandbox.org) for execution.
+- `]wolf`, `/wolf` (gaato bot): your query is sent to Wolfram|Alpha.
+- `/translate` and mention replies (gaato bot): the text to translate, or the
+ mentioning message together with up to 10 recent messages in the channel, is
+ sent to OpenAI. Messages authored by opted-out users are excluded from that
+ context.
+- `/tex`, `]tex`: LaTeX is rendered inside the bot itself; nothing is sent to
+ a third party.
+
+These services receive only what is needed to fulfil the command and are
+governed by their own privacy policies.
+
+## Opting out
+
+`/opt-out` stops all passive processing of your message content: prefix
+commands, mention replies, edit tracking, and inclusion of your messages in
+OpenAI context. Explicitly invoked slash commands and context menu commands
+keep working, since using them is itself consent to process that input.
+
+## Source code
+
+The bots are free software (AGPL-3.0-or-later). The full source code is
+available at , also shown by the
+`/source` command.
+
+## Data deletion
+
+We do not build up a store of user data: message content is discarded after a
+command completes. To remove your stored user ID from the opt-out list, use
+`/opt-in`. For anything else (e.g. an error log that captured your command),
+ask on the [support server](https://discord.gg/qRpYRTgvXM) and it will be
+deleted.
diff --git a/discord/bots/data/.gitignore b/discord/data/.gitignore
similarity index 100%
rename from discord/bots/data/.gitignore
rename to discord/data/.gitignore
diff --git a/discord/package.json b/discord/package.json
new file mode 100644
index 0000000..cc808d5
--- /dev/null
+++ b/discord/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "coderunbot-discord",
+ "version": "0.1.0",
+ "private": true,
+ "license": "AGPL-3.0-or-later",
+ "type": "module",
+ "packageManager": "pnpm@11.9.0",
+ "engines": {
+ "node": ">=24"
+ },
+ "scripts": {
+ "dev": "tsx watch src/index.ts",
+ "build": "tsc",
+ "start": "node dist/index.js",
+ "typecheck": "tsc --noEmit",
+ "lint": "biome check src && dependency-cruiser --config .dependency-cruiser.cjs --include-only '^src' src && madge --circular --extensions ts src",
+ "format": "biome format --write src",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@aws-sdk/client-s3": "^3.1089.0",
+ "@mathjax/mathjax-newcm-font": "^4.1.3",
+ "@mathjax/src": "^4.1.3",
+ "discord.js": "^14.27.0",
+ "dotenv": "^17.4.2",
+ "i18next": "^26.3.6",
+ "iso-639-1": "^3.1.6",
+ "openai": "^6.48.0",
+ "pino": "^10.3.1",
+ "sharp": "^0.35.3"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.5.4",
+ "@types/node": "^24.13.3",
+ "dependency-cruiser": "^18.1.0",
+ "madge": "^8.0.0",
+ "tsx": "^4.23.1",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.10"
+ }
+}
diff --git a/discord/pnpm-lock.yaml b/discord/pnpm-lock.yaml
new file mode 100644
index 0000000..c0944a9
--- /dev/null
+++ b/discord/pnpm-lock.yaml
@@ -0,0 +1,3461 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@aws-sdk/client-s3':
+ specifier: ^3.1089.0
+ version: 3.1089.0
+ '@mathjax/mathjax-newcm-font':
+ specifier: ^4.1.3
+ version: 4.1.3
+ '@mathjax/src':
+ specifier: ^4.1.3
+ version: 4.1.3
+ discord.js:
+ specifier: ^14.27.0
+ version: 14.27.0
+ dotenv:
+ specifier: ^17.4.2
+ version: 17.4.2
+ i18next:
+ specifier: ^26.3.6
+ version: 26.3.6(typescript@6.0.3)
+ iso-639-1:
+ specifier: ^3.1.6
+ version: 3.1.6
+ openai:
+ specifier: ^6.48.0
+ version: 6.48.0(@aws-sdk/credential-provider-node@3.972.70)(@smithy/signature-v4@5.6.6)(ws@8.21.1)
+ pino:
+ specifier: ^10.3.1
+ version: 10.3.1
+ sharp:
+ specifier: ^0.35.3
+ version: 0.35.3(@types/node@24.13.3)
+ devDependencies:
+ '@biomejs/biome':
+ specifier: ^2.5.4
+ version: 2.5.4
+ '@types/node':
+ specifier: ^24.13.3
+ version: 24.13.3
+ dependency-cruiser:
+ specifier: ^18.1.0
+ version: 18.1.0
+ madge:
+ specifier: ^8.0.0
+ version: 8.0.0(typescript@6.0.3)
+ tsx:
+ specifier: ^4.23.1
+ version: 4.23.1
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
+ vitest:
+ specifier: ^4.1.10
+ version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1))
+
+packages:
+
+ '@aws-sdk/checksums@3.1000.18':
+ resolution: {integrity: sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/client-s3@3.1089.0':
+ resolution: {integrity: sha512-Sc+JOtNeP+v+XdP9Rb4Hh+uhiNO2hh/CZQbKYooNakhOIgK6/K0cjoDCEGeFmkG0vf2DopTmSctzKlcKwy1BPw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/core@3.975.3':
+ resolution: {integrity: sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-env@3.972.59':
+ resolution: {integrity: sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-http@3.972.61':
+ resolution: {integrity: sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-ini@3.973.4':
+ resolution: {integrity: sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-login@3.972.66':
+ resolution: {integrity: sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-node@3.972.70':
+ resolution: {integrity: sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-process@3.972.59':
+ resolution: {integrity: sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-sso@3.973.3':
+ resolution: {integrity: sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-web-identity@3.972.65':
+ resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-sdk-s3@3.972.64':
+ resolution: {integrity: sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/nested-clients@3.997.33':
+ resolution: {integrity: sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/signature-v4-multi-region@3.996.41':
+ resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/token-providers@3.1088.0':
+ resolution: {integrity: sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/types@3.974.2':
+ resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/xml-builder@3.972.36':
+ resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws/lambda-invoke-store@0.3.0':
+ resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
+ '@biomejs/biome@2.5.4':
+ resolution: {integrity: sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==}
+ engines: {node: '>=14.21.3'}
+ hasBin: true
+
+ '@biomejs/cli-darwin-arm64@2.5.4':
+ resolution: {integrity: sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@biomejs/cli-darwin-x64@2.5.4':
+ resolution: {integrity: sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@biomejs/cli-linux-arm64-musl@2.5.4':
+ resolution: {integrity: sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@biomejs/cli-linux-arm64@2.5.4':
+ resolution: {integrity: sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@biomejs/cli-linux-x64-musl@2.5.4':
+ resolution: {integrity: sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@biomejs/cli-linux-x64@2.5.4':
+ resolution: {integrity: sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@biomejs/cli-win32-arm64@2.5.4':
+ resolution: {integrity: sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@biomejs/cli-win32-x64@2.5.4':
+ resolution: {integrity: sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [win32]
+
+ '@dependents/detective-less@5.0.3':
+ resolution: {integrity: sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==}
+ engines: {node: '>=18'}
+
+ '@discordjs/builders@1.14.1':
+ resolution: {integrity: sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==}
+ engines: {node: '>=16.11.0'}
+
+ '@discordjs/collection@1.5.3':
+ resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==}
+ engines: {node: '>=16.11.0'}
+
+ '@discordjs/collection@2.1.1':
+ resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==}
+ engines: {node: '>=18'}
+
+ '@discordjs/formatters@0.6.2':
+ resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==}
+ engines: {node: '>=16.11.0'}
+
+ '@discordjs/rest@2.6.2':
+ resolution: {integrity: sha512-c5HI3hJuRWWrnpyCZAUYfIUTAEv9weX4tE2UfDqtM3ztizdKE9RFEf3G5IWhPyO+kPldUDpGv02XJAQzdUTC9Q==}
+ engines: {node: '>=18'}
+
+ '@discordjs/util@1.2.0':
+ resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==}
+ engines: {node: '>=18'}
+
+ '@discordjs/ws@1.2.3':
+ resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==}
+ engines: {node: '>=16.11.0'}
+
+ '@discoveryjs/json-ext@1.1.0':
+ resolution: {integrity: sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==}
+ engines: {node: '>=14.17.0'}
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/runtime@1.11.2':
+ resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@esbuild/aix-ppc64@0.28.1':
+ resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.1':
+ resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.1':
+ resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.1':
+ resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.1':
+ resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.1':
+ resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.1':
+ resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.1':
+ resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.1':
+ resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.1':
+ resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.1':
+ resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.1':
+ resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.1':
+ resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.1':
+ resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.1':
+ resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.1':
+ resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.1':
+ resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.1':
+ resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.1':
+ resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.1':
+ resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.1':
+ resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.1':
+ resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.1':
+ resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.1':
+ resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.1':
+ resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.1':
+ resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.35.3':
+ resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.35.3':
+ resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-freebsd-wasm32@0.35.3':
+ resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
+ engines: {node: '>=20.9.0'}
+ os: [freebsd]
+
+ '@img/sharp-libvips-darwin-arm64@1.3.2':
+ resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.3.2':
+ resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.3.2':
+ resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-arm@1.3.2':
+ resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-ppc64@1.3.2':
+ resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-riscv64@1.3.2':
+ resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-s390x@1.3.2':
+ resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-x64@1.3.2':
+ resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+ resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+ resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linux-arm64@0.35.3':
+ resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-arm@0.35.3':
+ resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-ppc64@0.35.3':
+ resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-riscv64@0.35.3':
+ resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-s390x@0.35.3':
+ resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
+ engines: {node: '>=20.9.0'}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-x64@0.35.3':
+ resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linuxmusl-arm64@0.35.3':
+ resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linuxmusl-x64@0.35.3':
+ resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-wasm32@0.35.3':
+ resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
+ engines: {node: '>=20.9.0'}
+
+ '@img/sharp-webcontainers-wasm32@0.35.3':
+ resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
+ engines: {node: '>=20.9.0'}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.35.3':
+ resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.35.3':
+ resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
+ engines: {node: ^20.9.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.35.3':
+ resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@mathjax/mathjax-newcm-font@4.1.3':
+ resolution: {integrity: sha512-gzAB3dFHilHX1l5x2xUqRL+1jDQt3Fyza1DkEMVXWC4E8SvsGdlgEza47HYi2WhVcgfkvf4zgUGzuhbq3Pjlew==}
+
+ '@mathjax/src@4.1.3':
+ resolution: {integrity: sha512-rIrWquuBSoJuoMBdC/1qD+AUHTorlccPicoVy6P2xbUgnuDBpCcpbHtOAsB8L3hdCHtNBg92lF8e3Fz+pkcQbw==}
+
+ '@napi-rs/wasm-runtime@1.1.6':
+ resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.139.0':
+ resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
+
+ '@pinojs/redact@0.4.0':
+ resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
+
+ '@rolldown/binding-android-arm64@1.1.5':
+ resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.1.5':
+ resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.1.5':
+ resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.1.5':
+ resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.1.5':
+ resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.1.5':
+ resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.1.5':
+ resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.1.5':
+ resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.1.5':
+ resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.1.5':
+ resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@sapphire/async-queue@1.5.5':
+ resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==}
+ engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
+
+ '@sapphire/shapeshift@4.0.0':
+ resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==}
+ engines: {node: '>=v16'}
+
+ '@sapphire/snowflake@3.5.5':
+ resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==}
+ engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
+
+ '@smithy/core@3.29.5':
+ resolution: {integrity: sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/credential-provider-imds@4.4.10':
+ resolution: {integrity: sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/fetch-http-handler@5.6.7':
+ resolution: {integrity: sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/node-http-handler@4.9.7':
+ resolution: {integrity: sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/signature-v4@5.6.6':
+ resolution: {integrity: sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/types@4.16.1':
+ resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==}
+ engines: {node: '>=18.0.0'}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@ts-graphviz/adapter@2.0.6':
+ resolution: {integrity: sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==}
+ engines: {node: '>=18'}
+
+ '@ts-graphviz/ast@2.0.7':
+ resolution: {integrity: sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==}
+ engines: {node: '>=18'}
+
+ '@ts-graphviz/common@2.1.5':
+ resolution: {integrity: sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==}
+ engines: {node: '>=18'}
+
+ '@ts-graphviz/core@2.0.7':
+ resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==}
+ engines: {node: '>=18'}
+
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/node@24.13.3':
+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
+
+ '@types/ws@8.18.1':
+ resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
+ '@typescript-eslint/project-service@8.64.0':
+ resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/tsconfig-utils@8.64.0':
+ resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.64.0':
+ resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.64.0':
+ resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.64.0':
+ resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitest/expect@4.1.10':
+ resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
+
+ '@vitest/mocker@4.1.10':
+ resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.10':
+ resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
+
+ '@vitest/runner@4.1.10':
+ resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
+
+ '@vitest/snapshot@4.1.10':
+ resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
+
+ '@vitest/spy@4.1.10':
+ resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
+
+ '@vitest/utils@4.1.10':
+ resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
+
+ '@vladfrangu/async_event_emitter@2.4.7':
+ resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==}
+ engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
+
+ '@vue/compiler-core@3.5.40':
+ resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==}
+
+ '@vue/compiler-dom@3.5.40':
+ resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==}
+
+ '@vue/compiler-sfc@3.5.40':
+ resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==}
+
+ '@vue/compiler-ssr@3.5.40':
+ resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==}
+
+ '@vue/shared@3.5.40':
+ resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==}
+
+ '@xmldom/xmldom@0.9.10':
+ resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
+ engines: {node: '>=14.6'}
+
+ acorn-jsx-walk@2.0.0:
+ resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn-loose@8.5.2:
+ resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==}
+ engines: {node: '>=0.4.0'}
+
+ acorn-walk@8.3.5:
+ resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
+ engines: {node: '>=0.4.0'}
+
+ acorn@8.17.0:
+ resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+
+ app-module-path@2.2.0:
+ resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ ast-module-types@6.0.2:
+ resolution: {integrity: sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==}
+ engines: {node: '>=18'}
+
+ atomic-sleep@1.0.0:
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+ engines: {node: '>=8.0.0'}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
+ bowser@2.14.1:
+ resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
+
+ brace-expansion@5.0.7:
+ resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
+ engines: {node: 18 || 20 || >=22}
+
+ buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ cli-cursor@3.1.0:
+ resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
+ engines: {node: '>=8'}
+
+ cli-spinners@2.9.2:
+ resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+ engines: {node: '>=6'}
+
+ clone@1.0.4:
+ resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
+ engines: {node: '>=0.8'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ commander@12.1.0:
+ resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+ engines: {node: '>=18'}
+
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
+ commander@15.0.0:
+ resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==}
+ engines: {node: '>=22.12.0'}
+
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
+ commondir@1.0.1:
+ resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+
+ defaults@1.0.4:
+ resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
+
+ dependency-cruiser@18.1.0:
+ resolution: {integrity: sha512-pbsH0gQ15BxXi97SCuMeVgsh8VzYpziGIVaMdnALbVu+TYjSZrW9/nOvsPU+NjHLmJSF9R1UijjoACq6Ne/ybQ==}
+ engines: {node: ^22||^24||>=26}
+ hasBin: true
+
+ dependency-tree@11.5.0:
+ resolution: {integrity: sha512-K9zBwKDZrot3RkxizugpVSdImxULAg4Ycp3+ydy2r561k96oiiw6nfsOR15fwNDQ5BF2UXe+2JFM/H5Xz4MGQg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ detective-amd@6.1.0:
+ resolution: {integrity: sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ detective-cjs@6.1.1:
+ resolution: {integrity: sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA==}
+ engines: {node: '>=18'}
+
+ detective-es6@5.0.2:
+ resolution: {integrity: sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA==}
+ engines: {node: '>=18'}
+
+ detective-postcss@8.0.4:
+ resolution: {integrity: sha512-DZ7M/hWPZyr17ZUdoQ+TVXaPj70mYr4XXrAE+GeJbca44haCvZgb191L/jLJmFYewhxRJuBd4lUtNSu986TXag==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ postcss: ^8.4.47
+
+ detective-sass@6.0.2:
+ resolution: {integrity: sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA==}
+ engines: {node: '>=18'}
+
+ detective-scss@5.0.2:
+ resolution: {integrity: sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg==}
+ engines: {node: '>=18'}
+
+ detective-stylus@5.0.1:
+ resolution: {integrity: sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==}
+ engines: {node: '>=18'}
+
+ detective-typescript@14.1.2:
+ resolution: {integrity: sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ typescript: ^5.4.4 || ^6.0.2
+
+ detective-vue2@2.3.0:
+ resolution: {integrity: sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ typescript: ^5.4.4 || ^6.0.2
+
+ discord-api-types@0.38.50:
+ resolution: {integrity: sha512-J2n/bpIETX3DQ6AJ7/0xbsTLmYiJQtO/LKcXKC1YDbB56OUwtDbdXOFE8Q4g8jVGHBR2VAy1+D4ngaIgkMNV9w==}
+
+ discord.js@14.27.0:
+ resolution: {integrity: sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==}
+ engines: {node: '>=18'}
+
+ dotenv@17.4.2:
+ resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
+ engines: {node: '>=12'}
+
+ enhanced-resolve@5.24.2:
+ resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==}
+ engines: {node: '>=10.13.0'}
+
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@2.3.1:
+ resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
+
+ esbuild@0.28.1:
+ resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escodegen@2.1.0:
+ resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
+ engines: {node: '>=6.0'}
+ hasBin: true
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ filing-cabinet@5.5.1:
+ resolution: {integrity: sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-amd-module-type@6.0.2:
+ resolution: {integrity: sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ==}
+ engines: {node: '>=18'}
+
+ get-own-enumerable-property-symbols@3.0.2:
+ resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
+
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
+
+ global-directory@4.0.1:
+ resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
+ engines: {node: '>=18'}
+
+ gonzales-pe@4.3.0:
+ resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==}
+ engines: {node: '>=0.6.0'}
+ hasBin: true
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
+ i18next@26.3.6:
+ resolution: {integrity: sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==}
+ peerDependencies:
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ ignore@7.0.6:
+ resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
+ engines: {node: '>= 4'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ ini@4.1.1:
+ resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ interpret@3.1.1:
+ resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==}
+ engines: {node: '>=10.13.0'}
+
+ is-core-module@2.16.2:
+ resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==}
+ engines: {node: '>= 0.4'}
+
+ is-installed-globally@1.0.0:
+ resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==}
+ engines: {node: '>=18'}
+
+ is-interactive@1.0.0:
+ resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
+ engines: {node: '>=8'}
+
+ is-obj@1.0.1:
+ resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==}
+ engines: {node: '>=0.10.0'}
+
+ is-path-inside@4.0.0:
+ resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==}
+ engines: {node: '>=12'}
+
+ is-regexp@1.0.0:
+ resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==}
+ engines: {node: '>=0.10.0'}
+
+ is-unicode-supported@0.1.0:
+ resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+ engines: {node: '>=10'}
+
+ is-url-superb@4.0.0:
+ resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==}
+ engines: {node: '>=10'}
+
+ iso-639-1@3.1.6:
+ resolution: {integrity: sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==}
+ engines: {node: '>=6.0'}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ kleur@3.0.3:
+ resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
+ engines: {node: '>=6'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lodash.snakecase@4.1.1:
+ resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
+
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
+ log-symbols@4.1.0:
+ resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+ engines: {node: '>=10'}
+
+ lru-cache@11.5.2:
+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
+ engines: {node: 20 || >=22}
+
+ madge@8.0.0:
+ resolution: {integrity: sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.4.4
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ magic-bytes.js@1.13.0:
+ resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ mhchemparser@4.2.1:
+ resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
+
+ mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ mj-context-menu@1.0.0:
+ resolution: {integrity: sha512-OSgBFQRCVhrZwRa9lhqnKcJU92dd/YXgBjup0uyeuj9bOaVsf4myOyrjU4PfhYkdDk6AqVUTov7v5uFMvIbduA==}
+
+ module-definition@6.0.2:
+ resolution: {integrity: sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ module-lookup-amd@9.1.3:
+ resolution: {integrity: sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.16:
+ resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-source-walk@7.0.2:
+ resolution: {integrity: sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A==}
+ engines: {node: '>=18'}
+
+ obug@2.1.4:
+ resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
+ engines: {node: '>=12.20.0'}
+
+ on-exit-leak-free@2.1.2:
+ resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
+ engines: {node: '>=14.0.0'}
+
+ onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+
+ openai@6.48.0:
+ resolution: {integrity: sha512-KhVp+FyV50QrXNextvL9hIU5l6ox5HYuKQjGVk7lIqprgJol90+dQXWONV6S1lRWsKA1bXjrow8RsUT14M1hNA==}
+ peerDependencies:
+ '@aws-sdk/credential-provider-node': '>=3.972.0 <4'
+ '@smithy/hash-node': '>=4.3.0 <5'
+ '@smithy/signature-v4': '>=5.4.0 <6'
+ ws: ^8.18.0
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ '@aws-sdk/credential-provider-node':
+ optional: true
+ '@smithy/hash-node':
+ optional: true
+ '@smithy/signature-v4':
+ optional: true
+ ws:
+ optional: true
+ zod:
+ optional: true
+
+ ora@5.4.1:
+ resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
+ engines: {node: '>=10'}
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
+ parse-ms@2.1.0:
+ resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
+ engines: {node: '>=6'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ pino-abstract-transport@3.0.0:
+ resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
+
+ pino-std-serializers@7.1.0:
+ resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
+
+ pino@10.3.1:
+ resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
+ hasBin: true
+
+ pluralize@8.0.0:
+ resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
+ engines: {node: '>=4'}
+
+ postcss-values-parser@6.0.2:
+ resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ postcss: ^8.2.9
+
+ postcss@8.5.19:
+ resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ precinct@12.3.2:
+ resolution: {integrity: sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ pretty-ms@7.0.1:
+ resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==}
+ engines: {node: '>=10'}
+
+ process-warning@5.0.0:
+ resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
+
+ prompts@2.4.2:
+ resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
+ engines: {node: '>= 6'}
+
+ quick-format-unescaped@4.0.4:
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+
+ quote-unquote@1.0.0:
+ resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==}
+
+ rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
+ real-require@0.2.0:
+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+ engines: {node: '>= 12.13.0'}
+
+ real-require@1.0.0:
+ resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
+
+ rechoir@0.8.0:
+ resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
+ engines: {node: '>= 10.13.0'}
+
+ regexp-tree@0.1.27:
+ resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
+ hasBin: true
+
+ requirejs-config-file@4.0.0:
+ resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==}
+ engines: {node: '>=10.13.0'}
+
+ requirejs@2.3.8:
+ resolution: {integrity: sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ resolve-dependency-path@4.0.1:
+ resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==}
+ engines: {node: '>=18'}
+
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ restore-cursor@3.1.0:
+ resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
+ engines: {node: '>=8'}
+
+ rimraf@6.1.3:
+ resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==}
+ engines: {node: 20 || >=22}
+ hasBin: true
+
+ rolldown@1.1.5:
+ resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safe-regex@2.1.1:
+ resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==}
+
+ safe-stable-stringify@2.5.0:
+ resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
+ engines: {node: '>=10'}
+
+ sass-lookup@6.1.2:
+ resolution: {integrity: sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ sharp@0.35.3:
+ resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ sonic-boom@4.2.1:
+ resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ speech-rule-engine@5.0.0-rc.4:
+ resolution: {integrity: sha512-3dFcH2QtQNhtvUUc09TxoaEK4WG03B9Z57krFG9jocrKykKGu35BhIkWr0vgEAxZZomEWkeluff69y/EbYzP4Q==}
+ hasBin: true
+
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.2.0:
+ resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
+
+ stream-to-array@2.3.0:
+ resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==}
+
+ string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+ stringify-object@3.3.0:
+ resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
+ engines: {node: '>=4'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-bom@3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+
+ strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+
+ stylus-lookup@6.1.2:
+ resolution: {integrity: sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ thread-stream@4.2.0:
+ resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
+ engines: {node: '>=20'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.2.4:
+ resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.1.0:
+ resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ engines: {node: '>=14.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ ts-graphviz@2.1.6:
+ resolution: {integrity: sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==}
+ engines: {node: '>=18'}
+
+ ts-mixer@6.0.4:
+ resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==}
+
+ tsconfig-paths-webpack-plugin@4.2.0:
+ resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
+ engines: {node: '>=10.13.0'}
+
+ tsconfig-paths@4.2.0:
+ resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
+ engines: {node: '>=6'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tsx@4.23.1:
+ resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ undici@6.27.0:
+ resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==}
+ engines: {node: '>=18.17'}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vite@8.1.5:
+ resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.3.0
+ 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
+
+ vitest@4.1.10:
+ resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ 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.10
+ '@vitest/browser-preview': 4.1.10
+ '@vitest/browser-webdriverio': 4.1.10
+ '@vitest/coverage-istanbul': 4.1.10
+ '@vitest/coverage-v8': 4.1.10
+ '@vitest/ui': 4.1.10
+ 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
+
+ walkdir@0.4.1:
+ resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==}
+ engines: {node: '>=6.0.0'}
+
+ watskeburt@6.0.0:
+ resolution: {integrity: sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==}
+ engines: {node: ^22.13||^24||>=26}
+ hasBin: true
+
+ wcwidth@1.0.1:
+ resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ wicked-good-xpath@1.3.0:
+ resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==}
+
+ ws@8.21.1:
+ resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+snapshots:
+
+ '@aws-sdk/checksums@3.1000.18':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/client-s3@3.1089.0':
+ dependencies:
+ '@aws-sdk/checksums': 3.1000.18
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/credential-provider-node': 3.972.70
+ '@aws-sdk/middleware-sdk-s3': 3.972.64
+ '@aws-sdk/signature-v4-multi-region': 3.996.41
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/fetch-http-handler': 5.6.7
+ '@smithy/node-http-handler': 4.9.7
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/core@3.975.3':
+ dependencies:
+ '@aws-sdk/types': 3.974.2
+ '@aws-sdk/xml-builder': 3.972.36
+ '@aws/lambda-invoke-store': 0.3.0
+ '@smithy/core': 3.29.5
+ '@smithy/signature-v4': 5.6.6
+ '@smithy/types': 4.16.1
+ bowser: 2.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-env@3.972.59':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-http@3.972.61':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/fetch-http-handler': 5.6.7
+ '@smithy/node-http-handler': 4.9.7
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-ini@3.973.4':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/credential-provider-env': 3.972.59
+ '@aws-sdk/credential-provider-http': 3.972.61
+ '@aws-sdk/credential-provider-login': 3.972.66
+ '@aws-sdk/credential-provider-process': 3.972.59
+ '@aws-sdk/credential-provider-sso': 3.973.3
+ '@aws-sdk/credential-provider-web-identity': 3.972.65
+ '@aws-sdk/nested-clients': 3.997.33
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/credential-provider-imds': 4.4.10
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-login@3.972.66':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/nested-clients': 3.997.33
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-node@3.972.70':
+ dependencies:
+ '@aws-sdk/credential-provider-env': 3.972.59
+ '@aws-sdk/credential-provider-http': 3.972.61
+ '@aws-sdk/credential-provider-ini': 3.973.4
+ '@aws-sdk/credential-provider-process': 3.972.59
+ '@aws-sdk/credential-provider-sso': 3.973.3
+ '@aws-sdk/credential-provider-web-identity': 3.972.65
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/credential-provider-imds': 4.4.10
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-process@3.972.59':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-sso@3.973.3':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/nested-clients': 3.997.33
+ '@aws-sdk/token-providers': 3.1088.0
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-web-identity@3.972.65':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/nested-clients': 3.997.33
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-sdk-s3@3.972.64':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/signature-v4-multi-region': 3.996.41
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/nested-clients@3.997.33':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/signature-v4-multi-region': 3.996.41
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/fetch-http-handler': 5.6.7
+ '@smithy/node-http-handler': 4.9.7
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/signature-v4-multi-region@3.996.41':
+ dependencies:
+ '@aws-sdk/types': 3.974.2
+ '@smithy/signature-v4': 5.6.6
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/token-providers@3.1088.0':
+ dependencies:
+ '@aws-sdk/core': 3.975.3
+ '@aws-sdk/nested-clients': 3.997.33
+ '@aws-sdk/types': 3.974.2
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/types@3.974.2':
+ dependencies:
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws-sdk/xml-builder@3.972.36':
+ dependencies:
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@aws/lambda-invoke-store@0.3.0': {}
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@biomejs/biome@2.5.4':
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 2.5.4
+ '@biomejs/cli-darwin-x64': 2.5.4
+ '@biomejs/cli-linux-arm64': 2.5.4
+ '@biomejs/cli-linux-arm64-musl': 2.5.4
+ '@biomejs/cli-linux-x64': 2.5.4
+ '@biomejs/cli-linux-x64-musl': 2.5.4
+ '@biomejs/cli-win32-arm64': 2.5.4
+ '@biomejs/cli-win32-x64': 2.5.4
+
+ '@biomejs/cli-darwin-arm64@2.5.4':
+ optional: true
+
+ '@biomejs/cli-darwin-x64@2.5.4':
+ optional: true
+
+ '@biomejs/cli-linux-arm64-musl@2.5.4':
+ optional: true
+
+ '@biomejs/cli-linux-arm64@2.5.4':
+ optional: true
+
+ '@biomejs/cli-linux-x64-musl@2.5.4':
+ optional: true
+
+ '@biomejs/cli-linux-x64@2.5.4':
+ optional: true
+
+ '@biomejs/cli-win32-arm64@2.5.4':
+ optional: true
+
+ '@biomejs/cli-win32-x64@2.5.4':
+ optional: true
+
+ '@dependents/detective-less@5.0.3':
+ dependencies:
+ gonzales-pe: 4.3.0
+ node-source-walk: 7.0.2
+
+ '@discordjs/builders@1.14.1':
+ dependencies:
+ '@discordjs/formatters': 0.6.2
+ '@discordjs/util': 1.2.0
+ '@sapphire/shapeshift': 4.0.0
+ discord-api-types: 0.38.50
+ fast-deep-equal: 3.1.3
+ ts-mixer: 6.0.4
+ tslib: 2.8.1
+
+ '@discordjs/collection@1.5.3': {}
+
+ '@discordjs/collection@2.1.1': {}
+
+ '@discordjs/formatters@0.6.2':
+ dependencies:
+ discord-api-types: 0.38.50
+
+ '@discordjs/rest@2.6.2':
+ dependencies:
+ '@discordjs/collection': 2.1.1
+ '@discordjs/util': 1.2.0
+ '@sapphire/async-queue': 1.5.5
+ '@sapphire/snowflake': 3.5.5
+ '@vladfrangu/async_event_emitter': 2.4.7
+ discord-api-types: 0.38.50
+ magic-bytes.js: 1.13.0
+ tslib: 2.8.1
+ undici: 6.27.0
+
+ '@discordjs/util@1.2.0':
+ dependencies:
+ discord-api-types: 0.38.50
+
+ '@discordjs/ws@1.2.3':
+ dependencies:
+ '@discordjs/collection': 2.1.1
+ '@discordjs/rest': 2.6.2
+ '@discordjs/util': 1.2.0
+ '@sapphire/async-queue': 1.5.5
+ '@types/ws': 8.18.1
+ '@vladfrangu/async_event_emitter': 2.4.7
+ discord-api-types: 0.38.50
+ tslib: 2.8.1
+ ws: 8.21.1
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@discoveryjs/json-ext@1.1.0': {}
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild/aix-ppc64@0.28.1':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/android-arm@0.28.1':
+ optional: true
+
+ '@esbuild/android-x64@0.28.1':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.1':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.1':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.1':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.1':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.1':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.1':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.1':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.1':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.1':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.1':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.1':
+ optional: true
+
+ '@img/colour@1.1.0': {}
+
+ '@img/sharp-darwin-arm64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.3.2
+ optional: true
+
+ '@img/sharp-darwin-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.3.2
+ optional: true
+
+ '@img/sharp-freebsd-wasm32@0.35.3':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.3
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.3.2
+ optional: true
+
+ '@img/sharp-linux-arm@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.3.2
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.3.2
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.3.2
+ optional: true
+
+ '@img/sharp-linux-s390x@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.3.2
+ optional: true
+
+ '@img/sharp-linux-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.3.2
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+ optional: true
+
+ '@img/sharp-wasm32@0.35.3':
+ dependencies:
+ '@emnapi/runtime': 1.11.2
+ optional: true
+
+ '@img/sharp-webcontainers-wasm32@0.35.3':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.3
+ optional: true
+
+ '@img/sharp-win32-arm64@0.35.3':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.35.3':
+ optional: true
+
+ '@img/sharp-win32-x64@0.35.3':
+ optional: true
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@mathjax/mathjax-newcm-font@4.1.3': {}
+
+ '@mathjax/src@4.1.3':
+ dependencies:
+ '@mathjax/mathjax-newcm-font': 4.1.3
+ mhchemparser: 4.2.1
+ mj-context-menu: 1.0.0
+ speech-rule-engine: 5.0.0-rc.4
+
+ '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.3
+ optional: true
+
+ '@oxc-project/types@0.139.0': {}
+
+ '@pinojs/redact@0.4.0': {}
+
+ '@rolldown/binding-android-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.1.5':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.1.5':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.1.5':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@sapphire/async-queue@1.5.5': {}
+
+ '@sapphire/shapeshift@4.0.0':
+ dependencies:
+ fast-deep-equal: 3.1.3
+ lodash: 4.18.1
+
+ '@sapphire/snowflake@3.5.5': {}
+
+ '@smithy/core@3.29.5':
+ dependencies:
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@smithy/credential-provider-imds@4.4.10':
+ dependencies:
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@smithy/fetch-http-handler@5.6.7':
+ dependencies:
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@smithy/node-http-handler@4.9.7':
+ dependencies:
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@smithy/signature-v4@5.6.6':
+ dependencies:
+ '@smithy/core': 3.29.5
+ '@smithy/types': 4.16.1
+ tslib: 2.8.1
+
+ '@smithy/types@4.16.1':
+ dependencies:
+ tslib: 2.8.1
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@ts-graphviz/adapter@2.0.6':
+ dependencies:
+ '@ts-graphviz/common': 2.1.5
+
+ '@ts-graphviz/ast@2.0.7':
+ dependencies:
+ '@ts-graphviz/common': 2.1.5
+
+ '@ts-graphviz/common@2.1.5': {}
+
+ '@ts-graphviz/core@2.0.7':
+ dependencies:
+ '@ts-graphviz/ast': 2.0.7
+ '@ts-graphviz/common': 2.1.5
+
+ '@tybys/wasm-util@0.10.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/node@24.13.3':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/ws@8.18.1':
+ dependencies:
+ '@types/node': 24.13.3
+
+ '@typescript-eslint/project-service@8.64.0(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.64.0
+ debug: 4.4.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
+ '@typescript-eslint/types@8.64.0': {}
+
+ '@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.64.0(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.64.0
+ '@typescript-eslint/visitor-keys': 8.64.0
+ debug: 4.4.3
+ minimatch: 10.2.5
+ semver: 7.8.5
+ tinyglobby: 0.2.17
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.64.0':
+ dependencies:
+ '@typescript-eslint/types': 8.64.0
+ eslint-visitor-keys: 5.0.1
+
+ '@vitest/expect@4.1.10':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ chai: 6.2.2
+ tinyrainbow: 3.1.0
+
+ '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1))':
+ dependencies:
+ '@vitest/spy': 4.1.10
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1)
+
+ '@vitest/pretty-format@4.1.10':
+ dependencies:
+ tinyrainbow: 3.1.0
+
+ '@vitest/runner@4.1.10':
+ dependencies:
+ '@vitest/utils': 4.1.10
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/utils': 4.1.10
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.10': {}
+
+ '@vitest/utils@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.0
+
+ '@vladfrangu/async_event_emitter@2.4.7': {}
+
+ '@vue/compiler-core@3.5.40':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@vue/shared': 3.5.40
+ entities: 7.0.1
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.40':
+ dependencies:
+ '@vue/compiler-core': 3.5.40
+ '@vue/shared': 3.5.40
+
+ '@vue/compiler-sfc@3.5.40':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@vue/compiler-core': 3.5.40
+ '@vue/compiler-dom': 3.5.40
+ '@vue/compiler-ssr': 3.5.40
+ '@vue/shared': 3.5.40
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.19
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.40':
+ dependencies:
+ '@vue/compiler-dom': 3.5.40
+ '@vue/shared': 3.5.40
+
+ '@vue/shared@3.5.40': {}
+
+ '@xmldom/xmldom@0.9.10': {}
+
+ acorn-jsx-walk@2.0.0: {}
+
+ acorn-jsx@5.3.2(acorn@8.17.0):
+ dependencies:
+ acorn: 8.17.0
+
+ acorn-loose@8.5.2:
+ dependencies:
+ acorn: 8.17.0
+
+ acorn-walk@8.3.5:
+ dependencies:
+ acorn: 8.17.0
+
+ acorn@8.17.0: {}
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ any-promise@1.3.0: {}
+
+ app-module-path@2.2.0: {}
+
+ assertion-error@2.0.1: {}
+
+ ast-module-types@6.0.2: {}
+
+ atomic-sleep@1.0.0: {}
+
+ balanced-match@4.0.4: {}
+
+ base64-js@1.5.1: {}
+
+ bl@4.1.0:
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
+ bowser@2.14.1: {}
+
+ brace-expansion@5.0.7:
+ dependencies:
+ balanced-match: 4.0.4
+
+ buffer@5.7.1:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ chai@6.2.2: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ cli-cursor@3.1.0:
+ dependencies:
+ restore-cursor: 3.1.0
+
+ cli-spinners@2.9.2: {}
+
+ clone@1.0.4: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ commander@12.1.0: {}
+
+ commander@14.0.3: {}
+
+ commander@15.0.0: {}
+
+ commander@7.2.0: {}
+
+ commondir@1.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-extend@0.6.0: {}
+
+ defaults@1.0.4:
+ dependencies:
+ clone: 1.0.4
+
+ dependency-cruiser@18.1.0:
+ dependencies:
+ acorn: 8.17.0
+ acorn-jsx: 5.3.2(acorn@8.17.0)
+ acorn-jsx-walk: 2.0.0
+ acorn-loose: 8.5.2
+ acorn-walk: 8.3.5
+ commander: 15.0.0
+ enhanced-resolve: 5.24.2
+ ignore: 7.0.6
+ interpret: 3.1.1
+ is-installed-globally: 1.0.0
+ json5: 2.2.3
+ picomatch: 4.0.5
+ prompts: 2.4.2
+ rechoir: 0.8.0
+ safe-regex: 2.1.1
+ semver: 7.8.5
+ tsconfig-paths-webpack-plugin: 4.2.0
+ watskeburt: 6.0.0
+
+ dependency-tree@11.5.0:
+ dependencies:
+ '@discoveryjs/json-ext': 1.1.0
+ commander: 12.1.0
+ filing-cabinet: 5.5.1
+ precinct: 12.3.2
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ detect-libc@2.1.2: {}
+
+ detective-amd@6.1.0:
+ dependencies:
+ ast-module-types: 6.0.2
+ escodegen: 2.1.0
+ get-amd-module-type: 6.0.2
+ node-source-walk: 7.0.2
+
+ detective-cjs@6.1.1:
+ dependencies:
+ ast-module-types: 6.0.2
+ node-source-walk: 7.0.2
+
+ detective-es6@5.0.2:
+ dependencies:
+ node-source-walk: 7.0.2
+
+ detective-postcss@8.0.4(postcss@8.5.19):
+ dependencies:
+ is-url-superb: 4.0.0
+ postcss: 8.5.19
+ postcss-values-parser: 6.0.2(postcss@8.5.19)
+
+ detective-sass@6.0.2:
+ dependencies:
+ gonzales-pe: 4.3.0
+ node-source-walk: 7.0.2
+
+ detective-scss@5.0.2:
+ dependencies:
+ gonzales-pe: 4.3.0
+ node-source-walk: 7.0.2
+
+ detective-stylus@5.0.1: {}
+
+ detective-typescript@14.1.2(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3)
+ ast-module-types: 6.0.2
+ node-source-walk: 7.0.2
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ detective-vue2@2.3.0(typescript@5.9.3):
+ dependencies:
+ '@dependents/detective-less': 5.0.3
+ '@vue/compiler-sfc': 3.5.40
+ detective-es6: 5.0.2
+ detective-sass: 6.0.2
+ detective-scss: 5.0.2
+ detective-stylus: 5.0.1
+ detective-typescript: 14.1.2(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ discord-api-types@0.38.50: {}
+
+ discord.js@14.27.0:
+ dependencies:
+ '@discordjs/builders': 1.14.1
+ '@discordjs/collection': 1.5.3
+ '@discordjs/formatters': 0.6.2
+ '@discordjs/rest': 2.6.2
+ '@discordjs/util': 1.2.0
+ '@discordjs/ws': 1.2.3
+ '@sapphire/snowflake': 3.5.5
+ discord-api-types: 0.38.50
+ fast-deep-equal: 3.1.3
+ lodash.snakecase: 4.1.1
+ magic-bytes.js: 1.13.0
+ tslib: 2.8.1
+ undici: 6.27.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ dotenv@17.4.2: {}
+
+ enhanced-resolve@5.24.2:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ entities@7.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-module-lexer@2.3.1: {}
+
+ esbuild@0.28.1:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.1
+ '@esbuild/android-arm': 0.28.1
+ '@esbuild/android-arm64': 0.28.1
+ '@esbuild/android-x64': 0.28.1
+ '@esbuild/darwin-arm64': 0.28.1
+ '@esbuild/darwin-x64': 0.28.1
+ '@esbuild/freebsd-arm64': 0.28.1
+ '@esbuild/freebsd-x64': 0.28.1
+ '@esbuild/linux-arm': 0.28.1
+ '@esbuild/linux-arm64': 0.28.1
+ '@esbuild/linux-ia32': 0.28.1
+ '@esbuild/linux-loong64': 0.28.1
+ '@esbuild/linux-mips64el': 0.28.1
+ '@esbuild/linux-ppc64': 0.28.1
+ '@esbuild/linux-riscv64': 0.28.1
+ '@esbuild/linux-s390x': 0.28.1
+ '@esbuild/linux-x64': 0.28.1
+ '@esbuild/netbsd-arm64': 0.28.1
+ '@esbuild/netbsd-x64': 0.28.1
+ '@esbuild/openbsd-arm64': 0.28.1
+ '@esbuild/openbsd-x64': 0.28.1
+ '@esbuild/openharmony-arm64': 0.28.1
+ '@esbuild/sunos-x64': 0.28.1
+ '@esbuild/win32-arm64': 0.28.1
+ '@esbuild/win32-ia32': 0.28.1
+ '@esbuild/win32-x64': 0.28.1
+
+ escodegen@2.1.0:
+ dependencies:
+ esprima: 4.0.1
+ estraverse: 5.3.0
+ esutils: 2.0.3
+ optionalDependencies:
+ source-map: 0.6.1
+
+ eslint-visitor-keys@5.0.1: {}
+
+ esprima@4.0.1: {}
+
+ estraverse@5.3.0: {}
+
+ estree-walker@2.0.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ esutils@2.0.3: {}
+
+ expect-type@1.4.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ filing-cabinet@5.5.1:
+ dependencies:
+ app-module-path: 2.2.0
+ commander: 12.1.0
+ enhanced-resolve: 5.24.2
+ module-definition: 6.0.2
+ module-lookup-amd: 9.1.3
+ resolve: 1.22.12
+ resolve-dependency-path: 4.0.1
+ sass-lookup: 6.1.2
+ stylus-lookup: 6.1.2
+ tsconfig-paths: 4.2.0
+ typescript: 5.9.3
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-amd-module-type@6.0.2:
+ dependencies:
+ ast-module-types: 6.0.2
+ node-source-walk: 7.0.2
+
+ get-own-enumerable-property-symbols@3.0.2: {}
+
+ glob@13.0.6:
+ dependencies:
+ minimatch: 10.2.5
+ minipass: 7.1.3
+ path-scurry: 2.0.2
+
+ global-directory@4.0.1:
+ dependencies:
+ ini: 4.1.1
+
+ gonzales-pe@4.3.0:
+ dependencies:
+ minimist: 1.2.8
+
+ graceful-fs@4.2.11: {}
+
+ has-flag@4.0.0: {}
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
+ i18next@26.3.6(typescript@6.0.3):
+ optionalDependencies:
+ typescript: 6.0.3
+
+ ieee754@1.2.1: {}
+
+ ignore@7.0.6: {}
+
+ inherits@2.0.4: {}
+
+ ini@1.3.8: {}
+
+ ini@4.1.1: {}
+
+ interpret@3.1.1: {}
+
+ is-core-module@2.16.2:
+ dependencies:
+ hasown: 2.0.4
+
+ is-installed-globally@1.0.0:
+ dependencies:
+ global-directory: 4.0.1
+ is-path-inside: 4.0.0
+
+ is-interactive@1.0.0: {}
+
+ is-obj@1.0.1: {}
+
+ is-path-inside@4.0.0: {}
+
+ is-regexp@1.0.0: {}
+
+ is-unicode-supported@0.1.0: {}
+
+ is-url-superb@4.0.0: {}
+
+ iso-639-1@3.1.6: {}
+
+ json5@2.2.3: {}
+
+ kleur@3.0.3: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ 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
+
+ lodash.snakecase@4.1.1: {}
+
+ lodash@4.18.1: {}
+
+ log-symbols@4.1.0:
+ dependencies:
+ chalk: 4.1.2
+ is-unicode-supported: 0.1.0
+
+ lru-cache@11.5.2: {}
+
+ madge@8.0.0(typescript@6.0.3):
+ dependencies:
+ chalk: 4.1.2
+ commander: 7.2.0
+ commondir: 1.0.1
+ debug: 4.4.3
+ dependency-tree: 11.5.0
+ ora: 5.4.1
+ pluralize: 8.0.0
+ pretty-ms: 7.0.1
+ rc: 1.2.8
+ stream-to-array: 2.3.0
+ ts-graphviz: 2.1.6
+ walkdir: 0.4.1
+ optionalDependencies:
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ magic-bytes.js@1.13.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ mhchemparser@4.2.1: {}
+
+ mimic-fn@2.1.0: {}
+
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.7
+
+ minimist@1.2.8: {}
+
+ minipass@7.1.3: {}
+
+ mj-context-menu@1.0.0:
+ dependencies:
+ rimraf: 6.1.3
+
+ module-definition@6.0.2:
+ dependencies:
+ ast-module-types: 6.0.2
+ node-source-walk: 7.0.2
+
+ module-lookup-amd@9.1.3:
+ dependencies:
+ commander: 12.1.0
+ requirejs: 2.3.8
+ requirejs-config-file: 4.0.0
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.16: {}
+
+ node-source-walk@7.0.2:
+ dependencies:
+ '@babel/parser': 7.29.7
+
+ obug@2.1.4: {}
+
+ on-exit-leak-free@2.1.2: {}
+
+ onetime@5.1.2:
+ dependencies:
+ mimic-fn: 2.1.0
+
+ openai@6.48.0(@aws-sdk/credential-provider-node@3.972.70)(@smithy/signature-v4@5.6.6)(ws@8.21.1):
+ optionalDependencies:
+ '@aws-sdk/credential-provider-node': 3.972.70
+ '@smithy/signature-v4': 5.6.6
+ ws: 8.21.1
+
+ ora@5.4.1:
+ dependencies:
+ bl: 4.1.0
+ chalk: 4.1.2
+ cli-cursor: 3.1.0
+ cli-spinners: 2.9.2
+ is-interactive: 1.0.0
+ is-unicode-supported: 0.1.0
+ log-symbols: 4.1.0
+ strip-ansi: 6.0.1
+ wcwidth: 1.0.1
+
+ package-json-from-dist@1.0.1: {}
+
+ parse-ms@2.1.0: {}
+
+ path-parse@1.0.7: {}
+
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.5.2
+ minipass: 7.1.3
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ pino-abstract-transport@3.0.0:
+ dependencies:
+ split2: 4.2.0
+
+ pino-std-serializers@7.1.0: {}
+
+ pino@10.3.1:
+ dependencies:
+ '@pinojs/redact': 0.4.0
+ atomic-sleep: 1.0.0
+ on-exit-leak-free: 2.1.2
+ pino-abstract-transport: 3.0.0
+ pino-std-serializers: 7.1.0
+ process-warning: 5.0.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.2.0
+ safe-stable-stringify: 2.5.0
+ sonic-boom: 4.2.1
+ thread-stream: 4.2.0
+
+ pluralize@8.0.0: {}
+
+ postcss-values-parser@6.0.2(postcss@8.5.19):
+ dependencies:
+ color-name: 1.1.4
+ is-url-superb: 4.0.0
+ postcss: 8.5.19
+ quote-unquote: 1.0.0
+
+ postcss@8.5.19:
+ dependencies:
+ nanoid: 3.3.16
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ precinct@12.3.2:
+ dependencies:
+ '@dependents/detective-less': 5.0.3
+ commander: 12.1.0
+ detective-amd: 6.1.0
+ detective-cjs: 6.1.1
+ detective-es6: 5.0.2
+ detective-postcss: 8.0.4(postcss@8.5.19)
+ detective-sass: 6.0.2
+ detective-scss: 5.0.2
+ detective-stylus: 5.0.1
+ detective-typescript: 14.1.2(typescript@5.9.3)
+ detective-vue2: 2.3.0(typescript@5.9.3)
+ module-definition: 6.0.2
+ node-source-walk: 7.0.2
+ postcss: 8.5.19
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ pretty-ms@7.0.1:
+ dependencies:
+ parse-ms: 2.1.0
+
+ process-warning@5.0.0: {}
+
+ prompts@2.4.2:
+ dependencies:
+ kleur: 3.0.3
+ sisteransi: 1.0.5
+
+ quick-format-unescaped@4.0.4: {}
+
+ quote-unquote@1.0.0: {}
+
+ rc@1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ real-require@0.2.0: {}
+
+ real-require@1.0.0: {}
+
+ rechoir@0.8.0:
+ dependencies:
+ resolve: 1.22.12
+
+ regexp-tree@0.1.27: {}
+
+ requirejs-config-file@4.0.0:
+ dependencies:
+ esprima: 4.0.1
+ stringify-object: 3.3.0
+
+ requirejs@2.3.8: {}
+
+ resolve-dependency-path@4.0.1: {}
+
+ resolve@1.22.12:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.2
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ restore-cursor@3.1.0:
+ dependencies:
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+
+ rimraf@6.1.3:
+ dependencies:
+ glob: 13.0.6
+ package-json-from-dist: 1.0.1
+
+ rolldown@1.1.5:
+ dependencies:
+ '@oxc-project/types': 0.139.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.1.5
+ '@rolldown/binding-darwin-arm64': 1.1.5
+ '@rolldown/binding-darwin-x64': 1.1.5
+ '@rolldown/binding-freebsd-x64': 1.1.5
+ '@rolldown/binding-linux-arm-gnueabihf': 1.1.5
+ '@rolldown/binding-linux-arm64-gnu': 1.1.5
+ '@rolldown/binding-linux-arm64-musl': 1.1.5
+ '@rolldown/binding-linux-ppc64-gnu': 1.1.5
+ '@rolldown/binding-linux-s390x-gnu': 1.1.5
+ '@rolldown/binding-linux-x64-gnu': 1.1.5
+ '@rolldown/binding-linux-x64-musl': 1.1.5
+ '@rolldown/binding-openharmony-arm64': 1.1.5
+ '@rolldown/binding-wasm32-wasi': 1.1.5
+ '@rolldown/binding-win32-arm64-msvc': 1.1.5
+ '@rolldown/binding-win32-x64-msvc': 1.1.5
+
+ safe-buffer@5.2.1: {}
+
+ safe-regex@2.1.1:
+ dependencies:
+ regexp-tree: 0.1.27
+
+ safe-stable-stringify@2.5.0: {}
+
+ sass-lookup@6.1.2:
+ dependencies:
+ commander: 12.1.0
+ enhanced-resolve: 5.24.2
+
+ semver@7.8.5: {}
+
+ sharp@0.35.3(@types/node@24.13.3):
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.8.5
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.35.3
+ '@img/sharp-darwin-x64': 0.35.3
+ '@img/sharp-freebsd-wasm32': 0.35.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.2
+ '@img/sharp-libvips-darwin-x64': 1.3.2
+ '@img/sharp-libvips-linux-arm': 1.3.2
+ '@img/sharp-libvips-linux-arm64': 1.3.2
+ '@img/sharp-libvips-linux-ppc64': 1.3.2
+ '@img/sharp-libvips-linux-riscv64': 1.3.2
+ '@img/sharp-libvips-linux-s390x': 1.3.2
+ '@img/sharp-libvips-linux-x64': 1.3.2
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+ '@img/sharp-linux-arm': 0.35.3
+ '@img/sharp-linux-arm64': 0.35.3
+ '@img/sharp-linux-ppc64': 0.35.3
+ '@img/sharp-linux-riscv64': 0.35.3
+ '@img/sharp-linux-s390x': 0.35.3
+ '@img/sharp-linux-x64': 0.35.3
+ '@img/sharp-linuxmusl-arm64': 0.35.3
+ '@img/sharp-linuxmusl-x64': 0.35.3
+ '@img/sharp-webcontainers-wasm32': 0.35.3
+ '@img/sharp-win32-arm64': 0.35.3
+ '@img/sharp-win32-ia32': 0.35.3
+ '@img/sharp-win32-x64': 0.35.3
+ '@types/node': 24.13.3
+
+ siginfo@2.0.0: {}
+
+ signal-exit@3.0.7: {}
+
+ sisteransi@1.0.5: {}
+
+ sonic-boom@4.2.1:
+ dependencies:
+ atomic-sleep: 1.0.0
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.6.1:
+ optional: true
+
+ speech-rule-engine@5.0.0-rc.4:
+ dependencies:
+ '@xmldom/xmldom': 0.9.10
+ commander: 14.0.3
+ wicked-good-xpath: 1.3.0
+
+ split2@4.2.0: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.2.0: {}
+
+ stream-to-array@2.3.0:
+ dependencies:
+ any-promise: 1.3.0
+
+ string_decoder@1.3.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ stringify-object@3.3.0:
+ dependencies:
+ get-own-enumerable-property-symbols: 3.0.2
+ is-obj: 1.0.1
+ is-regexp: 1.0.0
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-bom@3.0.0: {}
+
+ strip-json-comments@2.0.1: {}
+
+ stylus-lookup@6.1.2:
+ dependencies:
+ commander: 12.1.0
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ tapable@2.3.3: {}
+
+ thread-stream@4.2.0:
+ dependencies:
+ real-require: 1.0.0
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.2.4: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ tinyrainbow@3.1.0: {}
+
+ ts-api-utils@2.5.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
+ ts-graphviz@2.1.6:
+ dependencies:
+ '@ts-graphviz/adapter': 2.0.6
+ '@ts-graphviz/ast': 2.0.7
+ '@ts-graphviz/common': 2.1.5
+ '@ts-graphviz/core': 2.0.7
+
+ ts-mixer@6.0.4: {}
+
+ tsconfig-paths-webpack-plugin@4.2.0:
+ dependencies:
+ chalk: 4.1.2
+ enhanced-resolve: 5.24.2
+ tapable: 2.3.3
+ tsconfig-paths: 4.2.0
+
+ tsconfig-paths@4.2.0:
+ dependencies:
+ json5: 2.2.3
+ minimist: 1.2.8
+ strip-bom: 3.0.0
+
+ tslib@2.8.1: {}
+
+ tsx@4.23.1:
+ dependencies:
+ esbuild: 0.28.1
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ typescript@5.9.3: {}
+
+ typescript@6.0.3: {}
+
+ undici-types@7.18.2: {}
+
+ undici@6.27.0: {}
+
+ util-deprecate@1.0.2: {}
+
+ vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.5
+ postcss: 8.5.19
+ rolldown: 1.1.5
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.13.3
+ esbuild: 0.28.1
+ fsevents: 2.3.3
+ tsx: 4.23.1
+
+ vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1)):
+ dependencies:
+ '@vitest/expect': 4.1.10
+ '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1))
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/runner': 4.1.10
+ '@vitest/snapshot': 4.1.10
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ es-module-lexer: 2.3.1
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ obug: 2.1.4
+ pathe: 2.0.3
+ picomatch: 4.0.5
+ std-env: 4.2.0
+ tinybench: 2.9.0
+ tinyexec: 1.2.4
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.0
+ vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 24.13.3
+ transitivePeerDependencies:
+ - msw
+
+ walkdir@0.4.1: {}
+
+ watskeburt@6.0.0: {}
+
+ wcwidth@1.0.1:
+ dependencies:
+ defaults: 1.0.4
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ wicked-good-xpath@1.3.0: {}
+
+ ws@8.21.1: {}
diff --git a/discord/pnpm-workspace.yaml b/discord/pnpm-workspace.yaml
new file mode 100644
index 0000000..dbb26c8
--- /dev/null
+++ b/discord/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+allowBuilds:
+ esbuild: true
+ sharp: true
diff --git a/discord/pyproject.toml b/discord/pyproject.toml
deleted file mode 100644
index 477d7c9..0000000
--- a/discord/pyproject.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[project]
-name = "coderunbot-discord"
-version = "0.1.0"
-description = "Discord bots for coderunbot and gaato-bot"
-requires-python = ">=3.14"
-dependencies = [
- "aiohttp",
- "google-api-python-client",
- "google-auth-oauthlib",
- "iso639-lang",
- "oci",
- "openai",
- "py-cord[voice]>=2.6",
- "python-dotenv",
-]
-
-[tool.uv]
-package = false
diff --git a/discord/src/config.ts b/discord/src/config.ts
new file mode 100644
index 0000000..5f44aae
--- /dev/null
+++ b/discord/src/config.ts
@@ -0,0 +1,31 @@
+/**
+ * Defines bot profiles used by the composition root to select features and defaults.
+ */
+import type { BotName } from "./env.js";
+import type { FeatureId } from "./types.js";
+
+export interface BotProfile {
+ readonly name: BotName;
+ readonly prefix: string;
+ readonly defaultLocale: "en" | "ja";
+ readonly features: readonly FeatureId[];
+}
+
+export const BOT_PROFILES = {
+ coderunbot: {
+ name: "coderunbot",
+ prefix: "]",
+ defaultLocale: "en",
+ features: ["tex", "code", "privacy"],
+ },
+ "gaato-bot": {
+ name: "gaato-bot",
+ prefix: ")",
+ defaultLocale: "ja",
+ features: ["tex", "code", "privacy", "wolfram", "misc", "translate"],
+ },
+} as const satisfies Record;
+
+export function getBotProfile(name: BotName): BotProfile {
+ return BOT_PROFILES[name];
+}
diff --git a/discord/src/env.test.ts b/discord/src/env.test.ts
new file mode 100644
index 0000000..8e61927
--- /dev/null
+++ b/discord/src/env.test.ts
@@ -0,0 +1,89 @@
+/**
+ * Covers environment parsing at the composition boundary, including local and S3 state modes.
+ */
+import { describe, expect, it } from "vitest";
+import { loadEnv } from "./env.js";
+
+const baseEnv = { CODERUNBOT_TOKEN: "token" };
+
+describe("loadEnv state configuration", () => {
+ it("uses current default OpenAI models", () => {
+ expect(loadEnv(baseEnv)).toMatchObject({
+ openAIChatModel: "gpt-5.2",
+ openAIChatModelLite: "gpt-5-mini",
+ openAITranslateModel: "gpt-5-mini",
+ });
+ });
+
+ it("loads optional gaato feature credentials", () => {
+ expect(
+ loadEnv({
+ GAATO_BOT: "1",
+ GAATO_BOT_TOKEN: "token",
+ WOLFRAM_APPID: "wolfram",
+ OPENAI_API_KEY: "openai",
+ }),
+ ).toMatchObject({
+ botName: "gaato-bot",
+ wolframAppId: "wolfram",
+ openAIApiKey: "openai",
+ });
+ });
+
+ it("uses the local state file by default", () => {
+ expect(loadEnv(baseEnv).state).toEqual({
+ backend: "local",
+ filePath: "data/opt-out-users.txt",
+ usageStatsFilePath: "data/usage-stats-coderunbot.json",
+ });
+ });
+
+ it("builds a normalized S3 object key", () => {
+ expect(
+ loadEnv({
+ ...baseEnv,
+ BOT_STATE_BACKEND: "s3",
+ BOT_STATE_PREFIX: "/bots/coderunbot/",
+ S3_ENDPOINT: "https://example.invalid",
+ S3_REGION: "test-region-1",
+ S3_BUCKET: "state",
+ S3_ACCESS_KEY_ID: "access-key",
+ S3_SECRET_ACCESS_KEY: "secret-key",
+ }).state,
+ ).toEqual({
+ backend: "s3",
+ endpoint: "https://example.invalid",
+ region: "test-region-1",
+ bucket: "state",
+ key: "bots/coderunbot/opt-out-users.txt",
+ usageStatsKey: "bots/coderunbot/usage-stats-coderunbot.json",
+ accessKeyId: "access-key",
+ secretAccessKey: "secret-key",
+ });
+ });
+
+ it("uses a separate usage statistics object for gaato bot", () => {
+ expect(
+ loadEnv({
+ GAATO_BOT: "1",
+ GAATO_BOT_TOKEN: "token",
+ }).state,
+ ).toEqual({
+ backend: "local",
+ filePath: "data/opt-out-users.txt",
+ usageStatsFilePath: "data/usage-stats-gaato-bot.json",
+ });
+ });
+
+ it("fails fast when an S3 setting is missing", () => {
+ expect(() =>
+ loadEnv({
+ ...baseEnv,
+ BOT_STATE_BACKEND: "s3",
+ S3_ENDPOINT: "https://example.invalid",
+ }),
+ ).toThrow(
+ "S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY must be set when BOT_STATE_BACKEND=s3",
+ );
+ });
+});
diff --git a/discord/src/env.ts b/discord/src/env.ts
new file mode 100644
index 0000000..b760f68
--- /dev/null
+++ b/discord/src/env.ts
@@ -0,0 +1,132 @@
+/**
+ * Loads and validates process configuration at the application's environment boundary.
+ * No other source file reads process.env, as enforced by Biome's noProcessEnv rule.
+ */
+import "dotenv/config";
+
+export type BotName = "coderunbot" | "gaato-bot";
+
+export interface LocalStateEnv {
+ readonly backend: "local";
+ readonly filePath: string;
+ readonly usageStatsFilePath: string;
+}
+
+export interface S3StateEnv {
+ readonly backend: "s3";
+ readonly endpoint: string;
+ readonly region: string;
+ readonly bucket: string;
+ readonly key: string;
+ readonly usageStatsKey: string;
+ readonly accessKeyId: string;
+ readonly secretAccessKey: string;
+}
+
+export type StateEnv = LocalStateEnv | S3StateEnv;
+
+export interface Env {
+ readonly botName: BotName;
+ readonly token: string;
+ readonly tokenEnvName: "CODERUNBOT_TOKEN" | "GAATO_BOT_TOKEN";
+ readonly logChannelId: string;
+ readonly developerId: string;
+ readonly supportServerLink: string;
+ readonly wolframAppId?: string;
+ readonly openAIApiKey?: string;
+ readonly openAIChatModel: string;
+ readonly openAIChatModelLite: string;
+ readonly openAITranslateModel: string;
+ readonly state: StateEnv;
+}
+
+const DEFAULT_LOG_CHANNEL_ID = "1118867011448078417";
+const DEFAULT_DEVELOPER_ID = "572432137035317249";
+const DEFAULT_SUPPORT_SERVER_LINK = "discord.gg/qRpYRTgvXM";
+const DEFAULT_OPENAI_CHAT_MODEL = "gpt-5.2";
+const DEFAULT_OPENAI_CHAT_MODEL_LITE = "gpt-5-mini";
+const DEFAULT_OPENAI_TRANSLATE_MODEL = "gpt-5-mini";
+
+function nonEmpty(value: string | undefined): string | undefined {
+ // Empty strings count as unset, matching the Python bot's truthiness check for GAATO_BOT.
+ return value === undefined || value.trim().length === 0 ? undefined : value;
+}
+
+function loadStateEnv(source: NodeJS.ProcessEnv, botName: BotName): StateEnv {
+ const backend = nonEmpty(source.BOT_STATE_BACKEND) ?? "local";
+ if (backend === "local") {
+ return {
+ backend,
+ filePath: "data/opt-out-users.txt",
+ usageStatsFilePath: `data/usage-stats-${botName}.json`,
+ };
+ }
+ if (backend !== "s3") {
+ throw new Error('BOT_STATE_BACKEND must be either "local" or "s3"');
+ }
+
+ const required = {
+ S3_ENDPOINT: nonEmpty(source.S3_ENDPOINT),
+ S3_REGION: nonEmpty(source.S3_REGION),
+ S3_BUCKET: nonEmpty(source.S3_BUCKET),
+ S3_ACCESS_KEY_ID: nonEmpty(source.S3_ACCESS_KEY_ID),
+ S3_SECRET_ACCESS_KEY: nonEmpty(source.S3_SECRET_ACCESS_KEY),
+ };
+ const missing = Object.entries(required)
+ .filter(([, value]) => value === undefined)
+ .map(([name]) => name);
+ if (missing.length > 0) {
+ throw new Error(
+ `${missing.join(", ")} must be set when BOT_STATE_BACKEND=s3`,
+ );
+ }
+
+ const prefix = (nonEmpty(source.BOT_STATE_PREFIX) ?? "").replace(
+ /^\/+|\/+$/gu,
+ "",
+ );
+ return {
+ backend,
+ endpoint: required.S3_ENDPOINT as string,
+ region: required.S3_REGION as string,
+ bucket: required.S3_BUCKET as string,
+ key:
+ prefix.length > 0 ? `${prefix}/opt-out-users.txt` : "opt-out-users.txt",
+ usageStatsKey:
+ prefix.length > 0
+ ? `${prefix}/usage-stats-${botName}.json`
+ : `usage-stats-${botName}.json`,
+ accessKeyId: required.S3_ACCESS_KEY_ID as string,
+ secretAccessKey: required.S3_SECRET_ACCESS_KEY as string,
+ };
+}
+
+export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env {
+ const gaatoBot = nonEmpty(source.GAATO_BOT) !== undefined;
+ const botName = gaatoBot ? "gaato-bot" : "coderunbot";
+ const tokenEnvName = gaatoBot ? "GAATO_BOT_TOKEN" : "CODERUNBOT_TOKEN";
+ const token = nonEmpty(source[tokenEnvName]);
+
+ if (token === undefined) {
+ throw new Error(`${tokenEnvName} must be set to a non-empty value`);
+ }
+
+ return {
+ botName,
+ token,
+ tokenEnvName,
+ logChannelId: nonEmpty(source.LOG_CHANNEL_ID) ?? DEFAULT_LOG_CHANNEL_ID,
+ developerId: nonEmpty(source.DEVELOPER_ID) ?? DEFAULT_DEVELOPER_ID,
+ supportServerLink:
+ nonEmpty(source.SUPPORT_SERVER_LINK) ?? DEFAULT_SUPPORT_SERVER_LINK,
+ wolframAppId: nonEmpty(source.WOLFRAM_APPID),
+ openAIApiKey: nonEmpty(source.OPENAI_API_KEY),
+ openAIChatModel:
+ nonEmpty(source.OPENAI_CHAT_MODEL) ?? DEFAULT_OPENAI_CHAT_MODEL,
+ openAIChatModelLite:
+ nonEmpty(source.OPENAI_CHAT_MODEL_LITE) ?? DEFAULT_OPENAI_CHAT_MODEL_LITE,
+ openAITranslateModel:
+ nonEmpty(source.OPENAI_TRANSLATE_MODEL) ?? DEFAULT_OPENAI_TRANSLATE_MODEL,
+ state: loadStateEnv(source, botName),
+ };
+}
diff --git a/discord/src/features/code/escape.test.ts b/discord/src/features/code/escape.test.ts
new file mode 100644
index 0000000..f2900cf
--- /dev/null
+++ b/discord/src/features/code/escape.test.ts
@@ -0,0 +1,38 @@
+/**
+ * Covers the escape context-menu reply for empty, normal, and oversized targets.
+ */
+import { describe, expect, it } from "vitest";
+import { escapeMentions, escapeReply } from "./index.js";
+
+describe("escapeMentions", () => {
+ it("neutralizes mentions with a zero-width space", () => {
+ expect(escapeMentions("@everyone")).toBe("@everyone");
+ });
+});
+
+describe("escapeReply", () => {
+ it("explains instead of failing when the target has no text", () => {
+ const reply = escapeReply("", "en");
+ expect(reply.kind).toBe("plain");
+ if (reply.kind === "plain") {
+ expect(reply.content).toBe("That message has no text to escape.");
+ }
+ expect(reply.ephemeral).toBe(true);
+ });
+
+ it("returns escaped markdown as plain ephemeral content", () => {
+ const reply = escapeReply("*hi* @everyone", "en");
+ expect(reply.kind).toBe("plain");
+ if (reply.kind === "plain") {
+ expect(reply.content).toBe("\\*hi\\* @everyone");
+ }
+ });
+
+ it("falls back to a file when escaping outgrows the content limit", () => {
+ // 1,500 asterisks escape to 3,000 characters, past the 2,000 limit.
+ const reply = escapeReply("*".repeat(1_500), "en");
+ expect(reply.kind).toBe("components-v2");
+ expect(reply.files).toHaveLength(1);
+ expect(reply.ephemeral).toBe(true);
+ });
+});
diff --git a/discord/src/features/code/index.ts b/discord/src/features/code/index.ts
new file mode 100644
index 0000000..feec1b2
--- /dev/null
+++ b/discord/src/features/code/index.ts
@@ -0,0 +1,214 @@
+/**
+ * Defines the code-execution feature and its Discord command entry points.
+ */
+import {
+ ActionRowBuilder,
+ ApplicationCommandType,
+ AttachmentBuilder,
+ ContextMenuCommandBuilder,
+ escapeMarkdown,
+ FileBuilder,
+ ModalBuilder,
+ SlashCommandBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+} from "discord.js";
+import { getFixedT } from "../../shared/i18n.js";
+import type { UsageStats } from "../../shared/usageStats.js";
+import type {
+ Feature,
+ FeatureDependencies,
+ OutgoingReply,
+} from "../../types.js";
+import {
+ parsePrefixRunInput,
+ runResultReply,
+ supportedLanguagesReply,
+ wandboxErrorReply,
+} from "./presentation.js";
+import {
+ normalizeLanguageKey,
+ WandboxClient,
+ WandboxError,
+} from "./wandbox.js";
+
+export function escapeMentions(content: string): string {
+ return content.replaceAll("@", "@\u200b");
+}
+
+const PLAIN_CONTENT_LIMIT = 2_000;
+
+export function escapeReply(content: string, locale: string): OutgoingReply {
+ const t = getFixedT(locale);
+ // Components V2 messages and attachment-only messages have no text content.
+ if (content.length === 0) {
+ return {
+ kind: "plain",
+ content: t("code.escape.noText"),
+ ephemeral: true,
+ };
+ }
+
+ const escaped = escapeMentions(escapeMarkdown(content));
+ // Escaping adds backslashes, so a long message can outgrow the content limit.
+ if (escaped.length > PLAIN_CONTENT_LIMIT) {
+ return {
+ kind: "components-v2",
+ components: [new FileBuilder().setURL("attachment://escaped.txt")],
+ files: [
+ new AttachmentBuilder(Buffer.from(escaped), { name: "escaped.txt" }),
+ ],
+ ephemeral: true,
+ };
+ }
+
+ return { kind: "plain", content: escaped, ephemeral: true };
+}
+
+export function createCodeFeature(dependencies: FeatureDependencies): Feature {
+ const wandbox = new WandboxClient({ logger: dependencies.logger });
+ const runCommand = new SlashCommandBuilder()
+ .setName("run")
+ .setDescription("Run code on Wandbox")
+ .setDescriptionLocalizations({ ja: "Wandbox でコードを実行します" });
+ runCommand.addStringOption((option) =>
+ option
+ .setName("language")
+ .setNameLocalizations({ ja: "言語" })
+ .setDescription("Programming language")
+ .setDescriptionLocalizations({ ja: "プログラミング言語" })
+ .setRequired(true)
+ .setAutocomplete(true),
+ );
+ const escapeCommand = new ContextMenuCommandBuilder()
+ .setName("escape")
+ .setNameLocalizations({ ja: "エスケープ" })
+ .setType(ApplicationCommandType.Message);
+
+ return {
+ id: "code",
+ init: () => wandbox.init(),
+ prefixCommands: [
+ {
+ name: "run",
+ async execute(_message, args, context) {
+ const input = parsePrefixRunInput(args);
+ return runCode(
+ wandbox,
+ dependencies.usageStats,
+ input.languageKey,
+ input.code,
+ "",
+ context.locale,
+ );
+ },
+ },
+ ],
+ slashCommands: [
+ {
+ data: runCommand,
+ async execute(interaction, context) {
+ if (!interaction.isChatInputCommand()) {
+ return undefined;
+ }
+ const languageKey = normalizeLanguageKey(
+ interaction.options.getString("language", true),
+ );
+ const t = getFixedT(context.locale);
+ const code = new TextInputBuilder()
+ .setCustomId("code")
+ .setLabel(t("code.modal.codeLabel"))
+ .setPlaceholder(t("code.modal.codePlaceholder"))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(true);
+ const stdin = new TextInputBuilder()
+ .setCustomId("stdin")
+ .setLabel(t("code.modal.stdinLabel"))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(false);
+ const modal = new ModalBuilder()
+ .setCustomId(`runModal:${languageKey}`)
+ .setTitle(t("code.modal.title"))
+ .addComponents(
+ new ActionRowBuilder().addComponents(code),
+ new ActionRowBuilder().addComponents(stdin),
+ );
+ // Discord accepts a modal only as the interaction's first response.
+ await interaction.showModal(modal);
+ return undefined;
+ },
+ async autocomplete(interaction) {
+ const focused = normalizeLanguageKey(
+ String(interaction.options.getFocused()),
+ );
+ await interaction.respond(
+ wandbox
+ .getLanguageChoices()
+ .filter(({ value }) => value.startsWith(focused))
+ // Discord caps autocomplete responses at 25 choices.
+ .slice(0, 25),
+ );
+ },
+ },
+ {
+ data: escapeCommand,
+ async execute(interaction, context) {
+ if (!interaction.isMessageContextMenuCommand()) {
+ return undefined;
+ }
+ return escapeReply(interaction.targetMessage.content, context.locale);
+ },
+ },
+ ],
+ modalHandlers: {
+ runModal: async (interaction, context) => {
+ const languageKey = normalizeLanguageKey(
+ interaction.customId.slice("runModal:".length),
+ );
+ const code = interaction.fields.getTextInputValue("code");
+ const stdin = interaction.fields.getTextInputValue("stdin");
+ // Acknowledge within Discord's three-second interaction window before remote work.
+ await interaction.deferReply();
+ return runCode(
+ wandbox,
+ dependencies.usageStats,
+ languageKey,
+ code,
+ stdin,
+ context.locale,
+ );
+ },
+ },
+ };
+}
+
+async function runCode(
+ wandbox: WandboxClient,
+ usageStats: Pick,
+ languageKey: string,
+ code: string,
+ stdin: string,
+ locale: string,
+): Promise {
+ const t = getFixedT(locale);
+ const resolution = await wandbox.resolveCompiler(languageKey);
+ if (resolution === undefined) {
+ return supportedLanguagesReply(wandbox.getLanguageChoices(), t);
+ }
+ usageStats.recordRunLanguage(languageKey);
+
+ try {
+ const result = await wandbox.compile(resolution.compiler, code, stdin);
+ return runResultReply(resolution.compiler, languageKey, code, result, t);
+ } catch (error) {
+ if (
+ error instanceof WandboxError &&
+ (error.code === "connection" ||
+ error.code === "http" ||
+ error.code === "non_json")
+ ) {
+ return wandboxErrorReply(error, t);
+ }
+ throw error;
+ }
+}
diff --git a/discord/src/features/code/presentation.test.ts b/discord/src/features/code/presentation.test.ts
new file mode 100644
index 0000000..a8383fd
--- /dev/null
+++ b/discord/src/features/code/presentation.test.ts
@@ -0,0 +1,93 @@
+/**
+ * Verifies code command parsing and Discord presentation payloads for the code feature.
+ */
+import { ContainerBuilder } from "discord.js";
+import { describe, expect, it } from "vitest";
+import { getFixedT } from "../../shared/i18n.js";
+import type { OutgoingReply } from "../../types.js";
+import {
+ parsePrefixRunInput,
+ runResultReply,
+ supportedLanguagesReply,
+} from "./presentation.js";
+
+function containerJson(reply: OutgoingReply) {
+ if (reply.kind !== "components-v2") {
+ throw new Error("expected a Components V2 reply");
+ }
+ const container = reply.components[0];
+ if (!(container instanceof ContainerBuilder)) {
+ throw new Error("expected a ContainerBuilder");
+ }
+ return container.toJSON();
+}
+
+describe("parsePrefixRunInput", () => {
+ it("extracts the language key and removes complete fence lines", () => {
+ expect(
+ parsePrefixRunInput(" Python ```python\nprint('hello')\n```"),
+ ).toEqual({ languageKey: "python", code: "print('hello')" });
+ });
+
+ it("does not rewrite language aliases", () => {
+ expect(parsePrefixRunInput("cpp code").languageKey).toBe("cpp");
+ });
+});
+
+describe("runResultReply", () => {
+ it("uses the status color and omits message fields", () => {
+ const reply = runResultReply(
+ "compiler",
+ "python",
+ "print(1)",
+ {
+ status: "0",
+ program_output: "1\n",
+ program_message: "duplicate output",
+ compiler_message: "ignored",
+ },
+ getFixedT("en"),
+ );
+ const json = containerJson(reply);
+
+ expect(json.accent_color).toBe(0x2ecc71);
+ expect(JSON.stringify(json)).not.toContain("duplicate output");
+ expect(JSON.stringify(json)).not.toContain("ignored");
+ expect(JSON.stringify(json)).toContain("print(1)");
+ });
+
+ it("uses a File component for output over 1000 characters", () => {
+ const reply = runResultReply(
+ "compiler",
+ "python",
+ "print(1)",
+ { status: "1", compiler_error: "x".repeat(1_001) },
+ getFixedT("en"),
+ );
+
+ expect(reply.files).toHaveLength(1);
+ expect(containerJson(reply)).toMatchObject({
+ accent_color: 0xe74c3c,
+ components: expect.arrayContaining([
+ { file: { url: "attachment://compiler_error.txt" }, type: 13 },
+ ]),
+ });
+ });
+});
+
+describe("supportedLanguagesReply", () => {
+ it("moves a long language list into a text attachment", () => {
+ const reply = supportedLanguagesReply(
+ Array.from({ length: 200 }, (_, index) => ({
+ name: `Language ${index}`,
+ value: `language-${index}`,
+ })),
+ getFixedT("en"),
+ );
+
+ expect(reply.files).toHaveLength(1);
+ expect(JSON.stringify(containerJson(reply))).toContain(
+ "supported-languages.txt",
+ );
+ });
+});
diff --git a/discord/src/features/code/presentation.ts b/discord/src/features/code/presentation.ts
new file mode 100644
index 0000000..445ab25
--- /dev/null
+++ b/discord/src/features/code/presentation.ts
@@ -0,0 +1,175 @@
+/**
+ * Translates code feature results into Discord reply values at the feature boundary.
+ */
+import {
+ type AttachmentBuilder,
+ ContainerBuilder,
+ FileBuilder,
+ TextDisplayBuilder,
+} from "discord.js";
+import type { TFunction } from "i18next";
+import {
+ FAILURE_ACCENT,
+ SUCCESS_ACCENT,
+ textComponent,
+} from "../../platform/discord/components.js";
+import type { OutgoingReply } from "../../types.js";
+import type { LanguageChoice, WandboxCompileResult } from "./wandbox.js";
+import { normalizeLanguageKey, type WandboxError } from "./wandbox.js";
+
+export interface PrefixRunInput {
+ readonly languageKey: string;
+ readonly code: string;
+}
+
+export function parsePrefixRunInput(args: string): PrefixRunInput {
+ const input = args.trimStart();
+ const separator = input.search(/\s/u);
+ if (separator === -1) {
+ return { languageKey: normalizeLanguageKey(input), code: "" };
+ }
+ return {
+ languageKey: normalizeLanguageKey(input.slice(0, separator)),
+ code: stripRunFences(input.slice(separator).trimStart()),
+ };
+}
+
+export function stripRunFences(code: string): string {
+ return code.replace(/^```.*$/gmu, "").trim();
+}
+
+function safeCodeBlock(text: string): string {
+ return text.replaceAll("```", "`\u200b``");
+}
+
+function safeFilename(field: string): string {
+ const safe = field.replace(/[^a-z0-9_-]/giu, "-");
+ return `${safe.length === 0 ? "output" : safe}.txt`;
+}
+
+function addTextSection(
+ container: ContainerBuilder,
+ files: AttachmentBuilder[],
+ heading: string,
+ text: string,
+ filename: string,
+ language = "",
+): void {
+ const rendered = textComponent(text, filename);
+ if (rendered.component instanceof FileBuilder) {
+ container
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(`### ${heading}`),
+ )
+ .addFileComponents(rendered.component);
+ } else {
+ rendered.component.setContent(
+ `### ${heading}\n\`\`\`${language}\n${safeCodeBlock(text)}\n\`\`\``,
+ );
+ container.addTextDisplayComponents(rendered.component);
+ }
+ files.push(...rendered.files);
+}
+
+export function supportedLanguagesReply(
+ choices: readonly LanguageChoice[],
+ t: TFunction,
+): OutgoingReply {
+ const container = new ContainerBuilder().setAccentColor(FAILURE_ACCENT);
+ const languages = choices.map(({ value }) => value).join(", ");
+ const rendered = textComponent(languages, "supported-languages.txt");
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `### ${t("code.supportedLanguagesHeading")}`,
+ ),
+ );
+ if (rendered.component instanceof FileBuilder) {
+ container.addFileComponents(rendered.component);
+ } else if (languages.length > 0) {
+ container.addTextDisplayComponents(rendered.component);
+ } else {
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(t("code.noLanguagesAvailable")),
+ );
+ }
+ return {
+ kind: "components-v2",
+ components: [container],
+ files: rendered.files,
+ };
+}
+
+export function runResultReply(
+ compiler: string,
+ languageKey: string,
+ code: string,
+ result: WandboxCompileResult,
+ t: TFunction,
+): OutgoingReply {
+ const succeeded = result.status === "0";
+ const container = new ContainerBuilder()
+ .setAccentColor(succeeded ? SUCCESS_ACCENT : FAILURE_ACCENT)
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `## ${t("code.resultHeading", { compiler })}`,
+ ),
+ );
+ const files: AttachmentBuilder[] = [];
+
+ for (const [field, value] of Object.entries(result)) {
+ if (
+ value === undefined ||
+ value === "" ||
+ field === "program_message" ||
+ field === "compiler_message"
+ ) {
+ continue;
+ }
+ addTextSection(container, files, field, value, safeFilename(field));
+ }
+ addTextSection(
+ container,
+ files,
+ t("code.codeHeading"),
+ code,
+ "code.txt",
+ languageKey,
+ );
+
+ return {
+ kind: "components-v2",
+ components: [container],
+ files,
+ };
+}
+
+export function wandboxErrorReply(
+ error: WandboxError,
+ t: TFunction,
+): OutgoingReply {
+ const description =
+ error.code === "non_json"
+ ? t("code.errors.nonJson")
+ : error.code === "http" && "status" in error
+ ? t("code.errors.http", { status: error.status })
+ : t("code.errors.connection");
+ const container = new ContainerBuilder()
+ .setAccentColor(FAILURE_ACCENT)
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `### ${t("code.errors.heading")}\n${description}`,
+ ),
+ );
+
+ if (error.code === "non_json" && "responseText" in error) {
+ const preview = String(error.responseText).slice(0, 750);
+ if (preview.length > 0) {
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `### ${t("code.errors.responsePreview")}\n\`\`\`\n${safeCodeBlock(preview)}\n\`\`\``,
+ ),
+ );
+ }
+ }
+ return { kind: "components-v2", components: [container] };
+}
diff --git a/discord/src/features/code/wandbox.test.ts b/discord/src/features/code/wandbox.test.ts
new file mode 100644
index 0000000..1d4a0c3
--- /dev/null
+++ b/discord/src/features/code/wandbox.test.ts
@@ -0,0 +1,197 @@
+/**
+ * Tests the discord.js-free Wandbox adapter's schema guards, selection, and cache behavior.
+ */
+import { describe, expect, it, vi } from "vitest";
+import {
+ languageChoices,
+ parseCompilerList,
+ resolveCompilerFromList,
+ WandboxClient,
+ WandboxNonJsonError,
+ WandboxSchemaError,
+} from "./wandbox.js";
+
+const silentLogger = {
+ error: vi.fn(),
+ warn: vi.fn(),
+};
+
+describe("resolveCompilerFromList", () => {
+ it("uses a pinned compiler when it is present", () => {
+ expect(
+ resolveCompilerFromList("python", [
+ { name: "cpython-head", language: "Python" },
+ { name: "cpython-3.14.0", language: "Python" },
+ ]),
+ ).toEqual({ compiler: "cpython-3.14.0", source: "pinned" });
+ });
+
+ it("uses the first stable compiler for an unpinned language", () => {
+ expect(
+ resolveCompilerFromList(
+ "brainfuck",
+ [
+ { name: "brainfuck-git", language: "Brainfuck" },
+ { name: "bfc-stable", language: "Brainfuck" },
+ ],
+ {},
+ ),
+ ).toEqual({ compiler: "bfc-stable", source: "heuristic" });
+ });
+
+ it("falls back to the first compiler if no stable compiler exists", () => {
+ expect(
+ resolveCompilerFromList(
+ "future",
+ [
+ { name: "future-head", language: "Future" },
+ { name: "future-git", language: "Future" },
+ ],
+ {},
+ ),
+ ).toEqual({ compiler: "future-head", source: "heuristic" });
+ });
+
+ it("falls back heuristically when a pinned compiler disappears", () => {
+ expect(
+ resolveCompilerFromList("python", [
+ { name: "cpython-head", language: "Python" },
+ { name: "cpython-3.15.0", language: "Python" },
+ ]),
+ ).toEqual({
+ compiler: "cpython-3.15.0",
+ source: "heuristic",
+ missingPinnedCompiler: "cpython-3.14.0",
+ });
+ });
+});
+
+describe("parseCompilerList", () => {
+ it("accepts entries containing the required fields", () => {
+ expect(
+ parseCompilerList([
+ { name: "compiler", language: "Language", extra: true },
+ ]),
+ ).toEqual([{ name: "compiler", language: "Language" }]);
+ });
+
+ it.each([
+ {},
+ [null],
+ [{ name: "compiler" }],
+ [{ name: 1, language: "Language" }],
+ ])("rejects a malformed response", (value) => {
+ expect(() => parseCompilerList(value)).toThrow(WandboxSchemaError);
+ });
+});
+
+describe("languageChoices", () => {
+ it("preserves display names and uses normalized keys without alias hacks", () => {
+ expect(
+ languageChoices([
+ { name: "gcc", language: "C++" },
+ { name: "clang", language: "C++" },
+ { name: "bfc", language: "Brain Fuck" },
+ ]),
+ ).toEqual([
+ { name: "C++", value: "c++" },
+ { name: "Brain Fuck", value: "brainfuck" },
+ ]);
+ });
+});
+
+describe("WandboxClient.compile", () => {
+ it("filters Nim compiler CC lines and sends the hint-suppression flags", async () => {
+ const fetchMock = vi.fn().mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ status: "0",
+ compiler_error: "CC: stdlib_system.nim\nwarning\nCC: main.nim\n",
+ }),
+ { status: 200 },
+ ),
+ );
+ const client = new WandboxClient({
+ logger: silentLogger,
+ fetch: fetchMock,
+ });
+
+ await expect(client.compile("nim-2.2.10", "echo 1", "")).resolves.toEqual({
+ status: "0",
+ compiler_error: "warning\n",
+ });
+ const body = JSON.parse(
+ String(fetchMock.mock.calls[0]?.[1]?.body),
+ ) as Record;
+ expect(body["compiler-option-raw"]?.split("\n")).toHaveLength(4);
+ });
+
+ it("retains the response text when JSON parsing fails", async () => {
+ const client = new WandboxClient({
+ logger: silentLogger,
+ fetch: vi
+ .fn()
+ .mockResolvedValue(
+ new Response("service unavailable", { status: 200 }),
+ ),
+ });
+
+ const error = await client
+ .compile("compiler", "code", "")
+ .catch((value: unknown) => value);
+ expect(error).toBeInstanceOf(WandboxNonJsonError);
+ expect((error as WandboxNonJsonError).responseText).toBe(
+ "service unavailable",
+ );
+ });
+});
+
+describe("WandboxClient compiler-list cache", () => {
+ it("keeps stale compilers when a TTL refresh fails", async () => {
+ let now = 0;
+ const logger = { error: vi.fn(), warn: vi.fn() };
+ const fetchMock = vi
+ .fn()
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify([{ name: "cpython-3.14.0", language: "Python" }]),
+ ),
+ )
+ .mockRejectedValueOnce(new TypeError("offline"));
+ const client = new WandboxClient({
+ logger,
+ fetch: fetchMock,
+ now: () => now,
+ cacheTtlMs: 100,
+ });
+
+ await client.init();
+ now = 101;
+
+ await expect(client.resolveCompiler("python")).resolves.toMatchObject({
+ compiler: "cpython-3.14.0",
+ });
+ expect(fetchMock).toHaveBeenCalledTimes(2);
+ expect(logger.warn).toHaveBeenCalledWith(
+ expect.objectContaining({ staleCompilerCount: 1 }),
+ expect.stringContaining("retaining stale cache"),
+ );
+ });
+
+ it("logs a schema error and starts with an empty list", async () => {
+ const logger = { error: vi.fn(), warn: vi.fn() };
+ const client = new WandboxClient({
+ logger,
+ fetch: vi
+ .fn()
+ .mockResolvedValue(new Response(JSON.stringify({ changed: true }))),
+ });
+
+ await expect(client.init()).resolves.toBeUndefined();
+ expect(client.getLanguageChoices()).toEqual([]);
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.objectContaining({ error: expect.any(WandboxSchemaError) }),
+ expect.stringContaining("expected schema"),
+ );
+ });
+});
diff --git a/discord/src/features/code/wandbox.ts b/discord/src/features/code/wandbox.ts
new file mode 100644
index 0000000..755f7ca
--- /dev/null
+++ b/discord/src/features/code/wandbox.ts
@@ -0,0 +1,368 @@
+/**
+ * Adapts Wandbox compiler discovery and execution for the code feature.
+ * The adapter is discord.js-free and unit-tested directly.
+ */
+import type { AppLogger } from "../../shared/logger.js";
+
+const LIST_URL = "https://wandbox.org/api/list.json";
+const COMPILE_URL = "https://wandbox.org/api/compile.json";
+export const WANDBOX_CACHE_TTL_MS = 60 * 60 * 1_000;
+
+const NIM_COMPILER_OPTIONS = [
+ "--hint[Processing]:off",
+ "--hint[Conf]:off",
+ "--hint[Link]:off",
+ "--hint[SuccessX]:off",
+].join("\n");
+
+// Dynamic selection broke twice: an API format changed, then a HEAD compiler won the heuristic.
+// Prefer known compiler pins and fall back to a non-development candidate when a pin disappears.
+export const PINNED_COMPILERS: Readonly> = {
+ python: "cpython-3.14.0",
+ "c++": "gcc-13.2.0",
+ c: "gcc-13.2.0-c",
+ "c#": "dotnetcore-8.0.402",
+ nim: "nim-2.2.10",
+ haskell: "ghc-9.10.1",
+ rust: "rust-1.82.0",
+ go: "go-1.23.2",
+ typescript: "typescript-5.6.2",
+ javascript: "nodejs-20.17.0",
+ java: "openjdk-jdk-21+35",
+ ruby: "ruby-3.4.9",
+ php: "php-8.3.12",
+ swift: "swift-6.0.1",
+ d: "dmd-2.109.1",
+ lua: "lua-5.4.7",
+ perl: "perl-5.40.0",
+ r: "r-4.4.1",
+ ocaml: "ocaml-5.2.0",
+};
+
+export interface WandboxCompiler {
+ readonly name: string;
+ readonly language: string;
+}
+
+export interface LanguageChoice {
+ readonly name: string;
+ readonly value: string;
+}
+
+export interface CompilerResolution {
+ readonly compiler: string;
+ readonly source: "pinned" | "heuristic";
+ readonly missingPinnedCompiler?: string;
+}
+
+export interface WandboxCompileResult {
+ readonly status?: string;
+ readonly signal?: string;
+ readonly compiler_output?: string;
+ readonly compiler_error?: string;
+ readonly compiler_message?: string;
+ readonly program_output?: string;
+ readonly program_error?: string;
+ readonly program_message?: string;
+ readonly permlink?: string;
+ readonly url?: string;
+ readonly [field: string]: string | undefined;
+}
+
+export type WandboxErrorCode = "connection" | "http" | "non_json" | "schema";
+
+export class WandboxError extends Error {
+ constructor(
+ readonly code: WandboxErrorCode,
+ message: string,
+ options?: ErrorOptions,
+ ) {
+ super(message, options);
+ this.name = "WandboxError";
+ }
+}
+
+export class WandboxHttpError extends WandboxError {
+ constructor(
+ readonly status: number,
+ readonly responseText: string,
+ ) {
+ super("http", `Wandbox returned HTTP ${status}`);
+ this.name = "WandboxHttpError";
+ }
+}
+
+export class WandboxNonJsonError extends WandboxError {
+ constructor(
+ readonly status: number,
+ readonly responseText: string,
+ options?: ErrorOptions,
+ ) {
+ super("non_json", "Wandbox returned a non-JSON response", options);
+ this.name = "WandboxNonJsonError";
+ }
+}
+
+export class WandboxSchemaError extends WandboxError {
+ constructor(message: string) {
+ super("schema", message);
+ this.name = "WandboxSchemaError";
+ }
+}
+
+export function normalizeLanguageKey(language: string): string {
+ return language.toLowerCase().replaceAll(" ", "");
+}
+
+export function parseCompilerList(value: unknown): WandboxCompiler[] {
+ // Guard the remote schema so an upstream format change produces a typed, loud failure.
+ if (!Array.isArray(value)) {
+ throw new WandboxSchemaError("Wandbox compiler list is not an array");
+ }
+
+ return value.map((entry, index) => {
+ if (
+ typeof entry !== "object" ||
+ entry === null ||
+ !("name" in entry) ||
+ !("language" in entry) ||
+ typeof entry.name !== "string" ||
+ typeof entry.language !== "string" ||
+ entry.name.length === 0 ||
+ entry.language.length === 0
+ ) {
+ throw new WandboxSchemaError(
+ `Wandbox compiler list entry ${index} is missing a valid name or language`,
+ );
+ }
+ return { name: entry.name, language: entry.language };
+ });
+}
+
+export function languageChoices(
+ compilers: readonly WandboxCompiler[],
+): LanguageChoice[] {
+ const choices = new Map();
+ for (const compiler of compilers) {
+ const value = normalizeLanguageKey(compiler.language);
+ if (!choices.has(value)) {
+ choices.set(value, { name: compiler.language, value });
+ }
+ }
+ return [...choices.values()];
+}
+
+export function resolveCompilerFromList(
+ languageKey: string,
+ compilers: readonly WandboxCompiler[],
+ pinnedCompilers: Readonly> = PINNED_COMPILERS,
+): CompilerResolution | undefined {
+ const key = normalizeLanguageKey(languageKey);
+ const candidates = compilers.filter(
+ (compiler) => normalizeLanguageKey(compiler.language) === key,
+ );
+ if (candidates.length === 0) {
+ return undefined;
+ }
+
+ const pinned = pinnedCompilers[key];
+ if (pinned !== undefined && candidates.some(({ name }) => name === pinned)) {
+ return { compiler: pinned, source: "pinned" };
+ }
+
+ const stable = candidates.find(
+ ({ name }) => !/(?:head|devel|git)/iu.test(name),
+ );
+ return {
+ compiler: (stable ?? candidates[0])?.name ?? "",
+ source: "heuristic",
+ ...(pinned === undefined ? {} : { missingPinnedCompiler: pinned }),
+ };
+}
+
+function parseCompileResult(value: unknown): WandboxCompileResult {
+ // Apply the same schema guard to compile output so response changes fail loudly.
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ throw new WandboxSchemaError("Wandbox compile result is not an object");
+ }
+
+ const result: Record = {};
+ for (const [field, fieldValue] of Object.entries(value)) {
+ if (typeof fieldValue !== "string") {
+ throw new WandboxSchemaError(
+ `Wandbox compile result field ${field} is not a string`,
+ );
+ }
+ result[field] = fieldValue;
+ }
+ return result;
+}
+
+export interface WandboxClientOptions {
+ readonly logger: Pick;
+ readonly fetch?: typeof fetch;
+ readonly now?: () => number;
+ readonly cacheTtlMs?: number;
+}
+
+export class WandboxClient {
+ readonly #logger: Pick;
+ readonly #fetch: typeof fetch;
+ readonly #now: () => number;
+ readonly #cacheTtlMs: number;
+ #compilers: WandboxCompiler[] = [];
+ // The compiler-list cache refreshes on a TTL rather than on every command.
+ #lastRefreshAttempt = Number.NEGATIVE_INFINITY;
+ #refreshing?: Promise;
+
+ constructor(options: WandboxClientOptions) {
+ this.#logger = options.logger;
+ this.#fetch = options.fetch ?? globalThis.fetch;
+ this.#now = options.now ?? Date.now;
+ this.#cacheTtlMs = options.cacheTtlMs ?? WANDBOX_CACHE_TTL_MS;
+ }
+
+ async init(): Promise {
+ await this.#refresh();
+ }
+
+ getLanguageChoices(): readonly LanguageChoice[] {
+ return languageChoices(this.#compilers);
+ }
+
+ async resolveCompiler(
+ languageKey: string,
+ ): Promise {
+ if (this.#now() - this.#lastRefreshAttempt >= this.#cacheTtlMs) {
+ await this.#refresh();
+ }
+ const resolution = resolveCompilerFromList(languageKey, this.#compilers);
+ return resolution;
+ }
+
+ async compile(
+ compiler: string,
+ code: string,
+ stdin: string,
+ ): Promise {
+ let response: Response;
+ try {
+ response = await this.#fetch(COMPILE_URL, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ compiler,
+ code,
+ stdin,
+ "compiler-option-raw": compiler.startsWith("nim-")
+ ? NIM_COMPILER_OPTIONS
+ : "",
+ }),
+ });
+ } catch (error) {
+ throw new WandboxError("connection", "Could not connect to Wandbox", {
+ cause: error,
+ });
+ }
+
+ if (!response.ok) {
+ throw new WandboxHttpError(response.status, await response.text());
+ }
+
+ const fallbackResponse = response.clone();
+ let value: unknown;
+ try {
+ value = await response.json();
+ } catch (error) {
+ throw new WandboxNonJsonError(
+ response.status,
+ await fallbackResponse.text(),
+ { cause: error },
+ );
+ }
+
+ const result = parseCompileResult(value);
+ if (compiler.startsWith("nim-") && result.compiler_error !== undefined) {
+ return {
+ ...result,
+ compiler_error: result.compiler_error.replace(/^CC: \S+\r?\n/gmu, ""),
+ };
+ }
+ return result;
+ }
+
+ async #refresh(): Promise {
+ if (this.#refreshing !== undefined) {
+ return this.#refreshing;
+ }
+
+ this.#refreshing = this.#refreshCompilerList();
+ try {
+ await this.#refreshing;
+ } finally {
+ this.#refreshing = undefined;
+ }
+ }
+
+ async #refreshCompilerList(): Promise {
+ this.#lastRefreshAttempt = this.#now();
+ try {
+ let response: Response;
+ try {
+ response = await this.#fetch(LIST_URL);
+ } catch (error) {
+ throw new WandboxError(
+ "connection",
+ "Could not fetch the Wandbox compiler list",
+ { cause: error },
+ );
+ }
+ if (!response.ok) {
+ throw new WandboxHttpError(response.status, await response.text());
+ }
+
+ const fallbackResponse = response.clone();
+ let value: unknown;
+ try {
+ value = await response.json();
+ } catch (error) {
+ throw new WandboxNonJsonError(
+ response.status,
+ await fallbackResponse.text(),
+ { cause: error },
+ );
+ }
+ this.#compilers = parseCompilerList(value);
+ for (const [languageKey, pinnedCompiler] of Object.entries(
+ PINNED_COMPILERS,
+ )) {
+ const resolution = resolveCompilerFromList(
+ languageKey,
+ this.#compilers,
+ );
+ if (resolution?.source !== "pinned") {
+ this.#logger.warn(
+ {
+ languageKey,
+ pinnedCompiler,
+ fallbackCompiler: resolution?.compiler,
+ },
+ "Pinned Wandbox compiler is unavailable; using heuristic fallback when possible",
+ );
+ }
+ }
+ } catch (error) {
+ // A failed refresh leaves #compilers untouched so commands can use stale valid data.
+ if (error instanceof WandboxSchemaError) {
+ this.#logger.error(
+ { error },
+ "Wandbox compiler list response did not match the expected schema",
+ );
+ }
+ this.#logger.warn(
+ { error, staleCompilerCount: this.#compilers.length },
+ "Failed to refresh Wandbox compiler list; retaining stale cache",
+ );
+ }
+ }
+}
diff --git a/discord/src/features/misc/index.test.ts b/discord/src/features/misc/index.test.ts
new file mode 100644
index 0000000..2d02381
--- /dev/null
+++ b/discord/src/features/misc/index.test.ts
@@ -0,0 +1,78 @@
+/**
+ * Covers mention throttling, parsed-mention gating, and private-history filtering.
+ */
+import type { Message } from "discord.js";
+import { describe, expect, it, vi } from "vitest";
+import type { FeatureDependencies } from "../../types.js";
+import {
+ type ChatCompletionClient,
+ createMiscFeature,
+ MentionRateLimiter,
+ SHORT_REPLY_SYSTEM_PROMPT,
+} from "./index.js";
+
+describe("MentionRateLimiter", () => {
+ it("allows three messages per user in sixty seconds", () => {
+ const limiter = new MentionRateLimiter();
+ expect(limiter.allow("user", 0)).toBe(true);
+ expect(limiter.allow("user", 1)).toBe(true);
+ expect(limiter.allow("user", 2)).toBe(true);
+ expect(limiter.allow("user", 3)).toBe(false);
+ expect(limiter.allow("user", 60_000)).toBe(true);
+ });
+});
+
+describe("misc mention response", () => {
+ it("filters opted-out history and uses the mocked completion", async () => {
+ const fetched = new Map([
+ ["3", { author: { id: "caller" }, content: "<@bot> hello" }],
+ ["2", { author: { id: "opted-out" }, content: "private" }],
+ ["1", { author: { id: "bot" }, content: "earlier reply" }],
+ ]);
+ const fetch = vi.fn().mockResolvedValue(fetched);
+ const sendTyping = vi.fn().mockResolvedValue(undefined);
+ const reply = vi.fn().mockResolvedValue(undefined);
+ const hasMention = vi.fn().mockReturnValue(true);
+ const message = {
+ author: { id: "caller", bot: false },
+ mentions: { has: hasMention },
+ channel: { messages: { fetch }, sendTyping },
+ reply,
+ } as unknown as Message;
+ const create = vi.fn().mockResolvedValue({
+ choices: [{ message: { content: "short answer" } }],
+ });
+ const completion = { create } as ChatCompletionClient;
+ const recordCommand = vi.fn();
+ const dependencies = {
+ client: { user: { id: "bot" } },
+ optOutUsers: { has: (id: string) => id === "opted-out" },
+ usageStats: { recordCommand },
+ env: {
+ openAIApiKey: "key",
+ developerId: "developer",
+ openAIChatModel: "full-model",
+ openAIChatModelLite: "lite-model",
+ },
+ } as unknown as FeatureDependencies;
+
+ const feature = createMiscFeature(dependencies, completion);
+ await feature?.onMessage?.(message, {} as never);
+
+ expect(hasMention).toHaveBeenCalledWith(dependencies.client.user);
+ expect(fetch).toHaveBeenCalledWith({ limit: 10 });
+ expect(create).toHaveBeenCalledWith({
+ model: "lite-model",
+ messages: [
+ { role: "system", content: SHORT_REPLY_SYSTEM_PROMPT },
+ { role: "assistant", content: "earlier reply" },
+ { role: "user", content: "<@bot> hello" },
+ ],
+ });
+ expect(reply).toHaveBeenCalledWith({
+ content: "short answer",
+ allowedMentions: { parse: [], repliedUser: true },
+ });
+ expect(recordCommand).toHaveBeenCalledWith("mention", "misc", "ok");
+ });
+});
diff --git a/discord/src/features/misc/index.ts b/discord/src/features/misc/index.ts
new file mode 100644
index 0000000..4a57a02
--- /dev/null
+++ b/discord/src/features/misc/index.ts
@@ -0,0 +1,128 @@
+/**
+ * Implements the miscellaneous mention-response feature over Discord and OpenAI.
+ */
+import type { Message } from "discord.js";
+import OpenAI from "openai";
+import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
+import type { OptOutUsers } from "../../shared/state.js";
+import type { Feature, FeatureDependencies } from "../../types.js";
+
+export const DEVELOPER_SYSTEM_PROMPT =
+ "これはDiscordでのチャットです。" +
+ "以下の様々なユーザーによる直近のメッセージ履歴を参考に、" +
+ "あなたがメンションされている最後のメッセージに返信してください。";
+
+export const SHORT_REPLY_SYSTEM_PROMPT =
+ "これはDiscordのチャットです。" +
+ "以下は直近のメッセージ履歴です。" +
+ "一言で返信してください。";
+
+const RATE_LIMIT_WINDOW_MS = 60_000;
+const RATE_LIMIT_COUNT = 3;
+
+export class MentionRateLimiter {
+ readonly #mentions = new Map();
+
+ allow(userId: string, now = Date.now()): boolean {
+ const recent = (this.#mentions.get(userId) ?? []).filter(
+ (timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS,
+ );
+ if (recent.length >= RATE_LIMIT_COUNT) {
+ this.#mentions.set(userId, recent);
+ return false;
+ }
+ recent.push(now);
+ this.#mentions.set(userId, recent);
+ return true;
+ }
+}
+
+export interface ChatCompletionClient {
+ create(options: {
+ readonly model: string;
+ readonly messages: readonly ChatCompletionMessageParam[];
+ }): Promise<{
+ readonly choices: readonly {
+ readonly message: { readonly content: string | null };
+ }[];
+ }>;
+}
+
+export async function fetchRecentHistory(
+ message: Message,
+ botUserId: string,
+ optOutUsers: Pick,
+ limit = 10,
+): Promise {
+ const fetched = await message.channel.messages.fetch({ limit });
+ // Opted-out users' messages must never enter the OpenAI conversation context.
+ return [...fetched.values()]
+ .reverse()
+ .filter((entry) => !optOutUsers.has(entry.author.id))
+ .map((entry) => ({
+ role: entry.author.id === botUserId ? "assistant" : "user",
+ content: entry.content,
+ }));
+}
+
+export function createMiscFeature(
+ dependencies: FeatureDependencies,
+ completionClient?: ChatCompletionClient,
+): Feature | undefined {
+ if (dependencies.env.openAIApiKey === undefined) {
+ return undefined;
+ }
+ const completion =
+ completionClient ??
+ new OpenAI({ apiKey: dependencies.env.openAIApiKey }).chat.completions;
+ const limiter = new MentionRateLimiter();
+
+ return {
+ id: "misc",
+ async onMessage(message) {
+ const botUser = dependencies.client.user;
+ if (
+ message.author.bot ||
+ botUser === null ||
+ // Parsed mentions avoid the old bot's false positives from substring-matching its ID.
+ !message.mentions.has(botUser) ||
+ !limiter.allow(message.author.id)
+ ) {
+ return undefined;
+ }
+
+ if ("sendTyping" in message.channel) {
+ // Messages use typing for feedback; interactions must ack within three seconds instead.
+ await message.channel.sendTyping();
+ }
+ const history = await fetchRecentHistory(
+ message,
+ botUser.id,
+ dependencies.optOutUsers,
+ );
+ const isDeveloper = message.author.id === dependencies.env.developerId;
+ const response = await completion.create({
+ model: isDeveloper
+ ? dependencies.env.openAIChatModel
+ : dependencies.env.openAIChatModelLite,
+ messages: [
+ {
+ role: "system",
+ content: isDeveloper
+ ? DEVELOPER_SYSTEM_PROMPT
+ : SHORT_REPLY_SYSTEM_PROMPT,
+ },
+ ...history,
+ ],
+ });
+ // Replies directly instead of returning an OutgoingReply, matching the old bot:
+ // chat responses get no Delete button and no edit-follow tracking.
+ await message.reply({
+ content: response.choices[0]?.message.content ?? "",
+ allowedMentions: { parse: [], repliedUser: true },
+ });
+ dependencies.usageStats.recordCommand("mention", "misc", "ok");
+ return undefined;
+ },
+ };
+}
diff --git a/discord/src/features/ping/index.ts b/discord/src/features/ping/index.ts
new file mode 100644
index 0000000..39ec1f8
--- /dev/null
+++ b/discord/src/features/ping/index.ts
@@ -0,0 +1,28 @@
+/**
+ * Supplies a small feature-layer smoke test through a Discord slash command.
+ */
+import { SlashCommandBuilder } from "discord.js";
+import { successContainer } from "../../platform/discord/components.js";
+import type { Feature, FeatureDependencies } from "../../types.js";
+
+// Temporary smoke-test feature for the TypeScript foundation. Remove it once
+// the real feature ports provide end-to-end command coverage.
+export function createPingFeature(_dependencies: FeatureDependencies): Feature {
+ return {
+ id: "ping",
+ slashCommands: [
+ {
+ data: new SlashCommandBuilder()
+ .setName("ping")
+ .setDescription("Check whether the bot is responding")
+ .setDescriptionLocalizations({ ja: "ボットの応答を確認します" }),
+ async execute() {
+ return {
+ kind: "components-v2",
+ components: [successContainer("pong")],
+ };
+ },
+ },
+ ],
+ };
+}
diff --git a/discord/src/features/privacy/index.test.ts b/discord/src/features/privacy/index.test.ts
new file mode 100644
index 0000000..3f3da27
--- /dev/null
+++ b/discord/src/features/privacy/index.test.ts
@@ -0,0 +1,41 @@
+/**
+ * Covers the privacy-policy reply shape and its Components V2 size budget.
+ */
+import { readFile } from "node:fs/promises";
+import type { ChatInputCommandInteraction } from "discord.js";
+import { describe, expect, it } from "vitest";
+import type { RequestContext } from "../../platform/discord/context.js";
+import type { FeatureDependencies } from "../../types.js";
+import { createPrivacyFeature } from "./index.js";
+
+const COMPONENTS_V2_TEXT_LIMIT = 4_000;
+
+function findCommand(name: string) {
+ const feature = createPrivacyFeature({} as FeatureDependencies);
+ const command = feature.slashCommands?.find(
+ (candidate) => candidate.data.name === name,
+ );
+ if (command === undefined) {
+ throw new Error(`missing /${name}`);
+ }
+ return command;
+}
+
+describe("privacy-policy command", () => {
+ it("fits the policy document within the Components V2 text budget", async () => {
+ const policy = await readFile(
+ new URL("../../../config/privacy-policy.md", import.meta.url),
+ "utf8",
+ );
+ expect(policy.length).toBeLessThanOrEqual(COMPONENTS_V2_TEXT_LIMIT);
+ });
+
+ it("replies with a Components V2 text display, not plain content", async () => {
+ const command = findCommand("privacy-policy");
+ const outgoing = await command.execute(
+ {} as ChatInputCommandInteraction,
+ { locale: "en", userId: "user" } as RequestContext,
+ );
+ expect(outgoing?.kind).toBe("components-v2");
+ });
+});
diff --git a/discord/src/features/privacy/index.ts b/discord/src/features/privacy/index.ts
new file mode 100644
index 0000000..d7eefb3
--- /dev/null
+++ b/discord/src/features/privacy/index.ts
@@ -0,0 +1,95 @@
+/**
+ * Implements privacy-policy and opt-out commands in the feature layer.
+ */
+import { readFile } from "node:fs/promises";
+import { SlashCommandBuilder, TextDisplayBuilder } from "discord.js";
+import { getFixedT } from "../../shared/i18n.js";
+import type { Feature, FeatureDependencies } from "../../types.js";
+
+const privacyPolicyUrl = new URL(
+ "../../../config/privacy-policy.md",
+ import.meta.url,
+);
+
+// AGPL-3.0 §13: users interacting with the bot over the network must be offered
+// its Corresponding Source. If you run a modified version, point this at your fork.
+const SOURCE_URL = "https://github.com/gaato/coderunbot";
+
+export function createPrivacyFeature({
+ optOutUsers,
+}: FeatureDependencies): Feature {
+ return {
+ id: "privacy",
+ slashCommands: [
+ {
+ data: new SlashCommandBuilder()
+ .setName("privacy-policy")
+ .setDescription("Show the privacy policy")
+ .setDescriptionLocalizations({
+ ja: "プライバシーポリシーを表示します",
+ }),
+ async execute() {
+ // The policy exceeds the 2,000-character plain-content limit; a Components V2
+ // text display raises the ceiling to 4,000 characters for the whole message.
+ return {
+ kind: "components-v2",
+ components: [
+ new TextDisplayBuilder().setContent(
+ await readFile(privacyPolicyUrl, "utf8"),
+ ),
+ ],
+ };
+ },
+ },
+ {
+ data: new SlashCommandBuilder()
+ .setName("source")
+ .setDescription("Show where to get this bot's source code")
+ .setDescriptionLocalizations({
+ ja: "このボットのソースコードの入手先を表示します",
+ }),
+ async execute(_interaction, context) {
+ const t = getFixedT(context.locale);
+ return {
+ kind: "plain",
+ content: t("privacy.source", { url: SOURCE_URL }),
+ };
+ },
+ },
+ {
+ data: new SlashCommandBuilder()
+ .setName("opt-out")
+ .setDescription("Stop the bot from processing your message content")
+ .setDescriptionLocalizations({
+ ja: "ボットによるメッセージ内容の処理を停止します",
+ }),
+ async execute(_interaction, context) {
+ const t = getFixedT(context.locale);
+ if (optOutUsers.has(context.userId)) {
+ return { kind: "plain", content: t("privacy.optOut.already") };
+ }
+
+ await optOutUsers.add(context.userId);
+ return { kind: "plain", content: t("privacy.optOut.success") };
+ },
+ },
+ {
+ data: new SlashCommandBuilder()
+ .setName("opt-in")
+ .setDescription("Allow the bot to process your message content again")
+ .setDescriptionLocalizations({
+ ja: "ボットによるメッセージ内容の処理を再開します",
+ }),
+ async execute(_interaction, context) {
+ const t = getFixedT(context.locale);
+ if (!optOutUsers.has(context.userId)) {
+ return { kind: "plain", content: t("privacy.optIn.already") };
+ }
+
+ await optOutUsers.remove(context.userId);
+ return { kind: "plain", content: t("privacy.optIn.success") };
+ },
+ },
+ ],
+ };
+}
diff --git a/discord/src/features/tex/index.ts b/discord/src/features/tex/index.ts
new file mode 100644
index 0000000..73d15b1
--- /dev/null
+++ b/discord/src/features/tex/index.ts
@@ -0,0 +1,136 @@
+/**
+ * Defines TeX feature commands and delegates rendering to the Discord-free adapter.
+ */
+import {
+ ActionRowBuilder,
+ ModalBuilder,
+ SlashCommandBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+} from "discord.js";
+import { getFixedT } from "../../shared/i18n.js";
+import type {
+ Feature,
+ FeatureDependencies,
+ OutgoingReply,
+} from "../../types.js";
+import {
+ stripTexFences,
+ texFailureReply,
+ texSuccessReply,
+} from "./presentation.js";
+import { renderTexToPng, TexRenderError } from "./renderer.js";
+
+type TexEnvironment = "align" | "gather";
+
+export function createTexFeature(_dependencies: FeatureDependencies): Feature {
+ const slashCommand = new SlashCommandBuilder()
+ .setName("tex")
+ .setDescription("Render TeX as an image")
+ .setDescriptionLocalizations({
+ ja: "TeX を画像としてレンダリングします",
+ });
+ slashCommand.addStringOption((option) =>
+ option
+ .setName("env")
+ .setDescription("The environment to use")
+ .setDescriptionLocalizations({ ja: "使用する環境" })
+ .addChoices(
+ { name: "align", value: "align" },
+ { name: "gather", value: "gather" },
+ ),
+ );
+ slashCommand.addBooleanOption((option) =>
+ option
+ .setName("spoiler")
+ .setDescription("Mark the rendered image as a spoiler")
+ .setDescriptionLocalizations({
+ ja: "レンダリング画像をスポイラーとして隠します",
+ }),
+ );
+
+ return {
+ id: "tex",
+ prefixCommands: [
+ {
+ name: "tex",
+ async execute(_message, args, context) {
+ return renderReply(stripTexFences(args), false, context.locale);
+ },
+ },
+ {
+ name: "stex",
+ async execute(_message, args, context) {
+ return renderReply(stripTexFences(args), true, context.locale);
+ },
+ },
+ ],
+ slashCommands: [
+ {
+ data: slashCommand,
+ async execute(interaction, context) {
+ if (!interaction.isChatInputCommand()) {
+ return undefined;
+ }
+ const environment = parseEnvironment(
+ interaction.options.getString("env"),
+ );
+ const spoiler = interaction.options.getBoolean("spoiler") ?? false;
+ const t = getFixedT(context.locale);
+ const input = new TextInputBuilder()
+ .setCustomId("latex")
+ .setLabel(t("tex.modal.codeLabel"))
+ .setPlaceholder(t("tex.modal.codePlaceholder"))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(true);
+ if (environment !== undefined) {
+ input.setValue(`\\begin{${environment}}\n\n\\end{${environment}}`);
+ }
+
+ const modal = new ModalBuilder()
+ .setCustomId(
+ `texModal:${spoiler}:${environment === undefined ? "" : environment}`,
+ )
+ .setTitle(t("tex.modal.title"))
+ .addComponents(
+ new ActionRowBuilder().addComponents(input),
+ );
+ // Discord accepts a modal only as the interaction's first response.
+ await interaction.showModal(modal);
+ return undefined;
+ },
+ },
+ ],
+ modalHandlers: {
+ texModal: async (interaction, context) => {
+ const [, spoilerValue] = interaction.customId.split(":");
+ const spoiler = spoilerValue === "true";
+ const latex = interaction.fields.getTextInputValue("latex");
+ // Acknowledge within Discord's three-second interaction window before rendering.
+ await interaction.deferReply();
+ return renderReply(latex, spoiler, context.locale);
+ },
+ },
+ };
+}
+
+async function renderReply(
+ latex: string,
+ spoiler: boolean,
+ locale: string,
+): Promise {
+ const t = getFixedT(locale);
+ try {
+ const png = await renderTexToPng(latex);
+ return texSuccessReply(latex, png, spoiler, t);
+ } catch (error) {
+ if (error instanceof TexRenderError) {
+ return texFailureReply(error.message, t);
+ }
+ throw error;
+ }
+}
+
+function parseEnvironment(value: string | null): TexEnvironment | undefined {
+ return value === "align" || value === "gather" ? value : undefined;
+}
diff --git a/discord/src/features/tex/presentation.test.ts b/discord/src/features/tex/presentation.test.ts
new file mode 100644
index 0000000..4651009
--- /dev/null
+++ b/discord/src/features/tex/presentation.test.ts
@@ -0,0 +1,90 @@
+/**
+ * Verifies TeX input cleanup, hints, and Discord presentation payloads.
+ */
+import { ContainerBuilder } from "discord.js";
+import { describe, expect, it } from "vitest";
+import { getFixedT } from "../../shared/i18n.js";
+import type { OutgoingReply } from "../../types.js";
+import {
+ shouldShowMultilineHint,
+ stripTexFences,
+ texSuccessReply,
+} from "./presentation.js";
+
+function containerJson(reply: OutgoingReply) {
+ if (reply.kind !== "components-v2") {
+ throw new Error("expected a Components V2 reply");
+ }
+ const container = reply.components[0];
+ if (!(container instanceof ContainerBuilder)) {
+ throw new Error("expected a ContainerBuilder");
+ }
+ return container.toJSON();
+}
+
+describe("stripTexFences", () => {
+ it.each([
+ ["```tex\nx^2\n```", "x^2"],
+ ["```\nx^2\n```", "x^2"],
+ [" x^2 ", "x^2"],
+ ])("removes an optional TeX or plain code fence", (input, expected) => {
+ expect(stripTexFences(input)).toBe(expected);
+ });
+});
+
+describe("shouldShowMultilineHint", () => {
+ it("suggests an environment for bare line breaks", () => {
+ expect(shouldShowMultilineHint("x \\\\ y")).toBe(true);
+ });
+
+ it.each([
+ "x + y",
+ "\\begin{align}x \\\\ y\\end{align}",
+ "\\begin{gather}x \\\\ y",
+ "x \\\\ y\\end{gather}",
+ ])("does not suggest one when inappropriate", (latex) => {
+ expect(shouldShowMultilineHint(latex)).toBe(false);
+ });
+});
+
+describe("texSuccessReply", () => {
+ it("marks the media-gallery item itself as a spoiler", () => {
+ const reply = texSuccessReply(
+ "x^2",
+ Buffer.from("png"),
+ true,
+ getFixedT("en"),
+ );
+
+ expect(containerJson(reply)).toMatchObject({
+ type: 17,
+ components: expect.arrayContaining([
+ {
+ type: 12,
+ items: [
+ {
+ media: { url: "attachment://tex.png" },
+ spoiler: true,
+ },
+ ],
+ },
+ ]),
+ });
+ });
+
+ it("uses a text attachment when the source is over the display limit", () => {
+ const reply = texSuccessReply(
+ "x".repeat(1_001),
+ Buffer.from("png"),
+ false,
+ getFixedT("en"),
+ );
+
+ expect(reply.files).toHaveLength(2);
+ expect(containerJson(reply)).toMatchObject({
+ components: expect.arrayContaining([
+ { file: { url: "attachment://tex-source.txt" }, type: 13 },
+ ]),
+ });
+ });
+});
diff --git a/discord/src/features/tex/presentation.ts b/discord/src/features/tex/presentation.ts
new file mode 100644
index 0000000..b343505
--- /dev/null
+++ b/discord/src/features/tex/presentation.ts
@@ -0,0 +1,88 @@
+/**
+ * Builds Discord reply values from TeX feature results at the presentation boundary.
+ */
+import {
+ AttachmentBuilder,
+ ContainerBuilder,
+ FileBuilder,
+ MediaGalleryBuilder,
+ MediaGalleryItemBuilder,
+ TextDisplayBuilder,
+} from "discord.js";
+import type { TFunction } from "i18next";
+import {
+ FAILURE_ACCENT,
+ SUCCESS_ACCENT,
+ textComponent,
+} from "../../platform/discord/components.js";
+import type { OutgoingReply } from "../../types.js";
+
+export function stripTexFences(input: string): string {
+ return input
+ .replace(/```tex/giu, "")
+ .replace(/```/gu, "")
+ .trim();
+}
+
+export function shouldShowMultilineHint(latex: string): boolean {
+ return (
+ latex.includes("\\\\") &&
+ !latex.includes("\\begin") &&
+ !latex.includes("\\end")
+ );
+}
+
+export function texSuccessReply(
+ latex: string,
+ png: Buffer,
+ spoiler: boolean,
+ t: TFunction,
+): OutgoingReply {
+ const container = new ContainerBuilder()
+ .setAccentColor(SUCCESS_ACCENT)
+ .addMediaGalleryComponents(
+ new MediaGalleryBuilder().addItems(
+ new MediaGalleryItemBuilder()
+ .setURL("attachment://tex.png")
+ .setSpoiler(spoiler),
+ ),
+ );
+
+ if (shouldShowMultilineHint(latex)) {
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `### ${t("tex.hintHeading")}\n${t("tex.multilineHint")}`,
+ ),
+ );
+ }
+
+ const source = textComponent(latex, "tex-source.txt");
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(`### ${t("tex.codeHeading")}`),
+ );
+ if (source.component instanceof FileBuilder) {
+ container.addFileComponents(source.component);
+ } else {
+ const safeLatex = latex.replaceAll("```", "`\u200b``");
+ source.component.setContent(`\`\`\`tex\n${safeLatex}\n\`\`\``);
+ container.addTextDisplayComponents(source.component);
+ }
+
+ return {
+ kind: "components-v2",
+ components: [container],
+ files: [new AttachmentBuilder(png, { name: "tex.png" }), ...source.files],
+ };
+}
+
+export function texFailureReply(message: string, t: TFunction): OutgoingReply {
+ const safeMessage = message.replaceAll("```", "`\u200b``");
+ const container = new ContainerBuilder()
+ .setAccentColor(FAILURE_ACCENT)
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `### ${t("tex.errorHeading")}\n\`\`\`\n${safeMessage}\n\`\`\``,
+ ),
+ );
+ return { kind: "components-v2", components: [container] };
+}
diff --git a/discord/src/features/tex/renderer.test.ts b/discord/src/features/tex/renderer.test.ts
new file mode 100644
index 0000000..365f7d2
--- /dev/null
+++ b/discord/src/features/tex/renderer.test.ts
@@ -0,0 +1,41 @@
+/**
+ * Exercises the discord.js-free TeX rendering adapter against SVG, PNG, and error cases.
+ */
+import { describe, expect, it } from "vitest";
+import { renderTexToPng, renderTexToSvg, TexRenderError } from "./renderer.js";
+
+describe("TeX renderer", () => {
+ it("renders TeX to a PNG buffer", async () => {
+ const png = await renderTexToPng("x^2");
+
+ expect(png.subarray(0, 8)).toEqual(
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
+ );
+ });
+
+ it("throws a typed error for invalid TeX", async () => {
+ await expect(renderTexToSvg("\\frac{1}{")).rejects.toThrow(TexRenderError);
+ });
+
+ it("throws a typed error for undefined macros", async () => {
+ await expect(renderTexToSvg("\\notarealmacro")).rejects.toThrow(
+ TexRenderError,
+ );
+ });
+
+ it("reports the original TeX message for invalid input", async () => {
+ await expect(renderTexToSvg("{")).rejects.toThrow(/missing close brace/iu);
+ });
+
+ it("sets the Japanese serif fallback on SVG text nodes", async () => {
+ const svg = await renderTexToSvg("\\text{日本語}");
+
+ const textTags = svg.match(/]*>/gu) ?? [];
+ expect(textTags.length).toBeGreaterThan(0);
+ expect(
+ textTags.every((tag) =>
+ tag.includes('font-family="Noto Serif CJK JP, serif"'),
+ ),
+ ).toBe(true);
+ });
+});
diff --git a/discord/src/features/tex/renderer.ts b/discord/src/features/tex/renderer.ts
new file mode 100644
index 0000000..e553559
--- /dev/null
+++ b/discord/src/features/tex/renderer.ts
@@ -0,0 +1,188 @@
+/**
+ * In-process LaTeX → SVG → PNG renderer built on MathJax v4.
+ *
+ * This is the discord.js-free adapter of the tex feature: it knows nothing
+ * about Discord and exposes plain functions returning SVG text / PNG buffers,
+ * so it can be unit-tested directly. It replaces the standalone tex web
+ * service the Python bot used to call over HTTP.
+ */
+import { MathJaxNewcmFont } from "@mathjax/mathjax-newcm-font/js/svg.js";
+import { liteAdaptor } from "@mathjax/src/js/adaptors/liteAdaptor.js";
+import { RegisterHTMLHandler } from "@mathjax/src/js/handlers/html.js";
+import TexError from "@mathjax/src/js/input/tex/TexError.js";
+import { TeX } from "@mathjax/src/js/input/tex.js";
+import { mathjax } from "@mathjax/src/js/mathjax.js";
+import { SVG } from "@mathjax/src/js/output/svg.js";
+// The newcm font data is split into dynamically loaded pieces; this import
+// teaches MathJax to load them with ESM import() when a glyph range is first
+// needed (which is also why conversion must go through convertPromise()).
+import "@mathjax/src/js/util/asyncLoad/esm.js";
+import sharp from "sharp";
+
+// MathJax v4 dropped the v3 AllPackages bundle, so every TeX extension has to
+// be imported for its registration side effect and listed in `packages`
+// below. This is the full set shipped in @mathjax/src, minus:
+// - noundefined: would render unknown macros as red text instead of letting
+// them raise the TexError we turn into a typed render failure
+// - noerrors: would swallow TeX errors and typeset the raw source
+// - autoload / require: dynamic extension loading is pointless (and
+// undesirable) when everything is preloaded
+// - setoptions: lets TeX input override parser options at runtime
+// - tagformat: only useful with a custom tag-format configuration
+// - colorv2: superseded by (and conflicting with) the color package
+// - fontsizev3: compatibility shim for v3 font-size behavior
+// - texhtml: embeds raw HTML in the output, which we never want
+import "@mathjax/src/js/input/tex/action/ActionConfiguration.js";
+import "@mathjax/src/js/input/tex/ams/AmsConfiguration.js";
+import "@mathjax/src/js/input/tex/amscd/AmsCdConfiguration.js";
+import "@mathjax/src/js/input/tex/bbm/BbmConfiguration.js";
+import "@mathjax/src/js/input/tex/bboldx/BboldxConfiguration.js";
+import "@mathjax/src/js/input/tex/bbox/BboxConfiguration.js";
+import "@mathjax/src/js/input/tex/begingroup/BegingroupConfiguration.js";
+import "@mathjax/src/js/input/tex/boldsymbol/BoldsymbolConfiguration.js";
+import "@mathjax/src/js/input/tex/braket/BraketConfiguration.js";
+import "@mathjax/src/js/input/tex/bussproofs/BussproofsConfiguration.js";
+import "@mathjax/src/js/input/tex/cancel/CancelConfiguration.js";
+import "@mathjax/src/js/input/tex/cases/CasesConfiguration.js";
+import "@mathjax/src/js/input/tex/centernot/CenternotConfiguration.js";
+import "@mathjax/src/js/input/tex/color/ColorConfiguration.js";
+import "@mathjax/src/js/input/tex/colortbl/ColortblConfiguration.js";
+import "@mathjax/src/js/input/tex/configmacros/ConfigMacrosConfiguration.js";
+import "@mathjax/src/js/input/tex/dsfont/DsfontConfiguration.js";
+import "@mathjax/src/js/input/tex/empheq/EmpheqConfiguration.js";
+import "@mathjax/src/js/input/tex/enclose/EncloseConfiguration.js";
+import "@mathjax/src/js/input/tex/extpfeil/ExtpfeilConfiguration.js";
+import "@mathjax/src/js/input/tex/gensymb/GensymbConfiguration.js";
+import "@mathjax/src/js/input/tex/html/HtmlConfiguration.js";
+import "@mathjax/src/js/input/tex/mathtools/MathtoolsConfiguration.js";
+import "@mathjax/src/js/input/tex/mhchem/MhchemConfiguration.js";
+import "@mathjax/src/js/input/tex/newcommand/NewcommandConfiguration.js";
+import "@mathjax/src/js/input/tex/physics/PhysicsConfiguration.js";
+import "@mathjax/src/js/input/tex/textcomp/TextcompConfiguration.js";
+import "@mathjax/src/js/input/tex/textmacros/TextMacrosConfiguration.js";
+import "@mathjax/src/js/input/tex/unicode/UnicodeConfiguration.js";
+import "@mathjax/src/js/input/tex/units/UnitsConfiguration.js";
+import "@mathjax/src/js/input/tex/upgreek/UpgreekConfiguration.js";
+import "@mathjax/src/js/input/tex/verb/VerbConfiguration.js";
+
+const RENDER_PACKAGES = [
+ "base",
+ "action",
+ "ams",
+ "amscd",
+ "bbm",
+ "bboldx",
+ "bbox",
+ "begingroup",
+ "boldsymbol",
+ "braket",
+ "bussproofs",
+ "cancel",
+ "cases",
+ "centernot",
+ "color",
+ "colortbl",
+ "configmacros",
+ "dsfont",
+ "empheq",
+ "enclose",
+ "extpfeil",
+ "gensymb",
+ "html",
+ "mathtools",
+ "mhchem",
+ "newcommand",
+ "physics",
+ "textcomp",
+ "textmacros",
+ "unicode",
+ "units",
+ "upgreek",
+ "verb",
+];
+
+// The newcm font has no CJK glyphs, so e.g. \text{日本語} comes out as plain
+// SVG elements. Forcing this family makes librsvg (inside sharp) pick
+// the Noto Serif CJK JP installed in the container instead of a fallback that
+// lacks the glyphs.
+const TEXT_FONT_FAMILY = "Noto Serif CJK JP, serif";
+const PADDING = 20;
+
+const adaptor = liteAdaptor();
+RegisterHTMLHandler(adaptor);
+
+export class TexRenderError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "TexRenderError";
+ }
+}
+
+export async function renderTexToSvg(latex: string): Promise {
+ // A fresh TeX input jax and document per call keeps parser state (e.g.
+ // \newcommand definitions, \begingroup scopes) from leaking across renders.
+ const tex = new TeX({
+ packages: RENDER_PACKAGES,
+ // By default MathJax typesets compile errors as red text; throw
+ // instead so callers get a typed failure with the original TeX message.
+ formatError(_jax: unknown, error: unknown) {
+ throw error;
+ },
+ });
+ const svg = new SVG({ fontData: MathJaxNewcmFont });
+ const document = mathjax.document("", {
+ InputJax: tex,
+ OutputJax: svg,
+ });
+
+ // convertPromise (not convert) so dynamically loaded font pieces can be
+ // awaited mid-conversion; see the asyncLoad/esm import above.
+ const converted = await document
+ .convertPromise(latex, {
+ display: true,
+ em: 16,
+ ex: 8,
+ containerWidth: 80,
+ })
+ .catch((error: unknown) => {
+ if (error instanceof TexError) {
+ throw new TexRenderError(error.message);
+ }
+ throw error;
+ });
+
+ const svgElement = adaptor.tags(converted, "svg")[0];
+ if (svgElement === undefined) {
+ throw new Error("MathJax did not produce an SVG element");
+ }
+
+ // Defensive fallback: formatError should surface every compile error as an
+ // exception, but fail loudly if an merror node slips through anyway.
+ const rendered = adaptor.outerHTML(svgElement);
+ if (rendered.includes("data-mjx-error")) {
+ const title = rendered.match(/\btitle=(['"])(.*?)\1/u)?.[2];
+ throw new TexRenderError(title ?? "Unknown LaTeX rendering error");
+ }
+
+ for (const textElement of adaptor.tags(svgElement, "text")) {
+ adaptor.setAttribute(textElement, "font-family", TEXT_FONT_FAMILY);
+ }
+ return adaptor.outerHTML(svgElement);
+}
+
+export async function renderTexToPng(latex: string): Promise {
+ const svg = await renderTexToSvg(latex);
+ const white = { r: 255, g: 255, b: 255 };
+ return sharp(Buffer.from(svg))
+ .resize({ height: 500 })
+ .flatten({ background: white })
+ .extend({
+ top: PADDING,
+ bottom: PADDING,
+ left: PADDING,
+ right: PADDING,
+ background: { ...white, alpha: 1 },
+ })
+ .png()
+ .toBuffer();
+}
diff --git a/discord/src/features/translate/index.test.ts b/discord/src/features/translate/index.test.ts
new file mode 100644
index 0000000..dad32e5
--- /dev/null
+++ b/discord/src/features/translate/index.test.ts
@@ -0,0 +1,35 @@
+/**
+ * Tests translation-language autocomplete and resolution without Discord transport calls.
+ */
+import { describe, expect, it } from "vitest";
+import { autocompleteLanguages, resolveLanguage } from "./index.js";
+
+describe("autocompleteLanguages", () => {
+ it("returns no choices for one character", () => {
+ expect(autocompleteLanguages("j")).toEqual([]);
+ });
+
+ it("resolves a two-letter code to its language name", () => {
+ expect(autocompleteLanguages("ja")).toEqual([
+ { name: "Japanese", value: "ja" },
+ ]);
+ });
+
+ it("never exceeds Discord's 25-choice limit", () => {
+ const names = Array.from(
+ { length: 30 },
+ (_, index) => `Test language ${index}`,
+ );
+ expect(autocompleteLanguages("test", names)).toHaveLength(25);
+ });
+});
+
+describe("resolveLanguage", () => {
+ it("accepts both codes and language names", () => {
+ expect(resolveLanguage("ja")).toEqual({ code: "ja", name: "Japanese" });
+ expect(resolveLanguage("Japanese")).toEqual({
+ code: "ja",
+ name: "Japanese",
+ });
+ });
+});
diff --git a/discord/src/features/translate/index.ts b/discord/src/features/translate/index.ts
new file mode 100644
index 0000000..6d648cb
--- /dev/null
+++ b/discord/src/features/translate/index.ts
@@ -0,0 +1,208 @@
+/**
+ * Implements the translation feature and its Discord presentation over OpenAI.
+ */
+import {
+ type AttachmentBuilder,
+ ContainerBuilder,
+ FileBuilder,
+ SlashCommandBuilder,
+ TextDisplayBuilder,
+} from "discord.js";
+import ISO6391 from "iso-639-1";
+import OpenAI from "openai";
+import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
+import {
+ SUCCESS_ACCENT,
+ textComponent,
+} from "../../platform/discord/components.js";
+import { getFixedT } from "../../shared/i18n.js";
+import type {
+ Feature,
+ FeatureDependencies,
+ OutgoingReply,
+} from "../../types.js";
+
+export interface LanguageChoice {
+ readonly name: string;
+ readonly value: string;
+}
+
+export interface ResolvedLanguage {
+ readonly code: string;
+ readonly name: string;
+}
+
+export function autocompleteLanguages(
+ input: string,
+ languageNames: readonly string[] = ISO6391.getAllNames(),
+): LanguageChoice[] {
+ const normalized = input.trim().toLowerCase();
+ if (normalized.length <= 1) {
+ return [];
+ }
+ if (normalized.length === 2) {
+ const name = ISO6391.getName(normalized);
+ return name.length === 0 ? [] : [{ name, value: normalized }];
+ }
+
+ // Discord caps autocomplete responses at 25 choices.
+ return languageNames
+ .filter((name) => name.toLowerCase().startsWith(normalized))
+ .sort(
+ (left, right) => left.length - right.length || left.localeCompare(right),
+ )
+ .slice(0, 25)
+ .map((name) => ({ name, value: ISO6391.getCode(name) }));
+}
+
+export function resolveLanguage(input: string): ResolvedLanguage | undefined {
+ const normalized = input.trim().toLowerCase();
+ const code = ISO6391.validate(normalized)
+ ? normalized
+ : ISO6391.getCode(input.trim());
+ if (code.length === 0) {
+ return undefined;
+ }
+ return { code, name: ISO6391.getName(code) };
+}
+
+interface TranslationCompletionClient {
+ create(options: {
+ readonly model: string;
+ readonly messages: readonly ChatCompletionMessageParam[];
+ }): Promise<{
+ readonly choices: readonly {
+ readonly message: { readonly content: string | null };
+ }[];
+ }>;
+}
+
+export function createTranslateFeature(
+ dependencies: FeatureDependencies,
+ completionClient?: TranslationCompletionClient,
+): Feature | undefined {
+ if (dependencies.env.openAIApiKey === undefined) {
+ return undefined;
+ }
+ const completion =
+ completionClient ??
+ new OpenAI({ apiKey: dependencies.env.openAIApiKey }).chat.completions;
+ const command = new SlashCommandBuilder()
+ .setName("translate")
+ .setDescription("Translate text")
+ .setDescriptionLocalizations({ ja: "テキストを翻訳します" });
+ command.addStringOption((option) =>
+ option
+ .setName("text")
+ .setDescription("Text to translate")
+ .setDescriptionLocalizations({ ja: "翻訳するテキスト" })
+ .setRequired(true),
+ );
+ command.addStringOption((option) =>
+ option
+ .setName("to")
+ .setDescription("Language to translate to")
+ .setDescriptionLocalizations({ ja: "翻訳先の言語" })
+ .setRequired(true)
+ .setAutocomplete(true),
+ );
+
+ return {
+ id: "translate",
+ slashCommands: [
+ {
+ data: command,
+ async autocomplete(interaction) {
+ await interaction.respond(
+ autocompleteLanguages(String(interaction.options.getFocused())),
+ );
+ },
+ async execute(interaction, context) {
+ if (!interaction.isChatInputCommand()) {
+ return undefined;
+ }
+ const t = getFixedT(context.locale);
+ const language = resolveLanguage(
+ interaction.options.getString("to", true),
+ );
+ if (language === undefined) {
+ return {
+ kind: "plain",
+ content: t("translate.invalidLanguage"),
+ ephemeral: true,
+ };
+ }
+
+ const original = interaction.options.getString("text", true);
+ // Acknowledge within Discord's three-second interaction window before the API call.
+ await interaction.deferReply();
+ const response = await completion.create({
+ model: dependencies.env.openAITranslateModel,
+ messages: [
+ {
+ role: "system",
+ content:
+ "This is a direct translation task. " +
+ `Translate the following text to ${language.name}. ` +
+ "Do not add any additional comments or language indicators.",
+ },
+ { role: "user", content: original },
+ ],
+ });
+ return translationReply(
+ original,
+ response.choices[0]?.message.content ?? "",
+ language.name,
+ context.locale,
+ );
+ },
+ },
+ ],
+ };
+}
+
+function addSection(
+ container: ContainerBuilder,
+ files: AttachmentBuilder[],
+ heading: string,
+ content: string,
+ filename: string,
+): void {
+ const rendered = textComponent(content, filename);
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(`### ${heading}`),
+ );
+ if (rendered.component instanceof FileBuilder) {
+ container.addFileComponents(rendered.component);
+ } else {
+ rendered.component.setContent(content.length === 0 ? "\u200b" : content);
+ container.addTextDisplayComponents(rendered.component);
+ }
+ files.push(...rendered.files);
+}
+
+function translationReply(
+ original: string,
+ translated: string,
+ languageName: string,
+ locale: string,
+): OutgoingReply {
+ const t = getFixedT(locale);
+ const container = new ContainerBuilder().setAccentColor(SUCCESS_ACCENT);
+ const files: AttachmentBuilder[] = [];
+ addSection(
+ container,
+ files,
+ t("translate.originalHeading"),
+ original,
+ "original.txt",
+ );
+ addSection(
+ container,
+ files,
+ t("translate.translatedHeading", { language: languageName }),
+ translated,
+ "translated.txt",
+ );
+ return { kind: "components-v2", components: [container], files };
+}
diff --git a/discord/src/features/wolfram/index.ts b/discord/src/features/wolfram/index.ts
new file mode 100644
index 0000000..1d71c1f
--- /dev/null
+++ b/discord/src/features/wolfram/index.ts
@@ -0,0 +1,147 @@
+/**
+ * Implements the Wolfram feature and paginates adapter results for Discord.
+ */
+import {
+ ContainerBuilder,
+ MediaGalleryBuilder,
+ MediaGalleryItemBuilder,
+ SlashCommandBuilder,
+ TextDisplayBuilder,
+} from "discord.js";
+import {
+ FAILURE_ACCENT,
+ SUCCESS_ACCENT,
+} from "../../platform/discord/components.js";
+import { paginatedReply } from "../../platform/discord/paginator.js";
+import { getFixedT } from "../../shared/i18n.js";
+import type {
+ Feature,
+ FeatureDependencies,
+ OutgoingReply,
+} from "../../types.js";
+import { WolframClient, type WolframQueryResult } from "./wolframClient.js";
+
+export function createWolframFeature(
+ dependencies: FeatureDependencies,
+): Feature | undefined {
+ if (dependencies.env.wolframAppId === undefined) {
+ return undefined;
+ }
+ const client = new WolframClient(dependencies.env.wolframAppId);
+ const slashCommand = new SlashCommandBuilder()
+ .setName("wolf")
+ .setDescription("Query Wolfram|Alpha")
+ .setDescriptionLocalizations({ ja: "Wolfram|Alpha に問い合わせます" });
+ slashCommand.addStringOption((option) =>
+ option
+ .setName("query")
+ .setDescription("Query to send to Wolfram|Alpha")
+ .setDescriptionLocalizations({
+ ja: "Wolfram|Alpha に送信するクエリ",
+ })
+ .setRequired(true),
+ );
+
+ const execute = async (
+ query: string,
+ userId: string,
+ locale: string,
+ ): Promise => {
+ const result = await client.query(query);
+ return wolframReply(result, userId, locale, dependencies);
+ };
+
+ return {
+ id: "wolfram",
+ prefixCommands: [
+ {
+ name: "wolf",
+ aliases: ["wolfram"],
+ execute(_message, args, context) {
+ return execute(args, context.userId, context.locale);
+ },
+ },
+ ],
+ slashCommands: [
+ {
+ data: slashCommand,
+ async execute(interaction, context) {
+ if (!interaction.isChatInputCommand()) {
+ return undefined;
+ }
+ // Acknowledge within Discord's three-second interaction window before the API call.
+ await interaction.deferReply();
+ return execute(
+ interaction.options.getString("query", true),
+ context.userId,
+ context.locale,
+ );
+ },
+ },
+ ],
+ };
+}
+
+function wolframReply(
+ result: WolframQueryResult,
+ userId: string,
+ locale: string,
+ dependencies: Pick,
+): OutgoingReply {
+ const t = getFixedT(locale);
+ if (!result.success) {
+ return {
+ kind: "components-v2",
+ components: [
+ new ContainerBuilder()
+ .setAccentColor(FAILURE_ACCENT)
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ `### ${t("wolfram.errorHeading")}\n${t("wolfram.notUnderstood")}`,
+ ),
+ ),
+ ],
+ };
+ }
+
+ const pages = result.pods.flatMap((pod) =>
+ pod.subpods.map((subpod) => {
+ const container = new ContainerBuilder()
+ .setAccentColor(SUCCESS_ACCENT)
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(`## ${pod.title}`),
+ );
+ if (subpod.plaintext.length > 0) {
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(subpod.plaintext),
+ );
+ }
+ if (subpod.img !== undefined) {
+ container.addMediaGalleryComponents(
+ new MediaGalleryBuilder().addItems(
+ new MediaGalleryItemBuilder().setURL(subpod.img.src),
+ ),
+ );
+ }
+ return container;
+ }),
+ );
+
+ if (pages.length === 0) {
+ return {
+ kind: "components-v2",
+ components: [
+ new ContainerBuilder()
+ .setAccentColor(FAILURE_ACCENT)
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(t("wolfram.noResults")),
+ ),
+ ],
+ };
+ }
+ return paginatedReply({
+ pages,
+ userId,
+ logger: dependencies.logger,
+ });
+}
diff --git a/discord/src/features/wolfram/wolframClient.test.ts b/discord/src/features/wolfram/wolframClient.test.ts
new file mode 100644
index 0000000..15d3cd3
--- /dev/null
+++ b/discord/src/features/wolfram/wolframClient.test.ts
@@ -0,0 +1,59 @@
+/**
+ * Tests the discord.js-free Wolfram adapter's response parsing and schema failures.
+ */
+import { describe, expect, it } from "vitest";
+import { parseWolframResponse, WolframResponseError } from "./wolframClient.js";
+
+describe("parseWolframResponse", () => {
+ it("returns typed pods and subpods", () => {
+ expect(
+ parseWolframResponse({
+ queryresult: {
+ success: true,
+ pods: [
+ {
+ title: "Result",
+ subpods: [
+ {
+ plaintext: "42",
+ img: { src: "https://example.invalid/result.png" },
+ },
+ ],
+ },
+ ],
+ },
+ }),
+ ).toEqual({
+ success: true,
+ pods: [
+ {
+ title: "Result",
+ subpods: [
+ {
+ plaintext: "42",
+ img: { src: "https://example.invalid/result.png" },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("accepts an unsuccessful query without pods", () => {
+ expect(parseWolframResponse({ queryresult: { success: false } })).toEqual({
+ success: false,
+ pods: [],
+ });
+ });
+
+ it("rejects malformed nested data", () => {
+ expect(() =>
+ parseWolframResponse({
+ queryresult: {
+ success: true,
+ pods: [{ title: "Result", subpods: [{ plaintext: 42 }] }],
+ },
+ }),
+ ).toThrow(WolframResponseError);
+ });
+});
diff --git a/discord/src/features/wolfram/wolframClient.ts b/discord/src/features/wolfram/wolframClient.ts
new file mode 100644
index 0000000..447cce6
--- /dev/null
+++ b/discord/src/features/wolfram/wolframClient.ts
@@ -0,0 +1,107 @@
+/**
+ * Adapts Wolfram|Alpha HTTP responses into feature-domain query results.
+ * The adapter is discord.js-free and unit-tested directly.
+ */
+const WOLFRAM_QUERY_URL = "http://api.wolframalpha.com/v2/query";
+
+export interface WolframImage {
+ readonly src: string;
+}
+
+export interface WolframSubpod {
+ readonly plaintext: string;
+ readonly img?: WolframImage;
+}
+
+export interface WolframPod {
+ readonly title: string;
+ readonly subpods: readonly WolframSubpod[];
+}
+
+export interface WolframQueryResult {
+ readonly success: boolean;
+ readonly pods: readonly WolframPod[];
+}
+
+export class WolframResponseError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "WolframResponseError";
+ }
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function parseSubpod(value: unknown): WolframSubpod {
+ if (!isRecord(value) || typeof value.plaintext !== "string") {
+ throw new WolframResponseError("invalid Wolfram|Alpha subpod");
+ }
+ if (value.img === undefined) {
+ return { plaintext: value.plaintext };
+ }
+ if (!isRecord(value.img) || typeof value.img.src !== "string") {
+ throw new WolframResponseError("invalid Wolfram|Alpha subpod image");
+ }
+ return { plaintext: value.plaintext, img: { src: value.img.src } };
+}
+
+function parsePod(value: unknown): WolframPod {
+ if (
+ !isRecord(value) ||
+ typeof value.title !== "string" ||
+ !Array.isArray(value.subpods)
+ ) {
+ throw new WolframResponseError("invalid Wolfram|Alpha pod");
+ }
+ return {
+ title: value.title,
+ subpods: value.subpods.map(parseSubpod),
+ };
+}
+
+export function parseWolframResponse(value: unknown): WolframQueryResult {
+ if (!isRecord(value) || !isRecord(value.queryresult)) {
+ throw new WolframResponseError("missing Wolfram|Alpha queryresult");
+ }
+ const queryResult = value.queryresult;
+ if (typeof queryResult.success !== "boolean") {
+ throw new WolframResponseError("invalid Wolfram|Alpha success flag");
+ }
+ if (queryResult.pods === undefined && !queryResult.success) {
+ return { success: false, pods: [] };
+ }
+ if (!Array.isArray(queryResult.pods)) {
+ throw new WolframResponseError("invalid Wolfram|Alpha pods");
+ }
+ return {
+ success: queryResult.success,
+ pods: queryResult.pods.map(parsePod),
+ };
+}
+
+export class WolframClient {
+ readonly #appId: string;
+ readonly #fetch: typeof fetch;
+
+ constructor(appId: string, fetchImplementation: typeof fetch = fetch) {
+ this.#appId = appId;
+ this.#fetch = fetchImplementation;
+ }
+
+ async query(input: string): Promise {
+ const url = new URL(WOLFRAM_QUERY_URL);
+ url.search = new URLSearchParams({
+ input,
+ format: "image,plaintext",
+ output: "JSON",
+ appid: this.#appId,
+ }).toString();
+ const response = await this.#fetch(url);
+ if (!response.ok) {
+ throw new Error(`Wolfram|Alpha returned HTTP status ${response.status}`);
+ }
+ return parseWolframResponse(await response.json());
+ }
+}
diff --git a/discord/src/index.ts b/discord/src/index.ts
new file mode 100644
index 0000000..9c9bb9a
--- /dev/null
+++ b/discord/src/index.ts
@@ -0,0 +1,172 @@
+/**
+ * Composes the selected bot profile, shared services, features, and Discord platform router.
+ * This is the application's single composition root.
+ */
+import { Client, GatewayIntentBits, Partials, REST } from "discord.js";
+import { getBotProfile } from "./config.js";
+import { loadEnv } from "./env.js";
+import { createCodeFeature } from "./features/code/index.js";
+import { createMiscFeature } from "./features/misc/index.js";
+import { createPingFeature } from "./features/ping/index.js";
+import { createPrivacyFeature } from "./features/privacy/index.js";
+import { createTexFeature } from "./features/tex/index.js";
+import { createTranslateFeature } from "./features/translate/index.js";
+import { createWolframFeature } from "./features/wolfram/index.js";
+import { ErrorPresenter } from "./platform/discord/errorPresenter.js";
+import { ReplyCoordinator } from "./platform/discord/replyCoordinator.js";
+import { DiscordRouter } from "./platform/discord/router.js";
+import { getFixedT } from "./shared/i18n.js";
+import { createLogger } from "./shared/logger.js";
+import {
+ LocalFileState,
+ OptOutUsers,
+ S3ObjectState,
+ type StateBackend,
+} from "./shared/state.js";
+import { UsageStats } from "./shared/usageStats.js";
+import type { Feature, FeatureFactory, FeatureId } from "./types.js";
+
+const featureFactories: Record = {
+ tex: createTexFeature,
+ code: createCodeFeature,
+ privacy: createPrivacyFeature,
+ wolfram: createWolframFeature,
+ misc: createMiscFeature,
+ translate: createTranslateFeature,
+ ping: createPingFeature,
+};
+
+export async function bootstrap(): Promise {
+ const env = loadEnv();
+ const profile = getBotProfile(env.botName);
+ const logger = createLogger();
+ getFixedT(profile.defaultLocale);
+
+ let stateBackend: StateBackend;
+ let usageStatsBackend: StateBackend;
+ if (env.state.backend === "local") {
+ stateBackend = new LocalFileState(env.state.filePath);
+ usageStatsBackend = new LocalFileState(env.state.usageStatsFilePath);
+ } else {
+ stateBackend = new S3ObjectState({
+ endpoint: env.state.endpoint,
+ region: env.state.region,
+ bucket: env.state.bucket,
+ key: env.state.key,
+ credentials: {
+ accessKeyId: env.state.accessKeyId,
+ secretAccessKey: env.state.secretAccessKey,
+ },
+ });
+ usageStatsBackend = new S3ObjectState({
+ endpoint: env.state.endpoint,
+ region: env.state.region,
+ bucket: env.state.bucket,
+ key: env.state.usageStatsKey,
+ credentials: {
+ accessKeyId: env.state.accessKeyId,
+ secretAccessKey: env.state.secretAccessKey,
+ },
+ });
+ }
+ const optOutUsers = new OptOutUsers(stateBackend);
+ await optOutUsers.init();
+ const usageStats = new UsageStats(usageStatsBackend, { logger });
+ await usageStats.init();
+ const client = new Client({
+ intents: [
+ GatewayIntentBits.Guilds,
+ GatewayIntentBits.GuildMessages,
+ GatewayIntentBits.DirectMessages,
+ GatewayIntentBits.MessageContent,
+ ],
+ partials: [Partials.Channel, Partials.Message],
+ });
+ let shuttingDown = false;
+ const shutdown = async (): Promise => {
+ if (shuttingDown) {
+ return;
+ }
+ shuttingDown = true;
+ try {
+ await usageStats.dispose();
+ } catch (error) {
+ logger.error({ error }, "Failed to flush usage statistics on shutdown");
+ }
+ // destroy() is async in discord.js v14; await it so the gateway close frame is sent.
+ await client.destroy().catch(() => undefined);
+ process.exit(0);
+ };
+ process.once("SIGTERM", () => {
+ void shutdown();
+ });
+ process.once("SIGINT", () => {
+ void shutdown();
+ });
+
+ const featureDependencies = {
+ optOutUsers,
+ usageStats,
+ logger,
+ env,
+ client,
+ };
+ if (env.botName === "gaato-bot") {
+ if (env.wolframAppId === undefined) {
+ logger.warn(
+ { envName: "WOLFRAM_APPID", feature: "wolfram" },
+ "gaato-bot feature disabled because an environment variable is missing",
+ );
+ }
+ if (env.openAIApiKey === undefined) {
+ logger.warn(
+ {
+ envName: "OPENAI_API_KEY",
+ features: ["misc", "translate"],
+ },
+ "gaato-bot features disabled because an environment variable is missing",
+ );
+ }
+ }
+ const rest = new REST({ version: "10" }).setToken(env.token);
+ const features = [
+ ...profile.features.map((id) => featureFactories[id](featureDependencies)),
+ featureFactories.ping(featureDependencies),
+ ].filter((feature): feature is Feature => feature !== undefined);
+
+ for (const feature of features) {
+ await feature.init?.();
+ }
+
+ const replyCoordinator = new ReplyCoordinator(100);
+ const errorPresenter = new ErrorPresenter({
+ client,
+ logger,
+ logChannelId: env.logChannelId,
+ supportServerLink: env.supportServerLink,
+ });
+ const router = new DiscordRouter({
+ client,
+ rest,
+ profile,
+ features,
+ replyCoordinator,
+ errorPresenter,
+ logger,
+ usageStats,
+ messageGate: (message) => !optOutUsers.has(message.author.id),
+ });
+ router.bind();
+
+ try {
+ await client.login(env.token);
+ } catch (error) {
+ logger.error(
+ { error, tokenEnvName: env.tokenEnvName },
+ "Discord login failed",
+ );
+ throw error;
+ }
+}
+
+await bootstrap();
diff --git a/discord/src/platform/discord/components.ts b/discord/src/platform/discord/components.ts
new file mode 100644
index 0000000..ced1649
--- /dev/null
+++ b/discord/src/platform/discord/components.ts
@@ -0,0 +1,74 @@
+/**
+ * Builds reusable Discord Components V2 presentation primitives for the platform layer.
+ */
+import {
+ ActionRowBuilder,
+ AttachmentBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ ContainerBuilder,
+ FileBuilder,
+ TextDisplayBuilder,
+} from "discord.js";
+
+export const SUCCESS_ACCENT = 0x2ecc71;
+export const FAILURE_ACCENT = 0xe74c3c;
+export const TEXT_FILE_CHARACTER_LIMIT = 1_000;
+export const TEXT_FILE_LINE_LIMIT = 100;
+
+export function successContainer(text: string): ContainerBuilder {
+ return new ContainerBuilder()
+ .setAccentColor(SUCCESS_ACCENT)
+ .addTextDisplayComponents(new TextDisplayBuilder().setContent(text));
+}
+
+export function failureContainer(text: string): ContainerBuilder {
+ return new ContainerBuilder()
+ .setAccentColor(FAILURE_ACCENT)
+ .addTextDisplayComponents(new TextDisplayBuilder().setContent(text));
+}
+
+export function deleteButton(userId: string): ButtonBuilder {
+ return new ButtonBuilder()
+ .setCustomId(`delete:${userId}`)
+ .setLabel("Delete")
+ .setStyle(ButtonStyle.Danger);
+}
+
+export function deleteButtonRow(
+ userId: string,
+): ActionRowBuilder {
+ return new ActionRowBuilder().addComponents(
+ deleteButton(userId),
+ );
+}
+
+export interface TextComponentResult {
+ readonly component: TextDisplayBuilder | FileBuilder;
+ readonly files: readonly AttachmentBuilder[];
+ readonly isFile: boolean;
+}
+
+export function textComponent(
+ text: string,
+ filename = "output.txt",
+): TextComponentResult {
+ const lineCount = text.length === 0 ? 0 : text.split("\n").length;
+ // Components V2 caps total text at 4,000 characters, so >1,000 chars or >100 lines use a file.
+ if (
+ text.length > TEXT_FILE_CHARACTER_LIMIT ||
+ lineCount > TEXT_FILE_LINE_LIMIT
+ ) {
+ return {
+ component: new FileBuilder().setURL(`attachment://${filename}`),
+ files: [new AttachmentBuilder(Buffer.from(text), { name: filename })],
+ isFile: true,
+ };
+ }
+
+ return {
+ component: new TextDisplayBuilder().setContent(text),
+ files: [],
+ isFile: false,
+ };
+}
diff --git a/discord/src/platform/discord/context.ts b/discord/src/platform/discord/context.ts
new file mode 100644
index 0000000..b5d032b
--- /dev/null
+++ b/discord/src/platform/discord/context.ts
@@ -0,0 +1,51 @@
+/**
+ * Normalizes Discord interactions and messages into the platform's request context.
+ */
+import type {
+ ButtonInteraction,
+ ChatInputCommandInteraction,
+ ContextMenuCommandInteraction,
+ Message,
+ ModalSubmitInteraction,
+} from "discord.js";
+
+export type ReplyTarget =
+ | Message
+ | ChatInputCommandInteraction
+ | ContextMenuCommandInteraction
+ | ButtonInteraction
+ | ModalSubmitInteraction;
+
+export interface RequestContext {
+ readonly userId: string;
+ readonly locale: string;
+ readonly inGuild: boolean;
+ readonly replyTarget: ReplyTarget;
+}
+
+type RequestInteraction = Exclude;
+
+export function contextForInteraction(
+ interaction: RequestInteraction,
+ defaultLocale: string,
+): RequestContext {
+ return {
+ userId: interaction.user.id,
+ locale:
+ interaction.locale || interaction.guild?.preferredLocale || defaultLocale,
+ inGuild: interaction.inGuild(),
+ replyTarget: interaction,
+ };
+}
+
+export function contextForMessage(
+ message: Message,
+ defaultLocale: string,
+): RequestContext {
+ return {
+ userId: message.author.id,
+ locale: message.guild?.preferredLocale ?? defaultLocale,
+ inGuild: message.inGuild(),
+ replyTarget: message,
+ };
+}
diff --git a/discord/src/platform/discord/errorPresenter.ts b/discord/src/platform/discord/errorPresenter.ts
new file mode 100644
index 0000000..8575afc
--- /dev/null
+++ b/discord/src/platform/discord/errorPresenter.ts
@@ -0,0 +1,118 @@
+/**
+ * Converts handler failures into user replies and reports diagnostics through Discord.
+ * This platform service keeps error delivery separate from feature handlers.
+ */
+import {
+ AttachmentBuilder,
+ type Client,
+ DiscordAPIError,
+ RESTJSONErrorCodes,
+} from "discord.js";
+import { getFixedT } from "../../shared/i18n.js";
+import type { AppLogger } from "../../shared/logger.js";
+import type { OutgoingReply } from "../../types.js";
+import { failureContainer } from "./components.js";
+import type { RequestContext } from "./context.js";
+
+export class InvalidInputError extends Error {}
+
+export interface ErrorPresenterOptions {
+ readonly client: Client;
+ readonly logger: AppLogger;
+ readonly logChannelId: string;
+ readonly supportServerLink: string;
+}
+
+export class ErrorPresenter {
+ readonly #client: Client;
+ readonly #logger: AppLogger;
+ readonly #logChannelId: string;
+ readonly #supportServerLink: string;
+
+ constructor(options: ErrorPresenterOptions) {
+ this.#client = options.client;
+ this.#logger = options.logger;
+ this.#logChannelId = options.logChannelId;
+ this.#supportServerLink = options.supportServerLink;
+ }
+
+ async present(
+ error: unknown,
+ context: RequestContext,
+ invocation: string,
+ ): Promise {
+ const normalized =
+ error instanceof Error ? error : new Error(String(error));
+ await this.#logToDiscord(invocation, normalized);
+
+ const t = getFixedT(context.locale);
+ const message =
+ error instanceof InvalidInputError
+ ? `## ${t("errors.invalidInput")}\n${error.message}`
+ : `## ${t("errors.unhandled")}\n${t("errors.report", {
+ supportLink: this.#supportServerLink,
+ })}`;
+
+ return {
+ kind: "components-v2",
+ components: [failureContainer(message)],
+ };
+ }
+
+ async #logToDiscord(invocation: string, error: Error): Promise {
+ const stack = error.stack ?? `${error.name}: ${error.message}`;
+ // Try the channel cache, then an API fetch; any lookup or send failure falls back to Pino.
+ let channel = this.#client.channels.cache.get(this.#logChannelId);
+
+ if (channel === undefined) {
+ try {
+ channel =
+ (await this.#client.channels.fetch(this.#logChannelId)) ?? undefined;
+ } catch (fetchError) {
+ this.#logger.error(
+ {
+ error: fetchError,
+ originalError: stack,
+ channelId: this.#logChannelId,
+ },
+ "Unable to fetch Discord log channel",
+ );
+ return;
+ }
+ }
+
+ if (channel === undefined || !channel.isSendable()) {
+ this.#logger.error(
+ { originalError: stack, channelId: this.#logChannelId },
+ "Discord log channel is not sendable",
+ );
+ return;
+ }
+
+ const content = `\`\`\`\n${invocation.slice(0, 1_900)}\n\`\`\``;
+ try {
+ await channel.send({
+ content,
+ files: [
+ new AttachmentBuilder(Buffer.from(stack), {
+ name: "error.txt",
+ }),
+ ],
+ });
+ } catch (sendError) {
+ const forbidden =
+ sendError instanceof DiscordAPIError &&
+ sendError.code === RESTJSONErrorCodes.MissingPermissions;
+ this.#logger.error(
+ {
+ error: sendError,
+ originalError: stack,
+ channelId: this.#logChannelId,
+ },
+ forbidden
+ ? "Bot is missing access to Discord log channel"
+ : "Failed to send error to Discord log channel",
+ );
+ }
+ }
+}
diff --git a/discord/src/platform/discord/paginator.ts b/discord/src/platform/discord/paginator.ts
new file mode 100644
index 0000000..4bec702
--- /dev/null
+++ b/discord/src/platform/discord/paginator.ts
@@ -0,0 +1,133 @@
+/**
+ * Adds process-local, collector-driven pagination to platform reply components.
+ */
+import { randomUUID } from "node:crypto";
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ ComponentType,
+ type ContainerBuilder,
+ type Message,
+ MessageFlags,
+} from "discord.js";
+import type { AppLogger } from "../../shared/logger.js";
+import type { OutgoingReply } from "../../types.js";
+import { deleteButtonRow } from "./components.js";
+
+export const PAGINATOR_TIMEOUT_MS = 10 * 60 * 1_000;
+
+interface StoppableCollector {
+ stop(reason?: string): void;
+}
+
+const activeCollectors = new WeakMap();
+
+export function cancelPaginator(message: Message): void {
+ activeCollectors.get(message)?.stop("replaced");
+ activeCollectors.delete(message);
+}
+
+export interface PaginatorOptions {
+ readonly pages: readonly ContainerBuilder[];
+ readonly userId: string;
+ readonly logger: AppLogger;
+ readonly timeoutMs?: number;
+}
+
+function navigationRow(
+ token: string,
+ pageIndex: number,
+ pageCount: number,
+ disabled = false,
+): ActionRowBuilder {
+ return new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId(`pg:${token}:previous`)
+ .setLabel("◀")
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(disabled || pageIndex === 0),
+ new ButtonBuilder()
+ .setCustomId(`pg:${token}:page`)
+ .setLabel(`${pageIndex + 1} / ${pageCount}`)
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(true),
+ new ButtonBuilder()
+ .setCustomId(`pg:${token}:next`)
+ .setLabel("▶")
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(disabled || pageIndex === pageCount - 1),
+ );
+}
+
+export function paginatedReply(options: PaginatorOptions): OutgoingReply {
+ if (options.pages.length === 0) {
+ throw new Error("paginator requires at least one page");
+ }
+
+ // The collector owns pg: interactions, so the reserved namespace bypasses the central router.
+ // Page state lives only in this process; after a restart, old buttons stop working by design.
+ const token = randomUUID();
+ let pageIndex = 0;
+ const messageComponents = (disabled = false) => [
+ options.pages[pageIndex] as ContainerBuilder,
+ navigationRow(token, pageIndex, options.pages.length, disabled),
+ deleteButtonRow(options.userId),
+ ];
+
+ return {
+ kind: "components-v2",
+ components: [
+ options.pages[0] as ContainerBuilder,
+ navigationRow(token, 0, options.pages.length),
+ ],
+ async onDelivered(message) {
+ // A message-scoped collector keeps each paginator's mutable page index out of the router.
+ const collector = message.createMessageComponentCollector({
+ componentType: ComponentType.Button,
+ filter: (interaction) =>
+ interaction.customId.startsWith(`pg:${token}:`),
+ time: options.timeoutMs ?? PAGINATOR_TIMEOUT_MS,
+ });
+ activeCollectors.set(message, collector);
+
+ collector.on("collect", (interaction) => {
+ void (async () => {
+ if (interaction.user.id !== options.userId) {
+ await interaction.deferUpdate();
+ return;
+ }
+
+ if (interaction.customId.endsWith(":previous")) {
+ pageIndex = Math.max(0, pageIndex - 1);
+ } else if (interaction.customId.endsWith(":next")) {
+ pageIndex = Math.min(options.pages.length - 1, pageIndex + 1);
+ }
+ await interaction.update({ components: messageComponents() });
+ })().catch((error: unknown) => {
+ options.logger.warn({ error }, "Paginator interaction failed");
+ });
+ });
+
+ collector.on("end", (_collected, reason) => {
+ if (reason === "replaced") {
+ return;
+ }
+ if (activeCollectors.get(message) === collector) {
+ activeCollectors.delete(message);
+ }
+ void message
+ .edit({
+ components: messageComponents(true),
+ flags: MessageFlags.IsComponentsV2,
+ })
+ .catch((error: unknown) => {
+ options.logger.debug(
+ { error },
+ "Unable to disable expired paginator",
+ );
+ });
+ });
+ },
+ };
+}
diff --git a/discord/src/platform/discord/replyCoordinator.test.ts b/discord/src/platform/discord/replyCoordinator.test.ts
new file mode 100644
index 0000000..82ea076
--- /dev/null
+++ b/discord/src/platform/discord/replyCoordinator.test.ts
@@ -0,0 +1,99 @@
+/**
+ * Exercises latest-wins generations and editable reply reuse in the Discord platform service.
+ */
+import type { MessageEditOptions } from "discord.js";
+import { describe, expect, it, vi } from "vitest";
+import type { ReplyTarget } from "./context.js";
+import {
+ type EditableReply,
+ ReplyCoordinator,
+ type ReplyTransport,
+} from "./replyCoordinator.js";
+
+function editableReply(): EditableReply {
+ return {
+ edit: vi.fn(async (_options: MessageEditOptions) => undefined),
+ delete: vi.fn(async () => undefined),
+ };
+}
+
+describe("ReplyCoordinator generations", () => {
+ it("drops late results from older generations and edits the tracked reply", async () => {
+ const reply = editableReply();
+ const transport: ReplyTransport = {
+ send: vi.fn(async () => reply),
+ edit: vi.fn(async (target, payload) => {
+ await target.edit({ content: payload.content });
+ }),
+ delete: vi.fn(async (target) => {
+ await target.delete();
+ }),
+ };
+ const coordinator = new ReplyCoordinator(100, transport);
+ const target = {} as ReplyTarget;
+ const firstGeneration = coordinator.begin("code", "source");
+ const latestGeneration = coordinator.begin("code", "source");
+
+ await expect(
+ coordinator.deliver({
+ featureId: "code",
+ target,
+ userId: "user",
+ outgoing: { kind: "plain", content: "old" },
+ sourceMessageId: "source",
+ generation: firstGeneration,
+ }),
+ ).resolves.toBe("stale");
+ expect(transport.send).not.toHaveBeenCalled();
+
+ await expect(
+ coordinator.deliver({
+ featureId: "code",
+ target,
+ userId: "user",
+ outgoing: { kind: "plain", content: "latest" },
+ sourceMessageId: "source",
+ generation: latestGeneration,
+ }),
+ ).resolves.toBe("sent");
+
+ const editedGeneration = coordinator.begin("code", "source");
+ await expect(
+ coordinator.deliver({
+ featureId: "code",
+ target,
+ userId: "user",
+ outgoing: { kind: "plain", content: "edited" },
+ sourceMessageId: "source",
+ generation: editedGeneration,
+ }),
+ ).resolves.toBe("edited");
+
+ expect(transport.send).toHaveBeenCalledTimes(1);
+ expect(transport.edit).toHaveBeenCalledTimes(1);
+ });
+
+ it("removes tracked replies when the invocation is deleted", async () => {
+ const reply = editableReply();
+ const transport: ReplyTransport = {
+ send: vi.fn(async () => reply),
+ edit: vi.fn(async () => undefined),
+ delete: vi.fn(async () => undefined),
+ };
+ const coordinator = new ReplyCoordinator(100, transport);
+ const generation = coordinator.begin("tex", "source");
+ await coordinator.deliver({
+ featureId: "tex",
+ target: {} as ReplyTarget,
+ userId: "user",
+ outgoing: { kind: "plain", content: "result" },
+ sourceMessageId: "source",
+ generation,
+ });
+
+ await coordinator.deleteReplies("source");
+
+ expect(transport.delete).toHaveBeenCalledWith(reply);
+ expect(coordinator.isTracked("tex", "source")).toBe(false);
+ });
+});
diff --git a/discord/src/platform/discord/replyCoordinator.ts b/discord/src/platform/discord/replyCoordinator.ts
new file mode 100644
index 0000000..493494f
--- /dev/null
+++ b/discord/src/platform/discord/replyCoordinator.ts
@@ -0,0 +1,253 @@
+/**
+ * Delivers feature reply values through Discord and tracks editable message replies.
+ * This platform service applies latest-wins coordination to message edits.
+ */
+import {
+ type BaseMessageOptions,
+ type InteractionEditReplyOptions,
+ type InteractionReplyOptions,
+ Message,
+ type MessageEditOptions,
+ MessageFlags,
+ type MessageReplyOptions,
+} from "discord.js";
+import { LimitedSizeMap } from "../../shared/limitedMap.js";
+import type { FeatureId, OutgoingReply } from "../../types.js";
+import { deleteButtonRow } from "./components.js";
+import type { ReplyTarget } from "./context.js";
+import { cancelPaginator } from "./paginator.js";
+
+export interface EditableReply {
+ edit(options: MessageEditOptions): Promise;
+ delete(): Promise;
+}
+
+export interface ReplyPayload {
+ readonly kind: OutgoingReply["kind"];
+ readonly content?: string;
+ readonly components: NonNullable;
+ readonly files?: BaseMessageOptions["files"];
+ readonly ephemeral: boolean;
+ readonly replaceAttachments: boolean;
+}
+
+export interface ReplyTransport {
+ send(target: ReplyTarget, payload: ReplyPayload): Promise;
+ edit(reply: EditableReply, payload: ReplyPayload): Promise;
+ delete(reply: EditableReply): Promise;
+}
+
+function messageFlags(payload: ReplyPayload): number | undefined {
+ let flags =
+ payload.kind === "components-v2" ? MessageFlags.IsComponentsV2 : 0;
+ if (payload.ephemeral) {
+ flags |= MessageFlags.Ephemeral;
+ }
+ return flags === 0 ? undefined : flags;
+}
+
+function editFlags(
+ payload: ReplyPayload,
+): MessageFlags.IsComponentsV2 | undefined {
+ return payload.kind === "components-v2"
+ ? MessageFlags.IsComponentsV2
+ : undefined;
+}
+
+const discordReplyTransport: ReplyTransport = {
+ async send(target, payload) {
+ if (target instanceof Message) {
+ const options: MessageReplyOptions = {
+ content: payload.content,
+ components: payload.components,
+ files: payload.files,
+ flags:
+ payload.kind === "components-v2"
+ ? MessageFlags.IsComponentsV2
+ : undefined,
+ };
+ return target.reply(options);
+ }
+
+ if (target.deferred || target.replied) {
+ const options: InteractionEditReplyOptions = {
+ content: payload.content,
+ components: payload.components,
+ files: payload.files,
+ // API v10 accepts an empty attachment set to replace files left by the prior reply.
+ attachments: payload.replaceAttachments ? [] : undefined,
+ flags: editFlags(payload),
+ };
+ await target.editReply(options);
+ return target.fetchReply();
+ }
+
+ const options: InteractionReplyOptions = {
+ content: payload.content,
+ components: payload.components,
+ files: payload.files,
+ flags: messageFlags(payload),
+ };
+ await target.reply(options);
+ return target.fetchReply();
+ },
+
+ async edit(reply, payload) {
+ const options: MessageEditOptions = {
+ content: payload.content ?? null,
+ components: payload.components,
+ files: payload.files,
+ // API v10 attachment replacement needs [] or stale files survive a message edit.
+ attachments: [],
+ flags: editFlags(payload),
+ };
+ await reply.edit(options);
+ },
+
+ async delete(reply) {
+ await reply.delete();
+ },
+};
+
+interface ReplyState {
+ readonly generation: number;
+ readonly reply?: EditableReply;
+ readonly kind?: OutgoingReply["kind"];
+}
+
+export interface DeliveryRequest {
+ readonly featureId: FeatureId;
+ readonly target: ReplyTarget;
+ readonly userId: string;
+ readonly outgoing: OutgoingReply;
+ readonly sourceMessageId?: string;
+ readonly generation?: number;
+}
+
+export type DeliveryResult = "sent" | "edited" | "stale";
+
+export class ReplyCoordinator {
+ readonly #capacity: number;
+ readonly #transport: ReplyTransport;
+ // Per-feature caps bound source-message tracking memory in long-running processes.
+ readonly #trackers = new Map>();
+
+ constructor(
+ capacity = 100,
+ transport: ReplyTransport = discordReplyTransport,
+ ) {
+ this.#capacity = capacity;
+ this.#transport = transport;
+ }
+
+ begin(featureId: FeatureId, sourceMessageId: string): number {
+ const tracker = this.#tracker(featureId);
+ const current = tracker.get(sourceMessageId);
+ // Each source edit starts a generation; only the latest async invocation may publish.
+ const generation = (current?.generation ?? 0) + 1;
+ tracker.set(sourceMessageId, { ...current, generation });
+ return generation;
+ }
+
+ isTracked(featureId: FeatureId, sourceMessageId: string): boolean {
+ return this.#trackers.get(featureId)?.has(sourceMessageId) ?? false;
+ }
+
+ async deliver(request: DeliveryRequest): Promise {
+ const payload = this.#payload(request.outgoing, request.userId);
+ if (
+ request.sourceMessageId === undefined ||
+ request.generation === undefined
+ ) {
+ const reply = await this.#transport.send(request.target, payload);
+ await this.#afterDelivery(request.outgoing, reply);
+ return "sent";
+ }
+
+ const sourceMessageId = request.sourceMessageId;
+ const generation = request.generation;
+ const tracker = this.#tracker(request.featureId);
+ const state = tracker.get(sourceMessageId);
+ // A newer invocation finished or remains in flight, so discard this stale result.
+ if (state?.generation !== generation) {
+ return "stale";
+ }
+
+ if (state.reply !== undefined) {
+ if (state.kind !== request.outgoing.kind) {
+ throw new Error(
+ `tracked reply kind cannot change from ${state.kind} to ${request.outgoing.kind}`,
+ );
+ }
+ await this.#transport.edit(state.reply, payload);
+ await this.#afterDelivery(request.outgoing, state.reply);
+ return "edited";
+ }
+
+ const reply = await this.#transport.send(request.target, payload);
+ const latest = tracker.get(sourceMessageId);
+ if (latest?.generation !== generation) {
+ // The generation changed during send; delete the reply that lost the race.
+ await this.#transport.delete(reply);
+ return "stale";
+ }
+
+ tracker.set(sourceMessageId, {
+ generation: latest.generation,
+ reply,
+ kind: request.outgoing.kind,
+ });
+ await this.#afterDelivery(request.outgoing, reply);
+ return "sent";
+ }
+
+ async deleteReplies(sourceMessageId: string): Promise {
+ const deletions: Promise[] = [];
+ for (const tracker of this.#trackers.values()) {
+ const state = tracker.get(sourceMessageId);
+ tracker.delete(sourceMessageId);
+ if (state?.reply !== undefined) {
+ deletions.push(this.#transport.delete(state.reply));
+ }
+ }
+ await Promise.allSettled(deletions);
+ }
+
+ #tracker(featureId: FeatureId): LimitedSizeMap {
+ let tracker = this.#trackers.get(featureId);
+ if (tracker === undefined) {
+ tracker = new LimitedSizeMap(this.#capacity);
+ this.#trackers.set(featureId, tracker);
+ }
+ return tracker;
+ }
+
+ #payload(outgoing: OutgoingReply, userId: string): ReplyPayload {
+ // Ephemeral replies cannot be deleted via message.delete(); Discord
+ // provides its own dismiss affordance, so no delete button there.
+ const deleteRow = outgoing.ephemeral ? [] : [deleteButtonRow(userId)];
+ const components =
+ outgoing.kind === "components-v2"
+ ? [...outgoing.components, ...deleteRow]
+ : deleteRow;
+
+ return {
+ kind: outgoing.kind,
+ content: outgoing.kind === "plain" ? outgoing.content : undefined,
+ components,
+ files: outgoing.files,
+ ephemeral: outgoing.ephemeral ?? false,
+ replaceAttachments: true,
+ };
+ }
+
+ async #afterDelivery(
+ outgoing: OutgoingReply,
+ reply: EditableReply,
+ ): Promise {
+ if (reply instanceof Message) {
+ cancelPaginator(reply);
+ await outgoing.onDelivered?.(reply);
+ }
+ }
+}
diff --git a/discord/src/platform/discord/router.test.ts b/discord/src/platform/discord/router.test.ts
new file mode 100644
index 0000000..b037d00
--- /dev/null
+++ b/discord/src/platform/discord/router.test.ts
@@ -0,0 +1,309 @@
+/**
+ * Tests startup route validation, prefix parsing, and anonymous dispatch instrumentation.
+ */
+import {
+ type Client,
+ Events,
+ type REST,
+ SlashCommandBuilder,
+} from "discord.js";
+import { describe, expect, it, vi } from "vitest";
+import type { BotProfile } from "../../config.js";
+import type { AppLogger } from "../../shared/logger.js";
+import type { UsageStats } from "../../shared/usageStats.js";
+import type { Feature } from "../../types.js";
+import type { ErrorPresenter } from "./errorPresenter.js";
+import type { ReplyCoordinator } from "./replyCoordinator.js";
+import {
+ buildRouteRegistry,
+ DiscordRouter,
+ parsePrefixCommand,
+} from "./router.js";
+
+describe("buildRouteRegistry", () => {
+ it("fails fast when customId namespaces overlap across handler kinds", () => {
+ const modalFeature: Feature = {
+ id: "code",
+ modalHandlers: {
+ shared: async () => undefined,
+ },
+ };
+ const componentFeature: Feature = {
+ id: "tex",
+ componentHandlers: {
+ shared: async () => undefined,
+ },
+ };
+
+ expect(() => buildRouteRegistry([modalFeature, componentFeature])).toThrow(
+ "duplicate customId namespace route: shared",
+ );
+ });
+
+ it("rejects central and collector-owned namespaces", () => {
+ const feature: Feature = {
+ id: "code",
+ componentHandlers: {
+ pg: async () => undefined,
+ },
+ };
+
+ expect(() => buildRouteRegistry([feature])).toThrow(
+ "customId namespace is reserved: pg",
+ );
+ });
+});
+
+describe("parsePrefixCommand", () => {
+ it("preserves the raw rest argument after one separator", () => {
+ expect(parsePrefixCommand("]run python\nprint(1)", "]")).toEqual({
+ name: "run",
+ args: " python\nprint(1)",
+ });
+ });
+});
+
+describe("DiscordRouter usage statistics", () => {
+ it("records successful slash, prefix, modal, and button dispatches", async () => {
+ const slashExecute = vi.fn().mockResolvedValue(undefined);
+ const prefixExecute = vi.fn().mockResolvedValue(undefined);
+ const modalHandler = vi.fn().mockResolvedValue(undefined);
+ const buttonHandler = vi.fn().mockResolvedValue(undefined);
+ const feature: Feature = {
+ id: "code",
+ slashCommands: [
+ {
+ data: new SlashCommandBuilder()
+ .setName("test")
+ .setDescription("Test command"),
+ execute: slashExecute,
+ },
+ ],
+ prefixCommands: [
+ {
+ name: "run",
+ aliases: ["r"],
+ execute: prefixExecute,
+ },
+ ],
+ modalHandlers: { form: modalHandler },
+ componentHandlers: { action: buttonHandler },
+ };
+ const harness = createHarness([feature]);
+
+ harness.dispatch(
+ Events.InteractionCreate,
+ interaction({
+ commandName: "test",
+ isChatInputCommand: () => true,
+ toString: () => "/test",
+ }),
+ );
+ await waitForCalls(harness.recordCommand, 1);
+
+ harness.dispatch(Events.MessageCreate, {
+ id: "message",
+ content: "]r python print(1)",
+ author: { id: "user", bot: false },
+ guild: null,
+ inGuild: () => false,
+ channel: { sendTyping: vi.fn().mockResolvedValue(undefined) },
+ });
+ await waitForCalls(harness.recordCommand, 2);
+
+ harness.dispatch(
+ Events.InteractionCreate,
+ interaction({
+ customId: "form:value",
+ isModalSubmit: () => true,
+ }),
+ );
+ await waitForCalls(harness.recordCommand, 3);
+
+ harness.dispatch(
+ Events.InteractionCreate,
+ interaction({
+ customId: "action:value",
+ isButton: () => true,
+ }),
+ );
+ await waitForCalls(harness.recordCommand, 4);
+
+ expect(harness.recordCommand.mock.calls).toEqual([
+ ["slash", "test", "ok"],
+ ["prefix", "run", "ok"],
+ ["modal", "form", "ok"],
+ ["button", "action", "ok"],
+ ]);
+ });
+
+ it("records a slash handler failure even when the error is presented", async () => {
+ const error = new Error("handler failed");
+ const feature: Feature = {
+ id: "code",
+ slashCommands: [
+ {
+ data: new SlashCommandBuilder()
+ .setName("test")
+ .setDescription("Test command"),
+ execute: vi.fn().mockRejectedValue(error),
+ },
+ ],
+ };
+ const harness = createHarness([feature]);
+
+ harness.dispatch(
+ Events.InteractionCreate,
+ interaction({
+ commandName: "test",
+ isChatInputCommand: () => true,
+ toString: () => "/test",
+ }),
+ );
+
+ await vi.waitFor(() => {
+ expect(harness.presentError).toHaveBeenCalledOnce();
+ expect(harness.deliver).toHaveBeenCalledOnce();
+ });
+ expect(harness.recordCommand).toHaveBeenCalledOnce();
+ expect(harness.recordCommand).toHaveBeenCalledWith(
+ "slash",
+ "test",
+ "error",
+ );
+ });
+
+ it("does not record reserved delete or pagination buttons", async () => {
+ const harness = createHarness([]);
+ const deleteMessage = vi.fn().mockResolvedValue(undefined);
+ const deferUpdate = vi.fn().mockResolvedValue(undefined);
+
+ harness.dispatch(
+ Events.InteractionCreate,
+ interaction({
+ customId: "pg:next",
+ isButton: () => true,
+ }),
+ );
+ harness.dispatch(
+ Events.InteractionCreate,
+ interaction({
+ customId: "delete:user",
+ isButton: () => true,
+ deferUpdate,
+ message: { delete: deleteMessage },
+ }),
+ );
+
+ await vi.waitFor(() => expect(deleteMessage).toHaveBeenCalledOnce());
+ expect(deferUpdate).toHaveBeenCalledOnce();
+ expect(harness.recordCommand).not.toHaveBeenCalled();
+ });
+
+ it("records guild counts on ready, join, and leave events", async () => {
+ const harness = createHarness([]);
+ harness.guildCache.set("one", {});
+ harness.guildCache.set("two", {});
+
+ harness.dispatch(Events.ClientReady, harness.client);
+ await vi.waitFor(() => expect(harness.put).toHaveBeenCalledOnce());
+ harness.guildCache.set("three", {});
+ harness.dispatch(Events.GuildCreate, {});
+ harness.guildCache.delete("three");
+ harness.dispatch(Events.GuildDelete, {});
+
+ expect(harness.recordGuildCount.mock.calls).toEqual([[2], [3], [2]]);
+ });
+});
+
+type Listener = (...args: never[]) => void;
+
+function createHarness(features: readonly Feature[]) {
+ const listeners = new Map();
+ const guildCache = new Map();
+ const client = {
+ application: { id: "application" },
+ guilds: { cache: guildCache },
+ once: vi.fn((event: string, listener: Listener) => {
+ listeners.set(event, listener);
+ }),
+ on: vi.fn((event: string, listener: Listener) => {
+ listeners.set(event, listener);
+ }),
+ };
+ const put = vi.fn().mockResolvedValue(undefined);
+ const deliver = vi.fn().mockResolvedValue("sent");
+ const presentError = vi.fn().mockResolvedValue({
+ kind: "plain",
+ content: "error",
+ });
+ const recordCommand = vi.fn();
+ const recordGuildCount = vi.fn();
+ const profile: BotProfile = {
+ name: "coderunbot",
+ prefix: "]",
+ defaultLocale: "en",
+ features: [],
+ };
+ const router = new DiscordRouter({
+ client: client as unknown as Client,
+ rest: { put } as unknown as REST,
+ profile,
+ features,
+ replyCoordinator: {
+ begin: vi.fn().mockReturnValue(1),
+ deliver,
+ deleteReplies: vi.fn().mockResolvedValue(undefined),
+ } as unknown as ReplyCoordinator,
+ errorPresenter: { present: presentError } as unknown as ErrorPresenter,
+ logger: {
+ info: vi.fn(),
+ error: vi.fn(),
+ debug: vi.fn(),
+ } as unknown as AppLogger,
+ usageStats: {
+ recordCommand,
+ recordGuildCount,
+ } as unknown as UsageStats,
+ });
+ router.bind();
+
+ return {
+ client,
+ deliver,
+ dispatch(event: string, ...args: unknown[]) {
+ const listener = listeners.get(event);
+ if (listener === undefined) {
+ throw new Error(`event is not bound: ${event}`);
+ }
+ listener(...(args as never[]));
+ },
+ guildCache,
+ presentError,
+ put,
+ recordCommand,
+ recordGuildCount,
+ };
+}
+
+function interaction(overrides: Record): unknown {
+ return {
+ user: { id: "user" },
+ locale: "en-US",
+ guild: null,
+ deferred: false,
+ replied: false,
+ inGuild: () => false,
+ isAutocomplete: () => false,
+ isChatInputCommand: () => false,
+ isContextMenuCommand: () => false,
+ isButton: () => false,
+ isModalSubmit: () => false,
+ toString: () => "interaction",
+ ...overrides,
+ };
+}
+
+async function waitForCalls(mock: ReturnType, count: number) {
+ await vi.waitFor(() => expect(mock).toHaveBeenCalledTimes(count));
+}
diff --git a/discord/src/platform/discord/router.ts b/discord/src/platform/discord/router.ts
new file mode 100644
index 0000000..eccd5a1
--- /dev/null
+++ b/discord/src/platform/discord/router.ts
@@ -0,0 +1,513 @@
+/**
+ * Registers feature routes and dispatches Discord events in the platform layer.
+ */
+import {
+ ApplicationIntegrationType,
+ type AutocompleteInteraction,
+ type ButtonInteraction,
+ type ChatInputCommandInteraction,
+ type Client,
+ type ContextMenuCommandInteraction,
+ Events,
+ type Interaction,
+ InteractionContextType,
+ type Message,
+ type ModalSubmitInteraction,
+ type PartialMessage,
+ type REST,
+ Routes,
+} from "discord.js";
+import type { BotProfile } from "../../config.js";
+import type { AppLogger } from "../../shared/logger.js";
+import type { UsageStats } from "../../shared/usageStats.js";
+import type {
+ ComponentHandler,
+ Feature,
+ FeatureId,
+ ModalHandler,
+ PrefixCommand,
+ SlashCommand,
+} from "../../types.js";
+import {
+ contextForInteraction,
+ contextForMessage,
+ type RequestContext,
+} from "./context.js";
+import type { ErrorPresenter } from "./errorPresenter.js";
+import type { ReplyCoordinator } from "./replyCoordinator.js";
+
+interface RegisteredSlash {
+ readonly featureId: FeatureId;
+ readonly command: SlashCommand;
+}
+
+interface RegisteredPrefix {
+ readonly featureId: FeatureId;
+ readonly command: PrefixCommand;
+}
+
+type CustomRoute =
+ | {
+ readonly kind: "modal";
+ readonly featureId: FeatureId;
+ readonly handler: ModalHandler;
+ }
+ | {
+ readonly kind: "component";
+ readonly featureId: FeatureId;
+ readonly handler: ComponentHandler;
+ };
+
+export interface RouteRegistry {
+ readonly slash: ReadonlyMap;
+ readonly prefix: ReadonlyMap;
+ readonly custom: ReadonlyMap;
+}
+
+function addUnique(
+ map: Map,
+ route: string,
+ value: Value,
+ routeType: string,
+): void {
+ // Registry construction happens in the router constructor, so duplicates fail at startup.
+ if (map.has(route)) {
+ throw new Error(`duplicate ${routeType} route: ${route}`);
+ }
+ map.set(route, value);
+}
+
+export function buildRouteRegistry(
+ features: readonly Feature[],
+): RouteRegistry {
+ const slash = new Map();
+ const prefix = new Map();
+ // customIds use namespace:...; the first colon-delimited segment selects the route.
+ const custom = new Map();
+ const featureIds = new Set();
+
+ for (const feature of features) {
+ if (featureIds.has(feature.id)) {
+ throw new Error(`duplicate feature id: ${feature.id}`);
+ }
+ featureIds.add(feature.id);
+
+ for (const command of feature.slashCommands ?? []) {
+ addUnique(
+ slash,
+ command.data.name,
+ { featureId: feature.id, command },
+ "application command",
+ );
+ }
+
+ for (const command of feature.prefixCommands ?? []) {
+ for (const name of [command.name, ...(command.aliases ?? [])]) {
+ addUnique(
+ prefix,
+ name.toLowerCase(),
+ { featureId: feature.id, command },
+ "prefix command",
+ );
+ }
+ }
+
+ for (const [namespace, handler] of Object.entries(
+ feature.modalHandlers ?? {},
+ )) {
+ addUnique(
+ custom,
+ namespace,
+ { kind: "modal", featureId: feature.id, handler },
+ "customId namespace",
+ );
+ }
+
+ for (const [namespace, handler] of Object.entries(
+ feature.componentHandlers ?? {},
+ )) {
+ addUnique(
+ custom,
+ namespace,
+ { kind: "component", featureId: feature.id, handler },
+ "customId namespace",
+ );
+ }
+ }
+
+ // Delete handling and collector-based pagination own these platform namespaces.
+ for (const reserved of ["delete", "pg"]) {
+ if (custom.has(reserved)) {
+ throw new Error(`customId namespace is reserved: ${reserved}`);
+ }
+ }
+
+ return { slash, prefix, custom };
+}
+
+export interface ParsedPrefixCommand {
+ readonly name: string;
+ readonly args: string;
+}
+
+export function parsePrefixCommand(
+ content: string,
+ prefix: string,
+): ParsedPrefixCommand | undefined {
+ if (!content.startsWith(prefix)) {
+ return undefined;
+ }
+
+ const input = content.slice(prefix.length);
+ const separator = input.search(/\s/u);
+ const name = (
+ separator === -1 ? input : input.slice(0, separator)
+ ).toLowerCase();
+ if (name.length === 0) {
+ return undefined;
+ }
+
+ return {
+ name,
+ // Remove only the command separator. The rest remains byte-for-byte intact.
+ args: separator === -1 ? "" : input.slice(separator + 1),
+ };
+}
+
+export type MessageGate = (message: Message) => boolean | Promise;
+
+export interface DiscordRouterOptions {
+ readonly client: Client;
+ readonly rest: REST;
+ readonly profile: BotProfile;
+ readonly features: readonly Feature[];
+ readonly replyCoordinator: ReplyCoordinator;
+ readonly errorPresenter: ErrorPresenter;
+ readonly logger: AppLogger;
+ readonly usageStats?: UsageStats;
+ readonly messageGate?: MessageGate;
+}
+
+export class DiscordRouter {
+ readonly #client: Client;
+ readonly #rest: REST;
+ readonly #profile: BotProfile;
+ readonly #features: readonly Feature[];
+ readonly #routes: RouteRegistry;
+ readonly #replyCoordinator: ReplyCoordinator;
+ readonly #errorPresenter: ErrorPresenter;
+ readonly #logger: AppLogger;
+ readonly #usageStats?: UsageStats;
+ readonly #messageGate: MessageGate;
+
+ constructor(options: DiscordRouterOptions) {
+ this.#client = options.client;
+ this.#rest = options.rest;
+ this.#profile = options.profile;
+ this.#features = options.features;
+ this.#routes = buildRouteRegistry(options.features);
+ this.#replyCoordinator = options.replyCoordinator;
+ this.#errorPresenter = options.errorPresenter;
+ this.#logger = options.logger;
+ this.#usageStats = options.usageStats;
+ this.#messageGate = options.messageGate ?? (() => true);
+ }
+
+ bind(): void {
+ this.#client.once(Events.ClientReady, (client) => {
+ void this.#onReady(client);
+ });
+ this.#client.on(Events.InteractionCreate, (interaction) => {
+ void this.#onInteraction(interaction);
+ });
+ this.#client.on(Events.MessageCreate, (message) => {
+ void this.#onMessage(message);
+ });
+ this.#client.on(Events.MessageUpdate, (before, after) => {
+ void this.#onMessageUpdate(before, after);
+ });
+ this.#client.on(Events.MessageDelete, (message) => {
+ void this.#replyCoordinator.deleteReplies(message.id);
+ });
+ this.#client.on(Events.GuildCreate, () => {
+ this.#usageStats?.recordGuildCount(this.#client.guilds.cache.size);
+ });
+ this.#client.on(Events.GuildDelete, () => {
+ this.#usageStats?.recordGuildCount(this.#client.guilds.cache.size);
+ });
+ }
+
+ async #onReady(client: Client): Promise {
+ this.#usageStats?.recordGuildCount(client.guilds.cache.size);
+ const body = [...this.#routes.slash.values()].map(({ command }) => ({
+ ...command.data.toJSON(),
+ // User-installable apps require both install types and all supported invocation contexts.
+ integration_types: [
+ ApplicationIntegrationType.GuildInstall,
+ ApplicationIntegrationType.UserInstall,
+ ],
+ contexts: [
+ InteractionContextType.Guild,
+ InteractionContextType.BotDM,
+ InteractionContextType.PrivateChannel,
+ ],
+ }));
+
+ try {
+ // ClientReady provides the application ID; REST.put bulk-overwrites commands idempotently.
+ await this.#rest.put(Routes.applicationCommands(client.application.id), {
+ body,
+ });
+ this.#logger.info(
+ { commandCount: body.length, bot: this.#profile.name },
+ "Registered Discord application commands",
+ );
+ } catch (error) {
+ this.#logger.error({ error }, "Failed to register application commands");
+ }
+ }
+
+ async #onInteraction(interaction: Interaction): Promise {
+ if (interaction.isAutocomplete()) {
+ await this.#onAutocomplete(interaction);
+ return;
+ }
+
+ if (
+ interaction.isChatInputCommand() ||
+ interaction.isContextMenuCommand()
+ ) {
+ await this.#onApplicationCommand(interaction);
+ return;
+ }
+
+ if (interaction.isButton()) {
+ await this.#onButton(interaction);
+ return;
+ }
+
+ if (interaction.isModalSubmit()) {
+ await this.#onModal(interaction);
+ }
+ }
+
+ async #onApplicationCommand(
+ interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction,
+ ): Promise {
+ const route = this.#routes.slash.get(interaction.commandName);
+ if (route === undefined) {
+ return;
+ }
+ const context = contextForInteraction(
+ interaction,
+ this.#profile.defaultLocale,
+ );
+ await this.#invoke(
+ route.featureId,
+ context,
+ interaction.toString(),
+ () => route.command.execute(interaction, context),
+ {
+ kind: interaction.isChatInputCommand() ? "slash" : "context-menu",
+ name: interaction.commandName,
+ },
+ );
+ }
+
+ async #onAutocomplete(interaction: AutocompleteInteraction): Promise {
+ const route = this.#routes.slash.get(interaction.commandName);
+ if (route?.command.autocomplete === undefined) {
+ return;
+ }
+
+ try {
+ await route.command.autocomplete(interaction);
+ } catch (error) {
+ const context = contextForInteraction(
+ interaction as unknown as ChatInputCommandInteraction,
+ this.#profile.defaultLocale,
+ );
+ await this.#errorPresenter.present(
+ error,
+ context,
+ interaction.toString(),
+ );
+ if (!interaction.responded) {
+ await interaction.respond([]);
+ }
+ }
+ }
+
+ async #onButton(interaction: ButtonInteraction): Promise {
+ const namespace = interaction.customId.split(":", 1)[0] ?? "";
+ if (namespace === "pg") {
+ return;
+ }
+ if (namespace === "delete") {
+ const ownerId = interaction.customId.slice("delete:".length);
+ if (ownerId === interaction.user.id) {
+ await interaction.deferUpdate();
+ await interaction.message.delete();
+ } else if (!interaction.deferred && !interaction.replied) {
+ await interaction.deferUpdate();
+ }
+ return;
+ }
+
+ const route = this.#routes.custom.get(namespace);
+ if (route?.kind !== "component") {
+ return;
+ }
+ const context = contextForInteraction(
+ interaction,
+ this.#profile.defaultLocale,
+ );
+ await this.#invoke(
+ route.featureId,
+ context,
+ interaction.customId,
+ () => route.handler(interaction, context),
+ { kind: "button", name: namespace },
+ );
+ }
+
+ async #onModal(interaction: ModalSubmitInteraction): Promise {
+ const namespace = interaction.customId.split(":", 1)[0] ?? "";
+ const route = this.#routes.custom.get(namespace);
+ if (route?.kind !== "modal") {
+ return;
+ }
+ const context = contextForInteraction(
+ interaction,
+ this.#profile.defaultLocale,
+ );
+ await this.#invoke(
+ route.featureId,
+ context,
+ interaction.customId,
+ () => route.handler(interaction, context),
+ { kind: "modal", name: namespace },
+ );
+ }
+
+ async #onMessage(message: Message): Promise {
+ if (message.author.bot || !(await this.#messageGate(message))) {
+ return;
+ }
+
+ const context = contextForMessage(message, this.#profile.defaultLocale);
+ const handledFeatures = new Set();
+ const parsed = parsePrefixCommand(message.content, this.#profile.prefix);
+ const prefixRoute = parsed && this.#routes.prefix.get(parsed.name);
+
+ if (prefixRoute !== undefined && parsed !== undefined) {
+ handledFeatures.add(prefixRoute.featureId);
+ try {
+ // Messages have no defer API; typing gives feedback while interactions must
+ // acknowledge within three seconds.
+ if ("sendTyping" in message.channel) {
+ await message.channel.sendTyping();
+ }
+ } catch (error) {
+ this.#logger.debug({ error }, "Unable to send typing indicator");
+ }
+ await this.#invokeMessage(
+ prefixRoute.featureId,
+ context,
+ message.content,
+ () => prefixRoute.command.execute(message, parsed.args, context),
+ { kind: "prefix", name: prefixRoute.command.name },
+ );
+ }
+
+ for (const feature of this.#features) {
+ if (feature.onMessage === undefined || handledFeatures.has(feature.id)) {
+ continue;
+ }
+ const onMessage = feature.onMessage;
+ await this.#invokeMessage(feature.id, context, message.content, () =>
+ onMessage(message, context),
+ );
+ }
+ }
+
+ async #onMessageUpdate(
+ before: Message | PartialMessage,
+ after: Message | PartialMessage,
+ ): Promise {
+ if (before.partial || after.partial || before.content === after.content) {
+ return;
+ }
+ await this.#onMessage(after);
+ }
+
+ async #invokeMessage(
+ featureId: FeatureId,
+ context: RequestContext,
+ invocation: string,
+ handler: () => ReturnType,
+ stat?: { readonly kind: string; readonly name: string },
+ ): Promise {
+ const sourceMessageId = (context.replyTarget as Message).id;
+ const generation = this.#replyCoordinator.begin(featureId, sourceMessageId);
+ await this.#invoke(featureId, context, invocation, handler, stat, {
+ sourceMessageId,
+ generation,
+ });
+ }
+
+ async #invoke(
+ featureId: FeatureId,
+ context: RequestContext,
+ invocation: string,
+ handler: () => ReturnType,
+ stat?: { readonly kind: string; readonly name: string },
+ tracking?: {
+ readonly sourceMessageId: string;
+ readonly generation: number;
+ },
+ ): Promise {
+ try {
+ const outgoing = await handler();
+ if (outgoing === undefined) {
+ if (stat !== undefined) {
+ this.#usageStats?.recordCommand(stat.kind, stat.name, "ok");
+ }
+ return;
+ }
+ await this.#replyCoordinator.deliver({
+ featureId,
+ target: context.replyTarget,
+ userId: context.userId,
+ outgoing,
+ ...tracking,
+ });
+ if (stat !== undefined) {
+ this.#usageStats?.recordCommand(stat.kind, stat.name, "ok");
+ }
+ } catch (error) {
+ if (stat !== undefined) {
+ this.#usageStats?.recordCommand(stat.kind, stat.name, "error");
+ }
+ try {
+ const outgoing = await this.#errorPresenter.present(
+ error,
+ context,
+ invocation,
+ );
+ await this.#replyCoordinator.deliver({
+ featureId,
+ target: context.replyTarget,
+ userId: context.userId,
+ outgoing,
+ ...tracking,
+ });
+ } catch (presentationError) {
+ this.#logger.error(
+ { error: presentationError, originalError: error },
+ "Failed to present handler error",
+ );
+ }
+ }
+ }
+}
diff --git a/discord/src/shared/i18n.ts b/discord/src/shared/i18n.ts
new file mode 100644
index 0000000..0994e4f
--- /dev/null
+++ b/discord/src/shared/i18n.ts
@@ -0,0 +1,160 @@
+/**
+ * Owns shared translation resources and request-scoped translator creation.
+ */
+import i18next from "i18next";
+
+const resources = {
+ en: {
+ translation: {
+ errors: {
+ unhandled: "An unexpected error occurred.",
+ invalidInput: "Invalid input",
+ report: "Please report this problem to us: {{supportLink}}",
+ },
+ privacy: {
+ source:
+ "This bot is free software licensed under the GNU AGPL v3 or later. The full source code is available at {{url}}",
+ optOut: {
+ already:
+ "Your message content is already off-track. To use other commands, please use the /opt-in command.",
+ success:
+ "This bot will not track your message content from now on. Most commands will no longer respond.",
+ },
+ optIn: {
+ already: "This bot is already tracking your message content.",
+ success:
+ "This bot will now track the content of your messages. It will only be used to provide commands. Use the /privacy-policy command to view the privacy policy.",
+ },
+ },
+ tex: {
+ hintHeading: "Hint",
+ multilineHint: "You can use the gather or align environment.",
+ codeHeading: "Code",
+ errorHeading: "Rendering Error",
+ modal: {
+ title: "LaTeX to Image",
+ codeLabel: "Code",
+ codePlaceholder: "Enter TeX code here",
+ },
+ },
+ code: {
+ escape: {
+ noText: "That message has no text to escape.",
+ },
+ supportedLanguagesHeading: "The following languages are supported",
+ noLanguagesAvailable:
+ "The language list is temporarily unavailable. Please try again later.",
+ resultHeading: "Result ({{compiler}})",
+ codeHeading: "Code",
+ modal: {
+ title: "Run code",
+ codeLabel: "Code",
+ codePlaceholder: "Write code here",
+ stdinLabel: "Standard Input",
+ },
+ errors: {
+ heading: "Wandbox Error",
+ connection: "Could not connect to Wandbox. Please try again later.",
+ http: "Wandbox returned HTTP status {{status}}.",
+ nonJson:
+ "Wandbox returned a non-JSON response. Please check the service status and try again later.",
+ responsePreview: "Response Preview",
+ },
+ },
+ wolfram: {
+ errorHeading: "Wolfram|Alpha Error",
+ notUnderstood: "Wolfram|Alpha could not understand that input.",
+ noResults: "Wolfram|Alpha returned no result pages.",
+ },
+ translate: {
+ invalidLanguage:
+ "Invalid language. Enter an ISO 639-1 code or language name.",
+ originalHeading: "Original",
+ translatedHeading: "Translated to {{language}}",
+ },
+ },
+ },
+ ja: {
+ translation: {
+ errors: {
+ unhandled: "予期しないエラーが発生しました。",
+ invalidInput: "入力が正しくありません",
+ report: "この問題を報告してください: {{supportLink}}",
+ },
+ privacy: {
+ source:
+ "このボットは GNU AGPL v3 以降のライセンスで公開されている自由ソフトウェアです。ソースコードはこちら: {{url}}",
+ optOut: {
+ already:
+ "メッセージ内容はすでに処理対象外です。他のコマンドを使用するには /opt-in コマンドを使用してください。",
+ success:
+ "今後、このボットはあなたのメッセージ内容を処理しません。ほとんどのコマンドは応答しなくなります。",
+ },
+ optIn: {
+ already: "このボットはすでにあなたのメッセージ内容を処理しています。",
+ success:
+ "このボットは今後、あなたのメッセージ内容をコマンド提供のためだけに処理します。プライバシーポリシーは /privacy-policy コマンドで確認できます。",
+ },
+ },
+ tex: {
+ hintHeading: "ヒント",
+ multilineHint: "gather または align 環境を利用できます。",
+ codeHeading: "コード",
+ errorHeading: "レンダリングエラー",
+ modal: {
+ title: "LaTeX を画像に変換",
+ codeLabel: "コード",
+ codePlaceholder: "TeX コードを入力してください",
+ },
+ },
+ code: {
+ escape: {
+ noText: "そのメッセージにはエスケープできるテキストがありません。",
+ },
+ supportedLanguagesHeading: "対応している言語",
+ noLanguagesAvailable:
+ "言語一覧を一時的に取得できません。しばらくしてからもう一度お試しください。",
+ resultHeading: "実行結果 ({{compiler}})",
+ codeHeading: "コード",
+ modal: {
+ title: "コードを実行",
+ codeLabel: "コード",
+ codePlaceholder: "コードを入力してください",
+ stdinLabel: "標準入力",
+ },
+ errors: {
+ heading: "Wandbox エラー",
+ connection:
+ "Wandbox に接続できませんでした。しばらくしてからもう一度お試しください。",
+ http: "Wandbox が HTTP ステータス {{status}} を返しました。",
+ nonJson:
+ "Wandbox から JSON ではない応答が返されました。サービスの状態を確認して、しばらくしてからもう一度お試しください。",
+ responsePreview: "応答のプレビュー",
+ },
+ },
+ wolfram: {
+ errorHeading: "Wolfram|Alpha エラー",
+ notUnderstood: "Wolfram|Alpha は、その入力を理解できませんでした。",
+ noResults: "Wolfram|Alpha から結果ページが返されませんでした。",
+ },
+ translate: {
+ invalidLanguage:
+ "無効な言語です。ISO 639-1 の2文字コードまたは言語名を入力してください。",
+ originalHeading: "原文",
+ translatedHeading: "翻訳結果({{language}})",
+ },
+ },
+ },
+} as const;
+
+const instance = i18next.createInstance();
+await instance.init({
+ fallbackLng: "en",
+ interpolation: { escapeValue: false },
+ resources,
+});
+
+export function getFixedT(locale: string) {
+ // A fixed translator avoids global changeLanguage calls bleeding across concurrent requests.
+ return instance.getFixedT(locale);
+}
diff --git a/discord/src/shared/limitedMap.test.ts b/discord/src/shared/limitedMap.test.ts
new file mode 100644
index 0000000..e7fab94
--- /dev/null
+++ b/discord/src/shared/limitedMap.test.ts
@@ -0,0 +1,40 @@
+/**
+ * Verifies the shared bounded map's validation and oldest-entry eviction behavior.
+ */
+import { describe, expect, it } from "vitest";
+import { LimitedSizeMap } from "./limitedMap.js";
+
+describe("LimitedSizeMap", () => {
+ it("evicts the oldest insertion when capacity is exceeded", () => {
+ const map = new LimitedSizeMap(2);
+ map.set("first", 1).set("second", 2).set("third", 3);
+
+ expect([...map]).toEqual([
+ ["second", 2],
+ ["third", 3],
+ ]);
+ });
+
+ it("keeps the original insertion order when an existing key is updated", () => {
+ const map = new LimitedSizeMap(2);
+ map.set("first", 1).set("second", 2).set("first", 10).set("third", 3);
+
+ expect(map.has("first")).toBe(false);
+ expect([...map]).toEqual([
+ ["second", 2],
+ ["third", 3],
+ ]);
+ });
+
+ it("supports undefined as a key", () => {
+ const map = new LimitedSizeMap(1);
+ map.set(undefined, 1).set("next", 2);
+
+ expect([...map]).toEqual([["next", 2]]);
+ });
+
+ it("rejects invalid capacities", () => {
+ expect(() => new LimitedSizeMap(0)).toThrow(RangeError);
+ expect(() => new LimitedSizeMap(1.5)).toThrow(RangeError);
+ });
+});
diff --git a/discord/src/shared/limitedMap.ts b/discord/src/shared/limitedMap.ts
new file mode 100644
index 0000000..adb74d1
--- /dev/null
+++ b/discord/src/shared/limitedMap.ts
@@ -0,0 +1,49 @@
+/**
+ * Provides a shared insertion-ordered map with a fixed upper bound.
+ */
+export class LimitedSizeMap implements Iterable<[Key, Value]> {
+ readonly #entries = new Map();
+ readonly #capacity: number;
+
+ constructor(capacity: number) {
+ if (!Number.isInteger(capacity) || capacity < 1) {
+ throw new RangeError("capacity must be a positive integer");
+ }
+ this.#capacity = capacity;
+ }
+
+ get size(): number {
+ return this.#entries.size;
+ }
+
+ get(key: Key): Value | undefined {
+ return this.#entries.get(key);
+ }
+
+ has(key: Key): boolean {
+ return this.#entries.has(key);
+ }
+
+ set(key: Key, value: Value): this {
+ this.#entries.set(key, value);
+ if (this.#entries.size > this.#capacity) {
+ const oldest = this.#entries.keys().next();
+ if (!oldest.done) {
+ this.#entries.delete(oldest.value);
+ }
+ }
+ return this;
+ }
+
+ delete(key: Key): boolean {
+ return this.#entries.delete(key);
+ }
+
+ clear(): void {
+ this.#entries.clear();
+ }
+
+ [Symbol.iterator](): MapIterator<[Key, Value]> {
+ return this.#entries[Symbol.iterator]();
+ }
+}
diff --git a/discord/src/shared/logger.ts b/discord/src/shared/logger.ts
new file mode 100644
index 0000000..295b3a1
--- /dev/null
+++ b/discord/src/shared/logger.ts
@@ -0,0 +1,10 @@
+/**
+ * Exposes the shared application logger type and its Pino-backed factory.
+ */
+import pino, { type Logger, type LoggerOptions } from "pino";
+
+export type AppLogger = Logger;
+
+export function createLogger(options?: LoggerOptions): AppLogger {
+ return pino(options);
+}
diff --git a/discord/src/shared/state.test.ts b/discord/src/shared/state.test.ts
new file mode 100644
index 0000000..6527800
--- /dev/null
+++ b/discord/src/shared/state.test.ts
@@ -0,0 +1,102 @@
+/**
+ * Tests the discord.js-free state adapters and serialized opt-out persistence directly.
+ */
+import { mkdtemp, readFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import { LocalFileState, OptOutUsers, type StateBackend } from "./state.js";
+
+class MemoryState implements StateBackend {
+ readonly writes: string[] = [];
+
+ constructor(readonly value = "") {}
+
+ async read(): Promise {
+ return this.value;
+ }
+
+ async write(value: string): Promise {
+ this.writes.push(value);
+ }
+}
+
+describe("OptOutUsers", () => {
+ it("parses newline-delimited string snowflakes and ignores blank lines", async () => {
+ const state = new MemoryState(
+ " 123456789012345678\r\n\n987654321098765432\n",
+ );
+ const users = new OptOutUsers(state);
+
+ await users.init();
+
+ expect(users.has("123456789012345678")).toBe(true);
+ expect(users.has("987654321098765432")).toBe(true);
+ expect(users.has("123456789012345679")).toBe(false);
+ });
+
+ it("serializes sorted IDs with a trailing newline", async () => {
+ const state = new MemoryState();
+ const users = new OptOutUsers(state);
+ await users.init();
+
+ await users.add("9");
+ await users.add("10");
+ await users.remove("9");
+
+ expect(state.writes).toEqual(["9\n", "10\n9\n", "10\n"]);
+ });
+
+ it("serializes concurrent writes in call order", async () => {
+ const writes: string[] = [];
+ let releaseFirstWrite: (() => void) | undefined;
+ const firstWriteBlocked = new Promise((resolve) => {
+ releaseFirstWrite = resolve;
+ });
+ let writeCount = 0;
+ const state: StateBackend = {
+ async read() {
+ return "";
+ },
+ async write(value) {
+ writeCount += 1;
+ if (writeCount === 1) {
+ await firstWriteBlocked;
+ }
+ writes.push(value);
+ },
+ };
+ const users = new OptOutUsers(state);
+ await users.init();
+
+ const first = users.add("2");
+ const second = users.add("1");
+ await Promise.resolve();
+ expect(writeCount).toBe(1);
+
+ releaseFirstWrite?.();
+ await Promise.all([first, second]);
+
+ expect(writes).toEqual(["2\n", "1\n2\n"]);
+ });
+});
+
+describe("LocalFileState", () => {
+ it("returns an empty string for a missing file", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "coderunbot-state-"));
+ const state = new LocalFileState(join(directory, "missing", "state.txt"));
+
+ await expect(state.read()).resolves.toBe("");
+ });
+
+ it("creates parent directories and writes UTF-8 text", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "coderunbot-state-"));
+ const filePath = join(directory, "nested", "state.txt");
+ const state = new LocalFileState(filePath);
+
+ await state.write("123\n");
+
+ await expect(readFile(filePath, "utf8")).resolves.toBe("123\n");
+ await expect(state.read()).resolves.toBe("123\n");
+ });
+});
diff --git a/discord/src/shared/state.ts b/discord/src/shared/state.ts
new file mode 100644
index 0000000..2f17318
--- /dev/null
+++ b/discord/src/shared/state.ts
@@ -0,0 +1,160 @@
+/**
+ * Adapts local files or S3-compatible storage to the shared persistent state contract.
+ * The adapter is discord.js-free and unit-tested directly.
+ */
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { dirname } from "node:path";
+import {
+ GetObjectCommand,
+ NoSuchKey,
+ PutObjectCommand,
+ S3Client,
+ type S3ClientConfig,
+} from "@aws-sdk/client-s3";
+
+export interface StateBackend {
+ read(): Promise;
+ write(value: string): Promise;
+}
+
+export class LocalFileState implements StateBackend {
+ readonly #filePath: string;
+
+ constructor(filePath: string) {
+ this.#filePath = filePath;
+ }
+
+ async read(): Promise {
+ try {
+ return await readFile(this.#filePath, "utf8");
+ } catch (error) {
+ if (isNodeError(error) && error.code === "ENOENT") {
+ return "";
+ }
+ throw error;
+ }
+ }
+
+ async write(value: string): Promise {
+ await mkdir(dirname(this.#filePath), { recursive: true });
+ await writeFile(this.#filePath, value, "utf8");
+ }
+}
+
+export interface S3ObjectStateOptions {
+ readonly endpoint: string;
+ readonly region: string;
+ readonly bucket: string;
+ readonly key: string;
+ readonly credentials: S3ClientConfig["credentials"];
+}
+
+export class S3ObjectState implements StateBackend {
+ readonly #client: S3Client;
+ readonly #bucket: string;
+ readonly #key: string;
+
+ constructor(options: S3ObjectStateOptions) {
+ this.#client = new S3Client({
+ endpoint: options.endpoint,
+ region: options.region,
+ credentials: options.credentials,
+ // OCI Object Storage exposes an S3 endpoint that requires path-style bucket addressing.
+ forcePathStyle: true,
+ });
+ this.#bucket = options.bucket;
+ this.#key = options.key;
+ }
+
+ async read(): Promise {
+ try {
+ const response = await this.#client.send(
+ new GetObjectCommand({ Bucket: this.#bucket, Key: this.#key }),
+ );
+ return (await response.Body?.transformToString("utf-8")) ?? "";
+ } catch (error) {
+ if (!isMissingS3Object(error)) {
+ throw error;
+ }
+ // NoSuchKey represents an uninitialized store, so seed the empty state once.
+ await this.write("");
+ return "";
+ }
+ }
+
+ async write(value: string): Promise {
+ await this.#client.send(
+ new PutObjectCommand({
+ Bucket: this.#bucket,
+ Key: this.#key,
+ Body: value,
+ ContentType: "text/plain; charset=utf-8",
+ }),
+ );
+ }
+}
+
+export class OptOutUsers {
+ readonly #backend: StateBackend;
+ // Discord snowflakes exceed Number.MAX_SAFE_INTEGER, so user IDs remain strings end to end.
+ #users = new Set();
+ // This promise-chain mutex serializes writes within the bot's single writer process.
+ #writeChain: Promise = Promise.resolve();
+
+ constructor(backend: StateBackend) {
+ this.#backend = backend;
+ }
+
+ async init(): Promise {
+ this.#users = parseUsers(await this.#backend.read());
+ }
+
+ has(userId: string): boolean {
+ return this.#users.has(userId);
+ }
+
+ add(userId: string): Promise {
+ this.#users.add(userId);
+ return this.#enqueueWrite(serializeUsers(this.#users));
+ }
+
+ remove(userId: string): Promise {
+ this.#users.delete(userId);
+ return this.#enqueueWrite(serializeUsers(this.#users));
+ }
+
+ #enqueueWrite(value: string): Promise {
+ const write = this.#writeChain.then(() => this.#backend.write(value));
+ this.#writeChain = write.catch(() => undefined);
+ return write;
+ }
+}
+
+function parseUsers(value: string): Set {
+ return new Set(
+ value
+ .split(/\r?\n/u)
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0),
+ );
+}
+
+function serializeUsers(users: ReadonlySet): string {
+ const sorted = [...users].sort();
+ return sorted.length === 0 ? "" : `${sorted.join("\n")}\n`;
+}
+
+function isNodeError(error: unknown): error is NodeJS.ErrnoException {
+ return error instanceof Error && "code" in error;
+}
+
+function isMissingS3Object(error: unknown): boolean {
+ if (error instanceof NoSuchKey) {
+ return true;
+ }
+ if (!(error instanceof Error) || !("$metadata" in error)) {
+ return false;
+ }
+ const metadata = error.$metadata as { readonly httpStatusCode?: number };
+ return error.name === "NoSuchKey" && metadata.httpStatusCode === 404;
+}
diff --git a/discord/src/shared/usageStats.test.ts b/discord/src/shared/usageStats.test.ts
new file mode 100644
index 0000000..c6d94d6
--- /dev/null
+++ b/discord/src/shared/usageStats.test.ts
@@ -0,0 +1,162 @@
+/**
+ * Tests anonymous usage aggregation, recovery, serialization, and restart persistence.
+ */
+import { describe, expect, it, vi } from "vitest";
+import type { AppLogger } from "./logger.js";
+import type { StateBackend } from "./state.js";
+import { UsageStats } from "./usageStats.js";
+
+class MemoryState implements StateBackend {
+ readonly writes: string[] = [];
+
+ constructor(public value = "") {}
+
+ async read(): Promise {
+ return this.value;
+ }
+
+ async write(value: string): Promise {
+ this.value = value;
+ this.writes.push(value);
+ }
+}
+
+describe("UsageStats", () => {
+ it("buckets command outcomes and languages by UTC month", async () => {
+ const state = new MemoryState();
+ let now = new Date("2026-07-31T23:59:59.000Z");
+ const stats = new UsageStats(state, { now: () => now });
+ await stats.init();
+
+ stats.recordCommand("slash", "tex", "ok");
+ stats.recordCommand("slash", "tex", "error");
+ stats.recordCommand("slash", "tex", "ok");
+ stats.recordRunLanguage("python");
+ now = new Date("2026-08-01T00:00:00.000Z");
+ stats.recordCommand("prefix", "run", "ok");
+ stats.recordRunLanguage("cpp");
+ await stats.flush();
+
+ expect(JSON.parse(state.value)).toEqual({
+ version: 1,
+ months: {
+ "2026-07": {
+ commands: { "slash/tex": { ok: 2, error: 1 } },
+ runLanguages: { python: 1 },
+ guilds: {},
+ },
+ "2026-08": {
+ commands: { "prefix/run": { ok: 1, error: 0 } },
+ runLanguages: { cpp: 1 },
+ guilds: {},
+ },
+ },
+ });
+ await stats.dispose();
+ });
+
+ it("overwrites the guild snapshot within a UTC day", async () => {
+ const state = new MemoryState();
+ let now = new Date("2026-07-18T00:00:01.000Z");
+ const stats = new UsageStats(state, { now: () => now });
+ await stats.init();
+
+ stats.recordGuildCount(40);
+ now = new Date("2026-07-18T23:59:59.000Z");
+ stats.recordGuildCount(42);
+ now = new Date("2026-07-19T00:00:00.000Z");
+ stats.recordGuildCount(41);
+ await stats.flush();
+
+ expect(JSON.parse(state.value).months["2026-07"].guilds).toEqual({
+ "2026-07-18": 42,
+ "2026-07-19": 41,
+ });
+ await stats.dispose();
+ });
+
+ it("recovers from corrupt JSON and warns through the optional logger", async () => {
+ const state = new MemoryState("{not json");
+ const warn = vi.fn();
+ const logger = { warn } as unknown as AppLogger;
+ const stats = new UsageStats(state, {
+ now: () => new Date("2026-07-18T12:00:00.000Z"),
+ logger,
+ });
+
+ await stats.init();
+ stats.recordCommand("slash", "run", "ok");
+ await stats.flush();
+
+ expect(warn).toHaveBeenCalledOnce();
+ expect(JSON.parse(state.value)).toEqual({
+ version: 1,
+ months: {
+ "2026-07": {
+ commands: { "slash/run": { ok: 1, error: 0 } },
+ runLanguages: {},
+ guilds: {},
+ },
+ },
+ });
+ await stats.dispose();
+ });
+
+ it("writes pretty-printed JSON with a trailing newline only when dirty", async () => {
+ const state = new MemoryState();
+ const stats = new UsageStats(state, {
+ now: () => new Date("2026-07-18T12:00:00.000Z"),
+ });
+ await stats.init();
+
+ await stats.flush();
+ expect(state.writes).toEqual([]);
+ stats.recordRunLanguage("python");
+ await stats.flush();
+
+ expect(state.writes).toEqual([
+ `${JSON.stringify(
+ {
+ version: 1,
+ months: {
+ "2026-07": {
+ commands: {},
+ runLanguages: { python: 1 },
+ guilds: {},
+ },
+ },
+ },
+ null,
+ 2,
+ )}\n`,
+ ]);
+ await stats.dispose();
+ });
+
+ it("loads persisted counters and continues them after a restart", async () => {
+ const state = new MemoryState();
+ const now = () => new Date("2026-07-18T12:00:00.000Z");
+ const first = new UsageStats(state, { now });
+ await first.init();
+ first.recordCommand("slash", "tex", "ok");
+ first.recordRunLanguage("python");
+ await first.dispose();
+
+ const second = new UsageStats(state, { now });
+ await second.init();
+ second.recordCommand("slash", "tex", "error");
+ second.recordRunLanguage("python");
+ await second.dispose();
+
+ expect(JSON.parse(state.value)).toEqual({
+ version: 1,
+ months: {
+ "2026-07": {
+ commands: { "slash/tex": { ok: 1, error: 1 } },
+ runLanguages: { python: 2 },
+ guilds: {},
+ },
+ },
+ });
+ });
+});
diff --git a/discord/src/shared/usageStats.ts b/discord/src/shared/usageStats.ts
new file mode 100644
index 0000000..3b8c57f
--- /dev/null
+++ b/discord/src/shared/usageStats.ts
@@ -0,0 +1,199 @@
+/**
+ * Stores anonymous monthly usage counters through the shared state backend.
+ * The collector is discord.js-free and keeps no invocation-level identifiers or content.
+ */
+import type { AppLogger } from "./logger.js";
+import type { StateBackend } from "./state.js";
+
+interface CommandCounts {
+ ok: number;
+ error: number;
+}
+
+interface MonthStats {
+ commands: Record;
+ runLanguages: Record;
+ guilds: Record;
+}
+
+interface UsageStatsDocument {
+ version: 1;
+ months: Record;
+}
+
+export interface UsageStatsOptions {
+ readonly flushIntervalMs?: number;
+ readonly now?: () => Date;
+ readonly logger?: AppLogger;
+}
+
+const DEFAULT_FLUSH_INTERVAL_MS = 300_000;
+
+export class UsageStats {
+ readonly #backend: StateBackend;
+ readonly #flushIntervalMs: number;
+ readonly #now: () => Date;
+ readonly #logger?: AppLogger;
+ #state: UsageStatsDocument = emptyDocument();
+ #dirty = false;
+ #flushTimer?: NodeJS.Timeout;
+ // This promise-chain mutex serializes writes within the bot's single writer process.
+ #writeChain: Promise = Promise.resolve();
+
+ constructor(backend: StateBackend, options: UsageStatsOptions = {}) {
+ this.#backend = backend;
+ this.#flushIntervalMs =
+ options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
+ this.#now = options.now ?? (() => new Date());
+ this.#logger = options.logger;
+ }
+
+ async init(): Promise {
+ const value = await this.#backend.read();
+ if (value.trim().length === 0) {
+ this.#state = emptyDocument();
+ } else {
+ try {
+ this.#state = parseDocument(value);
+ } catch (error) {
+ this.#logger?.warn(
+ { error },
+ "Usage statistics state is corrupt; starting with empty counters",
+ );
+ this.#state = emptyDocument();
+ }
+ }
+ this.#dirty = false;
+ this.#flushTimer = setInterval(() => {
+ void this.flush().catch((error) => {
+ this.#logger?.error(
+ { error },
+ "Failed to flush usage statistics state",
+ );
+ });
+ }, this.#flushIntervalMs);
+ this.#flushTimer.unref();
+ }
+
+ recordCommand(kind: string, name: string, outcome: "ok" | "error"): void {
+ const month = this.#currentMonth();
+ const key = `${kind}/${name}`;
+ let counts = month.commands[key];
+ if (counts === undefined) {
+ counts = { ok: 0, error: 0 };
+ month.commands[key] = counts;
+ }
+ counts[outcome] += 1;
+ this.#dirty = true;
+ }
+
+ recordRunLanguage(languageKey: string): void {
+ const month = this.#currentMonth();
+ month.runLanguages[languageKey] =
+ (month.runLanguages[languageKey] ?? 0) + 1;
+ this.#dirty = true;
+ }
+
+ recordGuildCount(count: number): void {
+ const now = this.#now();
+ const month = this.#month(now);
+ month.guilds[now.toISOString().slice(0, 10)] = count;
+ this.#dirty = true;
+ }
+
+ async flush(): Promise {
+ if (!this.#dirty) {
+ await this.#writeChain;
+ return;
+ }
+
+ this.#dirty = false;
+ const value = `${JSON.stringify(this.#state, null, 2)}\n`;
+ const write = this.#enqueueWrite(value);
+ try {
+ await write;
+ } catch (error) {
+ this.#dirty = true;
+ throw error;
+ }
+ }
+
+ async dispose(): Promise {
+ if (this.#flushTimer !== undefined) {
+ clearInterval(this.#flushTimer);
+ this.#flushTimer = undefined;
+ }
+ await this.flush();
+ }
+
+ #currentMonth(): MonthStats {
+ return this.#month(this.#now());
+ }
+
+ #month(now: Date): MonthStats {
+ const key = now.toISOString().slice(0, 7);
+ let month = this.#state.months[key];
+ if (month === undefined) {
+ month = emptyMonth();
+ this.#state.months[key] = month;
+ }
+ return month;
+ }
+
+ #enqueueWrite(value: string): Promise {
+ const write = this.#writeChain.then(() => this.#backend.write(value));
+ this.#writeChain = write.catch(() => undefined);
+ return write;
+ }
+}
+
+function emptyDocument(): UsageStatsDocument {
+ return { version: 1, months: {} };
+}
+
+function emptyMonth(): MonthStats {
+ return { commands: {}, runLanguages: {}, guilds: {} };
+}
+
+function parseDocument(value: string): UsageStatsDocument {
+ const parsed: unknown = JSON.parse(value);
+ if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.months)) {
+ throw new Error("usage statistics state has an unsupported shape");
+ }
+
+ for (const month of Object.values(parsed.months)) {
+ if (
+ !isRecord(month) ||
+ !isRecord(month.commands) ||
+ !isRecord(month.runLanguages) ||
+ !isRecord(month.guilds)
+ ) {
+ throw new Error("usage statistics month has an unsupported shape");
+ }
+ for (const counts of Object.values(month.commands)) {
+ if (
+ !isRecord(counts) ||
+ !isCounter(counts.ok) ||
+ !isCounter(counts.error)
+ ) {
+ throw new Error("usage statistics command counts are invalid");
+ }
+ }
+ if (
+ !Object.values(month.runLanguages).every(isCounter) ||
+ !Object.values(month.guilds).every(isCounter)
+ ) {
+ throw new Error("usage statistics aggregate counts are invalid");
+ }
+ }
+
+ return parsed as unknown as UsageStatsDocument;
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function isCounter(value: unknown): value is number {
+ return Number.isSafeInteger(value) && (value as number) >= 0;
+}
diff --git a/discord/src/types.ts b/discord/src/types.ts
new file mode 100644
index 0000000..0b8079b
--- /dev/null
+++ b/discord/src/types.ts
@@ -0,0 +1,99 @@
+/**
+ * Defines contracts shared across features, platform code, and the composition root.
+ */
+import type {
+ AutocompleteInteraction,
+ BaseMessageOptions,
+ ButtonInteraction,
+ ChatInputCommandInteraction,
+ ContextMenuCommandBuilder,
+ ContextMenuCommandInteraction,
+ Message,
+ ModalSubmitInteraction,
+ SlashCommandBuilder,
+} from "discord.js";
+import type { Env } from "./env.js";
+import type { RequestContext } from "./platform/discord/context.js";
+import type { AppLogger } from "./shared/logger.js";
+import type { OptOutUsers } from "./shared/state.js";
+import type { UsageStats } from "./shared/usageStats.js";
+
+export type FeatureId =
+ | "tex"
+ | "code"
+ | "privacy"
+ | "wolfram"
+ | "misc"
+ | "translate"
+ | "ping";
+
+type HandlerResult = Promise;
+
+export interface SlashCommand {
+ readonly data: SlashCommandBuilder | ContextMenuCommandBuilder;
+ execute(
+ interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction,
+ context: RequestContext,
+ ): HandlerResult;
+ autocomplete?(interaction: AutocompleteInteraction): Promise;
+}
+
+export interface PrefixCommand {
+ readonly name: string;
+ readonly aliases?: readonly string[];
+ execute(
+ message: Message,
+ args: string,
+ context: RequestContext,
+ ): HandlerResult;
+}
+
+export type ModalHandler = (
+ interaction: ModalSubmitInteraction,
+ context: RequestContext,
+) => HandlerResult;
+
+export type ComponentHandler = (
+ interaction: ButtonInteraction,
+ context: RequestContext,
+) => HandlerResult;
+
+export interface Feature {
+ readonly id: FeatureId;
+ readonly slashCommands?: readonly SlashCommand[];
+ readonly prefixCommands?: readonly PrefixCommand[];
+ readonly modalHandlers?: Readonly>;
+ readonly componentHandlers?: Readonly>;
+ onMessage?(message: Message, context: RequestContext): HandlerResult;
+ init?(): Promise;
+}
+
+export interface FeatureDependencies {
+ readonly optOutUsers: OptOutUsers;
+ readonly usageStats: UsageStats;
+ readonly logger: AppLogger;
+ readonly env: Env;
+ readonly client: import("discord.js").Client;
+}
+
+export type FeatureFactory = (
+ dependencies: FeatureDependencies,
+) => Feature | undefined;
+
+interface ReplyBase {
+ readonly files?: BaseMessageOptions["files"];
+ readonly ephemeral?: boolean;
+ readonly onDelivered?: (message: Message) => void | Promise;
+}
+
+export interface ComponentsV2Reply extends ReplyBase {
+ readonly kind: "components-v2";
+ readonly components: NonNullable;
+}
+
+export interface PlainContentReply extends ReplyBase {
+ readonly kind: "plain";
+ readonly content: string;
+}
+
+export type OutgoingReply = ComponentsV2Reply | PlainContentReply;
diff --git a/discord/tsconfig.json b/discord/tsconfig.json
new file mode 100644
index 0000000..f9b1acf
--- /dev/null
+++ b/discord/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2023",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "skipLibCheck": true,
+ "noUncheckedIndexedAccess": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "declaration": false
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/discord/uv.lock b/discord/uv.lock
deleted file mode 100644
index 3b19a93..0000000
--- a/discord/uv.lock
+++ /dev/null
@@ -1,1015 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.14"
-
-[[package]]
-name = "aiohappyeyeballs"
-version = "2.6.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
-]
-
-[[package]]
-name = "aiohttp"
-version = "3.13.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohappyeyeballs" },
- { name = "aiosignal" },
- { name = "attrs" },
- { name = "frozenlist" },
- { name = "multidict" },
- { name = "propcache" },
- { name = "yarl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" },
- { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" },
- { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" },
- { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" },
- { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" },
- { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" },
- { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" },
- { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" },
- { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" },
- { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" },
- { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" },
- { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" },
- { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" },
- { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" },
- { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" },
- { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" },
- { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" },
- { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" },
- { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" },
- { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" },
- { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" },
- { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" },
- { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" },
- { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" },
- { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" },
- { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" },
- { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" },
- { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" },
- { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" },
- { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" },
- { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" },
- { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" },
-]
-
-[[package]]
-name = "aiosignal"
-version = "1.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "frozenlist" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.13.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
-]
-
-[[package]]
-name = "attrs"
-version = "26.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.4.22"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
-]
-
-[[package]]
-name = "cffi"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pycparser", marker = "implementation_name != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
- { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
- { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
- { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
- { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
- { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
- { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
- { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
- { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
- { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
- { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
- { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
- { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
- { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
- { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
- { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
- { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
- { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
- { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
- { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
- { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
- { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
-]
-
-[[package]]
-name = "charset-normalizer"
-version = "3.4.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
- { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
- { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
- { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
- { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
- { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
- { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
- { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
- { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
- { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
- { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
- { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
- { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
- { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
- { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
- { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
- { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
- { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
- { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
- { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
- { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
- { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
- { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
- { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
- { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
- { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
- { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
- { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
- { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
- { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
- { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
- { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
- { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
-]
-
-[[package]]
-name = "circuitbreaker"
-version = "2.1.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/df/ac/de7a92c4ed39cba31fe5ad9203b76a25ca67c530797f6bb420fff5f65ccb/circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084", size = 10787, upload-time = "2025-03-31T08:12:08.963Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/34/15f08edd4628f65217de1fc3c1a27c82e46fe357d60c217fc9881e12ebcc/circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1", size = 7737, upload-time = "2025-03-31T08:12:07.802Z" },
-]
-
-[[package]]
-name = "coderunbot-discord"
-version = "0.1.0"
-source = { virtual = "." }
-dependencies = [
- { name = "aiohttp" },
- { name = "google-api-python-client" },
- { name = "google-auth-oauthlib" },
- { name = "iso639-lang" },
- { name = "oci" },
- { name = "openai" },
- { name = "py-cord", extra = ["voice"] },
- { name = "python-dotenv" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "aiohttp" },
- { name = "google-api-python-client" },
- { name = "google-auth-oauthlib" },
- { name = "iso639-lang" },
- { name = "oci" },
- { name = "openai" },
- { name = "py-cord", extras = ["voice"], specifier = ">=2.6" },
- { name = "python-dotenv" },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "cryptography"
-version = "46.0.7"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
- { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
- { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
- { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
- { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
- { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
- { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
- { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
- { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
- { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
- { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
- { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
- { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
- { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
- { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
- { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
- { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
- { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
- { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
- { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
- { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
- { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
- { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
- { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
- { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
- { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
- { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
- { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
- { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
- { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
- { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
- { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
- { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
- { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
- { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
- { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
- { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
- { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
-]
-
-[[package]]
-name = "distro"
-version = "1.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
-]
-
-[[package]]
-name = "frozenlist"
-version = "1.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
- { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
- { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
- { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
- { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
- { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
- { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
- { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
- { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
- { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
- { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
- { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
- { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
- { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
- { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
- { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
- { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
- { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
- { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
- { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
- { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
- { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
- { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
- { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
- { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
- { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
- { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
-]
-
-[[package]]
-name = "google-api-core"
-version = "2.30.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "google-auth" },
- { name = "googleapis-common-protos" },
- { name = "proto-plus" },
- { name = "protobuf" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" },
-]
-
-[[package]]
-name = "google-api-python-client"
-version = "2.194.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "google-api-core" },
- { name = "google-auth" },
- { name = "google-auth-httplib2" },
- { name = "httplib2" },
- { name = "uritemplate" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/60/ab/e83af0eb043e4ccc49571ca7a6a49984e9d00f4e9e6e6f1238d60bc84dce/google_api_python_client-2.194.0.tar.gz", hash = "sha256:db92647bd1a90f40b79c9618461553c2b20b6a43ce7395fa6de07132dc14f023", size = 14443469, upload-time = "2026-04-08T23:07:35.757Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/34/5a624e49f179aa5b0cb87b2ce8093960299030ff40423bfbde09360eb908/google_api_python_client-2.194.0-py3-none-any.whl", hash = "sha256:61eaaac3b8fc8fdf11c08af87abc3d1342d1b37319cc1b57405f86ef7697e717", size = 15016514, upload-time = "2026-04-08T23:07:33.093Z" },
-]
-
-[[package]]
-name = "google-auth"
-version = "2.49.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
- { name = "pyasn1-modules" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" },
-]
-
-[[package]]
-name = "google-auth-httplib2"
-version = "0.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "google-auth" },
- { name = "httplib2" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ed/99/107612bef8d24b298bb5a7c8466f908ecda791d43f9466f5c3978f5b24c1/google_auth_httplib2-0.3.1.tar.gz", hash = "sha256:0af542e815784cb64159b4469aa5d71dd41069ba93effa006e1916b1dcd88e55", size = 11152, upload-time = "2026-03-30T22:50:26.766Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/97/e9/93afb14d23a949acaa3f4e7cc51a0024671174e116e35f42850764b99634/google_auth_httplib2-0.3.1-py3-none-any.whl", hash = "sha256:682356a90ef4ba3d06548c37e9112eea6fc00395a11b0303a644c1a86abc275c", size = 9534, upload-time = "2026-03-30T22:49:03.384Z" },
-]
-
-[[package]]
-name = "google-auth-oauthlib"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "google-auth" },
- { name = "requests-oauthlib" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a6/82/62482931dcbe5266a2680d0da17096f2aab983ecb320277d9556700ce00e/google_auth_oauthlib-1.3.1.tar.gz", hash = "sha256:14c22c7b3dd3d06dbe44264144409039465effdd1eef94f7ce3710e486cc4bfa", size = 21663, upload-time = "2026-03-30T22:49:56.408Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" },
-]
-
-[[package]]
-name = "googleapis-common-protos"
-version = "1.74.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "protobuf" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
-]
-
-[[package]]
-name = "httplib2"
-version = "0.31.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyparsing" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.13"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
-]
-
-[[package]]
-name = "iso639-lang"
-version = "2.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9b/5a/49bbf16d155192255e7bb37e403b2ac360144992d0d112a865afc62e457f/iso639_lang-2.6.3.tar.gz", hash = "sha256:078ddb7cd0182dcc04367691acc8022ddf7158b6cb09f08f798af823fa864265", size = 319391, upload-time = "2025-07-23T09:04:53.568Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/c7/f6fd3db6c33a164631c39dce2ca26a3794e3abf91b875cc99a43a5565d88/iso639_lang-2.6.3-py3-none-any.whl", hash = "sha256:a6c2fb9f739dca180dc7f48b098880f303bcce2cdf93a4ca3152ed8bbbb94fbb", size = 324990, upload-time = "2025-07-23T09:04:52.221Z" },
-]
-
-[[package]]
-name = "jiter"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" },
- { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" },
- { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" },
- { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" },
- { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" },
- { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" },
- { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" },
- { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" },
- { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" },
- { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" },
- { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" },
- { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" },
- { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" },
- { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" },
- { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" },
- { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" },
- { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" },
- { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" },
- { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" },
- { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" },
- { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" },
- { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" },
- { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" },
- { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" },
- { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" },
- { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" },
- { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" },
-]
-
-[[package]]
-name = "multidict"
-version = "6.7.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
- { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
- { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
- { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
- { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
- { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
- { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
- { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
- { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
- { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
- { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
- { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
- { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
- { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
- { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
- { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
- { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
- { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
- { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
- { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
- { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
- { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
- { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
- { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
- { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
- { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
- { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
- { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
- { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
- { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
- { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
- { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
-]
-
-[[package]]
-name = "oauthlib"
-version = "3.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
-]
-
-[[package]]
-name = "oci"
-version = "2.172.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "circuitbreaker" },
- { name = "cryptography" },
- { name = "pyopenssl" },
- { name = "python-dateutil" },
- { name = "pytz" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/97/ed/e9c0fa954f0fa77946993a46de68862c984f7ae8e9e651406a776214d1c9/oci-2.172.0.tar.gz", hash = "sha256:4453ab363e4f9d4b9890a3030f082f1841398f2cea66b63e44dd51cf6be0d347", size = 17086041, upload-time = "2026-04-21T04:41:35.343Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/e5/193bca3a95b1fb272a0f0fb02d966a9ba1d10dd264d21143d85625c55de8/oci-2.172.0-py3-none-any.whl", hash = "sha256:61f07c086b0140033ccd57a0632bdd747ce2dbc3cedc198639116dd78510e3ca", size = 34825199, upload-time = "2026-04-21T04:41:26.297Z" },
-]
-
-[[package]]
-name = "openai"
-version = "2.32.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "distro" },
- { name = "httpx" },
- { name = "jiter" },
- { name = "pydantic" },
- { name = "sniffio" },
- { name = "tqdm" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" },
-]
-
-[[package]]
-name = "propcache"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
- { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
- { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
- { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
- { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
- { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
- { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
- { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
- { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
- { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
- { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
- { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
- { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
- { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
- { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
- { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
- { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
- { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
- { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
- { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
- { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
- { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
- { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
- { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
- { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
- { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
-]
-
-[[package]]
-name = "proto-plus"
-version = "1.27.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "protobuf" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" },
-]
-
-[[package]]
-name = "protobuf"
-version = "7.34.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" },
- { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" },
- { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" },
- { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" },
- { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" },
- { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" },
- { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" },
-]
-
-[[package]]
-name = "py-cord"
-version = "2.7.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/75/5b510b4f30c0fe9ae0c440d968e32df1d8857562e49cc652e4f491871384/py_cord-2.7.2.tar.gz", hash = "sha256:86d2172c5b0de33d3aa5611d940dd98acfb1b95adfa10ae7b749e6aaf923ffff", size = 1132471, upload-time = "2026-04-14T11:21:20.481Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/4c/1d623000e5a91236895d89bfc2a2947db328182715ac2d7e7ef727938e77/py_cord-2.7.2-py3-none-any.whl", hash = "sha256:e25e5e9392f297b3d8fb8f9f31e57989276c1e2b12ebed5a7b1040dd5891d972", size = 1208198, upload-time = "2026-04-14T11:21:18.741Z" },
-]
-
-[package.optional-dependencies]
-voice = [
- { name = "pynacl" },
-]
-
-[[package]]
-name = "pyasn1"
-version = "0.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
-]
-
-[[package]]
-name = "pyasn1-modules"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyasn1" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
-]
-
-[[package]]
-name = "pycparser"
-version = "3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.13.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-types" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.46.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
- { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
- { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
- { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
- { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
- { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
- { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
- { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
- { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
- { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
- { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
- { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
- { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
- { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
- { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
- { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
- { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
- { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
- { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
- { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
- { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
- { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
- { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
- { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
- { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
- { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
- { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
- { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
-]
-
-[[package]]
-name = "pynacl"
-version = "1.6.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" },
- { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" },
- { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" },
- { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" },
- { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" },
- { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" },
- { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" },
- { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" },
- { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" },
- { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" },
- { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" },
- { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" },
- { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" },
- { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" },
- { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" },
- { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" },
- { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" },
- { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" },
- { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" },
- { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" },
- { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" },
- { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" },
- { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
-]
-
-[[package]]
-name = "pyopenssl"
-version = "26.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" },
-]
-
-[[package]]
-name = "pyparsing"
-version = "3.3.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
-]
-
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
-]
-
-[[package]]
-name = "pytz"
-version = "2026.1.post1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
-]
-
-[[package]]
-name = "requests"
-version = "2.33.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
-]
-
-[[package]]
-name = "requests-oauthlib"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "oauthlib" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
-]
-
-[[package]]
-name = "six"
-version = "1.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
-]
-
-[[package]]
-name = "sniffio"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
-]
-
-[[package]]
-name = "tqdm"
-version = "4.67.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.15.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
-]
-
-[[package]]
-name = "uritemplate"
-version = "4.2.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
-]
-
-[[package]]
-name = "urllib3"
-version = "2.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
-]
-
-[[package]]
-name = "yarl"
-version = "1.23.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
- { name = "multidict" },
- { name = "propcache" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" },
- { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" },
- { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" },
- { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" },
- { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" },
- { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" },
- { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" },
- { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" },
- { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" },
- { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" },
- { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" },
- { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" },
- { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" },
- { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" },
- { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" },
- { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" },
- { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" },
- { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" },
- { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" },
- { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" },
- { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" },
- { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" },
- { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" },
- { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" },
- { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" },
- { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" },
- { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" },
- { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" },
- { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" },
- { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" },
- { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" },
- { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" },
- { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" },
- { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" },
- { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" },
- { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" },
- { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
-]
diff --git a/discord/vitest.config.ts b/discord/vitest.config.ts
new file mode 100644
index 0000000..5bfa2d5
--- /dev/null
+++ b/discord/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: ["src/**/*.test.ts"],
+ passWithNoTests: true,
+ },
+});
diff --git a/docs/en/index.md b/docs/en/index.md
index b7dc52f..5b9019d 100644
--- a/docs/en/index.md
+++ b/docs/en/index.md
@@ -1,118 +1,69 @@
-# Bot help
-## Index
-- [Introduction](#Intoroduction)
-- [Commands](#Commands)
- - [run](#run)
- - [Example](#e1)
- - [Languages](#Languages)
- - [save](#save)
- - [tex](#tex)
- - [Example](#e2)
- - [texp](#texp)
- - [Example](#e3)
-
-## Intoroduction
-This Bot is made by Gart ([@gaato__](https://twitter.com/gaato__)).
-
-[Here](https://discord.gg/qRpYRTgvXM) is the discord server.
+# How to use CodeRunBot
+
+## Introduction
+
+This bot is made by gaato ([@gaato__](https://twitter.com/gaato__)).
+
+- Official Discord server: [here](https://discord.gg/qRpYRTgvXM)
+- [Invite link](https://discord.com/api/oauth2/authorize?client_id=761428259241328680&permissions=0&scope=bot)
+
+## Installation
+
+There are two ways to use the bot.
+
+- **Add to a server**: adds slash commands plus `]`-prefixed text commands.
+- **User install**: add the app to your own account and use the slash commands
+ (`/run`, `/tex`, ...) in DMs and in servers that don't have the bot.
+
## Commands
-### run
+
+### /run
+
+Pick a language (with autocomplete) and a form opens for your code and
+standard input. The code runs on [Wandbox](https://wandbox.org).
+
+Supported languages are fetched dynamically from Wandbox — whatever appears in
+the autocomplete is what is available.
+
+### ]run (server only)
+
```
]run language
code
```
-runs your code.
-And the bot ignores "```" for a code block.
-#### Example
+Code fences (```) are ignored.
+
```
]run python
print('hello')
```
-#### Languages
-- Bash (bash)
-- C (c)
-- C# (c#)
-- CLISP (clisp)
-- CoffeeScript (coffeescript)
-- Crystal (crystal)
-- D (d)
-- Elixir (elixir)
-- Erlang (erlang)
-- F# (f#)
-- Free Pascal (fpc)
-- Go (go)
-- Groovy (groovy)
-- Haskell (haskell)
-- Java (java)
-- JavaScript (javascript)
-- Lazy K (lazyk)
-- Lua (lua)
-- Nim (nim)
-- OCaml (ocaml)
-- OpenSSL (openssl)
-- Perl (perl)
-- PHP (php)
-- Pony (pony)
-- PyPy (pypy)
-- Python (python)
-- R (r)
-- Rill (rill)
-- Ruby (ruby)
-- Rust (rust)
-- Scala (scala)
-- SQL (sql)
-- Swift (swift)
-- TypeScript (typescript)
-- VimScript (vimscript)
-
-### save
-```
-]save language
-code
-```
-saves your code.
-You can call it by sending as follows.
-```
-]run saved
-standard input
-```
-### tex
-```
-]tex
-math commands
-```
-images your math commands.
+### /tex
-And the bot ignores "```" for a code block.
+Opens a form for LaTeX input. The `env` option wraps your input in an
+align / gather environment, and `spoiler` posts the image as a spoiler.
+
+### ]tex / ]stex (server only)
-It uses LaTeX gather* environment.
-#### Example
-```
-]tex
-\int_0^1 f(x)\,dx
-```
-### texp
```
-]texp
-LaTeX text
+]tex x^2 + y^2 = r^2
```
-images your LaTeX text.
-And the bot ignores "```" for a code block.
+Renders LaTeX (math mode) as an image. `]stex` posts it as a spoiler.
-It uses LaTeX text mode.
-#### Example
-```
-]texp
-For real constants $a,b,c$ the solution of the quadratic equation
-\[
- ax^2+bx+c=0
-\]
-for complex number $x$ is
-\[
- x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
-\]
-.
-```
+### Right-click a message → Apps → escape
+
+Shows the message content with Markdown and mentions escaped (visible only to you).
+
+### /privacy-policy, /opt-out, /opt-in
+
+Shows the privacy policy and lets you opt out of / back into message content
+processing. See the [privacy policy](../../discord/config/privacy-policy.md)
+for details.
+
+## Handy behaviors
+
+- Every bot reply has a **Delete button** that only the invoking user can press.
+- If you **edit** the message that invoked `]run` or `]tex`, the bot's reply is
+ updated automatically.
diff --git a/docs/ja/index.md b/docs/ja/index.md
index 014595b..0e1dbfc 100644
--- a/docs/ja/index.md
+++ b/docs/ja/index.md
@@ -1,183 +1,67 @@
-# Bot の使い方
-
-## 目次
-
-- [はじめに](#はじめに)
-- [コマンド](#コマンド)
- - [run](#run)
- - [例](#e1)
- - [言語一覧](#言語一覧)
- - [save](#save)
- - [tex](#tex)
- - [例](#e2)
- - [texp](#texp)
- - [例](#e3)
- - [stex](#stex)
- - [stexp](#stexp)
- - [texpdf](#texpdf)
- - [例](#e4)
+# CodeRunBot の使い方
## はじめに
この Bot はがーと([@gaato__](https://twitter.com/gaato__))によって作られました.
-公式 Discord サーバーは[こちら](https://discord.gg/qRpYRTgvXM)
+- 公式 Discord サーバーは[こちら](https://discord.gg/qRpYRTgvXM)
+- [招待リンク](https://discord.com/api/oauth2/authorize?client_id=761428259241328680&permissions=0&scope=bot)
-## コマンド
+## 導入方法
-### run
+2 つの使い方があります.
-```
-]run 言語
-コード
-```
-であなたのコードを実行します.
-
-なおコードブロックのための "```" は無視されます.
-
-#### 例
-
-```
-]run python
-print('hello')
-```
-
-#### 言語一覧
-
-- Bash (bash)
-- C (c)
-- C# (c#)
-- CLISP (clisp)
-- CoffeeScript (coffeescript)
-- Crystal (crystal)
-- D (d)
-- Elixir (elixir)
-- Erlang (erlang)
-- F# (f#)
-- Free Pascal (fpc)
-- Go (go)
-- Groovy (groovy)
-- Haskell (haskell)
-- Java (java)
-- JavaScript (javascript)
-- Lazy K (lazyk)
-- Lua (lua)
-- Nim (nim)
-- OCaml (ocaml)
-- OpenSSL (openssl)
-- Perl (perl)
-- PHP (php)
-- Pony (pony)
-- PyPy (pypy)
-- Python (python)
-- R (r)
-- Rill (rill)
-- Ruby (ruby)
-- Rust (rust)
-- Scala (scala)
-- SQL (sql)
-- Swift (swift)
-- TypeScript (typescript)
-- VimScript (vimscript)
-
-### save
-
-```
-]save 言語
-コード
-```
-であなたのコードを保存します.
-```
-]run saved
-標準入力
-```
-で呼び出せます.
+- **サーバーに追加**: 招待リンクからサーバーに Bot を追加すると,スラッシュコマンドに加えて
+ `]` プレフィックスのテキストコマンドが使えます.
+- **ユーザーインストール**: 自分のアカウントにアプリとして追加すると,Bot がいないサーバーや
+ DM でもスラッシュコマンド(`/run`,`/tex` など)が使えます.
-### tex
+## コマンド
-```
-]tex
-数式コマンド
-```
-であなたの数式コマンドを画像化します.
+### /run
-なおコードブロックのための "```" は無視されます.
+言語を選ぶ(オートコンプリート対応)とコード入力用のフォームが開きます.
+コードと標準入力を入れて送信すると [Wandbox](https://wandbox.org) で実行されます.
-LaTeX の数式モード(gather*環境)を使っています.
+対応言語は Wandbox から動的に取得されるため,オートコンプリートに出るものがすべてです.
-#### 例
+### ]run(サーバー専用)
```
-]tex
-\int_0^1 f(x)\,dx
+]run 言語
+コード
```
-### texp
+コードブロックのための ``` は無視されます.
```
-]texp
-LaTeXによるテキスト
+]run python
+print('hello')
```
-であなたの LaTeX テキストを画像化します.
-なおコードブロックのための "```" は無視されます.
+### /tex
-LaTeX のテキストモードを使っています.
+数式入力用のフォームが開きます.`env` オプションで align / gather 環境を選べ,
+`spoiler` オプションでスポイラー画像にできます.
-#### 例
+### ]tex / ]stex(サーバー専用)
```
-]texp
-実定数$a,b,c$に対して,複素数$x$に関する二次方程式
-\[
- ax^2+bx+c=0
-\]
-の解は
-\[
- x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
-\]
-と表される.
+]tex x^2 + y^2 = r^2
```
-### stex
+LaTeX(数式モード)を画像にします.`]stex` はスポイラー画像になります.
-```
-]stex
-数式コマンド
-```
-[tex](#tex) コマンドでスポイラーをかけます.
+### メッセージを右クリック → アプリ → escape
-### stexp
-
-```
-]stexp
-LaTeXによるテキスト
-```
-[stex](#stex) + [texp](#texp)
+メッセージの Markdown とメンションをエスケープした生テキストを表示します(自分にのみ表示).
-### texpdf
+### /privacy-policy, /opt-out, /opt-in
-```
-]texpdf
-LaTeX文書
-```
-で LaTeX により PDF を作成します.
+プライバシーポリシーの表示と,メッセージ内容の処理のオプトアウト/再開ができます.
+詳しくは[プライバシーポリシー](../../discord/config/privacy-policy.md)を参照してください.
-#### 例
+## 便利な挙動
-```
-]texpdf
-\documentclass[uplatex]{jsarticle}
-\usepackage{amsmath}
-\begin{document}
-実定数$a,b,c$に対して,複素数$x$に関する二次方程式
-\[
- ax^2+bx+c=0
-\]
-の解は
-\[
- x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
-\]
-と表される.
-\end{document}
-```
\ No newline at end of file
+- Bot の返信には **Delete ボタン**が付き,コマンドを実行した本人だけが削除できます.
+- `]run` や `]tex` の呼び出しメッセージを**編集すると,Bot の返信も自動で更新**されます.
diff --git a/tex/.gitignore b/tex/.gitignore
deleted file mode 100644
index c6bba59..0000000
--- a/tex/.gitignore
+++ /dev/null
@@ -1,130 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-.pnpm-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# Snowpack dependency directory (https://snowpack.dev/)
-web_modules/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Optional stylelint cache
-.stylelintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variable files
-.env
-.env.development.local
-.env.test.local
-.env.production.local
-.env.local
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-.parcel-cache
-
-# Next.js build output
-.next
-out
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and not Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# vuepress v2.x temp and cache directory
-.temp
-.cache
-
-# Docusaurus cache and generated files
-.docusaurus
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
-
-# Stores VSCode versions used for testing VSCode extensions
-.vscode-test
-
-# yarn v2
-.yarn/cache
-.yarn/unplugged
-.yarn/build-state.yml
-.yarn/install-state.gz
-.pnp.*
diff --git a/tex/Dockerfile b/tex/Dockerfile
deleted file mode 100644
index 37d467b..0000000
--- a/tex/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM node:18-alpine
-
-WORKDIR /usr/src/app
-
-RUN apk --no-cache add font-noto-cjk-extra
-
-COPY package*.json ./
-RUN npm install
-
-COPY . .
-
-EXPOSE 80
-
-CMD [ "node", "server.js" ]
diff --git a/tex/README-ja.md b/tex/README-ja.md
deleted file mode 100644
index 41bb944..0000000
--- a/tex/README-ja.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# tex.gaato.net
-
-tex.gaato.net は、LaTeX 文字列を SVG や PNG 画像としてレンダリングするためのエンドポイントを提供します。
-
-## エンドポイント
-
-### POST `/render/png`
-
-LaTeX 文字列を PNG 画像としてレンダリングします。
-
-#### リクエストパラメータ
-
-| パラメータ | 型 | 説明 |
-|------------|--------|------------------------|
-| latex | string | レンダリングする LaTeX 文字列 |
-
-#### レスポンス
-
-- **200 OK**
- - `image/png`: PNG 画像
-- **400 Bad Request**
- - `text/plain`: LaTeX 文字列が不足している場合、または LaTeX の構文エラーが発生した場合のエラーメッセージ
-- **500 Internal Server Error**
- - `text/plain`: その他のエラーが発生した場合のエラーメッセージ
-
-### POST `/render/svg`
-
-LaTeX 文字列を SVG 画像としてレンダリングします。
-
-#### リクエストパラメータ
-
-| パラメータ | 型 | 説明 |
-|------------|--------|------------------------|
-| latex | string | レンダリングする LaTeX 文字列 |
-
-#### レスポンス
-
-- **200 OK**
- - `image/svg+xml`: SVG 画像
-- **400 Bad Request**
- - `text/plain`: LaTeX 文字列が不足している場合、または LaTeX の構文エラーが発生した場合のエラーメッセージ
-- **500 Internal Server Error**
- - `text/plain`: その他のエラーが発生した場合のエラーメッセージ
diff --git a/tex/README.md b/tex/README.md
deleted file mode 100644
index e52d811..0000000
--- a/tex/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# tex.gaato.net
-
-tex.gaato.net provides endpoints to render LaTeX strings as SVG or PNG images.
-
-## Endpoints
-
-### POST `/render/png`
-
-Renders a LaTeX string as a PNG image.
-
-#### Request Parameters
-
-| Parameter | Type | Description |
-|-----------|--------|-------------------------------|
-| latex | string | LaTeX string to be rendered |
-
-#### Response
-
-- **200 OK**
- - `image/png`: PNG image
-- **400 Bad Request**
- - `text/plain`: Error message if the LaTeX string is missing or if there's a LaTeX syntax error
-- **500 Internal Server Error**
- - `text/plain`: Error message if any other error occurs
-
-### POST `/render/svg`
-
-Renders a LaTeX string as an SVG image.
-
-#### Request Parameters
-
-| Parameter | Type | Description |
-|-----------|--------|-------------------------------|
-| latex | string | LaTeX string to be rendered |
-
-#### Response
-
-- **200 OK**
- - `image/svg+xml`: SVG image
-- **400 Bad Request**
- - `text/plain`: Error message if the LaTeX string is missing or if there's a LaTeX syntax error
-- **500 Internal Server Error**
- - `text/plain`: Error message if any other error occurs
diff --git a/tex/package-lock.json b/tex/package-lock.json
deleted file mode 100644
index 5fdb00f..0000000
--- a/tex/package-lock.json
+++ /dev/null
@@ -1,1628 +0,0 @@
-{
- "name": "tex.gaato.net",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "dependencies": {
- "express": "^4.18.2",
- "jsdom": "^22.1.0",
- "mathjax": "^3.2.2",
- "mathjax-full": "^3.2.2",
- "sharp": "^0.32.5"
- }
- },
- "node_modules/@tootallnate/once": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
- "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/abab": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
- "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
- "dependencies": {
- "debug": "4"
- },
- "engines": {
- "node": ">= 6.0.0"
- }
- },
- "node_modules/agent-base/node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/agent-base/node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
- },
- "node_modules/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
- },
- "node_modules/b4a": {
- "version": "1.6.4",
- "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
- "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
- },
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "node_modules/body-parser": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
- "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.11.0",
- "raw-body": "2.5.1",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
- "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
- "dependencies": {
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
- },
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
- }
- },
- "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==",
- "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=="
- },
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/commander": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
- "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==",
- "engines": {
- "node": "^12.20.0 || >=14"
- }
- },
- "node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "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==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
- },
- "node_modules/cssstyle": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz",
- "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==",
- "dependencies": {
- "rrweb-cssom": "^0.6.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/data-urls": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
- "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
- "dependencies": {
- "abab": "^2.0.6",
- "whatwg-mimetype": "^3.0.0",
- "whatwg-url": "^12.0.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/decimal.js": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
- "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
- },
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "dependencies": {
- "mimic-response": "^3.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "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==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
- "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/domexception": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
- "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
- "dependencies": {
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "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=="
- },
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "dependencies": {
- "once": "^1.4.0"
- }
- },
- "node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "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=="
- },
- "node_modules/esm": {
- "version": "3.2.25",
- "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
- "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/expand-template": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/express": {
- "version": "4.18.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
- "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.20.1",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.5.0",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "1.2.0",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "merge-descriptors": "1.0.1",
- "methods": "~1.1.2",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.7",
- "qs": "6.11.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.18.0",
- "serve-static": "1.15.0",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/fast-fifo": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
- "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
- },
- "node_modules/finalhandler": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
- "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "2.0.1",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fs-constants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
- },
- "node_modules/function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
- },
- "node_modules/get-intrinsic": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
- "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
- "dependencies": {
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/github-from-package": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
- },
- "node_modules/has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dependencies": {
- "function-bind": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/has-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
- "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/html-encoding-sniffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
- "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
- "dependencies": {
- "whatwg-encoding": "^2.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
- "dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/http-proxy-agent": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
- "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
- "dependencies": {
- "@tootallnate/once": "2",
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/http-proxy-agent/node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/http-proxy-agent/node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
- },
- "node_modules/https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
- "dependencies": {
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/https-proxy-agent/node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/https-proxy-agent/node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
- },
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
- },
- "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==",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
- },
- "node_modules/is-potential-custom-element-name": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
- "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
- },
- "node_modules/jsdom": {
- "version": "22.1.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz",
- "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==",
- "dependencies": {
- "abab": "^2.0.6",
- "cssstyle": "^3.0.0",
- "data-urls": "^4.0.0",
- "decimal.js": "^10.4.3",
- "domexception": "^4.0.0",
- "form-data": "^4.0.0",
- "html-encoding-sniffer": "^3.0.0",
- "http-proxy-agent": "^5.0.0",
- "https-proxy-agent": "^5.0.1",
- "is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.4",
- "parse5": "^7.1.2",
- "rrweb-cssom": "^0.6.0",
- "saxes": "^6.0.0",
- "symbol-tree": "^3.2.4",
- "tough-cookie": "^4.1.2",
- "w3c-xmlserializer": "^4.0.0",
- "webidl-conversions": "^7.0.0",
- "whatwg-encoding": "^2.0.0",
- "whatwg-mimetype": "^3.0.0",
- "whatwg-url": "^12.0.1",
- "ws": "^8.13.0",
- "xml-name-validator": "^4.0.0"
- },
- "engines": {
- "node": ">=16"
- },
- "peerDependencies": {
- "canvas": "^2.5.0"
- },
- "peerDependenciesMeta": {
- "canvas": {
- "optional": true
- }
- }
- },
- "node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/mathjax": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz",
- "integrity": "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw=="
- },
- "node_modules/mathjax-full": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz",
- "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==",
- "dependencies": {
- "esm": "^3.2.25",
- "mhchemparser": "^4.1.0",
- "mj-context-menu": "^0.6.1",
- "speech-rule-engine": "^4.0.6"
- }
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mhchemparser": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz",
- "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ=="
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/mj-context-menu": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
- "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
- },
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
- },
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/napi-build-utils": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
- "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
- },
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/node-abi": {
- "version": "3.47.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz",
- "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==",
- "dependencies": {
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/node-addon-api": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
- "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="
- },
- "node_modules/nwsapi": {
- "version": "2.2.7",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
- "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ=="
- },
- "node_modules/object-inspect": {
- "version": "1.12.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
- "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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==",
- "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==",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/parse5": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
- "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
- "dependencies": {
- "entities": "^4.4.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
- },
- "node_modules/prebuild-install": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
- "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
- "dependencies": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^1.0.1",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- },
- "bin": {
- "prebuild-install": "bin.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/prebuild-install/node_modules/tar-fs": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
- "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
- "dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "node_modules/prebuild-install/node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/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==",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/psl": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
- "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
- },
- "node_modules/pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
- "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
- "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/qs": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
- "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
- "dependencies": {
- "side-channel": "^1.0.4"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/querystringify": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
- "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
- },
- "node_modules/queue-tick": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
- "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
- "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/requires-port": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
- "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
- },
- "node_modules/rrweb-cssom": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
- "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw=="
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
- },
- "node_modules/saxes": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
- "dependencies": {
- "xmlchars": "^2.2.0"
- },
- "engines": {
- "node": ">=v12.22.7"
- }
- },
- "node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "2.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/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=="
- },
- "node_modules/serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
- "dependencies": {
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.18.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
- },
- "node_modules/sharp": {
- "version": "0.32.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz",
- "integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==",
- "hasInstallScript": true,
- "dependencies": {
- "color": "^4.2.3",
- "detect-libc": "^2.0.2",
- "node-addon-api": "^6.1.0",
- "prebuild-install": "^7.1.1",
- "semver": "^7.5.4",
- "simple-get": "^4.0.1",
- "tar-fs": "^3.0.4",
- "tunnel-agent": "^0.6.0"
- },
- "engines": {
- "node": ">=14.15.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
- "dependencies": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/simple-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
- "node_modules/simple-swizzle": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
- "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
- "node_modules/speech-rule-engine": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz",
- "integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==",
- "dependencies": {
- "commander": "9.2.0",
- "wicked-good-xpath": "1.3.0",
- "xmldom-sre": "0.1.31"
- },
- "bin": {
- "sre": "bin/sre"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/streamx": {
- "version": "2.15.1",
- "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz",
- "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==",
- "dependencies": {
- "fast-fifo": "^1.1.0",
- "queue-tick": "^1.0.1"
- }
- },
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/symbol-tree": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
- },
- "node_modules/tar-fs": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
- "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
- "dependencies": {
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^3.1.5"
- }
- },
- "node_modules/tar-stream": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
- "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
- "dependencies": {
- "b4a": "^1.6.4",
- "fast-fifo": "^1.2.0",
- "streamx": "^2.15.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==",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/tough-cookie": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
- "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
- "dependencies": {
- "psl": "^1.1.33",
- "punycode": "^2.1.1",
- "universalify": "^0.2.0",
- "url-parse": "^1.5.3"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/tr46": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
- "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
- "dependencies": {
- "punycode": "^2.3.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/universalify": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
- "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/url-parse": {
- "version": "1.5.10",
- "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
- "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
- "dependencies": {
- "querystringify": "^2.1.1",
- "requires-port": "^1.0.0"
- }
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "engines": {
- "node": ">= 0.4.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==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/w3c-xmlserializer": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
- "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
- "dependencies": {
- "xml-name-validator": "^4.0.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/webidl-conversions": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/whatwg-encoding": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
- "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
- "dependencies": {
- "iconv-lite": "0.6.3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/whatwg-encoding/node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/whatwg-mimetype": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
- "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/whatwg-url": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz",
- "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==",
- "dependencies": {
- "tr46": "^4.1.1",
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/wicked-good-xpath": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
- "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw=="
- },
- "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=="
- },
- "node_modules/ws": {
- "version": "8.13.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
- "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/xml-name-validator": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
- "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/xmlchars": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
- "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
- },
- "node_modules/xmldom-sre": {
- "version": "0.1.31",
- "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
- "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==",
- "engines": {
- "node": ">=0.1"
- }
- },
- "node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- }
- }
-}
diff --git a/tex/package.json b/tex/package.json
deleted file mode 100644
index 45f87df..0000000
--- a/tex/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "dependencies": {
- "express": "^4.18.2",
- "jsdom": "^22.1.0",
- "mathjax": "^3.2.2",
- "mathjax-full": "^3.2.2",
- "sharp": "^0.32.5"
- }
-}
diff --git a/tex/server.js b/tex/server.js
deleted file mode 100644
index 1abfd38..0000000
--- a/tex/server.js
+++ /dev/null
@@ -1,99 +0,0 @@
-const { mathjax } = require('mathjax-full/js/mathjax.js');
-const { TeX } = require('mathjax-full/js/input/tex.js');
-const { SVG } = require('mathjax-full/js/output/svg.js');
-const { liteAdaptor } = require('mathjax-full/js/adaptors/liteAdaptor.js');
-const { RegisterHTMLHandler } = require('mathjax-full/js/handlers/html.js');
-const { AllPackages } = require('mathjax-full/js/input/tex/AllPackages.js');
-const { JSDOM } = require('jsdom');
-const express = require('express');
-const bodyParser = require('body-parser');
-const sharp = require('sharp');
-
-
-const app = express();
-const host = process.env.HOST || '127.0.0.1';
-const port = Number.parseInt(process.env.PORT || '8080', 10);
-
-app.use(bodyParser.json());
-
-const adaptor = liteAdaptor();
-RegisterHTMLHandler(adaptor);
-
-async function convertToSvg(latex) {
- try {
- const tex = new TeX({ packages: AllPackages });
- const svg = new SVG();
- const doc = mathjax.document('', { InputJax: tex, OutputJax: svg });
- const node = doc.convert(latex, { display: true, em: 16, ex: 8, containerWidth: 80 });
-
- const svgString = adaptor.outerHTML(node);
- const svgTag = svgString.match(/