diff --git a/.github/release-drafter/graph-config.yml b/.github/release-drafter-base.yml
similarity index 88%
rename from .github/release-drafter/graph-config.yml
rename to .github/release-drafter-base.yml
index 88d76b78b55..ea259fc0d2d 100644
--- a/.github/release-drafter/graph-config.yml
+++ b/.github/release-drafter-base.yml
@@ -1,5 +1,5 @@
-name-template: 'graph@$NEXT_PATCH_VERSION'
-tag-template: 'graph@$NEXT_PATCH_VERSION'
+name-template: 'json@$NEXT_PATCH_VERSION'
+tag-template: 'json@$NEXT_PATCH_VERSION'
 autolabeler:
   - label: 'chore'
     files:
@@ -33,9 +33,10 @@ categories:
       - 'maintenance'
       - 'documentation'
       - 'docs'
+
 change-template: '- $TITLE (#$NUMBER)'
 include-paths:
-  - 'packages/graph'
+  - 'packages/json'
 exclude-labels:
   - 'skip-changelog'
 template: |
diff --git a/.github/release-drafter/entraid-config.yml b/.github/release-drafter/entraid-config.yml
new file mode 100644
index 00000000000..d0ddd00773a
--- /dev/null
+++ b/.github/release-drafter/entraid-config.yml
@@ -0,0 +1,50 @@
+name-template: 'entraid@$NEXT_PATCH_VERSION'
+tag-template: 'entraid@$NEXT_PATCH_VERSION'
+autolabeler:
+  - label: 'chore'
+    files:
+      - '*.md'
+      - '.github/*'
+  - label: 'bug'
+    branch:
+      - '/bug-.+'
+  - label: 'chore'
+    branch:
+      - '/chore-.+'
+  - label: 'feature'
+    branch:
+      - '/feature-.+'
+categories:
+  - title: 'Breaking Changes'
+    labels:
+      - 'breakingchange'
+  - title: '🚀 New Features'
+    labels:
+      - 'feature'
+      - 'enhancement'
+  - title: '🐛 Bug Fixes'
+    labels:
+      - 'fix'
+      - 'bugfix'
+      - 'bug'
+  - title: '🧰 Maintenance'
+    label:
+      - 'chore'
+      - 'maintenance'
+      - 'documentation'
+      - 'docs'
+
+change-template: '- $TITLE (#$NUMBER)'
+include-paths:
+  - 'packages/entraid'
+exclude-labels:
+  - 'skip-changelog'
+template: |
+  ## Changes
+
+  $CHANGES
+
+  ## Contributors
+  We'd like to thank all the contributors who worked on this release!
+
+  $CONTRIBUTORS
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index e095faf1918..a8c22752423 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -17,8 +17,6 @@ jobs:
         uses: actions/setup-node@v3
       - name: Install Packages
         run: npm ci
-      - name: Build tests tools
-        run: npm run build:tests-tools
       - name: Generate Documentation
         run: npm run documentation
       - name: Upload
diff --git a/.github/workflows/release-drafter-graph.yml b/.github/workflows/release-drafter-entraid.yml
similarity index 90%
rename from .github/workflows/release-drafter-graph.yml
rename to .github/workflows/release-drafter-entraid.yml
index 4d664e5f19e..d522c6cef6f 100644
--- a/.github/workflows/release-drafter-graph.yml
+++ b/.github/workflows/release-drafter-entraid.yml
@@ -19,6 +19,6 @@ jobs:
       - uses: release-drafter/release-drafter@v5
         with:
           # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
-           config-name: release-drafter/graph-config.yml
+           config-name: release-drafter/entraid-config.yml
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000000..e7c9d58fe71
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,51 @@
+name: Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Version to release ("major", "minor", "patch", or "pre*" version; or specify version like "5.3.3")'
+        required: true
+        type: string
+      args:
+        description: 'Additional arguments to pass to release-it (e.g. "--dry-run"). See docs: https://github.com/release-it/release-it/blob/main/docs/git.md#configuration-options'
+        required: false
+        type: string
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      packages: write
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          ssh-key: ${{ secrets.RELEASE_KEY }}
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '22'
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Install dependencies
+        run: npm ci
+
+      - name: Configure Git
+        run: |
+          git config --local user.email "action@github.com"
+          git config --local user.name "GitHub Action"
+
+      # Build all packages
+      - name: Build packages
+        run: npm run build
+
+      # Release using the monorepo approach
+      - name: Release packages
+        run: npm run release -- --ci -i ${{ github.event.inputs.version }} ${{ github.event.inputs.args }}
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
new file mode 100644
index 00000000000..445af1c818c
--- /dev/null
+++ b/.github/workflows/stale-issues.yml
@@ -0,0 +1,25 @@
+name: "Close stale issues"
+on:
+  schedule:
+  - cron: "0 0 * * *"
+
+permissions: {}
+jobs:
+  stale:
+    permissions:
+      issues: write  #  to close stale issues (actions/stale)
+      pull-requests: write  #  to close stale PRs (actions/stale)
+
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/stale@v9
+      with:
+        repo-token: ${{ secrets.GITHUB_TOKEN }}
+        stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.'
+        stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.'
+        days-before-stale: 365
+        days-before-close: 30
+        stale-issue-label: "Stale"
+        stale-pr-label: "Stale"
+        operations-per-run: 10
+        remove-stale-when-updated: true
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 84d70d6b4c0..df8cb1d1b6c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -5,25 +5,30 @@ on:
     branches:
       - master
       - v4.0
+      - v5
+    paths-ignore:
+      - "**/*.md"
   pull_request:
     branches:
       - master
       - v4.0
-
+      - v5
+    paths-ignore:
+      - "**/*.md"
 jobs:
   tests:
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        node-version: ['18', '20']
-        redis-version: ['5', '6.0', '6.2', '7.0', '7.2', '7.4-rc2']
+        node-version: ["18", "20", "22"]
+        redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2-rc1"]
     steps:
       - uses: actions/checkout@v4
         with:
           fetch-depth: 1
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
       - name: Update npm
@@ -31,10 +36,10 @@ jobs:
         if: ${{ matrix.node-version <= 14 }}
       - name: Install Packages
         run: npm ci
-      - name: Build tests tools
-        run: npm run build:tests-tools
+      - name: Build
+        run: npm run build
       - name: Run Tests
-        run: npm run test -- -- --forbid-only --redis-version=${{ matrix.redis-version }}
+        run: npm run test -ws --if-present -- --forbid-only --redis-version=${{ matrix.redis-version }}
       - name: Upload to Codecov
         run: |
           curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
diff --git a/.gitignore b/.gitignore
index dfd47ff6716..ecdef37dffd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ node_modules/
 .DS_Store
 dump.rdb
 documentation/
+tsconfig.tsbuildinfo
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index a36c5e83cf2..00000000000
--- a/.npmignore
+++ /dev/null
@@ -1,12 +0,0 @@
-.github/
-.vscode/
-docs/
-examples/
-packages/
-.deepsource.toml
-.release-it.json
-CONTRIBUTING.md
-SECURITY.md
-index.ts
-tsconfig.base.json
-tsconfig.json
diff --git a/.release-it.json b/.release-it.json
deleted file mode 100644
index 982b4ac6cbe..00000000000
--- a/.release-it.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "git": {
-    "tagName": "redis@${version}",
-    "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
-  }
-}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33cf69851c7..fbc3070381e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,7 @@
 
 - Fix `NOAUTH` error when using authentication & database (#1681)
 - Allow to `.quit()` in PubSub mode (#1766)
-- Add an option to configurate `name` on a client (#1758)
+- Add an option to configure `name` on a client (#1758)
 - Lowercase commands (`client.hset`) in `legacyMode`
 - Fix PubSub resubscribe (#1764)
 - Fix `RedisSocketOptions` type (#1741)
@@ -466,7 +466,7 @@ Features
 Bugfixes
 
 - Fixed a javascript parser regression introduced in 2.0 that could result in timeouts on high load. ([@BridgeAR](https://github.com/BridgeAR))
-- I was not able to write a regression test for this, since the error seems to only occur under heavy load with special conditions. So please have a look for timeouts with the js parser, if you use it and report all issues and switch to the hiredis parser in the meanwhile. If you're able to come up with a reproducable test case, this would be even better :)
+- I was not able to write a regression test for this, since the error seems to only occur under heavy load with special conditions. So please have a look for timeouts with the js parser, if you use it and report all issues and switch to the hiredis parser in the meanwhile. If you're able to come up with a reproducible test case, this would be even better :)
 - Fixed should_buffer boolean for .exec, .select and .auth commands not being returned and fix a couple special conditions ([@BridgeAR](https://github.com/BridgeAR))
 
 If you do not rely on transactions but want to reduce the RTT you can use .batch from now on. It'll behave just the same as .multi but it does not have any transaction and therefor won't roll back any failed commands.<br>
@@ -518,7 +518,7 @@ Bugfixes:
 
 - Fix argument mutation while using the array notation with the multi constructor (@BridgeAR)
 - Fix multi.hmset key not being type converted if used with an object and key not being a string (@BridgeAR)
-- Fix parser errors not being catched properly (@BridgeAR)
+- Fix parser errors not being caught properly (@BridgeAR)
 - Fix a crash that could occur if a redis server does not return the info command as usual #541 (@BridgeAR)
 - Explicitly passing undefined as a callback statement will work again. E.g. client.publish('channel', 'message', undefined); (@BridgeAR)
 
@@ -560,13 +560,13 @@ This is the biggest release that node_redis had since it was released in 2010. A
 - Increased coverage by 10% and add a lot of tests to make sure everything works as it should. We now reached 97% :-) (@BridgeAR)
 - Remove dead code, clean up and refactor very old chunks (@BridgeAR)
 - Don't flush the offline queue if reconnecting (@BridgeAR)
-- Emit all errors insteaf of throwing sometimes and sometimes emitting them (@BridgeAR)
+- Emit all errors instead of throwing sometimes and sometimes emitting them (@BridgeAR)
 - _auth_pass_ passwords are now checked to be a valid password (@jcppman & @BridgeAR)
 
 ## Bug fixes:
 
 - Don't kill the app anymore by randomly throwing errors sync instead of emitting them (@BridgeAR)
-- Don't catch user errors anymore occuring in callbacks (no try callback anymore & more fixes for the parser) (@BridgeAR)
+- Don't catch user errors anymore occurring in callbacks (no try callback anymore & more fixes for the parser) (@BridgeAR)
 - Early garbage collection of queued items (@dohse)
 - Fix js parser returning errors as strings (@BridgeAR)
 - Do not wrap errors into other errors (@BridgeAR)
@@ -588,19 +588,19 @@ This is the biggest release that node_redis had since it was released in 2010. A
 ## Breaking changes:
 
 1. redis.send_command commands have to be lower case from now on. This does only apply if you use `.send_command` directly instead of the convenient methods like `redis.command`.
-2. Error messages have changed quite a bit. If you depend on a specific wording please check your application carfully.
+2. Error messages have changed quite a bit. If you depend on a specific wording please check your application carefully.
 3. Errors are from now on always either returned if a callback is present or emitted. They won't be thrown (neither sync, nor async).
 4. The Multi error handling has changed a lot!
 
 - All errors are from now on errors instead of strings (this only applied to the js parser).
 - If an error occurs while queueing the commands an EXECABORT error will be returned including the failed commands as `.errors` property instead of an array with errors.
 - If an error occurs while executing the commands and that command has a callback it'll return the error as first parameter (`err, undefined` instead of `null, undefined`).
-- All the errors occuring while executing the commands will stay in the result value as error instance (if you used the js parser before they would have been strings). Be aware that the transaction won't be aborted if those error occurr!
+- All the errors occurring while executing the commands will stay in the result value as error instance (if you used the js parser before they would have been strings). Be aware that the transaction won't be aborted if those error occur!
 - If `multi.exec` does not have a callback and an EXECABORT error occurrs, it'll emit that error instead.
 
 5. If redis can't connect to your redis server it'll give up after a certain point of failures (either max connection attempts or connection timeout exceeded). If that is the case it'll emit an CONNECTION_BROKEN error. You'll have to initiate a new client to try again afterwards.
 6. The offline queue is not flushed anymore on a reconnect. It'll stay until node_redis gives up trying to reach the server or until you close the connection.
-7. Before this release node_redis catched user errors and threw them async back. This is not the case anymore! No user behavior of what so ever will be tracked or catched.
+7. Before this release node_redis caught user errors and threw them async back. This is not the case anymore! No user behavior of what so ever will be tracked or caught.
 8. The keyspace of `redis.server_info` (db0...) is from now on an object instead of an string.
 
 NodeRedis also thanks @qdb, @tobek, @cvibhagool, @frewsxcv, @davidbanham, @serv, @vitaliylag, @chrishamant, @GamingCoder and all other contributors that I may have missed for their contributions!
diff --git a/README.md b/README.md
index a590372b1b1..ab6b4707e6f 100644
--- a/README.md
+++ b/README.md
@@ -25,26 +25,12 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No
 
 [Work at Redis](https://redis.com/company/careers/jobs/)
 
-## Packages
-
-| Name                                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                    |
-|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [redis](./)                                  | [![Downloads](https://img.shields.io/npm/dm/redis.svg)](https://www.npmjs.com/package/redis) [![Version](https://img.shields.io/npm/v/redis.svg)](https://www.npmjs.com/package/redis)                                                                                                                                                                                                                                         |
-| [@redis/client](./packages/client)           | [![Downloads](https://img.shields.io/npm/dm/@redis/client.svg)](https://www.npmjs.com/package/@redis/client) [![Version](https://img.shields.io/npm/v/@redis/client.svg)](https://www.npmjs.com/package/@redis/client) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/client/)                                                                                               |
-| [@redis/bloom](./packages/bloom)             | [![Downloads](https://img.shields.io/npm/dm/@redis/bloom.svg)](https://www.npmjs.com/package/@redis/bloom) [![Version](https://img.shields.io/npm/v/@redis/bloom.svg)](https://www.npmjs.com/package/@redis/bloom) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/bloom/) [Redis Bloom](https://oss.redis.com/redisbloom/) commands                                          |
-| [@redis/graph](./packages/graph)             | [![Downloads](https://img.shields.io/npm/dm/@redis/graph.svg)](https://www.npmjs.com/package/@redis/graph) [![Version](https://img.shields.io/npm/v/@redis/graph.svg)](https://www.npmjs.com/package/@redis/graph) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/graph/) [Redis Graph](https://oss.redis.com/redisgraph/) commands                                          |
-| [@redis/json](./packages/json)               | [![Downloads](https://img.shields.io/npm/dm/@redis/json.svg)](https://www.npmjs.com/package/@redis/json) [![Version](https://img.shields.io/npm/v/@redis/json.svg)](https://www.npmjs.com/package/@redis/json) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/json/) [Redis JSON](https://oss.redis.com/redisjson/) commands                                                 |
-| [@redis/search](./packages/search)           | [![Downloads](https://img.shields.io/npm/dm/@redis/search.svg)](https://www.npmjs.com/package/@redis/search) [![Version](https://img.shields.io/npm/v/@redis/search.svg)](https://www.npmjs.com/package/@redis/search) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/search/) [RediSearch](https://oss.redis.com/redisearch/) commands                                    |
-| [@redis/time-series](./packages/time-series) | [![Downloads](https://img.shields.io/npm/dm/@redis/time-series.svg)](https://www.npmjs.com/package/@redis/time-series) [![Version](https://img.shields.io/npm/v/@redis/time-series.svg)](https://www.npmjs.com/package/@redis/time-series) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/time-series/) [Redis Time-Series](https://oss.redis.com/redistimeseries/) commands |
-
-> :warning: In version 4.1.0 we moved our subpackages from `@node-redis` to `@redis`. If you're just using `npm install redis`, you don't need to do anything—it'll upgrade automatically. If you're using the subpackages directly, you'll need to point to the new scope (e.g. `@redis/client` instead of `@node-redis/client`).
-
 ## Installation
 
 Start a redis via docker:
 
-``` bash
-docker run -p 6379:6379 -it redis/redis-stack-server:latest
+```bash
+docker run -p 6379:6379 -d redis:8.0-rc1
 ```
 
 To install node-redis, simply:
@@ -52,77 +38,99 @@ To install node-redis, simply:
 ```bash
 npm install redis
 ```
+> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands,
+> you can install the individual packages. See the list below.
+
+## Packages
+
+| Name                                           | Description                                                                                 |
+| ---------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis)                    | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules |
+| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client)           | The base clients (i.e `RedisClient`, `RedisCluster`, etc.)                                  |
+| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom)             | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands                     |
+| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json)               | [Redis JSON](https://redis.io/docs/data-types/json/) commands                               |
+| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search)           | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands                     |
+| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands                  |
+| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/packages/entraid)         | Secure token-based authentication for Redis clients using Microsoft Entra ID                |
 
-> :warning: The new interface is clean and cool, but if you have an existing codebase, you'll want to read the [migration guide](./docs/v3-to-v4.md).
+> Looking for a high-level library to handle object mapping?
+> See [redis-om-node](https://github.com/redis/redis-om-node)!
 
-Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)!
 
 ## Usage
 
 ### Basic Example
 
 ```typescript
-import { createClient } from 'redis';
+import { createClient } from "redis";
 
 const client = await createClient()
-  .on('error', err => console.log('Redis Client Error', err))
+  .on("error", (err) => console.log("Redis Client Error", err))
   .connect();
 
-await client.set('key', 'value');
-const value = await client.get('key');
-await client.disconnect();
+await client.set("key", "value");
+const value = await client.get("key");
+client.destroy();
 ```
 
-The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`:
+The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in
+the format `redis[s]://[[username][:password]@][host][:port][/db-number]`:
 
 ```typescript
 createClient({
-  url: 'redis://alice:foobared@awesome.redis.server:6380'
+  url: "redis://alice:foobared@awesome.redis.server:6380",
 });
 ```
 
-You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](./docs/client-configuration.md).
+You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in
+the [client configuration guide](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md).
 
-To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. `client.isOpen` is also available.  This returns `true` when the client's underlying socket is open, and `false` when it isn't (for example when the client is still connecting or reconnecting after a network error).
+To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean.
+`client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it
+isn't (for example when the client is still connecting or reconnecting after a network error).
 
 ### Redis Commands
 
-There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.):
+There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed
+using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`,
+etc.):
 
 ```typescript
 // raw Redis commands
-await client.HSET('key', 'field', 'value');
-await client.HGETALL('key');
+await client.HSET("key", "field", "value");
+await client.HGETALL("key");
 
 // friendly JavaScript commands
-await client.hSet('key', 'field', 'value');
-await client.hGetAll('key');
+await client.hSet("key", "field", "value");
+await client.hGetAll("key");
 ```
 
 Modifiers to commands are specified using a JavaScript object:
 
 ```typescript
-await client.set('key', 'value', {
+await client.set("key", "value", {
   EX: 10,
-  NX: true
+  NX: true,
 });
 ```
 
 Replies will be transformed into useful data structures:
 
 ```typescript
-await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' }
-await client.hVals('key'); // ['value1', 'value2']
+await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' }
+await client.hVals("key"); // ['value1', 'value2']
 ```
 
 `Buffer`s are supported as well:
 
 ```typescript
-await client.hSet('key', 'field', Buffer.from('value')); // 'OK'
-await client.hGetAll(
-  commandOptions({ returnBuffers: true }),
-  'key'
-); // { field: <Buffer 76 61 6c 75 65> }
+const client = createClient().withTypeMapping({
+  [RESP_TYPES.BLOB_STRING]: Buffer
+});
+
+await client.hSet("key", "field", Buffer.from("value")); // 'OK'
+await client.hGet("key", "field"); // { field: <Buffer 76 61 6c 75 65> }
+
 ```
 
 ### Unsupported Redis Commands
@@ -130,58 +138,62 @@ await client.hGetAll(
 If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`:
 
 ```typescript
-await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK'
+await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK'
 
-await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2']
+await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2']
 ```
 
 ### Transactions (Multi/Exec)
 
-Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results:
+Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When
+you're done, call `.exec()` and you'll get an array back with your results:
 
 ```typescript
-await client.set('another-key', 'another-value');
+await client.set("another-key", "another-value");
 
 const [setKeyReply, otherKeyValue] = await client
   .multi()
-  .set('key', 'value')
-  .get('another-key')
+  .set("key", "value")
+  .get("another-key")
   .exec(); // ['OK', 'another-value']
 ```
 
-You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change.
+You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling
+`.watch()`. Your transaction will abort if any of the watched keys change.
 
-To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md).
 
 ### Blocking Commands
 
-Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled.
+In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main"
+connection. However, there was no way to use the pool without a "main" connection:
 
-This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`:
+```javascript
+const client = await createClient()
+  .on("error", (err) => console.error(err))
+  .connect();
 
-```typescript
-import { commandOptions } from 'redis';
+await client.ping(client.commandOptions({ isolated: true }));
+```
 
-const blPopPromise = client.blPop(
-  commandOptions({ isolated: true }),
-  'key',
-  0
-);
+In v5 we've extracted this pool logic into its own class—`RedisClientPool`:
 
-await client.lPush('key', ['1', '2']);
+```javascript
+const pool = await createClientPool()
+  .on("error", (err) => console.error(err))
+  .connect();
 
-await blPopPromise; // '2'
+await pool.ping();
 ```
 
-To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md).
 
 ### Pub/Sub
 
-See the [Pub/Sub overview](./docs/pub-sub.md).
+See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md).
 
 ### Scan Iterator
 
-[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
+[`SCAN`](https://redis.io/commands/scan) results can be looped over
+using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
 
 ```typescript
 for await (const key of client.scanIterator()) {
@@ -193,187 +205,121 @@ for await (const key of client.scanIterator()) {
 This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
 
 ```typescript
-for await (const { field, value } of client.hScanIterator('hash')) {}
-for await (const member of client.sScanIterator('set')) {}
-for await (const { score, value } of client.zScanIterator('sorted-set')) {}
+for await (const { field, value } of client.hScanIterator("hash")) {
+}
+for await (const member of client.sScanIterator("set")) {
+}
+for await (const { score, value } of client.zScanIterator("sorted-set")) {
+}
 ```
 
 You can override the default options by providing a configuration object:
 
 ```typescript
 client.scanIterator({
-  TYPE: 'string', // `SCAN` only
-  MATCH: 'patter*',
-  COUNT: 100
+  TYPE: "string", // `SCAN` only
+  MATCH: "patter*",
+  COUNT: 100,
 });
 ```
 
-### [Programmability](https://redis.io/docs/manual/programmability/)
-
-Redis provides a programming interface allowing code execution on the redis server.
-
-#### [Functions](https://redis.io/docs/manual/programmability/functions-intro/)
-
-The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key _foo_ has the value _17_ and we run `add('foo', 25)`, it returns the answer to Life, the Universe and Everything.
-
-```lua
-#!lua name=library
-
-redis.register_function {
-  function_name = 'add',
-  callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end,
-  flags = { 'no-writes' }
-}
-```
+### Disconnecting
 
-Here is the same example, but in a format that can be pasted into the `redis-cli`.
+The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead
+of sending a `QUIT` command to the server, the client can simply close the network connection.
 
-```
-FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name=\"add\", callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={\"no-writes\"}}"
-```
-
-Load the prior redis function on the _redis server_ before running the example below.
+`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to
+`client.destroy()`.
 
 ```typescript
-import { createClient } from 'redis';
-
-const client = createClient({
-  functions: {
-    library: {
-      add: {
-        NUMBER_OF_KEYS: 1,
-        transformArguments(key: string, toAdd: number): Array<string> {
-          return [key, toAdd.toString()];
-        },
-        transformReply(reply: number): number {
-          return reply;
-        }
-      }
-    }
-  }
-});
-
-await client.connect();
-
-await client.set('key', '1');
-await client.library.add('key', 2); // 3
+client.destroy();
 ```
+### Client Side Caching
 
-#### [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/)
-
-The following is an end-to-end example of the prior concept.
+Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid.
 
 ```typescript
-import { createClient, defineScript } from 'redis';
-
+// Enable client side caching with RESP3
 const client = createClient({
-  scripts: {
-    add: defineScript({
-      NUMBER_OF_KEYS: 1,
-      SCRIPT:
-        'return redis.call("GET", KEYS[1]) + ARGV[1];',
-      transformArguments(key: string, toAdd: number): Array<string> {
-        return [key, toAdd.toString()];
-      },
-      transformReply(reply: number): number {
-        return reply;
-      }
-    })
+  RESP: 3, 
+  clientSideCache: {
+    ttl: 0,             // Time-to-live (0 = no expiration)
+    maxEntries: 0,      // Maximum entries (0 = unlimited)
+    evictPolicy: "LRU"  // Eviction policy: "LRU" or "FIFO"
   }
 });
-
-await client.connect();
-
-await client.set('key', '1');
-await client.add('key', 2); // 3
-```
-
-### Disconnecting
-
-There are two functions that disconnect a client from the Redis server. In most scenarios you should use `.quit()` to ensure that pending commands are sent to Redis before closing a connection.
-
-#### `.QUIT()`/`.quit()`
-
-Gracefully close a client's connection to Redis, by sending the [`QUIT`](https://redis.io/commands/quit) command to the server. Before quitting, the client executes any remaining commands in its queue, and will receive replies from Redis for each of them.
-
-```typescript
-const [ping, get, quit] = await Promise.all([
-  client.ping(),
-  client.get('key'),
-  client.quit()
-]); // ['PONG', null, 'OK']
-
-try {
-  await client.get('key');
-} catch (err) {
-  // ClosedClient Error
-}
 ```
 
-#### `.disconnect()`
-
-Forcibly close a client's connection to Redis immediately. Calling `disconnect` will not send further pending commands to the Redis server, or wait for or parse outstanding responses.
-
-```typescript
-await client.disconnect();
-```
+See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage.
 
 ### Auto-Pipelining
 
 Node Redis will automatically pipeline requests that are made during the same "tick".
 
 ```typescript
-client.set('Tm9kZSBSZWRpcw==', 'users:1');
-client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==');
+client.set("Tm9kZSBSZWRpcw==", "users:1");
+client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw==");
 ```
 
-Of course, if you don't do something with your Promises you're certain to get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take advantage of auto-pipelining and handle your Promises, use `Promise.all()`.
+Of course, if you don't do something with your Promises you're certain to
+get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take
+advantage of auto-pipelining and handle your Promises, use `Promise.all()`.
 
 ```typescript
 await Promise.all([
-  client.set('Tm9kZSBSZWRpcw==', 'users:1'),
-  client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==')
+  client.set("Tm9kZSBSZWRpcw==", "users:1"),
+  client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="),
 ]);
 ```
 
+### Programmability
+
+See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md).
+
 ### Clustering
 
-Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster.
+Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster.
 
 ### Events
 
 The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes:
 
-| Name                    | When                                                                               | Listener arguments                                         |
-|-------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------|
-| `connect`               | Initiating a connection to the server                                              | *No arguments*                                             |
-| `ready`                 | Client is ready to use                                                             | *No arguments*                                             |
-| `end`                   | Connection has been closed (via `.quit()` or `.disconnect()`)                      | *No arguments*                                             |
-| `error`                 | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)`                                           |
-| `reconnecting`          | Client is trying to reconnect to the server                                        | *No arguments*                                             |
-| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event)                          | See  [here](./docs/pub-sub.md#sharded-channel-moved-event) |
+| Name                    | When                                                                               | Listener arguments                                        |
+| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- |
+| `connect`               | Initiating a connection to the server                                              | _No arguments_                                            |
+| `ready`                 | Client is ready to use                                                             | _No arguments_                                            |
+| `end`                   | Connection has been closed (via `.disconnect()`)                                   | _No arguments_                                            |
+| `error`                 | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)`                                          |
+| `reconnecting`          | Client is trying to reconnect to the server                                        | _No arguments_                                            |
+| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event)                          | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) |
 
-> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details.
+> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and
+> an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details.
 
-> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above.
+> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above.
 
 ## Supported Redis versions
 
 Node Redis is supported with the following versions of Redis:
 
 | Version | Supported          |
-|---------|--------------------|
-| 7.0.z   | :heavy_check_mark: |
-| 6.2.z   | :heavy_check_mark: |
-| 6.0.z   | :heavy_check_mark: |
-| 5.0.z   | :heavy_check_mark: |
-| < 5.0   | :x:                |
+| ------- | ------------------ |
+| 8.0.z   | :heavy_check_mark: |
+| 7.4.z   | :heavy_check_mark: |
+| 7.2.z   | :heavy_check_mark: |
+| < 7.2   | :x:                |
 
 > Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support.
 
+## Migration
+
+- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md)
+- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md)
+- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md)
+
 ## Contributing
 
-If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md).
+If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md).
 
 Thank you to all the people who already contributed to Node Redis!
 
@@ -381,4 +327,4 @@ Thank you to all the people who already contributed to Node Redis!
 
 ## License
 
-This repository is licensed under the "MIT" license. See [LICENSE](LICENSE).
+This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE).
diff --git a/benchmark/lib/index.js b/benchmark/lib/index.js
index 15c8a12f401..5576999bfbc 100644
--- a/benchmark/lib/index.js
+++ b/benchmark/lib/index.js
@@ -1,10 +1,10 @@
 import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
-import { promises as fs } from 'fs';
-import { fork } from 'child_process';
-import { URL, fileURLToPath } from 'url';
-import { once } from 'events';
-import { extname } from 'path';
+import { promises as fs } from 'node:fs';
+import { fork } from 'node:child_process';
+import { URL, fileURLToPath } from 'node:url';
+import { once } from 'node:events';
+import { extname } from 'node:path';
 
 async function getPathChoices() {
     const dirents = await fs.readdir(new URL('.', import.meta.url), {
diff --git a/benchmark/lib/ping/ioredis-auto-pipeline.js b/benchmark/lib/ping/ioredis-auto-pipeline.js
new file mode 100644
index 00000000000..ee400fe6ca9
--- /dev/null
+++ b/benchmark/lib/ping/ioredis-auto-pipeline.js
@@ -0,0 +1,20 @@
+import Redis from 'ioredis';
+
+export default async (host) => {
+    const client = new Redis({
+        host,
+        lazyConnect: true,
+        enableAutoPipelining: true
+    });
+
+    await client.connect();
+
+    return {
+        benchmark() {
+            return client.ping();
+        },
+        teardown() {
+            return client.disconnect();
+        }
+    }
+};
diff --git a/benchmark/lib/ping/local-resp2.js b/benchmark/lib/ping/local-resp2.js
new file mode 100644
index 00000000000..873698a131f
--- /dev/null
+++ b/benchmark/lib/ping/local-resp2.js
@@ -0,0 +1,21 @@
+import { createClient } from 'redis-local';
+
+export default async (host) => {
+  const client = createClient({
+    socket: {
+      host
+    },
+    RESP: 2
+  });
+
+  await client.connect();
+
+  return {
+    benchmark() {
+      return client.ping();
+    },
+    teardown() {
+      return client.disconnect();
+    }
+  };
+};
diff --git a/benchmark/lib/ping/local-resp3-buffer-proxy.js b/benchmark/lib/ping/local-resp3-buffer-proxy.js
new file mode 100644
index 00000000000..2ded38b21ca
--- /dev/null
+++ b/benchmark/lib/ping/local-resp3-buffer-proxy.js
@@ -0,0 +1,23 @@
+import { createClient, RESP_TYPES } from 'redis-local';
+
+export default async (host) => {
+  const client = createClient({
+    socket: {
+      host
+    },
+    RESP: 3
+  }).withTypeMapping({
+    [RESP_TYPES.SIMPLE_STRING]: Buffer
+  });
+
+  await client.connect();
+
+  return {
+    benchmark() {
+      return client.ping();
+    },
+    teardown() {
+      return client.disconnect();
+    }
+  };
+};
diff --git a/benchmark/lib/ping/local-resp3-buffer.js b/benchmark/lib/ping/local-resp3-buffer.js
new file mode 100644
index 00000000000..624a524ce06
--- /dev/null
+++ b/benchmark/lib/ping/local-resp3-buffer.js
@@ -0,0 +1,24 @@
+import { createClient, RESP_TYPES } from 'redis-local';
+
+export default async (host) => {
+  const client = createClient({
+    socket: {
+      host
+    },
+    commandOptions: {
+      [RESP_TYPES.SIMPLE_STRING]: Buffer
+    },
+    RESP: 3
+  });
+
+  await client.connect();
+
+  return {
+    benchmark() {
+      return client.ping();
+    },
+    teardown() {
+      return client.disconnect();
+    }
+  };
+};
diff --git a/benchmark/lib/ping/local-resp3-module-with-flags.js b/benchmark/lib/ping/local-resp3-module-with-flags.js
new file mode 100644
index 00000000000..e58856dcb9e
--- /dev/null
+++ b/benchmark/lib/ping/local-resp3-module-with-flags.js
@@ -0,0 +1,27 @@
+import { createClient } from 'redis-local';
+import PING from 'redis-local/dist/lib/commands/PING.js';
+
+export default async (host) => {
+  const client = createClient({
+    socket: {
+      host
+    },
+    RESP: 3,
+    modules: {
+      module: {
+        ping: PING.default
+      }
+    }
+  });
+
+  await client.connect();
+
+  return {
+    benchmark() {
+      return client.withTypeMapping({}).module.ping();
+    },
+    teardown() {
+      return client.disconnect();
+    }
+  };
+};
diff --git a/benchmark/lib/ping/local-resp3-module.js b/benchmark/lib/ping/local-resp3-module.js
new file mode 100644
index 00000000000..66f6e3ec291
--- /dev/null
+++ b/benchmark/lib/ping/local-resp3-module.js
@@ -0,0 +1,27 @@
+import { createClient } from 'redis-local';
+import PING from 'redis-local/dist/lib/commands/PING.js';
+
+export default async (host) => {
+  const client = createClient({
+    socket: {
+      host
+    },
+    RESP: 3,
+    modules: {
+      module: {
+        ping: PING.default
+      }
+    }
+  });
+
+  await client.connect();
+
+  return {
+    benchmark() {
+      return client.module.ping();
+    },
+    teardown() {
+      return client.disconnect();
+    }
+  };
+};
diff --git a/benchmark/lib/ping/local-resp3.js b/benchmark/lib/ping/local-resp3.js
new file mode 100644
index 00000000000..a4ee4f24a2a
--- /dev/null
+++ b/benchmark/lib/ping/local-resp3.js
@@ -0,0 +1,21 @@
+import { createClient } from 'redis-local';
+
+export default async (host) => {
+  const client = createClient({
+    socket: {
+      host
+    },
+    RESP: 3
+  });
+
+  await client.connect();
+
+  return {
+    benchmark() {
+      return client.ping();
+    },
+    teardown() {
+      return client.disconnect();
+    }
+  };
+};
diff --git a/benchmark/lib/ping/v3.js b/benchmark/lib/ping/v3.js
index 26f269a42cf..e7e62d3e15a 100644
--- a/benchmark/lib/ping/v3.js
+++ b/benchmark/lib/ping/v3.js
@@ -1,6 +1,6 @@
 import { createClient } from 'redis-v3';
-import { once } from 'events';
-import { promisify } from 'util';
+import { once } from 'node:events';
+import { promisify } from 'node:util';
 
 export default async (host) => {
     const client = createClient({ host }),
diff --git a/benchmark/lib/ping/v4.js b/benchmark/lib/ping/v4.js
index 69aa3c06929..c570aa1477f 100644
--- a/benchmark/lib/ping/v4.js
+++ b/benchmark/lib/ping/v4.js
@@ -1,4 +1,4 @@
-import { createClient } from '@redis/client';
+import { createClient } from 'redis-v4';
 
 export default async (host) => {
     const client = createClient({
diff --git a/benchmark/lib/runner.js b/benchmark/lib/runner.js
index a96ff55cab0..7d81d3bb8c7 100644
--- a/benchmark/lib/runner.js
+++ b/benchmark/lib/runner.js
@@ -1,7 +1,7 @@
 import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
-import { basename } from 'path';
-import { promises as fs } from 'fs';
+import { basename } from 'node:path';
+import { promises as fs } from 'node:fs';
 import * as hdr from 'hdr-histogram-js';
 hdr.initWebAssemblySync();
 
@@ -71,7 +71,7 @@ const benchmarkStart = process.hrtime.bigint(),
     histogram = await run(times),
     benchmarkNanoseconds = process.hrtime.bigint() - benchmarkStart,
     json = {
-        timestamp,
+        // timestamp,
         operationsPerSecond: times / Number(benchmarkNanoseconds) * 1_000_000_000,
         p0: histogram.getValueAtPercentile(0),
         p50: histogram.getValueAtPercentile(50),
diff --git a/benchmark/lib/set-get-delete-string/index.js b/benchmark/lib/set-get-delete-string/index.js
index 719edfc7fdf..506b222a6cb 100644
--- a/benchmark/lib/set-get-delete-string/index.js
+++ b/benchmark/lib/set-get-delete-string/index.js
@@ -1,6 +1,6 @@
 import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
-import { randomBytes } from 'crypto';
+import { randomBytes } from 'node:crypto';
 
 const { size } = yargs(hideBin(process.argv))
     .option('size', {
diff --git a/benchmark/lib/set-get-delete-string/v3.js b/benchmark/lib/set-get-delete-string/v3.js
index 27ff6702a51..1e2122a0e49 100644
--- a/benchmark/lib/set-get-delete-string/v3.js
+++ b/benchmark/lib/set-get-delete-string/v3.js
@@ -1,6 +1,6 @@
 import { createClient } from 'redis-v3';
-import { once } from 'events';
-import { promisify } from 'util';
+import { once } from 'node:events';
+import { promisify } from 'node:util';
 
 export default async (host, { randomString }) => {
     const client = createClient({ host }),
diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json
index 441c0b07b67..30114847134 100644
--- a/benchmark/package-lock.json
+++ b/benchmark/package-lock.json
@@ -6,11 +6,12 @@
     "": {
       "name": "@redis/client-benchmark",
       "dependencies": {
-        "@redis/client": "../packages/client",
         "hdr-histogram-js": "3.0.0",
-        "ioredis": "5.3.2",
-        "redis-v3": "npm:redis@3.1.2",
-        "yargs": "17.7.2"
+        "ioredis": "5",
+        "redis-local": "file:../packages/client",
+        "redis-v3": "npm:redis@3",
+        "redis-v4": "npm:redis@4",
+        "yargs": "17.7.1"
       }
     },
     "node_modules/@assemblyscript/loader": {
@@ -23,10 +24,18 @@
       "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
       "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
     },
+    "node_modules/@redis/bloom": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
+      "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
+      "peerDependencies": {
+        "@redis/client": "^1.0.0"
+      }
+    },
     "node_modules/@redis/client": {
       "version": "1.5.7",
-      "resolved": "file:../packages/client",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
+      "integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
       "dependencies": {
         "cluster-key-slot": "1.1.2",
         "generic-pool": "3.9.0",
@@ -36,6 +45,38 @@
         "node": ">=14"
       }
     },
+    "node_modules/@redis/graph": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
+      "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
+      "peerDependencies": {
+        "@redis/client": "^1.0.0"
+      }
+    },
+    "node_modules/@redis/json": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
+      "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
+      "peerDependencies": {
+        "@redis/client": "^1.0.0"
+      }
+    },
+    "node_modules/@redis/search": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
+      "integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
+      "peerDependencies": {
+        "@redis/client": "^1.0.0"
+      }
+    },
+    "node_modules/@redis/time-series": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
+      "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
+      "peerDependencies": {
+        "@redis/client": "^1.0.0"
+      }
+    },
     "node_modules/ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -244,6 +285,20 @@
         "node": ">=4"
       }
     },
+    "node_modules/redis-local": {
+      "name": "@redis/client",
+      "version": "1.5.6",
+      "resolved": "file:../packages/client",
+      "license": "MIT",
+      "dependencies": {
+        "cluster-key-slot": "1.1.2",
+        "generic-pool": "3.9.0",
+        "yallist": "4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/redis-parser": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
@@ -282,6 +337,20 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/redis-v4": {
+      "name": "redis",
+      "version": "4.6.6",
+      "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
+      "integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
+      "dependencies": {
+        "@redis/bloom": "1.2.0",
+        "@redis/client": "1.5.7",
+        "@redis/graph": "1.1.0",
+        "@redis/json": "1.0.4",
+        "@redis/search": "1.1.2",
+        "@redis/time-series": "1.0.4"
+      }
+    },
     "node_modules/require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -349,9 +418,9 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yargs": {
-      "version": "17.7.2",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
-      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "version": "17.7.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
+      "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
       "dependencies": {
         "cliui": "^8.0.1",
         "escalade": "^3.1.1",
diff --git a/benchmark/package.json b/benchmark/package.json
index f46f0e00b22..73acf9d0f1c 100644
--- a/benchmark/package.json
+++ b/benchmark/package.json
@@ -7,10 +7,11 @@
     "start": "node ."
   },
   "dependencies": {
-    "@redis/client": "../packages/client",
     "hdr-histogram-js": "3.0.0",
-    "ioredis": "5.3.2",
-    "redis-v3": "npm:redis@3.1.2",
-    "yargs": "17.7.2"
+    "ioredis": "5",
+    "redis-local": "file:../packages/client",
+    "redis-v3": "npm:redis@3",
+    "redis-v4": "npm:redis@4",
+    "yargs": "17.7.1"
   }
 }
diff --git a/docs/RESP.md b/docs/RESP.md
new file mode 100644
index 00000000000..f8c2388226b
--- /dev/null
+++ b/docs/RESP.md
@@ -0,0 +1,46 @@
+# Mapping RESP types
+
+RESP, which stands for **R**edis **SE**rialization **P**rotocol, is the protocol used by Redis to communicate with clients. This document shows how RESP types can be mapped to JavaScript types. You can learn more about RESP itself in the [offical documentation](https://redis.io/docs/reference/protocol-spec/).
+
+By default, each type is mapped to the first option in the lists below. To change this, configure a [`typeMapping`](.).
+
+## RESP2
+
+- Integer (`:`) => `number`
+- Simple String (`+`) => `string | Buffer`
+- Blob String (`$`) => `string | Buffer`
+- Simple Error (`-`) => `ErrorReply`
+- Array (`*`) => `Array`
+
+> NOTE: the first type is the default type
+
+## RESP3
+
+- Null (`_`) => `null`
+- Boolean (`#`) => `boolean`
+- Number (`:`) => `number | string`
+- Big Number (`(`) => `BigInt | string`
+- Double (`,`) => `number | string`
+- Simple String (`+`) => `string | Buffer`
+- Blob String (`$`) => `string | Buffer`
+- Verbatim String (`=`) => `string | Buffer | VerbatimString` 
+- Simple Error (`-`) => `ErrorReply`
+- Blob Error (`!`) => `ErrorReply`
+- Array (`*`) => `Array`
+- Set (`~`) => `Array | Set`
+- Map (`%`) => `object | Map | Array`
+- Push (`>`) => `Array` => PubSub push/`'push'` event
+
+> NOTE: the first type is the default type
+
+### Map keys and Set members
+
+When decoding a Map to `Map | object` or a Set to `Set`, keys and members of type "Simple String" or "Blob String" will be decoded as `string`s which enables lookups by value, ignoring type mapping. If you want them as `Buffer`s, decode them as `Array`s instead.
+
+### Not Implemented
+
+These parts of RESP3 are not yet implemented in Redis itself (at the time of writing), so are not yet implemented in the Node-Redis client either:
+
+- [Attribute type](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#attribute-type)
+- [Streamed strings](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-strings)
+- [Streamed aggregated data types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-aggregated-data-types)
diff --git a/docs/client-configuration.md b/docs/client-configuration.md
index 1854f07158a..57af626bf71 100644
--- a/docs/client-configuration.md
+++ b/docs/client-configuration.md
@@ -1,31 +1,34 @@
 # `createClient` configuration
 
-| Property                 | Default                                  | Description                                                                                                                                                                                                                                         |
-|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| url                      |                                          | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
-| socket                   |                                          | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported        |
-| socket.port              | `6379`                                   | Redis server port                                                                                                                                                                                                                                   |
-| socket.host              | `'localhost'`                            | Redis server hostname                                                                                                                                                                                                                               |
-| socket.family            | `0`                                      | IP Stack version (one of `4 \| 6 \| 0`)                                                                                                                                                                                                             |
-| socket.path              |                                          | Path to the UNIX Socket                                                                                                                                                                                                                             |
-| socket.connectTimeout    | `5000`                                   | Connection Timeout (in milliseconds)                                                                                                                                                                                                                |
-| socket.noDelay           | `true`                                   | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay)                                                                                                                                                         |
-| socket.keepAlive         | `5000`                                   | Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality                                                                                                                                    |
-| socket.tls               |                                          | See explanation and examples [below](#TLS)                                                                                                                                                                                                          |
-| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic                                                                                                                                                                           |
-| username                 |                                          | ACL username ([see ACL guide](https://redis.io/topics/acl))                                                                                                                                                                                         |
-| password                 |                                          | ACL password or the old "--requirepass" password                                                                                                                                                                                                    |
-| name                     |                                          | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))                                                                                                                                                                      |
-| database                 |                                          | Redis database number (see [`SELECT`](https://redis.io/commands/select) command)                                                                                                                                                                    |
-| modules                  |                                          | Included [Redis Modules](../README.md#packages)                                                                                                                                                                                                     |
-| scripts                  |                                          | Script definitions (see [Lua Scripts](../README.md#lua-scripts))                                                                                                                                                                                    |
-| functions                |                                          | Function definitions (see [Functions](../README.md#functions))                                                                                                                                                                                      |
-| commandsQueueMaxLength   |                                          | Maximum length of the client's internal command queue                                                                                                                                                                                               |
-| disableOfflineQueue      | `false`                                  | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down)                                                                                                                                                               |
-| readonly                 | `false`                                  | Connect in [`READONLY`](https://redis.io/commands/readonly) mode                                                                                                                                                                                    |
-| legacyMode               | `false`                                  | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md))                                                                                                                                                                    |
-| isolationPoolOptions     |                                          | See the [Isolated Execution Guide](./isolated-execution.md)                                                                                                                                                                                         |
-| pingInterval             |                                          | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout)                                                          |
+| Property                     | Default                                  | Description                                                                                                                                                                                                                                         |
+|------------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| url                          |                                          | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
+| socket                       |                                          | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported        |
+| socket.port                  | `6379`                                   | Redis server port                                                                                                                                                                                                                                   |
+| socket.host                  | `'localhost'`                            | Redis server hostname                                                                                                                                                                                                                               |
+| socket.family                | `0`                                      | IP Stack version (one of `4 \| 6 \| 0`)                                                                                                                                                                                                             |
+| socket.path                  |                                          | Path to the UNIX Socket                                                                                                                                                                                                                             |
+| socket.connectTimeout        | `5000`                                   | Connection timeout (in milliseconds)                                                                                                                                                                                                                |
+| socket.socketTimeout           |                                          | The maximum duration (in milliseconds) that the socket can remain idle (i.e., with no data sent or received) before being automatically closed |
+| socket.noDelay               | `true`                                   | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay)                                                                                                                                                         |
+| socket.keepAlive             | `true`                                   | Toggle [`keep-alive`](https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay) functionality                                                                                                                                          |
+| socket.keepAliveInitialDelay | `5000`                                   | If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket                                                                                                                                   |
+| socket.tls                   |                                          | See explanation and examples [below](#TLS)                                                                                                                                                                                                          |
+| socket.reconnectStrategy     | Exponential backoff with a maximum of 2000 ms; plus 0-200 ms random jitter.       | A function containing the [Reconnect Strategy](#reconnect-strategy) logic                                                                                                                                                                           |
+| username                     |                                          | ACL username ([see ACL guide](https://redis.io/topics/acl))                                                                                                                                                                                         |
+| password                     |                                          | ACL password or the old "--requirepass" password                                                                                                                                                                                                    |
+| name                         |                                          | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))                                                                                                                                                                      |
+| database                     |                                          | Redis database number (see [`SELECT`](https://redis.io/commands/select) command)                                                                                                                                                                    |
+| modules                      |                                          | Included [Redis Modules](../README.md#packages)                                                                                                                                                                                                     |
+| scripts                      |                                          | Script definitions (see [Lua Scripts](../README.md#lua-scripts))                                                                                                                                                                                    |
+| functions                    |                                          | Function definitions (see [Functions](../README.md#functions))                                                                                                                                                                                      |
+| commandsQueueMaxLength       |                                          | Maximum length of the client's internal command queue                                                                                                                                                                                               |
+| disableOfflineQueue          | `false`                                  | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down)                                                                                                                                                               |
+| readonly                     | `false`                                  | Connect in [`READONLY`](https://redis.io/commands/readonly) mode                                                                                                                                                                                    |
+| legacyMode                   | `false`                                  | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md))                                                                                                                                                                    |
+| isolationPoolOptions         |                                          | An object that configures a pool of isolated connections, If you frequently need isolated connections, consider using [createClientPool](https://github.com/redis/node-redis/blob/master/docs/pool.md#creating-a-pool) instead                                                                                                     |
+| pingInterval                 |                                          | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout)                                                          |
+| disableClientInfo            | `false`                                  | Disables `CLIENT SETINFO LIB-NAME node-redis` and `CLIENT SETINFO LIB-VER X.X.X` commands                                                                                                                                                           | 
 
 ## Reconnect Strategy
 
@@ -34,12 +37,24 @@ When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`),
 2. `number` -> wait for `X` milliseconds before reconnecting.
 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
 
-By default the strategy is `Math.min(retries * 50, 500)`, but it can be overwritten like so:
+By default the strategy uses exponential backoff, but it can be overwritten like so:
 
 ```javascript
 createClient({
   socket: {
-    reconnectStrategy: retries => Math.min(retries * 50, 1000)
+    reconnectStrategy: (retries, cause) => {
+        // By default, do not reconnect on socket timeout.
+        if (cause instanceof SocketTimeoutError) {
+          return false;
+        }
+
+        // Generate a random jitter between 0 – 200 ms:
+        const jitter = Math.floor(Math.random() * 200);
+        // Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:
+        const delay = Math.min(Math.pow(2, retries) * 50, 2000);
+
+        return delay + jitter;
+    }
   }
 });
 ```
@@ -73,3 +88,9 @@ createClient({
   }
 });
 ```
+## Connection Pooling
+
+In most cases, a single Redis connection is sufficient, as the node-redis client efficiently handles commands using an underlying socket. Unlike traditional databases, Redis does not require connection pooling for optimal performance.
+
+However, if your use case requires exclusive connections see [RedisClientPool](https://github.com/redis/node-redis/blob/master/docs/pool.md), which allows you to create and manage multiple dedicated connections.
+
diff --git a/docs/clustering.md b/docs/clustering.md
index 28ea0e2964c..6803583fa51 100644
--- a/docs/clustering.md
+++ b/docs/clustering.md
@@ -4,26 +4,22 @@
 
 Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
 
-```typescript
+```javascript
 import { createCluster } from 'redis';
 
-const cluster = createCluster({
-  rootNodes: [
-    {
+const cluster = await createCluster({
+    rootNodes: [{
       url: 'redis://10.0.0.1:30001'
-    },
-    {
+    }, {
       url: 'redis://10.0.0.2:30002'
-    }
-  ]
-});
-
-cluster.on('error', (err) => console.log('Redis Cluster Error', err));
-
-await cluster.connect();
+    }]
+  })
+  .on('error', err => console.log('Redis Cluster Error', err))
+  .connect();
 
 await cluster.set('key', 'value');
 const value = await cluster.get('key');
+await cluster.close();
 ```
 
 ## `createCluster` configuration
@@ -32,18 +28,20 @@ const value = await cluster.get('key');
 
 | Property               | Default | Description                                                                                                                                                                                                                                                                                                         |
 |------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| rootNodes              |         | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server |
+| rootNodes              |         | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the cluster configuration from the server |
 | defaults               |         | The default configuration values for every client in the cluster.  Use this for example when specifying an ACL user to connect with                                                                                                                                                                                 |
 | useReplicas            | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes                                                                                                                                                        |
 | minimizeConnections    | `false` | When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. Useful for short-term or Pub/Sub-only connections.                                                                                                                                                 |
 | maxCommandRedirections | `16`    | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors                                                                                                                                                                                                                             |
 | nodeAddressMap         |         | Defines the [node address mapping](#node-address-map)                                                                                                                                                                                                                                                               |
 | modules                |         | Included [Redis Modules](../README.md#packages)                                                                                                                                                                                                                                                                     |
-| scripts                |         | Script definitions (see [Lua Scripts](../README.md#lua-scripts))                                                                                                                                                                                                                                                    |
-| functions              |         | Function definitions (see [Functions](../README.md#functions))                                                                                                                                                                                                                                                      |
+| scripts                |         | Script definitions (see [Lua Scripts](./programmability.md#lua-scripts))                                                                                                                                                                                                                                                    |
+| functions              |         | Function definitions (see [Functions](./programmability.md#functions))                                                                                                                                                                                                                                                      |
+
 ## Auth with password and username
 
 Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option.
+
 ```javascript
 createCluster({
   rootNodes: [{
@@ -107,7 +105,7 @@ createCluster({
 
 ### Commands that operate on Redis Keys
 
-Commands such as `GET`, `SET`, etc. are routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`.
+Commands such as `GET`, `SET`, etc. are routed by the first key specified. For example `MGET 1 2 3` will be routed by the key `1`.
 
 ### [Server Commands](https://redis.io/commands#server)
 
@@ -115,4 +113,4 @@ Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the
 
 ### "Forwarded Commands"
 
-Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. This client sends these commands to a random node in order to spread the load across the cluster.
+Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client sends these commands to a random node in order to spread the load across the cluster.
diff --git a/docs/command-options.md b/docs/command-options.md
new file mode 100644
index 00000000000..8583eae135b
--- /dev/null
+++ b/docs/command-options.md
@@ -0,0 +1,81 @@
+# Command Options
+
+> :warning: The command options API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#command-options).
+
+Command Options are used to create "proxy clients" that change the behavior of executed commands. See the sections below for details.
+
+## Type Mapping
+
+Some [RESP types](./RESP.md) can be mapped to more than one JavaScript type. For example, "Blob String" can be mapped to `string` or `Buffer`. You can override the default type mapping using the `withTypeMapping` function:
+
+```javascript
+await client.get('key'); // `string | null`
+
+const proxyClient = client.withTypeMapping({
+  [TYPES.BLOB_STRING]: Buffer
+});
+
+await proxyClient.get('key'); // `Buffer | null`
+```
+
+See [RESP](./RESP.md) for a full list of types.
+
+## Abort Signal
+
+The client [batches commands](./FAQ.md#how-are-commands-batched) before sending them to Redis. Commands that haven't been written to the socket yet can be aborted using the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) API:
+
+```javascript
+const controller = new AbortController(),
+  client = client.withAbortSignal(controller.signal);
+
+try {
+  const promise = client.get('key');
+  controller.abort();
+  await promise;
+} catch (err) {
+  // AbortError
+}
+```
+
+
+## Timeout
+
+This option is similar to the Abort Signal one, but provides an easier way to set timeout for commands. Again, this applies to commands that haven't been written to the socket yet.
+
+```javascript
+const client = createClient({
+  commandOptions: {
+    timeout: 1000
+  }
+})
+```
+
+## ASAP
+
+Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue.
+
+```javascript
+const asapClient = client.asap();
+await asapClient.ping();
+```
+
+## `withCommandOptions`
+
+You can set all of the above command options in a single call with the `withCommandOptions` function:
+
+```javascript
+client.withCommandOptions({
+  typeMapping: ...,
+  abortSignal: ...,
+  asap: ...
+});
+```
+
+If any of the above options are omitted, the default value will be used. For example, the following client would **not** be in ASAP mode:
+
+```javascript
+client.asap().withCommandOptions({
+  typeMapping: ...,
+  abortSignal: ...
+});
+```
diff --git a/docs/isolated-execution.md b/docs/isolated-execution.md
deleted file mode 100644
index 7870a4680e7..00000000000
--- a/docs/isolated-execution.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# Isolated Execution
-
-Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
-
-- You're using [transactions]() and need to `WATCH` a key or keys for changes.
-- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
-- You're using the `MONITOR` command which also takes over a connection.
-
-Below are several examples of how to use isolated execution.
-
-> NOTE: Behind the scenes we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
-
-## The Simple Scenario
-
-This just isolates execution on a single connection. Do what you want with that connection:
-
-```typescript
-await client.executeIsolated(async isolatedClient => {
-    await isolatedClient.set('key', 'value');
-    await isolatedClient.get('key');
-});
-```
-
-## Transactions
-
-Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
-
-```typescript
-try {
-    await client.executeIsolated(async isolatedClient => {
-        await isolatedClient.watch('key');
-
-        const multi = isolatedClient.multi()
-            .ping()
-            .get('key');
-
-        if (Math.random() > 0.5) {
-            await isolatedClient.watch('another-key');
-            multi.set('another-key', await isolatedClient.get('another-key') / 2);
-        }
-
-        return multi.exec();
-    });
-} catch (err) {
-    if (err instanceof WatchError) {
-        // the transaction aborted
-    }
-}
-
-```
-
-## Blocking Commands
-
-For blocking commands, you can execute a tidy little one-liner:
-
-```typescript
-await client.executeIsolated(isolatedClient => isolatedClient.blPop('key'));
-```
-
-Or, you can just run the command directly, and provide the `isolated` option:
-
-```typescript
-await client.blPop(
-    commandOptions({ isolated: true }),
-    'key'
-);
-```
diff --git a/docs/pool.md b/docs/pool.md
new file mode 100644
index 00000000000..7121e601d73
--- /dev/null
+++ b/docs/pool.md
@@ -0,0 +1,74 @@
+# `RedisClientPool`
+
+Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
+
+- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
+- You're using [transactions](https://redis.io/docs/interact/transactions/) and need to `WATCH` a key or keys for changes.
+- Some more...
+
+For those use cases you'll need to create a connection pool.
+
+## Creating a pool
+
+You can create a pool using the `createClientPool` function:
+
+```javascript
+import { createClientPool } from 'redis';
+
+const pool = await createClientPool()
+  .on('error', err => console.error('Redis Client Pool Error', err));
+```
+
+the function accepts two arguments, the client configuration (see [here](./client-configuration.md) for more details), and the pool configuration:
+
+| Property       | Default | Description                                                                                                                    |
+|----------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
+| minimum        | 1       | The minimum clients the pool should hold to. The pool won't close clients if the pool size is less than the minimum.           |
+| maximum        | 100     | The maximum clients the pool will have at once. The pool won't create any more resources and queue requests in memory.         |
+| acquireTimeout | 3000    | The maximum time (in ms) a task can wait in the queue. The pool will reject the task with `TimeoutError` in case of a timeout. |
+| cleanupDelay   | 3000    | The time to wait before cleaning up unused clients.                                                                            |
+
+You can also create a pool from a client (reusing it's configuration):
+```javascript
+const pool = await client.createPool()
+  .on('error', err => console.error('Redis Client Pool Error', err));
+```
+
+## The Simple Scenario
+
+All the client APIs are exposed on the pool instance directly, and will execute the commands using one of the available clients.
+
+```javascript
+await pool.sendCommand(['PING']); // 'PONG'
+await client.ping(); // 'PONG'
+await client.withTypeMapping({
+  [RESP_TYPES.SIMPLE_STRING]: Buffer
+}).ping(); // Buffer
+```
+
+## Transactions
+
+Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
+
+```javascript
+try {
+  await pool.execute(async client => {
+    await client.watch('key');
+
+    const multi = client.multi()
+      .ping()
+      .get('key');
+
+    if (Math.random() > 0.5) {
+      await client.watch('another-key');
+      multi.set('another-key', await client.get('another-key') / 2);
+    }
+
+    return multi.exec();
+  });
+} catch (err) {
+  if (err instanceof WatchError) {
+    // the transaction aborted
+  }
+}
+```
diff --git a/docs/programmability.md b/docs/programmability.md
new file mode 100644
index 00000000000..c5917c2387d
--- /dev/null
+++ b/docs/programmability.md
@@ -0,0 +1,85 @@
+# [Programmability](https://redis.io/docs/manual/programmability/)
+
+Redis provides a programming interface allowing code execution on the redis server.
+
+## [Functions](https://redis.io/docs/manual/programmability/functions-intro/)
+
+The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key _foo_ has the value _17_ and we run `add('foo', 25)`, it returns the answer to Life, the Universe and Everything.
+
+```lua
+#!lua name=library
+
+redis.register_function {
+  function_name = 'add',
+  callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end,
+  flags = { 'no-writes' }
+}
+```
+
+Here is the same example, but in a format that can be pasted into the `redis-cli`.
+
+```
+FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={'no-writes'}}"
+```
+
+Load the prior redis function on the _redis server_ before running the example below.
+
+```typescript
+import { CommandParser, createClient, RedisArgument } from '@redis/client';
+import { NumberReply } from '@redis/client/dist/lib/RESP/types.js';
+
+const client = createClient({
+  functions: {
+    library: {
+      add: {
+        NUMBER_OF_KEYS: 1,
+        parseCommand(
+          parser: CommandParser,
+          key: RedisArgument,
+          toAdd: RedisArgument
+        ) {
+          parser.pushKey(key)
+          parser.push(toAdd)
+        },
+        transformReply: undefined as unknown as () => NumberReply
+      }
+    }
+  }
+});
+
+await client.connect();
+await client.set('key', '1');
+await client.library.add('key', '2'); // 3
+```
+
+## [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/)
+
+The following is an end-to-end example of the prior concept.
+
+```typescript
+import { CommandParser, createClient, defineScript, RedisArgument } from '@redis/client';
+import { NumberReply } from '@redis/client/dist/lib/RESP/types.js';
+
+const client = createClient({
+  scripts: {
+    add: defineScript({
+      SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];',
+      NUMBER_OF_KEYS: 1,
+      FIRST_KEY_INDEX: 1,
+      parseCommand(
+        parser: CommandParser,
+        key: RedisArgument,
+        toAdd: RedisArgument
+      ) {
+        parser.pushKey(key)
+        parser.push(toAdd)
+      },
+      transformReply: undefined as unknown as () => NumberReply
+    })
+  }
+});
+
+await client.connect();
+await client.set('key', '1');
+await client.add('key', '2'); // 3
+```
diff --git a/docs/pub-sub.md b/docs/pub-sub.md
index b319925569d..7bbb0733c18 100644
--- a/docs/pub-sub.md
+++ b/docs/pub-sub.md
@@ -1,18 +1,20 @@
 # Pub/Sub
 
-The Pub/Sub API is implemented by `RedisClient` and `RedisCluster`.
+The Pub/Sub API is implemented by `RedisClient`, `RedisCluster`, and `RedisSentinel`.
 
 ## Pub/Sub with `RedisClient`
 
-Pub/Sub requires a dedicated stand-alone client. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
+### RESP2
 
-```typescript
+Using RESP2, Pub/Sub "takes over" the connection (a client with subscriptions will not execute commands), therefore it requires a dedicated connection. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
+
+```javascript
 const subscriber = client.duplicate();
 subscriber.on('error', err => console.error(err));
 await subscriber.connect();
 ```
 
-When working with a `RedisCluster`, this is handled automatically for you.
+> When working with either `RedisCluster` or `RedisSentinel`, this is handled automatically for you.
 
 ### `sharded-channel-moved` event
 
@@ -29,6 +31,8 @@ The event listener signature is as follows:
 )
 ```
 
+> When working with `RedisCluster`, this is handled automatically for you.
+
 ## Subscribing
 
 ```javascript
@@ -39,7 +43,7 @@ await client.pSubscribe('channe*', listener);
 await client.sSubscribe('channel', listener);
 ```
 
-> ⚠️ Subscribing to the same channel more than once will create multiple listeners which will each be called when a message is recieved.
+> ⚠️ Subscribing to the same channel more than once will create multiple listeners, each of which will be called when a message is received.
 
 ## Publishing
 
diff --git a/docs/scan-iterators.md b/docs/scan-iterators.md
new file mode 100644
index 00000000000..47c4d6c0567
--- /dev/null
+++ b/docs/scan-iterators.md
@@ -0,0 +1,30 @@
+# Scan Iterators
+
+> :warning: The scan iterators API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#scan-iterators).
+
+[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
+
+```javascript
+for await (const keys of client.scanIterator()) {
+  const values = await client.mGet(keys);
+}
+```
+
+This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
+
+```javascript
+for await (const entries of client.hScanIterator('hash')) {}
+for await (const members of client.sScanIterator('set')) {}
+for await (const membersWithScores of client.zScanIterator('sorted-set')) {}
+```
+
+You can override the default options by providing a configuration object:
+
+```javascript
+client.scanIterator({
+  cursor: '0', // optional, defaults to '0'
+  TYPE: 'string', // `SCAN` only
+  MATCH: 'patter*',
+  COUNT: 100
+});
+```
diff --git a/docs/sentinel.md b/docs/sentinel.md
new file mode 100644
index 00000000000..f10b2953df5
--- /dev/null
+++ b/docs/sentinel.md
@@ -0,0 +1,103 @@
+# Redis Sentinel
+
+The [Redis Sentinel](https://redis.io/docs/management/sentinel/) object of node-redis provides a high level object that provides access to a high availability redis installation managed by Redis Sentinel to provide enumeration of master and replica nodes belonging to an installation as well as reconfigure itself on demand for failover and topology changes.
+
+## Basic Example
+
+```javascript
+import { createSentinel } from 'redis';
+
+const sentinel = await createSentinel({
+    name: 'sentinel-db',
+    sentinelRootNodes: [{
+      host: 'example',
+      port: 1234
+    }]
+  })
+  .on('error', err => console.error('Redis Sentinel Error', err))
+  .connect();
+
+await sentinel.set('key', 'value');
+const value = await sentinel.get('key');
+await sentinel.close();
+```
+
+In the above example, we configure the sentinel object to fetch the configuration for the database Redis Sentinel is monitoring as "sentinel-db" with one of the sentinels being located at `example:1234`, then using it like a regular Redis client.
+
+## `createSentinel` configuration
+
+| Property                   | Default   | Description                                                                                                                                                                                                                                                                                                           |
+|----------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| name                       |           | The sentinel identifier for a particular database cluster                                                                                                                                                                                                                                                             |
+| sentinelRootNodes          |           | An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server |
+| maxCommandRediscovers      | `16`      | The maximum number of times a command will retry due to topology changes.                                                                                                                                                                                                                                             |
+| nodeClientOptions          |           | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with                                                                                                                                                                                              |
+| sentinelClientOptions      |           | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with                                                                                                                                                                                          |
+| masterPoolSize             | `1`       | The number of clients connected to the master node                                                                                                                                                                                                                                                                    |
+| replicaPoolSize            | `0`       | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes.                                                                                                       |
+| scanInterval               | `10000`   | Interval in milliseconds to periodically scan for changes in the sentinel topology. The client will query the sentinel for changes at this interval.                                                                                                                                                                 |
+| passthroughClientErrorEvents | `false` | When `true`, error events from client instances inside the sentinel will be propagated to the sentinel instance. This allows handling all client errors through a single error handler on the sentinel instance.                                                                                                     |
+| reserveClient              | `false`   | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool.                                                                                                                                                           |
+
+## PubSub
+
+It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down.
+
+```javascript
+await sentinel.subscribe('channel', message => {
+  // ...
+});
+await sentinel.unsubscribe('channel');
+```
+
+see [the PubSub guide](./pub-sub.md) for more details.
+
+## Sentinel as a pool
+
+The sentinel object provides the ability to manage a pool of clients for the master node:
+
+```javascript
+createSentinel({
+  // ...
+  masterPoolSize: 10
+});
+```
+
+In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them:
+
+```javascript
+createSentinel({
+  // ...
+  replicaPoolSize: 10
+});
+```
+
+## Master client lease
+
+Sometimes multiple commands needs to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`).
+
+There are 2 ways to get a client lease:
+
+`.use()`
+```javascript
+const result = await sentinel.use(async client => {
+  await client.watch('key');
+  return client.multi()
+    .get('key')
+    .exec();
+});
+```
+
+`.acquire()`
+```javascript
+const clientLease = await sentinel.acquire();
+
+try {
+  await clientLease.watch('key');
+  const resp = await clientLease.multi()
+    .get('key')
+    .exec();
+} finally {
+  clientLease.release();
+}
+```
diff --git a/docs/todo.md b/docs/todo.md
new file mode 100644
index 00000000000..49163444986
--- /dev/null
+++ b/docs/todo.md
@@ -0,0 +1,6 @@
+- "Isolation Pool" -> pool
+- Cluster request response policies (either implement, or block "server" commands in cluster)
+
+Docs:
+- [Command Options](./command-options.md)
+- [RESP](./RESP.md)
diff --git a/docs/transactions.md b/docs/transactions.md
new file mode 100644
index 00000000000..6331fef4be5
--- /dev/null
+++ b/docs/transactions.md
@@ -0,0 +1,53 @@
+# [Transactions](https://redis.io/docs/interact/transactions/) ([`MULTI`](https://redis.io/commands/multi/)/[`EXEC`](https://redis.io/commands/exec/))
+
+Start a [transaction](https://redis.io/docs/interact/transactions/) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results:
+
+```javascript
+const [setReply, getReply] = await client.multi()
+  .set('key', 'value')
+  .get('another-key')
+  .exec();
+```
+
+## `exec<'typed'>()`/`execTyped()`
+
+A transaction invoked with `.exec<'typed'>`/`execTyped()` will return types appropriate to the commands in the transaction:
+
+```javascript
+const multi = client.multi().ping();
+await multi.exec(); // Array<ReplyUnion>
+await multi.exec<'typed'>(); // [string]
+await multi.execTyped(); // [string]
+```
+
+> :warning: this only works when all the commands are invoked in a single "call chain"
+
+## [`WATCH`](https://redis.io/commands/watch/)
+
+You can also [watch](https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change or if the client reconnected between the `watch` and `exec` calls.
+
+The `WATCH` state is stored on the connection (by the server). In case you need to run multiple `WATCH` & `MULTI` in parallel you'll need to use a [pool](./pool.md).
+
+## `execAsPipeline`
+
+`execAsPipeline` will execute the commands without "wrapping" it with `MULTI` & `EXEC` (and lose the transactional semantics).
+
+```javascript
+await client.multi()
+  .get('a')
+  .get('b')
+  .execAsPipeline();
+```
+
+the diffrence between the above pipeline and `Promise.all`:
+
+```javascript
+await Promise.all([
+  client.get('a'),
+  client.get('b')
+]);
+```
+
+is that if the socket disconnects during the pipeline, any unwritten commands will be discarded. i.e. if the socket disconnects after `GET a` is written to the socket, but before `GET b` is:
+- using `Promise.all` - the client will try to execute `GET b` when the socket reconnects
+- using `execAsPipeline` - `GET b` promise will be rejected as well
diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md
new file mode 100644
index 00000000000..fbe02e7c4d6
--- /dev/null
+++ b/docs/v4-to-v5.md
@@ -0,0 +1,245 @@
+# v4 to v5 migration guide
+
+## Client Configuration
+
+### Keep Alive
+
+To better align with Node.js build-in [`net`](https://nodejs.org/api/net.html) and [`tls`](https://nodejs.org/api/tls.html) modules, the `keepAlive` option has been split into 2 options: `keepAlive` (`boolean`) and `keepAliveInitialDelay` (`number`). The defaults remain `true` and `5000`.
+
+### Legacy Mode
+
+In the previous version, you could access "legacy" mode by creating a client and passing in `{ legacyMode: true }`. Now, you can create one off of an existing client by calling the `.legacy()` function. This allows easier access to both APIs and enables better TypeScript support.
+
+```javascript
+// use `client` for the current API
+const client = createClient();
+await client.set('key', 'value');
+
+// use `legacyClient` for the "legacy" API
+const legacyClient = client.legacy();
+legacyClient.set('key', 'value', (err, reply) => {
+  // ...
+});
+```
+
+## Command Options
+
+In v4, command options are passed as a first optional argument:
+
+```javascript
+await client.get('key'); // `string | null`
+await client.get(client.commandOptions({ returnBuffers: true }), 'key'); // `Buffer | null`
+```
+
+This has a couple of flaws:
+1. The argument types are checked in runtime, which is a performance hit.
+2. Code suggestions are less readable/usable, due to "function overloading".
+3. Overall, "user code" is not as readable as it could be.
+
+### The new API for v5
+
+With the new API, instead of passing the options directly to the commands we use a "proxy client" to store them:
+
+```javascript
+await client.get('key'); // `string | null`
+
+const proxyClient = client.withCommandOptions({
+  typeMapping: {
+    [TYPES.BLOB_STRING]: Buffer
+  }
+});
+
+await proxyClient.get('key'); // `Buffer | null`
+```
+
+for more information, see the [Command Options guide](./command-options.md).
+
+## Quit VS Disconnect
+
+The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead of sending a `QUIT` command to the server, the client can simply close the network connection.
+
+`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to `client.destroy()`.
+
+## Scan Iterators
+
+Iterator commands like `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN` return collections of elements (depending on the data type). However, v4 iterators loop over these collections and yield individual items:
+
+```javascript
+for await (const key of client.scanIterator()) {
+  console.log(key, await client.get(key));
+}
+```
+
+This mismatch can be awkward and makes "multi-key" commands like `MGET`, `UNLINK`, etc. pointless. So, in v5 the iterators now yield a collection instead of an element:
+
+```javascript
+for await (const keys of client.scanIterator()) {
+  // we can now meaningfully utilize "multi-key" commands
+  console.log(keys, await client.mGet(keys));
+}
+```
+
+for more information, see the [Scan Iterators guide](./scan-iterators.md).
+
+## Isolation Pool
+
+In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main" connection. However, there was no way to use the pool without a "main" connection:
+```javascript
+const client = await createClient()
+  .on('error', err => console.error(err))
+  .connect();
+
+await client.ping(
+  client.commandOptions({ isolated: true })
+);
+```
+
+In v5 we've extracted this pool logic into its own class—`RedisClientPool`:
+
+```javascript
+const pool = await createClientPool()
+  .on('error', err => console.error(err))
+  .connect();
+
+await pool.ping();
+```
+
+See the [pool guide](./pool.md) for more information.
+
+## Cluster `MULTI`
+
+In v4, `cluster.multi()` did not support executing commands on replicas, even if they were readonly.
+
+```javascript
+// this might execute on a replica, depending on configuration
+await cluster.sendCommand('key', true, ['GET', 'key']);
+
+// this always executes on a master
+await cluster.multi()
+  .addCommand('key', ['GET', 'key'])
+  .exec();
+```
+
+To support executing commands on replicas, `cluster.multi().addCommand` now requires `isReadonly` as the second argument, which matches the signature of `cluster.sendCommand`:
+
+```javascript
+await cluster.multi()
+  .addCommand('key', true, ['GET', 'key'])
+  .exec();
+```
+
+## `MULTI.execAsPipeline()`
+
+```javascript
+await client.multi()
+  .set('a', 'a')
+  .set('b', 'b')
+  .execAsPipeline();
+```
+
+In older versions, if the socket disconnects during the pipeline execution, i.e. after writing `SET a a` and before `SET b b`, the returned promise is rejected, but `SET b b` will still be executed on the server.
+
+In v5, any unwritten commands (in the same pipeline) will be discarded.
+
+- `RedisFlushModes` ->  `REDIS_FLUSH_MODES` [^enum-to-constants]
+
+## Commands
+
+### Redis
+
+- `ACL GETUSER`: `selectors`
+- `COPY`: `destinationDb` -> `DB`, `replace` -> `REPLACE`, `boolean` -> `number` [^boolean-to-number]
+- `CLIENT KILL`: `enum ClientKillFilters` -> `const CLIENT_KILL_FILTERS` [^enum-to-constants]
+- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants]
+- `CLIENT TRACKINGINFO`: `flags` in RESP2 - `Set<string>` -> `Array<string>` (to match RESP3 default type mapping)
+- `CLUSTER INFO`:
+- `CLUSTER SETSLOT`: `ClusterSlotStates` -> `CLUSTER_SLOT_STATES` [^enum-to-constants]
+- `CLUSTER RESET`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
+- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants], the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
+- `CLUSTER LINKS`: `createTime` -> `create-time`, `sendBufferAllocated` -> `send-buffer-allocated`, `sendBufferUsed` -> `send-buffer-used` [^map-keys]
+- `CLUSTER NODES`, `CLUSTER REPLICAS`, `CLUSTER INFO`: returning the raw `VerbatimStringReply`
+- `EXPIRE`: `boolean` -> `number` [^boolean-to-number]
+- `EXPIREAT`: `boolean` -> `number` [^boolean-to-number]
+- `HSCAN`: `tuples` has been renamed to `entries`
+- `HEXISTS`: `boolean` -> `number` [^boolean-to-number]
+- `HRANDFIELD_COUNT_WITHVALUES`: `Record<BlobString, BlobString>` -> `Array<{ field: BlobString; value: BlobString; }>` (it can return duplicates).
+- `HSETNX`: `boolean` -> `number` [^boolean-to-number]
+- `INFO`:
+- `LCS IDX`: `length` has been changed to `len`, `matches` has been changed from `Array<{ key1: RangeReply; key2: RangeReply; }>` to `Array<[key1: RangeReply, key2: RangeReply]>`
+
+
+- `ZINTER`: instead of `client.ZINTER('key', { WEIGHTS: [1] })` use `client.ZINTER({ key: 'key', weight: 1 }])`
+- `ZINTER_WITHSCORES`: instead of `client.ZINTER_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZINTER_WITHSCORES({ key: 'key', weight: 1 }])`
+- `ZUNION`: instead of `client.ZUNION('key', { WEIGHTS: [1] })` use `client.ZUNION({ key: 'key', weight: 1 }])`
+- `ZUNION_WITHSCORES`: instead of `client.ZUNION_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZUNION_WITHSCORES({ key: 'key', weight: 1 }])`
+- `ZMPOP`: `{ elements: Array<{ member: string; score: number; }>; }` -> `{ members: Array<{ value: string; score: number; }>; }` to match other sorted set commands (e.g. `ZRANGE`, `ZSCAN`)
+
+- `MOVE`: `boolean` -> `number` [^boolean-to-number]
+- `PEXPIRE`: `boolean` -> `number` [^boolean-to-number]
+- `PEXPIREAT`: `boolean` -> `number` [^boolean-to-number]
+- `PFADD`: `boolean` -> `number` [^boolean-to-number]
+
+- `RENAMENX`: `boolean` -> `number` [^boolean-to-number]
+- `SETNX`: `boolean` -> `number` [^boolean-to-number]
+- `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN`: `reply.cursor` will not be converted to number to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561).
+- `SCRIPT EXISTS`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
+- `SISMEMBER`: `boolean` -> `number` [^boolean-to-number]
+- `SMISMEMBER`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
+- `SMOVE`: `boolean` -> `number` [^boolean-to-number]
+
+- `GEOSEARCH_WITH`/`GEORADIUS_WITH`: `GeoReplyWith` -> `GEO_REPLY_WITH` [^enum-to-constants]
+- `GEORADIUSSTORE` -> `GEORADIUS_STORE`
+- `GEORADIUSBYMEMBERSTORE` -> `GEORADIUSBYMEMBER_STORE`
+- `XACK`: `boolean` -> `number` [^boolean-to-number]
+- `XADD`: the `INCR` option has been removed, use `XADD_INCR` instead
+- `LASTSAVE`: `Date` -> `number` (unix timestamp)
+- `HELLO`: `protover` moved from the options object to it's own argument, `auth` -> `AUTH`, `clientName` -> `SETNAME`
+- `MODULE LIST`: `version` -> `ver` [^map-keys]
+- `MEMORY STATS`: [^map-keys]
+- `FUNCTION RESTORE`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
+- `FUNCTION STATS`: `runningScript` -> `running_script`, `durationMs` -> `duration_ms`, `librariesCount` -> `libraries_count`, `functionsCount` -> `functions_count` [^map-keys]
+
+- `TIME`: `Date` -> `[unixTimestamp: string, microseconds: string]`
+
+- `XGROUP_CREATECONSUMER`: [^boolean-to-number]
+- `XGROUP_DESTROY`: [^boolean-to-number]
+- `XINFO GROUPS`: `lastDeliveredId` -> `last-delivered-id` [^map-keys]
+- `XINFO STREAM`: `radixTreeKeys` -> `radix-tree-keys`, `radixTreeNodes` -> `radix-tree-nodes`, `lastGeneratedId` -> `last-generated-id`, `maxDeletedEntryId` -> `max-deleted-entry-id`, `entriesAdded` -> `entries-added`, `recordedFirstEntryId` -> `recorded-first-entry-id`, `firstEntry` -> `first-entry`, `lastEntry` -> `last-entry`
+- `XAUTOCLAIM`, `XCLAIM`, `XRANGE`, `XREVRANGE`: `Array<{ name: string; messages: Array<{ id: string; message: Record<string, string> }>; }>` -> `Record<string, Array<{ id: string; message: Record<string, string> }>>`
+
+- `COMMAND LIST`: `enum FilterBy` -> `const COMMAND_LIST_FILTER_BY` [^enum-to-constants], the filter argument has been moved from a "top level argument" into ` { FILTERBY: { type: <MODULE|ACLCAT|PATTERN>; value: <value> } }`
+
+### Bloom
+
+- `TOPK.QUERY`: `Array<number>` -> `Array<boolean>`
+
+### JSON
+
+- `JSON.ARRINDEX`: `start` and `end` arguments moved to `{ range: { start: number; end: number; }; }` [^future-proofing]
+- `JSON.ARRPOP`: `path` and `index` arguments moved to `{ path: string; index: number; }` [^future-proofing]
+- `JSON.ARRLEN`, `JSON.CLEAR`, `JSON.DEBUG MEMORY`, `JSON.DEL`, `JSON.FORGET`, `JSON.OBJKEYS`, `JSON.OBJLEN`, `JSON.STRAPPEND`, `JSON.STRLEN`, `JSON.TYPE`: `path` argument moved to `{ path: string; }` [^future-proofing]
+
+### Search
+
+- `FT.SUGDEL`: [^boolean-to-number]
+- `FT.CURSOR READ`: `cursor` type changed from `number` to `string` (in and out) to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561).
+- `AggregateGroupByReducers` -> `FT_AGGREGATE_GROUP_BY_REDUCERS` [^enum-to-constants]
+- `AggregateSteps` -> `FT_AGGREGATE_STEPS` [^enum-to-constants]
+- `RedisSearchLanguages` -> `REDISEARCH_LANGUAGE` [^enum-to-constants]
+- `SchemaFieldTypes` -> `SCHEMA_FIELD_TYPE` [^enum-to-constants]
+- `SchemaTextFieldPhonetics` -> `SCHEMA_TEXT_FIELD_PHONETIC` [^enum-to-constants]
+- `SearchOptions` -> `FtSearchOptions`
+- `VectorAlgorithms` -> `SCHEMA_VECTOR_FIELD_ALGORITHM` [^enum-to-constants]
+
+### Time Series
+
+- `TS.ADD`: `boolean` -> `number` [^boolean-to-number]
+- `TS.[M][REV]RANGE`: the `ALIGN` argument has been moved into `AGGREGATION`
+- `TS.SYNUPDATE`: `Array<string | Array<string>>` -> `Record<string, Array<string>>`
+- `TimeSeriesDuplicatePolicies` -> `TIME_SERIES_DUPLICATE_POLICIES` [^enum-to-constants]
+- `TimeSeriesEncoding` -> `TIME_SERIES_ENCODING` [^enum-to-constants]
+- `TimeSeriesAggregationType` -> `TIME_SERIES_AGGREGATION_TYPE` [^enum-to-constants]
+- `TimeSeriesReducers` -> `TIME_SERIES_REDUCERS` [^enum-to-constants]
+- `TimeSeriesBucketTimestamp` -> `TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants]
+
+[^map-keys]: To avoid unnecessary transformations and confusion, map keys will not be transformed to "js friendly" names (i.e. `number-of-keys` will not be renamed to `numberOfKeys`). See [here](https://github.com/redis/node-redis/discussions/2506).
diff --git a/docs/v5.md b/docs/v5.md
new file mode 100644
index 00000000000..15ef67c14ee
--- /dev/null
+++ b/docs/v5.md
@@ -0,0 +1,188 @@
+# RESP3 Support
+
+Node Redis v5 adds support for [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md), the new Redis serialization protocol. RESP3 offers richer data types and improved type handling compared to RESP2.
+
+To use RESP3, specify it when creating your client:
+
+```javascript
+import { createClient } from 'redis';
+
+const client = createClient({
+  RESP: 3
+});
+```
+
+## Type Mapping
+
+With RESP3, you can leverage the protocol's richer type system. You can customize how different Redis types are represented in JavaScript using type mapping:
+
+```javascript
+import { createClient, RESP_TYPES } from 'redis';
+
+// By default
+await client.hGetAll('key'); // Record<string, string>
+
+// Use Map instead of plain object
+await client.withTypeMapping({
+  [RESP_TYPES.MAP]: Map
+}).hGetAll('key'); // Map<string, string>
+
+// Use both Map and Buffer
+await client.withTypeMapping({
+  [RESP_TYPES.MAP]: Map,
+  [RESP_TYPES.BLOB_STRING]: Buffer
+}).hGetAll('key'); // Map<string, Buffer>
+```
+
+This replaces the previous approach of using `commandOptions({ returnBuffers: true })` in v4.
+
+## PubSub in RESP3
+
+RESP3 uses a different mechanism for handling Pub/Sub messages. Instead of modifying the `onReply` handler as in RESP2, RESP3 provides a dedicated `onPush` handler. When using RESP3, the client automatically uses this more efficient push notification system.
+
+## Known Limitations
+
+### Unstable Commands
+
+Some Redis commands have unstable RESP3 transformations. These commands will throw an error when used with RESP3 unless you explicitly opt in to using them by setting `unstableResp3: true` in your client configuration:
+
+```javascript
+const client = createClient({
+  RESP: 3,
+  unstableResp3: true
+});
+```
+
+The following commands have unstable RESP3 implementations:
+
+1. **Stream Commands**:
+   - `XREAD` and `XREADGROUP` - The response format differs between RESP2 and RESP3
+
+2. **Search Commands (RediSearch)**:
+   - `FT.AGGREGATE`
+   - `FT.AGGREGATE_WITHCURSOR`
+   - `FT.CURSOR_READ`
+   - `FT.INFO`
+   - `FT.PROFILE_AGGREGATE`
+   - `FT.PROFILE_SEARCH`
+   - `FT.SEARCH`
+   - `FT.SEARCH_NOCONTENT`
+   - `FT.SPELLCHECK`
+
+3. **Time Series Commands**:
+   - `TS.INFO`
+   - `TS.INFO_DEBUG`
+
+If you need to use these commands with RESP3, be aware that the response format might change in future versions.
+
+# Sentinel Support
+
+[Sentinel](./sentinel.md)
+
+# `multi.exec<'typed'>` / `multi.execTyped`
+
+We have introduced the ability to perform a "typed" `MULTI`/`EXEC` transaction. Rather than returning `Array<ReplyUnion>`, a transaction invoked with `.exec<'typed'>` will return types appropriate to the commands in the transaction where possible:
+
+```javascript
+const multi = client.multi().ping();
+await multi.exec(); // Array<ReplyUnion>
+await multi.exec<'typed'>(); // [string]
+await multi.execTyped(); // [string]
+```
+
+# Client Side Caching
+
+Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The server will notify the client when cached results are no longer valid.
+
+Client Side Caching is only supported with RESP3.
+
+## Usage
+
+There are two ways to implement client side caching:
+
+### Anonymous Cache
+
+```javascript
+const client = createClient({
+  RESP: 3,
+  clientSideCache: {
+    ttl: 0,             // Time-to-live in milliseconds (0 = no expiration)
+    maxEntries: 0,      // Maximum entries to store (0 = unlimited)
+    evictPolicy: "LRU"  // Eviction policy: "LRU" or "FIFO"
+  }
+});
+```
+
+In this instance, the cache is managed internally by the client.
+
+### Controllable Cache
+
+```javascript
+import { BasicClientSideCache } from 'redis';
+
+const cache = new BasicClientSideCache({
+  ttl: 0,
+  maxEntries: 0,
+  evictPolicy: "LRU"
+});
+
+const client = createClient({
+  RESP: 3,
+  clientSideCache: cache
+});
+```
+
+With this approach, you have direct access to the cache object for more control:
+
+```javascript
+// Manually invalidate keys
+cache.invalidate(key);
+
+// Clear the entire cache
+cache.clear();
+
+// Get cache metrics
+// `cache.stats()` returns a `CacheStats` object with comprehensive statistics.
+const statistics = cache.stats();
+
+// Key metrics:
+const hits = statistics.hitCount;        // Number of cache hits
+const misses = statistics.missCount;      // Number of cache misses
+const hitRate = statistics.hitRate();     // Cache hit rate (0.0 to 1.0)
+
+// Many other metrics are available on the `statistics` object, e.g.:
+// statistics.missRate(), statistics.loadSuccessCount,
+// statistics.averageLoadPenalty(), statistics.requestCount()
+```
+
+## Pooled Caching
+
+Client side caching also works with client pools. For pooled clients, the cache is shared across all clients in the pool:
+
+```javascript
+const client = createClientPool({RESP: 3}, {
+  clientSideCache: {
+    ttl: 0,
+    maxEntries: 0,
+    evictPolicy: "LRU"
+  },
+  minimum: 5
+});
+```
+
+For a controllable pooled cache:
+
+```javascript
+import { BasicPooledClientSideCache } from 'redis';
+
+const cache = new BasicPooledClientSideCache({
+  ttl: 0,
+  maxEntries: 0,
+  evictPolicy: "LRU"
+});
+
+const client = createClientPool({RESP: 3}, {
+  clientSideCache: cache,
+  minimum: 5
+});
+```
diff --git a/doctests/README.md b/doctests/README.md
new file mode 100644
index 00000000000..59d1cb0364c
--- /dev/null
+++ b/doctests/README.md
@@ -0,0 +1,32 @@
+# Command examples for redis.io
+
+## Setup
+
+To set up the examples folder so that you can run an example / develop one of your own:
+
+```
+$ git clone https://github.com/redis/node-redis.git
+$ cd node-redis
+$ npm install -ws && npm run build
+$ cd doctests
+$ npm install
+```
+
+## How to add examples
+
+Create regular node file in the current folder with meaningful name. It makes sense prefix example files with
+command category (e.g. string, set, list, hash, etc) to make navigation in the folder easier.
+
+### Special markup
+
+See https://github.com/redis-stack/redis-stack-website#readme for more details.
+
+## How to test the examples
+
+Just include necessary assertions in the example file and run
+```bash
+sh doctests/run_examples.sh
+```
+to test all examples in the current folder.
+
+See `tests.js` for more details.
diff --git a/doctests/cmds-cnxmgmt.js b/doctests/cmds-cnxmgmt.js
new file mode 100644
index 00000000000..8b616b6f105
--- /dev/null
+++ b/doctests/cmds-cnxmgmt.js
@@ -0,0 +1,49 @@
+// EXAMPLE: cmds_cnxmgmt
+// REMOVE_START
+import assert from "node:assert";
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START auth1
+// REMOVE_START
+await client.sendCommand(['CONFIG', 'SET', 'requirepass', 'temp_pass']);
+// REMOVE_END
+const res1 = await client.auth({ password: 'temp_pass' });
+console.log(res1); // OK
+
+const res2 = await client.auth({ username: 'default', password: 'temp_pass' });
+console.log(res2); // OK
+
+// REMOVE_START
+assert.equal(res1, "OK");
+assert.equal(res2, "OK");
+await client.sendCommand(['CONFIG', 'SET', 'requirepass', '']);
+// REMOVE_END
+// STEP_END
+
+// STEP_START auth2
+// REMOVE_START
+await client.sendCommand([
+  'ACL', 'SETUSER', 'test-user',
+  'on', '>strong_password', '+acl'
+]);
+// REMOVE_END
+const res3 = await client.auth({ username: 'test-user', password: 'strong_password' });
+console.log(res3); // OK
+
+// REMOVE_START
+assert.equal(res3, "OK");
+await client.auth({ username: 'default', password: '' })
+await client.sendCommand(['ACL', 'DELUSER', 'test-user']);
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-generic.js b/doctests/cmds-generic.js
new file mode 100644
index 00000000000..50329ede460
--- /dev/null
+++ b/doctests/cmds-generic.js
@@ -0,0 +1,195 @@
+// EXAMPLE: cmds_generic
+// REMOVE_START
+import assert from "node:assert";
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START del
+const delRes1 = await client.set('key1', 'Hello');
+console.log(delRes1); // OK
+
+const delRes2 = await client.set('key2', 'World');
+console.log(delRes2); // OK
+
+const delRes3 = await client.del(['key1', 'key2', 'key3']);
+console.log(delRes3); // 2
+// REMOVE_START
+assert.equal(delRes3, 2);
+// REMOVE_END
+// STEP_END
+
+// STEP_START expire
+const expireRes1 = await client.set('mykey', 'Hello');
+console.log(expireRes1); // OK
+
+const expireRes2 = await client.expire('mykey', 10);
+console.log(expireRes2); // 1
+
+const expireRes3 = await client.ttl('mykey');
+console.log(expireRes3); // 10
+// REMOVE_START
+assert.equal(expireRes3, 10);
+// REMOVE_END
+
+const expireRes4 = await client.set('mykey', 'Hello World');
+console.log(expireRes4); // OK
+
+const expireRes5 = await client.ttl('mykey');
+console.log(expireRes5); // -1
+// REMOVE_START
+assert.equal(expireRes5, -1);
+// REMOVE_END
+
+const expireRes6 = await client.expire('mykey', 10, "XX");
+console.log(expireRes6); // 0
+// REMOVE_START
+assert.equal(expireRes6, 0)
+// REMOVE_END
+
+const expireRes7 = await client.ttl('mykey');
+console.log(expireRes7); // -1
+// REMOVE_START
+assert.equal(expireRes7, -1);
+// REMOVE_END
+
+const expireRes8 = await client.expire('mykey', 10, "NX");
+console.log(expireRes8); // 1
+// REMOVE_START
+assert.equal(expireRes8, 1);
+// REMOVE_END
+
+const expireRes9 = await client.ttl('mykey');
+console.log(expireRes9); // 10
+// REMOVE_START
+assert.equal(expireRes9, 10);
+await client.del('mykey');
+// REMOVE_END
+// STEP_END
+
+// STEP_START ttl
+const ttlRes1 = await client.set('mykey', 'Hello');
+console.log(ttlRes1); // OK
+
+const ttlRes2 = await client.expire('mykey', 10);
+console.log(ttlRes2); // 1
+
+const ttlRes3 = await client.ttl('mykey');
+console.log(ttlRes3); // 10
+// REMOVE_START
+assert.equal(ttlRes3, 10);
+await client.del('mykey');
+// REMOVE_END
+// STEP_END
+
+// STEP_START scan1
+const scan1Res1 = await client.sAdd('myset', ['1', '2', '3', 'foo', 'foobar', 'feelsgood']);
+console.log(scan1Res1); // 6
+
+let scan1Res2 = [];
+for await (const values of client.sScanIterator('myset', { MATCH: 'f*' })) {
+    scan1Res2 = scan1Res2.concat(values);
+}
+console.log(scan1Res2); // ['foo', 'foobar', 'feelsgood']
+// REMOVE_START
+console.assert(scan1Res2.sort().toString() === ['foo', 'foobar', 'feelsgood'].sort().toString());
+await client.del('myset');
+// REMOVE_END
+// STEP_END
+
+// STEP_START scan2
+// REMOVE_START
+for (let i = 1; i <= 1000; i++) {
+    await client.set(`key:${i}`, i);
+}
+// REMOVE_END
+let cursor = '0';
+let scanResult;
+
+scanResult = await client.scan(cursor, { MATCH: '*11*' });
+console.log(scanResult.cursor, scanResult.keys);
+
+scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*' });
+console.log(scanResult.cursor, scanResult.keys);
+
+scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*' });
+console.log(scanResult.cursor, scanResult.keys);
+
+scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*' });
+console.log(scanResult.cursor, scanResult.keys);
+
+scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*', COUNT: 1000 });
+console.log(scanResult.cursor, scanResult.keys);
+// REMOVE_START
+console.assert(scanResult.keys.length === 18);
+cursor = '0';
+const prefix = 'key:*';
+do {
+    scanResult = await client.scan(cursor, { MATCH: prefix, COUNT: 1000 });
+    console.log(scanResult.cursor, scanResult.keys);
+    cursor = scanResult.cursor;
+    const keys = scanResult.keys;
+    if (keys.length) {
+        await client.del(keys);
+    }
+} while (cursor !== '0');
+// REMOVE_END
+// STEP_END
+
+// STEP_START scan3
+const scan3Res1 = await client.geoAdd('geokey', { longitude: 0, latitude: 0, member: 'value' });
+console.log(scan3Res1); // 1
+
+const scan3Res2 = await client.zAdd('zkey', [{ score: 1000, value: 'value' }]);
+console.log(scan3Res2); // 1
+
+const scan3Res3 = await client.type('geokey');
+console.log(scan3Res3); // zset
+// REMOVE_START
+console.assert(scan3Res3 === 'zset');
+// REMOVE_END
+
+const scan3Res4 = await client.type('zkey');
+console.log(scan3Res4); // zset
+// REMOVE_START
+console.assert(scan3Res4 === 'zset');
+// REMOVE_END
+
+const scan3Res5 = await client.scan('0', { TYPE: 'zset' });
+console.log(scan3Res5.keys); // ['zkey', 'geokey']
+// REMOVE_START
+console.assert(scan3Res5.keys.sort().toString() === ['zkey', 'geokey'].sort().toString());
+await client.del(['geokey', 'zkey']);
+// REMOVE_END
+// STEP_END
+
+// STEP_START scan4
+const scan4Res1 = await client.hSet('myhash', { a: 1, b: 2 });
+console.log(scan4Res1); // 2
+
+const scan4Res2 = await client.hScan('myhash', '0');
+console.log(scan4Res2.entries); // [{field: 'a', value: '1'}, {field: 'b', value: '2'}]
+// REMOVE_START
+assert.deepEqual(scan4Res2.entries, [
+  { field: 'a', value: '1' },
+  { field: 'b', value: '2' }
+]);
+// REMOVE_END
+
+const scan4Res3 = await client.hScan('myhash', '0', { COUNT: 10 });
+const items = scan4Res3.entries.map((item) => item.field)
+console.log(items); // ['a', 'b']
+// REMOVE_START
+assert.deepEqual(items, ['a', 'b'])
+await client.del('myhash');
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-hash.js b/doctests/cmds-hash.js
new file mode 100644
index 00000000000..8ce29785763
--- /dev/null
+++ b/doctests/cmds-hash.js
@@ -0,0 +1,109 @@
+// EXAMPLE: cmds_hash
+// HIDE_START
+import assert from 'node:assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START hset
+const res1 = await client.hSet('myhash', 'field1', 'Hello')
+console.log(res1) // 1
+
+const res2 = await client.hGet('myhash', 'field1')
+console.log(res2) // Hello
+
+const res3 = await client.hSet(
+  'myhash',
+  {
+    'field2': 'Hi',
+    'field3': 'World'
+  }
+)
+console.log(res3) // 2
+
+const res4 = await client.hGet('myhash', 'field2')
+console.log(res4) // Hi
+
+const res5 = await client.hGet('myhash', 'field3')
+console.log(res5) // World
+
+const res6 = await client.hGetAll('myhash')
+console.log(res6)  
+
+// REMOVE_START
+assert.equal(res1, 1);
+assert.equal(res2, 'Hello');
+assert.equal(res3, 2);
+assert.equal(res4, 'Hi');
+assert.equal(res5, 'World');
+assert.deepEqual(res6, {
+  field1: 'Hello',
+  field2: 'Hi',
+  field3: 'World'
+});
+await client.del('myhash')
+// REMOVE_END
+// STEP_END
+
+// STEP_START hget
+const res7 = await client.hSet('myhash', 'field1', 'foo')
+console.log(res7) // 1
+
+const res8 = await client.hGet('myhash', 'field1')
+console.log(res8) // foo
+
+const res9 = await client.hGet('myhash', 'field2')
+console.log(res9) // null
+
+// REMOVE_START
+assert.equal(res7, 1);
+assert.equal(res8, 'foo');
+assert.equal(res9, null);
+await client.del('myhash')
+// REMOVE_END
+// STEP_END
+
+// STEP_START hgetall
+const res10 = await client.hSet(
+  'myhash',
+  {
+    'field1': 'Hello',
+    'field2': 'World'
+  }
+)
+
+const res11 = await client.hGetAll('myhash')
+console.log(res11) // [Object: null prototype] { field1: 'Hello', field2: 'World' }
+
+// REMOVE_START
+assert.deepEqual(res11, {
+  field1: 'Hello',
+  field2: 'World'
+});
+await client.del('myhash')
+// REMOVE_END
+// STEP_END
+
+// STEP_START hvals
+const res12 = await client.hSet(
+  'myhash',
+  {
+    'field1': 'Hello',
+    'field2': 'World'
+  }
+)
+
+const res13 = await client.hVals('myhash')
+console.log(res13) // [ 'Hello', 'World' ]
+
+// REMOVE_START
+assert.deepEqual(res13, [ 'Hello', 'World' ]);
+await client.del('myhash')
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-list.js b/doctests/cmds-list.js
new file mode 100644
index 00000000000..9d1b4154dfe
--- /dev/null
+++ b/doctests/cmds-list.js
@@ -0,0 +1,129 @@
+// EXAMPLE: cmds_list
+// HIDE_START
+import assert from 'node:assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START lpush
+const res1 = await client.lPush('mylist', 'world');
+console.log(res1); // 1
+
+const res2 = await client.lPush('mylist', 'hello');
+console.log(res2); // 2
+
+const res3 = await client.lRange('mylist', 0, -1);
+console.log(res3); // [ 'hello', 'world' ]
+
+// REMOVE_START
+assert.deepEqual(res3, [ 'hello', 'world' ]);
+await client.del('mylist');
+// REMOVE_END
+// STEP_END
+
+// STEP_START lrange
+const res4 = await client.rPush('mylist', 'one');
+console.log(res4); // 1
+
+const res5 = await client.rPush('mylist', 'two');
+console.log(res5); // 2
+
+const res6 = await client.rPush('mylist', 'three');
+console.log(res6); // 3
+
+const res7 = await client.lRange('mylist', 0, 0);
+console.log(res7); // [ 'one' ]
+
+const res8 = await client.lRange('mylist', -3, 2);
+console.log(res8); // [ 'one', 'two', 'three' ]
+
+const res9 = await client.lRange('mylist', -100, 100);
+console.log(res9); // [ 'one', 'two', 'three' ]
+
+const res10 = await client.lRange('mylist', 5, 10);
+console.log(res10); // []
+
+// REMOVE_START
+assert.deepEqual(res7, [ 'one' ]);
+assert.deepEqual(res8, [ 'one', 'two', 'three' ]);
+assert.deepEqual(res9, [ 'one', 'two', 'three' ]);
+assert.deepEqual(res10, []);
+await client.del('mylist');
+// REMOVE_END
+// STEP_END
+
+// STEP_START llen
+const res11 = await client.lPush('mylist', 'World');
+console.log(res11); // 1
+
+const res12 = await client.lPush('mylist', 'Hello');
+console.log(res12); // 2
+
+const res13 = await client.lLen('mylist');
+console.log(res13); // 2
+
+// REMOVE_START
+assert.equal(res13, 2);
+await client.del('mylist');
+// REMOVE_END
+// STEP_END
+
+// STEP_START rpush
+const res14 = await client.rPush('mylist', 'hello');
+console.log(res14); // 1
+
+const res15 = await client.rPush('mylist', 'world');
+console.log(res15); // 2
+
+const res16 = await client.lRange('mylist', 0, -1);
+console.log(res16); // [ 'hello', 'world' ]
+
+// REMOVE_START
+assert.deepEqual(res16, [ 'hello', 'world' ]);
+await client.del('mylist');
+// REMOVE_END
+// STEP_END
+
+// STEP_START lpop
+const res17 = await client.rPush('mylist', ["one", "two", "three", "four", "five"]);
+console.log(res17); // 5
+
+const res18 = await client.lPop('mylist');
+console.log(res18); // 'one'
+
+const res19 = await client.lPopCount('mylist', 2);
+console.log(res19); // [ 'two', 'three' ]
+
+const res20 = await client.lRange('mylist', 0, -1);
+console.log(res20); // [ 'four', 'five' ]
+
+// REMOVE_START
+assert.deepEqual(res20, [ 'four', 'five' ]);
+await client.del('mylist');
+// REMOVE_END
+// STEP_END
+
+// STEP_START rpop
+const res21 = await client.rPush('mylist', ["one", "two", "three", "four", "five"]);
+console.log(res21); // 5
+
+const res22 = await client.rPop('mylist');
+console.log(res22); // 'five'
+
+const res23 = await client.rPopCount('mylist', 2);
+console.log(res23); // [ 'four', 'three' ]
+
+const res24 = await client.lRange('mylist', 0, -1);
+console.log(res24); // [ 'one', 'two' ]
+
+// REMOVE_START
+assert.deepEqual(res24, [ 'one', 'two' ]);
+await client.del('mylist');
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-servermgmt.js b/doctests/cmds-servermgmt.js
new file mode 100644
index 00000000000..0a53e05919d
--- /dev/null
+++ b/doctests/cmds-servermgmt.js
@@ -0,0 +1,45 @@
+// EXAMPLE: cmds_servermgmt
+// REMOVE_START
+import assert from 'node:assert';
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START flushall
+// REMOVE_START
+await client.set('foo', '1');
+await client.set('bar', '2');
+await client.set('baz', '3');
+// REMOVE_END
+const res1 = await client.flushAll('SYNC'); // or ASYNC
+console.log(res1); // OK
+
+const res2 = await client.keys('*');
+console.log(res2); // []
+
+// REMOVE_START
+assert.equal(res1, 'OK');
+assert.deepEqual(res2, []);
+// REMOVE_END
+// STEP_END
+
+// STEP_START info
+const res3 = await client.info();
+console.log(res3)
+// # Server
+// redis_version:7.4.0
+// redis_git_sha1:c9d29f6a
+// redis_git_dirty:0
+// redis_build_id:4c367a16e3f9616
+// redis_mode:standalone
+// ...
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-set.js b/doctests/cmds-set.js
new file mode 100644
index 00000000000..8a9a3837036
--- /dev/null
+++ b/doctests/cmds-set.js
@@ -0,0 +1,44 @@
+// EXAMPLE: cmds_set
+// REMOVE_START
+import assert from 'node:assert';
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START sadd
+const res1 = await client.sAdd('myset', ['Hello', 'World']);
+console.log(res1);  // 2
+
+const res2 = await client.sAdd('myset', ['World']);
+console.log(res2);  // 0
+
+const res3 = await client.sMembers('myset')
+console.log(res3);  // ['Hello', 'World']
+
+// REMOVE_START
+assert.deepEqual(res3, ['Hello', 'World']);
+await client.del('myset');
+// REMOVE_END
+// STEP_END
+
+// STEP_START smembers
+const res4 = await client.sAdd('myset', ['Hello', 'World']);
+console.log(res4);  // 2
+
+const res5 = await client.sMembers('myset')
+console.log(res5);  // ['Hello', 'World']
+
+// REMOVE_START
+assert.deepEqual(res5, ['Hello', 'World']);
+await client.del('myset');
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-sorted-set.js b/doctests/cmds-sorted-set.js
new file mode 100644
index 00000000000..b718938cc2b
--- /dev/null
+++ b/doctests/cmds-sorted-set.js
@@ -0,0 +1,115 @@
+// EXAMPLE: cmds_sorted_set
+// REMOVE_START
+import assert from "node:assert";
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+client.on('error', err => console.log('Redis Client Error', err));
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START zadd
+const val1 = await client.zAdd("myzset", [{ value: 'one', score: 1 }]);
+console.log(val1);
+// returns 1
+
+const val2 = await client.zAdd("myzset", [{ value: 'uno', score: 1 }]);
+console.log(val2);
+// returns 1
+
+const val3 = await client.zAdd("myzset", [{ value: 'two', score: 2 }, { value: 'three', score: 3 }]);
+console.log(val3);
+// returns 2
+
+const val4 = await client.zRangeWithScores("myzset", 0, -1);
+console.log(val4);
+// returns [{value: 'one', score: 1}, {value: 'uno', score: 1}, {value: 'two', score: 2}, {value: 'three', score: 3} ]
+
+// REMOVE_START
+assert.equal(val1, 1);
+assert.equal(val2, 1);
+assert.equal(val3, 2);
+assert.deepEqual(val4, [
+  { value: 'one', score: 1 },
+  { value: 'uno', score: 1 },
+  { value: 'two', score: 2 },
+  { value: 'three', score: 3 }
+]);
+await client.del('myzset');
+// REMOVE_END
+// STEP_END
+
+// STEP_START zrange1
+const val5 = await client.zAdd("myzset", [
+  { value: 'one', score: 1 },
+  { value: 'two', score: 2 },
+  { value: 'three', score: 3 }
+]);
+console.log(val5);
+// returns 3
+
+const val6 = await client.zRange('myzset', 0, -1);
+console.log(val6);
+// returns ['one', 'two', 'three']
+// REMOVE_START
+console.assert(JSON.stringify(val6) === JSON.stringify(['one', 'two', 'three']));
+// REMOVE_END
+
+const val7 = await client.zRange('myzset', 2, 3);
+console.log(val7);
+// returns ['three']
+// REMOVE_START
+console.assert(JSON.stringify(val7) === JSON.stringify(['three']));
+// REMOVE_END
+
+const val8 = await client.zRange('myzset', -2, -1);
+console.log(val8);
+// returns ['two', 'three']
+// REMOVE_START
+console.assert(JSON.stringify(val8) === JSON.stringify(['two', 'three']));
+await client.del('myzset');
+// REMOVE_END
+// STEP_END
+
+// STEP_START zrange2
+const val9 = await client.zAdd("myzset", [
+  { value: 'one', score: 1 },
+  { value: 'two', score: 2 },
+  { value: 'three', score: 3 }
+]);
+console.log(val9);
+// returns 3
+
+const val10 = await client.zRangeWithScores('myzset', 0, 1);
+console.log(val10);
+// returns [{value: 'one', score: 1}, {value: 'two', score: 2}]
+// REMOVE_START
+console.assert(JSON.stringify(val10) === JSON.stringify([{value: 'one', score: 1}, {value: 'two', score: 2}]));
+await client.del('myzset');
+// REMOVE_END
+// STEP_END
+
+// STEP_START zrange3
+const val11 = await client.zAdd("myzset", [
+  { value: 'one', score: 1 },
+  { value: 'two', score: 2 },
+  { value: 'three', score: 3 }
+]);
+console.log(val11);
+// returns 3
+
+const val12 = await client.zRange('myzset', 2, 3, { BY: 'SCORE', LIMIT: { offset: 1, count: 1 } });
+console.log(val12);
+// >>> ['three']
+// REMOVE_START
+console.assert(JSON.stringify(val12) === JSON.stringify(['three']));
+await client.del('myzset');
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/cmds-string.js b/doctests/cmds-string.js
new file mode 100644
index 00000000000..00b3f738fa3
--- /dev/null
+++ b/doctests/cmds-string.js
@@ -0,0 +1,27 @@
+// EXAMPLE: cmds_string
+// REMOVE_START
+import assert from "node:assert";
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+client.on('error', err => console.log('Redis Client Error', err));
+await client.connect().catch(console.error);
+// HIDE_END
+
+// STEP_START incr
+await client.set("mykey", "10");
+const value1 = await client.incr("mykey");
+console.log(value1);
+// returns 11
+// REMOVE_START
+assert.equal(value1, 11);
+await client.del('mykey');
+// REMOVE_END
+// STEP_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/doctests/data/query_em.json b/doctests/data/query_em.json
new file mode 100644
index 00000000000..bcf908aaf85
--- /dev/null
+++ b/doctests/data/query_em.json
@@ -0,0 +1,92 @@
+[
+  {
+	  "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, -74.0610 40.6678, -74.0610 40.7578))",
+	  "store_location": "-74.0060,40.7128",
+	  "brand": "Velorim",
+	  "model": "Jigger",
+	  "price": 270,
+	  "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.",
+	  "condition": "new"
+  },
+  {
+    "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, -118.2887 33.9872, -118.2887 34.0972))",
+	  "store_location": "-118.2437,34.0522",
+	  "brand": "Bicyk",
+	  "model": "Hillcraft",
+	  "price": 1200,
+	  "description": "Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!",
+	  "condition": "used"
+  },
+  {
+    "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, -87.6848 41.8231, -87.6848 41.9331))",
+	  "store_location": "-87.6298,41.8781",
+  	"brand": "Nord",
+  	"model": "Chook air 5",
+  	"price": 815,
+  	"description": "The Chook Air 5  gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower  top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.",
+  	"condition": "used"
+  },
+  {
+    "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))",
+  	"store_location": "-80.1918,25.7617",
+  	"brand": "Eva",
+  	"model": "Eva 291",
+  	"price": 3400,
+  	"description": "The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!",
+  	"condition": "used"
+  },
+  {
+    "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, -122.4644 37.7099, -122.4644 37.8199))",
+  	"store_location": "-122.4194,37.7749",
+  	"brand": "Noka Bikes",
+  	"model": "Kahuna",
+  	"price": 3200,
+  	"description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.",
+  	"condition": "used"
+  },
+  {
+    "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, -0.1778 51.4024, -0.1778 51.5524))",
+    "store_location": "-0.1278,51.5074",
+    "brand": "Breakout",
+    "model": "XBN 2.1 Alloy",
+    "price": 810,
+    "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s not to say that it’s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.",
+    "condition": "new"
+  },
+  {
+    "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, 2.1767 48.5516, 2.1767 48.9016))",
+    "store_location": "2.3522,48.8566",
+    "brand": "ScramBikes",
+    "model": "WattBike",
+    "price": 2300,
+    "description": "The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It’s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.",
+    "condition": "new"
+  },
+  {
+    "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, 13.3260 52.2700, 13.3260 52.5700))",
+    "store_location": "13.4050,52.5200",
+    "brand": "Peaknetic",
+    "model": "Secto",
+    "price": 430,
+    "description": "If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
+    "condition": "new"
+  },
+  {
+    "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))",
+    "store_location": "2.1734, 41.3851",
+    "brand": "nHill",
+    "model": "Summit",
+    "price": 1200,
+    "description": "This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you’re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.",
+    "condition": "new"
+  },
+  {
+    "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, 12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
+    "store_location": "12.4964,41.9028",
+    "model": "ThrillCycle",
+    "brand": "BikeShind",
+    "price": 815,
+    "description": "An artsy,  retro-inspired bicycle that’s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.",
+    "condition": "refurbished"
+  }
+]
diff --git a/doctests/data/query_vector.json b/doctests/data/query_vector.json
new file mode 100644
index 00000000000..625479e111b
--- /dev/null
+++ b/doctests/data/query_vector.json
@@ -0,0 +1,3952 @@
+[
+  {
+    "brand": "Velorim",
+    "condition": "new",
+    "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids\u2019 pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.",
+    "description_embeddings": [
+      -0.026918452233076096,
+      0.07200391590595245,
+      0.019199736416339874,
+      -0.024749649688601494,
+      -0.09264523535966873,
+      0.017702950164675713,
+      0.11252444237470627,
+      0.09377790987491608,
+      0.005099582951515913,
+      0.07054618746042252,
+      0.0025260779075324535,
+      -0.04007257893681526,
+      0.013598357327282429,
+      0.03940897434949875,
+      -0.0069704861380159855,
+      0.057934146374464035,
+      0.15386416018009186,
+      0.04337097704410553,
+      0.07119690626859665,
+      -0.048173222690820694,
+      -0.09069827198982239,
+      -0.016886970028281212,
+      -0.04425429925322533,
+      -0.019464140757918358,
+      -0.027470778673887253,
+      0.005336642265319824,
+      -0.09170512855052948,
+      0.03556380048394203,
+      0.023559197783470154,
+      -0.03628315031528473,
+      -0.04448218643665314,
+      0.011364061385393143,
+      0.009603296406567097,
+      -0.04861818626523018,
+      -0.03343017399311066,
+      -0.01483147218823433,
+      0.06086479872465134,
+      0.02109363302588463,
+      -0.025959225371479988,
+      0.014000430703163147,
+      -0.00846729427576065,
+      0.07305800914764404,
+      0.02457829751074314,
+      -0.12663941085338593,
+      0.010544337332248688,
+      0.013315590098500252,
+      0.07280771434307098,
+      -0.08232685923576355,
+      0.0040486594662070274,
+      -0.026350753381848335,
+      0.06408613175153732,
+      -0.01415738184005022,
+      0.04628903791308403,
+      -0.02050374448299408,
+      0.04177685081958771,
+      -0.09207800775766373,
+      -0.005421833600848913,
+      -0.005136478692293167,
+      -0.024564260616898537,
+      0.026354163885116577,
+      -0.05851329490542412,
+      0.03147275745868683,
+      -0.02183554694056511,
+      0.03346295654773712,
+      -0.02240697667002678,
+      -0.09603817760944366,
+      -0.02274233102798462,
+      -0.039677977561950684,
+      0.007695100735872984,
+      0.039304088801145554,
+      -0.017668871209025383,
+      0.022897064685821533,
+      -0.039273541420698166,
+      0.08864572644233704,
+      -0.04432578384876251,
+      -0.06769558042287827,
+      0.06696884334087372,
+      0.07118263095617294,
+      -0.024863263592123985,
+      0.01151553075760603,
+      -0.11591693758964539,
+      -0.025131937116384506,
+      0.052269868552684784,
+      -0.03035089746117592,
+      0.00906881783157587,
+      0.04585501551628113,
+      0.038361817598342896,
+      0.03026638925075531,
+      0.015340524725615978,
+      -0.006911538541316986,
+      0.022395918145775795,
+      0.13969141244888306,
+      0.047686025500297546,
+      0.05438247323036194,
+      0.02779674343764782,
+      -0.04191797226667404,
+      -0.021741557866334915,
+      0.003305602353066206,
+      -0.11100355535745621,
+      0.016258426010608673,
+      0.06977421790361404,
+      0.08189859241247177,
+      0.0966871827840805,
+      0.03519754856824875,
+      0.05674563720822334,
+      0.034512776881456375,
+      0.07052291929721832,
+      -0.06342307478189468,
+      0.051868196576833725,
+      -0.013776403851807117,
+      -0.007541927974671125,
+      -0.043183840811252594,
+      0.021481933072209358,
+      0.0380198135972023,
+      -0.07870583236217499,
+      -0.10873759537935257,
+      -0.08491706103086472,
+      0.03155837208032608,
+      -0.03790571913123131,
+      0.041968367993831635,
+      -0.00593406381085515,
+      0.036538854241371155,
+      -0.004705581348389387,
+      0.004229994490742683,
+      -0.00729013979434967,
+      -0.019296232610940933,
+      -0.014331319369375706,
+      -4.401502220942261e-34,
+      -0.0067550260573625565,
+      0.07402423769235611,
+      -0.012888211756944656,
+      -0.055266380310058594,
+      0.04810081049799919,
+      0.005175809375941753,
+      -0.004325157031416893,
+      -0.10392399877309799,
+      -0.03650582954287529,
+      0.07477248460054398,
+      0.0022102247457951307,
+      -0.05040738359093666,
+      -0.003033560933545232,
+      0.060498371720314026,
+      0.08619660884141922,
+      -0.04577762633562088,
+      -0.10468175262212753,
+      -0.07177772372961044,
+      -0.05756700038909912,
+      -0.02839704230427742,
+      -0.028650879859924316,
+      -0.010213681496679783,
+      0.008074316196143627,
+      0.03448071703314781,
+      -0.025478240102529526,
+      -0.029753824695944786,
+      0.05397271364927292,
+      -0.0006062929751351476,
+      -0.03292117267847061,
+      0.040799956768751144,
+      -0.0933983102440834,
+      -0.026921836659312248,
+      0.00327915046364069,
+      -0.025635670870542526,
+      -0.057946983724832535,
+      -0.06664302200078964,
+      0.04280361905694008,
+      -0.027111517265439034,
+      -0.08359260112047195,
+      0.03483080118894577,
+      0.023318039253354073,
+      -0.08598511666059494,
+      -0.08378149569034576,
+      0.054067932069301605,
+      -0.014853178523480892,
+      -0.05498708039522171,
+      0.0711284726858139,
+      0.12539872527122498,
+      -0.04355800896883011,
+      -0.027553806081414223,
+      -0.009111037477850914,
+      -0.058482203632593155,
+      0.07053986191749573,
+      -0.009027705527842045,
+      -0.017481567338109016,
+      0.011404255405068398,
+      0.06084389239549637,
+      -0.028110956773161888,
+      -0.08594057708978653,
+      0.05215488001704216,
+      -0.07651112973690033,
+      -0.027076181024312973,
+      -0.01357623003423214,
+      -0.01263132132589817,
+      -0.0193876251578331,
+      0.013576658442616463,
+      0.038156066089868546,
+      -0.04309772700071335,
+      -0.04051121696829796,
+      -0.025885144248604774,
+      -0.003073574509471655,
+      -0.0003303807170595974,
+      0.08043289929628372,
+      -0.039484549313783646,
+      0.10091833025217056,
+      -0.04735022410750389,
+      0.027813943102955818,
+      -0.0038837436586618423,
+      -0.05234759673476219,
+      -0.00716474698856473,
+      0.016360750421881676,
+      -0.025806615129113197,
+      -0.03212691470980644,
+      -0.08456144481897354,
+      -0.019326699897646904,
+      -0.03228791803121567,
+      0.07633069902658463,
+      -0.07643644511699677,
+      -0.03988131135702133,
+      0.02396279387176037,
+      -0.055901359766721725,
+      0.009231535717844963,
+      0.0344320572912693,
+      0.07486359030008316,
+      -0.03505600243806839,
+      -2.324423447670953e-34,
+      -0.04453577473759651,
+      0.06512241810560226,
+      0.03920532390475273,
+      0.062222111970186234,
+      -0.015745285898447037,
+      -0.017774563282728195,
+      0.08228200674057007,
+      -0.05798694118857384,
+      -0.042758435010910034,
+      -0.018822822719812393,
+      -0.07607664912939072,
+      -0.02666221559047699,
+      0.036936040967702866,
+      -0.034714240580797195,
+      0.06992944329977036,
+      0.00530517753213644,
+      -0.0005260169273242354,
+      -0.03961028903722763,
+      0.08799499273300171,
+      -0.04191635549068451,
+      0.07468635588884354,
+      -0.010930310003459454,
+      -0.0611649826169014,
+      -0.04100184887647629,
+      0.07131826132535934,
+      0.03241356834769249,
+      -0.0545443594455719,
+      -0.005295638460665941,
+      -0.04712966829538345,
+      0.032524388283491135,
+      -0.05130890756845474,
+      -0.01299980841577053,
+      0.06523969769477844,
+      -0.011433755978941917,
+      0.018730396404862404,
+      0.047184932976961136,
+      -0.043041545897722244,
+      -0.03231072053313255,
+      -0.015864262357354164,
+      0.03991076350212097,
+      -0.017617924138903618,
+      -0.03504975512623787,
+      0.027346905320882797,
+      0.05564267560839653,
+      0.01610865257680416,
+      0.0470576174557209,
+      -0.010647954419255257,
+      0.13047614693641663,
+      -0.011055804789066315,
+      0.011903814040124416,
+      -0.01350466813892126,
+      -0.0019897734746336937,
+      0.053073540329933167,
+      0.0717632919549942,
+      0.007322370074689388,
+      -0.0206251572817564,
+      0.061210062354803085,
+      0.03184640407562256,
+      -0.035093698650598526,
+      -0.0026315131690353155,
+      -0.03291690722107887,
+      -0.04229205846786499,
+      -0.04241437837481499,
+      0.1091129332780838,
+      0.02229561097919941,
+      0.02223002351820469,
+      0.03949614241719246,
+      0.031568314880132675,
+      -0.07121116667985916,
+      -0.07664268463850021,
+      -0.04235681891441345,
+      -0.011173299513757229,
+      0.1190338209271431,
+      -0.09825095534324646,
+      -0.0375107042491436,
+      0.007167852018028498,
+      0.047537703067064285,
+      0.044423725455999374,
+      0.022106878459453583,
+      -0.02811007760465145,
+      0.033864255994558334,
+      0.0643145889043808,
+      0.03725901246070862,
+      -0.0497952364385128,
+      -0.021733446046710014,
+      0.023898839950561523,
+      -0.11254694312810898,
+      -0.06519465893507004,
+      0.04424642026424408,
+      0.09124527126550674,
+      0.006083414424210787,
+      0.09144245833158493,
+      0.02653978019952774,
+      -0.01318738516420126,
+      0.0480327382683754,
+      -3.0391063887691416e-08,
+      0.051376331597566605,
+      -0.0002709411783143878,
+      -0.03103259764611721,
+      -0.018394535407423973,
+      0.05002995952963829,
+      0.05086217448115349,
+      -0.07317503541707993,
+      -0.0172730665653944,
+      -0.08379635214805603,
+      0.1180257499217987,
+      0.08445936441421509,
+      -0.025030585005879402,
+      -0.01965731382369995,
+      0.046042654663324356,
+      0.03724817931652069,
+      0.028524605557322502,
+      0.061249297112226486,
+      -0.027382537722587585,
+      -0.0011134583037346601,
+      -0.001871297718025744,
+      -0.04395337030291557,
+      0.002261978341266513,
+      0.06950556486845016,
+      -0.024213269352912903,
+      -0.0782783254981041,
+      -0.10320401936769485,
+      -0.022083906456828117,
+      -0.04333319142460823,
+      -0.0334695503115654,
+      0.007842703722417355,
+      -0.03523677587509155,
+      0.08107997477054596,
+      0.00924254022538662,
+      -0.013395791873335838,
+      -0.019067300483584404,
+      -0.008446489460766315,
+      -0.1053837463259697,
+      0.06697141379117966,
+      0.06984667479991913,
+      0.007155571132898331,
+      -0.038544610142707825,
+      0.0132181691005826,
+      -0.004773592576384544,
+      -0.022143904119729996,
+      -0.09064015746116638,
+      -0.07600560784339905,
+      -0.042070601135492325,
+      -0.08189931511878967,
+      0.03302472084760666,
+      0.043238356709480286,
+      -0.01407547201961279,
+      -0.03778013586997986,
+      0.030578600242733955,
+      0.021573437377810478,
+      0.04664295166730881,
+      0.056082408875226974,
+      -0.07687672227621078,
+      -0.0018553169211372733,
+      -0.051700614392757416,
+      0.043752558529376984,
+      -0.02636834792792797,
+      0.05589277669787407,
+      0.05282546952366829,
+      -0.016411008313298225
+    ],
+    "model": "Jigger",
+    "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, -74.0610 40.6678, -74.0610 40.7578))",
+    "price": 270,
+    "store_location": "-74.0060,40.7128"
+  },
+  {
+    "brand": "Bicyk",
+    "condition": "used",
+    "description": "Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!",
+    "description_embeddings": [
+      -0.004883771762251854,
+      0.08099519461393356,
+      -0.022444017231464386,
+      0.05437565594911575,
+      -0.07422323524951935,
+      0.0066548739559948444,
+      -0.06268022209405899,
+      0.042389899492263794,
+      -0.03745086118578911,
+      0.058961447328329086,
+      0.025613723322749138,
+      -0.04209878668189049,
+      0.06861244142055511,
+      0.01983577199280262,
+      0.026353053748607635,
+      0.045618414878845215,
+      0.040685027837753296,
+      0.09574265778064728,
+      0.005801026243716478,
+      -0.027659950777888298,
+      -0.0223013274371624,
+      0.040641166269779205,
+      0.06608107686042786,
+      0.0691058486700058,
+      -0.03629102557897568,
+      0.035505786538124084,
+      -0.09211395680904388,
+      -0.011358118616044521,
+      -0.025078972801566124,
+      -0.017709167674183846,
+      0.07587391883134842,
+      0.08128049969673157,
+      0.060521550476551056,
+      -0.0845090001821518,
+      -0.03779749944806099,
+      0.030346086248755455,
+      0.017926080152392387,
+      0.003489845432341099,
+      -0.05622200667858124,
+      -0.06886664777994156,
+      -0.051538050174713135,
+      0.029196197167038918,
+      -0.0028146395925432444,
+      0.012419342994689941,
+      -0.06346380710601807,
+      -0.011617675423622131,
+      0.04980290308594704,
+      -0.0799335315823555,
+      0.016635078936815262,
+      0.07064730674028397,
+      0.04530491679906845,
+      -0.04372858256101608,
+      0.07056037336587906,
+      -0.05052798613905907,
+      -0.01064316462725401,
+      -0.04754374921321869,
+      -0.08878123015165329,
+      0.005363269243389368,
+      0.032587066292762756,
+      -0.05610528588294983,
+      -0.0012875061947852373,
+      -0.03215320408344269,
+      -0.0045777312479913235,
+      -0.026692084968090057,
+      -0.09758491814136505,
+      -0.046251099556684494,
+      0.03897765651345253,
+      -0.06587375700473785,
+      -0.013586618937551975,
+      -0.020807752385735512,
+      0.023367363959550858,
+      0.011167124845087528,
+      0.003386110533028841,
+      -0.024887114763259888,
+      -0.029615335166454315,
+      -0.02571641281247139,
+      -0.03150812163949013,
+      -0.0395360104739666,
+      -0.049686528742313385,
+      -0.023117102682590485,
+      -0.07580453157424927,
+      0.020851964130997658,
+      0.0917917937040329,
+      -0.038357049226760864,
+      0.05106140300631523,
+      -0.03367459401488304,
+      0.05801103636622429,
+      0.0628814697265625,
+      0.024997225031256676,
+      0.015594316646456718,
+      -0.01490987278521061,
+      0.07070998847484589,
+      -0.039144083857536316,
+      0.0657331570982933,
+      -0.053744420409202576,
+      -0.01675831526517868,
+      -0.008745769970119,
+      -0.07664742320775986,
+      -0.06751038879156113,
+      0.0023392336443066597,
+      0.018902592360973358,
+      0.06754770874977112,
+      0.07430258393287659,
+      0.0806465670466423,
+      -0.031056180596351624,
+      0.06557579338550568,
+      0.06529161334037781,
+      -0.03742394223809242,
+      0.0007822285988368094,
+      0.10523669421672821,
+      0.0038901956286281347,
+      -0.014934191480278969,
+      0.0647430568933487,
+      0.03438747301697731,
+      -0.046527378261089325,
+      0.014247977174818516,
+      -0.020184241235256195,
+      0.016480082646012306,
+      -0.05491460859775543,
+      0.07232335209846497,
+      -0.016330908983945847,
+      0.011368552222847939,
+      -0.001964043825864792,
+      0.0009170984267257154,
+      0.019140562042593956,
+      -0.002758787479251623,
+      -0.05629577115178108,
+      1.7957766384702998e-33,
+      -0.10579885542392731,
+      0.08271613717079163,
+      0.03821941837668419,
+      0.06078806892037392,
+      0.017647448927164078,
+      -0.07404755055904388,
+      0.06083450838923454,
+      -0.07097837328910828,
+      -0.01949647255241871,
+      -0.005204185377806425,
+      0.0160058680921793,
+      -0.03624944016337395,
+      0.065463587641716,
+      -0.04834574833512306,
+      0.09314870834350586,
+      -0.022509299218654633,
+      -0.047614336013793945,
+      -0.042122796177864075,
+      0.0014064351562410593,
+      0.08215921372175217,
+      0.0144058121368289,
+      -0.08526691794395447,
+      -0.01885370910167694,
+      0.020506983622908592,
+      -0.0041589876636862755,
+      -0.0928102508187294,
+      0.0965222716331482,
+      0.05469893291592598,
+      0.002785224001854658,
+      0.006347258575260639,
+      -0.09394793212413788,
+      -0.08587668836116791,
+      0.00999284815043211,
+      -0.015109539031982422,
+      0.035454027354717255,
+      -0.08842843770980835,
+      -0.015698572620749474,
+      0.05549640208482742,
+      -0.011119373142719269,
+      0.012295924127101898,
+      0.007523554377257824,
+      -0.03497130423784256,
+      -0.05309790000319481,
+      -0.021819932386279106,
+      0.011010204441845417,
+      0.0778549313545227,
+      0.122015580534935,
+      0.0451776348054409,
+      -0.0894458070397377,
+      0.0031173918396234512,
+      -0.003828236600384116,
+      0.0010151821188628674,
+      0.0007775757694616914,
+      -0.0007406148943118751,
+      0.0005911831394769251,
+      0.029611686244606972,
+      -0.010095393285155296,
+      -0.015750357881188393,
+      -0.08871200680732727,
+      0.06563369184732437,
+      0.052563928067684174,
+      -0.02150006778538227,
+      0.032858334481716156,
+      -0.039781685918569565,
+      -0.02986454777419567,
+      -0.017254218459129333,
+      0.013349524699151516,
+      0.04903600737452507,
+      -0.102760910987854,
+      0.027411801740527153,
+      -0.007306735496968031,
+      0.03547230362892151,
+      0.03793823719024658,
+      -0.014224819839000702,
+      0.004229242447763681,
+      -0.04914051666855812,
+      -0.05566011741757393,
+      -0.08426816016435623,
+      0.0378078892827034,
+      -0.02177048847079277,
+      0.037800222635269165,
+      0.01145790982991457,
+      -0.03493969514966011,
+      -0.06417357921600342,
+      -0.04812582954764366,
+      -0.030254419893026352,
+      -0.12552082538604736,
+      0.0017056012293323874,
+      -0.053679559379816055,
+      0.019939688965678215,
+      -0.04766315221786499,
+      -0.143480584025383,
+      -0.024615854024887085,
+      0.06507551670074463,
+      0.01710103265941143,
+      -2.55080180279124e-33,
+      -0.01073896698653698,
+      0.08023330569267273,
+      0.028500312939286232,
+      -0.033364687114953995,
+      0.018465891480445862,
+      -0.018969086930155754,
+      0.116150863468647,
+      -0.04905116185545921,
+      0.0067994301207363605,
+      -0.051097989082336426,
+      -0.047208935022354126,
+      0.003005147911608219,
+      -0.006951641291379929,
+      0.0299075860530138,
+      0.023957515135407448,
+      0.005555577110499144,
+      -0.020836569368839264,
+      0.013542957603931427,
+      0.09286782890558243,
+      -0.04009733721613884,
+      0.05567550286650658,
+      0.01991306059062481,
+      -0.16575683653354645,
+      -0.003300475887954235,
+      0.11635366082191467,
+      0.008300523273646832,
+      -0.1112738847732544,
+      0.05307481065392494,
+      0.009467027150094509,
+      0.11263766884803772,
+      -0.04102758690714836,
+      -0.0505208782851696,
+      0.1890914887189865,
+      -0.01593983918428421,
+      0.011381726711988449,
+      0.01095605455338955,
+      -0.08038999140262604,
+      -0.012621873058378696,
+      -0.005316049326211214,
+      0.017261112108826637,
+      0.03283751755952835,
+      -0.04533768445253372,
+      0.03397509828209877,
+      0.04211656376719475,
+      0.024692395702004433,
+      -0.02541458234190941,
+      0.02313675545156002,
+      0.02338019199669361,
+      -0.011879520490765572,
+      -0.05438990890979767,
+      0.03806900233030319,
+      0.01261812262237072,
+      0.02512892708182335,
+      -0.0028746703173965216,
+      -0.016077643260359764,
+      -0.032072994858026505,
+      -0.006427581422030926,
+      0.01777057908475399,
+      0.02934812381863594,
+      -0.05759994685649872,
+      -2.871774631785229e-05,
+      -0.03137838840484619,
+      -0.06273766607046127,
+      0.04409930482506752,
+      -0.05993351340293884,
+      0.007546861190348864,
+      0.0053585791029036045,
+      0.042325496673583984,
+      -0.007369876839220524,
+      0.04489513114094734,
+      -0.12103329598903656,
+      0.017391694709658623,
+      0.0304956566542387,
+      -0.034047987312078476,
+      0.02484256401658058,
+      -0.06834809482097626,
+      0.06508748978376389,
+      0.08324999362230301,
+      -0.020252887159585953,
+      -0.014722783118486404,
+      0.02126440405845642,
+      -0.05160334333777428,
+      0.045947108417749405,
+      0.022960059344768524,
+      0.023375188931822777,
+      -0.060902271419763565,
+      -0.05150751397013664,
+      -0.1094929575920105,
+      -0.04899677261710167,
+      0.09132419526576996,
+      0.051848214119672775,
+      -0.0077659315429627895,
+      0.0012422297149896622,
+      0.058530740439891815,
+      0.040777210146188736,
+      -3.354356081786136e-08,
+      0.025891084223985672,
+      -0.04088461399078369,
+      -0.06885679066181183,
+      0.01951301097869873,
+      0.047974348068237305,
+      0.04472370818257332,
+      0.004657004959881306,
+      0.001041706302203238,
+      -0.02763887122273445,
+      0.03814717009663582,
+      0.03166148066520691,
+      0.0063626947812736034,
+      0.09577886760234833,
+      0.06234167888760567,
+      0.0010398400481790304,
+      0.010609040968120098,
+      0.020408503711223602,
+      0.05596008151769638,
+      0.00923844799399376,
+      0.011290326714515686,
+      0.02393697388470173,
+      -0.03378620743751526,
+      0.010788901709020138,
+      0.0072112190537154675,
+      -0.03552679717540741,
+      -0.10475718975067139,
+      0.003995304461568594,
+      -0.002284976886585355,
+      -0.014504319056868553,
+      -0.06887608021497726,
+      0.03398992121219635,
+      -0.005206231493502855,
+      0.049566611647605896,
+      0.00902023445814848,
+      0.06874160468578339,
+      0.014804325066506863,
+      -0.07230424880981445,
+      0.0428827665746212,
+      0.013657039031386375,
+      0.027973631396889687,
+      -0.035619381815195084,
+      0.06485525518655777,
+      -0.06238642707467079,
+      -0.012459578923881054,
+      0.020500177517533302,
+      -0.0715484470129013,
+      -0.16523504257202148,
+      0.013638298027217388,
+      0.07008316367864609,
+      0.026970835402607918,
+      0.004871702753007412,
+      -0.0012540861498564482,
+      -0.028708957135677338,
+      0.05812879279255867,
+      0.12611250579357147,
+      0.09877888858318329,
+      -0.04118988662958145,
+      -0.02214396744966507,
+      -0.10328112542629242,
+      0.029945021495223045,
+      0.004513312131166458,
+      0.011272193863987923,
+      0.03294430673122406,
+      -0.042709026485681534
+    ],
+    "model": "Hillcraft",
+    "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, -118.2887 33.9872, -118.2887 34.0972))",
+    "price": 1200,
+    "store_location": "-118.2437,34.0522"
+  },
+  {
+    "brand": "Nord",
+    "condition": "used",
+    "description": "The Chook Air 5  gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower  top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.",
+    "description_embeddings": [
+      -0.0018494834657758474,
+      0.057690851390361786,
+      0.038153987377882004,
+      0.06570218503475189,
+      0.028856752440333366,
+      -0.062333013862371445,
+      -0.014953209087252617,
+      0.046022992581129074,
+      -0.08184631168842316,
+      0.03648914024233818,
+      0.03869181126356125,
+      0.010564669035375118,
+      -0.020310621708631516,
+      -0.04062078520655632,
+      -0.0125962495803833,
+      0.14169928431510925,
+      0.03418859466910362,
+      -0.06641822308301926,
+      0.005633663386106491,
+      -0.09943458437919617,
+      0.023237863555550575,
+      -0.04369724169373512,
+      0.016574522480368614,
+      0.07258585095405579,
+      0.018674470484256744,
+      -0.05764088034629822,
+      -0.0795072391629219,
+      0.04034125804901123,
+      -0.036483921110630035,
+      -0.033106740564107895,
+      0.02980787307024002,
+      -0.0028512384742498398,
+      0.00786224752664566,
+      -0.03016488254070282,
+      -0.12349128723144531,
+      0.031072411686182022,
+      0.08362030982971191,
+      0.025227056816220284,
+      -0.030982907861471176,
+      -0.006486377213150263,
+      -0.023590318858623505,
+      -0.03374557942152023,
+      -0.04145599156618118,
+      -0.09421771764755249,
+      -0.0013142612297087908,
+      -0.003397064981982112,
+      -0.0031338112894445658,
+      -0.11464792490005493,
+      0.040542472153902054,
+      0.02896481193602085,
+      0.007327641360461712,
+      -0.06064218282699585,
+      0.049546848982572556,
+      -0.05917377769947052,
+      -0.01963184028863907,
+      -0.002139812568202615,
+      -0.14361988008022308,
+      -0.05401389300823212,
+      0.12506668269634247,
+      -0.07141950726509094,
+      -0.0032961040269583464,
+      0.015251584351062775,
+      -0.05507654324173927,
+      -0.009836667217314243,
+      -0.02802908606827259,
+      -0.01053905300796032,
+      -0.03239851072430611,
+      -0.10646941512823105,
+      0.03140658512711525,
+      0.028125958517193794,
+      -0.004000179003924131,
+      -0.0018343725241720676,
+      0.01727917790412903,
+      -0.013935663737356663,
+      -0.02435036562383175,
+      0.04411163926124573,
+      -0.009158330969512463,
+      -0.023309389129281044,
+      0.01229795441031456,
+      -0.04689493402838707,
+      -0.02138197235763073,
+      0.013063939288258553,
+      0.02832808345556259,
+      0.031972818076610565,
+      0.020882662385702133,
+      -0.015083174221217632,
+      0.002903456799685955,
+      -0.047304242849349976,
+      -0.10658963769674301,
+      -0.06274145841598511,
+      -0.030370736494660378,
+      0.0539257749915123,
+      0.00848578754812479,
+      0.02172423154115677,
+      -0.007691903971135616,
+      -0.10581931471824646,
+      0.06078812852501869,
+      0.007988635450601578,
+      -0.116583913564682,
+      0.05237157270312309,
+      0.024445366114377975,
+      -0.02832716703414917,
+      0.04004029557108879,
+      0.02844531089067459,
+      -0.0455174520611763,
+      -0.07379734516143799,
+      0.09453199058771133,
+      -0.011599121615290642,
+      -0.0027194281574338675,
+      0.014355232007801533,
+      -0.059690698981285095,
+      -0.012937567196786404,
+      0.034315045922994614,
+      -0.047598548233509064,
+      -0.03261064738035202,
+      0.05839747563004494,
+      -0.12691465020179749,
+      0.03778312727808952,
+      0.006131120957434177,
+      0.04806787148118019,
+      -0.03441976010799408,
+      0.08042734116315842,
+      0.008934197947382927,
+      0.027216315269470215,
+      0.0016972541343420744,
+      -0.10113930702209473,
+      -0.0003218930505681783,
+      9.75532108829862e-34,
+      -0.0537416972219944,
+      0.06875170767307281,
+      -0.01566525548696518,
+      0.01952524110674858,
+      -0.0005404680850915611,
+      -0.08984242379665375,
+      0.04537447541952133,
+      -0.1295408457517624,
+      7.603532867506146e-05,
+      0.040753066539764404,
+      0.016371281817555428,
+      0.029906686395406723,
+      -0.005372706335037947,
+      -0.06687828153371811,
+      0.11607439070940018,
+      0.016209086403250694,
+      -0.11238335072994232,
+      -0.057236358523368835,
+      -0.09619198739528656,
+      0.027146028354763985,
+      -0.09542766213417053,
+      -0.0360424630343914,
+      -0.09913153201341629,
+      0.04242968559265137,
+      0.05494842678308487,
+      -0.025294257327914238,
+      0.06307625770568848,
+      0.007745219860225916,
+      -0.019641151651740074,
+      0.07056662440299988,
+      -0.05425839126110077,
+      0.012385660782456398,
+      -0.006104010157287121,
+      -0.07186716794967651,
+      -0.10919132828712463,
+      -0.0017968777101486921,
+      0.010471112094819546,
+      -0.011221444234251976,
+      -0.035078633576631546,
+      0.009300827980041504,
+      0.0802159234881401,
+      -0.10042990744113922,
+      -0.01718892529606819,
+      0.05525779724121094,
+      -0.019847391173243523,
+      0.08405174314975739,
+      0.06403975188732147,
+      0.0627448782324791,
+      -0.07871688157320023,
+      0.0002676364383660257,
+      -0.05110163986682892,
+      0.006078454665839672,
+      -0.019803548231720924,
+      -0.014253707602620125,
+      0.039836447685956955,
+      0.021812820807099342,
+      0.00014793995069339871,
+      -0.006739508826285601,
+      -0.023166682571172714,
+      0.05429920181632042,
+      0.1337796151638031,
+      -0.01773720420897007,
+      0.024304436519742012,
+      -0.016411837190389633,
+      -0.07777299731969833,
+      0.042669400572776794,
+      0.06226645037531853,
+      -0.023803485557436943,
+      0.00396164134144783,
+      -0.049458179622888565,
+      -0.004774407483637333,
+      0.036529600620269775,
+      0.02002328634262085,
+      -0.02465248480439186,
+      -0.024076614528894424,
+      -0.03887653350830078,
+      0.057176534086465836,
+      -0.03888818621635437,
+      -0.027698175981640816,
+      0.001469603506848216,
+      0.04755152389407158,
+      -0.0938708707690239,
+      0.013991769403219223,
+      -0.026987746357917786,
+      -0.03277081623673439,
+      -0.04837489873170853,
+      0.02711542509496212,
+      -0.09185922145843506,
+      -0.03453921154141426,
+      0.03532274439930916,
+      -0.024472877383232117,
+      -0.09732313454151154,
+      0.008513586595654488,
+      0.03207352012395859,
+      0.030766190961003304,
+      -2.5405224720352836e-33,
+      0.08067575097084045,
+      -0.008160247467458248,
+      0.03101508319377899,
+      0.005022854544222355,
+      0.0010000152979046106,
+      0.06958161294460297,
+      0.10594375431537628,
+      -0.06177408620715141,
+      -0.01650322414934635,
+      0.03043895959854126,
+      -0.018333615735173225,
+      0.06436911225318909,
+      0.02439672127366066,
+      -0.01213911734521389,
+      0.09937868267297745,
+      0.02093592658638954,
+      -0.003140130080282688,
+      -0.016580305993556976,
+      0.10430759936571121,
+      -0.019038936123251915,
+      0.05574416741728783,
+      0.07455366849899292,
+      -0.029936065897345543,
+      0.0017704741330817342,
+      0.07022519409656525,
+      0.02108696848154068,
+      -0.050372399389743805,
+      0.027957700192928314,
+      0.005698348395526409,
+      0.03494682535529137,
+      -0.02610155940055847,
+      -0.0068950653076171875,
+      0.1346345692873001,
+      0.03947756066918373,
+      -0.05966781824827194,
+      -0.010489783249795437,
+      -0.04099087417125702,
+      -0.027186688035726547,
+      -0.04620056599378586,
+      -0.02478279173374176,
+      -0.007060651201754808,
+      -0.023514581844210625,
+      0.06082035228610039,
+      -0.05849218741059303,
+      -0.0036981222219765186,
+      0.04169313609600067,
+      0.09352244436740875,
+      0.020741308107972145,
+      -0.0019505824893712997,
+      -0.10489305853843689,
+      0.08789870887994766,
+      0.039999835193157196,
+      -0.014334832318127155,
+      0.008347390219569206,
+      0.03113699145615101,
+      0.10894497483968735,
+      0.027165820822119713,
+      0.0064145540818572044,
+      -0.00803150050342083,
+      -0.055484138429164886,
+      -0.03251631557941437,
+      0.02290980890393257,
+      0.04825572296977043,
+      0.01608354039490223,
+      -0.04969468340277672,
+      -0.004120110999792814,
+      -0.03278858959674835,
+      0.009696378372609615,
+      -0.04376300796866417,
+      -0.0009336083312518895,
+      -0.0313178189098835,
+      -0.05882050469517708,
+      0.057815250009298325,
+      -0.02050788328051567,
+      -0.024381538853049278,
+      0.06283771246671677,
+      0.06954411417245865,
+      0.09720556437969208,
+      -0.056403111666440964,
+      -0.04526498168706894,
+      -0.07259991019964218,
+      0.001844316371716559,
+      0.04090375825762749,
+      0.06941819936037064,
+      -0.041316937655210495,
+      0.002292247023433447,
+      -0.03425106778740883,
+      -0.0628972053527832,
+      0.0063382769003510475,
+      0.09668626636266708,
+      0.062228571623563766,
+      0.03658486530184746,
+      -0.08789398521184921,
+      0.0009696391643956304,
+      -0.004108588211238384,
+      -3.183854957455878e-08,
+      0.09346635639667511,
+      0.042943451553583145,
+      -0.0005091542261652648,
+      0.026524608954787254,
+      0.009858721867203712,
+      0.03737989068031311,
+      -0.014056878164410591,
+      0.038327474147081375,
+      -0.010239921510219574,
+      0.05757640674710274,
+      0.014411918818950653,
+      0.038742948323488235,
+      0.06475342065095901,
+      -0.0011537548853084445,
+      -0.017729472368955612,
+      0.05167960748076439,
+      0.024419916793704033,
+      0.08681508898735046,
+      0.015235288999974728,
+      0.036676522344350815,
+      -0.03421042487025261,
+      -0.019762659445405006,
+      0.09467294812202454,
+      -0.053133491426706314,
+      -0.04356615990400314,
+      -0.03919585049152374,
+      0.015591761097311974,
+      0.021372869610786438,
+      -0.0058142030611634254,
+      -0.022864477708935738,
+      -0.02901598811149597,
+      0.0458187572658062,
+      -0.030826766043901443,
+      -0.008975986391305923,
+      0.03365883231163025,
+      -0.010484383441507816,
+      -0.1445801854133606,
+      0.05828193947672844,
+      -0.028194306418299675,
+      0.05975533276796341,
+      0.014028828591108322,
+      0.0036430624313652515,
+      0.024983061477541924,
+      0.01454286277294159,
+      -0.006972659844905138,
+      0.037446193397045135,
+      -0.06949692964553833,
+      -0.09630566090345383,
+      0.03263894096016884,
+      0.048720087856054306,
+      -0.06886433064937592,
+      0.018142051994800568,
+      0.03894415125250816,
+      0.05843216925859451,
+      0.06860719621181488,
+      0.04971907287836075,
+      -0.025701280683279037,
+      -0.06293400377035141,
+      -0.05422094464302063,
+      0.01912975125014782,
+      0.009564549662172794,
+      0.055643752217292786,
+      -0.0027948219794780016,
+      0.0329461470246315
+    ],
+    "model": "Chook air 5",
+    "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, -87.6848 41.8231, -87.6848 41.9331))",
+    "price": 815,
+    "store_location": "-87.6298,41.8781"
+  },
+  {
+    "brand": "Eva",
+    "condition": "used",
+    "description": "The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It\u2019s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!",
+    "description_embeddings": [
+      0.035103436559438705,
+      0.02666405402123928,
+      -0.06364161521196365,
+      0.02551678754389286,
+      -0.005281867925077677,
+      -0.04041111096739769,
+      -0.020820949226617813,
+      -0.03485208749771118,
+      -0.07683732360601425,
+      0.021527189761400223,
+      0.007516156416386366,
+      -0.0034474905114620924,
+      0.030465560033917427,
+      -0.06572654843330383,
+      -0.020914506167173386,
+      0.032637566328048706,
+      0.0815017819404602,
+      -0.022764643654227257,
+      0.07385077327489853,
+      0.058004263788461685,
+      -0.03062591142952442,
+      -0.03927106410264969,
+      0.01410673838108778,
+      0.05480317771434784,
+      -0.05647570267319679,
+      -0.008725483901798725,
+      -0.05373937636613846,
+      0.03546926751732826,
+      -0.026455262675881386,
+      -0.09718386828899384,
+      -0.040623344480991364,
+      0.03622450679540634,
+      0.08971034735441208,
+      -0.038574062287807465,
+      -0.05105822533369064,
+      0.021020587533712387,
+      0.003312851767987013,
+      -0.008969941176474094,
+      -0.0568903312087059,
+      0.033502694219350815,
+      -0.0578635074198246,
+      -0.03721226751804352,
+      -0.036668986082077026,
+      0.0022641047835350037,
+      0.038053013384342194,
+      0.03350543603301048,
+      0.05258995667099953,
+      -0.007619654294103384,
+      -0.05145309492945671,
+      0.026286069303750992,
+      0.09238694608211517,
+      -0.06733417510986328,
+      0.05791507661342621,
+      -0.07623173296451569,
+      -0.0052175214514136314,
+      -0.04393303394317627,
+      -0.15249019861221313,
+      -0.019764462485909462,
+      -0.03331150859594345,
+      -0.11831824481487274,
+      0.05683637782931328,
+      0.01903577335178852,
+      -0.023615414276719093,
+      0.04120348393917084,
+      -0.05669170990586281,
+      0.002014430705457926,
+      0.009953595697879791,
+      0.008390115574002266,
+      -0.025982441380620003,
+      -0.05977262556552887,
+      0.0611012764275074,
+      0.010146236047148705,
+      -0.047625984996557236,
+      0.09061623364686966,
+      0.02551751770079136,
+      0.06041749566793442,
+      0.1113404780626297,
+      0.017673874273896217,
+      -0.05184035003185272,
+      0.023109234869480133,
+      -0.04408435523509979,
+      -0.09029985964298248,
+      0.06733208149671555,
+      0.049696795642375946,
+      0.05554559826850891,
+      0.01563390903174877,
+      -0.019527558237314224,
+      -0.014796985313296318,
+      0.003595865098759532,
+      0.012917418964207172,
+      -0.06858907639980316,
+      -0.008697138167917728,
+      0.04721860587596893,
+      0.03153855353593826,
+      -0.05275731533765793,
+      0.019923491403460503,
+      -0.009707989171147346,
+      -0.018920045346021652,
+      0.03773808106780052,
+      0.042773403227329254,
+      -0.03819388151168823,
+      0.04029008001089096,
+      0.060834936797618866,
+      0.07531940191984177,
+      -0.04695741459727287,
+      0.00488079572096467,
+      0.13518987596035004,
+      -0.014429144561290741,
+      0.0314130075275898,
+      0.04135369136929512,
+      -0.021350333467125893,
+      -0.01289785373955965,
+      0.032701168209314346,
+      -0.037027571350336075,
+      -0.018168980255723,
+      -0.07188623398542404,
+      -0.036501020193099976,
+      0.03303954005241394,
+      0.10676111280918121,
+      -0.03277475759387016,
+      -0.06245853006839752,
+      -0.011879103258252144,
+      0.0533418171107769,
+      -0.01446340698748827,
+      0.03530410677194595,
+      0.05820532143115997,
+      -0.0015766610158607364,
+      -8.477015062817539e-34,
+      -0.10837127268314362,
+      0.015582970343530178,
+      0.0182977095246315,
+      0.055213626474142075,
+      -0.03584470599889755,
+      0.02778925560414791,
+      0.08698058873414993,
+      -0.04999695345759392,
+      -0.0842076763510704,
+      -0.040448326617479324,
+      -0.06311061233282089,
+      0.02668358013033867,
+      0.04676111042499542,
+      -0.006417605560272932,
+      0.12788759171962738,
+      0.02197197638452053,
+      0.07035308331251144,
+      -0.04392284154891968,
+      -0.008788101375102997,
+      -0.001108768628910184,
+      0.07960236072540283,
+      -0.0019975434988737106,
+      -0.010499294847249985,
+      0.03599945828318596,
+      0.02329486794769764,
+      -0.01962442137300968,
+      0.15486162900924683,
+      0.027215363457798958,
+      -0.022036811336874962,
+      0.04227740690112114,
+      -0.07894206047058105,
+      -0.03180933743715286,
+      0.020022651180624962,
+      -0.05859709531068802,
+      -0.0009564562351442873,
+      0.009732870385050774,
+      -0.09007531404495239,
+      0.041766807436943054,
+      0.003291067900136113,
+      0.0950121283531189,
+      0.02693203091621399,
+      -0.01629827171564102,
+      -0.01884087361395359,
+      -0.029889501631259918,
+      -0.003714299062266946,
+      0.09247232973575592,
+      0.06151247024536133,
+      0.09120925515890121,
+      -0.011266379617154598,
+      0.03680066391825676,
+      -0.1628604233264923,
+      -0.008045083843171597,
+      -0.023876454681158066,
+      0.046465914696455,
+      0.01841152086853981,
+      0.01759498566389084,
+      -0.012628139927983284,
+      0.02123965509235859,
+      -0.07098977267742157,
+      0.025028834119439125,
+      -0.032171521335840225,
+      0.0538402758538723,
+      -0.00211805896833539,
+      -0.019548164680600166,
+      -0.046793293207883835,
+      0.04394293949007988,
+      0.022090770304203033,
+      -0.058215122669935226,
+      -0.05101722851395607,
+      0.014057288877665997,
+      -0.05860457569360733,
+      0.006166193168610334,
+      0.04641130194067955,
+      0.03165149688720703,
+      0.045289356261491776,
+      0.038580797612667084,
+      -0.0335664264857769,
+      0.013378577306866646,
+      0.058321163058280945,
+      -0.03730795532464981,
+      -0.01996016688644886,
+      0.025774186477065086,
+      0.001352976425550878,
+      0.023636724799871445,
+      0.026727410033345222,
+      -0.0730309784412384,
+      -0.01613243669271469,
+      -0.009525856003165245,
+      -0.03985843434929848,
+      -0.003976964857429266,
+      -0.006743384525179863,
+      -0.06324627995491028,
+      0.03959967568516731,
+      0.021898096427321434,
+      -0.004184887744486332,
+      -3.110083923319741e-34,
+      0.03726857528090477,
+      -0.016945818439126015,
+      0.05512943118810654,
+      0.0471712127327919,
+      0.097488172352314,
+      0.02456829510629177,
+      0.08276744186878204,
+      -0.02230781689286232,
+      -0.03785759210586548,
+      0.010082835331559181,
+      0.07151538133621216,
+      -0.04426267370581627,
+      0.03402319550514221,
+      0.05808456242084503,
+      0.07566764950752258,
+      0.07006501406431198,
+      0.002404613886028528,
+      -0.10078012198209763,
+      0.0029093879275023937,
+      -0.12370351701974869,
+      -0.03431423008441925,
+      0.08334372937679291,
+      0.001441179309040308,
+      -0.08001153916120529,
+      0.020076438784599304,
+      0.011177998036146164,
+      0.034560561180114746,
+      0.041609298437833786,
+      -0.04921843111515045,
+      0.03598331660032272,
+      -0.10671709477901459,
+      0.02969883382320404,
+      0.048317357897758484,
+      0.12375228852033615,
+      0.040479566901922226,
+      0.0330270379781723,
+      -0.06994733214378357,
+      0.008698385208845139,
+      0.03447363153100014,
+      -0.012882913462817669,
+      -0.02672695368528366,
+      -0.06423910707235336,
+      -0.0506032295525074,
+      0.0053747911006212234,
+      0.0682206079363823,
+      -0.005316274706274271,
+      0.027013784274458885,
+      -0.006269444711506367,
+      0.08528510481119156,
+      -0.0731915608048439,
+      0.0046186731196939945,
+      -0.024244142696261406,
+      0.0360478051006794,
+      0.020591434091329575,
+      0.04302601516246796,
+      -0.14392279088497162,
+      0.05873512104153633,
+      -0.010028650052845478,
+      -0.04548479616641998,
+      -0.013535127975046635,
+      0.01113649271428585,
+      0.028603754937648773,
+      -0.04036158323287964,
+      0.0704532265663147,
+      -0.09373150765895844,
+      -0.07198052108287811,
+      -0.002203285926952958,
+      -0.0855080857872963,
+      -0.08842256665229797,
+      -9.343179408460855e-05,
+      -0.029457710683345795,
+      -0.02197202481329441,
+      -0.048936717212200165,
+      0.06165122613310814,
+      -0.0626755878329277,
+      0.009602922946214676,
+      0.07251632958650589,
+      0.03113914094865322,
+      -0.0010769155342131853,
+      -0.040936876088380814,
+      -0.04328843951225281,
+      -0.020654935389757156,
+      0.08143945783376694,
+      0.03210373967885971,
+      0.03849901258945465,
+      0.08388370275497437,
+      -0.04882088676095009,
+      -0.04764509201049805,
+      -0.01594746671617031,
+      0.08005915582180023,
+      -0.0007682700525037944,
+      0.04356953501701355,
+      -0.08784972876310349,
+      -0.009003485552966595,
+      -0.012332507409155369,
+      -3.9398202034135466e-08,
+      0.006421600468456745,
+      0.04334808140993118,
+      0.0013437344459816813,
+      -0.009495558217167854,
+      -0.022657591849565506,
+      -0.012704327702522278,
+      0.018703341484069824,
+      -0.07545771449804306,
+      -0.07807240635156631,
+      0.0031038280576467514,
+      -0.023932253941893578,
+      0.040701914578676224,
+      0.10603249073028564,
+      0.03773405775427818,
+      0.05925403907895088,
+      -0.02083730883896351,
+      0.052759915590286255,
+      0.08195225894451141,
+      -0.057340435683727264,
+      -0.03538660705089569,
+      0.04492119327187538,
+      -0.07101765275001526,
+      0.02868317998945713,
+      -0.11165601015090942,
+      -0.03126508370041847,
+      -0.07953942567110062,
+      -0.022893071174621582,
+      -0.08839578926563263,
+      0.00727836275473237,
+      -0.08685773611068726,
+      0.026177138090133667,
+      0.028831886127591133,
+      0.03626682609319687,
+      -0.045381225645542145,
+      -0.013574030250310898,
+      0.028583087027072906,
+      -0.004301569424569607,
+      0.04326719045639038,
+      -0.023370176553726196,
+      0.057897310703992844,
+      -0.029195263981819153,
+      -0.001789534231647849,
+      0.032133687287569046,
+      0.003419605316594243,
+      0.028055502101778984,
+      -0.0038284522015601397,
+      -0.022835813462734222,
+      -0.07568305730819702,
+      0.017481982707977295,
+      0.013085611164569855,
+      0.009357997216284275,
+      -0.030347391963005066,
+      -0.0020967889577150345,
+      0.03544173017144203,
+      0.004640925209969282,
+      0.025202661752700806,
+      -0.0916307270526886,
+      -0.06341513246297836,
+      -0.017053432762622833,
+      0.06747056543827057,
+      0.0467485710978508,
+      -0.14364545047283173,
+      0.024641912430524826,
+      -0.04414863884449005
+    ],
+    "model": "Eva 291",
+    "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))",
+    "price": 3400,
+    "store_location": "-80.1918,25.7617"
+  },
+  {
+    "brand": "Noka Bikes",
+    "condition": "used",
+    "description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women\u2019s saddle, different bars and unique colourway.",
+    "description_embeddings": [
+      0.07420087605714798,
+      -0.01575474441051483,
+      0.010670951567590237,
+      0.07449527084827423,
+      0.0060751838609576225,
+      0.03211340680718422,
+      -0.008086569607257843,
+      0.025549469515681267,
+      -0.07154879719018936,
+      0.014914358966052532,
+      -0.030715830624103546,
+      -0.0064881909638643265,
+      -0.0071744490414857864,
+      -0.0495535172522068,
+      0.03856685757637024,
+      0.056132566183805466,
+      0.10794447362422943,
+      -0.010181701742112637,
+      0.0811777114868164,
+      0.10135157406330109,
+      -0.0769873708486557,
+      -0.019640743732452393,
+      -0.028702793642878532,
+      0.07654724270105362,
+      -0.07045057415962219,
+      -0.10104052722454071,
+      -0.004679638426750898,
+      -0.0027665267698466778,
+      0.030919065698981285,
+      -0.05461210012435913,
+      -0.020669877529144287,
+      0.031864751130342484,
+      0.0835040882229805,
+      -0.05930671840906143,
+      -0.006361816544085741,
+      -0.044132985174655914,
+      0.035945694893598557,
+      -0.0027891425415873528,
+      -0.07598478347063065,
+      -0.03105698712170124,
+      -0.059601303189992905,
+      -0.018435664474964142,
+      -0.03314891457557678,
+      0.06877987831830978,
+      -0.016064301133155823,
+      0.01545438077300787,
+      0.056307148188352585,
+      -0.08382677286863327,
+      -0.05274924635887146,
+      0.01139853335916996,
+      0.10750263929367065,
+      -0.004042057786136866,
+      0.003356053726747632,
+      -0.01083006989210844,
+      -0.06153054162859917,
+      -0.054990023374557495,
+      -0.1360272616147995,
+      0.025806479156017303,
+      0.043233320116996765,
+      -0.009591769427061081,
+      0.04422037675976753,
+      0.02436559833586216,
+      -0.0012137923622503877,
+      -0.015348327346146107,
+      0.02425999939441681,
+      -0.03409525752067566,
+      -0.08487176895141602,
+      0.017345253378152847,
+      0.035498712211847305,
+      -0.05615586042404175,
+      0.02745831571519375,
+      0.04017677903175354,
+      -0.0936250239610672,
+      0.048734042793512344,
+      0.024287866428494453,
+      -0.030274393036961555,
+      0.08800201117992401,
+      0.0879313051700592,
+      -0.0550895519554615,
+      -0.0026407642289996147,
+      -0.09552258998155594,
+      -0.05870680510997772,
+      0.09009534120559692,
+      0.05674433708190918,
+      0.05360940843820572,
+      -0.0023017164785414934,
+      0.005036584101617336,
+      -0.017987212166190147,
+      -0.05129106715321541,
+      -0.0012550759129226208,
+      -0.05818512290716171,
+      0.06725005060434341,
+      0.02540011890232563,
+      0.054589059203863144,
+      0.01723511517047882,
+      -0.04752320423722267,
+      0.10586173087358475,
+      0.05530202388763428,
+      -0.03287632390856743,
+      0.05258375406265259,
+      0.01655220240354538,
+      0.011849797330796719,
+      0.032285332679748535,
+      0.03760116547346115,
+      -0.017522728070616722,
+      -0.05457543954253197,
+      0.006817341782152653,
+      -0.028400525450706482,
+      -0.0027318967040628195,
+      -0.04200681298971176,
+      -0.026171568781137466,
+      -0.03272707015275955,
+      0.016305547207593918,
+      0.01545319240540266,
+      0.0056663742288947105,
+      -0.0977277085185051,
+      -0.0009383464348502457,
+      -0.005693590268492699,
+      0.039240892976522446,
+      0.01645808108150959,
+      -0.01950509287416935,
+      0.007762903813272715,
+      -0.004406424704939127,
+      0.016364052891731262,
+      -0.006125410553067923,
+      -0.029666034504771233,
+      0.044946398586034775,
+      1.8357034061219326e-33,
+      -0.053835414350032806,
+      0.03495992720127106,
+      -0.05977822467684746,
+      -0.009059705771505833,
+      0.004659880883991718,
+      0.022712793201208115,
+      0.04409413039684296,
+      -0.10094501823186874,
+      -0.08702167868614197,
+      0.0066354661248624325,
+      0.011878615245223045,
+      0.0033176038414239883,
+      -0.012380938045680523,
+      0.0010130798909813166,
+      0.0644172728061676,
+      -0.07130810618400574,
+      -0.0045928386971354485,
+      -0.03375661373138428,
+      -0.022520871832966805,
+      0.07504180818796158,
+      0.009861051104962826,
+      0.07191396504640579,
+      -0.014361198991537094,
+      0.01449025422334671,
+      -0.010663183405995369,
+      -0.028888769447803497,
+      0.09297508746385574,
+      0.04386264830827713,
+      -0.09579017758369446,
+      0.007718369830399752,
+      -0.11800525337457657,
+      -0.020133504644036293,
+      -0.0034684871789067984,
+      -0.03735410049557686,
+      0.006975250784307718,
+      -0.012902614660561085,
+      0.04201272875070572,
+      0.04273228347301483,
+      -0.07133302837610245,
+      0.09145014733076096,
+      0.0048939259722828865,
+      -0.058044496923685074,
+      -0.017591776326298714,
+      -0.03502552956342697,
+      -0.05678534135222435,
+      0.05471408739686012,
+      0.09568087011575699,
+      0.09486310184001923,
+      -0.045761823654174805,
+      0.018188882619142532,
+      -0.12331638485193253,
+      -0.023129422217607498,
+      -0.05455191433429718,
+      0.06732556223869324,
+      0.048286039382219315,
+      -0.0014048831071704626,
+      0.03308422118425369,
+      -0.009839850477874279,
+      -0.05661558359861374,
+      0.03290552645921707,
+      0.03661618381738663,
+      0.0047610136680305,
+      -0.05254451557993889,
+      0.006052205804735422,
+      -0.13153813779354095,
+      -0.006893055979162455,
+      0.037368785589933395,
+      -0.03058353252708912,
+      -0.0017269864911213517,
+      -0.008851475082337856,
+      -0.0740399956703186,
+      0.057353563606739044,
+      0.12296456098556519,
+      0.0413251556456089,
+      0.09911972284317017,
+      0.016817500814795494,
+      -0.048074446618556976,
+      0.05658562108874321,
+      -0.012700358405709267,
+      -0.08663205802440643,
+      0.0014393541496247053,
+      0.00613820506259799,
+      -0.04446813836693764,
+      0.04673916846513748,
+      -0.02757285162806511,
+      -0.05133393779397011,
+      -0.028082016855478287,
+      -0.04668722674250603,
+      -0.03849129378795624,
+      -0.047927819192409515,
+      -0.06503037363290787,
+      -0.08361010253429413,
+      0.0393000952899456,
+      0.04658354073762894,
+      0.009724845178425312,
+      -2.0251938301584547e-33,
+      0.07265108078718185,
+      -0.04307040572166443,
+      0.04996086284518242,
+      0.03520473092794418,
+      0.1084771379828453,
+      0.005833788774907589,
+      0.046216338872909546,
+      -0.04042917117476463,
+      -0.04179416596889496,
+      0.03598152473568916,
+      0.04082870855927467,
+      -0.10612837225198746,
+      -0.010728210210800171,
+      0.03610331192612648,
+      0.027484672144055367,
+      -0.008130725473165512,
+      0.026157474145293236,
+      -0.07420457899570465,
+      0.025218356400728226,
+      -0.0898737907409668,
+      -0.02742879092693329,
+      0.03535477817058563,
+      -0.062408916652202606,
+      -0.1569257229566574,
+      -0.018789052963256836,
+      0.04845775291323662,
+      -0.015149657614529133,
+      -0.0018356313230469823,
+      -0.04590461030602455,
+      0.023325689136981964,
+      -0.13069787621498108,
+      -0.01994006149470806,
+      -0.021577563136816025,
+      -0.0231519415974617,
+      -0.002478161361068487,
+      0.022444186732172966,
+      -0.04681165888905525,
+      0.02769998274743557,
+      0.003438781714066863,
+      0.03829822316765785,
+      0.0243418887257576,
+      -0.020368436351418495,
+      -0.005010040942579508,
+      0.04648580029606819,
+      0.061260052025318146,
+      0.03784928843379021,
+      0.008547516539692879,
+      0.027585946023464203,
+      0.043608926236629486,
+      0.047329846769571304,
+      0.10433284938335419,
+      0.0069401939399540424,
+      0.043503858149051666,
+      0.06499945372343063,
+      -0.01040438748896122,
+      -0.11596397310495377,
+      0.06451629102230072,
+      0.032956890761852264,
+      0.0007023254293017089,
+      0.0898808017373085,
+      -0.055529963225126266,
+      -0.006572310347110033,
+      -0.06089714914560318,
+      0.022685443982481956,
+      -0.0013110691215842962,
+      -0.09440209716558456,
+      -0.03121936321258545,
+      -0.003408250631764531,
+      -0.08051110059022903,
+      0.01802976056933403,
+      0.032015636563301086,
+      -9.986010991269723e-05,
+      -0.011806532740592957,
+      0.06818002462387085,
+      0.028290249407291412,
+      -0.005312844179570675,
+      0.06016719341278076,
+      0.014397598803043365,
+      0.07122596353292465,
+      0.007864296436309814,
+      0.03002011403441429,
+      -0.05413847416639328,
+      0.09744291007518768,
+      -0.01965983398258686,
+      0.04150940105319023,
+      0.14307892322540283,
+      -0.11320102959871292,
+      0.03503091260790825,
+      0.003515399992465973,
+      -0.023677969351410866,
+      0.03955819085240364,
+      0.0534457303583622,
+      0.01904974691569805,
+      0.031267836689949036,
+      -0.018026702105998993,
+      -3.930426117904062e-08,
+      0.0003057526773773134,
+      0.013398992829024792,
+      -0.10113491117954254,
+      -0.016498351469635963,
+      -0.00534934364259243,
+      0.017148228362202644,
+      -0.03217773512005806,
+      -0.030934104695916176,
+      -0.06334194540977478,
+      0.07242853194475174,
+      -0.0047585368156433105,
+      -0.01996637135744095,
+      -0.0016463182400912046,
+      0.01777566224336624,
+      -0.04115547239780426,
+      0.02906038798391819,
+      0.10273877531290054,
+      0.022776247933506966,
+      0.030065912753343582,
+      -0.014570299535989761,
+      0.047994960099458694,
+      -0.044575825333595276,
+      0.025753378868103027,
+      -0.002644166350364685,
+      -0.09429919719696045,
+      -0.11280428618192673,
+      0.021306486800312996,
+      0.010426397435367107,
+      0.021093064919114113,
+      -0.00363176385872066,
+      -0.024409662932157516,
+      0.11473581194877625,
+      0.0020395806059241295,
+      -0.08371622860431671,
+      -0.03764123469591141,
+      0.039440982043743134,
+      -0.022172994911670685,
+      0.0410802997648716,
+      -0.05086303874850273,
+      0.053952641785144806,
+      -0.06084167957305908,
+      -0.036132071167230606,
+      0.02080748789012432,
+      -0.043120622634887695,
+      0.04048219695687294,
+      0.05917482078075409,
+      0.04085611552000046,
+      -0.05118294060230255,
+      0.015727631747722626,
+      0.01915580965578556,
+      0.03531794995069504,
+      -0.058007121086120605,
+      0.08498086780309677,
+      0.0036822939291596413,
+      0.018125539645552635,
+      0.10145474225282669,
+      -0.05804500728845596,
+      -0.008451723493635654,
+      -0.029611116275191307,
+      0.008798911236226559,
+      -0.03220565244555473,
+      -0.05617227777838707,
+      0.03482669219374657,
+      -0.03508705273270607
+    ],
+    "model": "Kahuna",
+    "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, -122.4644 37.7099, -122.4644 37.8199))",
+    "price": 3200,
+    "store_location": "-122.4194,37.7749"
+  },
+  {
+    "brand": "Breakout",
+    "condition": "new",
+    "description": "The XBN 2.1 Alloy is our entry-level road bike \u2013 but that\u2019s not to say that it\u2019s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance.",
+    "description_embeddings": [
+      -0.03482655808329582,
+      -0.024290302768349648,
+      0.029588153585791588,
+      -0.04068901762366295,
+      0.006173150148242712,
+      -0.06071849912405014,
+      0.021348219364881516,
+      0.032668184489011765,
+      -0.12000397592782974,
+      -0.026135660707950592,
+      -0.0262606181204319,
+      -0.014927615411579609,
+      0.06986956298351288,
+      -0.006424048915505409,
+      -0.002834598533809185,
+      -0.005902229342609644,
+      0.07471967488527298,
+      -0.10197333246469498,
+      0.08836513012647629,
+      0.0504477359354496,
+      0.005102860741317272,
+      0.0359189435839653,
+      -0.0477643720805645,
+      0.02106260135769844,
+      0.030146656557917595,
+      0.04048784077167511,
+      -0.023416390642523766,
+      -0.0016326266340911388,
+      0.039820387959480286,
+      -0.04475116729736328,
+      -0.03422572463750839,
+      0.09266746789216995,
+      -0.04106999188661575,
+      -0.02442021667957306,
+      -0.006325263995677233,
+      -0.057226430624723434,
+      0.12074039876461029,
+      0.012658749707043171,
+      -0.09748225659132004,
+      -0.048439353704452515,
+      -0.004550903104245663,
+      -0.014449959620833397,
+      0.03684601932764053,
+      0.018772389739751816,
+      0.09577438980340958,
+      0.04358510673046112,
+      0.06987427175045013,
+      -0.0561334528028965,
+      -0.03684744983911514,
+      0.02584502473473549,
+      0.039536770433187485,
+      -0.061133887618780136,
+      0.05351594462990761,
+      -0.025124184787273407,
+      0.09431525319814682,
+      0.05062168091535568,
+      -0.11715397238731384,
+      0.044368330389261246,
+      -0.024262988939881325,
+      -0.024561986327171326,
+      0.036067184060811996,
+      0.025171849876642227,
+      0.033648427575826645,
+      0.046933531761169434,
+      0.09548858553171158,
+      -0.06253167241811752,
+      -0.045195333659648895,
+      -0.04183578118681908,
+      -0.040385909378528595,
+      -0.027590686455368996,
+      0.08240272104740143,
+      -0.04743463918566704,
+      0.004482025280594826,
+      0.03219064325094223,
+      0.0007497097249142826,
+      -0.04202196002006531,
+      0.10904812812805176,
+      -0.06721051037311554,
+      -0.023717032745480537,
+      -0.01718866638839245,
+      -0.11568725854158401,
+      0.038704562932252884,
+      0.04929516091942787,
+      -0.03311185538768768,
+      -0.0017465045675635338,
+      -0.06153444945812225,
+      0.019559966400265694,
+      -0.010708335787057877,
+      -0.06623212993144989,
+      0.03114122338593006,
+      0.03581896796822548,
+      0.05368824675679207,
+      0.016093581914901733,
+      -0.0077278027310967445,
+      0.025581376627087593,
+      0.02375067211687565,
+      -0.024797597900032997,
+      0.08371419459581375,
+      -0.023117447271943092,
+      0.04535554349422455,
+      -0.007224411237984896,
+      0.06131899356842041,
+      0.02283627539873123,
+      -0.052097078412771225,
+      -0.07922855019569397,
+      0.0016958570340648293,
+      0.051785532385110855,
+      0.06701217591762543,
+      -0.02913537621498108,
+      0.020526740700006485,
+      0.03941992297768593,
+      -0.02217993699014187,
+      -0.08051256835460663,
+      -0.10155311226844788,
+      -0.05453299731016159,
+      -0.06470758467912674,
+      0.002029003808274865,
+      -0.013401892967522144,
+      -0.011006158776581287,
+      0.045922696590423584,
+      -0.040529411286115646,
+      0.00991761963814497,
+      -0.06682797521352768,
+      0.05892198532819748,
+      -0.028617633506655693,
+      -0.06339140236377716,
+      0.0044001322239637375,
+      -4.362402064591546e-33,
+      -0.10147389024496078,
+      0.039431530982255936,
+      -0.04599899426102638,
+      -0.049156975001096725,
+      -0.02249804697930813,
+      -0.026725362986326218,
+      0.04369090870022774,
+      -0.0005211823736317456,
+      -0.030739039182662964,
+      -0.0065231663174927235,
+      0.025333819910883904,
+      0.10470504313707352,
+      0.021332278847694397,
+      0.05556178465485573,
+      0.035125020891427994,
+      -0.1538446545600891,
+      0.007054235320538282,
+      -0.036041803658008575,
+      0.05932909622788429,
+      0.0337519533932209,
+      0.014841740019619465,
+      0.030220910906791687,
+      0.04296395927667618,
+      0.02538421005010605,
+      -0.006869863253086805,
+      -0.0031259972602128983,
+      0.11390665918588638,
+      -0.046557214111089706,
+      0.00429841224104166,
+      0.0428440123796463,
+      -0.1384042203426361,
+      0.04695465788245201,
+      -0.0574827715754509,
+      -0.030760489404201508,
+      -0.07478123158216476,
+      -0.029447706416249275,
+      -0.06204935908317566,
+      -0.03058161959052086,
+      -0.02502700313925743,
+      -0.019530421122908592,
+      -0.02094511315226555,
+      -0.03218250721693039,
+      -0.05274674668908119,
+      -0.02680223248898983,
+      -0.01955571211874485,
+      0.018619371578097343,
+      0.007585515268146992,
+      0.07155954837799072,
+      -0.018960801884531975,
+      -0.05782055854797363,
+      0.017819860950112343,
+      0.04109930992126465,
+      -0.03562675416469574,
+      0.016705146059393883,
+      0.0415029413998127,
+      0.05103381723165512,
+      0.035572972148656845,
+      -0.015020242892205715,
+      -0.03179502114653587,
+      0.0891512930393219,
+      0.007475084625184536,
+      0.042234599590301514,
+      -0.0542532242834568,
+      0.05480321869254112,
+      -0.10602187365293503,
+      0.054763536900281906,
+      0.04739809036254883,
+      -0.03327919542789459,
+      0.0082072913646698,
+      -0.0650608167052269,
+      -0.08271145075559616,
+      -0.10922762006521225,
+      0.03824940696358681,
+      0.05978637561202049,
+      0.10005365312099457,
+      0.029007570818066597,
+      -0.034110404551029205,
+      0.02545103058218956,
+      0.05190473049879074,
+      -0.003596875350922346,
+      -0.08473207801580429,
+      0.012021052651107311,
+      0.018175840377807617,
+      0.017920689657330513,
+      -0.03348258137702942,
+      0.04048585146665573,
+      0.030843179672956467,
+      0.019531769677996635,
+      -0.00910205114632845,
+      0.05721440538764,
+      0.007200753781944513,
+      -0.11394353210926056,
+      0.03483451530337334,
+      0.029621547088027,
+      -0.03755185008049011,
+      9.393710556981876e-34,
+      -0.009642334654927254,
+      -0.001342272269539535,
+      0.01180787943303585,
+      0.026348579674959183,
+      -0.011158440262079239,
+      -0.0019478596514090896,
+      0.028490403667092323,
+      -0.052807874977588654,
+      -0.029139718040823936,
+      0.08380713313817978,
+      0.09657946228981018,
+      0.00991911068558693,
+      0.03502354025840759,
+      -0.016754556447267532,
+      0.017460109665989876,
+      -0.01642783358693123,
+      0.022211389616131783,
+      -0.008778599090874195,
+      3.654864485724829e-05,
+      -0.09229966253042221,
+      0.04083291441202164,
+      0.08299239724874496,
+      -0.04151370748877525,
+      -0.05360836163163185,
+      -0.07950830459594727,
+      0.03959093242883682,
+      -0.13340364396572113,
+      0.054901909083127975,
+      0.05736061558127403,
+      0.04770279303193092,
+      -0.11388063430786133,
+      0.0237369854003191,
+      0.02305987849831581,
+      -0.013226242735981941,
+      -0.020716888830065727,
+      0.03368164971470833,
+      -0.002396275522187352,
+      -0.012604329735040665,
+      -0.02715742215514183,
+      0.0019254887010902166,
+      -0.00887741707265377,
+      -0.028728270903229713,
+      -0.05864499509334564,
+      0.10937061905860901,
+      -0.004027256276458502,
+      0.019197454676032066,
+      -0.03928225114941597,
+      -0.018220100551843643,
+      0.02466396614909172,
+      0.00980517640709877,
+      0.05513712763786316,
+      0.10625418275594711,
+      0.08366440236568451,
+      -0.02691512741148472,
+      -0.026599230244755745,
+      -0.049529410898685455,
+      -0.05376818776130676,
+      -0.003328752936795354,
+      -0.04345694184303284,
+      0.11166279017925262,
+      0.0407760851085186,
+      -0.00792837142944336,
+      0.03168283402919769,
+      -0.0018123856279999018,
+      0.0357111431658268,
+      -0.1368872970342636,
+      0.00755245191976428,
+      -0.010281031019985676,
+      -0.07121222466230392,
+      0.03101137839257717,
+      0.0662078782916069,
+      0.007085337769240141,
+      -0.007063422352075577,
+      -0.05238816514611244,
+      -0.004185437690466642,
+      0.00880429521203041,
+      0.05528423562645912,
+      -0.08964741230010986,
+      0.0042182342149317265,
+      -0.029020531103014946,
+      0.014597366563975811,
+      0.0002752764557953924,
+      0.032267484813928604,
+      0.11250412464141846,
+      0.002768452512100339,
+      -0.026546282693743706,
+      0.017761094495654106,
+      0.023686127737164497,
+      -0.012699384242296219,
+      0.018606871366500854,
+      -0.009665136225521564,
+      -0.06902427226305008,
+      -0.04932688549160957,
+      0.051991984248161316,
+      -0.05918996408581734,
+      -4.18107468647122e-08,
+      -0.06375139951705933,
+      -0.0029491656459867954,
+      0.032401617616415024,
+      -0.024946069344878197,
+      -0.026347795501351357,
+      0.020192604511976242,
+      -0.025759287178516388,
+      0.037004951387643814,
+      0.043629322201013565,
+      0.06278910487890244,
+      0.02097572200000286,
+      -0.07064115256071091,
+      -0.005097216460853815,
+      0.016486743465065956,
+      -0.0032796994782984257,
+      0.07957274466753006,
+      0.03776371479034424,
+      0.04259084165096283,
+      -0.005312138702720404,
+      -0.11992045491933823,
+      0.0859612300992012,
+      -0.0500827394425869,
+      0.02553708106279373,
+      -0.052115052938461304,
+      -0.07368568331003189,
+      -0.11394031345844269,
+      -0.04513951390981674,
+      0.022121727466583252,
+      -0.010451988317072392,
+      0.003997989930212498,
+      -0.13713733851909637,
+      0.030619636178016663,
+      0.05786789208650589,
+      0.05840904265642166,
+      0.04732450470328331,
+      -0.012547117657959461,
+      -0.027409126982092857,
+      0.049672048538923264,
+      -0.00018399256805423647,
+      -0.004273589234799147,
+      -0.011101006530225277,
+      0.017240682616829872,
+      -0.002812016289681196,
+      0.034935809671878815,
+      0.006601597648113966,
+      0.014211715199053288,
+      -0.06332860141992569,
+      -0.05397455021739006,
+      -0.038572490215301514,
+      -0.06450864672660828,
+      0.07963147014379501,
+      0.029951799660921097,
+      0.03153093531727791,
+      -0.03852103650569916,
+      -0.025755248963832855,
+      0.13077980279922485,
+      -0.08185151219367981,
+      -0.09442253410816193,
+      0.01002975832670927,
+      0.02509395219385624,
+      -0.029981056228280067,
+      -0.025097224861383438,
+      0.04001101851463318,
+      0.006130080670118332
+    ],
+    "model": "XBN 2.1 Alloy",
+    "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, -0.1778 51.4024, -0.1778 51.5524))",
+    "price": 810,
+    "store_location": "-0.1278,51.5074"
+  },
+  {
+    "brand": "ScramBikes",
+    "condition": "new",
+    "description": "The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\u2019s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.",
+    "description_embeddings": [
+      -0.006927311420440674,
+      0.15900678932666779,
+      -0.005495064426213503,
+      0.06652409583330154,
+      0.027506737038493156,
+      0.03928178921341896,
+      0.0212758406996727,
+      0.08142763376235962,
+      -0.004556896630674601,
+      0.04261007905006409,
+      0.0852041020989418,
+      -0.048658017069101334,
+      0.04896014556288719,
+      0.008289206773042679,
+      0.09533551335334778,
+      0.042673129588365555,
+      0.11990982294082642,
+      -0.09381455183029175,
+      -0.001154645229689777,
+      -0.009989846497774124,
+      0.042972203344106674,
+      0.055328626185655594,
+      0.044754382222890854,
+      0.01764347031712532,
+      0.04503790661692619,
+      -0.003830699948593974,
+      -0.03431522846221924,
+      0.03080279380083084,
+      -0.07722456008195877,
+      -0.09261710941791534,
+      0.04810558632016182,
+      0.04917953535914421,
+      0.027227703481912613,
+      -0.04199009761214256,
+      -0.11837311834096909,
+      0.028090154752135277,
+      0.08678824454545975,
+      -0.052036624401807785,
+      -0.06585966050624847,
+      -0.04037070646882057,
+      -0.08040877431631088,
+      0.0018185328226536512,
+      0.043989114463329315,
+      -0.011817573569715023,
+      0.006730342749506235,
+      -0.011594205163419247,
+      0.008361537009477615,
+      -0.054372794926166534,
+      0.033345889300107956,
+      -0.03933562710881233,
+      0.10115987062454224,
+      -0.08934503048658371,
+      0.04519934952259064,
+      0.03582148626446724,
+      -0.021424520760774612,
+      -0.07357319444417953,
+      -0.07426925748586655,
+      0.018053170293569565,
+      0.04639451205730438,
+      -0.07499486207962036,
+      0.057791586965322495,
+      -0.05478687211871147,
+      0.030340412631630898,
+      -0.0056327772326767445,
+      -0.0452428013086319,
+      -0.044913534075021744,
+      0.0526629202067852,
+      -0.08259481936693192,
+      -0.05616062134504318,
+      0.0005400424124673009,
+      -0.04933836683630943,
+      1.7464293705415912e-05,
+      -0.007984945550560951,
+      0.009467384777963161,
+      -0.04055757448077202,
+      -0.0056908270344138145,
+      0.036597516387701035,
+      0.023873118683695793,
+      0.00418807053938508,
+      0.04676923528313637,
+      -0.053795333951711655,
+      -0.015527212992310524,
+      -0.080276258289814,
+      0.026996882632374763,
+      0.1144171804189682,
+      -0.011178486980497837,
+      0.06854862719774246,
+      0.022201502695679665,
+      -0.01913747377693653,
+      0.009713435545563698,
+      0.01423699501901865,
+      0.04772024229168892,
+      0.034634001553058624,
+      0.0756441131234169,
+      -0.01204933412373066,
+      -0.031088251620531082,
+      -0.02462930977344513,
+      -0.04386857897043228,
+      -0.07706759870052338,
+      -0.002071107504889369,
+      -0.019828464835882187,
+      0.10314348340034485,
+      0.07235224545001984,
+      0.03167572245001793,
+      -0.02631053328514099,
+      -0.036559153348207474,
+      0.012040914967656136,
+      0.08063311874866486,
+      0.008269569836556911,
+      -0.012434666976332664,
+      0.00650354428216815,
+      -0.05601133033633232,
+      -0.028117282316088676,
+      -0.025071244686841965,
+      -0.01907099038362503,
+      0.003760937135666609,
+      -0.052525606006383896,
+      0.07173370569944382,
+      0.09372185170650482,
+      0.1006019189953804,
+      0.023999499157071114,
+      0.041847433894872665,
+      0.02114962227642536,
+      0.05726422369480133,
+      0.008218048140406609,
+      -0.06967302411794662,
+      0.01968124881386757,
+      1.844028477519647e-33,
+      -0.031731415539979935,
+      0.03648626059293747,
+      -0.026035945862531662,
+      -0.026796391233801842,
+      0.009511047042906284,
+      0.050696976482868195,
+      -0.03008556365966797,
+      -0.027142174541950226,
+      -0.07182826101779938,
+      -0.02520623430609703,
+      -0.005983793176710606,
+      0.07632830739021301,
+      0.09301365166902542,
+      0.0744069442152977,
+      0.10173527896404266,
+      -0.08317957073450089,
+      -0.06219454109668732,
+      -0.0776224136352539,
+      0.013563201762735844,
+      0.005839265417307615,
+      0.011761599220335484,
+      -0.0701083242893219,
+      0.005647188518196344,
+      -0.007439272943884134,
+      -4.4855809392174706e-05,
+      -0.05095696449279785,
+      0.13620494306087494,
+      0.017342044040560722,
+      -0.01352923084050417,
+      0.03086051531136036,
+      -0.0740780308842659,
+      -0.06721310317516327,
+      -0.06769067794084549,
+      0.01185466069728136,
+      -0.07234086841344833,
+      0.0020215404219925404,
+      -0.0108563881367445,
+      0.007788154762238264,
+      0.07912690192461014,
+      -0.02952556498348713,
+      -0.010350959375500679,
+      0.025074176490306854,
+      -0.050776757299900055,
+      0.00947178341448307,
+      0.03259417414665222,
+      0.0319671556353569,
+      0.0379379540681839,
+      0.05242982879281044,
+      -0.07516643404960632,
+      0.020074544474482536,
+      -0.08712191134691238,
+      -0.01204316969960928,
+      -0.006554455496370792,
+      -0.005875407252460718,
+      -0.06922540068626404,
+      0.041081465780735016,
+      -0.003257623640820384,
+      0.0391659140586853,
+      -0.03731334209442139,
+      -0.03579063341021538,
+      -0.04008869081735611,
+      0.05648549646139145,
+      -0.01142149232327938,
+      -0.05515728518366814,
+      -0.06406072527170181,
+      -0.013155528344213963,
+      0.0029902313835918903,
+      -0.020829875022172928,
+      -0.13008210062980652,
+      -0.039412979036569595,
+      0.05974568799138069,
+      -0.06617670506238937,
+      0.09968123584985733,
+      -0.04378099367022514,
+      0.008272947743535042,
+      -0.007245620246976614,
+      -0.05228377878665924,
+      -0.05929296463727951,
+      -0.07816570997238159,
+      0.04665905237197876,
+      -0.01630396395921707,
+      -0.020880484953522682,
+      -0.06532212346792221,
+      -0.003213261254131794,
+      0.0416768379509449,
+      -0.05093088001012802,
+      -0.0871049091219902,
+      -0.014571009203791618,
+      0.0010796786518767476,
+      0.007087667006999254,
+      0.05550219491124153,
+      -0.07753361761569977,
+      0.022848013788461685,
+      0.02596968039870262,
+      0.04259824752807617,
+      -2.380165022444961e-33,
+      0.05820159241557121,
+      0.046025633811950684,
+      0.10894608497619629,
+      0.05244075506925583,
+      0.15792003273963928,
+      0.024086199700832367,
+      0.029237573966383934,
+      -0.023072607815265656,
+      -0.07495220005512238,
+      -0.013094011694192886,
+      -0.004260784015059471,
+      -0.053080882877111435,
+      -0.00895928218960762,
+      -0.03149857744574547,
+      0.13309381902217865,
+      0.038643430918455124,
+      -0.02136029675602913,
+      -0.030247559770941734,
+      0.08822915703058243,
+      -0.026076463982462883,
+      0.050446517765522,
+      0.011856893077492714,
+      -0.06957101821899414,
+      -0.02773614600300789,
+      0.04625667631626129,
+      0.008786994963884354,
+      -0.07701855897903442,
+      0.001076446962542832,
+      -0.004858669359236956,
+      0.0067254831083118916,
+      -0.02791707031428814,
+      -0.019495468586683273,
+      0.0703381896018982,
+      0.030558260157704353,
+      -0.05546342208981514,
+      0.0420801043510437,
+      -0.03730127215385437,
+      -0.01322201732546091,
+      0.038336288183927536,
+      0.06427455693483353,
+      0.028238974511623383,
+      -0.06713984161615372,
+      0.03150900453329086,
+      0.024600861594080925,
+      0.016221575438976288,
+      0.025838203728199005,
+      -0.02313261851668358,
+      0.0557485967874527,
+      0.015211678110063076,
+      0.029895616695284843,
+      0.06008606031537056,
+      0.04473312199115753,
+      -0.050083715468645096,
+      -0.017571061849594116,
+      -0.014091495424509048,
+      -0.06641074270009995,
+      -0.008071008138358593,
+      0.03312089666724205,
+      -0.09482058882713318,
+      -0.08763624727725983,
+      0.030528157949447632,
+      0.013274747878313065,
+      0.06158939003944397,
+      0.0748405009508133,
+      -0.12128522247076035,
+      -0.12076683342456818,
+      0.0464591421186924,
+      0.0045278542675077915,
+      -0.002574144396930933,
+      -0.028266169130802155,
+      -0.04953430965542793,
+      -0.007604515179991722,
+      -0.01084962673485279,
+      -0.06370151042938232,
+      0.00812828354537487,
+      -0.027805447578430176,
+      0.044624149799346924,
+      0.016610005870461464,
+      -0.027103105559945107,
+      -0.046117331832647324,
+      -0.002852817066013813,
+      0.05479831248521805,
+      0.05342277139425278,
+      -0.03752776235342026,
+      -0.0053502353839576244,
+      -0.018950853496789932,
+      -0.07712738960981369,
+      -0.13029317557811737,
+      -0.013171681202948093,
+      0.02069253847002983,
+      0.029317859560251236,
+      0.018668048083782196,
+      -0.061371881514787674,
+      0.05376929044723511,
+      0.01483837515115738,
+      -3.964297690117746e-08,
+      0.03559036925435066,
+      -0.005289694294333458,
+      -0.013635152950882912,
+      -0.020672710612416267,
+      0.039649732410907745,
+      -0.06206038221716881,
+      0.04933065176010132,
+      0.010378899984061718,
+      0.020493512973189354,
+      0.027486076578497887,
+      0.07419533282518387,
+      -0.03473059833049774,
+      0.05251400172710419,
+      0.013588557951152325,
+      0.01814397983253002,
+      -0.024619124829769135,
+      0.11751414835453033,
+      0.05483951419591904,
+      0.022471528500318527,
+      0.0301313828676939,
+      0.018649602308869362,
+      -0.06219587102532387,
+      0.04238469526171684,
+      -0.053293377161026,
+      0.002427657600492239,
+      -0.0019911346025764942,
+      0.004944113548845053,
+      -0.061433035880327225,
+      -0.013191037811338902,
+      -0.030179856345057487,
+      -0.051340680569410324,
+      0.022094713523983955,
+      -0.035341646522283554,
+      -0.007219062652438879,
+      -0.06182244420051575,
+      -0.06059279292821884,
+      -0.041057273745536804,
+      0.002089729532599449,
+      -0.043397895991802216,
+      0.07170896232128143,
+      0.036471735686063766,
+      -0.015439609996974468,
+      -0.01848314143717289,
+      0.0056609525345265865,
+      -0.0012752920156344771,
+      -0.056571539491415024,
+      -0.01979043148458004,
+      -0.12011151015758514,
+      -0.02223217859864235,
+      0.029830070212483406,
+      0.027551136910915375,
+      -0.0010704555315896869,
+      0.000702365068718791,
+      -0.005173679441213608,
+      -0.019360698759555817,
+      0.17009279131889343,
+      -0.04601005092263222,
+      -0.04016156122088432,
+      -0.01600208878517151,
+      0.04198170080780983,
+      0.02032368630170822,
+      -0.06263051927089691,
+      -0.048464443534612656,
+      0.009351130574941635
+    ],
+    "model": "WattBike",
+    "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, 2.1767 48.5516, 2.1767 48.9016))",
+    "price": 2300,
+    "store_location": "2.3522,48.8566"
+  },
+  {
+    "brand": "Peaknetic",
+    "condition": "new",
+    "description": "If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
+    "description_embeddings": [
+      0.0022135202307254076,
+      0.0681997612118721,
+      0.0064607178792357445,
+      0.007070810534060001,
+      -0.054231829941272736,
+      0.008059866726398468,
+      0.040072083473205566,
+      0.0956423208117485,
+      0.049143481999635696,
+      0.0379914790391922,
+      -0.020757146179676056,
+      0.06460786610841751,
+      0.07982552796602249,
+      -0.0321788415312767,
+      -0.006194670218974352,
+      0.0375053696334362,
+      0.1270381063222885,
+      -0.02341461181640625,
+      -0.007247396744787693,
+      0.0682845488190651,
+      -0.026470346376299858,
+      0.005658577661961317,
+      0.04653697833418846,
+      0.03987714648246765,
+      -0.12344618886709213,
+      0.03188862279057503,
+      -0.02708076685667038,
+      0.06964577734470367,
+      0.02201199159026146,
+      -0.02890665829181671,
+      -0.040280576795339584,
+      0.06318672746419907,
+      0.07309666275978088,
+      -0.08932560682296753,
+      -0.11648543179035187,
+      -0.029937371611595154,
+      0.10487373173236847,
+      0.041655637323856354,
+      -0.022871389985084534,
+      -0.04677774757146835,
+      0.02746077999472618,
+      0.01847464218735695,
+      0.08173345029354095,
+      0.06561841815710068,
+      0.05970228090882301,
+      -0.024480478838086128,
+      0.06907600909471512,
+      -0.03947169706225395,
+      0.024855393916368484,
+      -0.013199429027736187,
+      0.09996063262224197,
+      0.0028578736819326878,
+      0.08506831526756287,
+      0.014810853637754917,
+      -0.03929787874221802,
+      0.0018542942125350237,
+      -0.14215077459812164,
+      0.06970464438199997,
+      0.0265016071498394,
+      -0.06698428839445114,
+      0.07732488960027695,
+      -0.04583248123526573,
+      0.028696633875370026,
+      0.013155206106603146,
+      0.02578958310186863,
+      -0.029291296377778053,
+      -0.00031816380214877427,
+      -0.125708669424057,
+      -0.042060736566782,
+      -0.05381627008318901,
+      -0.0538906455039978,
+      0.0014778936747461557,
+      -0.09889215975999832,
+      -0.0429794080555439,
+      -0.013458915054798126,
+      -0.04558553919196129,
+      0.09005370736122131,
+      -0.04213777929544449,
+      -0.1349189579486847,
+      0.055852413177490234,
+      0.006197084207087755,
+      0.016338912770152092,
+      0.032463233917951584,
+      0.02548987604677677,
+      0.030681701377034187,
+      -0.0703219398856163,
+      0.05395807698369026,
+      -0.009628057479858398,
+      -0.031475286930799484,
+      -0.027579376474022865,
+      0.04098684713244438,
+      0.03901920095086098,
+      -0.01192146260291338,
+      -0.04101356491446495,
+      -0.08753280341625214,
+      0.0705665871500969,
+      0.03578994423151016,
+      0.032069701701402664,
+      -0.03211764618754387,
+      0.09590566158294678,
+      0.0611797571182251,
+      0.004700345452874899,
+      0.10438291728496552,
+      0.07409676164388657,
+      -0.0090691689401865,
+      0.011407091282308102,
+      0.05370165407657623,
+      -0.020438825711607933,
+      -0.04398776963353157,
+      0.032895661890506744,
+      -0.0012730252929031849,
+      0.018297234550118446,
+      -0.04333885386586189,
+      0.06208101287484169,
+      -0.08955910801887512,
+      -0.04672509804368019,
+      -0.06376977264881134,
+      0.022537674754858017,
+      0.02881341427564621,
+      -0.0030273371376097202,
+      -0.06542699784040451,
+      0.007231372408568859,
+      0.01855703443288803,
+      0.012365416623651981,
+      0.018434280529618263,
+      -0.04855069890618324,
+      -0.08907819539308548,
+      -2.615521077916409e-34,
+      -0.046131156384944916,
+      -0.028526470065116882,
+      0.01342072058469057,
+      -0.010418130084872246,
+      -0.019669201225042343,
+      -0.07430055737495422,
+      -0.006000629626214504,
+      -0.03320513665676117,
+      -0.05303066596388817,
+      0.07680810242891312,
+      -0.0061429338529706,
+      0.06154831871390343,
+      0.09432042390108109,
+      0.0038354273419827223,
+      0.14506474137306213,
+      -0.07032876461744308,
+      -0.07443196326494217,
+      -0.04439792409539223,
+      -0.005104243289679289,
+      -3.3934433304239064e-05,
+      -0.08257872611284256,
+      -0.056131236255168915,
+      0.04174238070845604,
+      0.0017745267832651734,
+      -0.002772972686216235,
+      0.006998051423579454,
+      0.09371285885572433,
+      -0.013138788752257824,
+      -0.042773280292749405,
+      0.00896062795072794,
+      -0.09441300481557846,
+      -0.007952050305902958,
+      -0.015586148016154766,
+      -0.017506571486592293,
+      0.01347337942570448,
+      0.027937229722738266,
+      -0.11657726019620895,
+      -0.03324989974498749,
+      -0.018427638337016106,
+      -0.04819897934794426,
+      -0.04077771306037903,
+      -0.0001697312545729801,
+      -0.05697356536984444,
+      0.07191669940948486,
+      0.017228955402970314,
+      0.03711475431919098,
+      0.042057864367961884,
+      0.004786928184330463,
+      -0.06956200301647186,
+      0.06036132574081421,
+      -0.0016305814497172832,
+      0.02468821406364441,
+      -0.013794313184916973,
+      -0.025994934141635895,
+      -0.07489914447069168,
+      0.01067660003900528,
+      -0.010298662818968296,
+      -0.022154971957206726,
+      -0.09746527671813965,
+      0.06335768103599548,
+      -0.022728655487298965,
+      -0.0005628599901683629,
+      -0.04313022270798683,
+      -0.04123309254646301,
+      -0.08378659188747406,
+      0.020546691492199898,
+      0.01680913008749485,
+      -0.010155763477087021,
+      0.008547178469598293,
+      -0.0211178008466959,
+      0.023050235584378242,
+      0.06479462236166,
+      0.061528537422418594,
+      0.022156352177262306,
+      0.019619867205619812,
+      0.07456795871257782,
+      0.049971628934144974,
+      -0.07878398895263672,
+      0.04972408711910248,
+      -0.015442566946148872,
+      0.044758349657058716,
+      -0.037393294274806976,
+      -0.03831558674573898,
+      -0.016534404829144478,
+      -0.05692937225103378,
+      0.02438316121697426,
+      -0.008604591712355614,
+      -0.06003899499773979,
+      0.04884570837020874,
+      -0.05407913774251938,
+      -0.004864877089858055,
+      -0.039421387016773224,
+      -0.0335439033806324,
+      0.05149518698453903,
+      -0.00761334178969264,
+      -4.919814414418271e-34,
+      0.0605306513607502,
+      0.013155259191989899,
+      0.0022042335476726294,
+      -0.016238724812865257,
+      0.017820321023464203,
+      -0.029504502192139626,
+      0.1179562360048294,
+      -0.08494631201028824,
+      -0.05847567319869995,
+      -0.03412671759724617,
+      0.01584434323012829,
+      0.0205944012850523,
+      0.011703073978424072,
+      0.024820921942591667,
+      0.07683313637971878,
+      0.016703670844435692,
+      -0.0465223602950573,
+      -0.08929521590471268,
+      0.013778958469629288,
+      -0.06448621302843094,
+      0.047237128019332886,
+      0.11140323430299759,
+      -0.036479681730270386,
+      -0.055252984166145325,
+      -0.08155377954244614,
+      -0.0011454259511083364,
+      -0.07414430379867554,
+      0.04839516803622246,
+      -0.036127883940935135,
+      0.04249665141105652,
+      -0.05564387887716293,
+      0.008439254947006702,
+      0.056384216994047165,
+      0.0037136792670935392,
+      -0.06267311424016953,
+      0.06599505990743637,
+      -0.0944608598947525,
+      -0.01900491677224636,
+      0.0017078104428946972,
+      0.018181079998612404,
+      0.0072936019860208035,
+      0.03901578113436699,
+      0.022009696811437607,
+      -0.02042953297495842,
+      0.04422936588525772,
+      0.099826380610466,
+      0.0022175773046910763,
+      0.05569764971733093,
+      0.005229232832789421,
+      -0.06281892955303192,
+      0.06203164905309677,
+      0.05493532121181488,
+      0.06598878651857376,
+      0.06272768974304199,
+      0.05623844638466835,
+      -0.053184594959020615,
+      0.012609804049134254,
+      -0.009191329590976238,
+      -0.07956400513648987,
+      -0.005095178727060556,
+      -0.06295286118984222,
+      0.05799472704529762,
+      0.004298996180295944,
+      -0.0245245061814785,
+      -0.010027579963207245,
+      -0.02338206022977829,
+      -0.03576310724020004,
+      -0.0735088586807251,
+      -0.10477573424577713,
+      0.010318689979612827,
+      -0.031293049454689026,
+      -0.06913845986127853,
+      0.07762496173381805,
+      -0.029345618560910225,
+      0.03347797319293022,
+      0.014305013231933117,
+      0.06321006268262863,
+      -0.09872464835643768,
+      -0.040937263518571854,
+      0.028350593522191048,
+      -0.08784928917884827,
+      0.005375882610678673,
+      0.059733789414167404,
+      0.027322115376591682,
+      -0.04657347500324249,
+      0.02783248759806156,
+      -0.12815773487091064,
+      0.004414730705320835,
+      -0.009058866649866104,
+      0.06062375009059906,
+      0.04072198644280434,
+      0.05082780495285988,
+      -0.036888547241687775,
+      0.07301440089941025,
+      0.022250277921557426,
+      -4.836826761334123e-08,
+      -0.02333931438624859,
+      -0.0006597689352929592,
+      -0.011001646518707275,
+      0.01690472476184368,
+      -0.0037487675435841084,
+      0.039681874215602875,
+      -0.012263616546988487,
+      0.010128140449523926,
+      0.03165998309850693,
+      0.0028211181052029133,
+      0.009292887523770332,
+      -0.05068610981106758,
+      -0.05350435897707939,
+      0.0027450143825262785,
+      -0.02869512513279915,
+      0.18523184955120087,
+      0.010015024803578854,
+      0.04042758792638779,
+      0.010014397092163563,
+      -0.07638011872768402,
+      -0.04126984626054764,
+      -0.06051325798034668,
+      0.04609273001551628,
+      -0.00863336119800806,
+      -0.010159372352063656,
+      -0.0686831921339035,
+      -0.022274712100625038,
+      0.054596077650785446,
+      -0.004217579495161772,
+      0.005010589957237244,
+      -0.0023027928546071053,
+      0.054350487887859344,
+      0.051168326288461685,
+      -0.0005977987311780453,
+      0.0008231411338783801,
+      -0.005349422339349985,
+      -0.04023698344826698,
+      0.01875622756779194,
+      0.051792532205581665,
+      0.0725129023194313,
+      0.003990984987467527,
+      -0.0412585511803627,
+      -0.030975697562098503,
+      0.02229507826268673,
+      -0.023536084219813347,
+      0.004254089202731848,
+      -0.047596003860235214,
+      0.054139409214258194,
+      0.024750487878918648,
+      0.00830126740038395,
+      0.04204926639795303,
+      -0.05580601468682289,
+      0.014396552927792072,
+      0.0785103291273117,
+      -0.01755904220044613,
+      0.09679318964481354,
+      -0.01722336746752262,
+      -0.03680667281150818,
+      -0.021823544055223465,
+      0.044612541794776917,
+      -0.0008680447936058044,
+      0.027066197246313095,
+      -0.008826435543596745,
+      -0.034606464207172394
+    ],
+    "model": "Secto",
+    "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, 13.3260 52.2700, 13.3260 52.5700))",
+    "price": 430,
+    "store_location": "13.4050,52.5200"
+  },
+  {
+    "brand": "nHill",
+    "condition": "new",
+    "description": "This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\u2019re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.",
+    "description_embeddings": [
+      -0.024333441630005836,
+      0.029308300465345383,
+      -0.012847754172980785,
+      -0.013123984448611736,
+      -0.039905961602926254,
+      -0.04596070945262909,
+      -0.01287133153527975,
+      0.03984961286187172,
+      -0.043438445776700974,
+      0.012349214404821396,
+      -0.021342391148209572,
+      -0.027458613738417625,
+      -0.004793129395693541,
+      0.016479987651109695,
+      0.02052542380988598,
+      -0.009542089886963367,
+      0.10066469013690948,
+      0.017397526651620865,
+      0.07893014699220657,
+      -0.0765308141708374,
+      -0.007650941610336304,
+      9.538845188217238e-05,
+      0.03314477205276489,
+      0.026631362736225128,
+      -0.01621824875473976,
+      0.03593063727021217,
+      -0.014871303923428059,
+      0.05259871482849121,
+      0.027785824611783028,
+      -0.044416770339012146,
+      -0.026691904291510582,
+      -0.01390022411942482,
+      -0.0573929138481617,
+      -0.08761026710271835,
+      0.012642575427889824,
+      0.04252056032419205,
+      0.04015251249074936,
+      -0.0052475095726549625,
+      -0.08608037978410721,
+      0.0028531427960842848,
+      -0.003371630096808076,
+      -0.02032443881034851,
+      -0.029511747881770134,
+      -0.043690912425518036,
+      0.042122989892959595,
+      -0.057133886963129044,
+      0.019293654710054398,
+      -0.019127611070871353,
+      0.036713697016239166,
+      -0.00833116751164198,
+      0.009040397591888905,
+      -0.1087319627404213,
+      0.09127230197191238,
+      -0.048213958740234375,
+      -0.023604029789566994,
+      0.0008899428648874164,
+      -0.093184694647789,
+      -0.03436879441142082,
+      0.00807907897979021,
+      -0.0520450733602047,
+      0.02262270636856556,
+      -0.03259866312146187,
+      -0.0391012541949749,
+      0.001295328140258789,
+      0.06636986881494522,
+      -0.04248524457216263,
+      -0.07093628495931625,
+      -0.06715728342533112,
+      0.007415436673909426,
+      -0.06185786426067352,
+      0.036563266068696976,
+      0.030749274417757988,
+      0.007705106865614653,
+      0.0023627770133316517,
+      -0.026215652003884315,
+      -0.020864665508270264,
+      0.05270038917660713,
+      0.021477004513144493,
+      0.0068344962783157825,
+      0.024355463683605194,
+      -0.03746088594198227,
+      0.031043346971273422,
+      0.09879995882511139,
+      -0.050035204738378525,
+      0.08045479655265808,
+      -0.04274336248636246,
+      0.02994127944111824,
+      -0.03783947601914406,
+      0.039749279618263245,
+      0.046901557594537735,
+      0.08667739480733871,
+      0.02831847593188286,
+      -0.008148318156599998,
+      0.007401767186820507,
+      -0.10989774018526077,
+      -0.014092149212956429,
+      9.036048140842468e-06,
+      0.054770372807979584,
+      -0.012413500808179379,
+      0.01861056312918663,
+      0.060349978506565094,
+      0.06291640549898148,
+      0.007055714726448059,
+      -0.021361181512475014,
+      0.03852728381752968,
+      -0.018908292055130005,
+      0.047424111515283585,
+      0.054501011967659,
+      0.017648596316576004,
+      0.11429999768733978,
+      -0.041358742862939835,
+      -0.03363256901502609,
+      0.0030338421929627657,
+      0.030056871473789215,
+      0.008060693740844727,
+      -0.0688784271478653,
+      -0.09984277933835983,
+      0.030267179012298584,
+      -0.028509166091680527,
+      0.02268315851688385,
+      -0.07580948621034622,
+      -0.0405895970761776,
+      0.03384138271212578,
+      0.07081738114356995,
+      0.019772004336118698,
+      -0.06172173097729683,
+      0.008248022757470608,
+      1.9840379362262432e-33,
+      0.037930894643068314,
+      0.04577890411019325,
+      -0.03732670471072197,
+      -0.032416991889476776,
+      0.014588817022740841,
+      -0.06602118909358978,
+      -0.013814577832818031,
+      -0.10539791733026505,
+      -0.12850415706634521,
+      -0.005076196976006031,
+      -0.07302963733673096,
+      0.07235066592693329,
+      -0.04646436870098114,
+      0.059371400624513626,
+      0.09111672639846802,
+      -0.10133209824562073,
+      -0.05483068525791168,
+      -0.053797587752342224,
+      -0.03709062561392784,
+      0.08241501450538635,
+      0.03958267718553543,
+      0.01433452870696783,
+      0.05912068858742714,
+      -0.0029660870786756277,
+      0.01806815154850483,
+      -0.0680701732635498,
+      0.013625666499137878,
+      0.015582672320306301,
+      -0.02142208069562912,
+      0.06357447057962418,
+      -0.12094264477491379,
+      -0.09666852653026581,
+      -0.03333089128136635,
+      -0.06621623784303665,
+      -0.03821765258908272,
+      -0.010776739567518234,
+      -0.08198492974042892,
+      -0.03277386352419853,
+      0.014794036746025085,
+      0.013517632149159908,
+      -0.0034003634937107563,
+      0.005664682947099209,
+      -0.022207774221897125,
+      0.015086682513356209,
+      -0.06799489259719849,
+      0.08191754668951035,
+      0.15600024163722992,
+      0.07579171657562256,
+      -0.05843832343816757,
+      -0.008412746712565422,
+      -0.07857701182365417,
+      -0.0038521387614309788,
+      -0.02901686355471611,
+      0.0006679021753370762,
+      -0.0697081908583641,
+      -0.05330892279744148,
+      0.053166620433330536,
+      0.004783661104738712,
+      0.013741954229772091,
+      0.07203977555036545,
+      -0.015861764550209045,
+      0.033332549035549164,
+      -0.022547632455825806,
+      -0.011850795708596706,
+      -0.09526507556438446,
+      -0.016078950837254524,
+      -0.004922114312648773,
+      0.004731250926852226,
+      -0.006536174099892378,
+      0.004852978512644768,
+      0.01268527377396822,
+      0.05300697684288025,
+      0.11899229139089584,
+      0.025274425745010376,
+      0.11763036996126175,
+      -0.08781222999095917,
+      -0.016429787501692772,
+      -0.02661072462797165,
+      -0.01769474893808365,
+      0.05961911380290985,
+      0.005877051502466202,
+      -0.03780582174658775,
+      -0.028037618845701218,
+      0.023397648707032204,
+      -0.006716609466820955,
+      0.015020519495010376,
+      0.040884219110012054,
+      -0.06197969987988472,
+      -0.029691174626350403,
+      -0.004976964555680752,
+      -0.03422437235713005,
+      0.018984060734510422,
+      -0.00813105795532465,
+      -0.020747167989611626,
+      0.0281930360943079,
+      -3.2326105667872445e-33,
+      0.12847235798835754,
+      0.026267321780323982,
+      0.10983140766620636,
+      0.011159190908074379,
+      -0.044980239123106,
+      0.017553674057126045,
+      -0.004794386215507984,
+      -0.015802551060914993,
+      -0.01226617582142353,
+      -0.019019361585378647,
+      0.011876302771270275,
+      -0.023136194795370102,
+      0.03209578990936279,
+      0.07442695647478104,
+      0.05641649663448334,
+      -0.05300089344382286,
+      -0.03638714924454689,
+      -0.014843880198895931,
+      0.036886364221572876,
+      -0.1257546991109848,
+      -0.008151554502546787,
+      0.07542898505926132,
+      -0.11252967268228531,
+      -0.09049033373594284,
+      -0.0030374047346413136,
+      0.04011000320315361,
+      -0.1538117229938507,
+      0.07717141509056091,
+      -0.04436258599162102,
+      0.07290291786193848,
+      -0.0072043901309370995,
+      0.03950807452201843,
+      0.002569766016677022,
+      -0.006248284596949816,
+      0.001176450401544571,
+      0.12515375018119812,
+      0.02005600742995739,
+      -0.00013864059292245656,
+      0.024221621453762054,
+      0.05465780198574066,
+      0.09940708428621292,
+      -0.053944025188684464,
+      0.09602796286344528,
+      0.00014552564243786037,
+      0.026602625846862793,
+      0.016811460256576538,
+      0.0015170868718996644,
+      0.08731727302074432,
+      0.005771235562860966,
+      -0.009904555976390839,
+      0.06958170980215073,
+      0.09265917539596558,
+      0.005830116104334593,
+      0.10210693627595901,
+      0.0011325017549097538,
+      -0.006749417632818222,
+      0.01503710262477398,
+      -0.03714510798454285,
+      -0.0939728170633316,
+      -0.03357759863138199,
+      -0.050196800380945206,
+      -0.006316252052783966,
+      -0.06414957344532013,
+      0.007235861383378506,
+      -0.01670873910188675,
+      -0.07423098385334015,
+      0.018221961334347725,
+      -0.05320512503385544,
+      -0.010248126462101936,
+      0.013013344258069992,
+      -0.12916545569896698,
+      -0.04244375228881836,
+      0.08774643391370773,
+      -0.03917357325553894,
+      -0.027767449617385864,
+      0.02099072001874447,
+      0.0595061257481575,
+      0.01945589855313301,
+      0.03815269470214844,
+      -0.028747402131557465,
+      -0.025533201172947884,
+      -0.038349006325006485,
+      -0.014486055821180344,
+      -0.013576257973909378,
+      0.051731329411268234,
+      0.06515145301818848,
+      -0.011720783077180386,
+      -0.1163521260023117,
+      -0.03931708261370659,
+      0.03394515439867973,
+      0.09380073100328445,
+      0.00931628793478012,
+      -0.01171857863664627,
+      0.04104544222354889,
+      -0.008916886523365974,
+      -4.140364140425845e-08,
+      0.061763226985931396,
+      0.02101300284266472,
+      -0.019075985997915268,
+      -0.002017116406932473,
+      0.015830116346478462,
+      -0.009857675060629845,
+      -0.018932191655039787,
+      -0.0007055550813674927,
+      0.013249439187347889,
+      0.0642324835062027,
+      0.12860508263111115,
+      0.03213700279593468,
+      -0.0670522004365921,
+      0.041800037026405334,
+      -0.0426088348031044,
+      0.0437115803360939,
+      0.021514814347028732,
+      0.12901803851127625,
+      0.015236952342092991,
+      -0.02232329733669758,
+      -0.05031055584549904,
+      -0.052778035402297974,
+      0.000233030179515481,
+      -0.022497626021504402,
+      -0.014370465651154518,
+      0.017518600448966026,
+      0.014312495477497578,
+      0.010902844369411469,
+      0.0027771666646003723,
+      -0.04195915535092354,
+      -0.06504479050636292,
+      0.038553908467292786,
+      -0.01374481339007616,
+      0.03843652829527855,
+      0.09058628976345062,
+      -0.0230315700173378,
+      -0.11761228740215302,
+      0.06487669050693512,
+      -0.005885662976652384,
+      0.06288695335388184,
+      0.05191958323121071,
+      0.011840583756566048,
+      -0.027309859171509743,
+      0.040654800832271576,
+      -0.005980184301733971,
+      0.03252403438091278,
+      -0.043744225054979324,
+      0.006068602204322815,
+      -0.00032751375692896545,
+      -0.022902367636561394,
+      -0.050694938749074936,
+      0.026743048802018166,
+      0.06005466729402542,
+      0.02092430181801319,
+      0.02072584442794323,
+      0.08342021703720093,
+      -0.05768749862909317,
+      -0.08585236966609955,
+      -0.03160274401307106,
+      0.05222279205918312,
+      0.029474787414073944,
+      -0.08763033896684647,
+      -0.01591801643371582,
+      -0.07798203080892563
+    ],
+    "model": "Summit",
+    "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))",
+    "price": 1200,
+    "store_location": "2.1734, 41.3851"
+  },
+  {
+    "brand": "BikeShind",
+    "condition": "refurbished",
+    "description": "An artsy,  retro-inspired bicycle that\u2019s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn\u2019t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.",
+    "description_embeddings": [
+      -0.005154624115675688,
+      0.10099548101425171,
+      0.04890008643269539,
+      0.0625317171216011,
+      -0.0363173745572567,
+      0.025850096717476845,
+      0.032137978821992874,
+      -0.03442877531051636,
+      -0.10096120089292526,
+      -0.007441149093210697,
+      0.002616145182400942,
+      0.002316742204129696,
+      0.042584337294101715,
+      0.026208963245153427,
+      0.018415318801999092,
+      0.050033390522003174,
+      0.14831797778606415,
+      -0.006160213612020016,
+      0.05086936056613922,
+      0.0131875304505229,
+      -0.05973455682396889,
+      0.045886117964982986,
+      -0.025052331387996674,
+      0.04576171189546585,
+      -0.006168896332383156,
+      0.03315199911594391,
+      -0.07763667404651642,
+      0.05837876722216606,
+      -0.03229084238409996,
+      -0.021293187513947487,
+      -0.008595496416091919,
+      0.06880632787942886,
+      -0.04528025537729263,
+      -0.020664094015955925,
+      -0.029125794768333435,
+      0.0445701889693737,
+      0.023799002170562744,
+      -0.013527574017643929,
+      -0.01646343804895878,
+      0.027023920789361,
+      0.0014843698590993881,
+      0.014952401630580425,
+      -0.010172702372074127,
+      0.07603776454925537,
+      0.017975280061364174,
+      -0.08911248296499252,
+      0.055482957512140274,
+      -0.027819648385047913,
+      0.019395964220166206,
+      0.05424710363149643,
+      0.06634850054979324,
+      -0.05045575276017189,
+      -0.0006240577204152942,
+      -0.06339468061923981,
+      -0.023632608354091644,
+      -0.060364823788404465,
+      -0.11193189769983292,
+      0.046562716364860535,
+      -0.010353066958487034,
+      -0.043506212532520294,
+      0.051063187420368195,
+      0.003231980837881565,
+      0.027102531865239143,
+      0.002380193443968892,
+      -0.0056939758360385895,
+      -0.026826998218894005,
+      -0.014482468366622925,
+      -0.0185822993516922,
+      0.013927196152508259,
+      -0.10704471915960312,
+      0.0673733651638031,
+      -0.0010599792003631592,
+      -0.07455477863550186,
+      -0.014452059753239155,
+      -0.02294282428920269,
+      -0.010201872326433659,
+      0.023037923499941826,
+      -0.05595972761511803,
+      -0.09120096266269684,
+      -0.029177049174904823,
+      -0.02343042939901352,
+      -0.029726101085543633,
+      0.07366959005594254,
+      -0.046458736062049866,
+      0.006261167582124472,
+      -0.013148028403520584,
+      -0.0014217026764526963,
+      0.017440352588891983,
+      -0.04511420056223869,
+      0.008813070133328438,
+      -0.022654451429843903,
+      -0.014097515493631363,
+      -0.020799456164240837,
+      -0.05456947907805443,
+      -0.07338607311248779,
+      0.017073828727006912,
+      -0.04925030097365379,
+      -0.024428002536296844,
+      -0.00979585014283657,
+      0.025696750730276108,
+      0.07033320516347885,
+      0.0389082096517086,
+      0.12711860239505768,
+      0.07472020387649536,
+      -0.019501805305480957,
+      0.01663217693567276,
+      0.027799105271697044,
+      0.039703886955976486,
+      -0.029871169477701187,
+      0.005997804459184408,
+      -0.0015701024094596505,
+      0.0411832258105278,
+      8.85713889147155e-05,
+      -0.029442811384797096,
+      0.009789851494133472,
+      -0.04723270237445831,
+      -0.09891993552446365,
+      0.05102938041090965,
+      -0.007144609931856394,
+      0.01668076030910015,
+      0.004902719985693693,
+      0.02218448370695114,
+      0.03827798366546631,
+      0.001542922342196107,
+      0.004610520787537098,
+      -0.12595036625862122,
+      0.07361572980880737,
+      -1.521196586481568e-33,
+      -0.01484542153775692,
+      0.024973904713988304,
+      0.006633534096181393,
+      0.01254032738506794,
+      0.06355929374694824,
+      -0.04809075966477394,
+      0.02005922608077526,
+      -0.06251886487007141,
+      -0.06941430270671844,
+      0.02304103970527649,
+      -0.020975833758711815,
+      0.08137834072113037,
+      0.049319952726364136,
+      0.11945393681526184,
+      0.17472721636295319,
+      -0.03430051729083061,
+      -0.07276412844657898,
+      -0.11828659474849701,
+      0.04918891564011574,
+      -0.07279565185308456,
+      -0.06228293105959892,
+      -0.03494764119386673,
+      0.004517378751188517,
+      -0.0431801863014698,
+      -0.07457974553108215,
+      -0.0003888327337335795,
+      0.05853814631700516,
+      0.05362739413976669,
+      0.01866878569126129,
+      0.004751134198158979,
+      -0.07936650514602661,
+      0.033309247344732285,
+      -0.01660560630261898,
+      -0.08851712942123413,
+      0.042956508696079254,
+      0.03460550308227539,
+      -0.054352447390556335,
+      -0.016094308346509933,
+      0.019760286435484886,
+      -0.03591147065162659,
+      -0.04307156056165695,
+      -0.04029138758778572,
+      -0.09315019100904465,
+      0.055713504552841187,
+      0.003642909461632371,
+      0.0391569547355175,
+      0.1300928145647049,
+      0.026774607598781586,
+      -0.054334208369255066,
+      -0.060861144214868546,
+      -0.020662454888224602,
+      -0.034525640308856964,
+      0.004193050786852837,
+      0.02453654631972313,
+      -0.09028972685337067,
+      0.004444751888513565,
+      0.06320545822381973,
+      0.01895289309322834,
+      -0.04660886526107788,
+      0.08611723780632019,
+      0.05705415457487106,
+      0.030612647533416748,
+      0.019753821194171906,
+      -0.061483170837163925,
+      -0.025262145325541496,
+      0.054222241044044495,
+      0.017605014145374298,
+      -0.013110420666635036,
+      0.04744894057512283,
+      -0.004785404074937105,
+      0.04680679365992546,
+      0.001831064117141068,
+      0.07773134112358093,
+      0.008347200229763985,
+      0.08674833923578262,
+      0.024294976145029068,
+      -0.03695613518357277,
+      -0.0440739244222641,
+      0.00895663257688284,
+      -0.0060102143324911594,
+      -0.0361272394657135,
+      -0.08447428047657013,
+      -0.08971717208623886,
+      0.03131894767284393,
+      0.0721370130777359,
+      0.031180810183286667,
+      0.039390258491039276,
+      -0.09044987708330154,
+      -0.007888346910476685,
+      0.008131768554449081,
+      -0.03624662756919861,
+      -0.033649034798145294,
+      0.031620997935533524,
+      0.05870470404624939,
+      0.018141048029065132,
+      -5.7662626416839565e-34,
+      0.09117024391889572,
+      -0.023953478783369064,
+      0.080879345536232,
+      0.0035459804348647594,
+      -0.013222851790487766,
+      -0.025237491354346275,
+      0.019038159400224686,
+      -0.06588941067457199,
+      -0.11689981818199158,
+      0.0007165187271311879,
+      -0.0933104082942009,
+      0.014618181623518467,
+      0.12644915282726288,
+      0.0027253602165728807,
+      0.15038658678531647,
+      0.0053163510747253895,
+      -0.005645217839628458,
+      0.009321562945842743,
+      0.002719732467085123,
+      -0.01651378907263279,
+      0.0063886744901537895,
+      0.05738385394215584,
+      -0.14213474094867706,
+      -0.0669025406241417,
+      -0.05766160413622856,
+      0.0031909833196550608,
+      -0.13478563725948334,
+      -0.03741385415196419,
+      0.041654907166957855,
+      0.04030514881014824,
+      -0.05102578178048134,
+      0.0028968513943254948,
+      0.0738285705447197,
+      -0.03643025830388069,
+      -0.015445208176970482,
+      0.06674294173717499,
+      0.029759766533970833,
+      -0.024196317419409752,
+      -0.009355752728879452,
+      0.023269686847925186,
+      0.01623220555484295,
+      -0.08251868188381195,
+      0.020170293748378754,
+      0.0749305933713913,
+      0.08206077665090561,
+      0.012237507849931717,
+      -0.026570556685328484,
+      0.016253553330898285,
+      -0.003425935748964548,
+      0.005059909541159868,
+      0.12109038233757019,
+      0.08107315003871918,
+      -0.04097432270646095,
+      0.0009462210582569242,
+      0.058092083781957626,
+      -0.03609737753868103,
+      -0.01580999232828617,
+      -0.0342475026845932,
+      0.012185491621494293,
+      -0.058348096907138824,
+      -0.05934947729110718,
+      0.026715252548456192,
+      -0.031246516853570938,
+      0.012632215395569801,
+      -0.01100403256714344,
+      -0.05962579324841499,
+      -0.000801989808678627,
+      -0.06919746845960617,
+      -0.1299775391817093,
+      -0.03721063956618309,
+      -0.03989635780453682,
+      0.04918158799409866,
+      -0.0022671804763376713,
+      -0.04244564101099968,
+      -0.02631748840212822,
+      -0.04100838303565979,
+      0.08634430915117264,
+      0.05031242221593857,
+      0.06688959896564484,
+      0.024407675489783287,
+      0.0018788984743878245,
+      -0.026026425883173943,
+      0.04438408091664314,
+      0.02198263816535473,
+      -0.014974343590438366,
+      -0.026490231975913048,
+      -0.09914876520633698,
+      -0.09834708273410797,
+      0.017346272245049477,
+      0.00089951854897663,
+      0.05497178062796593,
+      0.06180766969919205,
+      -0.050425756722688675,
+      0.013637324795126915,
+      -0.03599657490849495,
+      -4.404103037813911e-08,
+      -0.0024933917447924614,
+      0.049585312604904175,
+      -0.020710783079266548,
+      -0.014283453114330769,
+      -0.0064896950498223305,
+      0.04579087719321251,
+      0.02242390066385269,
+      0.02515084482729435,
+      -0.031156959012150764,
+      0.006718308199197054,
+      0.061417631804943085,
+      -0.009935885667800903,
+      -0.021248064935207367,
+      0.011152776889503002,
+      -0.059073783457279205,
+      0.05840323865413666,
+      0.06523993611335754,
+      0.03690555691719055,
+      0.03174441307783127,
+      0.02336188219487667,
+      0.003073832020163536,
+      -0.017117813229560852,
+      0.02877660095691681,
+      0.0017827276606112719,
+      -0.07995487004518509,
+      -0.032443612813949585,
+      -0.01474942360073328,
+      -0.016080526635050774,
+      0.0771399736404419,
+      0.017354682087898254,
+      -0.02578366920351982,
+      0.004991421941667795,
+      0.006590475793927908,
+      -0.032372940331697464,
+      0.038421064615249634,
+      -0.1204696074128151,
+      -0.05457846075296402,
+      0.036750562489032745,
+      0.013932188041508198,
+      0.0031626380514353514,
+      0.009978784248232841,
+      -0.0138266421854496,
+      0.006769631989300251,
+      0.059896957129240036,
+      0.00822784099727869,
+      -0.05025117099285126,
+      -0.10305225104093552,
+      -0.07187453657388687,
+      0.00836241990327835,
+      0.049537766724824905,
+      0.007570290472358465,
+      -0.08729138970375061,
+      -0.020592620596289635,
+      0.04918212443590164,
+      0.054685790091753006,
+      0.04091939702630043,
+      -0.05420788377523422,
+      -0.04473182559013367,
+      -0.11178043484687805,
+      0.09808126091957092,
+      -0.015394269488751888,
+      -0.0039725536480546,
+      0.03283742815256119,
+      -0.05677533522248268
+    ],
+    "model": "ThrillCycle",
+    "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, 12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
+    "price": 815,
+    "store_location": "12.4964,41.9028"
+  }
+]
\ No newline at end of file
diff --git a/doctests/dt-bitfield.js b/doctests/dt-bitfield.js
new file mode 100644
index 00000000000..9685372de41
--- /dev/null
+++ b/doctests/dt-bitfield.js
@@ -0,0 +1,76 @@
+// EXAMPLE: bitfield_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START bf
+let res1 = await client.bitField("bike:1:stats", [{
+  operation: 'SET',
+  encoding: 'u32',
+  offset: '#0',
+  value: 1000
+}]);
+console.log(res1);  // >>> [0]
+
+let res2 = await client.bitField('bike:1:stats', [
+  {
+    operation: 'INCRBY',
+    encoding: 'u32',
+    offset: '#0',
+    increment: -50
+  },
+  {
+    operation: 'INCRBY',
+    encoding: 'u32',
+    offset: '#1',
+    increment: 1
+  }
+]);
+console.log(res2);  // >>> [950, 1]
+
+let res3 = await client.bitField('bike:1:stats', [
+  {
+    operation: 'INCRBY',
+    encoding: 'u32',
+    offset: '#0',
+    increment: 500
+  },
+  {
+    operation: 'INCRBY',
+    encoding: 'u32',
+    offset: '#1',
+    increment: 1
+  }
+]);
+console.log(res3);  // >>> [1450, 2]
+
+let res4 = await client.bitField('bike:1:stats', [
+  {
+    operation: 'GET',
+    encoding: 'u32',
+    offset: '#0'
+  },
+  {
+    operation: 'GET',
+    encoding: 'u32',
+    offset: '#1'
+  }
+]);
+console.log(res4);  // >>> [1450, 2]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res1, [0])
+assert.deepEqual(res2, [950, 1])
+assert.deepEqual(res3, [1450, 2])
+assert.deepEqual(res4, [1450, 2])
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-bitmap.js b/doctests/dt-bitmap.js
new file mode 100644
index 00000000000..845fd2a721c
--- /dev/null
+++ b/doctests/dt-bitmap.js
@@ -0,0 +1,39 @@
+// EXAMPLE: bitmap_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START ping
+const res1 = await client.setBit("pings:2024-01-01-00:00", 123, 1)
+console.log(res1)  // >>> 0
+
+const res2 = await client.getBit("pings:2024-01-01-00:00", 123)
+console.log(res2)  // >>> 1
+
+const res3 = await client.getBit("pings:2024-01-01-00:00", 456)
+console.log(res3)  // >>> 0
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 0)
+// REMOVE_END
+
+// STEP_START bitcount
+// HIDE_START
+await client.setBit("pings:2024-01-01-00:00", 123, 1)
+// HIDE_END
+const res4 = await client.bitCount("pings:2024-01-01-00:00")
+console.log(res4)  // >>> 1
+// STEP_END
+// REMOVE_START
+assert.equal(res4, 1)
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/dt-bloom.js b/doctests/dt-bloom.js
new file mode 100644
index 00000000000..d065937e763
--- /dev/null
+++ b/doctests/dt-bloom.js
@@ -0,0 +1,46 @@
+// EXAMPLE: bf_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START bloom
+const res1 = await client.bf.reserve('bikes:models', 0.01, 1000);
+console.log(res1);  // >>> OK
+
+const res2 = await client.bf.add('bikes:models', 'Smoky Mountain Striker');
+console.log(res2);  // >>> true
+
+const res3 = await client.bf.exists('bikes:models', 'Smoky Mountain Striker');
+console.log(res3);  // >>> true
+
+const res4 = await client.bf.mAdd('bikes:models', [
+  'Rocky Mountain Racer',
+  'Cloudy City Cruiser',
+  'Windy City Wippet'
+]);
+console.log(res4);  // >>> [true, true, true]
+
+const res5 = await client.bf.mExists('bikes:models', [
+  'Rocky Mountain Racer',
+  'Cloudy City Cruiser',
+  'Windy City Wippet'
+]);
+console.log(res5);  // >>> [true, true, true]
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 'OK')
+assert.equal(res2, true)
+assert.equal(res3, true)
+assert.deepEqual(res4, [true, true, true])
+assert.deepEqual(res5, [true, true, true])
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-cms.js b/doctests/dt-cms.js
new file mode 100644
index 00000000000..b0d9fa68469
--- /dev/null
+++ b/doctests/dt-cms.js
@@ -0,0 +1,50 @@
+// EXAMPLE: cms_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START cms
+const res1 = await client.cms.initByProb('bikes:profit', 0.001, 0.002);
+console.log(res1);  // >>> OK
+
+const res2 = await client.cms.incrBy('bikes:profit', {
+    item: 'Smoky Mountain Striker',
+    incrementBy: 100
+});
+console.log(res2);  // >>> [100]
+
+const res3 = await client.cms.incrBy('bikes:profit', [
+  {
+    item: 'Rocky Mountain Racer',
+    incrementBy: 200
+  },
+  {
+    item: 'Cloudy City Cruiser',
+    incrementBy: 150
+  }
+]);
+console.log(res3);  // >>> [200, 150]
+
+const res4 = await client.cms.query('bikes:profit', 'Smoky Mountain Striker');
+console.log(res4);  // >>> [100]
+
+const res5 = await client.cms.info('bikes:profit');
+console.log(res5.width, res5.depth, res5.count);  // >>> 2000 9 450
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 'OK')
+assert.deepEqual(res2, [100])
+assert.deepEqual(res3, [200, 150])
+assert.deepEqual(res4, [100])
+assert.deepEqual(res5, { width: 2000, depth: 9, count: 450 })
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/dt-cuckoo.js b/doctests/dt-cuckoo.js
new file mode 100644
index 00000000000..4b11bca2345
--- /dev/null
+++ b/doctests/dt-cuckoo.js
@@ -0,0 +1,38 @@
+// EXAMPLE: cuckoo_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START cuckoo
+const res1 = await client.cf.reserve('bikes:models', 1000000);
+console.log(res1);  // >>> OK
+
+const res2 = await client.cf.add('bikes:models', 'Smoky Mountain Striker');
+console.log(res2);  // >>> true
+
+const res3 = await client.cf.exists('bikes:models', 'Smoky Mountain Striker');
+console.log(res3);  // >>> true
+
+const res4 = await client.cf.exists('bikes:models', 'Terrible Bike Name');
+console.log(res4);  // >>> false
+
+const res5 = await client.cf.del('bikes:models', 'Smoky Mountain Striker');
+console.log(res5);  // >>> true
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 'OK')
+assert.equal(res2, true)
+assert.equal(res3, true)
+assert.equal(res4, false)
+assert.equal(res5, true)
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-geo.js b/doctests/dt-geo.js
new file mode 100644
index 00000000000..7ec9376a8a7
--- /dev/null
+++ b/doctests/dt-geo.js
@@ -0,0 +1,59 @@
+// EXAMPLE: geo_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.del('bikes:rentable')
+// REMOVE_END
+
+// STEP_START geoAdd
+const res1 = await client.geoAdd('bikes:rentable', { 
+  longitude: -122.27652,
+  latitude: 37.805186,
+  member: 'station:1'
+});
+console.log(res1)  // 1
+
+const res2 = await client.geoAdd('bikes:rentable', {
+  longitude: -122.2674626,
+  latitude: 37.8062344,
+  member: 'station:2'
+});
+console.log(res2)  // 1
+
+const res3 = await client.geoAdd('bikes:rentable', {
+  longitude: -122.2469854,
+  latitude: 37.8104049,
+  member: 'station:3'
+})
+console.log(res3)  // 1
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 1);
+assert.equal(res2, 1);
+assert.equal(res3, 1);
+// REMOVE_END
+
+// STEP_START geoSearch
+const res4 = await client.geoSearch(
+  'bikes:rentable', {
+    longitude: -122.27652,
+    latitude: 37.805186,
+  },
+  { radius: 5,
+    unit: 'km'
+  }
+);
+console.log(res4)  // ['station:1', 'station:2', 'station:3']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res4, ['station:1', 'station:2', 'station:3']);
+// REMOVE_END
+await client.close()
diff --git a/doctests/dt-hash.js b/doctests/dt-hash.js
new file mode 100644
index 00000000000..e77d6fd7b95
--- /dev/null
+++ b/doctests/dt-hash.js
@@ -0,0 +1,98 @@
+// EXAMPLE: hash_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+// STEP_START set_get_all
+const res1 = await client.hSet(
+  'bike:1',
+  {
+    'model': 'Deimos',
+    'brand': 'Ergonom',
+    'type': 'Enduro bikes',
+    'price': 4972,
+  }
+)
+console.log(res1) // 4
+
+const res2 = await client.hGet('bike:1', 'model')
+console.log(res2)  // 'Deimos'
+
+const res3 = await client.hGet('bike:1', 'price')
+console.log(res3)  // '4972'
+
+const res4 = await client.hGetAll('bike:1')
+console.log(res4)  
+/*
+{
+  brand: 'Ergonom',
+  model: 'Deimos',
+  price: '4972',
+  type: 'Enduro bikes'
+}
+*/
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 4);
+assert.equal(res2, 'Deimos');
+assert.equal(res3, '4972');
+assert.deepEqual(res4, {
+  model: 'Deimos',
+  brand: 'Ergonom',
+  type: 'Enduro bikes',
+  price: '4972'
+});
+// REMOVE_END
+
+// STEP_START hmGet
+const res5 = await client.hmGet('bike:1', ['model', 'price'])
+console.log(res5)  // ['Deimos', '4972']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(Object.values(res5), ['Deimos', '4972'])
+// REMOVE_END
+
+// STEP_START hIncrBy
+const res6 = await client.hIncrBy('bike:1', 'price', 100)
+console.log(res6)  // 5072
+const res7 = await client.hIncrBy('bike:1', 'price', -100)
+console.log(res7)  // 4972
+// STEP_END
+
+// REMOVE_START
+assert.equal(res6, 5072)
+assert.equal(res7, 4972)
+// REMOVE_END
+
+// STEP_START hIncrBy_hGet_hMget
+const res11 = await client.hIncrBy('bike:1:stats', 'rides', 1)
+console.log(res11)  // 1
+const res12 = await client.hIncrBy('bike:1:stats', 'rides', 1)
+console.log(res12)  // 2
+const res13 = await client.hIncrBy('bike:1:stats', 'rides', 1)
+console.log(res13)  // 3
+const res14 = await client.hIncrBy('bike:1:stats', 'crashes', 1)
+console.log(res14)  // 1
+const res15 = await client.hIncrBy('bike:1:stats', 'owners', 1)
+console.log(res15)  // 1
+const res16 = await client.hGet('bike:1:stats', 'rides')
+console.log(res16)  // 3
+const res17 = await client.hmGet('bike:1:stats', ['crashes', 'owners'])
+console.log(res17)  // ['1', '1']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res11, 1);
+assert.equal(res12, 2);
+assert.equal(res13, 3);
+assert.equal(res14, 1);
+assert.equal(res15, 1);
+assert.equal(res16, '3');
+assert.deepEqual(res17, ['1', '1']);
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/dt-hll.js b/doctests/dt-hll.js
new file mode 100644
index 00000000000..762ce5db15e
--- /dev/null
+++ b/doctests/dt-hll.js
@@ -0,0 +1,38 @@
+// EXAMPLE: hll_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START pfadd
+const res1 = await client.pfAdd('bikes', ['Hyperion', 'Deimos', 'Phoebe', 'Quaoar']);
+console.log(res1);  // >>> 1
+
+const res2 = await client.pfCount('bikes');
+console.log(res2);  // >>> 4
+
+const res3 = await client.pfAdd('commuter_bikes', ['Salacia', 'Mimas', 'Quaoar']);
+console.log(res3);  // >>> 1
+
+const res4 = await client.pfMerge('all_bikes', ['bikes', 'commuter_bikes']);
+console.log(res4);  // >>> OK
+
+const res5 = await client.pfCount('all_bikes');
+console.log(res5);  // >>> 6
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 1)
+assert.equal(res2, 4)
+assert.equal(res3, 1)
+assert.equal(res4, 'OK')
+assert.equal(res5, 6)
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-json.js b/doctests/dt-json.js
new file mode 100644
index 00000000000..00578f48f33
--- /dev/null
+++ b/doctests/dt-json.js
@@ -0,0 +1,425 @@
+// EXAMPLE: json_tutorial
+// HIDE_START
+import assert from 'assert';
+import {
+  createClient
+} from 'redis';
+
+const client = await createClient();
+await client.connect();
+// HIDE_END
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START set_get
+const res1 = await client.json.set("bike", "$", '"Hyperion"');
+console.log(res1); // OK
+
+const res2 = await client.json.get("bike", { path: "$" });
+console.log(res2); // ['"Hyperion"']
+
+const res3 = await client.json.type("bike", { path: "$" });
+console.log(res3); //  [ 'string' ]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res2, ['"Hyperion"']);
+// REMOVE_END
+
+// STEP_START str
+const res4 = await client.json.strLen("bike", { path: "$" });
+console.log(res4) //  [10]
+
+const res5 = await client.json.strAppend("bike", '" (Enduro bikes)"');
+console.log(res5) //  27
+
+const res6 = await client.json.get("bike", { path: "$" });
+console.log(res6) //  ['"Hyperion"" (Enduro bikes)"']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res6, ['"Hyperion"" (Enduro bikes)"']);
+// REMOVE_END
+
+// STEP_START num
+const res7 = await client.json.set("crashes", "$", 0);
+console.log(res7) //  OK
+
+const res8 = await client.json.numIncrBy("crashes", "$", 1);
+console.log(res8) //  [1]
+
+const res9 = await client.json.numIncrBy("crashes", "$", 1.5);
+console.log(res9) //  [2.5]
+
+const res10 = await client.json.numIncrBy("crashes", "$", -0.75);
+console.log(res10) //  [1.75]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res10, [1.75])
+// REMOVE_END
+
+// STEP_START arr
+const res11 = await client.json.set("newbike", "$", ["Deimos", {"crashes": 0 }, null]);
+console.log(res11); //  OK
+
+const res12 = await client.json.get("newbike", { path: "$" });
+console.log(res12); //  [[ 'Deimos', { crashes: 0 }, null ]]
+
+const res13 = await client.json.get("newbike", { path: "$[1].crashes" });
+console.log(res13); //  [0]
+
+const res14 = await client.json.del("newbike", { path: "$.[-1]"} );
+console.log(res14); //  1
+
+const res15 = await client.json.get("newbike", { path: "$" });
+console.log(res15); //  [[ 'Deimos', { crashes: 0 } ]]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res15, [["Deimos", {
+  "crashes": 0
+}]]);
+// REMOVE_END
+
+// STEP_START arr2
+const res16 = await client.json.set("riders", "$", []);
+console.log(res16); //  OK
+
+const res17 = await client.json.arrAppend("riders", "$", "Norem");
+console.log(res17); //  [1]
+
+const res18 = await client.json.get("riders", { path: "$" });
+console.log(res18); //  [[ 'Norem' ]]
+
+const res19 = await client.json.arrInsert("riders", "$", 1, "Prickett", "Royse", "Castilla");
+console.log(res19); //  [4]
+
+const res20 = await client.json.get("riders", { path: "$" });
+console.log(res20); //  [[ 'Norem', 'Prickett', 'Royse', 'Castilla' ]]
+
+const res21 = await client.json.arrTrim("riders", "$", 1, 1);
+console.log(res21); //  [1]
+
+const res22 = await client.json.get("riders", { path: "$" });
+console.log(res22); //  [[ 'Prickett' ]]
+
+const res23 = await client.json.arrPop("riders", { path: "$" });
+console.log(res23); //  [ 'Prickett' ]
+
+const res24 = await client.json.arrPop("riders", { path: "$" });
+console.log(res24); //  [null]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res24, [null]);
+// REMOVE_END
+
+// STEP_START obj
+const res25 = await client.json.set(
+  "bike:1", "$", {
+    "model": "Deimos",
+    "brand": "Ergonom",
+    "price": 4972
+  }
+);
+console.log(res25); //  OK
+
+const res26 = await client.json.objLen("bike:1", { path: "$" });
+console.log(res26); //  [3]
+
+const res27 = await client.json.objKeys("bike:1", { path: "$" });
+console.log(res27); //  [['model', 'brand', 'price']]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res27, [
+  ["model", "brand", "price"]
+]);
+// REMOVE_END
+
+// STEP_START set_bikes
+// HIDE_START
+const inventoryJSON = {
+  "inventory": {
+    "mountain_bikes": [{
+        "id": "bike:1",
+        "model": "Phoebe",
+        "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there\u2019s room for mudguards and a rack too.  This is the bike for the rider who wants trail manners with low fuss ownership.",
+        "price": 1920,
+        "specs": {
+          "material": "carbon",
+          "weight": 13.1
+        },
+        "colors": ["black", "silver"],
+      },
+      {
+        "id": "bike:2",
+        "model": "Quaoar",
+        "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and teaawait client. All in all it's an impressive package for the price, making it very competitive.",
+        "price": 2072,
+        "specs": {
+          "material": "aluminium",
+          "weight": 7.9
+        },
+        "colors": ["black", "white"],
+      },
+      {
+        "id": "bike:3",
+        "model": "Weywot",
+        "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.",
+        "price": 3264,
+        "specs": {
+          "material": "alloy",
+          "weight": 13.8
+        },
+      },
+    ],
+    "commuter_bikes": [{
+        "id": "bike:4",
+        "model": "Salacia",
+        "description": "This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance.  It\u2019s for the rider who wants both efficiency and capability.",
+        "price": 1475,
+        "specs": {
+          "material": "aluminium",
+          "weight": 16.6
+        },
+        "colors": ["black", "silver"],
+      },
+      {
+        "id": "bike:5",
+        "model": "Mimas",
+        "description": "A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.",
+        "price": 3941,
+        "specs": {
+          "material": "alloy",
+          "weight": 11.6
+        },
+      },
+    ],
+  }
+};
+// HIDE_END
+
+const res28 = await client.json.set("bikes:inventory", "$", inventoryJSON);
+console.log(res28); //  OK
+// STEP_END
+
+// STEP_START get_bikes
+const res29 = await client.json.get("bikes:inventory", {
+  path: "$.inventory.*"
+});
+console.log(res29);
+/*
+[
+  [
+    {
+      id: 'bike:1',
+      model: 'Phoebe',
+      description: 'This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  This is the bike for the rider who wants trail manners with low fuss ownership.',
+      price: 1920,
+      specs: [Object],
+      colors: [Array]
+    },
+    {
+      id: 'bike:2',
+      model: 'Quaoar',
+      description: "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and teaawait client. All in all it's an impressive package for the price, making it very competitive.",
+      price: 2072,
+      specs: [Object],
+      colors: [Array]
+    },
+    {
+      id: 'bike:3',
+      model: 'Weywot',
+      description: "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.",
+      price: 3264,
+      specs: [Object]
+    }
+  ],
+  [
+    {
+      id: 'bike:4',
+      model: 'Salacia',
+      description: 'This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It’s for the rider who wants both efficiency and capability.',
+      price: 1475,
+      specs: [Object],
+      colors: [Array]
+    },
+    {
+      id: 'bike:5',
+      model: 'Mimas',
+      description: 'A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.',
+      price: 3941,
+      specs: [Object]
+    }
+  ]
+]
+*/
+// STEP_END
+
+// STEP_START get_mtnbikes
+const res30 = await client.json.get("bikes:inventory", {
+  path: "$.inventory.mountain_bikes[*].model"
+});
+console.log(res30); //  ['Phoebe', 'Quaoar', 'Weywot']
+
+const res31 = await client.json.get("bikes:inventory", {
+  path: '$.inventory["mountain_bikes"][*].model'
+});
+console.log(res31); //  ['Phoebe', 'Quaoar', 'Weywot']
+
+const res32 = await client.json.get("bikes:inventory", {
+  path: "$..mountain_bikes[*].model"
+});
+console.log(res32); //  ['Phoebe', 'Quaoar', 'Weywot']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res30, ["Phoebe", "Quaoar", "Weywot"]);
+assert.deepEqual(res31, ["Phoebe", "Quaoar", "Weywot"]);
+assert.deepEqual(res32, ["Phoebe", "Quaoar", "Weywot"]);
+// REMOVE_END
+
+// STEP_START get_models
+const res33 = await client.json.get("bikes:inventory", {
+  path: "$..model"
+});
+console.log(res33); //  ['Phoebe', 'Quaoar', 'Weywot', 'Salacia', 'Mimas']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res33, ["Phoebe", "Quaoar", "Weywot", "Salacia", "Mimas"]);
+// REMOVE_END
+
+// STEP_START get2mtnbikes
+const res34 = await client.json.get("bikes:inventory", {
+  path: "$..mountain_bikes[0:2].model"
+});
+console.log(res34); //  ['Phoebe', 'Quaoar']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res34, ["Phoebe", "Quaoar"]);
+// REMOVE_END
+
+// STEP_START filter1
+const res35 = await client.json.get("bikes:inventory", {
+  path: "$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]"
+});
+console.log(res35);
+/*
+[
+  {
+    id: 'bike:2',
+    model: 'Quaoar',
+    description: "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and teaawait client. All in all it's an impressive package for the price, making it very competitive.",
+    price: 2072,
+    specs: { material: 'aluminium', weight: 7.9 },
+    colors: [ 'black', 'white' ]
+  }
+]
+*/
+// STEP_END
+
+// STEP_START filter2
+//  names of bikes made from an alloy
+const res36 = await client.json.get("bikes:inventory", {
+  path: "$..[?(@.specs.material == 'alloy')].model"
+});
+console.log(res36); //  ['Weywot', 'Mimas']
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res36, ["Weywot", "Mimas"]);
+// REMOVE_END
+
+// STEP_START filter3
+const res37 = await client.json.get("bikes:inventory", {
+  path: "$..[?(@.specs.material =~ '(?i)al')].model"
+});
+console.log(res37); //  ['Quaoar', 'Weywot', 'Salacia', 'Mimas']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res37, ["Quaoar", "Weywot", "Salacia", "Mimas"]);
+// REMOVE_END
+
+// STEP_START filter4
+const res37a = await client.json.set(
+  'bikes:inventory', 
+  '$.inventory.mountain_bikes[0].regex_pat', 
+  '(?i)al'
+);
+
+const res37b = await client.json.set(
+  'bikes:inventory', 
+  '$.inventory.mountain_bikes[1].regex_pat', 
+  '(?i)al'
+);
+
+const res37c = await client.json.set(
+  'bikes:inventory', 
+  '$.inventory.mountain_bikes[2].regex_pat', 
+  '(?i)al'
+);
+
+const res37d = await client.json.get(
+  'bikes:inventory',
+  { path: '$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model' }
+);
+console.log(res37d); // ['Quaoar', 'Weywot']
+// STEP_END
+
+// STEP_START update_bikes
+const res38 = await client.json.get("bikes:inventory", {
+  path: "$..price"
+});
+console.log(res38);  //  [1920, 2072, 3264, 1475, 3941]
+
+const res39 = await client.json.numIncrBy("bikes:inventory", "$..price", -100);
+console.log(res39);  //  [1820, 1972, 3164, 1375, 3841]
+
+const res40 = await client.json.numIncrBy("bikes:inventory", "$..price", 100);
+console.log(res40);  //  [1920, 2072, 3264, 1475, 3941]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res40.sort(), [1475, 1920, 2072, 3264, 3941]);
+// REMOVE_END
+
+// STEP_START update_filters1
+const res40a = await client.json.set(
+  'bikes:inventory', 
+  '$.inventory.*[?(@.price<2000)].price', 
+  1500
+);
+
+// Get all prices from the inventory
+const res40b = await client.json.get(
+  'bikes:inventory',
+  { path: "$..price" }
+);
+console.log(res40b); // [1500, 2072, 3264, 1500, 3941]
+// STEP_END
+
+// STEP_START update_filters2
+const res41 = await client.json.arrAppend(
+    "bikes:inventory", "$.inventory.*[?(@.price<2000)].colors", "pink"
+);
+console.log(res41);  //  [3, 3]
+
+const res42 = await client.json.get("bikes:inventory", {
+  path: "$..[*].colors"
+});
+console.log(res42);  //  [['black', 'silver', 'pink'], ['black', 'white'], ['black', 'silver', 'pink']]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res42, [
+    ["black", "silver", "pink"],
+    ["black", "white"],
+    ["black", "silver", "pink"],
+]);
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-list.js b/doctests/dt-list.js
new file mode 100644
index 00000000000..a2d0fb86c66
--- /dev/null
+++ b/doctests/dt-list.js
@@ -0,0 +1,329 @@
+// EXAMPLE: list_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+// REMOVE_START
+await client.del('bikes:repairs');
+await client.del('bikes:finished');
+// REMOVE_END
+
+// STEP_START queue
+const res1 = await client.lPush('bikes:repairs', 'bike:1');
+console.log(res1);  // 1
+
+const res2 = await client.lPush('bikes:repairs', 'bike:2');
+console.log(res2);  // 2
+
+const res3 = await client.rPop('bikes:repairs');
+console.log(res3);  // bike:1
+
+const res4 = await client.rPop('bikes:repairs');
+console.log(res4);  // bike:2
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 1);
+assert.equal(res2, 2);
+assert.equal(res3, 'bike:1');
+assert.equal(res4, 'bike:2');
+// REMOVE_END
+
+// STEP_START stack
+const res5 = await client.lPush('bikes:repairs', 'bike:1');
+console.log(res5);  // 1
+
+const res6 = await client.lPush('bikes:repairs', 'bike:2');
+console.log(res6); // 2
+
+const res7 = await client.lPop('bikes:repairs');
+console.log(res7);  // bike:2
+
+const res8 = await client.lPop('bikes:repairs');
+console.log(res8);  // bike:1
+// STEP_END
+
+// REMOVE_START
+assert.equal(res5, 1);
+assert.equal(res6, 2);
+assert.equal(res7, 'bike:2');
+assert.equal(res8, 'bike:1');
+// REMOVE_END
+
+// STEP_START lLen
+const res9 = await client.lLen('bikes:repairs');
+console.log(res9); // 0
+// STEP_END
+
+// REMOVE_START
+assert.equal(res9, 0);
+// REMOVE_END
+
+// STEP_START lMove_lRange
+const res10 = await client.lPush('bikes:repairs', 'bike:1');
+console.log(res10);  // 1
+
+const res11 = await client.lPush('bikes:repairs', 'bike:2');
+console.log(res11);  // 2
+
+const res12 = await client.lMove('bikes:repairs', 'bikes:finished', 'LEFT', 'LEFT');
+console.log(res12);  // 'bike:2'
+
+const res13 = await client.lRange('bikes:repairs', 0, -1);
+console.log(res13);  // ['bike:1']
+
+const res14 = await client.lRange('bikes:finished', 0, -1);
+console.log(res14);  // ['bike:2']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res10, 1);
+assert.equal(res11, 2);
+assert.equal(res12, 'bike:2');
+assert.deepEqual(res13, ['bike:1']);
+assert.deepEqual(res14, ['bike:2']);
+await client.del('bikes:repairs');
+// REMOVE_END
+
+// STEP_START lPush_rPush
+const res15 = await client.rPush('bikes:repairs', 'bike:1');
+console.log(res15);  // 1
+
+const res16 = await client.rPush('bikes:repairs', 'bike:2');
+console.log(res16);  // 2
+
+const res17 = await client.lPush('bikes:repairs', 'bike:important_bike');
+console.log(res17);  // 3
+
+const res18 = await client.lRange('bikes:repairs', 0, -1);
+console.log(res18);  // ['bike:important_bike', 'bike:1', 'bike:2']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res15, 1);
+assert.equal(res16, 2);
+assert.equal(res17, 3);
+assert.deepEqual(res18, ['bike:important_bike', 'bike:1', 'bike:2']);
+await client.del('bikes:repairs');
+// REMOVE_END
+
+// STEP_START variadic
+const res19 = await client.rPush('bikes:repairs', ['bike:1', 'bike:2', 'bike:3']);
+console.log(res19);  // 3
+
+const res20 = await client.lPush(
+  'bikes:repairs', ['bike:important_bike', 'bike:very_important_bike']
+);
+console.log(res20);  // 5
+
+const res21 = await client.lRange('bikes:repairs', 0, -1);
+console.log(res21);  // ['bike:very_important_bike', 'bike:important_bike', 'bike:1', 'bike:2', 'bike:3']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res19, 3);
+assert.equal(res20, 5);
+assert.deepEqual(res21, [
+  'bike:very_important_bike',
+  'bike:important_bike',
+  'bike:1',
+  'bike:2',
+  'bike:3',
+]);
+await client.del('bikes:repairs');
+// REMOVE_END
+
+// STEP_START lPop_rPop
+const res22 = await client.rPush('bikes:repairs', ['bike:1', 'bike:2', 'bike:3']);
+console.log(res22);  // 3
+
+const res23 = await client.rPop('bikes:repairs');
+console.log(res23);  // 'bike:3'
+
+const res24 = await client.lPop('bikes:repairs');
+console.log(res24);  // 'bike:1'
+
+const res25 = await client.rPop('bikes:repairs');
+console.log(res25);  // 'bike:2'
+
+const res26 = await client.rPop('bikes:repairs');
+console.log(res26);  // null
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res22, 3);
+assert.equal(res23, 'bike:3');
+assert.equal(res24, 'bike:1');
+assert.equal(res25, 'bike:2');
+assert.equal(res26, null);
+// REMOVE_END
+
+// STEP_START lTrim
+const res27 = await client.lPush(
+  'bikes:repairs', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5']
+);
+console.log(res27);  // 5
+
+const res28 = await client.lTrim('bikes:repairs', 0, 2);
+console.log(res28);  // OK
+
+const res29 = await client.lRange('bikes:repairs', 0, -1);
+console.log(res29);  // ['bike:5', 'bike:4', 'bike:3']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res27, 5);
+assert.equal(res28, 'OK');
+assert.deepEqual(res29, ['bike:5', 'bike:4', 'bike:3']);
+await client.del('bikes:repairs');
+// REMOVE_END
+
+// STEP_START lTrim_end_of_list
+const res27eol = await client.rPush(
+  'bikes:repairs', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5']
+);
+console.log(res27eol);  // 5
+
+const res28eol = await client.lTrim('bikes:repairs', -3, -1);
+console.log(res28eol);  // 'OK'
+
+const res29eol = await client.lRange('bikes:repairs', 0, -1);
+console.log(res29eol);  // ['bike:3', 'bike:4', 'bike:5']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res27eol, 5);
+assert.equal(res28eol, 'OK');
+assert.deepEqual(res29eol, ['bike:3', 'bike:4', 'bike:5']);
+await client.del('bikes:repairs');
+// REMOVE_END
+
+// STEP_START brPop
+const res31 = await client.rPush('bikes:repairs', ['bike:1', 'bike:2']);
+console.log(res31);  // 2
+
+const res32 = await client.brPop('bikes:repairs', 1);
+console.log(res32);  // { key: 'bikes:repairs', element: 'bike:2' }
+
+const res33 = await client.brPop('bikes:repairs', 1);
+console.log(res33);  // { key: 'bikes:repairs', element: 'bike:1' }
+
+const res34 = await client.brPop('bikes:repairs', 1);
+console.log(res34);  // null
+// STEP_END
+
+// REMOVE_START
+assert.equal(res31, 2);
+assert.deepEqual(res32, { key: 'bikes:repairs', element: 'bike:2' });
+assert.deepEqual(res33, { key: 'bikes:repairs', element: 'bike:1' });
+assert.equal(res34, null);
+await client.del('bikes:repairs');
+await client.del('new_bikes');
+// REMOVE_END
+
+// STEP_START rule_1
+const res35 = await client.del('new_bikes');
+console.log(res35); // 0
+
+const res36 = await client.lPush('new_bikes', ['bike:1', 'bike:2', 'bike:3']);
+console.log(res36); // 3
+// STEP_END
+
+// REMOVE_START
+assert.equal(res35, 0);
+assert.equal(res36, 3);
+await client.del('new_bikes');
+// REMOVE_END
+
+// STEP_START rule_1.1
+const res37 = await client.set('new_bikes', 'bike:1');
+console.log(res37);  // 'OK'
+
+const res38 = await client.type('new_bikes');
+console.log(res38);  // 'string'
+
+try {
+  const res39 = await client.lPush('new_bikes', 'bike:2', 'bike:3');
+  // redis.exceptions.ResponseError:
+  // [SimpleError: WRONGTYPE Operation against a key holding the wrong kind of value]
+}
+catch(e){
+  console.log(e);
+}
+// STEP_END
+
+// REMOVE_START
+assert.equal(res37, 'OK');
+assert.equal(res38, 'string');
+await client.del('new_bikes');
+// REMOVE_END
+
+// STEP_START rule_2
+await client.lPush('bikes:repairs', ['bike:1', 'bike:2', 'bike:3']);
+console.log(res36);  // 3
+
+const res40 = await client.exists('bikes:repairs')
+console.log(res40);  // 1
+
+const res41 = await client.lPop('bikes:repairs');
+console.log(res41);  // 'bike:3'
+
+const res42 = await client.lPop('bikes:repairs');
+console.log(res42);  // 'bike:2'
+
+const res43 = await client.lPop('bikes:repairs');
+console.log(res43);  // 'bike:1'
+
+const res44 = await client.exists('bikes:repairs');
+console.log(res44);  // 0
+// STEP_END
+
+// REMOVE_START
+assert.equal(res40, 1);
+assert.equal(res41, 'bike:3');
+assert.equal(res42, 'bike:2');
+assert.equal(res43, 'bike:1');
+assert.equal(res44, 0);
+await client.del('bikes:repairs');
+// REMOVE_END
+
+// STEP_START rule_3
+const res45 = await client.del('bikes:repairs');
+console.log(res45);  // 0
+
+const res46 = await client.lLen('bikes:repairs');
+console.log(res46);  // 0
+
+const res47 = await client.lPop('bikes:repairs');
+console.log(res47);  // null
+// STEP_END
+
+// REMOVE_START
+assert.equal(res45, 0);
+assert.equal(res46, 0);
+assert.equal(res47, null);
+// REMOVE_END
+
+// STEP_START lTrim.1
+const res48 = await client.lPush(
+  'bikes:repairs', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5']
+);
+console.log(res48);  // 5
+
+const res49 = await client.lTrim('bikes:repairs', 0, 2);
+console.log(res49);  // 'OK'
+
+const res50 = await client.lRange('bikes:repairs', 0, -1);
+console.log(res50);  // ['bike:5', 'bike:4', 'bike:3']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res48, 5);
+assert.equal(res49, 'OK');
+assert.deepEqual(res50, ['bike:5', 'bike:4', 'bike:3']);
+await client.del('bikes:repairs');
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/dt-set.js b/doctests/dt-set.js
new file mode 100644
index 00000000000..e3d34096399
--- /dev/null
+++ b/doctests/dt-set.js
@@ -0,0 +1,176 @@
+// EXAMPLE: sets_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+// REMOVE_START
+await client.del('bikes:racing:france')
+await client.del('bikes:racing:usa')
+// REMOVE_END
+
+// STEP_START sAdd
+const res1 = await client.sAdd('bikes:racing:france', 'bike:1')
+console.log(res1)  // >>> 1
+
+const res2 = await client.sAdd('bikes:racing:france', 'bike:1')
+console.log(res2)  // >>> 0
+const res3 = await client.sAdd('bikes:racing:france', ['bike:2', 'bike:3'])
+console.log(res3)  // >>> 2
+const res4 = await client.sAdd('bikes:racing:usa', ['bike:1', 'bike:4'])
+console.log(res4)  // >>> 2
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 1)
+assert.equal(res2, 0)
+assert.equal(res3, 2) 
+assert.equal(res4, 2) 
+// REMOVE_END
+
+// STEP_START sIsMember
+// HIDE_START
+await client.del('bikes:racing:france')
+await client.del('bikes:racing:usa')
+await client.sAdd('bikes:racing:france', 'bike:1', 'bike:2', 'bike:3')
+await client.sAdd('bikes:racing:usa', 'bike:1', 'bike:4')
+// HIDE_END
+const res5 = await client.sIsMember('bikes:racing:usa', 'bike:1')
+console.log(res5)  // >>> 1
+
+const res6 = await client.sIsMember('bikes:racing:usa', 'bike:2')
+console.log(res6)  // >>> 0
+// STEP_END
+
+// REMOVE_START
+assert.equal(res5, 1)
+assert.equal(res6, 0)
+// REMOVE_END
+
+// STEP_START sinster
+// HIDE_START
+await client.del('bikes:racing:france')
+await client.del('bikes:racing:usa')
+await client.sAdd('bikes:racing:france', 'bike:1', 'bike:2', 'bike:3')
+await client.sAdd('bikes:racing:usa', 'bike:1', 'bike:4')
+// HIDE_END
+const res7 = await client.sInter('bikes:racing:france', 'bikes:racing:usa')
+console.log(res7)  // >>> {'bike:1'}
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res7, [ 'bike:1' ])
+// REMOVE_END
+
+// STEP_START sCard
+// HIDE_START
+await client.del('bikes:racing:france')
+await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3'])
+// HIDE_END
+const res8 = await client.sCard('bikes:racing:france')
+console.log(res8)  // >>> 3
+// STEP_END
+
+// REMOVE_START
+assert.equal(res8, 3)
+await client.del('bikes:racing:france')
+// REMOVE_END
+
+// STEP_START sAdd_sMembers
+const res9 = await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3'])
+console.log(res9)  // >>> 3
+
+const res10 = await client.sMembers('bikes:racing:france')
+console.log(res10)  // >>> ['bike:1', 'bike:2', 'bike:3']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res9, 3)
+assert.deepEqual(res10.sort(), ['bike:1', 'bike:2', 'bike:3'])
+// REMOVE_END
+
+// STEP_START smIsMember
+const res11 = await client.sIsMember('bikes:racing:france', 'bike:1')
+console.log(res11)  // >>> 1
+
+const res12 = await client.smIsMember('bikes:racing:france', ['bike:2', 'bike:3', 'bike:4'])
+console.log(res12)  // >>> [1, 1, 0]
+// STEP_END
+
+// REMOVE_START
+assert.equal(res11, 1)
+assert.deepEqual(res12, [1, 1, 0])
+// REMOVE_END
+
+// STEP_START sDiff
+await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3'])
+await client.sAdd('bikes:racing:usa', ['bike:1', 'bike:4'])
+const res13 = await client.sDiff(['bikes:racing:france', 'bikes:racing:usa'])
+console.log(res13)  // >>> [ 'bike:2', 'bike:3' ]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res13.sort(), ['bike:2', 'bike:3'].sort())
+await client.del('bikes:racing:france')
+await client.del('bikes:racing:usa')
+// REMOVE_END
+
+// STEP_START multisets
+await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3'])
+await client.sAdd('bikes:racing:usa', ['bike:1', 'bike:4'])
+await client.sAdd('bikes:racing:italy', ['bike:1', 'bike:2', 'bike:3', 'bike:4'])
+
+const res14 = await client.sInter(
+  ['bikes:racing:france', 'bikes:racing:usa', 'bikes:racing:italy']
+)
+console.log(res14)  // >>> ['bike:1']
+
+const res15 = await client.sUnion(
+  ['bikes:racing:france', 'bikes:racing:usa', 'bikes:racing:italy']
+)
+console.log(res15)  // >>> ['bike:1', 'bike:2', 'bike:3', 'bike:4']
+
+const res16 = await client.sDiff(['bikes:racing:france', 'bikes:racing:usa', 'bikes:racing:italy'])
+console.log(res16)  // >>> []
+
+const res17 = await client.sDiff(['bikes:racing:usa', 'bikes:racing:france'])
+console.log(res17)  // >>> ['bike:4']
+
+const res18 = await client.sDiff(['bikes:racing:france', 'bikes:racing:usa'])
+console.log(res18)  // >>> ['bike:2', 'bike:3']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res14, ['bike:1'])
+assert.deepEqual(res15.sort(), ['bike:1', 'bike:2', 'bike:3', 'bike:4'])
+assert.deepEqual(res16, [])
+assert.deepEqual(res17, ['bike:4'])
+assert.deepEqual(res18.sort(), ['bike:2', 'bike:3'].sort())
+await client.del('bikes:racing:france')
+await client.del('bikes:racing:usa')
+await client.del('bikes:racing:italy')
+// REMOVE_END
+
+// STEP_START sRem
+await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5'])
+
+const res19 = await client.sRem('bikes:racing:france', 'bike:1')
+console.log(res19)  // >>> 1
+
+const res20 = await client.sPop('bikes:racing:france')
+console.log(res20)  // >>> bike:3 or other random value
+
+const res21 = await client.sMembers('bikes:racing:france')
+console.log(res21)  // >>> ['bike:2', 'bike:4', 'bike:5']; depends on previous result
+
+const res22 = await client.sRandMember('bikes:racing:france')
+console.log(res22)  // >>> bike:4 or other random value
+// STEP_END
+
+// REMOVE_START
+assert.equal(res19, 1)
+await client.close()
+// none of the other results are deterministic
+// REMOVE_END
diff --git a/doctests/dt-ss.js b/doctests/dt-ss.js
new file mode 100644
index 00000000000..bba85b11580
--- /dev/null
+++ b/doctests/dt-ss.js
@@ -0,0 +1,162 @@
+// EXAMPLE: ss_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START zadd
+const res1 = await client.zAdd('racer_scores', { score: 10, value: 'Norem' });
+console.log(res1);  // >>> 1
+
+const res2 = await client.zAdd('racer_scores', { score: 12, value: 'Castilla' });
+console.log(res2);  // >>> 1
+
+const res3 = await client.zAdd('racer_scores', [
+  { score: 8, value: 'Sam-Bodden' },
+  { score: 10, value: 'Royce' },
+  { score: 6, value: 'Ford' },
+  { score: 14, value: 'Prickett' },
+  { score: 12, value: 'Castilla' }
+]);
+console.log(res3);  // >>> 4
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 1)
+assert.equal(res2, 1)
+assert.equal(res3, 4)
+// REMOVE_END
+
+// REMOVE_START
+const count = await client.zCard('racer_scores');
+console.assert(count === 6);
+// REMOVE_END
+
+// STEP_START zrange
+const res4 = await client.zRange('racer_scores', 0, -1);
+console.log(res4);  // >>> ['Ford', 'Sam-Bodden', 'Norem', 'Royce', 'Castilla', 'Prickett']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res4, ['Ford', 'Sam-Bodden', 'Norem', 'Royce', 'Castilla', 'Prickett'])
+// REMOVE_END
+
+// STEP_START zrange_withscores
+const res6 = await client.zRangeWithScores('racer_scores', 0, -1);
+console.log(res6);
+// >>> [
+//       { value: 'Ford', score: 6 }, { value: 'Sam-Bodden', score: 8 },
+//       { value: 'Norem', score: 10 }, { value: 'Royce', score: 10 },
+//       { value: 'Castilla', score: 12 }, { value: 'Prickett', score: 14 }
+// ]
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res6, [ { value: 'Ford', score: 6 }, { value: 'Sam-Bodden', score: 8 }, { value: 'Norem', score: 10 }, { value: 'Royce', score: 10 }, { value: 'Castilla', score: 12 }, { value: 'Prickett', score: 14 } ]
+)
+// REMOVE_END
+
+// STEP_START zrangebyscore
+const res7 = await client.zRangeByScore('racer_scores', '-inf', 10);
+console.log(res7);  // >>> ['Ford', 'Sam-Bodden', 'Norem', 'Royce']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res7, ['Ford', 'Sam-Bodden', 'Norem', 'Royce'])
+// REMOVE_END
+
+// STEP_START zremrangebyscore
+const res8 = await client.zRem('racer_scores', 'Castilla');
+console.log(res8);  // >>> 1
+
+const res9 = await client.zRemRangeByScore('racer_scores', '-inf', 9);
+console.log(res9);  // >>> 2
+
+// REMOVE_START
+assert.equal(res8, 1)
+assert.equal(res9, 2)
+// REMOVE_END
+
+const res10 = await client.zRange('racer_scores', 0, -1);
+console.log(res10);  // >>> ['Norem', 'Royce', 'Prickett']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res10, ['Norem', 'Royce', 'Prickett'])
+// REMOVE_END
+
+// REMOVE_START
+const count2 = await client.zCard('racer_scores');
+console.assert(count2 === 3);
+// REMOVE_END
+
+// STEP_START zrank
+const res11 = await client.zRank('racer_scores', 'Norem');
+console.log(res11);  // >>> 0
+
+const res12 = await client.zRevRank('racer_scores', 'Norem');
+console.log(res12);  // >>> 2
+// STEP_END
+
+// STEP_START zadd_lex
+const res13 = await client.zAdd('racer_scores', [
+  { score: 0, value: 'Norem' },
+  { score: 0, value: 'Sam-Bodden' },
+  { score: 0, value: 'Royce' },
+  { score: 0, value: 'Ford' },
+  { score: 0, value: 'Prickett' },
+  { score: 0, value: 'Castilla' }
+]);
+console.log(res13);  // >>> 3
+
+// REMOVE_START
+assert.equal(count2, 3)
+assert.equal(res11, 0)
+assert.equal(res12, 2)
+assert.equal(res13, 3)
+// REMOVE_END
+
+const res14 = await client.zRange('racer_scores', 0, -1);
+console.log(res14);  // >>> ['Castilla', 'Ford', 'Norem', 'Prickett', 'Royce', 'Sam-Bodden']
+
+const res15 = await client.zRangeByLex('racer_scores', '[A', '[L');
+console.log(res15);  // >>> ['Castilla', 'Ford']
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res14, ['Castilla', 'Ford', 'Norem', 'Prickett', 'Royce', 'Sam-Bodden'])
+assert.deepEqual(res15, ['Castilla', 'Ford'])
+// REMOVE_END
+
+// STEP_START leaderboard
+const res16 = await client.zAdd('racer_scores', { score: 100, value: 'Wood' });
+console.log(res16);  // >>> 1
+
+const res17 = await client.zAdd('racer_scores', { score: 100, value: 'Henshaw' });
+console.log(res17);  // >>> 1
+
+const res18 = await client.zAdd('racer_scores', { score: 150, value: 'Henshaw' }, { nx: true });
+console.log(res18);  // >>> 0
+
+const res19 = await client.zIncrBy('racer_scores', 50, 'Wood');
+console.log(res19);  // >>> 150.0
+
+const res20 = await client.zIncrBy('racer_scores', 50, 'Henshaw');
+console.log(res20);  // >>> 200.0
+// STEP_END
+
+// REMOVE_START
+assert.equal(res16, 1)
+assert.equal(res17, 1)
+assert.equal(res18, 0)
+assert.equal(res19, 150.0)
+assert.equal(res20, 200.0)
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-streams.js b/doctests/dt-streams.js
new file mode 100644
index 00000000000..00768654730
--- /dev/null
+++ b/doctests/dt-streams.js
@@ -0,0 +1,366 @@
+// EXAMPLE: stream_tutorial
+// HIDE_START
+import assert from 'assert';
+import {
+  createClient
+} from 'redis';
+
+const client = await createClient();
+await client.connect();
+// HIDE_END
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START xAdd
+const res1 = await client.xAdd(
+  'race:france', '*', {
+    'rider': 'Castilla',
+    'speed': '30.2',
+    'position': '1',
+    'location_id': '1'
+  }
+);
+console.log(res1); // >>> 1700073067968-0 N.B. actual values will differ from these examples
+
+const res2 = await client.xAdd(
+  'race:france', '*', {
+    'rider': 'Norem',
+    'speed': '28.8',
+    'position': '3',
+    'location_id': '1'
+  },
+);
+console.log(res2); // >>> 1692629594113-0
+
+const res3 = await client.xAdd(
+  'race:france', '*', {
+    'rider': 'Prickett',
+    'speed': '29.7',
+    'position': '2',
+    'location_id': '1'
+  },
+);
+console.log(res3); // >>> 1692629613374-0
+// STEP_END
+
+// REMOVE_START
+assert.equal(await client.xLen('race:france'), 3);
+// REMOVE_END
+
+// STEP_START xRange
+const res4 = await client.xRange('race:france', '1691765278160-0', '+', {COUNT: 2});
+console.log(res4); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }]
+// STEP_END
+
+// STEP_START xread_block
+const res5 = await client.xRead({
+  key: 'race:france',
+  id: '0-0'
+}, {
+  COUNT: 100,
+  BLOCK: 300
+});
+console.log(res5); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }] }]
+// STEP_END
+
+// STEP_START xAdd_2
+const res6 = await client.xAdd(
+  'race:france', '*', {
+    'rider': 'Castilla',
+    'speed': '29.9',
+    'position': '1',
+    'location_id': '2'
+  }
+);
+console.log(res6); // >>> 1692629676124-0
+// STEP_END
+
+// STEP_START xlen
+const res7 = await client.xLen('race:france');
+console.log(res7); // >>> 4
+// STEP_END
+
+
+// STEP_START xAdd_id
+const res8 = await client.xAdd('race:usa', '0-1', {
+  'racer': 'Castilla'
+});
+console.log(res8); // >>> 0-1
+
+const res9 = await client.xAdd('race:usa', '0-2', {
+  'racer': 'Norem'
+});
+console.log(res9); // >>> 0-2
+// STEP_END
+
+// STEP_START xAdd_bad_id
+try {
+  const res10 = await client.xAdd('race:usa', '0-1', {
+    'racer': 'Prickett'
+  });
+  console.log(res10); // >>> 0-1
+} catch (error) {
+  console.error(error); // >>> [SimpleError: ERR The ID specified in XADD is equal or smaller than the target stream top item]
+}
+// STEP_END
+
+// STEP_START xadd_7
+const res11a = await client.xAdd('race:usa', '0-*', { racer: 'Norem' });
+console.log(res11a); // >>> 0-3
+// STEP_END
+
+// STEP_START xRange_all
+const res11 = await client.xRange('race:france', '-', '+');
+console.log(res11); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }]
+// STEP_END
+
+// STEP_START xRange_time
+const res12 = await client.xRange('race:france', '1692629576965', '1692629576967');
+console.log(res12); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }]
+// STEP_END
+
+// STEP_START xRange_step_1
+const res13 = await client.xRange('race:france', '-', '+', {COUNT: 2});
+console.log(res13); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }]
+// STEP_END
+
+// STEP_START xRange_step_2
+const res14 = await client.xRange('race:france', '(1692629594113-0', '+', {COUNT: 2});
+console.log(res14); // >>> [{ id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }]
+// STEP_END
+
+// STEP_START xRange_empty
+const res15 = await client.xRange('race:france', '(1692629676124-0', '+', {COUNT: 2});
+console.log(res15); // >>> []
+// STEP_END
+
+// STEP_START xrevrange
+const res16 = await client.xRevRange('race:france', '+', '-', {COUNT: 1});
+console.log(
+  res16
+); // >>> [{ id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }]
+// STEP_END
+
+// STEP_START xread
+const res17 = await client.xRead({
+  key: 'race:france',
+  id: '0-0'
+}, {
+  COUNT: 2
+});
+console.log(res17); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] }]
+// STEP_END
+
+// STEP_START xgroup_create
+const res18 = await client.xGroupCreate('race:france', 'france_riders', '$');
+console.log(res18); // >>> OK
+// STEP_END
+
+// STEP_START xgroup_create_mkstream
+const res19 = await client.xGroupCreate('race:italy', 'italy_riders', '$', {
+  MKSTREAM: true
+});
+console.log(res19); // >>> OK
+// STEP_END
+
+// STEP_START xgroup_read
+await client.xAdd('race:italy', '*', {
+  'rider': 'Castilla'
+});
+await client.xAdd('race:italy', '*', {
+  'rider': 'Royce'
+});
+await client.xAdd('race:italy', '*', {
+  'rider': 'Sam-Bodden'
+});
+await client.xAdd('race:italy', '*', {
+  'rider': 'Prickett'
+});
+await client.xAdd('race:italy', '*', {
+  'rider': 'Norem'
+});
+
+const res20 = await client.xReadGroup(
+  'italy_riders',
+  'Alice', {
+    key: 'race:italy',
+    id: '>'
+  }, {
+    COUNT: 1
+  }
+);
+console.log(res20); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }]
+// STEP_END
+
+// STEP_START xgroup_read_id
+const res21 = await client.xReadGroup(
+  'italy_riders',
+  'Alice', {
+    key: 'race:italy',
+    id: '0'
+  }, {
+    COUNT: 1
+  }
+);
+console.log(res21); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }]
+// STEP_END
+
+// STEP_START xack
+const res22 = await client.xAck('race:italy', 'italy_riders', '1692629925771-0')
+console.log(res22); // >>> 1
+
+const res23 = await client.xReadGroup(
+  'italy_riders',
+  'Alice', {
+    key: 'race:italy',
+    id: '0'
+  }, {
+    COUNT: 1
+  }
+);
+console.log(res23); // >>> [{ name: 'race:italy', messages: [] }]
+// STEP_END
+
+// STEP_START xgroup_read_bob
+const res24 = await client.xReadGroup(
+  'italy_riders',
+  'Bob', {
+    key: 'race:italy',
+    id: '>'
+  }, {
+    COUNT: 2
+  }
+);
+console.log(res24); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }] }]
+// STEP_END
+
+// STEP_START xpending
+const res25 = await client.xPending('race:italy', 'italy_riders');
+console.log(res25); // >>> {'pending': 2, 'firstId': '1692629925789-0', 'lastId': '1692629925790-0', 'consumers': [{'name': 'Bob', 'deliveriesCounter': 2}]}
+// STEP_END
+
+// STEP_START xpending_plus_minus
+const res26 = await client.xPendingRange('race:italy', 'italy_riders', '-', '+', 10);
+console.log(res26); // >>> [{'id': '1692629925789-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter:': 1}, {'id': '1692629925790-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter': 1}]
+// STEP_END
+
+// STEP_START xRange_pending
+const res27 = await client.xRange('race:italy', '1692629925789-0', '1692629925789-0');
+console.log(res27); // >>> [{ id: '1692629925789-0', message: { rider: 'Royce' } }]
+// STEP_END
+
+// STEP_START xclaim
+const res28 = await client.xClaim(
+  'race:italy', 'italy_riders', 'Alice', 60000, ['1692629925789-0']
+);
+console.log(res28); // >>> [{ id: '1692629925789-0', message: { rider: 'Royce' } }]
+// STEP_END
+
+// STEP_START xautoclaim
+const res29 = await client.xAutoClaim('race:italy', 'italy_riders', 'Alice', 1, '0-0', {
+  COUNT: 1
+});
+console.log(res29); // >>> { nextId: '1692629925790-0', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }], deletedMessages: [] }
+// STEP_END
+
+// STEP_START xautoclaim_cursor
+const res30 = await client.xAutoClaim(
+  'race:italy', 'italy_riders', 'Alice', 1, '(1692629925789-0',
+  {
+    COUNT: 1
+  }
+);
+console.log(res30); // >>> { nextId: '0-0', messages: [{ id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }], deletedMessages: [] }
+// STEP_END
+
+// STEP_START xinfo
+const res31 = await client.xInfoStream('race:italy');
+console.log(res31); // >>> { length: 5, 'radix-tree-keys': 1, 'radix-tree-nodes': 2, 'last-generated-id': '1692629926436-0', 'max-deleted-entry-id': '0-0', 'entries-added': 5, 'recorded-first-entry-id': '1692629925771-0', groups: 1, 'first-entry': { id: '1692629925771-0', message: { rider: 'Castilla' } }, 'last-entry': { id: '1692629926436-0', message: { rider: 'Norem' } } }
+// STEP_END
+
+// STEP_START xinfo_groups
+const res32 = await client.xInfoGroups('race:italy');
+console.log(res32); // >>> [{ name: 'italy_riders', consumers: 2, pending: 3, 'last-delivered-id': '1692629925790-0', 'entries-read': 3, lag: 2 }]
+// STEP_END
+
+// STEP_START xinfo_consumers
+const res33 = await client.xInfoConsumers('race:italy', 'italy_riders');
+console.log(res33); // >>> [{ name: 'Alice', pending: 3, idle: 170582, inactive: 170582 }, { name: 'Bob', pending: 0, idle: 489404, inactive: 489404 }]
+// STEP_END
+
+// STEP_START maxlen
+await client.xAdd('race:italy', '*', {
+  'rider': 'Jones'
+}, {
+  TRIM: {
+    strategy: 'MAXLEN',
+    strategyModifier: '~',
+    threshold: 2
+  }
+});
+await client.xAdd('race:italy', '*', {
+  'rider': 'Wood'
+}, {
+  TRIM: {
+    strategy: 'MAXLEN',
+    strategyModifier: '~',
+    threshold: 2
+  }
+});
+await client.xAdd('race:italy', '*', {
+  'rider': 'Henshaw'
+}, {
+  TRIM: {
+    strategy: 'MAXLEN',
+    strategyModifier: '~',
+    threshold: 2
+  }
+});
+
+const res34 = await client.xLen('race:italy');
+console.log(res34); // >>> 8
+
+const res35 = await client.xRange('race:italy', '-', '+');
+console.log(res35); // >>> [{ id: '1692629925771-0', message: { rider: 'Castilla' } }, { id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }, { id: '1692629925791-0', message: { rider: 'Prickett' } }, { id: '1692629926436-0', message: { rider: 'Norem' } }, { id: '1692630612602-0', message: { rider: 'Jones' } }, { id: '1692630641947-0', message: { rider: 'Wood' } }, { id: '1692630648281-0', message: { rider: 'Henshaw' } }]
+
+await client.xAdd('race:italy', '*', {
+  'rider': 'Smith'
+}, {
+  TRIM: {
+    strategy: 'MAXLEN',
+    strategyModifier: '=',
+    threshold: 2
+  }
+});
+
+const res36 = await client.xRange('race:italy', '-', '+');
+console.log(res36); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }, { id: '1692631018238-0', message: { rider: 'Smith' } }]
+// STEP_END
+
+// STEP_START xTrim
+const res37 = await client.xTrim('race:italy', 'MAXLEN', 10, {
+  strategyModifier: '=',
+});
+console.log(res37); // >>> 0
+// STEP_END
+
+// STEP_START xTrim2
+const res38 = await client.xTrim('race:italy', "MAXLEN", 10);
+console.log(res38); // >>> 0
+// STEP_END
+
+// STEP_START xDel
+const res39 = await client.xRange('race:italy', '-', '+');
+console.log(res39); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }, { id: '1692631018238-0', message: { rider: 'Smith' } }]
+
+const res40 = await client.xDel('race:italy', '1692631018238-0');
+console.log(res40); // >>> 1
+
+const res41 = await client.xRange('race:italy', '-', '+');
+console.log(res41); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }]
+// STEP_END
+
+// REMOVE_START
+await client.quit();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/dt-string.js b/doctests/dt-string.js
new file mode 100644
index 00000000000..6f77709abfa
--- /dev/null
+++ b/doctests/dt-string.js
@@ -0,0 +1,68 @@
+// EXAMPLE: set_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START set_get
+const res1 = await client.set("bike:1", "Deimos");
+console.log(res1);  // OK
+const res2 = await client.get("bike:1");
+console.log(res2);  // Deimos
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 'OK');
+assert.equal(res2, 'Deimos');
+// REMOVE_END
+
+// STEP_START setnx_xx
+const res3 = await client.set("bike:1", "bike", {'NX': true});
+console.log(res3);  // null
+console.log(await client.get("bike:1"));  // Deimos
+const res4 = await client.set("bike:1", "bike", {'XX': true});
+console.log(res4);  // OK
+// STEP_END
+
+// REMOVE_START
+assert.equal(res3, null);
+assert.equal(res4, 'OK');
+// REMOVE_END
+
+// STEP_START mset
+const res5 = await client.mSet([
+  ["bike:1", "Deimos"],
+  ["bike:2", "Ares"],
+  ["bike:3", "Vanth"]
+]);
+
+console.log(res5);  // OK
+const res6 = await client.mGet(["bike:1", "bike:2", "bike:3"]);
+console.log(res6);  // ['Deimos', 'Ares', 'Vanth']
+// STEP_END
+
+// REMOVE_START
+assert.equal(res5, 'OK');
+assert.deepEqual(res6, ["Deimos", "Ares", "Vanth"]);
+// REMOVE_END
+
+// STEP_START incr
+await client.set("total_crashes", 0);
+const res7 = await client.incr("total_crashes");
+console.log(res7); // 1
+const res8 = await client.incrBy("total_crashes", 10);
+console.log(res8); // 11
+// STEP_END
+
+// REMOVE_START
+assert.equal(res7, 1);
+assert.equal(res8, 11);
+
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-tdigest.js b/doctests/dt-tdigest.js
new file mode 100644
index 00000000000..c59168076e3
--- /dev/null
+++ b/doctests/dt-tdigest.js
@@ -0,0 +1,85 @@
+// EXAMPLE: tdigest_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START tdig_start
+const res1 = await client.tDigest.create('bikes:sales', 100);
+console.log(res1);  // >>> OK
+
+const res2 = await client.tDigest.add('bikes:sales', [21]);
+console.log(res2);  // >>> OK
+
+const res3 = await client.tDigest.add('bikes:sales', [150, 95, 75, 34]);
+console.log(res3);  // >>> OK
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 'OK')
+assert.equal(res2, 'OK')
+assert.equal(res3, 'OK')
+// REMOVE_END
+
+// STEP_START tdig_cdf
+const res4 = await client.tDigest.create('racer_ages');
+console.log(res4);  // >>> OK
+
+const res5 = await client.tDigest.add('racer_ages', [
+  45.88, 44.2, 58.03, 19.76, 39.84, 69.28, 50.97, 25.41, 19.27, 85.71, 42.63
+]);
+console.log(res5);  // >>> OK
+
+const res6 = await client.tDigest.rank('racer_ages', [50]);
+console.log(res6);  // >>> [7]
+
+const res7 = await client.tDigest.rank('racer_ages', [50, 40]);
+console.log(res7);  // >>> [7, 4]
+// STEP_END
+
+// REMOVE_START
+assert.equal(res4, 'OK')
+assert.equal(res5, 'OK')
+assert.deepEqual(res6, [7])
+assert.deepEqual(res7, [7, 4])
+// REMOVE_END
+
+// STEP_START tdig_quant
+const res8 = await client.tDigest.quantile('racer_ages', [0.5]);
+console.log(res8);  // >>> [44.2]
+
+const res9 = await client.tDigest.byRank('racer_ages', [4]);
+console.log(res9);  // >>> [42.63]
+// STEP_END
+
+// STEP_START tdig_min
+const res10 = await client.tDigest.min('racer_ages');
+console.log(res10);  // >>> 19.27
+
+const res11 = await client.tDigest.max('racer_ages');
+console.log(res11);  // >>> 85.71
+// STEP_END
+
+// REMOVE_START
+assert.deepEqual(res8, [44.2])
+assert.deepEqual(res9, [42.63])
+assert.equal(res10, 19.27)
+assert.equal(res11, 85.71)
+// REMOVE_END
+
+// STEP_START tdig_reset
+const res12 = await client.tDigest.reset('racer_ages');
+console.log(res12);  // >>> OK
+// STEP_END
+
+// REMOVE_START
+assert.equal(res12, 'OK')
+await client.close();
+// REMOVE_END
diff --git a/doctests/dt-time-series.js b/doctests/dt-time-series.js
new file mode 100644
index 00000000000..d2d94e7dc40
--- /dev/null
+++ b/doctests/dt-time-series.js
@@ -0,0 +1,635 @@
+// EXAMPLE: time_series_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+import { TIME_SERIES_AGGREGATION_TYPE, TIME_SERIES_REDUCERS } from '@redis/time-series';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.del([
+  'thermometer:1', 'thermometer:2', 'thermometer:3',
+  'rg:1', 'rg:2', 'rg:3', 'rg:4',
+  'sensor3',
+  'wind:1', 'wind:2', 'wind:3', 'wind:4',
+  'hyg:1', 'hyg:compacted'
+]);
+// REMOVE_END
+
+// STEP_START create
+const res1 = await client.ts.create('thermometer:1');
+console.log(res1); // >>> OK
+
+const res2 = await client.type('thermometer:1');
+console.log(res2); // >>> TSDB-TYPE
+
+const res3 = await client.ts.info('thermometer:1');
+console.log(res3);
+// >>> { rules: [], ... totalSamples: 0, ...
+// STEP_END
+// REMOVE_START
+assert.equal(res1, 'OK');
+assert.equal(res2, 'TSDB-TYPE');
+assert.equal(res3.totalSamples, 0);
+// REMOVE_END
+
+// STEP_START create_retention
+const res4 = await client.ts.add('thermometer:2', 1, 10.8, { RETENTION: 100 });
+console.log(res4); // >>> 1
+
+const res5 = await client.ts.info('thermometer:2');
+console.log(res5);
+// >>> { rules: [], ... retentionTime: 100, ...
+// STEP_END
+// REMOVE_START
+assert.equal(res4, 1);
+assert.equal(res5.retentionTime, 100);
+// REMOVE_END
+
+// STEP_START create_labels
+const res6 = await client.ts.add('thermometer:3', 1, 10.4, {
+  LABELS: { location: 'UK', type: 'Mercury' }
+});
+console.log(res6); // >>> 1
+
+const res7 = await client.ts.info('thermometer:3');
+console.log(res7);
+// >>> { labels: [{ name: 'location', value: 'UK' }, { name: 'type', value: 'Mercury' }], ... }
+// STEP_END
+// REMOVE_START
+assert.equal(res6, 1);
+assert.deepEqual(res7.labels, [
+  { name: 'location', value: 'UK' },
+  { name: 'type', value: 'Mercury' },
+]);
+// REMOVE_END
+
+// STEP_START madd
+const res8 = await client.ts.mAdd([
+  { key: 'thermometer:1', timestamp: 1, value: 9.2 },
+  { key: 'thermometer:1', timestamp: 2, value: 9.9 },
+  { key: 'thermometer:2', timestamp: 2, value: 10.3 }
+]);
+console.log(res8); // >>> [1, 2, 2]
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res8, [1, 2, 2]);
+// REMOVE_END
+
+// STEP_START get
+// The last recorded temperature for thermometer:2
+// was 10.3 at time 2.
+const res9 = await client.ts.get('thermometer:2');
+console.log(res9); // >>> { timestamp: 2, value: 10.3 }
+// STEP_END
+// REMOVE_START
+assert.equal(res9.timestamp, 2);
+assert.equal(res9.value, 10.3);
+// REMOVE_END
+
+// STEP_START range
+// Add 5 data points to a time series named "rg:1".
+const res10 = await client.ts.create('rg:1');
+console.log(res10); // >>> OK
+
+const res11 = await client.ts.mAdd([
+  { key: 'rg:1', timestamp: 0, value: 18 },
+  { key: 'rg:1', timestamp: 1, value: 14 },
+  { key: 'rg:1', timestamp: 2, value: 22 },
+  { key: 'rg:1', timestamp: 3, value: 18 },
+  { key: 'rg:1', timestamp: 4, value: 24 }
+]);
+console.log(res11); // >>> [0, 1, 2, 3, 4]
+
+// Retrieve all the data points in ascending order.
+const res12 = await client.ts.range('rg:1', '-', '+');
+console.log(res12);
+// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }, ...]
+
+// Retrieve data points up to time 1 (inclusive).
+const res13 = await client.ts.range('rg:1', '-', 1);
+console.log(res13);
+// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }]
+
+// Retrieve data points from time 3 onwards.
+const res14 = await client.ts.range('rg:1', 3, '+');
+console.log(res14);
+// >>> [{ timestamp: 3, value: 18 }, { timestamp: 4, value: 24 }]
+
+// Retrieve all the data points in descending order.
+const res15 = await client.ts.revRange('rg:1', '-', '+');
+console.log(res15);
+// >>> [{ timestamp: 4, value: 24 }, { timestamp: 3, value: 18 }, ...]
+
+// Retrieve data points up to time 1 (inclusive), but return them
+// in descending order.
+const res16 = await client.ts.revRange('rg:1', '-', 1);
+console.log(res16);
+// >>> [{ timestamp: 1, value: 14 }, { timestamp: 0, value: 18 }]
+// STEP_END
+// REMOVE_START
+assert.equal(res10, 'OK');
+assert.deepEqual(res11, [0, 1, 2, 3, 4]);
+
+assert.deepEqual(res12, [
+  { timestamp: 0, value: 18 },
+  { timestamp: 1, value: 14 },
+  { timestamp: 2, value: 22 },
+  { timestamp: 3, value: 18 },
+  { timestamp: 4, value: 24 }
+]);
+assert.deepEqual(res13, [
+  { timestamp: 0, value: 18 },
+  { timestamp: 1, value: 14 }
+]);
+assert.deepEqual(res14, [
+  { timestamp: 3, value: 18 },
+  { timestamp: 4, value: 24 }
+]);
+assert.deepEqual(res15, [
+  { timestamp: 4, value: 24 },
+  { timestamp: 3, value: 18 },
+  { timestamp: 2, value: 22 },
+  { timestamp: 1, value: 14 },
+  { timestamp: 0, value: 18 }
+]);
+assert.deepEqual(res16, [
+  { timestamp: 1, value: 14 },
+  { timestamp: 0, value: 18 }
+]);
+// REMOVE_END
+
+// STEP_START range_filter
+const res17 = await client.ts.range('rg:1', '-', '+', {
+  FILTER_BY_TS: [0, 2, 4]
+});
+console.log(res17);
+// >>> [{ timestamp: 0, value: 18 }, { timestamp: 2, value: 22 }, { timestamp: 4, value: 24 }]
+
+const res18 = await client.ts.revRange('rg:1', '-', '+', {
+  FILTER_BY_TS: [0, 2, 4],
+  FILTER_BY_VALUE: { min: 20, max: 25 }
+});
+console.log(res18);
+// >>> [{ timestamp: 4, value: 24 }, { timestamp: 2, value: 22 }]
+
+const res19 = await client.ts.revRange('rg:1', '-', '+', {
+  FILTER_BY_TS: [0, 2, 4],
+  FILTER_BY_VALUE: { min: 22, max: 22 },
+  COUNT: 1
+});
+console.log(res19);
+// >>> [{ timestamp: 2, value: 22 }]
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res17, [
+  { timestamp: 0, value: 18 },
+  { timestamp: 2, value: 22 },
+  { timestamp: 4, value: 24 }
+]);
+assert.deepEqual(res18, [
+  { timestamp: 4, value: 24 },
+  { timestamp: 2, value: 22 }
+]);
+assert.deepEqual(res19, [
+  { timestamp: 2, value: 22 }
+]);
+// REMOVE_END
+
+// STEP_START query_multi
+// Create three new "rg:" time series (two in the US
+// and one in the UK, with different units) and add some
+// data points.
+const res20 = await client.ts.create('rg:2', {
+  LABELS: { location: 'us', unit: 'cm' }
+});
+console.log(res20); // >>> OK
+
+const res21 = await client.ts.create('rg:3', {
+  LABELS: { location: 'us', unit: 'in' }
+});
+console.log(res21); // >>> OK
+
+const res22 = await client.ts.create('rg:4', {
+  LABELS: { location: 'uk', unit: 'mm' }
+});
+console.log(res22); // >>> OK
+
+const res23 = await client.ts.mAdd([
+  { key: 'rg:2', timestamp: 0, value: 1.8 },
+  { key: 'rg:3', timestamp: 0, value: 0.9 },
+  { key: 'rg:4', timestamp: 0, value: 25 }
+]);
+console.log(res23); // >>> [0, 0, 0]
+
+const res24 = await client.ts.mAdd([
+  { key: 'rg:2', timestamp: 1, value: 2.1 },
+  { key: 'rg:3', timestamp: 1, value: 0.77 },
+  { key: 'rg:4', timestamp: 1, value: 18 }
+]);
+console.log(res24); // >>> [1, 1, 1]
+
+const res25 = await client.ts.mAdd([
+  { key: 'rg:2', timestamp: 2, value: 2.3 },
+  { key: 'rg:3', timestamp: 2, value: 1.1 },
+  { key: 'rg:4', timestamp: 2, value: 21 }
+]);
+console.log(res25); // >>> [2, 2, 2]
+
+const res26 = await client.ts.mAdd([
+  { key: 'rg:2', timestamp: 3, value: 1.9 },
+  { key: 'rg:3', timestamp: 3, value: 0.81 },
+  { key: 'rg:4', timestamp: 3, value: 19 }
+]);
+console.log(res26); // >>> [3, 3, 3]
+
+const res27 = await client.ts.mAdd([
+  { key: 'rg:2', timestamp: 4, value: 1.78 },
+  { key: 'rg:3', timestamp: 4, value: 0.74 },
+  { key: 'rg:4', timestamp: 4, value: 23 }
+]);
+console.log(res27); // >>> [4, 4, 4]
+
+// Retrieve the last data point from each US time series.
+const res28 = await client.ts.mGet(['location=us']);
+console.log(res28);
+// >>> { "rg:2": { sample: { timestamp: 4, value: 1.78 } }, "rg:3": { sample: { timestamp: 4, value: 0.74 } } }
+
+// Retrieve the same data points, but include the `unit`
+// label in the results.
+const res29 = await client.ts.mGetSelectedLabels(['location=us'], ['unit']);
+console.log(res29);
+// >>> { "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } }
+
+// Retrieve data points up to time 2 (inclusive) from all
+// time series that use millimeters as the unit. Include all
+// labels in the results.
+const res30 = await client.ts.mRangeWithLabels('-', 2, 'unit=mm');
+console.log(res30);
+// >>> { "rg:4": { labels: { location: 'uk', unit: 'mm' }, samples: [
+//   { timestamp: 0, value: 25 },
+//   { timestamp: 1, value: 18 },
+//   { timestamp: 2, value: 21 }
+// ] } }
+
+// Retrieve data points from time 1 to time 3 (inclusive) from
+// all time series that use centimeters or millimeters as the unit,
+// but only return the `location` label. Return the results
+// in descending order of timestamp.
+const res31 = await client.ts.mRevRangeSelectedLabels(
+  1, 3,
+  ['location'],
+  ['unit=(cm,mm)']
+);
+console.log(res31);
+// >>> { "rg:2": { labels: { location: 'us' }, samples: [
+//   { timestamp: 3, value: 1.9 },
+//   { timestamp: 2, value: 2.3 },
+//   { timestamp: 1, value: 2.1 }
+// ] }, "rg:4": { labels: { location: 'uk' }, samples: [
+//   { timestamp: 3, value: 19 },
+//   { timestamp: 2, value: 21 },
+//   { timestamp: 1, value: 18 }
+// ] } }
+// STEP_END
+// REMOVE_START
+assert.equal(res20, 'OK');
+assert.equal(res21, 'OK');
+assert.equal(res22, 'OK');
+assert.deepEqual(res23, [0, 0, 0]);
+assert.deepEqual(res24, [1, 1, 1]);
+assert.deepEqual(res25, [2, 2, 2]);
+assert.deepEqual(res26, [3, 3, 3]);
+assert.deepEqual(res27, [4, 4, 4]);
+
+assert.deepEqual(res28, {
+  "rg:2": { sample: { timestamp: 4, value: 1.78 } },
+  "rg:3": { sample: { timestamp: 4, value: 0.74 } }
+});
+assert.deepEqual(res29, {
+  "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } },
+  "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } }
+});
+
+assert.deepEqual(res30, {
+  "rg:4": {
+    labels: { location: 'uk', unit: 'mm' },
+    samples: [
+      { timestamp: 0, value: 25 },
+      { timestamp: 1, value: 18 },
+      { timestamp: 2, value: 21 }
+    ]
+  }
+});
+assert.deepEqual(res31, {
+  "rg:2": {
+    labels: { location: 'us' },
+    samples: [
+      { timestamp: 3, value: 1.9 },
+      { timestamp: 2, value: 2.3 },
+      { timestamp: 1, value: 2.1 }
+    ]
+  },
+  "rg:4": {
+    labels: { location: 'uk' },
+    samples: [
+      { timestamp: 3, value: 19 },
+      { timestamp: 2, value: 21 },
+      { timestamp: 1, value: 18 }
+    ]
+  }
+});
+// REMOVE_END
+
+// STEP_START agg
+const res32 = await client.ts.range('rg:2', '-', '+', {
+  AGGREGATION: {
+    type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+    timeBucket: 2
+  }
+});
+console.log(res32);
+// >>> [{ timestamp: 0, value: 1.9500000000000002 },{ timestamp: 2, value: 2.0999999999999996 }, { timestamp: 4, value: 1.78 }]
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res32, [
+  { timestamp: 0, value: 1.9500000000000002 },
+  { timestamp: 2, value: 2.0999999999999996 },
+  { timestamp: 4, value: 1.78 }
+]);
+// REMOVE_END
+
+// STEP_START agg_bucket
+const res33 = await client.ts.create('sensor3');
+console.log(res33); // >>> OK
+
+const res34 = await client.ts.mAdd([
+  { key: 'sensor3', timestamp: 10, value: 1000 },
+  { key: 'sensor3', timestamp: 20, value: 2000 },
+  { key: 'sensor3', timestamp: 30, value: 3000 },
+  { key: 'sensor3', timestamp: 40, value: 4000 },
+  { key: 'sensor3', timestamp: 50, value: 5000 },
+  { key: 'sensor3', timestamp: 60, value: 6000 },
+  { key: 'sensor3', timestamp: 70, value: 7000 }
+]);
+console.log(res34); // >>> [10, 20, 30, 40, 50, 60, 70]
+
+const res35 = await client.ts.range('sensor3', 10, 70, {
+  AGGREGATION: {
+    type: TIME_SERIES_AGGREGATION_TYPE.MIN,
+    timeBucket: 25
+  }
+});
+console.log(res35);
+// >>> [{ timestamp: 0, value: 1000 }, { timestamp: 25, value: 3000 }, { timestamp: 50, value: 5000 }]
+// STEP_END
+// REMOVE_START
+assert.equal(res33, 'OK');
+assert.deepEqual(res34, [10, 20, 30, 40, 50, 60, 70]);
+assert.deepEqual(res35, [
+  { timestamp: 0, value: 1000 },
+  { timestamp: 25, value: 3000 },
+  { timestamp: 50, value: 5000 }
+]);
+// REMOVE_END
+
+// STEP_START agg_align
+const res36 = await client.ts.range('sensor3', 10, 70, {
+  AGGREGATION: {
+    type: TIME_SERIES_AGGREGATION_TYPE.MIN,
+    timeBucket: 25
+  },
+  ALIGN: 'START'
+});
+console.log(res36);
+// >>> [{ timestamp: 10, value: 1000 }, { timestamp: 35, value: 4000 }, { timestamp: 60, value: 6000 }]
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res36, [
+  { timestamp: 10, value: 1000 },
+  { timestamp: 35, value: 4000 },
+  { timestamp: 60, value: 6000 }
+]);
+// REMOVE_END
+
+// STEP_START agg_multi
+const res37 = await client.ts.create('wind:1', {
+  LABELS: { country: 'uk' }
+});
+console.log(res37); // >>> OK
+
+const res38 = await client.ts.create('wind:2', {
+  LABELS: { country: 'uk' }
+});
+console.log(res38); // >>> OK
+
+const res39 = await client.ts.create('wind:3', {
+  LABELS: { country: 'us' }
+});
+console.log(res39); // >>> OK
+
+const res40 = await client.ts.create('wind:4', {
+  LABELS: { country: 'us' }
+});
+console.log(res40); // >>> OK
+
+const res41 = await client.ts.mAdd([
+  { key: 'wind:1', timestamp: 1, value: 12 },
+  { key: 'wind:2', timestamp: 1, value: 18 },
+  { key: 'wind:3', timestamp: 1, value: 5 },
+  { key: 'wind:4', timestamp: 1, value: 20 }
+]);
+console.log(res41); // >>> [1, 1, 1, 1]
+
+const res42 = await client.ts.mAdd([
+  { key: 'wind:1', timestamp: 2, value: 14 },
+  { key: 'wind:2', timestamp: 2, value: 21 },
+  { key: 'wind:3', timestamp: 2, value: 4 },
+  { key: 'wind:4', timestamp: 2, value: 25 }
+]);
+console.log(res42); // >>> [2, 2, 2, 2]
+
+const res43 = await client.ts.mAdd([
+  { key: 'wind:1', timestamp: 3, value: 10 },
+  { key: 'wind:2', timestamp: 3, value: 24 },
+  { key: 'wind:3', timestamp: 3, value: 8 },
+  { key: 'wind:4', timestamp: 3, value: 18 }
+]);
+console.log(res43); // >>> [3, 3, 3, 3]
+
+// The result pairs contain the timestamp and the maximum sample value
+// for the country at that timestamp.
+const res44 = await client.ts.mRangeGroupBy(
+  '-', '+', ['country=(us,uk)'],
+  {label: 'country', REDUCE: TIME_SERIES_REDUCERS.MAX}
+);
+console.log(res44);
+// >>> { "country=uk": { samples: [
+//   { timestamp: 1, value: 18 },
+//   { timestamp: 2, value: 21 },
+//   { timestamp: 3, value: 24 }
+// ] }, "country=us": { samples: [
+//   { timestamp: 1, value: 20 },
+//   { timestamp: 2, value: 25 },
+//   { timestamp: 3, value: 18 }
+// ] } }
+
+// The result pairs contain the timestamp and the average sample value
+// for the country at that timestamp.
+const res45 = await client.ts.mRangeGroupBy(
+  '-', '+', ['country=(us,uk)'],
+  { label: 'country', REDUCE: TIME_SERIES_REDUCERS.AVG}
+);
+console.log(res45);
+// >>> {
+// "country=uk": {
+//   samples: [{ timestamp: 1, value: 15 }, { timestamp: 2, value: 17.5 }, { timestamp: 3, value: 17 }]
+// },
+// "country=us": {
+//   samples: [{ timestamp: 1, value: 12.5 }, { timestamp: 2, value: 14.5 }, { timestamp: 3, value: 13 }]
+// }
+// }
+// STEP_END
+// REMOVE_START
+assert.equal(res37, 'OK');
+assert.equal(res38, 'OK');
+assert.equal(res39, 'OK');
+assert.equal(res40, 'OK');
+assert.deepEqual(res41, [1, 1, 1, 1]);
+assert.deepEqual(res42, [2, 2, 2, 2]);
+assert.deepEqual(res43, [3, 3, 3, 3]);
+
+assert.deepEqual(res44, {
+  "country=uk": {
+    samples: [
+      { timestamp: 1, value: 18 },
+      { timestamp: 2, value: 21 },
+      { timestamp: 3, value: 24 }
+    ]
+  },
+  "country=us": {
+    samples: [
+      { timestamp: 1, value: 20 },
+      { timestamp: 2, value: 25 },
+      { timestamp: 3, value: 18 }
+    ]
+  }
+});
+assert.deepEqual(res45, {
+  "country=uk": {
+    samples: [
+      { timestamp: 1, value: 15 },
+      { timestamp: 2, value: 17.5 },
+      { timestamp: 3, value: 17 }
+    ]
+  },
+  "country=us": {
+    samples: [
+      { timestamp: 1, value: 12.5 },
+      { timestamp: 2, value: 14.5 },
+      { timestamp: 3, value: 13 }
+    ]
+  }
+});
+// REMOVE_END
+
+// STEP_START create_compaction
+const res46 = await client.ts.create('hyg:1');
+console.log(res46); // >>> OK
+
+const res47 = await client.ts.create('hyg:compacted');
+console.log(res47); // >>> OK
+
+const res48 = await client.ts.createRule('hyg:1', 'hyg:compacted', TIME_SERIES_AGGREGATION_TYPE.MIN, 3);
+console.log(res48); // >>> OK
+
+const res49 = await client.ts.info('hyg:1');
+console.log(res49.rules);
+// >>> [{ aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3}]
+
+const res50 = await client.ts.info('hyg:compacted');
+console.log(res50.sourceKey); // >>> 'hyg:1'
+// STEP_END
+// REMOVE_START
+assert.equal(res46, 'OK');
+assert.equal(res47, 'OK');
+assert.equal(res48, 'OK');
+assert.deepEqual(res49.rules, [
+  { aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3}
+]);
+assert.equal(res50.sourceKey, 'hyg:1');
+// REMOVE_END
+
+// STEP_START comp_add
+const res51 = await client.ts.mAdd([
+  { key: 'hyg:1', timestamp: 0, value: 75 },
+  { key: 'hyg:1', timestamp: 1, value: 77 },
+  { key: 'hyg:1', timestamp: 2, value: 78 }
+]);
+console.log(res51); // >>> [0, 1, 2]
+
+const res52 = await client.ts.range('hyg:compacted', '-', '+');
+console.log(res52); // >>> []
+
+const res53 = await client.ts.add('hyg:1', 3, 79);
+console.log(res53); // >>> 3
+
+const res54 = await client.ts.range('hyg:compacted', '-', '+');
+console.log(res54); // >>> [{ timestamp: 0, value: 75 }]
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res51, [0, 1, 2]);
+assert.deepEqual(res52, []);
+assert.equal(res53, 3);
+assert.deepEqual(res54, [{ timestamp: 0, value: 75 }]);
+// REMOVE_END
+
+// STEP_START del
+const res55 = await client.ts.info('thermometer:1');
+console.log(res55.totalSamples); // >>> 2
+console.log(res55.firstTimestamp); // >>> 1
+console.log(res55.lastTimestamp); // >>> 2
+
+const res56 = await client.ts.add('thermometer:1', 3, 9.7);
+console.log(res56); // >>> 3
+
+const res57 = await client.ts.info('thermometer:1');
+console.log(res57.totalSamples); // >>> 3
+console.log(res57.firstTimestamp); // >>> 1
+console.log(res57.lastTimestamp); // >>> 3
+
+const res58 = await client.ts.del('thermometer:1', 1, 2);
+console.log(res58); // >>> 2
+
+const res59 = await client.ts.info('thermometer:1');
+console.log(res59.totalSamples); // >>> 1
+console.log(res59.firstTimestamp); // >>> 3
+console.log(res59.lastTimestamp); // >>> 3
+
+const res60 = await client.ts.del('thermometer:1', 3, 3);
+console.log(res60); // >>> 1
+
+const res61 = await client.ts.info('thermometer:1');
+console.log(res61.totalSamples); // >>> 0
+// STEP_END
+// REMOVE_START
+assert.equal(res55.totalSamples, 2);
+assert.equal(res55.firstTimestamp, 1);
+assert.equal(res55.lastTimestamp, 2);
+assert.equal(res56, 3);
+assert.equal(res57.totalSamples, 3);
+assert.equal(res57.firstTimestamp, 1);
+assert.equal(res57.lastTimestamp, 3);
+assert.equal(res58, 2);
+assert.equal(res59.totalSamples, 1);
+assert.equal(res59.firstTimestamp, 3);
+assert.equal(res59.lastTimestamp, 3);
+assert.equal(res60, 1);
+assert.equal(res61.totalSamples, 0);
+// REMOVE_END
+
+// HIDE_START
+await client.quit();
+// HIDE_END
\ No newline at end of file
diff --git a/doctests/dt-topk.js b/doctests/dt-topk.js
new file mode 100644
index 00000000000..49b929aed8a
--- /dev/null
+++ b/doctests/dt-topk.js
@@ -0,0 +1,48 @@
+// EXAMPLE: topk_tutorial
+// HIDE_START
+import assert from 'assert';
+import { createClient } from 'redis';
+
+const client = createClient();
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.flushDb();
+// REMOVE_END
+
+// STEP_START topk
+const res1 = await client.topK.reserve('bikes:keywords', 5, {
+    width: 2000,
+    depth: 7,
+    decay: 0.925
+});
+console.log(res1);  // >>> OK
+
+const res2 = await client.topK.add('bikes:keywords', [
+  'store',
+  'seat',
+  'handlebars',
+  'handles',
+  'pedals',
+  'tires',
+  'store',
+  'seat'
+]);
+console.log(res2);  // >>> [null, null, null, null, null, 'handlebars', null, null]
+
+const res3 = await client.topK.list('bikes:keywords');
+console.log(res3);  // >>> ['store', 'seat', 'pedals', 'tires', 'handles']
+
+const res4 = await client.topK.query('bikes:keywords', ['store', 'handlebars']);
+console.log(res4);  // >>> [true, false]
+// STEP_END
+
+// REMOVE_START
+assert.equal(res1, 'OK')
+assert.deepEqual(res2, [null, null, null, null, null, 'handlebars', null, null])
+assert.deepEqual(res3, ['store', 'seat', 'pedals', 'tires', 'handles'])
+assert.deepEqual(res4, [1, 0])
+await client.close();
+// REMOVE_END
+
diff --git a/doctests/dt-vec-set.js b/doctests/dt-vec-set.js
new file mode 100644
index 00000000000..0e8cb918d7e
--- /dev/null
+++ b/doctests/dt-vec-set.js
@@ -0,0 +1,281 @@
+// EXAMPLE: vecset_tutorial
+// REMOVE_START
+/**
+ * Code samples for Vector set doc pages:
+ *     https://redis.io/docs/latest/develop/data-types/vector-sets/
+ */
+
+import assert from 'assert';
+// REMOVE_END
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient({
+  RESP: 3  // Required for vector set commands
+});
+
+await client.connect();
+// HIDE_END
+
+// REMOVE_START
+await client.del([
+  "points", "quantSetQ8", "quantSetNoQ",
+  "quantSetBin", "setNotReduced", "setReduced"
+]);
+// REMOVE_END
+
+// STEP_START vadd
+const res1 = await client.vAdd("points", [1.0, 1.0], "pt:A");
+console.log(res1);  // >>> true
+
+const res2 = await client.vAdd("points", [-1.0, -1.0], "pt:B");
+console.log(res2);  // >>> true
+
+const res3 = await client.vAdd("points", [-1.0, 1.0], "pt:C");
+console.log(res3);  // >>> true
+
+const res4 = await client.vAdd("points", [1.0, -1.0], "pt:D");
+console.log(res4);  // >>> true
+
+const res5 = await client.vAdd("points", [1.0, 0], "pt:E");
+console.log(res5);  // >>> true
+
+const res6 = await client.type("points");
+console.log(res6);  // >>> vectorset
+// STEP_END
+// REMOVE_START
+assert.equal(res1, true);
+assert.equal(res2, true);
+assert.equal(res3, true);
+assert.equal(res4, true);
+assert.equal(res5, true);
+assert.equal(res6, "vectorset");
+// REMOVE_END
+
+// STEP_START vcardvdim
+const res7 = await client.vCard("points");
+console.log(res7);  // >>> 5
+
+const res8 = await client.vDim("points");
+console.log(res8);  // >>> 2
+// STEP_END
+// REMOVE_START
+assert.equal(res7, 5);
+assert.equal(res8, 2);
+// REMOVE_END
+
+// STEP_START vemb
+const res9 = await client.vEmb("points", "pt:A");
+console.log(res9);  // >>> [0.9999999403953552, 0.9999999403953552]
+
+const res10 = await client.vEmb("points", "pt:B");
+console.log(res10);  // >>> [-0.9999999403953552, -0.9999999403953552]
+
+const res11 = await client.vEmb("points", "pt:C");
+console.log(res11);  // >>> [-0.9999999403953552, 0.9999999403953552]
+
+const res12 = await client.vEmb("points", "pt:D");
+console.log(res12);  // >>> [0.9999999403953552, -0.9999999403953552]
+
+const res13 = await client.vEmb("points", "pt:E");
+console.log(res13);  // >>> [1, 0]
+// STEP_END
+// REMOVE_START
+assert(Math.abs(1 - res9[0]) < 0.001);
+assert(Math.abs(1 - res9[1]) < 0.001);
+assert(Math.abs(1 + res10[0]) < 0.001);
+assert(Math.abs(1 + res10[1]) < 0.001);
+assert(Math.abs(1 + res11[0]) < 0.001);
+assert(Math.abs(1 - res11[1]) < 0.001);
+assert(Math.abs(1 - res12[0]) < 0.001);
+assert(Math.abs(1 + res12[1]) < 0.001);
+assert.deepEqual(res13, [1, 0]);
+// REMOVE_END
+
+// STEP_START attr
+const res14 = await client.vSetAttr("points", "pt:A", {
+  name: "Point A",
+  description: "First point added"
+});
+console.log(res14);  // >>> true
+
+const res15 = await client.vGetAttr("points", "pt:A");
+console.log(res15);
+// >>> {name: 'Point A', description: 'First point added'}
+
+const res16 = await client.vSetAttr("points", "pt:A", "");
+console.log(res16);  // >>> true
+
+const res17 = await client.vGetAttr("points", "pt:A");
+console.log(res17);  // >>> null
+// STEP_END
+// REMOVE_START
+assert.equal(res14, true);
+assert.deepEqual(res15, {name: "Point A", description: "First point added"});
+assert.equal(res16, true);
+assert.equal(res17, null);
+// REMOVE_END
+
+// STEP_START vrem
+const res18 = await client.vAdd("points", [0, 0], "pt:F");
+console.log(res18);  // >>> true
+
+const res19 = await client.vCard("points");
+console.log(res19);  // >>> 6
+
+const res20 = await client.vRem("points", "pt:F");
+console.log(res20);  // >>> true
+
+const res21 = await client.vCard("points");
+console.log(res21);  // >>> 5
+// STEP_END
+// REMOVE_START
+assert.equal(res18, true);
+assert.equal(res19, 6);
+assert.equal(res20, true);
+assert.equal(res21, 5);
+// REMOVE_END
+
+// STEP_START vsim_basic
+const res22 = await client.vSim("points", [0.9, 0.1]);
+console.log(res22);
+// >>> ['pt:E', 'pt:A', 'pt:D', 'pt:C', 'pt:B']
+// STEP_END
+// REMOVE_START
+assert.deepEqual(res22, ["pt:E", "pt:A", "pt:D", "pt:C", "pt:B"]);
+// REMOVE_END
+
+// STEP_START vsim_options
+const res23 = await client.vSimWithScores("points", "pt:A", { COUNT: 4 });
+console.log(res23);
+// >>> {pt:A: 1.0, pt:E: 0.8535534143447876, pt:D: 0.5, pt:C: 0.5}
+// STEP_END
+// REMOVE_START
+assert.equal(res23["pt:A"], 1.0);
+assert.equal(res23["pt:C"], 0.5);
+assert.equal(res23["pt:D"], 0.5);
+assert(Math.abs(res23["pt:E"] - 0.85) < 0.005);
+// REMOVE_END
+
+// STEP_START vsim_filter
+const res24 = await client.vSetAttr("points", "pt:A", {
+  size: "large",
+  price: 18.99
+});
+console.log(res24);  // >>> true
+
+const res25 = await client.vSetAttr("points", "pt:B", {
+  size: "large",
+  price: 35.99
+});
+console.log(res25);  // >>> true
+
+const res26 = await client.vSetAttr("points", "pt:C", {
+  size: "large",
+  price: 25.99
+});
+console.log(res26);  // >>> true
+
+const res27 = await client.vSetAttr("points", "pt:D", {
+  size: "small",
+  price: 21.00
+});
+console.log(res27);  // >>> true
+
+const res28 = await client.vSetAttr("points", "pt:E", {
+  size: "small",
+  price: 17.75
+});
+console.log(res28);  // >>> true
+
+// Return elements in order of distance from point A whose
+// `size` attribute is `large`.
+const res29 = await client.vSim("points", "pt:A", {
+  FILTER: '.size == "large"'
+});
+console.log(res29);  // >>> ['pt:A', 'pt:C', 'pt:B']
+
+// Return elements in order of distance from point A whose size is
+// `large` and whose price is greater than 20.00.
+const res30 = await client.vSim("points", "pt:A", {
+  FILTER: '.size == "large" && .price > 20.00'
+});
+console.log(res30);  // >>> ['pt:C', 'pt:B']
+// STEP_END
+// REMOVE_START
+assert.equal(res24, true);
+assert.equal(res25, true);
+assert.equal(res26, true);
+assert.equal(res27, true);
+assert.equal(res28, true);
+assert.deepEqual(res29, ['pt:A', 'pt:C', 'pt:B']);
+assert.deepEqual(res30, ['pt:C', 'pt:B']);
+// REMOVE_END
+
+// STEP_START add_quant
+const res31 = await client.vAdd("quantSetQ8", [1.262185, 1.958231], "quantElement", {
+  QUANT: 'Q8'
+});
+console.log(res31);  // >>> true
+
+const res32 = await client.vEmb("quantSetQ8", "quantElement");
+console.log(`Q8: ${res32}`);
+// >>> Q8: [1.2643694877624512, 1.958230972290039]
+
+const res33 = await client.vAdd("quantSetNoQ", [1.262185, 1.958231], "quantElement", {
+  QUANT: 'NOQUANT'
+});
+console.log(res33);  // >>> true
+
+const res34 = await client.vEmb("quantSetNoQ", "quantElement");
+console.log(`NOQUANT: ${res34}`);
+// >>> NOQUANT: [1.262184977531433, 1.958230972290039]
+
+const res35 = await client.vAdd("quantSetBin", [1.262185, 1.958231], "quantElement", {
+  QUANT: 'BIN'
+});
+console.log(res35);  // >>> true
+
+const res36 = await client.vEmb("quantSetBin", "quantElement");
+console.log(`BIN: ${res36}`);
+// >>> BIN: [1, 1]
+// STEP_END
+// REMOVE_START
+assert.equal(res31, true);
+assert(Math.abs(res32[0] - 1.2643694877624512) < 0.001);
+assert(Math.abs(res32[1] - 1.958230972290039) < 0.001);
+assert.equal(res33, true);
+assert(Math.abs(res34[0] - 1.262184977531433) < 0.001);
+assert(Math.abs(res34[1] - 1.958230972290039) < 0.001);
+assert.equal(res35, true);
+assert.deepEqual(res36, [1, 1]);
+// REMOVE_END
+
+// STEP_START add_reduce
+// Create a list of 300 arbitrary values.
+const values = Array.from({length: 300}, (_, x) => x / 299);
+
+const res37 = await client.vAdd("setNotReduced", values, "element");
+console.log(res37);  // >>> true
+
+const res38 = await client.vDim("setNotReduced");
+console.log(res38);  // >>> 300
+
+const res39 = await client.vAdd("setReduced", values, "element", {
+  REDUCE: 100
+});
+console.log(res39);  // >>> true
+
+const res40 = await client.vDim("setReduced");
+console.log(res40);  // >>> 100
+// STEP_END
+// REMOVE_START
+assert.equal(res37, true);
+assert.equal(res38, 300);
+assert.equal(res39, true);
+assert.equal(res40, 100);
+// REMOVE_END
+
+// HIDE_START
+await client.quit();
+// HIDE_END
diff --git a/doctests/package-lock.json b/doctests/package-lock.json
new file mode 100644
index 00000000000..2f30678e529
--- /dev/null
+++ b/doctests/package-lock.json
@@ -0,0 +1,889 @@
+{
+  "name": "node-redis-doctests",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "node-redis-doctests",
+      "version": "1.0.0",
+      "dependencies": {
+        "@xenova/transformers": "^2.17.2",
+        "redis": "file:/packages/redis"
+      }
+    },
+    "..": {
+      "name": "redis-monorepo",
+      "extraneous": true,
+      "workspaces": [
+        "./packages/client",
+        "./packages/test-utils",
+        "./packages/bloom",
+        "./packages/json",
+        "./packages/search",
+        "./packages/time-series",
+        "./packages/entraid",
+        "./packages/redis"
+      ],
+      "devDependencies": {
+        "@istanbuljs/nyc-config-typescript": "^1.0.2",
+        "@release-it/bumper": "^7.0.5",
+        "@types/mocha": "^10.0.6",
+        "@types/node": "^20.11.16",
+        "gh-pages": "^6.1.1",
+        "mocha": "^10.2.0",
+        "nyc": "^15.1.0",
+        "release-it": "^19.0.2",
+        "ts-node": "^10.9.2",
+        "tsx": "^4.7.0",
+        "typedoc": "^0.25.7",
+        "typescript": "^5.3.3"
+      }
+    },
+    "../../../../../packages/redis": {},
+    "node_modules/@huggingface/jinja": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
+      "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@types/long": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+      "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.0.10",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
+      "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.8.0"
+      }
+    },
+    "node_modules/@xenova/transformers": {
+      "version": "2.17.2",
+      "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
+      "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@huggingface/jinja": "^0.2.2",
+        "onnxruntime-web": "1.14.0",
+        "sharp": "^0.32.0"
+      },
+      "optionalDependencies": {
+        "onnxruntime-node": "1.14.0"
+      }
+    },
+    "node_modules/b4a": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+      "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/bare-events": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
+      "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
+      "license": "Apache-2.0",
+      "optional": true
+    },
+    "node_modules/bare-fs": {
+      "version": "4.1.6",
+      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz",
+      "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "bare-events": "^2.5.4",
+        "bare-path": "^3.0.0",
+        "bare-stream": "^2.6.4"
+      },
+      "engines": {
+        "bare": ">=1.16.0"
+      },
+      "peerDependencies": {
+        "bare-buffer": "*"
+      },
+      "peerDependenciesMeta": {
+        "bare-buffer": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/bare-os": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
+      "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "bare": ">=1.14.0"
+      }
+    },
+    "node_modules/bare-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+      "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "bare-os": "^3.0.1"
+      }
+    },
+    "node_modules/bare-stream": {
+      "version": "2.6.5",
+      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
+      "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "streamx": "^2.21.0"
+      },
+      "peerDependencies": {
+        "bare-buffer": "*",
+        "bare-events": "*"
+      },
+      "peerDependenciesMeta": {
+        "bare-buffer": {
+          "optional": true
+        },
+        "bare-events": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "license": "ISC"
+    },
+    "node_modules/color": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/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==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "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==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+      "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/expand-template": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+      "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+      "license": "(MIT OR WTFPL)",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/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==",
+      "license": "MIT"
+    },
+    "node_modules/flatbuffers": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
+      "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==",
+      "license": "SEE LICENSE IN LICENSE.txt"
+    },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "license": "MIT"
+    },
+    "node_modules/github-from-package": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+      "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+      "license": "MIT"
+    },
+    "node_modules/guid-typescript": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
+      "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
+      "license": "ISC"
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "license": "ISC"
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+      "license": "Apache-2.0"
+    },
+    "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==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+      "license": "MIT"
+    },
+    "node_modules/napi-build-utils": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+      "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+      "license": "MIT"
+    },
+    "node_modules/node-abi": {
+      "version": "3.75.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
+      "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT"
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onnx-proto": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
+      "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
+      "license": "MIT",
+      "dependencies": {
+        "protobufjs": "^6.8.8"
+      }
+    },
+    "node_modules/onnxruntime-common": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz",
+      "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==",
+      "license": "MIT"
+    },
+    "node_modules/onnxruntime-node": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz",
+      "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==",
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32",
+        "darwin",
+        "linux"
+      ],
+      "dependencies": {
+        "onnxruntime-common": "~1.14.0"
+      }
+    },
+    "node_modules/onnxruntime-web": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz",
+      "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==",
+      "license": "MIT",
+      "dependencies": {
+        "flatbuffers": "^1.12.0",
+        "guid-typescript": "^1.0.9",
+        "long": "^4.0.0",
+        "onnx-proto": "^4.0.4",
+        "onnxruntime-common": "~1.14.0",
+        "platform": "^1.3.6"
+      }
+    },
+    "node_modules/platform": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+      "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+      "license": "MIT"
+    },
+    "node_modules/prebuild-install": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+      "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+      "license": "MIT",
+      "dependencies": {
+        "detect-libc": "^2.0.0",
+        "expand-template": "^2.0.3",
+        "github-from-package": "0.0.0",
+        "minimist": "^1.2.3",
+        "mkdirp-classic": "^0.5.3",
+        "napi-build-utils": "^2.0.0",
+        "node-abi": "^3.3.0",
+        "pump": "^3.0.0",
+        "rc": "^1.2.7",
+        "simple-get": "^4.0.0",
+        "tar-fs": "^2.0.0",
+        "tunnel-agent": "^0.6.0"
+      },
+      "bin": {
+        "prebuild-install": "bin.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/prebuild-install/node_modules/tar-fs": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
+      "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/protobufjs": {
+      "version": "6.11.4",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
+      "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/long": "^4.0.1",
+        "@types/node": ">=13.7.0",
+        "long": "^4.0.0"
+      },
+      "bin": {
+        "pbjs": "bin/pbjs",
+        "pbts": "bin/pbts"
+      }
+    },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+      "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+      "dependencies": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      },
+      "bin": {
+        "rc": "cli.js"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/redis": {
+      "resolved": "../../../../../packages/redis",
+      "link": true
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.32.6",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+      "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "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/simple-concat": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/simple-get": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+      "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "decompress-response": "^6.0.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
+    "node_modules/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==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/streamx": {
+      "version": "2.22.1",
+      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",
+      "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-fifo": "^1.3.2",
+        "text-decoder": "^1.1.0"
+      },
+      "optionalDependencies": {
+        "bare-events": "^2.2.0"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/tar-fs": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
+      "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
+      "license": "MIT",
+      "dependencies": {
+        "pump": "^3.0.0",
+        "tar-stream": "^3.1.5"
+      },
+      "optionalDependencies": {
+        "bare-fs": "^4.0.1",
+        "bare-path": "^3.0.0"
+      }
+    },
+    "node_modules/tar-stream": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+      "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+      "license": "MIT",
+      "dependencies": {
+        "b4a": "^1.6.4",
+        "fast-fifo": "^1.2.0",
+        "streamx": "^2.15.0"
+      }
+    },
+    "node_modules/text-decoder": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
+      "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "b4a": "^1.6.4"
+      }
+    },
+    "node_modules/tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+      "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+      "license": "MIT"
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    }
+  }
+}
diff --git a/doctests/package.json b/doctests/package.json
new file mode 100644
index 00000000000..ee07eb44fb8
--- /dev/null
+++ b/doctests/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "node-redis-doctests",
+  "version": "1.0.0",
+  "description": "Code examples for redis.io",
+  "main": "index.js",
+  "private": true,
+  "type": "module",
+  "dependencies": {
+    "redis": "file:/packages/redis",
+    "@xenova/transformers": "^2.17.2"
+  }
+}
diff --git a/doctests/query-agg.js b/doctests/query-agg.js
new file mode 100644
index 00000000000..82d378b86b2
--- /dev/null
+++ b/doctests/query-agg.js
@@ -0,0 +1,139 @@
+// EXAMPLE: query_agg
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient } from 'redis';
+import { SCHEMA_FIELD_TYPE, FT_AGGREGATE_STEPS, FT_AGGREGATE_GROUP_BY_REDUCERS } from '@redis/search';
+
+const client = createClient();
+
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+  '$.condition': {
+    type: SCHEMA_FIELD_TYPE.TAG,
+    AS: 'condition'
+  },
+  '$.price': {
+    type: SCHEMA_FIELD_TYPE.NUMERIC,
+    AS: 'price'
+  }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+})
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START agg1
+const res1 = await client.ft.aggregate('idx:bicycle', '@condition:{new}', {
+  LOAD: ['__key', 'price'],
+  APPLY: {
+    expression: '@price - (@price * 0.1)',
+    AS: 'discounted'
+  }
+});
+
+console.log(res1.results.length); // >>> 5
+console.log(res1.results); // >>>
+//[
+//  [Object: null prototype] { __key: 'bicycle:0', price: '270' },
+//  [Object: null prototype] { __key: 'bicycle:5', price: '810' },
+//  [Object: null prototype] { __key: 'bicycle:6', price: '2300' },
+//  [Object: null prototype] { __key: 'bicycle:7', price: '430' },
+//  [Object: null prototype] { __key: 'bicycle:8', price: '1200' }
+//]
+// REMOVE_START
+assert.strictEqual(res1.results.length, 5);
+// REMOVE_END
+// STEP_END
+
+// STEP_START agg2
+const res2 = await client.ft.aggregate('idx:bicycle', '*', {
+  LOAD: ['@price'],
+  STEPS: [{
+      type: FT_AGGREGATE_STEPS.APPLY,
+      expression: '@price<1000',
+      AS: 'price_category'
+    },{
+      type: FT_AGGREGATE_STEPS.GROUPBY,
+      properties: '@condition',
+      REDUCE:[{
+        type: FT_AGGREGATE_GROUP_BY_REDUCERS.SUM,
+        property: '@price_category',
+        AS: 'num_affordable'
+      }]
+    }]
+});
+console.log(res2.results.length); // >>> 3
+console.log(res2.results); // >>>
+//[[Object: null prototype] { condition: 'refurbished', num_affordable: '1' },
+//  [Object: null prototype] { condition: 'used', num_affordable: '1' },
+//  [Object: null prototype] { condition: 'new', num_affordable: '3' }
+//]
+// REMOVE_START
+assert.strictEqual(res2.results.length, 3);
+// REMOVE_END
+// STEP_END
+
+// STEP_START agg3
+const res3 = await client.ft.aggregate('idx:bicycle', '*', {
+  STEPS: [{
+      type: FT_AGGREGATE_STEPS.APPLY,
+      expression: "'bicycle'",
+      AS: 'type'
+    }, {
+      type: FT_AGGREGATE_STEPS.GROUPBY,
+      properties: '@type',
+      REDUCE: [{
+        type: FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT,
+        property: null,
+        AS: 'num_total'
+      }]
+    }]
+});
+console.log(res3.results.length); // >>> 1
+console.log(res3.results); // >>>
+//[ [Object: null prototype] { type: 'bicycle', num_total: '10' } ]
+// REMOVE_START
+assert.strictEqual(res3.results.length, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START agg4
+const res4 = await client.ft.aggregate('idx:bicycle', '*', {
+  LOAD: ['__key'],
+  STEPS: [{
+      type: FT_AGGREGATE_STEPS.GROUPBY,
+      properties: '@condition',
+      REDUCE: [{
+        type: FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST,
+        property: '__key',
+        AS: 'bicycles'
+      }]
+    }]
+});
+console.log(res4.results.length); // >>> 3
+console.log(res4.results); // >>>
+//[[Object: null prototype] {condition: 'refurbished', bicycles: [ 'bicycle:9' ]},
+//  [Object: null prototype] {condition: 'used', bicycles: [ 'bicycle:1', 'bicycle:2', 'bicycle:3', 'bicycle:4' ]},
+//  [Object: null prototype] {condition: 'new', bicycles: [ 'bicycle:5', 'bicycle:6', 'bicycle:7', 'bicycle:0', 'bicycle:8' ]}]
+// REMOVE_START
+assert.strictEqual(res4.results.length, 3);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/query-combined.js b/doctests/query-combined.js
new file mode 100644
index 00000000000..a2ad8d43c93
--- /dev/null
+++ b/doctests/query-combined.js
@@ -0,0 +1,191 @@
+// EXAMPLE: query_combined
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient } from 'redis';
+import { SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from '@redis/search';
+import { pipeline } from '@xenova/transformers';
+
+function float32Buffer(arr) {
+  const floatArray = new Float32Array(arr);
+  const float32Buffer = Buffer.from(floatArray.buffer);
+  return float32Buffer;
+}
+
+async function embedText(sentence) {
+  let modelName = 'Xenova/all-MiniLM-L6-v2';
+  let pipe = await pipeline('feature-extraction', modelName);
+
+  let vectorOutput = await pipe(sentence, {
+      pooling: 'mean',
+      normalize: true,
+  });
+
+  if (vectorOutput == null) {
+    throw new Error('vectorOutput is undefined');
+  }
+
+  const embedding = Object.values(vectorOutput.data);
+
+  return embedding;
+}
+
+let vector_query = float32Buffer(await embedText('That is a very happy person'));
+
+const client = createClient();
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+    '$.description': {
+      type: SCHEMA_FIELD_TYPE.TEXT,
+      AS: 'description'
+    },
+    '$.condition': {
+      type: SCHEMA_FIELD_TYPE.TAG,
+      AS: 'condition'
+    },
+    '$.price': {
+      type: SCHEMA_FIELD_TYPE.NUMERIC,
+      AS: 'price'
+    },
+    '$.description_embeddings': {
+        type: SCHEMA_FIELD_TYPE.VECTOR,
+        TYPE: 'FLOAT32',
+        ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT,
+        DIM: 384,
+        DISTANCE_METRIC: 'COSINE',
+        AS: 'vector',
+    }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+});
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_vector.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START combined1
+const res1 = await client.ft.search('idx:bicycle', '@price:[500 1000] @condition:{new}');
+console.log(res1.total); // >>> 1
+console.log(res1); // >>>
+//{
+//  total: 1,
+//  documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]
+//}
+// REMOVE_START
+assert.strictEqual(res1.total, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START combined2
+const res2 = await client.ft.search('idx:bicycle', 'kids @price:[500 1000] @condition:{used}');
+console.log(res2.total); // >>> 1
+console.log(res2); // >>>
+// {
+//   total: 1,
+//   documents: [ { id: 'bicycle:2', value: [Object: null prototype] } ]
+// }
+// REMOVE_START
+assert.strictEqual(res2.total, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START combined3
+const res3 = await client.ft.search('idx:bicycle', '(kids | small) @condition:{used}');
+console.log(res3.total); // >>> 2
+console.log(res3); // >>>
+//{
+//  total: 2,
+//  documents: [
+//    { id: 'bicycle:2', value: [Object: null prototype] },
+//    { id: 'bicycle:1', value: [Object: null prototype] }
+//  ]
+//}
+// REMOVE_START
+assert.strictEqual(res3.total, 2);
+// REMOVE_END
+// STEP_END
+
+// STEP_START combined4
+const res4 = await client.ft.search('idx:bicycle', '@description:(kids | small) @condition:{used}');
+console.log(res4.total); // >>> 2
+console.log(res4); // >>>
+//{
+//  total: 2,
+//  documents: [
+//    { id: 'bicycle:2', value: [Object: null prototype] },
+//    { id: 'bicycle:1', value: [Object: null prototype] }
+//  ]
+//}
+// REMOVE_START
+assert.strictEqual(res4.total, 2);
+// REMOVE_END
+// STEP_END
+
+// STEP_START combined5
+const res5 = await client.ft.search('idx:bicycle', '@description:(kids | small) @condition:{new | used}');
+console.log(res5.total); // >>> 3
+console.log(res5); // >>>
+//{
+//  total: 3,
+//  documents: [
+//    { id: 'bicycle:1', value: [Object: null prototype] },
+//    { id: 'bicycle:0', value: [Object: null prototype] },
+//    { id: 'bicycle:2', value: [Object: null prototype] }
+//  ]
+//}
+// REMOVE_START
+assert.strictEqual(res5.total, 3);
+// REMOVE_END
+// STEP_END
+
+// STEP_START combined6
+const res6 = await client.ft.search('idx:bicycle', '@price:[500 1000] -@condition:{new}');
+console.log(res6.total); // >>> 2
+console.log(res6); // >>>
+//{
+//  total: 2,
+//  documents: [
+//    { id: 'bicycle:2', value: [Object: null prototype] },
+//    { id: 'bicycle:9', value: [Object: null prototype] }
+//  ]
+//}
+// REMOVE_START
+assert.strictEqual(res6.total, 2);
+// REMOVE_END
+// STEP_END
+
+// STEP_START combined7
+const res7 = await client.ft.search('idx:bicycle', 
+  '(@price:[500 1000] -@condition:{new})=>[KNN 3 @vector $query_vector]', {
+    PARAMS: { query_vector: vector_query },
+    DIALECT: 2
+  }
+);
+console.log(res7.total); // >>> 2
+console.log(res7); // >>>
+//{
+//  total: 2,
+//  documents: [
+//    { id: 'bicycle:2', value: [Object: null prototype] },
+//    { id: 'bicycle:9', value: [Object: null prototype] }
+//  ]
+//}
+// REMOVE_START
+assert.strictEqual(res7.total, 2);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/query-em.js b/doctests/query-em.js
new file mode 100644
index 00000000000..9b5782e09ce
--- /dev/null
+++ b/doctests/query-em.js
@@ -0,0 +1,121 @@
+// EXAMPLE: query_em
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient, SCHEMA_FIELD_TYPE } from 'redis';
+
+const client = createClient();
+
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+  '$.description': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'description'
+  },
+  '$.price': {
+    type: SCHEMA_FIELD_TYPE.NUMERIC,
+    AS: 'price'
+  },
+  '$.condition': {
+    type: SCHEMA_FIELD_TYPE.TAG,
+    AS: 'condition'
+  }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+})
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START em1
+const res1 = await client.ft.search('idx:bicycle', '@price:[270 270]');
+console.log(res1.total); // >>> 1
+// REMOVE_START
+assert.strictEqual(res1.total, 1);
+// REMOVE_END
+
+try {
+    const res2 = await client.ft.search('idx:bicycle', '@price:[270]');
+    console.log(res2.total); // >>> 1
+    assert.strictEqual(res2.total, 1);
+} catch (err) {
+    console.log("'@price:[270]' syntax not yet supported.");
+}
+
+try {
+    const res3 = await client.ft.search('idx:bicycle', '@price==270');
+    console.log(res3.total); // >>> 1
+    assert.strictEqual(res3.total, 1);
+} catch (err) {
+    console.log("'@price==270' syntax not yet supported.");
+}
+
+// FILTER is not supported
+// const res4 = await client.ft.search('idx:bicycle', '*', {
+//   FILTER: {
+//       field: 'price',
+//       min: 270,
+//       max: 270,
+//   }
+// });
+// console.log(res4.total); // >>> 1
+// REMOVE_START
+// assert.strictEqual(res4.total, 10);
+// REMOVE_END
+// STEP_END
+
+// STEP_START em2
+const res5 = await client.ft.search('idx:bicycle', '@condition:{new}');
+console.log(res5.total); // >>> 5
+// REMOVE_START
+assert.strictEqual(res5.total, 5);
+// REMOVE_END
+// STEP_END
+
+// STEP_START em3
+await client.ft.create('idx:email', {
+  '$.email': {
+    type: SCHEMA_FIELD_TYPE.TAG,
+    AS: 'email'
+  }
+}, {
+  ON: 'JSON',
+  PREFIX: 'key:'
+})
+
+await client.json.set('key:1', '$', { email: 'test@redis.com' });
+
+try {
+    const res6 = await client.ft.search('idx:email', 'test@redis.com', { DIALECT: 2 });
+    console.log(res6);
+} catch (err) {
+    console.log("'test@redis.com' syntax not yet supported.");
+}
+// REMOVE_START
+await client.ft.dropIndex('idx:email', { DD: true });
+// REMOVE_END
+// STEP_END
+
+// STEP_START em4
+const res7 = await client.ft.search('idx:bicycle', '@description:"rough terrain"');
+console.log(res7.total); // >>> 1 (Result{1 total, docs: [Document {'id': 'bicycle:8'...)
+// REMOVE_START
+assert.strictEqual(res7.total, 1);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
diff --git a/doctests/query-ft.js b/doctests/query-ft.js
new file mode 100644
index 00000000000..a70b2ea3f5d
--- /dev/null
+++ b/doctests/query-ft.js
@@ -0,0 +1,84 @@
+// EXAMPLE: query_ft
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient, SCHEMA_FIELD_TYPE } from 'redis';
+
+const client = createClient();
+
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+  '$.model': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'model'
+  },
+  '$.brand': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'brand'
+  },
+  '$.description': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'description'
+  }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+})
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START ft1
+const res1 = await client.ft.search('idx:bicycle', '@description: kids');
+console.log(res1.total); // >>> 2
+// REMOVE_START
+assert.strictEqual(res1.total, 2);
+// REMOVE_END
+// STEP_END
+
+// STEP_START ft2
+const res2 = await client.ft.search('idx:bicycle', '@model: ka*');
+console.log(res2.total); // >>> 1
+// REMOVE_START
+assert.strictEqual(res2.total, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START ft3
+const res3 = await client.ft.search('idx:bicycle', '@brand: *bikes');
+console.log(res3.total); // >>> 2
+// REMOVE_START
+assert.strictEqual(res3.total, 2);
+// REMOVE_END
+// STEP_END
+
+// STEP_START ft4
+const res4 = await client.ft.search('idx:bicycle', '%optamized%');
+console.log(res4); // >>> { total: 1, documents: [ { id: 'bicycle:3', value: [Object: null prototype] } ]}
+// REMOVE_START
+assert.strictEqual(res4.total, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START ft5
+const res5 = await client.ft.search('idx:bicycle', '%%optamised%%');
+console.log(res5); // >>> { total: 1, documents: [ { id: 'bicycle:3', value: [Object: null prototype] } ]}
+// REMOVE_START
+assert.strictEqual(res5.total, 1);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/query-geo.js b/doctests/query-geo.js
new file mode 100644
index 00000000000..41ef2141eb8
--- /dev/null
+++ b/doctests/query-geo.js
@@ -0,0 +1,82 @@
+// EXAMPLE: query_geo
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient } from 'redis';
+import { SCHEMA_FIELD_TYPE } from '@redis/search';
+
+const client = createClient();
+
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+  '$.store_location': {
+    type: SCHEMA_FIELD_TYPE.GEO,
+    AS: 'store_location'
+  },
+   '$.pickup_zone': {
+    type: SCHEMA_FIELD_TYPE.GEOSHAPE,
+    AS: 'pickup_zone'
+  }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+})
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START geo1
+const res1= await client.ft.search('idx:bicycle', '@store_location:[-0.1778 51.5524 20 mi]');
+console.log(res1.total); // >>> 1
+console.log(res1); // >>> {total: 1, documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]}
+// REMOVE_START
+assert.strictEqual(res1.total, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START geo2
+const params_dict_geo2 = { bike: 'POINT(-0.1278 51.5074)' };
+const q_geo2 = '@pickup_zone:[CONTAINS $bike]';
+const res2 = await client.ft.search('idx:bicycle', q_geo2, { PARAMS: params_dict_geo2, DIALECT: 3 });
+console.log(res2.total); // >>> 1
+console.log(res2); // >>> {total: 1, documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]}
+// REMOVE_START
+assert.strictEqual(res2.total, 1);
+// REMOVE_END
+// STEP_END
+
+// STEP_START geo3
+const params_dict_geo3 = { europe: 'POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))' };
+const q_geo3 = '@pickup_zone:[WITHIN $europe]';
+const res3 = await client.ft.search('idx:bicycle', q_geo3, { PARAMS: params_dict_geo3, DIALECT: 3 });
+console.log(res3.total); // >>> 5
+console.log(res3); // >>>
+// {
+//   total: 5,
+//   documents: [
+//     { id: 'bicycle:5', value: [Object: null prototype] },
+//     { id: 'bicycle:6', value: [Object: null prototype] },
+//     { id: 'bicycle:7', value: [Object: null prototype] },
+//     { id: 'bicycle:8', value: [Object: null prototype] },
+//     { id: 'bicycle:9', value: [Object: null prototype] }
+//   ]
+// }
+// REMOVE_START
+assert.strictEqual(res3.total, 5);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/query-range.js b/doctests/query-range.js
new file mode 100644
index 00000000000..29b269e37b5
--- /dev/null
+++ b/doctests/query-range.js
@@ -0,0 +1,98 @@
+// EXAMPLE: query_range
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient, SCHEMA_FIELD_TYPE,} from 'redis';
+
+const client = createClient();
+
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+  '$.description': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'description'
+  },
+  '$.price': {
+    type: SCHEMA_FIELD_TYPE.NUMERIC,
+    AS: 'price'
+  },
+  '$.condition': {
+    type: SCHEMA_FIELD_TYPE.TAG,
+    AS: 'condition'
+  }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+})
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START range1
+const res1 = await client.ft.search('idx:bicycle', '@price:[500 1000]');
+console.log(res1.total); // >>> 3
+// REMOVE_START
+assert.strictEqual(res1.total, 3);
+// REMOVE_END
+// STEP_END
+
+// STEP_START range2
+// FILTER is not supported
+// const res2 = await client.ft.search('idx:bicycle', '*', {
+//   FILTER: {
+//     field: 'price',
+//     min: 500,
+//     max: 1000,
+//   }
+// });
+// console.log(res2.total); // >>> 3
+// REMOVE_START
+// assert.strictEqual(res2.total, 3);
+// REMOVE_END
+// STEP_END
+
+// STEP_START range3
+// FILTER is not supported
+// const res3 = await client.ft.search('idx:bicycle', '*', {
+//   FILTER: {
+//     field: 'price',
+//     min: '(1000',
+//     max: '+inf,
+//   }
+// });
+// console.log(res3.total); // >>> 5
+// REMOVE_START
+// assert.strictEqual(res3.total, 5);
+// REMOVE_END
+// STEP_END
+
+// STEP_START range4
+const res4 = await client.ft.search(
+  'idx:bicycle',
+  '@price:[-inf 2000]',
+  {
+    SORTBY: 'price',
+    LIMIT: { from: 0, size: 5 }
+  }
+);
+console.log(res4.total); // >>> 7
+console.log(res4); // >>> { total: 7, documents: [ { id: 'bicycle:0', value: [Object: null prototype] }, { id: 'bicycle:7', value: [Object: null prototype] }, { id: 'bicycle:5', value: [Object: null prototype] }, { id: 'bicycle:2', value: [Object: null prototype] }, { id: 'bicycle:9', value: [Object: null prototype] } ] }
+// REMOVE_START
+assert.strictEqual(res4.total, 7);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/query-vector.js b/doctests/query-vector.js
new file mode 100644
index 00000000000..91ee63120d3
--- /dev/null
+++ b/doctests/query-vector.js
@@ -0,0 +1,110 @@
+// EXAMPLE: query_vector
+// HIDE_START
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { createClient } from 'redis';
+import { SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from '@redis/search';
+import { pipeline } from '@xenova/transformers';
+
+function float32Buffer(arr) {
+  const floatArray = new Float32Array(arr);
+  const float32Buffer = Buffer.from(floatArray.buffer);
+  return float32Buffer;
+}
+
+async function embedText(sentence) {
+  let modelName = 'Xenova/all-MiniLM-L6-v2';
+  let pipe = await pipeline('feature-extraction', modelName);
+
+  let vectorOutput = await pipe(sentence, {
+      pooling: 'mean',
+      normalize: true,
+  });
+
+  const embedding = Object.values(vectorOutput?.data);
+
+  return embedding;
+}
+
+const vector_query = float32Buffer(await embedText('That is a very happy person'));
+
+const client = createClient();
+await client.connect().catch(console.error);
+
+// create index
+await client.ft.create('idx:bicycle', {
+    '$.description': {
+      type: SCHEMA_FIELD_TYPE.TEXT,
+      AS: 'description'
+    },
+    '$.description_embeddings': {
+        type: SCHEMA_FIELD_TYPE.VECTOR,
+        TYPE: 'FLOAT32',
+        ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT,
+        DIM: 384,
+        DISTANCE_METRIC: 'COSINE',
+        AS: 'vector'
+    }
+}, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+});
+
+// load data
+const bicycles = JSON.parse(fs.readFileSync('data/query_vector.json', 'utf8'));
+
+await Promise.all(
+  bicycles.map((bicycle, bid) => {
+      return client.json.set(`bicycle:${bid}`, '$', bicycle);
+  })
+);
+// HIDE_END
+
+// STEP_START vector1
+const res1 = await client.ft.search('idx:bicycle', 
+  '*=>[KNN 3 @vector $query_vector AS score]', {
+    PARAMS: { query_vector: vector_query },
+    RETURN: ['description'],
+    DIALECT: 2
+  }
+);
+console.log(res1.total); // >>> 3
+console.log(res1); // >>>
+//{
+//  total: 3,
+//  documents: [
+//    { id: 'bicycle:0', value: [Object: null prototype] {} },
+//    { id: 'bicycle:2', value: [Object: null prototype] {} },
+//    { id: 'bicycle:9', value: [Object: null prototype] {} }
+//  ]
+//}
+// REMOVE_START
+assert.strictEqual(res1.total, 3);
+// REMOVE_END
+// STEP_END
+
+// STEP_START vector2
+const res2 = await client.ft.search('idx:bicycle', 
+  '@vector:[VECTOR_RANGE 0.9 $query_vector]=>{$YIELD_DISTANCE_AS: vector_dist}', {
+    PARAMS: { query_vector: vector_query },
+    SORTBY: 'vector_dist',
+    RETURN: ['vector_dist', 'description'],
+    DIALECT: 2
+  }
+);
+console.log(res2.total); // >>> 1
+console.log(res2); // >>>
+//{
+//  total: 1,
+//  documents: [ { id: 'bicycle:0', value: [Object: null prototype] } ]
+//}
+// REMOVE_START
+assert.strictEqual(res2.total, 1);
+// REMOVE_END
+// STEP_END
+
+// REMOVE_START
+// destroy index and data
+await client.ft.dropIndex('idx:bicycle', { DD: true });
+await client.close();
+// REMOVE_END
\ No newline at end of file
diff --git a/doctests/run_examples.sh b/doctests/run_examples.sh
new file mode 100755
index 00000000000..ee7b50dc69a
--- /dev/null
+++ b/doctests/run_examples.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+
+basepath=`readlink -f $1`
+if [ $? -ne 0 ]; then
+basepath=`readlink -f $(dirname $0)`
+fi
+echo "No path specified, using ${basepath}"
+
+set -e
+cd ${basepath}
+for i in `ls ${basepath}/*.js`; do
+    redis-cli flushdb
+    node $i
+done
\ No newline at end of file
diff --git a/doctests/search-quickstart.js b/doctests/search-quickstart.js
new file mode 100644
index 00000000000..ec3f65a5e3a
--- /dev/null
+++ b/doctests/search-quickstart.js
@@ -0,0 +1,231 @@
+// EXAMPLE: search_quickstart
+// REMOVE_START
+import assert from 'assert';
+// REMOVE_END
+// HIDE_START
+import { createClient, SCHEMA_FIELD_TYPE } from 'redis';
+// HIDE_END
+// STEP_START connect
+const client = createClient();
+client.on('error', err => console.log('Redis Client Error', err));
+
+await client.connect();
+// STEP_END
+
+// STEP_START data_sample
+const bicycle1 = {
+  brand: 'Velorim',
+  model: 'Jigger',
+  price: 270,
+  description:
+    'Small and powerful, the Jigger is the best ' +
+    'ride for the smallest of tikes! This is the tiniest kids\u2019 ' +
+    'pedal bike on the market available without a coaster brake, the ' +
+    'Jigger is the vehicle of choice for the rare tenacious little' +
+    'rider raring to go.',
+  condition: 'new'
+};
+// STEP_END
+const bicycles = [
+  bicycle1,
+  {
+    brand: 'Bicyk',
+    model: 'Hillcraft',
+    price: 1200,
+    description: 'Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!',
+    condition: 'used'
+  },
+  {
+    brand: 'Nord',
+    model: 'Chook air 5',
+    price: 815,
+    description: 'The Chook Air 5 gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.',
+    condition: 'used'
+  },
+  {
+    brand: 'Eva',
+    model: 'Eva 291',
+    price: 3400,
+    description: 'The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It\u2019s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!',
+    condition: 'used'
+  },
+  {
+    brand: 'Noka Bikes',
+    model: 'Kahuna',
+    price: 3200,
+    description: 'Whether you want to try your hand at XC racing or are looking for a lively trail bike that\'s just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women\u2019s saddle, different bars and unique colourway.',
+    condition: 'used'
+  },
+  {
+    brand: 'Breakout',
+    model: 'XBN 2.1 Alloy',
+    price: 810,
+    description: 'The XBN 2.1 Alloy is our entry-level road bike \u2013 but that\u2019s not to say that it\u2019s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance.',
+    condition: 'new'
+  },
+  {
+    brand: 'ScramBikes',
+    model: 'WattBike',
+    price: 2300,
+    description: 'The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\u2019s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.',
+    condition: 'new'
+  },
+  {
+    brand: 'Peaknetic',
+    model: 'Secto',
+    price: 430,
+    description: 'If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.',
+    condition: 'new'
+  },
+  {
+    brand: 'nHill',
+    model: 'Summit',
+    price: 1200,
+    description: 'This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\u2019re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.',
+    condition: 'new'
+  },
+  {
+    model: 'ThrillCycle',
+    brand: 'BikeShind',
+    price: 815,
+    description: 'An artsy, retro-inspired bicycle that\u2019s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn\u2019t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.',
+    condition: 'refurbished'
+  }
+];
+// STEP_START create_index
+const schema = {
+  '$.brand': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    SORTABLE: true,
+    AS: 'brand'
+  },
+  '$.model': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'model'
+  },
+  '$.description': {
+    type: SCHEMA_FIELD_TYPE.TEXT,
+    AS: 'description'
+  },
+  '$.price': {
+    type: SCHEMA_FIELD_TYPE.NUMERIC,
+    AS: 'price'
+  },
+  '$.condition': {
+    type: SCHEMA_FIELD_TYPE.TAG,
+    AS: 'condition'
+  }
+};
+
+try {
+  await client.ft.create('idx:bicycle', schema, {
+    ON: 'JSON',
+    PREFIX: 'bicycle:'
+  });
+} catch (e) {
+  if (e.message === 'Index already exists') {
+    console.log('Index exists already, skipped creation.');
+  } else {
+    // Something went wrong, perhaps RediSearch isn't installed...
+    console.error(e);
+    process.exit(1);
+  }
+}
+// STEP_END
+
+// STEP_START add_documents
+await Promise.all(
+  bicycles.map((bicycle, i) => client.json.set(`bicycle:${i}`, '$', bicycle))
+);
+// STEP_END
+
+// STEP_START wildcard_query
+let result = await client.ft.search('idx:bicycle', '*', {
+  LIMIT: {
+    from: 0,
+    size: 10
+  }
+});
+
+console.log(JSON.stringify(result, null, 2));
+
+/*
+{
+  "total": 10,
+  "documents": ...
+}
+*/
+// STEP_END
+
+// REMOVE_START
+assert.equal(result.documents[0].id, 'bicycle:0');
+// REMOVE_END
+
+// STEP_START query_single_term
+result = await client.ft.search(
+  'idx:bicycle',
+  '@model:Jigger',
+  {
+    LIMIT: {
+    from: 0,
+    size: 10
+  }
+});
+
+console.log(JSON.stringify(result, null, 2));
+/*
+{
+  "total": 1,
+  "documents": [{
+    "id": "bicycle:0",
+    "value": {
+      "brand": "Velorim",
+      "model": "Jigger",
+      "price": 270,
+      "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.",
+      "condition": "new"
+    }
+  }]
+}
+ */
+// STEP_END
+// REMOVE_START
+assert.equal(result.documents[0].id, 'bicycle:0');
+// REMOVE_END
+
+// STEP_START query_exact_matching
+result = await client.ft.search(
+  'idx:bicycle',
+  '@brand:"Noka Bikes"',
+  {
+    LIMIT: {
+      from: 0,
+      size: 10
+    }
+  }
+);
+
+console.log(JSON.stringify(result, null, 2));
+
+/*
+{
+  "total": 1,
+  "documents": [{
+    "id": "bicycle:4",
+    "value": {
+      "brand": "Noka Bikes",
+      "model": "Kahuna",
+      "price": 3200,
+      "description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.",
+      "condition": "used"
+    }
+  }]
+}
+*/
+// STEP_END
+
+// REMOVE_START
+assert.equal(result.documents[0].id, 'bicycle:4');
+// REMOVE END
+
+await client.close();
diff --git a/doctests/string-set-get-example.js b/doctests/string-set-get-example.js
new file mode 100644
index 00000000000..b9be382285e
--- /dev/null
+++ b/doctests/string-set-get-example.js
@@ -0,0 +1,27 @@
+// EXAMPLE: set_and_get
+// REMOVE_START
+import assert from "node:assert";
+// REMOVE_END
+
+// HIDE_START
+import { createClient } from 'redis';
+
+const client = createClient();
+
+client.on('error', err => console.log('Redis Client Error', err));
+
+await client.connect().catch(console.error);
+
+// HIDE_END
+await client.set('bike:1', 'Process 134');
+const value = await client.get('bike:1');
+console.log(value);
+// returns 'Process 134'
+//REMOVE_START
+assert.equal(value, 'Process 134');
+await client.del('bike:1');
+//REMOVE_END
+
+// HIDE_START
+await client.close();
+// HIDE_END
diff --git a/examples/README.md b/examples/README.md
index 4e7655a3519..47c9912fd31 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -40,12 +40,12 @@ We'd love to see more examples here. If you have an idea that you'd like to see
 
 To set up the examples folder so that you can run an example / develop one of your own:
 
-```
-$ git clone https://github.com/redis/node-redis.git
-$ cd node-redis
-$ npm install -ws && npm run build-all
-$ cd examples
-$ npm install
+```bash
+git clone https://github.com/redis/node-redis.git
+cd node-redis
+npm install -ws && npm run build
+cd examples
+npm install
 ```
 
 ### Coding Guidelines for Examples
@@ -91,5 +91,5 @@ await client.connect();
 
 // Add your example code here...
 
-await client.quit();
+client.close();
 ```
diff --git a/examples/blocking-list-pop.js b/examples/blocking-list-pop.js
index 099c73a2a96..f29a409398c 100644
--- a/examples/blocking-list-pop.js
+++ b/examples/blocking-list-pop.js
@@ -4,16 +4,15 @@
 // The script will be blocked until the LPUSH command is executed.
 // After which we log the list and quit the client.
 
-import { createClient, commandOptions } from 'redis';
+import {  createClientPool } from 'redis';
 
-const client = createClient();
+const client = createClientPool();
 
 await client.connect();
 
 const keyName = 'keyName';
 
 const blpopPromise = client.blPop(
-  commandOptions({ isolated: true }),
   keyName,
   0
 );
@@ -27,4 +26,4 @@ console.log('blpopPromise resolved');
 // {"key":"keyName","element":"value"}
 console.log(`listItem is '${JSON.stringify(listItem)}'`);
 
-await client.quit();
+client.destroy();
diff --git a/examples/bloom-filter.js b/examples/bloom-filter.js
index cf5f1940b3e..a133b0274f2 100644
--- a/examples/bloom-filter.js
+++ b/examples/bloom-filter.js
@@ -77,4 +77,4 @@ const info = await client.bf.info('mybloom');
 //  }
 console.log(info);
 
-await client.quit();
+client.destroy();
diff --git a/examples/check-connection-status.js b/examples/check-connection-status.js
index 0ccf8ff5e21..ae3c863fb14 100644
--- a/examples/check-connection-status.js
+++ b/examples/check-connection-status.js
@@ -25,4 +25,4 @@ console.log('Afer connectPromise has resolved...');
 // isReady will return True here, client is ready to use.
 console.log(`client.isOpen: ${client.isOpen}, client.isReady: ${client.isReady}`);
 
-await client.quit();
+client.destroy();
diff --git a/examples/command-with-modifiers.js b/examples/command-with-modifiers.js
index 974f78dc5d8..356304722c0 100644
--- a/examples/command-with-modifiers.js
+++ b/examples/command-with-modifiers.js
@@ -8,18 +8,24 @@ const client = createClient();
 await client.connect();
 await client.del('mykey');
 
-let result = await client.set('mykey', 'myvalue', {
-  EX: 60,
-  GET: true
-});
-
-console.log(result); //null
-
-result = await client.set('mykey', 'newvalue', {
-  EX: 60,
-  GET: true
-});
-
-console.log(result); //myvalue
-
-await client.quit();
+console.log(
+  await client.set('mykey', 'myvalue', {
+    expiration: {
+      type: 'EX',
+      value: 60
+    },
+    GET: true
+  })
+); // null
+
+console.log(
+  await client.set('mykey', 'newvalue', {
+    expiration: { 
+      type: 'EX',
+      value: 60
+    },
+    GET: true
+  })
+); // 'myvalue'
+
+await client.close();
diff --git a/examples/connect-as-acl-user.js b/examples/connect-as-acl-user.js
index df46aa1e288..bc3069b5bbc 100644
--- a/examples/connect-as-acl-user.js
+++ b/examples/connect-as-acl-user.js
@@ -23,4 +23,4 @@ try {
   console.log(`GET command failed: ${e.message}`);
 }
 
-await client.quit();
+client.destroy();
diff --git a/examples/connect-to-cluster.js b/examples/connect-to-cluster.js
index 98655497c9e..86e45b87968 100644
--- a/examples/connect-to-cluster.js
+++ b/examples/connect-to-cluster.js
@@ -1,7 +1,7 @@
 // This is an example script to connect to a running cluster.
 // After connecting to the cluster the code sets and get a value.
 
-// To setup this cluster you can follow the guide here: 
+// To setup this cluster you can follow the guide here:
 // https://redis.io/docs/manual/scaling/
 // In this guide the ports which are being used are 7000 - 7005
 
@@ -29,5 +29,4 @@ await cluster.connect();
 await cluster.set('hello', 'cluster');
 const value = await cluster.get('hello');
 console.log(value);
-
-await cluster.quit();
+await cluster.close();
diff --git a/examples/count-min-sketch.js b/examples/count-min-sketch.js
index f88a148986f..ffbe13a7c27 100644
--- a/examples/count-min-sketch.js
+++ b/examples/count-min-sketch.js
@@ -77,4 +77,4 @@ console.log('Count-Min Sketch info:');
 // }
 console.log(info);
 
-await client.quit();
+client.destroy();
diff --git a/examples/cuckoo-filter.js b/examples/cuckoo-filter.js
index 87976f3fefb..6ab58fbfa5c 100644
--- a/examples/cuckoo-filter.js
+++ b/examples/cuckoo-filter.js
@@ -76,4 +76,4 @@ const info = await client.cf.info('mycuckoo');
 // }
 console.log(info);
 
-await client.quit();
+client.destroy();
diff --git a/examples/dump-and-restore.js b/examples/dump-and-restore.js
index 081e44f9f9a..f464fd38be1 100644
--- a/examples/dump-and-restore.js
+++ b/examples/dump-and-restore.js
@@ -1,22 +1,32 @@
 // This example demonstrates the use of the DUMP and RESTORE commands
 
-import { commandOptions, createClient } from 'redis';
+import { createClient, RESP_TYPES } from 'redis';
 
-const client = createClient();
-await client.connect();
+const client = await createClient({
+  commandOptions: {
+    typeMapping: {
+      [RESP_TYPES.BLOB_STRING]: Buffer
+    }
+  }
+}).on('error', err => {
+  console.log('Redis Client Error', err);
+}).connect();
+
+// Make sure the source key exists
+await client.set('source', 'value');
+
+// Make sure destination doesnt exist
+await client.del('destination');
 
 // DUMP a specific key into a local variable
-const dump = await client.dump(
-    commandOptions({ returnBuffers: true }),
-    'source'
-);
+const dump = await client.dump('source');
 
 // RESTORE into a new key
 await client.restore('destination', 0, dump);
 
 // RESTORE and REPLACE an existing key
 await client.restore('destination', 0, dump, {
-    REPLACE: true
+  REPLACE: true
 });
 
-await client.quit();
+await client.close();
diff --git a/examples/get-server-time.js b/examples/get-server-time.js
index 967859f0136..752264df349 100644
--- a/examples/get-server-time.js
+++ b/examples/get-server-time.js
@@ -6,7 +6,13 @@ const client = createClient();
 await client.connect();
 
 const serverTime = await client.time();
-// 2022-02-25T12:57:40.000Z { microseconds: 351346 }
+// In v5, TIME returns [unixTimestamp: string, microseconds: string] instead of Date
+// Example: ['1708956789', '123456']
 console.log(serverTime);
 
-await client.quit();
+// Convert to JavaScript Date if needed
+const [seconds, microseconds] = serverTime;
+const date = new Date(parseInt(seconds) * 1000 + parseInt(microseconds) / 1000);
+console.log('Converted to Date:', date);
+
+client.close();
diff --git a/examples/hyperloglog.js b/examples/hyperloglog.js
index 4ac9b575f96..1f8f04f2a6c 100644
--- a/examples/hyperloglog.js
+++ b/examples/hyperloglog.js
@@ -9,7 +9,7 @@ const client = createClient();
 await client.connect();
 
 // Use `pfAdd` to add an element to a Hyperloglog, creating the Hyperloglog if necessary.
-// await client.pfAdd(key, value)
+// await client.pfAdd(key, value) // returns 1 or 0
 
 // To get a count, the `pfCount` method is used.
 // await client.pfCount(key)
@@ -48,4 +48,4 @@ try {
   console.error(e);
 }
 
-await client.quit();
+client.close();
diff --git a/examples/lua-multi-incr.js b/examples/lua-multi-incr.js
index 8eb1092c295..71b12bdab0f 100644
--- a/examples/lua-multi-incr.js
+++ b/examples/lua-multi-incr.js
@@ -12,9 +12,11 @@ const client = createClient({
         'redis.pcall("INCRBY", KEYS[1], ARGV[1]),' +
         'redis.pcall("INCRBY", KEYS[2], ARGV[1])' +
         '}',
-      transformArguments(key1, key2, increment) {
-        return [key1, key2, increment.toString()];
-      },
+        parseCommand(parser, key1, key2, increment) {
+          parser.pushKey(key1);
+          parser.pushKey(key2);
+          parser.push(increment.toString());
+        },
     }),
   },
 });
@@ -24,4 +26,4 @@ await client.connect();
 await client.set('mykey', '5');
 console.log(await client.mincr('mykey', 'myotherkey', 10)); // [ 15, 10 ]
 
-await client.quit();
+client.destroy();
diff --git a/examples/managing-json.js b/examples/managing-json.js
index 81949d5c222..0f1eb3b3c21 100644
--- a/examples/managing-json.js
+++ b/examples/managing-json.js
@@ -57,7 +57,7 @@ results = await client.json.get('noderedis:jsondata', {
 });
 
 // Goldie is 3 years old now.
-console.log(`Goldie is ${JSON.stringify(results[0])} years old now.`);
+console.log(`Goldie is ${JSON.parse(results)[0]} years old now.`);
 
 // Add a new pet...
 await client.json.arrAppend('noderedis:jsondata', '$.pets', {
@@ -68,9 +68,14 @@ await client.json.arrAppend('noderedis:jsondata', '$.pets', {
 });
 
 // How many pets do we have now?
-const numPets = await client.json.arrLen('noderedis:jsondata', '$.pets');
+const numPets = await client.json.arrLen('noderedis:jsondata', { path: '$.pets' });
 
 // We now have 4 pets.
 console.log(`We now have ${numPets} pets.`);
 
-await client.quit();
+const rex = { name: 'Rex', species: 'dog', age: 3, isMammal: true }
+
+const index = await client.json.arrIndex( 'noderedis:jsondata', '$.pets', rex);
+console.log(`Rex is at index ${index}`);
+
+client.close();
diff --git a/examples/package.json b/examples/package.json
index 65ba1442f7e..c350c0b248b 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -1,12 +1,12 @@
 {
   "name": "node-redis-examples",
   "version": "1.0.0",
-  "description": "node-redis 4 example script",
+  "description": "node-redis 5 example script",
   "main": "index.js",
   "private": true,
   "type": "module",
   "dependencies": {
-    "redis": "../"
+    "redis": "../packages/redis"
   }
 }
 
diff --git a/examples/search-hashes.js b/examples/search-hashes.js
index 2f8b5fbf7b6..a496fec823a 100644
--- a/examples/search-hashes.js
+++ b/examples/search-hashes.js
@@ -1,7 +1,7 @@
 // This example demonstrates how to use RediSearch to index and query data 
 // stored in Redis hashes.
 
-import { createClient, SchemaFieldTypes } from 'redis';
+import { createClient, SCHEMA_FIELD_TYPE } from 'redis';
 
 const client = createClient();
 
@@ -12,11 +12,11 @@ try {
   // Documentation: https://redis.io/commands/ft.create/
   await client.ft.create('idx:animals', {
     name: {
-      type: SchemaFieldTypes.TEXT,
+      type: SCHEMA_FIELD_TYPE.TEXT,
       SORTABLE: true
     },
-    species: SchemaFieldTypes.TAG,
-    age: SchemaFieldTypes.NUMERIC
+    species: SCHEMA_FIELD_TYPE.TAG,
+    age: SCHEMA_FIELD_TYPE.NUMERIC
   }, {
     ON: 'HASH',
     PREFIX: 'noderedis:animals'
@@ -85,4 +85,4 @@ for (const doc of results.documents) {
   console.log(`${doc.id}: ${doc.value.name}, ${doc.value.age} years old.`);
 }
 
-await client.quit();
+client.destroy();
diff --git a/examples/search-json.js b/examples/search-json.js
index 6481889ecfd..60f2ff095ed 100644
--- a/examples/search-json.js
+++ b/examples/search-json.js
@@ -3,7 +3,7 @@
 // https://redis.io/docs/stack/search/
 // https://redis.io/docs/stack/json/
 
-import { createClient, SchemaFieldTypes, AggregateGroupByReducers, AggregateSteps } from 'redis';
+import { createClient, SCHEMA_FIELD_TYPE, FT_AGGREGATE_GROUP_BY_REDUCERS, FT_AGGREGATE_STEPS } from 'redis';
 
 const client = createClient();
 
@@ -14,19 +14,19 @@ await client.connect();
 try {
   await client.ft.create('idx:users', {
     '$.name': {
-      type: SchemaFieldTypes.TEXT,
+      type: SCHEMA_FIELD_TYPE.TEXT,
       SORTABLE: true
     },
     '$.age': {
-      type: SchemaFieldTypes.NUMERIC,
+      type: SCHEMA_FIELD_TYPE.NUMERIC,
       AS: 'age'
     },
     '$.coins': {
-      type: SchemaFieldTypes.NUMERIC,
+      type: SCHEMA_FIELD_TYPE.NUMERIC,
       AS: 'coins'
     },
     '$.email': {
-      type: SchemaFieldTypes.TAG,
+      type: SCHEMA_FIELD_TYPE.TAG,
       AS: 'email'
     }
   }, {
@@ -119,13 +119,13 @@ console.log(
   JSON.stringify(
     await client.ft.aggregate('idx:users', '*', {
       STEPS: [{
-        type: AggregateSteps.GROUPBY,
+        type: FT_AGGREGATE_STEPS.GROUPBY,
         REDUCE: [{
-          type: AggregateGroupByReducers.AVG,
+          type: FT_AGGREGATE_GROUP_BY_REDUCERS.AVG,
           property: 'age',
           AS: 'averageAge'
         }, {
-          type: AggregateGroupByReducers.SUM,
+          type: FT_AGGREGATE_GROUP_BY_REDUCERS.SUM,
           property: 'coins',
           AS: 'totalCoins'
         }]
@@ -145,4 +145,4 @@ console.log(
 //   ]
 // }
 
-await client.quit();
+client.destroy();
diff --git a/examples/search-knn.js b/examples/search-knn.js
index ea20f52e3fe..abfce990189 100644
--- a/examples/search-knn.js
+++ b/examples/search-knn.js
@@ -4,7 +4,7 @@
 // Inspired by RediSearch Python tests:
 // https://github.com/RediSearch/RediSearch/blob/06e36d48946ea08bd0d8b76394a4e82eeb919d78/tests/pytests/test_vecsim.py#L96
 
-import { createClient, SchemaFieldTypes, VectorAlgorithms } from 'redis';
+import { createClient, SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from 'redis';
 
 const client = createClient();
 
@@ -15,8 +15,8 @@ try {
   // Documentation: https://redis.io/docs/stack/search/reference/vectors/
   await client.ft.create('idx:knn-example', {
     v: {
-      type: SchemaFieldTypes.VECTOR,
-      ALGORITHM: VectorAlgorithms.HNSW,
+      type: SCHEMA_FIELD_TYPE.VECTOR,
+      ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW,
       TYPE: 'FLOAT32',
       DIM: 2,
       DISTANCE_METRIC: 'COSINE'
@@ -88,4 +88,4 @@ console.log(JSON.stringify(results, null, 2));
 //     }
 //   ]
 // }
-await client.quit();
+client.destroy();
diff --git a/examples/set-scan.js b/examples/set-scan.js
index 73f6c443444..698b05983b0 100644
--- a/examples/set-scan.js
+++ b/examples/set-scan.js
@@ -8,8 +8,14 @@ const client = createClient();
 await client.connect();
 
 const setName = 'setName';
-for await (const member of client.sScanIterator(setName)) {
-  console.log(member);
+
+for await (const members of client.sScanIterator(setName)) {
+  console.log('Batch of members:', members);
+
+  // Process each member in the batch if needed
+  for (const member of members) {
+    console.log('Individual member:', member);
+  }
 }
 
-await client.quit();
+client.close();
diff --git a/examples/sorted-set.js b/examples/sorted-set.js
index eb1f82867c1..830427bea9a 100644
--- a/examples/sorted-set.js
+++ b/examples/sorted-set.js
@@ -24,8 +24,30 @@ await client.zAdd('mysortedset', [
 // Get all of the values/scores from the sorted set using
 // the scan approach:
 // https://redis.io/commands/zscan
-for await (const memberWithScore of client.zScanIterator('mysortedset')) {
-  console.log(memberWithScore);
+for await (const membersWithScores of client.zScanIterator('mysortedset')) {
+  console.log('Batch of members with scores:', membersWithScores);
+
+  for (const memberWithScore of membersWithScores) {
+    console.log('Individual member with score:', memberWithScore);
+  }
 }
 
-await client.quit();
+await client.zAdd('anothersortedset', [
+  {
+    score: 99,
+    value: 'Ninety Nine'
+  },
+  {
+    score: 102,
+    value: 'One Hundred and Two'
+  }
+]);
+
+// Intersection of two sorted sets
+const intersection = await client.zInter([
+  { key: 'mysortedset', weight: 1 },
+  { key: 'anothersortedset', weight: 1 }
+]);
+console.log('Intersection:', intersection);
+
+client.close();
diff --git a/examples/stream-consumer-group.js b/examples/stream-consumer-group.js
index 0161b5b4d32..cf82b5e96af 100644
--- a/examples/stream-consumer-group.js
+++ b/examples/stream-consumer-group.js
@@ -20,7 +20,7 @@
 //
 // $ node stream-consumer-group.js consumer2
 
-import { createClient, commandOptions } from 'redis';
+import { createClient } from 'redis';
 
 const client = createClient();
 
@@ -46,14 +46,13 @@ try {
 
 console.log(`Starting consumer ${consumerName}.`);
 
+const pool = client.createPool();
+
 while (true) {
   try {
     // https://redis.io/commands/xreadgroup/
-    let response = await client.xReadGroup(
-      commandOptions({
-        isolated: true
-      }),
-      'myconsumergroup', 
+    let response = await pool.xReadGroup(
+      'myconsumergroup',
       consumerName, [
         // XREADGROUP can read from multiple streams, starting at a
         // different ID for each...
@@ -91,9 +90,10 @@ while (true) {
       // stream entry.
       // https://redis.io/commands/xack/
       const entryId = response[0].messages[0].id;
-      await client.xAck('mystream', 'myconsumergroup', entryId);
+      const ackResult = await pool.xAck('mystream', 'myconsumergroup', entryId);
 
-      console.log(`Acknowledged processing of entry ${entryId}.`);
+      // ackResult will be 1 if the message was successfully acknowledged, 0 otherwise
+      console.log(`Acknowledged processing of entry ${entryId}. Result: ${ackResult}`);
     } else {
       // Response is null, we have read everything that is
       // in the stream right now...
diff --git a/examples/stream-producer.js b/examples/stream-producer.js
index f81931e5197..113265dbd40 100644
--- a/examples/stream-producer.js
+++ b/examples/stream-producer.js
@@ -47,4 +47,4 @@ console.log(`Length of mystream: ${await client.xLen('mystream')}.`);
 // Should be approximately 1000:
 console.log(`Length of mytrimmedstream: ${await client.xLen('mytrimmedstream')}.`);
 
-await client.quit();
+client.destroy();
diff --git a/examples/time-series.js b/examples/time-series.js
index 2f2ac598032..75df2736f81 100644
--- a/examples/time-series.js
+++ b/examples/time-series.js
@@ -2,7 +2,7 @@
 // Requires the RedisTimeSeries module: https://redis.io/docs/stack/timeseries/
 
 import { createClient } from 'redis';
-import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding, TimeSeriesAggregationType } from '@redis/time-series';
+import { TIME_SERIES_DUPLICATE_POLICIES, TIME_SERIES_ENCODING, TIME_SERIES_AGGREGATION_TYPE } from '@redis/time-series';
 
 const client = createClient();
 
@@ -14,8 +14,8 @@ try {
   // https://redis.io/commands/ts.create/
   const created = await client.ts.create('mytimeseries', {
     RETENTION: 86400000, // 1 day in milliseconds
-    ENCODING: TimeSeriesEncoding.UNCOMPRESSED, // No compression
-    DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK // No duplicates
+    ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED, // No compression
+    DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK // No duplicates
   });
 
   if (created === 'OK') {
@@ -74,7 +74,7 @@ try {
   const rangeResponse = await client.ts.range('mytimeseries', fromTimestamp, toTimestamp, {
     // Group into 10 second averages.
     AGGREGATION: {
-      type: TimeSeriesAggregationType.AVERAGE,
+      type: TIME_SERIES_AGGREGATION_TYPE.AVG,
       timeBucket: 10000
     }
   });
@@ -119,4 +119,4 @@ try {
   console.error(e);
 }
 
-await client.quit();
+client.close();
diff --git a/examples/topk.js b/examples/topk.js
index 35cdc4a8500..10cc29950ed 100644
--- a/examples/topk.js
+++ b/examples/topk.js
@@ -1,4 +1,4 @@
-// This example demonstrates the use of the Top K 
+// This example demonstrates the use of the Top K
 // in the RedisBloom module (https://redis.io/docs/stack/bloom/)
 
 import { createClient } from 'redis';
@@ -95,10 +95,10 @@ const [ steve, suze, leibale, frederick ] = await client.topK.query('mytopk', [
   'frederick'
 ]);
 
-console.log(`steve ${steve === 1 ? 'is': 'is not'} in the top 10.`);
-console.log(`suze ${suze === 1 ? 'is': 'is not'} in the top 10.`);
-console.log(`leibale ${leibale === 1 ? 'is': 'is not'} in the top 10.`);
-console.log(`frederick ${frederick === 1 ? 'is': 'is not'} in the top 10.`);
+console.log(`steve ${steve ? 'is': 'is not'} in the top 10.`);
+console.log(`suze ${suze ? 'is': 'is not'} in the top 10.`);
+console.log(`leibale ${leibale ? 'is': 'is not'} in the top 10.`);
+console.log(`frederick ${frederick ? 'is': 'is not'} in the top 10.`);
 
 // Get count estimate for some team members with TOPK.COUNT:
 // https://redis.io/commands/topk.count/
@@ -110,4 +110,4 @@ const [ simonCount, lanceCount ] = await client.topK.count('mytopk', [
 console.log(`Count estimate for simon: ${simonCount}.`);
 console.log(`Count estimate for lance: ${lanceCount}.`);
 
-await client.quit();
+client.close();
diff --git a/examples/transaction-with-arbitrary-commands.js b/examples/transaction-with-arbitrary-commands.js
index 274a362d57e..cc22a659678 100644
--- a/examples/transaction-with-arbitrary-commands.js
+++ b/examples/transaction-with-arbitrary-commands.js
@@ -1,6 +1,6 @@
-// How to mix and match supported commands that have named functions with 
+// How to mix and match supported commands that have named functions with
 // commands sent as arbitrary strings in the same transaction context.
-// Use this when working with new Redis commands that haven't been added to 
+// Use this when working with new Redis commands that haven't been added to
 // node-redis yet, or when working with commands that have been added to Redis
 // by modules other than those directly supported by node-redis.
 
@@ -23,18 +23,29 @@ await client.sendCommand(['hset', 'hash2', 'number', '3']);
 // In a transaction context, use addCommand to send arbitrary commands.
 // addCommand can be mixed and matched with named command functions as
 // shown.
-const responses = await client
-  .multi()
+const multi = client.multi()
   .hGetAll('hash2')
   .addCommand(['hset', 'hash3', 'number', '4'])
-  .hGet('hash3', 'number')
-  .exec();
+  .hGet('hash3', 'number');
+
+// exec() returns Array<ReplyUnion>
+const responses = await multi.exec();
 
 // responses will be:
 // [ [Object: null prototype] { name: 'hash2', number: '3' }, 0, '4' ]
-console.log(responses);
+console.log('Using exec():', responses);
+
+// This is equivalent to multi.exec<'typed'>()
+const typedResponses = await multi
+  .hGetAll('hash2')
+  .addCommand(['hset', 'hash3', 'number', '4'])
+  .hGet('hash3', 'number')
+  .execTyped();
+
+// typedResponses will have more specific types
+console.log('Using execTyped():', typedResponses);
 
 // Clean up fixtures.
 await client.del(['hash1', 'hash2', 'hash3']);
 
-await client.quit();
+client.close();
diff --git a/examples/transaction-with-watch.js b/examples/transaction-with-watch.js
index d92b910dfa3..752d0b6a4e3 100644
--- a/examples/transaction-with-watch.js
+++ b/examples/transaction-with-watch.js
@@ -13,9 +13,11 @@ function restrictFunctionCalls(fn, maxCalls) {
 
 const fn = restrictFunctionCalls(transaction, 4);
 
+const pool = await client.createPool();
+
 async function transaction() {
   try {
-    await client.executeIsolated(async (isolatedClient) => {
+    await pool.execute(async (isolatedClient) => {
       await isolatedClient.watch('paymentId:1259');
       const multi = isolatedClient
         .multi()
diff --git a/index.ts b/index.ts
deleted file mode 100644
index 5b5a6e81294..00000000000
--- a/index.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
-    RedisModules,
-    RedisFunctions,
-    RedisScripts,
-    createClient as _createClient,
-    RedisClientOptions,
-    RedisClientType as _RedisClientType,
-    createCluster as _createCluster,
-    RedisClusterOptions,
-    RedisClusterType as _RedisClusterType
-} from '@redis/client';
-import RedisBloomModules from '@redis/bloom';
-import RedisGraph from '@redis/graph';
-import RedisJSON from '@redis/json';
-import RediSearch from '@redis/search';
-import RedisTimeSeries from '@redis/time-series';
-
-export * from '@redis/client';
-export * from '@redis/bloom';
-export * from '@redis/graph';
-export * from '@redis/json';
-export * from '@redis/search';
-export * from '@redis/time-series';
-
-const modules = {
-    ...RedisBloomModules,
-    graph: RedisGraph,
-    json: RedisJSON,
-    ft: RediSearch,
-    ts: RedisTimeSeries
-};
-
-export type RedisDefaultModules = typeof modules;
-
-export type RedisClientType<
-    M extends RedisModules = RedisDefaultModules,
-    F extends RedisFunctions = Record<string, never>,
-    S extends RedisScripts = Record<string, never>
-> = _RedisClientType<M, F, S>;
-
-export function createClient<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
->(
-    options?: RedisClientOptions<M, F, S>
-): _RedisClientType<RedisDefaultModules & M, F, S> {
-    return _createClient({
-        ...options,
-        modules: {
-            ...modules,
-            ...(options?.modules as M)
-        }
-    });
-}
-
-export type RedisClusterType<
-    M extends RedisModules = RedisDefaultModules,
-    F extends RedisFunctions = Record<string, never>,
-    S extends RedisScripts = Record<string, never>
-> = _RedisClusterType<M, F, S>;
-
-export function createCluster<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
->(
-    options: RedisClusterOptions<M, F, S>
-): RedisClusterType<RedisDefaultModules & M, F, S> {
-    return _createCluster({
-        ...options,
-        modules: {
-            ...modules,
-            ...(options?.modules as M)
-        }
-    });
-}
diff --git a/package-lock.json b/package-lock.json
index 18a7003947e..5ecc96aff06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,45 +1,39 @@
 {
-  "name": "redis",
-  "version": "4.7.0",
+  "name": "redis-monorepo",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
-      "name": "redis",
-      "version": "4.7.0",
-      "license": "MIT",
+      "name": "redis-monorepo",
       "workspaces": [
-        "./packages/*"
+        "./packages/client",
+        "./packages/test-utils",
+        "./packages/bloom",
+        "./packages/json",
+        "./packages/search",
+        "./packages/time-series",
+        "./packages/entraid",
+        "./packages/redis"
       ],
-      "dependencies": {
-        "@redis/bloom": "1.2.0",
-        "@redis/client": "1.6.0",
-        "@redis/graph": "1.1.1",
-        "@redis/json": "1.0.7",
-        "@redis/search": "1.2.0",
-        "@redis/time-series": "1.1.0"
-      },
       "devDependencies": {
-        "@tsconfig/node14": "^14.1.0",
-        "gh-pages": "^6.0.0",
-        "release-it": "^16.1.5",
-        "typescript": "^5.2.2"
-      }
-    },
-    "node_modules/@aashutoshrathi/word-wrap": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
-      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
+        "@istanbuljs/nyc-config-typescript": "^1.0.2",
+        "@release-it/bumper": "^7.0.5",
+        "@types/mocha": "^10.0.6",
+        "@types/node": "^20.11.16",
+        "gh-pages": "^6.1.1",
+        "mocha": "^10.2.0",
+        "nyc": "^15.1.0",
+        "release-it": "^19.0.2",
+        "ts-node": "^10.9.2",
+        "tsx": "^4.7.0",
+        "typedoc": "^0.25.7",
+        "typescript": "^5.3.3"
       }
     },
     "node_modules/@ampproject/remapping": {
       "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
-      "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.0",
         "@jridgewell/trace-mapping": "^0.3.9"
@@ -48,13 +42,225 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/@azure/abort-controller": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+      "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/core-auth": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
+      "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.0.0",
+        "@azure/core-util": "^1.11.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/core-client": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz",
+      "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.0.0",
+        "@azure/core-auth": "^1.4.0",
+        "@azure/core-rest-pipeline": "^1.9.1",
+        "@azure/core-tracing": "^1.0.0",
+        "@azure/core-util": "^1.6.1",
+        "@azure/logger": "^1.0.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/core-rest-pipeline": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.0.tgz",
+      "integrity": "sha512-bM3308LRyg5g7r3Twprtqww0R/r7+GyVxj4BafcmVPo4WQoGt5JXuaqxHEFjw2o3rvFZcUPiqJMg6WuvEEeVUA==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.0.0",
+        "@azure/core-auth": "^1.8.0",
+        "@azure/core-tracing": "^1.0.1",
+        "@azure/core-util": "^1.11.0",
+        "@azure/logger": "^1.0.0",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/core-tracing": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz",
+      "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/core-util": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
+      "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.0.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/identity": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.7.0.tgz",
+      "integrity": "sha512-6z/S2KorkbKaZ0DgZFVRdu7RCuATmMSTjKpuhj7YpjxkJ0vnJ7kTM3cpNgzFgk9OPYfZ31wrBEtC/iwAS4jQDA==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.0.0",
+        "@azure/core-auth": "^1.9.0",
+        "@azure/core-client": "^1.9.2",
+        "@azure/core-rest-pipeline": "^1.17.0",
+        "@azure/core-tracing": "^1.0.0",
+        "@azure/core-util": "^1.11.0",
+        "@azure/logger": "^1.0.0",
+        "@azure/msal-browser": "^4.2.0",
+        "@azure/msal-node": "^3.2.1",
+        "events": "^3.0.0",
+        "jws": "^4.0.0",
+        "open": "^10.1.0",
+        "stoppable": "^1.1.0",
+        "tslib": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/identity/node_modules/@azure/msal-common": {
+      "version": "15.2.0",
+      "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
+      "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/identity/node_modules/@azure/msal-node": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.2.3.tgz",
+      "integrity": "sha512-0eaPqBIWEAizeYiXdeHb09Iq0tvHJ17ztvNEaLdr/KcJJhJxbpkkEQf09DB+vKlFE0tzYi7j4rYLTXtES/InEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-common": "15.2.0",
+        "jsonwebtoken": "^9.0.0",
+        "uuid": "^8.3.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@azure/identity/node_modules/jwa": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
+      "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/@azure/identity/node_modules/jws": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+      "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/@azure/logger": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz",
+      "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/msal-browser": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.4.0.tgz",
+      "integrity": "sha512-rU6juYXk67CKQmpgi6fDgZoPQ9InZ1760z1BSAH7RbeIc4lHZM/Tu+H0CyRk7cnrfvTkexyYE4pjYhMghpzheA==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-common": "15.2.0"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": {
+      "version": "15.2.0",
+      "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
+      "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/msal-common": {
+      "version": "14.16.0",
+      "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
+      "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/msal-node": {
+      "version": "2.16.2",
+      "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
+      "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-common": "14.16.0",
+        "jsonwebtoken": "^9.0.0",
+        "uuid": "^8.3.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
     "node_modules/@babel/code-frame": {
-      "version": "7.22.13",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
-      "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+      "version": "7.23.5",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/highlight": "^7.22.13",
+        "@babel/highlight": "^7.23.4",
         "chalk": "^2.4.2"
       },
       "engines": {
@@ -63,9 +269,8 @@
     },
     "node_modules/@babel/code-frame/node_modules/ansi-styles": {
       "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -75,9 +280,8 @@
     },
     "node_modules/@babel/code-frame/node_modules/chalk": {
       "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -89,54 +293,68 @@
     },
     "node_modules/@babel/code-frame/node_modules/color-convert": {
       "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "color-name": "1.1.3"
       }
     },
     "node_modules/@babel/code-frame/node_modules/color-name": {
       "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
       "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.8.0"
       }
     },
+    "node_modules/@babel/code-frame/node_modules/has-flag": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@babel/code-frame/node_modules/supports-color": {
+      "version": "5.5.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/@babel/compat-data": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz",
-      "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==",
+      "version": "7.23.5",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz",
-      "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==",
+      "version": "7.23.9",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.22.13",
-        "@babel/generator": "^7.22.15",
-        "@babel/helper-compilation-targets": "^7.22.15",
-        "@babel/helper-module-transforms": "^7.22.20",
-        "@babel/helpers": "^7.22.15",
-        "@babel/parser": "^7.22.16",
-        "@babel/template": "^7.22.15",
-        "@babel/traverse": "^7.22.20",
-        "@babel/types": "^7.22.19",
-        "convert-source-map": "^1.7.0",
+        "@babel/code-frame": "^7.23.5",
+        "@babel/generator": "^7.23.6",
+        "@babel/helper-compilation-targets": "^7.23.6",
+        "@babel/helper-module-transforms": "^7.23.3",
+        "@babel/helpers": "^7.23.9",
+        "@babel/parser": "^7.23.9",
+        "@babel/template": "^7.23.9",
+        "@babel/traverse": "^7.23.9",
+        "@babel/types": "^7.23.9",
+        "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
         "json5": "^2.2.3",
@@ -150,13 +368,17 @@
         "url": "https://opencollective.com/babel"
       }
     },
+    "node_modules/@babel/core/node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@babel/generator": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz",
-      "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==",
+      "version": "7.23.6",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.22.15",
+        "@babel/types": "^7.23.6",
         "@jridgewell/gen-mapping": "^0.3.2",
         "@jridgewell/trace-mapping": "^0.3.17",
         "jsesc": "^2.5.1"
@@ -166,14 +388,13 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
-      "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+      "version": "7.23.6",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/compat-data": "^7.22.9",
-        "@babel/helper-validator-option": "^7.22.15",
-        "browserslist": "^4.21.9",
+        "@babel/compat-data": "^7.23.5",
+        "@babel/helper-validator-option": "^7.23.5",
+        "browserslist": "^4.22.2",
         "lru-cache": "^5.1.1",
         "semver": "^6.3.1"
       },
@@ -181,38 +402,21 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-      "dev": true
-    },
     "node_modules/@babel/helper-environment-visitor": {
       "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
-      "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-function-name": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
-      "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+      "version": "7.23.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.22.5",
-        "@babel/types": "^7.22.5"
+        "@babel/template": "^7.22.15",
+        "@babel/types": "^7.23.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -220,9 +424,8 @@
     },
     "node_modules/@babel/helper-hoist-variables": {
       "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
-      "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/types": "^7.22.5"
       },
@@ -232,9 +435,8 @@
     },
     "node_modules/@babel/helper-module-imports": {
       "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
-      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/types": "^7.22.15"
       },
@@ -243,10 +445,9 @@
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz",
-      "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==",
+      "version": "7.23.3",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/helper-environment-visitor": "^7.22.20",
         "@babel/helper-module-imports": "^7.22.15",
@@ -263,9 +464,8 @@
     },
     "node_modules/@babel/helper-simple-access": {
       "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
-      "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/types": "^7.22.5"
       },
@@ -275,9 +475,8 @@
     },
     "node_modules/@babel/helper-split-export-declaration": {
       "version": "7.22.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
-      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/types": "^7.22.5"
       },
@@ -286,51 +485,46 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
-      "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+      "version": "7.23.4",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
       "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-option": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
-      "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+      "version": "7.23.5",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz",
-      "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==",
+      "version": "7.23.9",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.22.15",
-        "@babel/traverse": "^7.22.15",
-        "@babel/types": "^7.22.15"
+        "@babel/template": "^7.23.9",
+        "@babel/traverse": "^7.23.9",
+        "@babel/types": "^7.23.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
-      "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+      "version": "7.23.4",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
@@ -342,9 +536,8 @@
     },
     "node_modules/@babel/highlight/node_modules/ansi-styles": {
       "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -354,9 +547,8 @@
     },
     "node_modules/@babel/highlight/node_modules/chalk": {
       "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -368,33 +560,48 @@
     },
     "node_modules/@babel/highlight/node_modules/color-convert": {
       "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "color-name": "1.1.3"
       }
     },
     "node_modules/@babel/highlight/node_modules/color-name": {
       "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
       "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.8.0"
       }
     },
+    "node_modules/@babel/highlight/node_modules/has-flag": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@babel/highlight/node_modules/supports-color": {
+      "version": "5.5.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/@babel/parser": {
-      "version": "7.22.16",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
-      "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
+      "version": "7.23.9",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -403,57 +610,45 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
-      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+      "version": "7.23.9",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.22.13",
-        "@babel/parser": "^7.22.15",
-        "@babel/types": "^7.22.15"
+        "@babel/code-frame": "^7.23.5",
+        "@babel/parser": "^7.23.9",
+        "@babel/types": "^7.23.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz",
-      "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==",
+      "version": "7.23.9",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.22.13",
-        "@babel/generator": "^7.22.15",
+        "@babel/code-frame": "^7.23.5",
+        "@babel/generator": "^7.23.6",
         "@babel/helper-environment-visitor": "^7.22.20",
-        "@babel/helper-function-name": "^7.22.5",
+        "@babel/helper-function-name": "^7.23.0",
         "@babel/helper-hoist-variables": "^7.22.5",
         "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.22.16",
-        "@babel/types": "^7.22.19",
-        "debug": "^4.1.0",
+        "@babel/parser": "^7.23.9",
+        "@babel/types": "^7.23.9",
+        "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/traverse/node_modules/globals": {
-      "version": "11.12.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/@babel/types": {
-      "version": "7.22.19",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz",
-      "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==",
+      "version": "7.23.9",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.22.5",
-        "@babel/helper-validator-identifier": "^7.22.19",
+        "@babel/helper-string-parser": "^7.23.4",
+        "@babel/helper-validator-identifier": "^7.22.20",
         "to-fast-properties": "^2.0.0"
       },
       "engines": {
@@ -465,6 +660,7 @@
       "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
       "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/trace-mapping": "0.3.9"
       },
@@ -477,123 +673,369 @@
       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
       "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.0.3",
         "@jridgewell/sourcemap-codec": "^1.4.10"
       }
     },
-    "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.19.12",
+      "cpu": [
+        "x64"
+      ],
       "dev": true,
-      "dependencies": {
-        "eslint-visitor-keys": "^3.3.0"
-      },
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "peerDependencies": {
-        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+        "node": ">=12"
       }
     },
-    "node_modules/@eslint-community/regexpp": {
-      "version": "4.8.1",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz",
-      "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==",
+    "node_modules/@iarna/toml": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz",
+      "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==",
       "dev": true,
-      "engines": {
-        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
-      }
+      "license": "ISC"
     },
-    "node_modules/@eslint/eslintrc": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
-      "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
+    "node_modules/@inquirer/checkbox": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.7.tgz",
+      "integrity": "sha512-VEr2vnI4TSM2Q50fAck98mzWJGAoxbF0rb48tcSEjkJ2kn3mM6c/YsJwnyu45PlXd6aNWObMGWmQVleL2BJy6w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^9.6.0",
-        "globals": "^13.19.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
-        "strip-json-comments": "^3.1.1"
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/figures": "^1.0.12",
+        "@inquirer/type": "^3.0.7",
+        "ansi-escapes": "^4.3.2",
+        "yoctocolors-cjs": "^2.1.2"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": ">=18"
       },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
+      "peerDependencies": {
+        "@types/node": ">=18"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
       }
     },
-    "node_modules/@eslint/js": {
-      "version": "8.49.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
-      "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
+    "node_modules/@inquirer/confirm": {
+      "version": "5.1.11",
+      "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.11.tgz",
+      "integrity": "sha512-HgVha2B1lurfZ8u7cBWmu60HpkpnnIT/1IrreBx5g2oxQOVYU15WQDl6oZqjuXVbzteFKSpmMkLTMf2OmbUjaw==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7"
+      },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
       }
     },
-    "node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.11",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
-      "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
+    "node_modules/@inquirer/core": {
+      "version": "10.1.12",
+      "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.12.tgz",
+      "integrity": "sha512-uoaDadeJCYSVKYCMPwJi3AjCF9w+l9aWbHYA4iskKX84cVW/A2M6bJlWBoy3k81GpFp6EX3IElV1Z5xKw0g1QQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@humanwhocodes/object-schema": "^1.2.1",
-        "debug": "^4.1.1",
-        "minimatch": "^3.0.5"
+        "@inquirer/figures": "^1.0.12",
+        "@inquirer/type": "^3.0.7",
+        "ansi-escapes": "^4.3.2",
+        "cli-width": "^4.1.0",
+        "mute-stream": "^2.0.0",
+        "signal-exit": "^4.1.0",
+        "wrap-ansi": "^6.2.0",
+        "yoctocolors-cjs": "^2.1.2"
       },
       "engines": {
-        "node": ">=10.10.0"
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
       }
     },
-    "node_modules/@humanwhocodes/module-importer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+    "node_modules/@inquirer/core/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=12.22"
+        "node": ">=14"
       },
       "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/nzakas"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/@humanwhocodes/object-schema": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
-      "dev": true
+    "node_modules/@inquirer/editor": {
+      "version": "4.2.12",
+      "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.12.tgz",
+      "integrity": "sha512-YNOCY79iqI/ksWohdudGtnO02N/a2j82b6akK/+hy1/C6xoU07dsKFUBfQ36nLCxE98ICS74Uyandq7nBS31Mw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7",
+        "external-editor": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
     },
-    "node_modules/@iarna/toml": {
-      "version": "2.2.5",
-      "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
-      "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
-      "dev": true
+    "node_modules/@inquirer/expand": {
+      "version": "4.0.14",
+      "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.14.tgz",
+      "integrity": "sha512-aon4yACMp4Qwc/2f6xafcC6jzAJ5vXBwL5+z4bS2y4YIOGF+QOe+Jzd5hLz1hOo+bhzVS7q07dNXTeBjaFAqRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7",
+        "yoctocolors-cjs": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/figures": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz",
+      "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@inquirer/input": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.11.tgz",
+      "integrity": "sha512-gzcBWLWMiBaY507HFg4B1NJ18InnHhLjj4DTLfyoz9Rv7dSPpJ9JSj7Of8ea5QE2D+ms3ESTl/4MdzrC1//B0Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/number": {
+      "version": "3.0.14",
+      "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.14.tgz",
+      "integrity": "sha512-8B4jX8ArK9zvb8/tB04jGLja4XoFfjvrTLJ5YeLlFnJh3jPa9VTQt2kxJZubGKc8YHX68e1XQxv4Nu/WZUnXIw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/password": {
+      "version": "4.0.14",
+      "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.14.tgz",
+      "integrity": "sha512-/N/5PeI+QWE23dTn2D4erD9Y3yYeh0bUDkO9tt2d11mAVuCswiOKzoHrV9KYGQhoD6ae+Nff1G8TPqbfUUh8Ag==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7",
+        "ansi-escapes": "^4.3.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/prompts": {
+      "version": "7.5.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.2.tgz",
+      "integrity": "sha512-+jsUm6G9X5PUD97HkcGojzwyPsz5oSB2FUbj+D+NOYFQUj0XqvhDcDfk9mhMxFG/RDIgT9Kq4x0rm5pC5zVHUg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/checkbox": "^4.1.7",
+        "@inquirer/confirm": "^5.1.11",
+        "@inquirer/editor": "^4.2.12",
+        "@inquirer/expand": "^4.0.14",
+        "@inquirer/input": "^4.1.11",
+        "@inquirer/number": "^3.0.14",
+        "@inquirer/password": "^4.0.14",
+        "@inquirer/rawlist": "^4.1.2",
+        "@inquirer/search": "^3.0.14",
+        "@inquirer/select": "^4.2.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/rawlist": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.2.tgz",
+      "integrity": "sha512-VDuhV58w3FuKNl24GR9ygdbu3NkGfuaK7D2gyMWeY79Lr4GVbj7ySxw1isAnelSzU1ecZC/TwICa5rCy0za2OA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/type": "^3.0.7",
+        "yoctocolors-cjs": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/search": {
+      "version": "3.0.14",
+      "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.14.tgz",
+      "integrity": "sha512-+VdtRD5nVR50K5fEMq/qbtHGH08vfqm69NJtojavlMXj6fsYymQZrNqjxEISPs2PDvtsemTJVFGs0uI6Zti6Dw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/figures": "^1.0.12",
+        "@inquirer/type": "^3.0.7",
+        "yoctocolors-cjs": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/select": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.2.tgz",
+      "integrity": "sha512-3X8AAPE1WPUwY3IawT19BapD0kKpAUP7SVUu5mxmRjnl/f4q0MQz8CU8ToCC6Im0SzyOTWmSauE3GBgyOv1rBw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.12",
+        "@inquirer/figures": "^1.0.12",
+        "@inquirer/type": "^3.0.7",
+        "ansi-escapes": "^4.3.2",
+        "yoctocolors-cjs": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/type": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz",
+      "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
     },
     "node_modules/@istanbuljs/load-nyc-config": {
       "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
-      "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "camelcase": "^5.3.1",
         "find-up": "^4.1.0",
@@ -607,27 +1049,28 @@
     },
     "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
       "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "sprintf-js": "~1.0.2"
       }
     },
-    "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+    "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
+      "version": "4.1.0",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
       "engines": {
-        "node": ">=6"
+        "node": ">=8"
       }
     },
     "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
       "version": "3.14.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
-      "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "argparse": "^1.0.7",
         "esprima": "^4.0.0"
@@ -636,20 +1079,46 @@
         "js-yaml": "bin/js-yaml.js"
       }
     },
-    "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
+    "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
       "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
+      "version": "2.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/@istanbuljs/nyc-config-typescript": {
       "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz",
-      "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "@istanbuljs/schema": "^0.1.2"
       },
@@ -662,18 +1131,16 @@
     },
     "node_modules/@istanbuljs/schema": {
       "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
-      "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
-      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/set-array": "^1.0.1",
         "@jridgewell/sourcemap-codec": "^1.4.10",
@@ -685,52 +1152,40 @@
     },
     "node_modules/@jridgewell/resolve-uri": {
       "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/set-array": {
       "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.4.15",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.19",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
-      "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+      "version": "0.3.22",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
-    "node_modules/@ljharb/through": {
-      "version": "2.3.9",
-      "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.9.tgz",
-      "integrity": "sha512-yN599ZBuMPPK4tdoToLlvgJB4CLK8fGl7ntfy0Wn7U6ttNvHYurd81bfUiK/6sMkiIwm65R6ck4L6+Y3DfVbNQ==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
         "run-parallel": "^1.1.9"
@@ -744,6 +1199,7 @@
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
       "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 8"
       }
@@ -753,6 +1209,7 @@
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
       "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
         "fastq": "^1.6.0"
@@ -761,236 +1218,228 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@nodeutils/defaults-deep": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@nodeutils/defaults-deep/-/defaults-deep-1.1.0.tgz",
+      "integrity": "sha512-gG44cwQovaOFdSR02jR9IhVRpnDP64VN6JdjYJTfNz4J4fWn7TQnmrf22nSjRqlwlxPcW8PL/L3KbJg3tdwvpg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lodash": "^4.15.0"
+      }
+    },
     "node_modules/@octokit/auth-token": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz",
-      "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
+      "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/core": {
-      "version": "4.2.4",
-      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz",
-      "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==",
+      "version": "6.1.5",
+      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
+      "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/auth-token": "^3.0.0",
-        "@octokit/graphql": "^5.0.0",
-        "@octokit/request": "^6.0.0",
-        "@octokit/request-error": "^3.0.0",
-        "@octokit/types": "^9.0.0",
-        "before-after-hook": "^2.2.0",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/auth-token": "^5.0.0",
+        "@octokit/graphql": "^8.2.2",
+        "@octokit/request": "^9.2.3",
+        "@octokit/request-error": "^6.1.8",
+        "@octokit/types": "^14.0.0",
+        "before-after-hook": "^3.0.2",
+        "universal-user-agent": "^7.0.0"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/endpoint": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz",
-      "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==",
+      "version": "10.1.4",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
+      "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/types": "^9.0.0",
-        "is-plain-object": "^5.0.0",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/types": "^14.0.0",
+        "universal-user-agent": "^7.0.2"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/graphql": {
-      "version": "5.0.6",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz",
-      "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==",
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
+      "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/request": "^6.0.0",
-        "@octokit/types": "^9.0.0",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/request": "^9.2.3",
+        "@octokit/types": "^14.0.0",
+        "universal-user-agent": "^7.0.0"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/openapi-types": {
-      "version": "18.0.0",
-      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz",
-      "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==",
-      "dev": true
+      "version": "25.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz",
+      "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@octokit/plugin-paginate-rest": {
-      "version": "6.1.2",
-      "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz",
-      "integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==",
+      "version": "11.6.0",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
+      "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/tsconfig": "^1.0.2",
-        "@octokit/types": "^9.2.3"
+        "@octokit/types": "^13.10.0"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@octokit/core": ">=4"
+        "@octokit/core": ">=6"
+      }
+    },
+    "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
+      "version": "24.2.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
+      "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
+      "version": "13.10.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
+      "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/openapi-types": "^24.2.0"
       }
     },
     "node_modules/@octokit/plugin-request-log": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
-      "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz",
+      "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==",
       "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 18"
+      },
       "peerDependencies": {
-        "@octokit/core": ">=3"
+        "@octokit/core": ">=6"
       }
     },
     "node_modules/@octokit/plugin-rest-endpoint-methods": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz",
-      "integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==",
+      "version": "13.5.0",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz",
+      "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/types": "^10.0.0"
+        "@octokit/types": "^13.10.0"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@octokit/core": ">=3"
+        "@octokit/core": ">=6"
       }
     },
+    "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
+      "version": "24.2.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
+      "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
-      "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
+      "version": "13.10.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
+      "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/openapi-types": "^18.0.0"
+        "@octokit/openapi-types": "^24.2.0"
       }
     },
     "node_modules/@octokit/request": {
-      "version": "6.2.8",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz",
-      "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==",
+      "version": "9.2.3",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
+      "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/endpoint": "^7.0.0",
-        "@octokit/request-error": "^3.0.0",
-        "@octokit/types": "^9.0.0",
-        "is-plain-object": "^5.0.0",
-        "node-fetch": "^2.6.7",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/endpoint": "^10.1.4",
+        "@octokit/request-error": "^6.1.8",
+        "@octokit/types": "^14.0.0",
+        "fast-content-type-parse": "^2.0.0",
+        "universal-user-agent": "^7.0.2"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/request-error": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
-      "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
+      "version": "6.1.8",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
+      "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/types": "^9.0.0",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
+        "@octokit/types": "^14.0.0"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       }
     },
-    "node_modules/@octokit/request/node_modules/node-fetch": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
-      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+    "node_modules/@octokit/rest": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
+      "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "whatwg-url": "^5.0.0"
+        "@octokit/core": "^6.1.4",
+        "@octokit/plugin-paginate-rest": "^11.4.2",
+        "@octokit/plugin-request-log": "^5.3.1",
+        "@octokit/plugin-rest-endpoint-methods": "^13.3.0"
       },
       "engines": {
-        "node": "4.x || >=6.0.0"
-      },
-      "peerDependencies": {
-        "encoding": "^0.1.0"
-      },
-      "peerDependenciesMeta": {
-        "encoding": {
-          "optional": true
-        }
+        "node": ">= 18"
       }
     },
-    "node_modules/@octokit/rest": {
-      "version": "19.0.13",
-      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz",
-      "integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/core": "^4.2.1",
-        "@octokit/plugin-paginate-rest": "^6.1.2",
-        "@octokit/plugin-request-log": "^1.0.4",
-        "@octokit/plugin-rest-endpoint-methods": "^7.1.2"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@octokit/tsconfig": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz",
-      "integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==",
-      "dev": true
-    },
-    "node_modules/@octokit/types": {
-      "version": "9.3.2",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz",
-      "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==",
+    "node_modules/@octokit/types": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz",
+      "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@octokit/openapi-types": "^18.0.0"
-      }
-    },
-    "node_modules/@pnpm/config.env-replace": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
-      "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.22.0"
+        "@octokit/openapi-types": "^25.0.0"
       }
     },
-    "node_modules/@pnpm/network.ca-file": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz",
-      "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==",
+    "node_modules/@phun-ky/typeof": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-1.2.8.tgz",
+      "integrity": "sha512-7J6ca1tK0duM2BgVB+CuFMh3idlIVASOP2QvOCbNWDc6JnvjtKa9nufPoJQQ4xrwBonwgT1TIhRRcEtzdVgWsA==",
       "dev": true,
-      "dependencies": {
-        "graceful-fs": "4.2.10"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=12.22.0"
-      }
-    },
-    "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": {
-      "version": "4.2.10",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
-      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
-      "dev": true
-    },
-    "node_modules/@pnpm/npm-conf": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz",
-      "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==",
-      "dev": true,
-      "dependencies": {
-        "@pnpm/config.env-replace": "^1.1.0",
-        "@pnpm/network.ca-file": "^1.0.1",
-        "config-chain": "^1.1.11"
+        "node": "^20.9.0 || >=22.0.0",
+        "npm": ">=10.8.2"
       },
-      "engines": {
-        "node": ">=12"
+      "funding": {
+        "url": "https://github.com/phun-ky/typeof?sponsor=1"
       }
     },
     "node_modules/@redis/bloom": {
@@ -1001,8 +1450,8 @@
       "resolved": "packages/client",
       "link": true
     },
-    "node_modules/@redis/graph": {
-      "resolved": "packages/graph",
+    "node_modules/@redis/entraid": {
+      "resolved": "packages/entraid",
       "link": true
     },
     "node_modules/@redis/json": {
@@ -1021,41 +1470,62 @@
       "resolved": "packages/time-series",
       "link": true
     },
-    "node_modules/@sindresorhus/is": {
-      "version": "5.6.0",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
-      "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
+    "node_modules/@release-it/bumper": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-7.0.5.tgz",
+      "integrity": "sha512-HCFMqDHreLYg4jjTWL//pW1GzZZMn3p7HDbwS2y7y5m0L6p8hEaOEixC3tEzwyVV7VP1VGjqxMvxfa360q8+Tg==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@iarna/toml": "^3.0.0",
+        "cheerio": "^1.0.0",
+        "detect-indent": "7.0.1",
+        "fast-glob": "^3.3.3",
+        "ini": "^5.0.0",
+        "js-yaml": "^4.1.0",
+        "lodash-es": "^4.17.21",
+        "semver": "^7.7.1"
+      },
       "engines": {
-        "node": ">=14.16"
+        "node": "^20.9.0 || >=22.0.0"
       },
-      "funding": {
-        "url": "https://github.com/sindresorhus/is?sponsor=1"
+      "peerDependencies": {
+        "release-it": ">=18.0.0 || >=19.0.0"
+      }
+    },
+    "node_modules/@release-it/bumper/node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/@sinonjs/commons": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
-      "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+      "version": "3.0.1",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "type-detect": "4.0.8"
       }
     },
     "node_modules/@sinonjs/fake-timers": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
-      "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+      "version": "11.2.2",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@sinonjs/commons": "^3.0.0"
       }
     },
     "node_modules/@sinonjs/samsam": {
       "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
-      "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@sinonjs/commons": "^2.0.0",
         "lodash.get": "^4.4.2",
@@ -1064,657 +1534,675 @@
     },
     "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
-      "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "type-detect": "4.0.8"
       }
     },
     "node_modules/@sinonjs/text-encoding": {
       "version": "0.7.2",
-      "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
-      "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
-      "dev": true
-    },
-    "node_modules/@szmarczak/http-timer": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
-      "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
       "dev": true,
-      "dependencies": {
-        "defer-to-connect": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=14.16"
-      }
+      "license": "(Unlicense OR Apache-2.0)"
     },
     "node_modules/@tootallnate/quickjs-emscripten": {
       "version": "0.23.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
       "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@tsconfig/node10": {
-      "version": "1.0.9",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
-      "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
-      "dev": true
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+      "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@tsconfig/node12": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
       "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@tsconfig/node14": {
-      "version": "14.1.0",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.0.tgz",
-      "integrity": "sha512-VmsCG04YR58ciHBeJKBDNMWWfYbyP8FekWVuTlpstaUPlat1D0x/tXzkWP7yCMU0eSz9V4OZU0LBWTFJ3xZf6w==",
-      "dev": true
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@tsconfig/node16": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
       "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/@types/http-cache-semantics": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",
-      "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==",
-      "dev": true
+    "node_modules/@types/body-parser": {
+      "version": "1.19.5",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+      "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/express": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+      "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^4.17.33",
+        "@types/qs": "*",
+        "@types/serve-static": "*"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "4.19.6",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+      "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*",
+        "@types/send": "*"
+      }
+    },
+    "node_modules/@types/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
+    "node_modules/@types/http-errors": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+      "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/@types/json-schema": {
-      "version": "7.0.13",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
-      "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==",
-      "dev": true
+    "node_modules/@types/mime": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+      "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/mocha": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz",
-      "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==",
-      "dev": true
+      "version": "10.0.6",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "20.6.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
-      "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==",
-      "dev": true
+      "version": "20.11.16",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
     },
-    "node_modules/@types/semver": {
-      "version": "7.5.2",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz",
-      "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==",
-      "dev": true
+    "node_modules/@types/parse-path": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
+      "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/qs": {
+      "version": "6.9.17",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
+      "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/send": {
+      "version": "0.17.4",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+      "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/mime": "^1",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "1.15.7",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+      "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/http-errors": "*",
+        "@types/node": "*",
+        "@types/send": "*"
+      }
     },
     "node_modules/@types/sinon": {
-      "version": "10.0.16",
-      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz",
-      "integrity": "sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==",
+      "version": "17.0.3",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/sinonjs__fake-timers": "*"
       }
     },
     "node_modules/@types/sinonjs__fake-timers": {
-      "version": "8.1.2",
-      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
-      "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
-      "dev": true
-    },
-    "node_modules/@types/yallist": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/@types/yallist/-/yallist-4.0.1.tgz",
-      "integrity": "sha512-G3FNJfaYtN8URU6wd6+uwFI62KO79j7n3XTYcwcFncP8gkfoi0b821GoVVt0oqKVnCqKYOMNKIGpakPoFhzAGA==",
-      "dev": true
+      "version": "8.1.5",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/yargs": {
-      "version": "17.0.24",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
-      "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
+      "version": "17.0.32",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@types/yargs-parser": "*"
       }
     },
     "node_modules/@types/yargs-parser": {
-      "version": "21.0.0",
-      "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
-      "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
-      "dev": true
-    },
-    "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.2.tgz",
-      "integrity": "sha512-ooaHxlmSgZTM6CHYAFRlifqh1OAr3PAQEwi7lhYhaegbnXrnh7CDcHmc3+ihhbQC7H0i4JF0psI5ehzkF6Yl6Q==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "6.7.2",
-        "@typescript-eslint/type-utils": "6.7.2",
-        "@typescript-eslint/utils": "6.7.2",
-        "@typescript-eslint/visitor-keys": "6.7.2",
-        "debug": "^4.3.4",
-        "graphemer": "^1.4.0",
-        "ignore": "^5.2.4",
-        "natural-compare": "^1.4.0",
-        "semver": "^7.5.4",
-        "ts-api-utils": "^1.0.1"
-      },
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependencies": {
-        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
-        "eslint": "^7.0.0 || ^8.0.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
+      "version": "21.0.3",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/@typescript-eslint/eslint-plugin/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==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "yallist": "^4.0.0"
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/@typescript-eslint/eslint-plugin/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==",
+    "node_modules/acorn": {
+      "version": "8.14.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+      "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
       "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
+      "license": "MIT",
       "bin": {
-        "semver": "bin/semver.js"
+        "acorn": "bin/acorn"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=0.4.0"
       }
     },
-    "node_modules/@typescript-eslint/parser": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.2.tgz",
-      "integrity": "sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==",
+    "node_modules/acorn-walk": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+      "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "6.7.2",
-        "@typescript-eslint/types": "6.7.2",
-        "@typescript-eslint/typescript-estree": "6.7.2",
-        "@typescript-eslint/visitor-keys": "6.7.2",
-        "debug": "^4.3.4"
+        "acorn": "^8.11.0"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "node": ">=0.4.0"
       }
     },
-    "node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.2.tgz",
-      "integrity": "sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "6.7.2",
-        "@typescript-eslint/visitor-keys": "6.7.2"
-      },
+    "node_modules/agent-base": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+      "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+      "license": "MIT",
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
+        "node": ">= 14"
       }
     },
-    "node_modules/@typescript-eslint/type-utils": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.2.tgz",
-      "integrity": "sha512-36F4fOYIROYRl0qj95dYKx6kybddLtsbmPIYNK0OBeXv2j9L5nZ17j9jmfy+bIDHKQgn2EZX+cofsqi8NPATBQ==",
+    "node_modules/aggregate-error": {
+      "version": "3.1.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "6.7.2",
-        "@typescript-eslint/utils": "6.7.2",
-        "debug": "^4.3.4",
-        "ts-api-utils": "^1.0.1"
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "node": ">=8"
       }
     },
-    "node_modules/@typescript-eslint/types": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.2.tgz",
-      "integrity": "sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==",
+    "node_modules/ansi-colors": {
+      "version": "4.1.1",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
+        "node": ">=6"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz",
-      "integrity": "sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==",
+    "node_modules/ansi-escapes": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "6.7.2",
-        "@typescript-eslint/visitor-keys": "6.7.2",
-        "debug": "^4.3.4",
-        "globby": "^11.1.0",
-        "is-glob": "^4.0.3",
-        "semver": "^7.5.4",
-        "ts-api-utils": "^1.0.1"
+        "type-fest": "^0.21.3"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": ">=8"
       },
       "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-escapes/node_modules/type-fest": {
+      "version": "0.21.3",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
       },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
-      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
-      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+    "node_modules/ansi-sequence-parser": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "array-union": "^2.1.0",
-        "dir-glob": "^3.0.1",
-        "fast-glob": "^3.2.9",
-        "ignore": "^5.2.0",
-        "merge2": "^1.4.1",
-        "slash": "^3.0.0"
+        "color-convert": "^2.0.1"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=8"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/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==",
+    "node_modules/anymatch": {
+      "version": "3.1.3",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "yallist": "^4.0.0"
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 8"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/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==",
+    "node_modules/append-transform": {
+      "version": "2.0.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
+        "default-require-extensions": "^3.0.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=8"
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
-      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+    "node_modules/archy": {
+      "version": "1.0.0",
       "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
+      "license": "MIT"
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "Python-2.0"
     },
-    "node_modules/@typescript-eslint/utils": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.2.tgz",
-      "integrity": "sha512-ZCcBJug/TS6fXRTsoTkgnsvyWSiXwMNiPzBUani7hDidBdj1779qwM1FIAmpH4lvlOZNF3EScsxxuGifjpLSWQ==",
+    "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==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/array-union": {
+      "version": "1.0.2",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "@types/json-schema": "^7.0.12",
-        "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.7.2",
-        "@typescript-eslint/types": "6.7.2",
-        "@typescript-eslint/typescript-estree": "6.7.2",
-        "semver": "^7.5.4"
+        "array-uniq": "^1.0.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/@typescript-eslint/utils/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==",
+    "node_modules/array-uniq": {
+      "version": "1.0.3",
       "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/@typescript-eslint/utils/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==",
+    "node_modules/ast-types": {
+      "version": "0.13.4",
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+      "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
+        "tslib": "^2.0.1"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=4"
       }
     },
-    "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.7.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz",
-      "integrity": "sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==",
+    "node_modules/async": {
+      "version": "3.2.5",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/async-retry": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+      "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "6.7.2",
-        "eslint-visitor-keys": "^3.4.1"
-      },
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
+        "retry": "0.13.1"
       }
     },
-    "node_modules/acorn": {
-      "version": "8.10.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
-      "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
       "dev": true,
-      "bin": {
-        "acorn": "bin/acorn"
-      },
+      "license": "MIT"
+    },
+    "node_modules/basic-ftp": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
+      "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
+      "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">=10.0.0"
       }
     },
-    "node_modules/acorn-jsx": {
-      "version": "5.3.2",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
-      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+    "node_modules/before-after-hook": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+      "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
       "dev": true,
-      "peerDependencies": {
-        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
-      }
+      "license": "Apache-2.0"
     },
-    "node_modules/acorn-walk": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
-      "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">=8"
       }
     },
-    "node_modules/agent-base": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
-      "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+    "node_modules/body-parser": {
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "debug": "^4.3.4"
+        "bytes": "3.1.2",
+        "content-type": "~1.0.5",
+        "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.13.0",
+        "raw-body": "2.5.2",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
       }
     },
-    "node_modules/aggregate-error": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
-      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+    "node_modules/body-parser/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "clean-stack": "^2.0.0",
-        "indent-string": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
+        "ms": "2.0.0"
       }
     },
-    "node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+    "node_modules/body-parser/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
       "dev": true,
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
+      "license": "MIT"
     },
-    "node_modules/ansi-align": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
-      "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
       "dev": true,
-      "dependencies": {
-        "string-width": "^4.1.0"
-      }
+      "license": "ISC"
     },
-    "node_modules/ansi-colors": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
-      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
       "dev": true,
-      "engines": {
-        "node": ">=6"
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
       }
     },
-    "node_modules/ansi-escapes": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
-      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "type-fest": "^0.21.3"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/ansi-regex": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+    "node_modules/browser-stdout": {
+      "version": "1.3.1",
       "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/browserslist": {
+      "version": "4.22.3",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001580",
+        "electron-to-chromium": "^1.4.648",
+        "node-releases": "^2.0.14",
+        "update-browserslist-db": "^1.0.13"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
       "engines": {
-        "node": ">=8"
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
-    "node_modules/ansi-sequence-parser": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
-      "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==",
-      "dev": true
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
     },
-    "node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "license": "MIT",
       "dependencies": {
-        "color-convert": "^2.0.1"
+        "run-applescript": "^7.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=18"
       },
       "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/anymatch": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
-      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
       "dev": true,
-      "dependencies": {
-        "normalize-path": "^3.0.0",
-        "picomatch": "^2.0.4"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">= 8"
+        "node": ">= 0.8"
       }
     },
-    "node_modules/append-transform": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
-      "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==",
+    "node_modules/c12": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/c12/-/c12-3.0.3.tgz",
+      "integrity": "sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "default-require-extensions": "^3.0.0"
+        "chokidar": "^4.0.3",
+        "confbox": "^0.2.2",
+        "defu": "^6.1.4",
+        "dotenv": "^16.4.7",
+        "exsolve": "^1.0.4",
+        "giget": "^2.0.0",
+        "jiti": "^2.4.2",
+        "ohash": "^2.0.11",
+        "pathe": "^2.0.3",
+        "perfect-debounce": "^1.0.0",
+        "pkg-types": "^2.1.0",
+        "rc9": "^2.1.2"
       },
-      "engines": {
-        "node": ">=8"
+      "peerDependencies": {
+        "magicast": "^0.3.5"
+      },
+      "peerDependenciesMeta": {
+        "magicast": {
+          "optional": true
+        }
       }
     },
-    "node_modules/archy": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
-      "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
-      "dev": true
-    },
-    "node_modules/arg": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
-      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
-      "dev": true
-    },
-    "node_modules/argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true
-    },
-    "node_modules/array-buffer-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
-      "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+    "node_modules/c12/node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "is-array-buffer": "^3.0.1"
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://paulmillr.com/funding/"
       }
     },
-    "node_modules/array-union": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
-      "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
+    "node_modules/c12/node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
       "dev": true,
-      "dependencies": {
-        "array-uniq": "^1.0.1"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
       }
     },
-    "node_modules/array-uniq": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
-      "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
+    "node_modules/caching-transform": {
+      "version": "4.0.0",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasha": "^5.0.0",
+        "make-dir": "^3.0.0",
+        "package-hash": "^4.0.0",
+        "write-file-atomic": "^3.0.0"
+      },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">=8"
       }
     },
-    "node_modules/array.prototype.map": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz",
-      "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==",
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "es-array-method-boxes-properly": "^1.0.0",
-        "is-string": "^1.0.7"
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -1723,1427 +2211,66 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/arraybuffer.prototype.slice": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
-      "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+      "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.0",
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "get-intrinsic": "^1.2.1",
-        "is-array-buffer": "^3.0.2",
-        "is-shared-array-buffer": "^1.0.2"
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
       },
       "engines": {
         "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/ast-types": {
-      "version": "0.13.4",
-      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
-      "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+    "node_modules/camelcase": {
+      "version": "5.3.1",
       "dev": true,
-      "dependencies": {
-        "tslib": "^2.0.1"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=4"
+        "node": ">=6"
       }
     },
-    "node_modules/async": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
-      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
-      "dev": true
-    },
-    "node_modules/async-retry": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
-      "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
-      "dev": true,
-      "dependencies": {
-        "retry": "0.13.1"
-      }
-    },
-    "node_modules/available-typed-arrays": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
-      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/balanced-match": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true
-    },
-    "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==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/basic-ftp": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz",
-      "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.0.0"
-      }
-    },
-    "node_modules/before-after-hook": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
-      "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
-      "dev": true
-    },
-    "node_modules/big-integer": {
-      "version": "1.6.51",
-      "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
-      "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.6"
-      }
-    },
-    "node_modules/binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/bl": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
-      "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
-      "dev": true,
-      "dependencies": {
-        "buffer": "^6.0.3",
-        "inherits": "^2.0.4",
-        "readable-stream": "^3.4.0"
-      }
-    },
-    "node_modules/boxen": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz",
-      "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==",
-      "dev": true,
-      "dependencies": {
-        "ansi-align": "^3.0.1",
-        "camelcase": "^7.0.1",
-        "chalk": "^5.2.0",
-        "cli-boxes": "^3.0.0",
-        "string-width": "^5.1.2",
-        "type-fest": "^2.13.0",
-        "widest-line": "^4.0.1",
-        "wrap-ansi": "^8.1.0"
-      },
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/boxen/node_modules/ansi-regex": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
-      }
-    },
-    "node_modules/boxen/node_modules/ansi-styles": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
-      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/boxen/node_modules/emoji-regex": {
-      "version": "9.2.2",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true
-    },
-    "node_modules/boxen/node_modules/string-width": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
-      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-      "dev": true,
-      "dependencies": {
-        "eastasianwidth": "^0.2.0",
-        "emoji-regex": "^9.2.2",
-        "strip-ansi": "^7.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/boxen/node_modules/strip-ansi": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
-      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-      "dev": true,
-      "dependencies": {
-        "ansi-regex": "^6.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
-      }
-    },
-    "node_modules/boxen/node_modules/type-fest": {
-      "version": "2.19.0",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
-      "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.20"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/boxen/node_modules/wrap-ansi": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
-      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^6.1.0",
-        "string-width": "^5.0.1",
-        "strip-ansi": "^7.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-      }
-    },
-    "node_modules/bplist-parser": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
-      "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
-      "dev": true,
-      "dependencies": {
-        "big-integer": "^1.6.44"
-      },
-      "engines": {
-        "node": ">= 5.10.0"
-      }
-    },
-    "node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
-      "dependencies": {
-        "fill-range": "^7.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/browser-stdout": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
-      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
-      "dev": true
-    },
-    "node_modules/browserslist": {
-      "version": "4.21.10",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
-      "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/browserslist"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/browserslist"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "dependencies": {
-        "caniuse-lite": "^1.0.30001517",
-        "electron-to-chromium": "^1.4.477",
-        "node-releases": "^2.0.13",
-        "update-browserslist-db": "^1.0.11"
-      },
-      "bin": {
-        "browserslist": "cli.js"
-      },
-      "engines": {
-        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
-      }
-    },
-    "node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/buffer-from": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
-      "dev": true
-    },
-    "node_modules/bundle-name": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
-      "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
-      "dev": true,
-      "dependencies": {
-        "run-applescript": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/cacheable-lookup": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
-      "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
-      "dev": true,
-      "engines": {
-        "node": ">=14.16"
-      }
-    },
-    "node_modules/cacheable-request": {
-      "version": "10.2.13",
-      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz",
-      "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==",
-      "dev": true,
-      "dependencies": {
-        "@types/http-cache-semantics": "^4.0.1",
-        "get-stream": "^6.0.1",
-        "http-cache-semantics": "^4.1.1",
-        "keyv": "^4.5.3",
-        "mimic-response": "^4.0.0",
-        "normalize-url": "^8.0.0",
-        "responselike": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=14.16"
-      }
-    },
-    "node_modules/caching-transform": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
-      "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==",
-      "dev": true,
-      "dependencies": {
-        "hasha": "^5.0.0",
-        "make-dir": "^3.0.0",
-        "package-hash": "^4.0.0",
-        "write-file-atomic": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=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==",
-      "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/camelcase": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
-      "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
-      "dev": true,
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/caniuse-lite": {
-      "version": "1.0.30001535",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001535.tgz",
-      "integrity": "sha512-48jLyUkiWFfhm/afF7cQPqPjaUmSraEhK4j+FCTJpgnGGEZHqyLe3hmWH7lIooZdSzXL0ReMvHz0vKDoTBsrwg==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/browserslist"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ]
-    },
-    "node_modules/chalk": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
-      "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
-      "dev": true,
-      "engines": {
-        "node": "^12.17.0 || ^14.13 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/chardet": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
-      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
-      "dev": true
-    },
-    "node_modules/chokidar": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ],
-      "dependencies": {
-        "anymatch": "~3.1.2",
-        "braces": "~3.0.2",
-        "glob-parent": "~5.1.2",
-        "is-binary-path": "~2.1.0",
-        "is-glob": "~4.0.1",
-        "normalize-path": "~3.0.0",
-        "readdirp": "~3.6.0"
-      },
-      "engines": {
-        "node": ">= 8.10.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/ci-info": {
-      "version": "3.8.0",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
-      "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/sibiraj-s"
-        }
-      ],
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/clean-stack": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
-      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/cli-boxes": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
-      "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/cli-cursor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
-      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
-      "dev": true,
-      "dependencies": {
-        "restore-cursor": "^3.1.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/cli-spinners": {
-      "version": "2.9.1",
-      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz",
-      "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/cli-width": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
-      "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
-      "dev": true,
-      "engines": {
-        "node": ">= 12"
-      }
-    },
-    "node_modules/cliui": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
-      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
-      "dev": true,
-      "dependencies": {
-        "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.1",
-        "wrap-ansi": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/cliui/node_modules/wrap-ansi": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-      }
-    },
-    "node_modules/clone": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
-      "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
-    "node_modules/cluster-key-slot": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
-      "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
-      "engines": {
-        "node": ">=0.10.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==",
-      "dev": true,
-      "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==",
-      "dev": true
-    },
-    "node_modules/commander": {
-      "version": "11.0.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
-      "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=16"
-      }
-    },
-    "node_modules/commondir": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
-      "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
-      "dev": true
-    },
-    "node_modules/concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true
-    },
-    "node_modules/config-chain": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
-      "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
-      "dev": true,
-      "dependencies": {
-        "ini": "^1.3.4",
-        "proto-list": "~1.2.1"
-      }
-    },
-    "node_modules/config-chain/node_modules/ini": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-      "dev": true
-    },
-    "node_modules/configstore": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz",
-      "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==",
-      "dev": true,
-      "dependencies": {
-        "dot-prop": "^6.0.1",
-        "graceful-fs": "^4.2.6",
-        "unique-string": "^3.0.0",
-        "write-file-atomic": "^3.0.3",
-        "xdg-basedir": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/yeoman/configstore?sponsor=1"
-      }
-    },
-    "node_modules/convert-source-map": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
-      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
-      "dev": true
-    },
-    "node_modules/cosmiconfig": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz",
-      "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==",
-      "dev": true,
-      "dependencies": {
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "parse-json": "^5.0.0",
-        "path-type": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/d-fischer"
-      }
-    },
-    "node_modules/create-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
-      "dev": true
-    },
-    "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^3.1.0",
-        "shebang-command": "^2.0.0",
-        "which": "^2.0.1"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/crypto-random-string": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
-      "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==",
-      "dev": true,
-      "dependencies": {
-        "type-fest": "^1.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/crypto-random-string/node_modules/type-fest": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
-      "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/data-uri-to-buffer": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
-      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
-      "dev": true,
-      "engines": {
-        "node": ">= 12"
-      }
-    },
-    "node_modules/debug": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
-      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-      "dev": true,
-      "dependencies": {
-        "ms": "2.1.2"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "peerDependenciesMeta": {
-        "supports-color": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/decamelize": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "mimic-response": "^3.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/decompress-response/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==",
-      "dev": true,
-      "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==",
-      "dev": true,
-      "engines": {
-        "node": ">=4.0.0"
-      }
-    },
-    "node_modules/deep-is": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
-      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-      "dev": true
-    },
-    "node_modules/default-browser": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
-      "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
-      "dev": true,
-      "dependencies": {
-        "bundle-name": "^3.0.0",
-        "default-browser-id": "^3.0.0",
-        "execa": "^7.1.1",
-        "titleize": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/default-browser-id": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
-      "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
-      "dev": true,
-      "dependencies": {
-        "bplist-parser": "^0.2.0",
-        "untildify": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/default-require-extensions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
-      "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==",
-      "dev": true,
-      "dependencies": {
-        "strip-bom": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/defaults": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
-      "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
-      "dev": true,
-      "dependencies": {
-        "clone": "^1.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/defer-to-connect": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
-      "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/define-data-property": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
-      "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==",
-      "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/define-lazy-prop": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
-      "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/define-properties": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
-      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
-      "dev": true,
-      "dependencies": {
-        "define-data-property": "^1.0.1",
-        "has-property-descriptors": "^1.0.0",
-        "object-keys": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/degenerator": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
-      "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
-      "dev": true,
-      "dependencies": {
-        "ast-types": "^0.13.4",
-        "escodegen": "^2.1.0",
-        "esprima": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/deprecation": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
-      "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
-      "dev": true
-    },
-    "node_modules/diff": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
-      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.3.1"
-      }
-    },
-    "node_modules/dir-glob": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
-      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
-      "dev": true,
-      "dependencies": {
-        "path-type": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/doctrine": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
-      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-      "dev": true,
-      "dependencies": {
-        "esutils": "^2.0.2"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/dot-prop": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
-      "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
-      "dev": true,
-      "dependencies": {
-        "is-obj": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eastasianwidth": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-      "dev": true
-    },
-    "node_modules/electron-to-chromium": {
-      "version": "1.4.523",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz",
-      "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==",
-      "dev": true
-    },
-    "node_modules/email-addresses": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
-      "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
-      "dev": true
-    },
-    "node_modules/emoji-regex": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
-    },
-    "node_modules/error-ex": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
-      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
-      "dependencies": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
-    "node_modules/es-abstract": {
-      "version": "1.22.2",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz",
-      "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==",
-      "dev": true,
-      "dependencies": {
-        "array-buffer-byte-length": "^1.0.0",
-        "arraybuffer.prototype.slice": "^1.0.2",
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
-        "es-set-tostringtag": "^2.0.1",
-        "es-to-primitive": "^1.2.1",
-        "function.prototype.name": "^1.1.6",
-        "get-intrinsic": "^1.2.1",
-        "get-symbol-description": "^1.0.0",
-        "globalthis": "^1.0.3",
-        "gopd": "^1.0.1",
-        "has": "^1.0.3",
-        "has-property-descriptors": "^1.0.0",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "internal-slot": "^1.0.5",
-        "is-array-buffer": "^3.0.2",
-        "is-callable": "^1.2.7",
-        "is-negative-zero": "^2.0.2",
-        "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.2",
-        "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.12",
-        "is-weakref": "^1.0.2",
-        "object-inspect": "^1.12.3",
-        "object-keys": "^1.1.1",
-        "object.assign": "^4.1.4",
-        "regexp.prototype.flags": "^1.5.1",
-        "safe-array-concat": "^1.0.1",
-        "safe-regex-test": "^1.0.0",
-        "string.prototype.trim": "^1.2.8",
-        "string.prototype.trimend": "^1.0.7",
-        "string.prototype.trimstart": "^1.0.7",
-        "typed-array-buffer": "^1.0.0",
-        "typed-array-byte-length": "^1.0.0",
-        "typed-array-byte-offset": "^1.0.0",
-        "typed-array-length": "^1.0.4",
-        "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.11"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/es-array-method-boxes-properly": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
-      "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
-      "dev": true
-    },
-    "node_modules/es-get-iterator": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
-      "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.3",
-        "has-symbols": "^1.0.3",
-        "is-arguments": "^1.1.1",
-        "is-map": "^2.0.2",
-        "is-set": "^2.0.2",
-        "is-string": "^1.0.7",
-        "isarray": "^2.0.5",
-        "stop-iteration-iterator": "^1.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/es-set-tostringtag": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
-      "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
-      "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.1.3",
-        "has": "^1.0.3",
-        "has-tostringtag": "^1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-to-primitive": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
-      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
-      "dependencies": {
-        "is-callable": "^1.1.4",
-        "is-date-object": "^1.0.1",
-        "is-symbol": "^1.0.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/es6-error": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
-      "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
-      "dev": true
-    },
-    "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/escape-goat": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
-      "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/escape-string-regexp": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
-      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/escodegen": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
-      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
-      "dev": true,
-      "dependencies": {
-        "esprima": "^4.0.1",
-        "estraverse": "^5.2.0",
-        "esutils": "^2.0.2"
-      },
-      "bin": {
-        "escodegen": "bin/escodegen.js",
-        "esgenerate": "bin/esgenerate.js"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "optionalDependencies": {
-        "source-map": "~0.6.1"
-      }
-    },
-    "node_modules/eslint": {
-      "version": "8.49.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
-      "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.6.1",
-        "@eslint/eslintrc": "^2.1.2",
-        "@eslint/js": "8.49.0",
-        "@humanwhocodes/config-array": "^0.11.11",
-        "@humanwhocodes/module-importer": "^1.0.1",
-        "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.12.4",
-        "chalk": "^4.0.0",
-        "cross-spawn": "^7.0.2",
-        "debug": "^4.3.2",
-        "doctrine": "^3.0.0",
-        "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.2.2",
-        "eslint-visitor-keys": "^3.4.3",
-        "espree": "^9.6.1",
-        "esquery": "^1.4.2",
-        "esutils": "^2.0.2",
-        "fast-deep-equal": "^3.1.3",
-        "file-entry-cache": "^6.0.1",
-        "find-up": "^5.0.0",
-        "glob-parent": "^6.0.2",
-        "globals": "^13.19.0",
-        "graphemer": "^1.4.0",
-        "ignore": "^5.2.0",
-        "imurmurhash": "^0.1.4",
-        "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
-        "js-yaml": "^4.1.0",
-        "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
-        "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
-        "natural-compare": "^1.4.0",
-        "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
-        "text-table": "^0.2.0"
-      },
-      "bin": {
-        "eslint": "bin/eslint.js"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint-scope": {
-      "version": "7.2.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
-      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
-      "dev": true,
-      "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint-visitor-keys": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-      "dev": true,
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/eslint/node_modules/escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint/node_modules/find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-      "dev": true,
-      "dependencies": {
-        "locate-path": "^6.0.0",
-        "path-exists": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint/node_modules/glob-parent": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
-      "dependencies": {
-        "is-glob": "^4.0.3"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/eslint/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/eslint/node_modules/locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-      "dev": true,
-      "dependencies": {
-        "p-locate": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint/node_modules/p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001584",
       "dev": true,
-      "dependencies": {
-        "yocto-queue": "^0.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
     },
-    "node_modules/eslint/node_modules/p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+    "node_modules/chalk": {
+      "version": "4.1.2",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "p-limit": "^3.0.2"
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
       },
       "engines": {
         "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
-    "node_modules/eslint/node_modules/supports-color": {
+    "node_modules/chalk/node_modules/supports-color": {
       "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -3151,506 +2278,420 @@
         "node": ">=8"
       }
     },
-    "node_modules/espree": {
-      "version": "9.6.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
-      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.9.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.4.1"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+    "node_modules/chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true,
-      "bin": {
-        "esparse": "bin/esparse.js",
-        "esvalidate": "bin/esvalidate.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
+      "license": "MIT"
     },
-    "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+    "node_modules/cheerio": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
+      "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "estraverse": "^5.1.0"
+        "cheerio-select": "^2.1.0",
+        "dom-serializer": "^2.0.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.1.0",
+        "encoding-sniffer": "^0.2.0",
+        "htmlparser2": "^9.1.0",
+        "parse5": "^7.1.2",
+        "parse5-htmlparser2-tree-adapter": "^7.0.0",
+        "parse5-parser-stream": "^7.1.2",
+        "undici": "^6.19.5",
+        "whatwg-mimetype": "^4.0.0"
       },
       "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/esrecurse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
-      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
-      "dev": true,
-      "dependencies": {
-        "estraverse": "^5.2.0"
+        "node": ">=18.17"
       },
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/estraverse": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/esutils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
+      "funding": {
+        "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
       }
     },
-    "node_modules/execa": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
-      "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+    "node_modules/cheerio-select": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+      "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^6.0.1",
-        "human-signals": "^4.3.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^3.0.7",
-        "strip-final-newline": "^3.0.0"
-      },
-      "engines": {
-        "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+        "boolbase": "^1.0.0",
+        "css-select": "^5.1.0",
+        "css-what": "^6.1.0",
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.0.1"
       },
       "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+        "url": "https://github.com/sponsors/fb55"
       }
     },
-    "node_modules/external-editor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
-      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+    "node_modules/chokidar": {
+      "version": "3.5.3",
       "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "license": "MIT",
       "dependencies": {
-        "chardet": "^0.7.0",
-        "iconv-lite": "^0.4.24",
-        "tmp": "^0.0.33"
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
       }
     },
-    "node_modules/fast-deep-equal": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
-    },
-    "node_modules/fast-glob": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
-      "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+    "node_modules/ci-info": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
+      "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
       "dev": true,
-      "dependencies": {
-        "@nodelib/fs.stat": "^2.0.2",
-        "@nodelib/fs.walk": "^1.2.3",
-        "glob-parent": "^5.1.2",
-        "merge2": "^1.3.0",
-        "micromatch": "^4.0.4"
-      },
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "license": "MIT",
       "engines": {
-        "node": ">=8.6.0"
+        "node": ">=8"
       }
     },
-    "node_modules/fast-json-stable-stringify": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-      "dev": true
-    },
-    "node_modules/fast-levenshtein": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-      "dev": true
-    },
-    "node_modules/fastq": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
-      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+    "node_modules/citty": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+      "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "reusify": "^1.0.4"
+        "consola": "^3.2.3"
       }
     },
-    "node_modules/fetch-blob": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
-      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+    "node_modules/clean-stack": {
+      "version": "2.2.0",
       "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/jimmywarting"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/jimmywarting"
-        }
-      ],
-      "dependencies": {
-        "node-domexception": "^1.0.0",
-        "web-streams-polyfill": "^3.0.3"
-      },
+      "license": "MIT",
       "engines": {
-        "node": "^12.20 || >= 14.13"
+        "node": ">=6"
       }
     },
-    "node_modules/figures": {
+    "node_modules/cli-cursor": {
       "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
-      "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "escape-string-regexp": "^5.0.0",
-        "is-unicode-supported": "^1.2.0"
+        "restore-cursor": "^5.0.0"
       },
       "engines": {
-        "node": ">=14"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/file-entry-cache": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
-      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+    "node_modules/cli-spinners": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+      "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
       "dev": true,
-      "dependencies": {
-        "flat-cache": "^3.0.4"
-      },
+      "license": "MIT",
       "engines": {
-        "node": "^10.12.0 || >=12.0.0"
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/filename-reserved-regex": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
-      "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==",
+    "node_modules/cli-width": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+      "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=4"
+        "node": ">= 12"
       }
     },
-    "node_modules/filenamify": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz",
-      "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==",
+    "node_modules/cliui": {
+      "version": "7.0.4",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "filename-reserved-regex": "^2.0.0",
-        "strip-outer": "^1.0.1",
-        "trim-repeated": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
       }
     },
-    "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "7.0.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "to-regex-range": "^5.0.1"
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
-    "node_modules/find-cache-dir": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
-      "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
-      "dev": true,
-      "dependencies": {
-        "commondir": "^1.0.1",
-        "make-dir": "^3.0.2",
-        "pkg-dir": "^4.1.0"
-      },
+    "node_modules/cluster-key-slot": {
+      "version": "1.1.2",
+      "license": "Apache-2.0",
       "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/find-up": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+    "node_modules/color-convert": {
+      "version": "2.0.1",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "locate-path": "^5.0.0",
-        "path-exists": "^4.0.0"
+        "color-name": "~1.1.4"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=7.0.0"
       }
     },
-    "node_modules/flat": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
-      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+    "node_modules/color-name": {
+      "version": "1.1.4",
       "dev": true,
-      "bin": {
-        "flat": "cli.js"
-      }
+      "license": "MIT"
     },
-    "node_modules/flat-cache": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz",
-      "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==",
+    "node_modules/commander": {
+      "version": "11.1.0",
       "dev": true,
-      "dependencies": {
-        "flatted": "^3.2.7",
-        "keyv": "^4.5.3",
-        "rimraf": "^3.0.2"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=16"
       }
     },
-    "node_modules/flatted": {
-      "version": "3.2.9",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
-      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
-      "dev": true
+    "node_modules/commondir": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/for-each": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
-      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+    "node_modules/concat-map": {
+      "version": "0.0.1",
       "dev": true,
-      "dependencies": {
-        "is-callable": "^1.1.3"
-      }
+      "license": "MIT"
     },
-    "node_modules/foreground-child": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
-      "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+    "node_modules/confbox": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+      "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
       "dev": true,
-      "dependencies": {
-        "cross-spawn": "^7.0.0",
-        "signal-exit": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
+      "license": "MIT"
     },
-    "node_modules/form-data-encoder": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
-      "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
+    "node_modules/consola": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+      "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 14.17"
+        "node": "^14.18.0 || >=16.10.0"
       }
     },
-    "node_modules/formdata-polyfill": {
-      "version": "4.0.10",
-      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
-      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "fetch-blob": "^3.1.2"
+        "safe-buffer": "5.2.1"
       },
       "engines": {
-        "node": ">=12.20.0"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/fromentries": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz",
-      "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/fs-extra": {
-      "version": "11.1.1",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
-      "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
+    "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==",
       "dev": true,
-      "dependencies": {
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=14.14"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true
+    "node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/fsevents": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+    "node_modules/cookie": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
       "dev": true,
-      "hasInstallScript": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
+      "license": "MIT",
       "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/function-bind": {
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/create-require": {
       "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/function.prototype.name": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
-      "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "functions-have-names": "^1.2.3"
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-select": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
+      "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0",
+        "css-what": "^6.1.0",
+        "domhandler": "^5.0.2",
+        "domutils": "^3.0.1",
+        "nth-check": "^2.0.1"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/fb55"
       }
     },
-    "node_modules/functions-have-names": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
-      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+    "node_modules/css-what": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
       "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">= 6"
+      },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/fb55"
       }
     },
-    "node_modules/generic-pool": {
-      "version": "3.9.0",
-      "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
-      "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
+    "node_modules/data-uri-to-buffer": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+      "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+      "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 4"
+        "node": ">= 14"
       }
     },
-    "node_modules/gensync": {
-      "version": "1.0.0-beta.2",
-      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
-      "dev": true,
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
       "engines": {
-        "node": ">=6.9.0"
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
       }
     },
-    "node_modules/get-caller-file": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+    "node_modules/debug/node_modules/ms": {
+      "version": "2.1.2",
+      "license": "MIT"
+    },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": "6.* || 8.* || >= 10.*"
+        "node": ">=0.10.0"
       }
     },
-    "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==",
-      "dev": true,
+    "node_modules/default-browser": {
+      "version": "5.2.1",
+      "license": "MIT",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3"
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/get-package-type": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
-      "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
-      "dev": true,
+    "node_modules/default-browser-id": {
+      "version": "5.0.0",
+      "license": "MIT",
       "engines": {
-        "node": ">=8.0.0"
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/get-stream": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
-      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+    "node_modules/default-require-extensions": {
+      "version": "3.0.1",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "strip-bom": "^4.0.0"
+      },
       "engines": {
-        "node": ">=10"
+        "node": ">=8"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/get-symbol-description": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
-      "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.1"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3659,343 +2700,454 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/get-uri": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz",
-      "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==",
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/defu": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+      "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/degenerator": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+      "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "basic-ftp": "^5.0.2",
-        "data-uri-to-buffer": "^5.0.1",
-        "debug": "^4.3.4",
-        "fs-extra": "^8.1.0"
+        "ast-types": "^0.13.4",
+        "escodegen": "^2.1.0",
+        "esprima": "^4.0.1"
       },
       "engines": {
         "node": ">= 14"
       }
     },
-    "node_modules/get-uri/node_modules/data-uri-to-buffer": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz",
-      "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 14"
+        "node": ">= 0.8"
       }
     },
-    "node_modules/get-uri/node_modules/fs-extra": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
-      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+    "node_modules/destr": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+      "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
       "dev": true,
-      "dependencies": {
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^4.0.0",
-        "universalify": "^0.1.0"
-      },
+      "license": "MIT"
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=6 <7 || >=8"
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
       }
     },
-    "node_modules/get-uri/node_modules/jsonfile": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
-      "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+    "node_modules/detect-indent": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz",
+      "integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==",
       "dev": true,
-      "optionalDependencies": {
-        "graceful-fs": "^4.1.6"
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20"
       }
     },
-    "node_modules/get-uri/node_modules/universalify": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
-      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+    "node_modules/diff": {
+      "version": "5.0.0",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
-        "node": ">= 4.0.0"
+        "node": ">=0.3.1"
       }
     },
-    "node_modules/gh-pages": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.0.0.tgz",
-      "integrity": "sha512-FXZWJRsvP/fK2HJGY+Di6FRNHvqFF6gOIELaopDjXXgjeOYSNURcuYwEO/6bwuq6koP5Lnkvnr5GViXzuOB89g==",
+    "node_modules/dom-serializer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+      "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "async": "^3.2.4",
-        "commander": "^11.0.0",
-        "email-addresses": "^5.0.0",
-        "filenamify": "^4.3.0",
-        "find-cache-dir": "^3.3.1",
-        "fs-extra": "^11.1.1",
-        "globby": "^6.1.0"
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.2",
+        "entities": "^4.2.0"
       },
-      "bin": {
-        "gh-pages": "bin/gh-pages.js",
-        "gh-pages-clean": "bin/gh-pages-clean.js"
+      "funding": {
+        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "^2.3.0"
+      },
+      "engines": {
+        "node": ">= 4"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domhandler?sponsor=1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "^2.0.0",
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domutils?sponsor=1"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.4.7",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+      "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
+      "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.4"
       }
     },
-    "node_modules/git-up": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz",
-      "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==",
-      "dev": true,
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "is-ssh": "^1.4.0",
-        "parse-url": "^8.1.0"
+        "safe-buffer": "^5.0.1"
       }
     },
-    "node_modules/git-url-parse": {
-      "version": "13.1.0",
-      "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz",
-      "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==",
+    "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==",
       "dev": true,
-      "dependencies": {
-        "git-up": "^7.0.0"
+      "license": "MIT"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.4.656",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/email-addresses": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
       }
     },
-    "node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+    "node_modules/encoding-sniffer": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
+      "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      },
-      "engines": {
-        "node": "*"
+        "iconv-lite": "^0.6.3",
+        "whatwg-encoding": "^3.1.1"
       },
       "funding": {
-        "url": "https://github.com/sponsors/isaacs"
+        "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
       }
     },
-    "node_modules/glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+    "node_modules/encoding-sniffer/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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "is-glob": "^4.0.1"
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
       "engines": {
-        "node": ">= 6"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/global-dirs": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz",
-      "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==",
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
       "dev": true,
-      "dependencies": {
-        "ini": "2.0.0"
-      },
+      "license": "BSD-2-Clause",
       "engines": {
-        "node": ">=10"
+        "node": ">=0.12"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/fb55/entities?sponsor=1"
       }
     },
-    "node_modules/globals": {
-      "version": "13.21.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
-      "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
       "dev": true,
-      "dependencies": {
-        "type-fest": "^0.20.2"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.4"
       }
     },
-    "node_modules/globals/node_modules/type-fest": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
-      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+    "node_modules/es-errors": {
+      "version": "1.3.0",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.4"
       }
     },
-    "node_modules/globalthis": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
-      "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+    "node_modules/es6-error": {
+      "version": "4.1.1",
       "dev": true,
-      "dependencies": {
-        "define-properties": "^1.1.3"
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.19.12",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=12"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.19.12",
+        "@esbuild/android-arm": "0.19.12",
+        "@esbuild/android-arm64": "0.19.12",
+        "@esbuild/android-x64": "0.19.12",
+        "@esbuild/darwin-arm64": "0.19.12",
+        "@esbuild/darwin-x64": "0.19.12",
+        "@esbuild/freebsd-arm64": "0.19.12",
+        "@esbuild/freebsd-x64": "0.19.12",
+        "@esbuild/linux-arm": "0.19.12",
+        "@esbuild/linux-arm64": "0.19.12",
+        "@esbuild/linux-ia32": "0.19.12",
+        "@esbuild/linux-loong64": "0.19.12",
+        "@esbuild/linux-mips64el": "0.19.12",
+        "@esbuild/linux-ppc64": "0.19.12",
+        "@esbuild/linux-riscv64": "0.19.12",
+        "@esbuild/linux-s390x": "0.19.12",
+        "@esbuild/linux-x64": "0.19.12",
+        "@esbuild/netbsd-x64": "0.19.12",
+        "@esbuild/openbsd-x64": "0.19.12",
+        "@esbuild/sunos-x64": "0.19.12",
+        "@esbuild/win32-arm64": "0.19.12",
+        "@esbuild/win32-ia32": "0.19.12",
+        "@esbuild/win32-x64": "0.19.12"
       }
     },
-    "node_modules/globby": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
-      "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==",
+    "node_modules/escalade": {
+      "version": "3.1.1",
       "dev": true,
-      "dependencies": {
-        "array-union": "^1.0.1",
-        "glob": "^7.0.3",
-        "object-assign": "^4.0.1",
-        "pify": "^2.0.0",
-        "pinkie-promise": "^2.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">=6"
       }
     },
-    "node_modules/gopd": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
-      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+    "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==",
       "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.1.3"
+      "license": "MIT"
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/got": {
-      "version": "13.0.0",
-      "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
-      "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
+    "node_modules/escodegen": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
-        "@sindresorhus/is": "^5.2.0",
-        "@szmarczak/http-timer": "^5.0.1",
-        "cacheable-lookup": "^7.0.0",
-        "cacheable-request": "^10.2.8",
-        "decompress-response": "^6.0.0",
-        "form-data-encoder": "^2.1.2",
-        "get-stream": "^6.0.1",
-        "http2-wrapper": "^2.1.10",
-        "lowercase-keys": "^3.0.0",
-        "p-cancelable": "^3.0.0",
-        "responselike": "^3.0.0"
+        "esprima": "^4.0.1",
+        "estraverse": "^5.2.0",
+        "esutils": "^2.0.2"
+      },
+      "bin": {
+        "escodegen": "bin/escodegen.js",
+        "esgenerate": "bin/esgenerate.js"
       },
       "engines": {
-        "node": ">=16"
+        "node": ">=6.0"
       },
-      "funding": {
-        "url": "https://github.com/sindresorhus/got?sponsor=1"
+      "optionalDependencies": {
+        "source-map": "~0.6.1"
       }
     },
-    "node_modules/graceful-fs": {
-      "version": "4.2.11",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
-      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
-      "dev": true
-    },
-    "node_modules/graphemer": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-      "dev": true
-    },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+    "node_modules/esprima": {
+      "version": "4.0.1",
       "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.1"
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
       },
       "engines": {
-        "node": ">= 0.4.0"
+        "node": ">=4"
       }
     },
-    "node_modules/has-bigints": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
-      "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
       "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
       }
     },
-    "node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "engines": {
-        "node": ">=4"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/has-property-descriptors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
-      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+    "node_modules/eta": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz",
+      "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==",
       "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.1.1"
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/eta-dev/eta?sponsor=1"
       }
     },
-    "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==",
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 0.6"
       }
     },
-    "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==",
-      "dev": true,
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=0.8.x"
       }
     },
-    "node_modules/has-tostringtag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
-      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+    "node_modules/execa": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^8.0.1",
+        "human-signals": "^5.0.0",
+        "is-stream": "^3.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^5.1.0",
+        "onetime": "^6.0.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^3.0.0"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=16.17"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
       }
     },
-    "node_modules/has-yarn": {
+    "node_modules/execa/node_modules/is-stream": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz",
-      "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
       },
@@ -4003,262 +3155,372 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/hasha": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
-      "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
+    "node_modules/execa/node_modules/onetime": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "is-stream": "^2.0.0",
-        "type-fest": "^0.8.0"
+        "mimic-fn": "^4.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/hasha/node_modules/is-stream": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+    "node_modules/execa/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=8"
+        "node": ">=14"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/hasha/node_modules/type-fest": {
-      "version": "0.8.1",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
-      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+    "node_modules/express": {
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
       "dev": true,
-      "engines": {
-        "node": ">=8"
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.3",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.7.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.3.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.13.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.19.0",
+        "serve-static": "1.16.2",
+        "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"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
       }
     },
-    "node_modules/he": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+    "node_modules/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
       "dev": true,
-      "bin": {
-        "he": "bin/he"
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.7",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
       }
     },
-    "node_modules/html-escaper": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
-      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
-      "dev": true
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
     },
-    "node_modules/http-cache-semantics": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
-      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
-      "dev": true
+    "node_modules/express-session/node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/http-proxy-agent": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
-      "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
+    "node_modules/express-session/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "agent-base": "^7.1.0",
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
+        "ms": "2.0.0"
       }
     },
-    "node_modules/http2-wrapper": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz",
-      "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==",
+    "node_modules/express-session/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/express/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "quick-lru": "^5.1.1",
-        "resolve-alpn": "^1.2.0"
-      },
-      "engines": {
-        "node": ">=10.19.0"
+        "ms": "2.0.0"
       }
     },
-    "node_modules/https-proxy-agent": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
-      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+    "node_modules/express/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
       "dev": true,
-      "dependencies": {
-        "agent-base": "^7.0.2",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
+      "license": "MIT"
     },
-    "node_modules/human-signals": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
-      "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+    "node_modules/express/node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
       "dev": true,
-      "engines": {
-        "node": ">=14.18.0"
-      }
+      "license": "MIT"
     },
-    "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==",
+    "node_modules/exsolve": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
+      "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">=4"
       }
     },
-    "node_modules/ieee754": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+    "node_modules/fast-content-type-parse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
+      "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
       "dev": true,
       "funding": [
         {
           "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
+          "url": "https://github.com/sponsors/fastify"
         },
         {
-          "type": "consulting",
-          "url": "https://feross.org/support"
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
         }
-      ]
+      ],
+      "license": "MIT"
     },
-    "node_modules/ignore": {
-      "version": "5.2.4",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
-      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
       "engines": {
-        "node": ">= 4"
+        "node": ">=8.6.0"
       }
     },
-    "node_modules/import-fresh": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
-      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/filename-reserved-regex": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/filenamify": {
+      "version": "4.3.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
+        "filename-reserved-regex": "^2.0.0",
+        "strip-outer": "^1.0.1",
+        "trim-repeated": "^1.0.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=8"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/import-lazy": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
-      "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/imurmurhash": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+    "node_modules/finalhandler": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "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.19"
+        "node": ">= 0.8"
       }
     },
-    "node_modules/indent-string": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+    "node_modules/finalhandler/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/finalhandler/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/find-cache-dir": {
+      "version": "3.3.2",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "commondir": "^1.0.1",
+        "make-dir": "^3.0.2",
+        "pkg-dir": "^4.1.0"
+      },
       "engines": {
         "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
       }
     },
-    "node_modules/inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+    "node_modules/find-up": {
+      "version": "5.0.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "once": "^1.3.0",
-        "wrappy": "1"
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+    "node_modules/flat": {
+      "version": "5.0.2",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "bin": {
+        "flat": "cli.js"
+      }
     },
-    "node_modules/ini": {
+    "node_modules/foreground-child": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
-      "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
       "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^3.0.2"
+      },
       "engines": {
-        "node": ">=10"
+        "node": ">=8.0.0"
       }
     },
-    "node_modules/inquirer": {
-      "version": "9.2.10",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.10.tgz",
-      "integrity": "sha512-tVVNFIXU8qNHoULiazz612GFl+yqNfjMTbLuViNJE/d860Qxrd3NMrse8dm40VUQLOQeULvaQF8lpAhvysjeyA==",
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
       "dev": true,
-      "dependencies": {
-        "@ljharb/through": "^2.3.9",
-        "ansi-escapes": "^4.3.2",
-        "chalk": "^5.3.0",
-        "cli-cursor": "^3.1.0",
-        "cli-width": "^4.1.0",
-        "external-editor": "^3.1.0",
-        "figures": "^5.0.0",
-        "lodash": "^4.17.21",
-        "mute-stream": "1.0.0",
-        "ora": "^5.4.1",
-        "run-async": "^3.0.0",
-        "rxjs": "^7.8.1",
-        "string-width": "^4.2.3",
-        "strip-ansi": "^6.0.1",
-        "wrap-ansi": "^6.2.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=14.18.0"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/inquirer/node_modules/bl": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
-      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
       "dev": true,
-      "dependencies": {
-        "buffer": "^5.5.0",
-        "inherits": "^2.0.4",
-        "readable-stream": "^3.4.0"
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
       }
     },
-    "node_modules/inquirer/node_modules/buffer": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+    "node_modules/fromentries": {
+      "version": "1.3.2",
       "dev": true,
       "funding": [
         {
@@ -4274,222 +3536,252 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT"
+    },
+    "node_modules/fs-extra": {
+      "version": "11.2.0",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.1.13"
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
       }
     },
-    "node_modules/inquirer/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=8"
+        "node": ">=6.9.0"
       }
     },
-    "node_modules/inquirer/node_modules/is-interactive": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
-      "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=8"
+        "node": "6.* || 8.* || >= 10.*"
       }
     },
-    "node_modules/inquirer/node_modules/is-unicode-supported": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+    "node_modules/get-east-asian-width": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+      "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/inquirer/node_modules/log-symbols": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+    "node_modules/get-intrinsic": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz",
+      "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "chalk": "^4.1.0",
-        "is-unicode-supported": "^0.1.0"
+        "call-bind-apply-helpers": "^1.0.0",
+        "dunder-proto": "^1.0.0",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-package-type": {
+      "version": "0.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/inquirer/node_modules/log-symbols/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+    "node_modules/get-tsconfig": {
+      "version": "4.7.2",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
+        "resolve-pkg-maps": "^1.0.0"
       },
       "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
       }
     },
-    "node_modules/inquirer/node_modules/ora": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
-      "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+    "node_modules/get-uri": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
+      "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "bl": "^4.1.0",
-        "chalk": "^4.1.0",
-        "cli-cursor": "^3.1.0",
-        "cli-spinners": "^2.5.0",
-        "is-interactive": "^1.0.0",
-        "is-unicode-supported": "^0.1.0",
-        "log-symbols": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "wcwidth": "^1.0.1"
+        "basic-ftp": "^5.0.2",
+        "data-uri-to-buffer": "^6.0.2",
+        "debug": "^4.3.4"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 14"
       }
     },
-    "node_modules/inquirer/node_modules/ora/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+    "node_modules/gh-pages": {
+      "version": "6.1.1",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
+        "async": "^3.2.4",
+        "commander": "^11.0.0",
+        "email-addresses": "^5.0.0",
+        "filenamify": "^4.3.0",
+        "find-cache-dir": "^3.3.1",
+        "fs-extra": "^11.1.1",
+        "globby": "^6.1.0"
+      },
+      "bin": {
+        "gh-pages": "bin/gh-pages.js",
+        "gh-pages-clean": "bin/gh-pages-clean.js"
       },
       "engines": {
         "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
-    "node_modules/inquirer/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+    "node_modules/giget": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
+      "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "has-flag": "^4.0.0"
+        "citty": "^0.1.6",
+        "consola": "^3.4.0",
+        "defu": "^6.1.4",
+        "node-fetch-native": "^1.6.6",
+        "nypm": "^0.6.0",
+        "pathe": "^2.0.3"
       },
-      "engines": {
-        "node": ">=8"
+      "bin": {
+        "giget": "dist/cli.mjs"
       }
     },
-    "node_modules/internal-slot": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
-      "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
+    "node_modules/git-up": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz",
+      "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "get-intrinsic": "^1.2.0",
-        "has": "^1.0.3",
-        "side-channel": "^1.0.4"
-      },
-      "engines": {
-        "node": ">= 0.4"
+        "is-ssh": "^1.4.0",
+        "parse-url": "^9.2.0"
       }
     },
-    "node_modules/interpret": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
-      "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
+    "node_modules/git-url-parse": {
+      "version": "16.1.0",
+      "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz",
+      "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==",
       "dev": true,
-      "engines": {
-        "node": ">= 0.10"
+      "license": "MIT",
+      "dependencies": {
+        "git-up": "^8.1.0"
       }
     },
-    "node_modules/ip": {
-      "version": "1.1.8",
-      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
-      "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==",
-      "dev": true
-    },
-    "node_modules/is-arguments": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
-      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+    "node_modules/glob": {
+      "version": "7.2.3",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": "*"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/is-array-buffer": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
-      "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.0",
-        "is-typed-array": "^1.1.10"
+        "is-glob": "^4.0.1"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "engines": {
+        "node": ">= 6"
       }
     },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
-    },
-    "node_modules/is-bigint": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
-      "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+    "node_modules/globals": {
+      "version": "11.12.0",
       "dev": true,
-      "dependencies": {
-        "has-bigints": "^1.0.1"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
       }
     },
-    "node_modules/is-binary-path": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+    "node_modules/globby": {
+      "version": "6.1.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "binary-extensions": "^2.0.0"
+        "array-union": "^1.0.1",
+        "glob": "^7.0.3",
+        "object-assign": "^4.0.1",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/is-boolean-object": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
-      "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
       "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
-      },
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       },
@@ -4497,50 +3789,38 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/is-callable": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
-      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
       "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
+      "license": "ISC"
     },
-    "node_modules/is-ci": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
-      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+    "node_modules/has-flag": {
+      "version": "4.0.0",
       "dev": true,
-      "dependencies": {
-        "ci-info": "^3.2.0"
-      },
-      "bin": {
-        "is-ci": "bin.js"
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
       }
     },
-    "node_modules/is-core-module": {
-      "version": "2.13.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
-      "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "has": "^1.0.3"
+        "es-define-property": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/is-date-object": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
-      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
       "dev": true,
-      "dependencies": {
-        "has-tostringtag": "^1.0.0"
-      },
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       },
@@ -4548,385 +3828,396 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/is-docker": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
-      "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+    "node_modules/hasha": {
+      "version": "5.2.2",
       "dev": true,
-      "bin": {
-        "is-docker": "cli.js"
+      "license": "MIT",
+      "dependencies": {
+        "is-stream": "^2.0.0",
+        "type-fest": "^0.8.0"
       },
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=8"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
-    "node_modules/is-fullwidth-code-point": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+    "node_modules/he": {
+      "version": "1.2.0",
       "dev": true,
-      "engines": {
-        "node": ">=8"
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
       }
     },
-    "node_modules/is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+    "node_modules/html-escaper": {
+      "version": "2.0.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/htmlparser2": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
+      "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
       "dev": true,
+      "funding": [
+        "https://github.com/fb55/htmlparser2?sponsor=1",
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "MIT",
       "dependencies": {
-        "is-extglob": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.1.0",
+        "entities": "^4.5.0"
       }
     },
-    "node_modules/is-inside-container": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
-      "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "is-docker": "^3.0.0"
-      },
-      "bin": {
-        "is-inside-container": "cli.js"
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
       },
       "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.8"
       }
     },
-    "node_modules/is-installed-globally": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
-      "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
-      "dev": true,
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "license": "MIT",
       "dependencies": {
-        "global-dirs": "^3.0.0",
-        "is-path-inside": "^3.0.2"
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+      "engines": {
+        "node": ">= 14"
       }
     },
-    "node_modules/is-interactive": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
-      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+    "node_modules/human-signals": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
       "dev": true,
+      "license": "Apache-2.0",
       "engines": {
-        "node": ">=12"
+        "node": ">=16.17.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/is-map": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
-      "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
       "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
       }
     },
-    "node_modules/is-negative-zero": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
-      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+    "node_modules/indent-string": {
+      "version": "4.0.0",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=8"
       }
     },
-    "node_modules/is-npm": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz",
-      "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==",
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
+      "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": "^18.17.0 || >=20.5.0"
       }
     },
-    "node_modules/is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+    "node_modules/inquirer": {
+      "version": "12.6.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.6.0.tgz",
+      "integrity": "sha512-3zmmccQd/8o65nPOZJZ+2wqt76Ghw3+LaMrmc6JE/IzcvQhJ1st+QLCOo/iLS85/tILU0myG31a2TAZX0ysAvg==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.1.10",
+        "@inquirer/prompts": "^7.5.0",
+        "@inquirer/type": "^3.0.6",
+        "ansi-escapes": "^4.3.2",
+        "mute-stream": "^2.0.0",
+        "run-async": "^3.0.0",
+        "rxjs": "^7.8.2"
+      },
       "engines": {
-        "node": ">=0.12.0"
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
       }
     },
-    "node_modules/is-number-object": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
-      "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+    "node_modules/ip-address": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+      "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "jsbn": "1.1.0",
+        "sprintf-js": "^1.1.3"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 12"
       }
     },
-    "node_modules/is-obj": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
-      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+    "node_modules/ip-address/node_modules/sprintf-js": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+      "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
       "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
+      "license": "BSD-3-Clause"
     },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=8"
+        "node": ">= 0.10"
       }
     },
-    "node_modules/is-plain-obj": {
+    "node_modules/is-binary-path": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
-      "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/is-plain-object": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
-      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
-      "dev": true,
+    "node_modules/is-docker": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
       "engines": {
-        "node": ">=0.10.0"
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-regex": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
-      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
       "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/is-set": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
-      "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
       "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
       }
     },
-    "node_modules/is-shared-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+    "node_modules/is-glob": {
+      "version": "4.0.3",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2"
+        "is-extglob": "^2.1.1"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/is-ssh": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz",
-      "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==",
-      "dev": true,
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "license": "MIT",
       "dependencies": {
-        "protocols": "^2.0.1"
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-string": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
-      "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
       "dev": true,
-      "dependencies": {
-        "has-tostringtag": "^1.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=0.12.0"
       }
     },
-    "node_modules/is-symbol": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
-      "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+    "node_modules/is-plain-obj": {
+      "version": "2.1.0",
       "dev": true,
-      "dependencies": {
-        "has-symbols": "^1.0.2"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=8"
       }
     },
-    "node_modules/is-typed-array": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
-      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+    "node_modules/is-ssh": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz",
+      "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "which-typed-array": "^1.1.11"
-      },
+        "protocols": "^2.0.1"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=8"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/is-typedarray": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/is-unicode-supported": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
-      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+      "version": "0.1.0",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=12"
+        "node": ">=10"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-weakref": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
-      "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
     "node_modules/is-windows": {
       "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
-      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/is-wsl": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
-      "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
-      "dev": true,
+      "version": "3.1.0",
+      "license": "MIT",
       "dependencies": {
-        "is-docker": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-wsl/node_modules/is-docker": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
-      "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
-      "dev": true,
-      "bin": {
-        "is-docker": "cli.js"
+        "is-inside-container": "^1.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=16"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-yarn-global": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz",
-      "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/isarray": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
-      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
-      "dev": true
-    },
     "node_modules/isexe": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/issue-parser": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz",
-      "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==",
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz",
+      "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "lodash.capitalize": "^4.2.1",
         "lodash.escaperegexp": "^4.1.2",
@@ -4935,23 +4226,21 @@
         "lodash.uniqby": "^4.7.0"
       },
       "engines": {
-        "node": ">=10.13"
+        "node": "^18.17 || >=20.6.1"
       }
     },
     "node_modules/istanbul-lib-coverage": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
-      "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+      "version": "3.2.2",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/istanbul-lib-hook": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz",
-      "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "append-transform": "^2.0.0"
       },
@@ -4961,9 +4250,8 @@
     },
     "node_modules/istanbul-lib-instrument": {
       "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz",
-      "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@babel/core": "^7.7.5",
         "@istanbuljs/schema": "^0.1.2",
@@ -4976,9 +4264,8 @@
     },
     "node_modules/istanbul-lib-processinfo": {
       "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
-      "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "archy": "^1.0.0",
         "cross-spawn": "^7.0.3",
@@ -4993,9 +4280,8 @@
     },
     "node_modules/istanbul-lib-report": {
       "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
-      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "istanbul-lib-coverage": "^3.0.0",
         "make-dir": "^4.0.0",
@@ -5005,20 +4291,10 @@
         "node": ">=10"
       }
     },
-    "node_modules/istanbul-lib-report/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/istanbul-lib-report/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==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "yallist": "^4.0.0"
       },
@@ -5028,9 +4304,8 @@
     },
     "node_modules/istanbul-lib-report/node_modules/make-dir": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
-      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "semver": "^7.5.3"
       },
@@ -5043,9 +4318,8 @@
     },
     "node_modules/istanbul-lib-report/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==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "lru-cache": "^6.0.0"
       },
@@ -5058,9 +4332,8 @@
     },
     "node_modules/istanbul-lib-report/node_modules/supports-color": {
       "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -5068,11 +4341,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/istanbul-lib-report/node_modules/yallist": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/istanbul-lib-source-maps": {
       "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
-      "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "debug": "^4.1.1",
         "istanbul-lib-coverage": "^3.0.0",
@@ -5084,9 +4361,8 @@
     },
     "node_modules/istanbul-reports": {
       "version": "3.1.6",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
-      "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "html-escaper": "^2.0.0",
         "istanbul-lib-report": "^3.0.0"
@@ -5095,39 +4371,25 @@
         "node": ">=8"
       }
     },
-    "node_modules/iterate-iterator": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz",
-      "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/iterate-value": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
-      "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
+    "node_modules/jiti": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
+      "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
       "dev": true,
-      "dependencies": {
-        "es-get-iterator": "^1.0.2",
-        "iterate-iterator": "^1.0.1"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
       }
     },
     "node_modules/js-tokens": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/js-yaml": {
       "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "argparse": "^2.0.1"
       },
@@ -5135,11 +4397,17 @@
         "js-yaml": "bin/js-yaml.js"
       }
     },
+    "node_modules/jsbn": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+      "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/jsesc": {
       "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "jsesc": "bin/jsesc"
       },
@@ -5147,35 +4415,10 @@
         "node": ">=4"
       }
     },
-    "node_modules/json-buffer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
-      "dev": true
-    },
-    "node_modules/json-parse-even-better-errors": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
-      "dev": true
-    },
-    "node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
-    },
-    "node_modules/json-stable-stringify-without-jsonify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-      "dev": true
-    },
     "node_modules/json5": {
       "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "json5": "lib/cli.js"
       },
@@ -5184,16 +4427,14 @@
       }
     },
     "node_modules/jsonc-parser": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
-      "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
-      "dev": true
+      "version": "3.2.1",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/jsonfile": {
       "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "universalify": "^2.0.0"
       },
@@ -5201,169 +4442,204 @@
         "graceful-fs": "^4.1.6"
       }
     },
-    "node_modules/just-extend": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
-      "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
-      "dev": true
-    },
-    "node_modules/keyv": {
-      "version": "4.5.3",
-      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
-      "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==",
-      "dev": true,
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+      "license": "MIT",
       "dependencies": {
-        "json-buffer": "3.0.1"
+        "jws": "^3.2.2",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
       }
     },
-    "node_modules/latest-version": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz",
-      "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==",
-      "dev": true,
-      "dependencies": {
-        "package-json": "^8.1.0"
+    "node_modules/jsonwebtoken/node_modules/semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
       },
       "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=10"
       }
     },
-    "node_modules/levn": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
-      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+    "node_modules/just-extend": {
+      "version": "6.2.0",
       "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jwa": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+      "license": "MIT",
       "dependencies": {
-        "prelude-ls": "^1.2.1",
-        "type-check": "~0.4.0"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
       }
     },
-    "node_modules/lines-and-columns": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
-      "dev": true
+    "node_modules/jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
     },
     "node_modules/locate-path": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "version": "6.0.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "p-locate": "^4.1.0"
+        "p-locate": "^5.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/lodash.capitalize": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz",
       "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/lodash.escaperegexp": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
       "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/lodash.flattendeep": {
       "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
-      "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/lodash.get": {
       "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "license": "MIT"
     },
     "node_modules/lodash.isplainobject": {
       "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
-      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
-      "dev": true
+      "license": "MIT"
     },
     "node_modules/lodash.isstring": {
       "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
-      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
-      "dev": true
+      "license": "MIT"
     },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+      "license": "MIT"
     },
     "node_modules/lodash.uniqby": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz",
       "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/log-symbols": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
-      "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
+      "version": "4.1.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "chalk": "^5.0.0",
-        "is-unicode-supported": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=12"
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/lowercase-keys": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
-      "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
-      "dev": true,
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=10"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/lru-cache": {
-      "version": "7.18.3",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
-      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "version": "5.1.1",
       "dev": true,
-      "engines": {
-        "node": ">=12"
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
       }
     },
     "node_modules/lunr": {
       "version": "2.3.9",
-      "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
-      "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/macos-release": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.2.0.tgz",
-      "integrity": "sha512-fSErXALFNsnowREYZ49XCdOHF8wOPWuFOGQrAhP7x5J/BqQv+B02cNsTykGpDgRVx43EKg++6ANmTaGTtW+hUA==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.3.0.tgz",
+      "integrity": "sha512-tPJQ1HeyiU2vRruNGhZ+VleWuMQRro8iFtJxYgnS4NQe+EukKF6aGiIT+7flZhISAt2iaXBCfFGvAyif7/f8nQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
       },
@@ -5373,9 +4649,8 @@
     },
     "node_modules/make-dir": {
       "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "semver": "^6.0.0"
       },
@@ -5390,13 +4665,13 @@
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
       "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/marked": {
       "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
-      "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "marked": "bin/marked.js"
       },
@@ -5404,439 +4679,242 @@
         "node": ">= 12"
       }
     },
-    "node_modules/merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
-    "node_modules/merge2": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
-      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
-      "dev": true,
-      "dependencies": {
-        "braces": "^3.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=8.6"
-      }
-    },
-    "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==",
-      "dev": true,
-      "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==",
+    "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==",
       "dev": true,
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
+      "license": "MIT",
       "engines": {
         "node": ">= 0.6"
       }
     },
-    "node_modules/mimic-fn": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/mimic-response": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
-      "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/minimist": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/mocha": {
-      "version": "10.2.0",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
-      "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
-      "dev": true,
-      "dependencies": {
-        "ansi-colors": "4.1.1",
-        "browser-stdout": "1.3.1",
-        "chokidar": "3.5.3",
-        "debug": "4.3.4",
-        "diff": "5.0.0",
-        "escape-string-regexp": "4.0.0",
-        "find-up": "5.0.0",
-        "glob": "7.2.0",
-        "he": "1.2.0",
-        "js-yaml": "4.1.0",
-        "log-symbols": "4.1.0",
-        "minimatch": "5.0.1",
-        "ms": "2.1.3",
-        "nanoid": "3.3.3",
-        "serialize-javascript": "6.0.0",
-        "strip-json-comments": "3.1.1",
-        "supports-color": "8.1.1",
-        "workerpool": "6.2.1",
-        "yargs": "16.2.0",
-        "yargs-parser": "20.2.4",
-        "yargs-unparser": "2.0.0"
-      },
-      "bin": {
-        "_mocha": "bin/_mocha",
-        "mocha": "bin/mocha.js"
-      },
-      "engines": {
-        "node": ">= 14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mochajs"
-      }
-    },
-    "node_modules/mocha/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/mocha/node_modules/chalk/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/mocha/node_modules/cliui": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
-      "dev": true,
-      "dependencies": {
-        "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.0",
-        "wrap-ansi": "^7.0.0"
-      }
-    },
-    "node_modules/mocha/node_modules/escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/mocha/node_modules/find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
       "dev": true,
-      "dependencies": {
-        "locate-path": "^6.0.0",
-        "path-exists": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
+      "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/mocha/node_modules/glob": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
-      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
-      "dev": true,
-      "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.0.4",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      },
-      "engines": {
-        "node": "*"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
       "dev": true,
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
-      }
+      "license": "MIT"
     },
-    "node_modules/mocha/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=8"
+        "node": ">= 8"
       }
     },
-    "node_modules/mocha/node_modules/is-unicode-supported": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/mocha/node_modules/locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "p-locate": "^5.0.0"
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=8.6"
       }
     },
-    "node_modules/mocha/node_modules/log-symbols": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
       "dev": true,
-      "dependencies": {
-        "chalk": "^4.1.0",
-        "is-unicode-supported": "^0.1.0"
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=4"
       }
     },
-    "node_modules/mocha/node_modules/minimatch": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
-      "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+    "node_modules/mime-db": {
+      "version": "1.52.0",
       "dev": true,
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+    "node_modules/mime-types": {
+      "version": "2.1.35",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "balanced-match": "^1.0.0"
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
       }
     },
-    "node_modules/mocha/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==",
-      "dev": true
-    },
-    "node_modules/mocha/node_modules/p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+    "node_modules/mimic-fn": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
       "dev": true,
-      "dependencies": {
-        "yocto-queue": "^0.1.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/mocha/node_modules/p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
       "dev": true,
-      "dependencies": {
-        "p-limit": "^3.0.2"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/mocha/node_modules/strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+    "node_modules/minimatch": {
+      "version": "3.1.2",
       "dev": true,
-      "engines": {
-        "node": ">=8"
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+      "engines": {
+        "node": "*"
       }
     },
-    "node_modules/mocha/node_modules/supports-color": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+    "node_modules/mocha": {
+      "version": "10.2.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "has-flag": "^4.0.0"
+        "ansi-colors": "4.1.1",
+        "browser-stdout": "1.3.1",
+        "chokidar": "3.5.3",
+        "debug": "4.3.4",
+        "diff": "5.0.0",
+        "escape-string-regexp": "4.0.0",
+        "find-up": "5.0.0",
+        "glob": "7.2.0",
+        "he": "1.2.0",
+        "js-yaml": "4.1.0",
+        "log-symbols": "4.1.0",
+        "minimatch": "5.0.1",
+        "ms": "2.1.3",
+        "nanoid": "3.3.3",
+        "serialize-javascript": "6.0.0",
+        "strip-json-comments": "3.1.1",
+        "supports-color": "8.1.1",
+        "workerpool": "6.2.1",
+        "yargs": "16.2.0",
+        "yargs-parser": "20.2.4",
+        "yargs-unparser": "2.0.0"
+      },
+      "bin": {
+        "_mocha": "bin/_mocha",
+        "mocha": "bin/mocha.js"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 14.0.0"
       },
       "funding": {
-        "url": "https://github.com/chalk/supports-color?sponsor=1"
+        "type": "opencollective",
+        "url": "https://opencollective.com/mochajs"
       }
     },
-    "node_modules/mocha/node_modules/wrap-ansi": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+    "node_modules/mocha/node_modules/glob": {
+      "version": "7.2.0",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": "*"
       },
       "funding": {
-        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/mocha/node_modules/yargs": {
-      "version": "16.2.0",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
-      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+    "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "cliui": "^7.0.2",
-        "escalade": "^3.1.1",
-        "get-caller-file": "^2.0.5",
-        "require-directory": "^2.1.1",
-        "string-width": "^4.2.0",
-        "y18n": "^5.0.5",
-        "yargs-parser": "^20.2.2"
+        "brace-expansion": "^1.1.7"
       },
       "engines": {
-        "node": ">=10"
+        "node": "*"
       }
     },
-    "node_modules/mocha/node_modules/yargs-parser": {
-      "version": "20.2.4",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
-      "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+    "node_modules/mocha/node_modules/minimatch": {
+      "version": "5.0.1",
       "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
       "engines": {
         "node": ">=10"
       }
     },
+    "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
     "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==",
-      "dev": true
+      "version": "2.1.3",
+      "license": "MIT"
     },
     "node_modules/mute-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
-      "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+      "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+        "node": "^18.17.0 || >=20.5.0"
       }
     },
     "node_modules/nanoid": {
       "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
-      "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
@@ -5844,17 +4922,22 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
-    "node_modules/natural-compare": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-      "dev": true
+    "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==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
     },
     "node_modules/netmask": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
       "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4.0"
       }
@@ -5864,6 +4947,7 @@
       "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-2.0.0.tgz",
       "integrity": "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "type-fest": "^2.5.1"
       },
@@ -5879,6 +4963,7 @@
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
       "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
       "dev": true,
+      "license": "(MIT OR CC0-1.0)",
       "engines": {
         "node": ">=12.20"
       },
@@ -5887,69 +4972,28 @@
       }
     },
     "node_modules/nise": {
-      "version": "5.1.4",
-      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz",
-      "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==",
-      "dev": true,
-      "dependencies": {
-        "@sinonjs/commons": "^2.0.0",
-        "@sinonjs/fake-timers": "^10.0.2",
-        "@sinonjs/text-encoding": "^0.7.1",
-        "just-extend": "^4.0.2",
-        "path-to-regexp": "^1.7.0"
-      }
-    },
-    "node_modules/nise/node_modules/@sinonjs/commons": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
-      "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+      "version": "5.1.9",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
-        "type-detect": "4.0.8"
-      }
-    },
-    "node_modules/node-domexception": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
-      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/jimmywarting"
-        },
-        {
-          "type": "github",
-          "url": "https://paypal.me/jimmywarting"
-        }
-      ],
-      "engines": {
-        "node": ">=10.5.0"
+        "@sinonjs/commons": "^3.0.0",
+        "@sinonjs/fake-timers": "^11.2.2",
+        "@sinonjs/text-encoding": "^0.7.2",
+        "just-extend": "^6.2.0",
+        "path-to-regexp": "^6.2.1"
       }
     },
-    "node_modules/node-fetch": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
-      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+    "node_modules/node-fetch-native": {
+      "version": "1.6.6",
+      "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
+      "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==",
       "dev": true,
-      "dependencies": {
-        "data-uri-to-buffer": "^4.0.0",
-        "fetch-blob": "^3.1.4",
-        "formdata-polyfill": "^4.0.10"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/node-fetch"
-      }
+      "license": "MIT"
     },
     "node_modules/node-preload": {
       "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
-      "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "process-on-spawn": "^1.0.0"
       },
@@ -5958,37 +5002,24 @@
       }
     },
     "node_modules/node-releases": {
-      "version": "2.0.13",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
-      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
-      "dev": true
+      "version": "2.0.14",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/normalize-path": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
-    "node_modules/normalize-url": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz",
-      "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==",
-      "dev": true,
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/npm-run-path": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
-      "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^4.0.0"
       },
@@ -6004,6 +5035,7 @@
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
       "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
@@ -6011,11 +5043,23 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
     "node_modules/nyc": {
       "version": "15.1.0",
-      "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
-      "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "@istanbuljs/load-nyc-config": "^1.0.0",
         "@istanbuljs/schema": "^0.1.2",
@@ -6052,46 +5096,73 @@
         "node": ">=8.9"
       }
     },
-    "node_modules/nyc/node_modules/camelcase": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/nyc/node_modules/cliui": {
       "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
-      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "string-width": "^4.2.0",
         "strip-ansi": "^6.0.0",
         "wrap-ansi": "^6.2.0"
       }
     },
-    "node_modules/nyc/node_modules/resolve-from": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+    "node_modules/nyc/node_modules/find-up": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/nyc/node_modules/locate-path": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/nyc/node_modules/p-limit": {
+      "version": "2.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/nyc/node_modules/p-locate": {
+      "version": "4.1.0",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/nyc/node_modules/y18n": {
       "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
-      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/nyc/node_modules/yargs": {
       "version": "15.4.1",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
-      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "cliui": "^6.0.0",
         "decamelize": "^1.2.0",
@@ -6111,9 +5182,8 @@
     },
     "node_modules/nyc/node_modules/yargs-parser": {
       "version": "18.1.3",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
-      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "camelcase": "^5.0.0",
         "decamelize": "^1.2.0"
@@ -6122,138 +5192,151 @@
         "node": ">=6"
       }
     },
+    "node_modules/nypm": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz",
+      "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "citty": "^0.1.6",
+        "consola": "^3.4.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^2.0.0",
+        "tinyexec": "^0.3.2"
+      },
+      "bin": {
+        "nypm": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": "^14.16.0 || >=16.10.0"
+      }
+    },
+    "node_modules/nypm/node_modules/tinyexec": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+      "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/object-assign": {
       "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "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==",
+      "version": "1.13.1",
       "dev": true,
+      "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/object-keys": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+    "node_modules/ohash": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+      "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
       "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      }
+      "license": "MIT"
     },
-    "node_modules/object.assign": {
-      "version": "4.1.4",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
-      "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "has-symbols": "^1.0.3",
-        "object-keys": "^1.1.1"
+        "ee-first": "1.1.1"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "dev": true,
+      "license": "MIT",
+      "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==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "wrappy": "1"
       }
     },
     "node_modules/onetime": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "mimic-fn": "^4.0.0"
+        "mimic-function": "^5.0.0"
       },
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/open": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
-      "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
-      "dev": true,
+      "version": "10.1.2",
+      "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
+      "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
+      "license": "MIT",
       "dependencies": {
-        "default-browser": "^4.0.0",
+        "default-browser": "^5.2.1",
         "define-lazy-prop": "^3.0.0",
         "is-inside-container": "^1.0.0",
-        "is-wsl": "^2.2.0"
+        "is-wsl": "^3.1.0"
       },
       "engines": {
-        "node": ">=14.16"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/optionator": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
-      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
-      "dev": true,
-      "dependencies": {
-        "@aashutoshrathi/word-wrap": "^1.2.3",
-        "deep-is": "^0.1.3",
-        "fast-levenshtein": "^2.0.6",
-        "levn": "^0.4.1",
-        "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
     "node_modules/ora": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz",
-      "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
+      "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "chalk": "^5.3.0",
-        "cli-cursor": "^4.0.0",
-        "cli-spinners": "^2.9.0",
+        "cli-cursor": "^5.0.0",
+        "cli-spinners": "^2.9.2",
         "is-interactive": "^2.0.0",
-        "is-unicode-supported": "^1.3.0",
-        "log-symbols": "^5.1.0",
-        "stdin-discarder": "^0.1.0",
-        "string-width": "^6.1.0",
+        "is-unicode-supported": "^2.0.0",
+        "log-symbols": "^6.0.0",
+        "stdin-discarder": "^0.2.2",
+        "string-width": "^7.2.0",
         "strip-ansi": "^7.1.0"
       },
       "engines": {
-        "node": ">=16"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/ora/node_modules/ansi-regex": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
@@ -6261,79 +5344,82 @@
         "url": "https://github.com/chalk/ansi-regex?sponsor=1"
       }
     },
-    "node_modules/ora/node_modules/cli-cursor": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
-      "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+    "node_modules/ora/node_modules/chalk": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+      "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
       "dev": true,
-      "dependencies": {
-        "restore-cursor": "^4.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
     "node_modules/ora/node_modules/emoji-regex": {
-      "version": "10.2.1",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz",
-      "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==",
-      "dev": true
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+      "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/ora/node_modules/mimic-fn": {
+    "node_modules/ora/node_modules/is-unicode-supported": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=6"
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/ora/node_modules/onetime": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+    "node_modules/ora/node_modules/log-symbols": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+      "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "mimic-fn": "^2.1.0"
+        "chalk": "^5.3.0",
+        "is-unicode-supported": "^1.3.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/ora/node_modules/restore-cursor": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
-      "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+    "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
       "dev": true,
-      "dependencies": {
-        "onetime": "^5.1.0",
-        "signal-exit": "^3.0.2"
-      },
+      "license": "MIT",
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/ora/node_modules/string-width": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz",
-      "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "eastasianwidth": "^0.2.0",
-        "emoji-regex": "^10.2.1",
-        "strip-ansi": "^7.0.1"
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
       },
       "engines": {
-        "node": ">=16"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -6344,6 +5430,7 @@
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
       "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-regex": "^6.0.1"
       },
@@ -6355,16 +5442,17 @@
       }
     },
     "node_modules/os-name": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/os-name/-/os-name-5.1.0.tgz",
-      "integrity": "sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.0.0.tgz",
+      "integrity": "sha512-bv608E0UX86atYi2GMGjDe0vF/X1TJjemNS8oEW6z22YW1Rc3QykSYoGfkQbX0zZX9H0ZB6CQP/3GTf1I5hURg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "macos-release": "^3.1.0",
-        "windows-release": "^5.0.1"
+        "macos-release": "^3.2.0",
+        "windows-release": "^6.0.0"
       },
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -6375,51 +5463,43 @@
       "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
       "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
-    "node_modules/p-cancelable": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
-      "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.20"
-      }
-    },
     "node_modules/p-limit": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "version": "3.1.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "p-try": "^2.0.0"
+        "yocto-queue": "^0.1.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=10"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/p-locate": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "version": "5.0.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "p-limit": "^2.2.0"
+        "p-limit": "^3.0.2"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/p-map": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
-      "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "aggregate-error": "^3.0.0"
       },
@@ -6429,40 +5509,40 @@
     },
     "node_modules/p-try": {
       "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
     },
     "node_modules/pac-proxy-agent": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz",
-      "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+      "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@tootallnate/quickjs-emscripten": "^0.23.0",
-        "agent-base": "^7.0.2",
+        "agent-base": "^7.1.2",
         "debug": "^4.3.4",
         "get-uri": "^6.0.1",
         "http-proxy-agent": "^7.0.0",
-        "https-proxy-agent": "^7.0.2",
-        "pac-resolver": "^7.0.0",
-        "socks-proxy-agent": "^8.0.2"
+        "https-proxy-agent": "^7.0.6",
+        "pac-resolver": "^7.0.1",
+        "socks-proxy-agent": "^8.0.5"
       },
       "engines": {
         "node": ">= 14"
       }
     },
     "node_modules/pac-resolver": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz",
-      "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==",
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+      "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "degenerator": "^5.0.0",
-        "ip": "^1.1.8",
         "netmask": "^2.0.2"
       },
       "engines": {
@@ -6471,9 +5551,8 @@
     },
     "node_modules/package-hash": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz",
-      "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "graceful-fs": "^4.1.15",
         "hasha": "^5.0.0",
@@ -6484,192 +5563,145 @@
         "node": ">=8"
       }
     },
-    "node_modules/package-json": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz",
-      "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==",
+    "node_modules/parse-path": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz",
+      "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "got": "^12.1.0",
-        "registry-auth-token": "^5.0.1",
-        "registry-url": "^6.0.0",
-        "semver": "^7.3.7"
-      },
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "protocols": "^2.0.0"
       }
     },
-    "node_modules/package-json/node_modules/got": {
-      "version": "12.6.1",
-      "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz",
-      "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==",
+    "node_modules/parse-url": {
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz",
+      "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@sindresorhus/is": "^5.2.0",
-        "@szmarczak/http-timer": "^5.0.1",
-        "cacheable-lookup": "^7.0.0",
-        "cacheable-request": "^10.2.8",
-        "decompress-response": "^6.0.0",
-        "form-data-encoder": "^2.1.2",
-        "get-stream": "^6.0.1",
-        "http2-wrapper": "^2.1.10",
-        "lowercase-keys": "^3.0.0",
-        "p-cancelable": "^3.0.0",
-        "responselike": "^3.0.0"
+        "@types/parse-path": "^7.0.0",
+        "parse-path": "^7.0.0"
       },
       "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/got?sponsor=1"
+        "node": ">=14.13.0"
       }
     },
-    "node_modules/package-json/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==",
+    "node_modules/parse5": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+      "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "yallist": "^4.0.0"
+        "entities": "^6.0.0"
       },
-      "engines": {
-        "node": ">=10"
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
       }
     },
-    "node_modules/package-json/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==",
+    "node_modules/parse5-htmlparser2-tree-adapter": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+      "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
+        "domhandler": "^5.0.3",
+        "parse5": "^7.0.0"
       },
-      "engines": {
-        "node": ">=10"
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
       }
     },
-    "node_modules/parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+    "node_modules/parse5-parser-stream": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+      "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "callsites": "^3.0.0"
+        "parse5": "^7.0.0"
       },
-      "engines": {
-        "node": ">=6"
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
       }
     },
-    "node_modules/parse-json": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
-      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+    "node_modules/parse5/node_modules/entities": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
+      "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
       "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.0.0",
-        "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
-        "lines-and-columns": "^1.1.6"
-      },
+      "license": "BSD-2-Clause",
       "engines": {
-        "node": ">=8"
+        "node": ">=0.12"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/parse-path": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz",
-      "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==",
-      "dev": true,
-      "dependencies": {
-        "protocols": "^2.0.0"
+        "url": "https://github.com/fb55/entities?sponsor=1"
       }
     },
-    "node_modules/parse-url": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz",
-      "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==",
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
       "dev": true,
-      "dependencies": {
-        "parse-path": "^7.0.0"
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
       }
     },
     "node_modules/path-exists": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/path-is-absolute": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/path-key": {
       "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
-    },
     "node_modules/path-to-regexp": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
-      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+      "version": "6.2.1",
       "dev": true,
-      "dependencies": {
-        "isarray": "0.0.1"
-      }
+      "license": "MIT"
     },
-    "node_modules/path-to-regexp/node_modules/isarray": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
-      "dev": true
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
       "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
+      "license": "MIT"
     },
     "node_modules/picocolors": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/picomatch": {
       "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8.6"
       },
@@ -6679,60 +5711,106 @@
     },
     "node_modules/pify": {
       "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/pinkie": {
       "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
-      "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/pinkie-promise": {
       "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
-      "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pinkie": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/find-up": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/locate-path": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/p-limit": {
+      "version": "2.3.0",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "pinkie": "^2.0.0"
+        "p-try": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/pkg-dir": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
-      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+    "node_modules/pkg-dir/node_modules/p-locate": {
+      "version": "4.1.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "find-up": "^4.0.0"
+        "p-limit": "^2.2.0"
       },
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/prelude-ls": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
-      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+    "node_modules/pkg-types": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
+      "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
       "dev": true,
-      "engines": {
-        "node": ">= 0.8.0"
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.1",
+        "exsolve": "^1.0.1",
+        "pathe": "^2.0.3"
       }
     },
     "node_modules/process-on-spawn": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
-      "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "fromentries": "^1.2.0"
       },
@@ -6740,85 +5818,78 @@
         "node": ">=8"
       }
     },
-    "node_modules/promise.allsettled": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz",
-      "integrity": "sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==",
+    "node_modules/protocols": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz",
+      "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==",
       "dev": true,
+      "license": "MIT"
+    },
+    "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==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "array.prototype.map": "^1.0.5",
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4",
-        "get-intrinsic": "^1.1.3",
-        "iterate-value": "^1.0.2"
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 0.10"
       }
     },
-    "node_modules/proto-list": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
-      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
-      "dev": true
-    },
-    "node_modules/protocols": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz",
-      "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==",
-      "dev": true
-    },
     "node_modules/proxy-agent": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz",
-      "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==",
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
+      "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "agent-base": "^7.0.2",
+        "agent-base": "^7.1.2",
         "debug": "^4.3.4",
-        "http-proxy-agent": "^7.0.0",
-        "https-proxy-agent": "^7.0.0",
+        "http-proxy-agent": "^7.0.1",
+        "https-proxy-agent": "^7.0.6",
         "lru-cache": "^7.14.1",
-        "pac-proxy-agent": "^7.0.0",
+        "pac-proxy-agent": "^7.1.0",
         "proxy-from-env": "^1.1.0",
-        "socks-proxy-agent": "^8.0.1"
+        "socks-proxy-agent": "^8.0.5"
       },
       "engines": {
         "node": ">= 14"
       }
     },
+    "node_modules/proxy-agent/node_modules/lru-cache": {
+      "version": "7.18.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-      "dev": true
-    },
-    "node_modules/punycode": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
-      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
       "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
+      "license": "MIT"
     },
-    "node_modules/pupa": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
-      "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==",
+    "node_modules/qs": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
-        "escape-goat": "^4.0.0"
+        "side-channel": "^1.0.6"
       },
       "engines": {
-        "node": ">=12.20"
+        "node": ">=0.6"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/queue-microtask": {
@@ -6839,212 +5910,158 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ]
+      ],
+      "license": "MIT"
     },
-    "node_modules/quick-lru": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
-      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.8"
       }
     },
     "node_modules/randombytes": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
-      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "safe-buffer": "^5.1.0"
       }
     },
-    "node_modules/rc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
-      "dev": true,
-      "dependencies": {
-        "deep-extend": "^0.6.0",
-        "ini": "~1.3.0",
-        "minimist": "^1.2.0",
-        "strip-json-comments": "~2.0.1"
-      },
-      "bin": {
-        "rc": "cli.js"
-      }
-    },
-    "node_modules/rc/node_modules/ini": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-      "dev": true
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "inherits": "^2.0.3",
-        "string_decoder": "^1.1.1",
-        "util-deprecate": "^1.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/readdirp": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+    "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==",
       "dev": true,
-      "dependencies": {
-        "picomatch": "^2.2.1"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=8.10.0"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/rechoir": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
-      "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+    "node_modules/raw-body": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "resolve": "^1.1.6"
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
       },
       "engines": {
-        "node": ">= 0.10"
+        "node": ">= 0.8"
       }
     },
-    "node_modules/regexp.prototype.flags": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
-      "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+    "node_modules/rc9": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
+      "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "set-function-name": "^2.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "defu": "^6.1.4",
+        "destr": "^2.0.3"
       }
     },
-    "node_modules/registry-auth-token": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz",
-      "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==",
+    "node_modules/readdirp": {
+      "version": "3.6.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@pnpm/npm-conf": "^2.1.0"
+        "picomatch": "^2.2.1"
       },
       "engines": {
-        "node": ">=14"
+        "node": ">=8.10.0"
       }
     },
-    "node_modules/registry-url": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz",
-      "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==",
-      "dev": true,
-      "dependencies": {
-        "rc": "1.2.8"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
+    "node_modules/redis": {
+      "resolved": "packages/redis",
+      "link": true
     },
     "node_modules/release-it": {
-      "version": "16.1.5",
-      "resolved": "https://registry.npmjs.org/release-it/-/release-it-16.1.5.tgz",
-      "integrity": "sha512-w/zCljPZBSYcCwR9fjDB1zaYwie1CAQganUrwNqjtXacXhrrsS5E6dDUNLcxm2ypu8GWAgZNMJfuBJqIO2E7fA==",
+      "version": "19.0.2",
+      "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.0.2.tgz",
+      "integrity": "sha512-tGRCcKeXNOMrK9Qe+ZIgQiMlQgjV8PLxZjTq1XGlCk5u1qPgx+Pps0i8HIt667FDt0wLjFtvn5o9ItpitKnVUA==",
       "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/webpro"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/webpro"
+        }
+      ],
+      "license": "MIT",
       "dependencies": {
-        "@iarna/toml": "2.2.5",
-        "@octokit/rest": "19.0.13",
+        "@nodeutils/defaults-deep": "1.1.0",
+        "@octokit/rest": "21.1.1",
+        "@phun-ky/typeof": "1.2.8",
         "async-retry": "1.3.3",
-        "chalk": "5.3.0",
-        "cosmiconfig": "8.2.0",
-        "execa": "7.2.0",
-        "git-url-parse": "13.1.0",
-        "globby": "13.2.2",
-        "got": "13.0.0",
-        "inquirer": "9.2.10",
-        "is-ci": "3.0.1",
-        "issue-parser": "6.0.0",
-        "lodash": "4.17.21",
-        "mime-types": "2.1.35",
+        "c12": "3.0.3",
+        "ci-info": "^4.2.0",
+        "eta": "3.5.0",
+        "git-url-parse": "16.1.0",
+        "inquirer": "12.6.0",
+        "issue-parser": "7.0.1",
+        "lodash.get": "4.4.2",
+        "lodash.merge": "4.6.2",
+        "mime-types": "3.0.1",
         "new-github-release-url": "2.0.0",
-        "node-fetch": "3.3.2",
-        "open": "9.1.0",
-        "ora": "7.0.1",
-        "os-name": "5.1.0",
-        "promise.allsettled": "1.0.6",
-        "proxy-agent": "6.3.0",
-        "semver": "7.5.4",
-        "shelljs": "0.8.5",
-        "update-notifier": "6.0.2",
+        "open": "10.1.2",
+        "ora": "8.2.0",
+        "os-name": "6.0.0",
+        "proxy-agent": "6.5.0",
+        "semver": "7.7.1",
+        "tinyexec": "1.0.1",
+        "tinyglobby": "0.2.13",
+        "undici": "6.21.2",
         "url-join": "5.0.0",
-        "wildcard-match": "5.1.2",
+        "wildcard-match": "5.1.4",
         "yargs-parser": "21.1.1"
       },
       "bin": {
         "release-it": "bin/release-it.js"
       },
       "engines": {
-        "node": ">=16"
+        "node": "^20.12.0 || >=22.0.0"
       }
     },
-    "node_modules/release-it/node_modules/globby": {
-      "version": "13.2.2",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz",
-      "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==",
+    "node_modules/release-it/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
       "dev": true,
-      "dependencies": {
-        "dir-glob": "^3.0.1",
-        "fast-glob": "^3.3.0",
-        "ignore": "^5.2.4",
-        "merge2": "^1.4.1",
-        "slash": "^4.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.6"
       }
     },
-    "node_modules/release-it/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==",
+    "node_modules/release-it/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "yallist": "^4.0.0"
+        "mime-db": "^1.54.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.6"
       }
     },
     "node_modules/release-it/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==",
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
       "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -7052,11 +6069,30 @@
         "node": ">=10"
       }
     },
+    "node_modules/release-it/node_modules/undici": {
+      "version": "6.21.2",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz",
+      "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.17"
+      }
+    },
+    "node_modules/release-it/node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/release-zalgo": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
-      "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "es6-error": "^4.0.1"
       },
@@ -7066,239 +6102,106 @@
     },
     "node_modules/require-directory": {
       "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/require-main-filename": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-      "dev": true
-    },
-    "node_modules/resolve": {
-      "version": "1.22.6",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
-      "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
       "dev": true,
-      "dependencies": {
-        "is-core-module": "^2.13.0",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
-      },
-      "bin": {
-        "resolve": "bin/resolve"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/resolve-alpn": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
-      "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
-      "dev": true
+      "license": "ISC"
     },
     "node_modules/resolve-from": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/responselike": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
-      "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
-      "dev": true,
-      "dependencies": {
-        "lowercase-keys": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/restore-cursor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
-      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "version": "5.0.0",
       "dev": true,
-      "dependencies": {
-        "onetime": "^5.1.0",
-        "signal-exit": "^3.0.2"
-      },
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/restore-cursor/node_modules/mimic-fn": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/restore-cursor/node_modules/onetime": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
-      "dev": true,
-      "dependencies": {
-        "mimic-fn": "^2.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/retry": {
-      "version": "0.13.1",
-      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
-      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/reusify": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
-      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-      "dev": true,
-      "engines": {
-        "iojs": ">=1.0.0",
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/rimraf": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-      "dev": true,
-      "dependencies": {
-        "glob": "^7.1.3"
-      },
-      "bin": {
-        "rimraf": "bin.js"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/run-applescript": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
-      "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
       "dev": true,
-      "dependencies": {
-        "execa": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
+      "license": "MIT",
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
       }
     },
-    "node_modules/run-applescript/node_modules/execa": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
-      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^6.0.0",
-        "human-signals": "^2.1.0",
-        "is-stream": "^2.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^4.0.1",
-        "onetime": "^5.1.2",
-        "signal-exit": "^3.0.3",
-        "strip-final-newline": "^2.0.0"
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=18"
       },
       "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
-      }
-    },
-    "node_modules/run-applescript/node_modules/human-signals": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
-      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.17.0"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/run-applescript/node_modules/is-stream": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+    "node_modules/restore-cursor/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=8"
+        "node": ">=14"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/run-applescript/node_modules/mimic-fn": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+    "node_modules/retry": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=6"
+        "node": ">= 4"
       }
     },
-    "node_modules/run-applescript/node_modules/npm-run-path": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
-      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
       "dev": true,
-      "dependencies": {
-        "path-key": "^3.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=8"
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/run-applescript/node_modules/onetime": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+    "node_modules/rimraf": {
+      "version": "3.0.2",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "mimic-fn": "^2.1.0"
+        "glob": "^7.1.3"
       },
-      "engines": {
-        "node": ">=6"
+      "bin": {
+        "rimraf": "bin.js"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/run-applescript/node_modules/strip-final-newline": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
-      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
-      "dev": true,
+    "node_modules/run-applescript": {
+      "version": "7.0.0",
+      "license": "MIT",
       "engines": {
-        "node": ">=6"
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/run-async": {
@@ -7306,6 +6209,7 @@
       "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
       "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
       }
@@ -7329,42 +6233,23 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "queue-microtask": "^1.2.2"
       }
     },
     "node_modules/rxjs": {
-      "version": "7.8.1",
-      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
-      "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+      "version": "7.8.2",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+      "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "tslib": "^2.1.0"
       }
     },
-    "node_modules/safe-array-concat": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
-      "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
-        "has-symbols": "^1.0.3",
-        "isarray": "^2.0.5"
-      },
-      "engines": {
-        "node": ">=0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
     "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==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -7378,113 +6263,132 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ]
-    },
-    "node_modules/safe-regex-test": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
-      "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.3",
-        "is-regex": "^1.1.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
+      ],
+      "license": "MIT"
     },
     "node_modules/safer-buffer": {
       "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/semver": {
       "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
       }
     },
-    "node_modules/semver-diff": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz",
-      "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==",
+    "node_modules/send": {
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "semver": "^7.3.5"
+        "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": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.8.0"
       }
     },
-    "node_modules/semver-diff/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==",
+    "node_modules/send/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
+        "ms": "2.0.0"
       }
     },
-    "node_modules/semver-diff/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==",
+    "node_modules/send/node_modules/debug/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
       "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
+      "license": "MIT"
+    },
+    "node_modules/send/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==",
+      "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.8"
       }
     },
     "node_modules/serialize-javascript": {
       "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
-      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "randombytes": "^2.1.0"
       }
     },
+    "node_modules/serve-static": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.19.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
     "node_modules/set-blocking": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
-    "node_modules/set-function-name": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
-      "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+    "node_modules/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "define-data-property": "^1.0.1",
-        "functions-have-names": "^1.2.3",
-        "has-property-descriptors": "^1.0.0"
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "shebang-regex": "^3.0.0"
       },
@@ -7494,35 +6398,16 @@
     },
     "node_modules/shebang-regex": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/shelljs": {
-      "version": "0.8.5",
-      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
-      "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
-      "dev": true,
-      "dependencies": {
-        "glob": "^7.0.0",
-        "interpret": "^1.0.0",
-        "rechoir": "^0.6.2"
-      },
-      "bin": {
-        "shjs": "bin/shjs"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/shiki": {
-      "version": "0.14.4",
-      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz",
-      "integrity": "sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==",
+      "version": "0.14.7",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-sequence-parser": "^1.1.0",
         "jsonc-parser": "^3.2.0",
@@ -7531,14 +6416,19 @@
       }
     },
     "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==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "object-inspect": "^1.13.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -7546,21 +6436,19 @@
     },
     "node_modules/signal-exit": {
       "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/sinon": {
-      "version": "16.0.0",
-      "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.0.0.tgz",
-      "integrity": "sha512-B8AaZZm9CT5pqe4l4uWJztfD/mOTa7dL8Qo0W4+s+t74xECOgSZDDQCBjNgIK3+n4kyxQrSTv2V5ul8K25qkiQ==",
+      "version": "17.0.1",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@sinonjs/commons": "^3.0.0",
-        "@sinonjs/fake-timers": "^10.3.0",
+        "@sinonjs/fake-timers": "^11.2.2",
         "@sinonjs/samsam": "^8.0.0",
         "diff": "^5.1.0",
-        "nise": "^5.1.4",
+        "nise": "^5.1.5",
         "supports-color": "^7.2.0"
       },
       "funding": {
@@ -7570,27 +6458,16 @@
     },
     "node_modules/sinon/node_modules/diff": {
       "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.3.1"
       }
     },
-    "node_modules/sinon/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/sinon/node_modules/supports-color": {
       "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -7598,86 +6475,59 @@
         "node": ">=8"
       }
     },
-    "node_modules/slash": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
-      "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/smart-buffer": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
       "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 6.0.0",
         "npm": ">= 3.0.0"
       }
     },
     "node_modules/socks": {
-      "version": "2.7.1",
-      "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
-      "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
+      "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ip": "^2.0.0",
+        "ip-address": "^9.0.5",
         "smart-buffer": "^4.2.0"
       },
       "engines": {
-        "node": ">= 10.13.0",
+        "node": ">= 10.0.0",
         "npm": ">= 3.0.0"
       }
     },
     "node_modules/socks-proxy-agent": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
-      "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
+      "version": "8.0.5",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+      "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "agent-base": "^7.0.2",
+        "agent-base": "^7.1.2",
         "debug": "^4.3.4",
-        "socks": "^2.7.1"
+        "socks": "^2.8.3"
       },
       "engines": {
         "node": ">= 14"
       }
     },
-    "node_modules/socks/node_modules/ip": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
-      "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
-      "dev": true
-    },
     "node_modules/source-map": {
       "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
     },
-    "node_modules/source-map-support": {
-      "version": "0.5.21",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
-      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
-      "dev": true,
-      "dependencies": {
-        "buffer-from": "^1.0.0",
-        "source-map": "^0.6.0"
-      }
-    },
     "node_modules/spawn-wrap": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
-      "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "foreground-child": "^2.0.0",
         "is-windows": "^1.0.2",
@@ -7692,51 +6542,46 @@
     },
     "node_modules/sprintf-js": {
       "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
-      "dev": true
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
     },
     "node_modules/stdin-discarder": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
-      "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+      "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
       "dev": true,
-      "dependencies": {
-        "bl": "^5.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/stop-iteration-iterator": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
-      "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
-      "dev": true,
-      "dependencies": {
-        "internal-slot": "^1.0.4"
-      },
+    "node_modules/stoppable": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
+      "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "safe-buffer": "~5.2.0"
+        "node": ">=4",
+        "npm": ">=6"
       }
     },
     "node_modules/string-width": {
       "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -7746,56 +6591,10 @@
         "node": ">=8"
       }
     },
-    "node_modules/string.prototype.trim": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
-      "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/string.prototype.trimend": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
-      "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/string.prototype.trimstart": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
-      "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -7805,9 +6604,8 @@
     },
     "node_modules/strip-bom": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
-      "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -7817,6 +6615,7 @@
       "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
       "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
@@ -7825,19 +6624,20 @@
       }
     },
     "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==",
+      "version": "3.1.1",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/strip-outer": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
-      "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "escape-string-regexp": "^1.0.2"
       },
@@ -7847,67 +6647,89 @@
     },
     "node_modules/strip-outer/node_modules/escape-string-regexp": {
       "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.8.0"
       }
     },
     "node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "version": "8.1.1",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "has-flag": "^3.0.0"
+        "has-flag": "^4.0.0"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
-    "node_modules/supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+    "node_modules/test-exclude": {
+      "version": "6.0.0",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@istanbuljs/schema": "^0.1.2",
+        "glob": "^7.1.4",
+        "minimatch": "^3.0.4"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tinyexec": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
+      "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+      "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2"
+      },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=12.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/SuperchupuDev"
       }
     },
-    "node_modules/test-exclude": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
-      "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.4.4",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+      "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
       "dev": true,
-      "dependencies": {
-        "@istanbuljs/schema": "^0.1.2",
-        "glob": "^7.1.4",
-        "minimatch": "^3.0.4"
+      "license": "MIT",
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
       },
-      "engines": {
-        "node": ">=8"
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
       }
     },
-    "node_modules/text-table": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
-    },
-    "node_modules/titleize": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
-      "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=12"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
     "node_modules/tmp": {
@@ -7915,6 +6737,7 @@
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
       "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "os-tmpdir": "~1.0.2"
       },
@@ -7924,9 +6747,8 @@
     },
     "node_modules/to-fast-properties": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
@@ -7936,6 +6758,7 @@
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
       },
@@ -7943,17 +6766,20 @@
         "node": ">=8.0"
       }
     },
-    "node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
-      "dev": true
+    "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==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
     },
     "node_modules/trim-repeated": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
-      "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "escape-string-regexp": "^1.0.2"
       },
@@ -7963,30 +6789,18 @@
     },
     "node_modules/trim-repeated/node_modules/escape-string-regexp": {
       "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.8.0"
       }
     },
-    "node_modules/ts-api-utils": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
-      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
-      "dev": true,
-      "engines": {
-        "node": ">=16.13.0"
-      },
-      "peerDependencies": {
-        "typescript": ">=4.2.0"
-      }
-    },
     "node_modules/ts-node": {
-      "version": "10.9.1",
-      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
-      "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@cspotcode/source-map-support": "^0.8.0",
         "@tsconfig/node10": "^1.0.7",
@@ -8025,144 +6839,85 @@
         }
       }
     },
-    "node_modules/ts-node/node_modules/@tsconfig/node14": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
-      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
-      "dev": true
-    },
     "node_modules/ts-node/node_modules/diff": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
       "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.3.1"
       }
     },
     "node_modules/tslib": {
       "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
-      "dev": true
+      "license": "0BSD"
     },
-    "node_modules/type-check": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
-      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+    "node_modules/tsx": {
+      "version": "4.7.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "prelude-ls": "^1.2.1"
+        "esbuild": "~0.19.10",
+        "get-tsconfig": "^4.7.2"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
       },
       "engines": {
-        "node": ">= 0.8.0"
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
       }
     },
     "node_modules/type-detect": {
       "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
-      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
     },
     "node_modules/type-fest": {
-      "version": "0.21.3",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
-      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/typed-array-buffer": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
-      "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
-        "is-typed-array": "^1.1.10"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/typed-array-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
-      "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+      "version": "0.8.1",
       "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
-      },
+      "license": "(MIT OR CC0-1.0)",
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=8"
       }
     },
-    "node_modules/typed-array-byte-offset": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
-      "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
-        "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/typed-array-length": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
-      "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "for-each": "^0.3.3",
-        "is-typed-array": "^1.1.9"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 0.6"
       }
     },
     "node_modules/typedarray-to-buffer": {
       "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
-      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "is-typedarray": "^1.0.0"
       }
     },
     "node_modules/typedoc": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz",
-      "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==",
+      "version": "0.25.7",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "lunr": "^2.3.9",
         "marked": "^4.3.0",
         "minimatch": "^9.0.3",
-        "shiki": "^0.14.1"
+        "shiki": "^0.14.7"
       },
       "bin": {
         "typedoc": "bin/typedoc"
@@ -8171,23 +6926,21 @@
         "node": ">= 16"
       },
       "peerDependencies": {
-        "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x"
+        "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x"
       }
     },
     "node_modules/typedoc/node_modules/brace-expansion": {
       "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
     },
     "node_modules/typedoc/node_modules/minimatch": {
       "version": "9.0.3",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
-      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -8199,10 +6952,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
-      "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+      "version": "5.3.3",
       "dev": true,
+      "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -8211,64 +6963,61 @@
         "node": ">=14.17"
       }
     },
-    "node_modules/unbox-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
-      "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-bigints": "^1.0.2",
-        "has-symbols": "^1.0.3",
-        "which-boxed-primitive": "^1.0.2"
+        "random-bytes": "~1.0.0"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "engines": {
+        "node": ">= 0.8"
       }
     },
-    "node_modules/unique-string": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz",
-      "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==",
+    "node_modules/undici": {
+      "version": "6.21.3",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
+      "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
       "dev": true,
-      "dependencies": {
-        "crypto-random-string": "^4.0.0"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=18.17"
       }
     },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
-      "dev": true
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+      "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/universalify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+      "version": "2.0.1",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">= 10.0.0"
       }
     },
-    "node_modules/untildify": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
-      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=8"
+        "node": ">= 0.8"
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
-      "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+      "version": "1.0.13",
       "dev": true,
       "funding": [
         {
@@ -8284,6 +7033,7 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "escalade": "^3.1.1",
         "picocolors": "^1.0.0"
@@ -8295,388 +7045,147 @@
         "browserslist": ">= 4.21.0"
       }
     },
-    "node_modules/update-notifier": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz",
-      "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==",
-      "dev": true,
-      "dependencies": {
-        "boxen": "^7.0.0",
-        "chalk": "^5.0.1",
-        "configstore": "^6.0.0",
-        "has-yarn": "^3.0.0",
-        "import-lazy": "^4.0.0",
-        "is-ci": "^3.0.1",
-        "is-installed-globally": "^0.4.0",
-        "is-npm": "^6.0.0",
-        "is-yarn-global": "^0.4.0",
-        "latest-version": "^7.0.0",
-        "pupa": "^3.1.0",
-        "semver": "^7.3.7",
-        "semver-diff": "^4.0.0",
-        "xdg-basedir": "^5.1.0"
-      },
-      "engines": {
-        "node": ">=14.16"
-      },
-      "funding": {
-        "url": "https://github.com/yeoman/update-notifier?sponsor=1"
-      }
-    },
-    "node_modules/update-notifier/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==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/update-notifier/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==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/uri-js": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-      "dev": true,
-      "dependencies": {
-        "punycode": "^2.1.0"
-      }
-    },
     "node_modules/url-join": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
       "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.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==",
-      "dev": true
-    },
-    "node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true,
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
-    "node_modules/v8-compile-cache-lib": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
-      "dev": true
-    },
-    "node_modules/vscode-oniguruma": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
-      "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
-      "dev": true
-    },
-    "node_modules/vscode-textmate": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
-      "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
-      "dev": true
-    },
-    "node_modules/wcwidth": {
+    "node_modules/utils-merge": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
-      "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
-      "dev": true,
-      "dependencies": {
-        "defaults": "^1.0.3"
-      }
-    },
-    "node_modules/web-streams-polyfill": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
-      "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
-      "dev": true
-    },
-    "node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dev": true,
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
+        "node": ">= 0.4.0"
       }
     },
-    "node_modules/which": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "dev": true,
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "license": "MIT",
       "bin": {
-        "node-which": "bin/node-which"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/which-boxed-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
-      "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
-      "dev": true,
-      "dependencies": {
-        "is-bigint": "^1.0.1",
-        "is-boolean-object": "^1.1.0",
-        "is-number-object": "^1.0.4",
-        "is-string": "^1.0.5",
-        "is-symbol": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/which-module": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
-      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
-      "dev": true
-    },
-    "node_modules/which-typed-array": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
-      "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
-      "dev": true,
-      "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
-        "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/widest-line": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
-      "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
-      "dev": true,
-      "dependencies": {
-        "string-width": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/widest-line/node_modules/ansi-regex": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+        "uuid": "dist/bin/uuid"
       }
     },
-    "node_modules/widest-line/node_modules/emoji-regex": {
-      "version": "9.2.2",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/widest-line/node_modules/string-width": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
-      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+    "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==",
       "dev": true,
-      "dependencies": {
-        "eastasianwidth": "^0.2.0",
-        "emoji-regex": "^9.2.2",
-        "strip-ansi": "^7.0.1"
-      },
+      "license": "MIT",
       "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">= 0.8"
       }
     },
-    "node_modules/widest-line/node_modules/strip-ansi": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
-      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+    "node_modules/vscode-oniguruma": {
+      "version": "1.7.0",
       "dev": true,
-      "dependencies": {
-        "ansi-regex": "^6.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
-      }
+      "license": "MIT"
     },
-    "node_modules/wildcard-match": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.2.tgz",
-      "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==",
-      "dev": true
+    "node_modules/vscode-textmate": {
+      "version": "8.0.0",
+      "dev": true,
+      "license": "MIT"
     },
-    "node_modules/windows-release": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.1.1.tgz",
-      "integrity": "sha512-NMD00arvqcq2nwqc5Q6KtrSRHK+fVD31erE5FEMahAw5PmVCgD7MUXodq3pdZSUkqA9Cda2iWx6s1XYwiJWRmw==",
+    "node_modules/whatwg-encoding": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+      "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "execa": "^5.1.1"
+        "iconv-lite": "0.6.3"
       },
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=18"
       }
     },
-    "node_modules/windows-release/node_modules/execa": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
-      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+    "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==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^6.0.0",
-        "human-signals": "^2.1.0",
-        "is-stream": "^2.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^4.0.1",
-        "onetime": "^5.1.2",
-        "signal-exit": "^3.0.3",
-        "strip-final-newline": "^2.0.0"
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/windows-release/node_modules/human-signals": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
-      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+    "node_modules/whatwg-mimetype": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=10.17.0"
+        "node": ">=18"
       }
     },
-    "node_modules/windows-release/node_modules/is-stream": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+    "node_modules/which": {
+      "version": "2.0.2",
       "dev": true,
-      "engines": {
-        "node": ">=8"
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
       }
     },
-    "node_modules/windows-release/node_modules/mimic-fn": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+    "node_modules/which-module": {
+      "version": "2.0.1",
       "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
+      "license": "ISC"
     },
-    "node_modules/windows-release/node_modules/npm-run-path": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
-      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+    "node_modules/wildcard-match": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz",
+      "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==",
       "dev": true,
-      "dependencies": {
-        "path-key": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
+      "license": "ISC"
     },
-    "node_modules/windows-release/node_modules/onetime": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+    "node_modules/windows-release": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz",
+      "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "mimic-fn": "^2.1.0"
+        "execa": "^8.0.1"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/windows-release/node_modules/strip-final-newline": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
-      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/workerpool": {
       "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
-      "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
-      "dev": true
+      "dev": true,
+      "license": "Apache-2.0"
     },
     "node_modules/wrap-ansi": {
       "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
-      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -8688,15 +7197,13 @@
     },
     "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==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/write-file-atomic": {
       "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
-      "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "imurmurhash": "^0.1.4",
         "is-typedarray": "^1.0.0",
@@ -8704,64 +7211,48 @@
         "typedarray-to-buffer": "^3.1.5"
       }
     },
-    "node_modules/xdg-basedir": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
-      "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/y18n": {
       "version": "5.0.8",
-      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
-      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
       "dev": true,
+      "license": "ISC",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+      "version": "3.1.1",
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/yargs": {
-      "version": "17.7.2",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
-      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "version": "16.2.0",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "cliui": "^8.0.1",
+        "cliui": "^7.0.2",
         "escalade": "^3.1.1",
         "get-caller-file": "^2.0.5",
         "require-directory": "^2.1.1",
-        "string-width": "^4.2.3",
+        "string-width": "^4.2.0",
         "y18n": "^5.0.5",
-        "yargs-parser": "^21.1.1"
+        "yargs-parser": "^20.2.2"
       },
       "engines": {
-        "node": ">=12"
+        "node": ">=10"
       }
     },
     "node_modules/yargs-parser": {
-      "version": "21.1.1",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
-      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "version": "20.2.4",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=12"
+        "node": ">=10"
       }
     },
     "node_modules/yargs-unparser": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
-      "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "camelcase": "^6.0.0",
         "decamelize": "^4.0.0",
@@ -8774,9 +7265,8 @@
     },
     "node_modules/yargs-unparser/node_modules/camelcase": {
       "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
-      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=10"
       },
@@ -8786,9 +7276,8 @@
     },
     "node_modules/yargs-unparser/node_modules/decamelize": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
-      "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=10"
       },
@@ -8801,15 +7290,15 @@
       "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
       "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
     },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
-      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=10"
       },
@@ -8817,147 +7306,239 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/yoctocolors-cjs": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
+      "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "packages/authx": {
+      "name": "@redis/authx",
+      "version": "5.0.0-next.5",
+      "extraneous": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-node": "^2.16.1"
+      },
+      "devDependencies": {},
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@redis/client": "^5.0.0-next.5"
+      }
+    },
     "packages/bloom": {
       "name": "@redis/bloom",
-      "version": "1.2.0",
+      "version": "5.7.0",
       "license": "MIT",
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
-        "@redis/test-utils": "*",
-        "@types/node": "^20.6.2",
-        "nyc": "^15.1.0",
-        "release-it": "^16.1.5",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typedoc": "^0.25.1",
-        "typescript": "^5.2.2"
+        "@redis/test-utils": "*"
+      },
+      "engines": {
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@redis/client": "^1.0.0"
+        "@redis/client": "^5.7.0"
       }
     },
     "packages/client": {
       "name": "@redis/client",
-      "version": "1.6.0",
+      "version": "5.7.0",
       "license": "MIT",
       "dependencies": {
-        "cluster-key-slot": "1.1.2",
-        "generic-pool": "3.9.0",
-        "yallist": "4.0.0"
+        "cluster-key-slot": "1.1.2"
       },
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
         "@redis/test-utils": "*",
-        "@types/node": "^20.6.2",
-        "@types/sinon": "^10.0.16",
-        "@types/yallist": "^4.0.1",
-        "@typescript-eslint/eslint-plugin": "^6.7.2",
-        "@typescript-eslint/parser": "^6.7.2",
-        "eslint": "^8.49.0",
-        "nyc": "^15.1.0",
-        "release-it": "^16.1.5",
-        "sinon": "^16.0.0",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typedoc": "^0.25.1",
-        "typescript": "^5.2.2"
+        "@types/sinon": "^17.0.3",
+        "sinon": "^17.0.1"
       },
       "engines": {
-        "node": ">=14"
+        "node": ">= 18"
+      }
+    },
+    "packages/entraid": {
+      "name": "@redis/entraid",
+      "version": "5.7.0",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/identity": "^4.7.0",
+        "@azure/msal-node": "^2.16.1"
+      },
+      "devDependencies": {
+        "@redis/test-utils": "*",
+        "@types/express": "^4.17.21",
+        "@types/express-session": "^1.18.0",
+        "@types/node": "^22.9.0",
+        "dotenv": "^16.3.1",
+        "express": "^4.21.1",
+        "express-session": "^1.18.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@redis/client": "^5.7.0"
       }
     },
+    "packages/entraid/node_modules/@types/node": {
+      "version": "22.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.20.0"
+      }
+    },
+    "packages/entraid/node_modules/undici-types": {
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "packages/graph": {
       "name": "@redis/graph",
-      "version": "1.1.1",
+      "version": "5.0.0-next.6",
+      "extraneous": true,
       "license": "MIT",
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
-        "@redis/test-utils": "*",
-        "@types/node": "^20.6.2",
-        "nyc": "^15.1.0",
-        "release-it": "^16.1.5",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typedoc": "^0.25.1",
-        "typescript": "^5.2.2"
+        "@redis/test-utils": "*"
+      },
+      "engines": {
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@redis/client": "^1.0.0"
+        "@redis/client": "^5.0.0-next.6"
       }
     },
     "packages/json": {
       "name": "@redis/json",
-      "version": "1.0.7",
+      "version": "5.7.0",
       "license": "MIT",
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
-        "@redis/test-utils": "*",
-        "@types/node": "^20.6.2",
-        "nyc": "^15.1.0",
-        "release-it": "^16.1.5",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typedoc": "^0.25.1",
-        "typescript": "^5.2.2"
+        "@redis/test-utils": "*"
+      },
+      "engines": {
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@redis/client": "^1.0.0"
+        "@redis/client": "^5.7.0"
+      }
+    },
+    "packages/redis": {
+      "version": "5.7.0",
+      "license": "MIT",
+      "dependencies": {
+        "@redis/bloom": "5.7.0",
+        "@redis/client": "5.7.0",
+        "@redis/json": "5.7.0",
+        "@redis/search": "5.7.0",
+        "@redis/time-series": "5.7.0"
+      },
+      "engines": {
+        "node": ">= 18"
       }
     },
     "packages/search": {
       "name": "@redis/search",
-      "version": "1.2.0",
+      "version": "5.7.0",
       "license": "MIT",
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
-        "@redis/test-utils": "*",
-        "@types/node": "^20.6.2",
-        "nyc": "^15.1.0",
-        "release-it": "^16.1.5",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typedoc": "^0.25.1",
-        "typescript": "^5.2.2"
+        "@redis/test-utils": "*"
+      },
+      "engines": {
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@redis/client": "^1.0.0"
+        "@redis/client": "^5.7.0"
       }
     },
     "packages/test-utils": {
       "name": "@redis/test-utils",
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
-        "@types/mocha": "^10.0.1",
-        "@types/node": "^20.6.2",
-        "@types/yargs": "^17.0.24",
-        "mocha": "^10.2.0",
-        "nyc": "^15.1.0",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typescript": "^5.2.2",
+        "@types/yargs": "^17.0.32",
         "yargs": "^17.7.2"
       },
       "peerDependencies": {
-        "@redis/client": "^1.0.0"
+        "@redis/client": "*"
+      }
+    },
+    "packages/test-utils/node_modules/cliui": {
+      "version": "8.0.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "packages/test-utils/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "packages/test-utils/node_modules/yargs": {
+      "version": "17.7.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "packages/test-utils/node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
       }
     },
     "packages/time-series": {
       "name": "@redis/time-series",
-      "version": "1.1.0",
+      "version": "5.7.0",
       "license": "MIT",
       "devDependencies": {
-        "@istanbuljs/nyc-config-typescript": "^1.0.2",
-        "@redis/test-utils": "*",
-        "@types/node": "^20.6.2",
-        "nyc": "^15.1.0",
-        "release-it": "^16.1.5",
-        "source-map-support": "^0.5.21",
-        "ts-node": "^10.9.1",
-        "typedoc": "^0.25.1",
-        "typescript": "^5.2.2"
+        "@redis/test-utils": "*"
+      },
+      "engines": {
+        "node": ">= 18"
       },
       "peerDependencies": {
-        "@redis/client": "^1.0.0"
+        "@redis/client": "^5.7.0"
       }
     }
   }
diff --git a/package.json b/package.json
index e8ceef7173d..e192e69d55e 100644
--- a/package.json
+++ b/package.json
@@ -1,50 +1,36 @@
 {
-  "name": "redis",
-  "description": "A modern, high performance Redis client",
-  "version": "4.7.0",
-  "license": "MIT",
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
-  "files": [
-    "dist/"
-  ],
+  "name": "redis-monorepo",
+  "private": true,
   "workspaces": [
-    "./packages/*"
+    "./packages/client",
+    "./packages/test-utils",
+    "./packages/bloom",
+    "./packages/json",
+    "./packages/search",
+    "./packages/time-series",
+    "./packages/entraid",
+    "./packages/redis"
   ],
   "scripts": {
+    "test-single": "TS_NODE_PROJECT='./packages/test-utils/tsconfig.json' mocha --require ts-node/register/transpile-only ",
     "test": "npm run test -ws --if-present",
-    "build:client": "npm run build -w ./packages/client",
-    "build:test-utils": "npm run build -w ./packages/test-utils",
-    "build:tests-tools": "npm run build:client && npm run build:test-utils",
-    "build:modules": "find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'client' ! -name 'test-utils' -exec npm run build -w {} \\;",
-    "build": "tsc",
-    "build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build",
-    "documentation": "npm run documentation -ws --if-present",
-    "gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'"
-  },
-  "dependencies": {
-    "@redis/bloom": "1.2.0",
-    "@redis/client": "1.6.0",
-    "@redis/graph": "1.1.1",
-    "@redis/json": "1.0.7",
-    "@redis/search": "1.2.0",
-    "@redis/time-series": "1.1.0"
+    "build": "tsc --build",
+    "documentation": "typedoc --out ./documentation",
+    "gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'",
+    "release": "npm run release --workspaces --if-present --"
   },
   "devDependencies": {
-    "@tsconfig/node14": "^14.1.0",
-    "gh-pages": "^6.0.0",
-    "release-it": "^16.1.5",
-    "typescript": "^5.2.2"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git://github.com/redis/node-redis.git"
-  },
-  "bugs": {
-    "url": "https://github.com/redis/node-redis/issues"
-  },
-  "homepage": "https://github.com/redis/node-redis",
-  "keywords": [
-    "redis"
-  ]
+    "@istanbuljs/nyc-config-typescript": "^1.0.2",
+    "@release-it/bumper": "^7.0.5",
+    "@types/mocha": "^10.0.6",
+    "@types/node": "^20.11.16",
+    "gh-pages": "^6.1.1",
+    "mocha": "^10.2.0",
+    "nyc": "^15.1.0",
+    "release-it": "^19.0.2",
+    "ts-node": "^10.9.2",
+    "tsx": "^4.7.0",
+    "typedoc": "^0.25.7",
+    "typescript": "^5.3.3"
+  }
 }
diff --git a/packages/bloom/.npmignore b/packages/bloom/.npmignore
deleted file mode 100644
index bbef2b404fb..00000000000
--- a/packages/bloom/.npmignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.nyc_output/
-coverage/
-lib/
-.nycrc.json
-.release-it.json
-tsconfig.json
diff --git a/packages/bloom/.release-it.json b/packages/bloom/.release-it.json
index 5d11263645f..23e1cf09078 100644
--- a/packages/bloom/.release-it.json
+++ b/packages/bloom/.release-it.json
@@ -1,10 +1,22 @@
 {
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
   "git": {
     "tagName": "bloom@${version}",
+    "tagMatch": "bloom@*",
     "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
   },
-  "npm": {
-    "publishArgs": ["--access", "public"]
+  "plugins": {
+    "@release-it/bumper": {
+      "out": {
+        "file": "package.json",
+        "path": ["peerDependencies.@redis/client"],
+        "versionPrefix": "^"
+      }
+    }
   }
 }
diff --git a/packages/bloom/README.md b/packages/bloom/README.md
index 8eb1445d188..e527ff5552c 100644
--- a/packages/bloom/README.md
+++ b/packages/bloom/README.md
@@ -1,14 +1,17 @@
 # @redis/bloom
 
-This package provides support for the [RedisBloom](https://redisbloom.io) module, which adds additional probabilistic data structures to Redis.  It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediBloom commands.
+This package provides support for the [RedisBloom](https://redis.io/docs/data-types/probabilistic/) module, which adds additional probabilistic data structures to Redis.
 
-To use these extra commands, your Redis server must have the RedisBloom module installed.
+Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis).
+
+:warning: To use these extra commands, your Redis server must have the RedisBloom module installed.
 
 RedisBloom provides the following probabilistic data structures:
 
 * Bloom Filter: for checking set membership with a high degree of certainty.
 * Cuckoo Filter: for checking set membership with a high degree of certainty.
-* Count-Min Sketch: Determine the frequency of events in a stream.
+* T-Digest: for estimating the quantiles of a stream of data.
 * Top-K: Maintain a list of k most frequently seen items.
+* Count-Min Sketch: Determine the frequency of events in a stream.
 
-For complete examples, see `bloom-filter.js`, `cuckoo-filter.js`, `count-min-sketch.js` and `topk.js` in the Node Redis examples folder.
+For some examples, see [`bloom-filter.js`](https://github.com/redis/node-redis/tree/master/examples/bloom-filter.js), [`cuckoo-filter.js`](https://github.com/redis/node-redis/tree/master/examples/cuckoo-filter.js), [`count-min-sketch.js`](https://github.com/redis/node-redis/tree/master/examples/count-min-sketch.js) and [`topk.js`](https://github.com/redis/node-redis/tree/master/examples/topk.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples).
diff --git a/packages/bloom/lib/commands/bloom/ADD.spec.ts b/packages/bloom/lib/commands/bloom/ADD.spec.ts
index e7ec3409136..a229936c7df 100644
--- a/packages/bloom/lib/commands/bloom/ADD.spec.ts
+++ b/packages/bloom/lib/commands/bloom/ADD.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './ADD';
+import ADD from './ADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF ADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['BF.ADD', 'key', 'item']
-        );
-    });
+describe('BF.ADD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ADD, 'key', 'item'),
+      ['BF.ADD', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.add', async client => {
-        assert.equal(
-            await client.bf.add('key', 'item'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.add', async client => {
+    assert.equal(
+      await client.bf.add('key', 'item'),
+      true
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/ADD.ts b/packages/bloom/lib/commands/bloom/ADD.ts
index d8938f4c2b0..bf976606997 100644
--- a/packages/bloom/lib/commands/bloom/ADD.ts
+++ b/packages/bloom/lib/commands/bloom/ADD.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['BF.ADD', key, item];
-}
-
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds an item to a Bloom Filter
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter
+   * @param item - The item to add to the filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('BF.ADD');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/CARD.spec.ts b/packages/bloom/lib/commands/bloom/CARD.spec.ts
index 4d5620ea196..32a28cdf6f7 100644
--- a/packages/bloom/lib/commands/bloom/CARD.spec.ts
+++ b/packages/bloom/lib/commands/bloom/CARD.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './CARD';
+import CARD from './CARD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF CARD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('bloom'),
-            ['BF.CARD', 'bloom']
-        );
-    });
+describe('BF.CARD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CARD, 'bloom'),
+      ['BF.CARD', 'bloom']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.card', async client => {
-        assert.equal(
-            await client.bf.card('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.card', async client => {
+    assert.equal(
+      await client.bf.card('key'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/CARD.ts b/packages/bloom/lib/commands/bloom/CARD.ts
index 530284c3f60..e1873e1faf1 100644
--- a/packages/bloom/lib/commands/bloom/CARD.ts
+++ b/packages/bloom/lib/commands/bloom/CARD.ts
@@ -1,9 +1,16 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string): Array<string> {
-    return ['BF.CARD', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the cardinality (number of items) in a Bloom Filter
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter to query
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('BF.CARD');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts
index 1088e739e61..4d2cc70074a 100644
--- a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts
+++ b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './EXISTS';
+import EXISTS from './EXISTS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF EXISTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['BF.EXISTS', 'key', 'item']
-        );
-    });
+describe('BF.EXISTS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EXISTS, 'key', 'item'),
+      ['BF.EXISTS', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.exists', async client => {
-        assert.equal(
-            await client.bf.exists('key', 'item'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.exists', async client => {
+    assert.equal(
+      await client.bf.exists('key', 'item'),
+      false
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/EXISTS.ts b/packages/bloom/lib/commands/bloom/EXISTS.ts
index d044207e244..db16253e2c7 100644
--- a/packages/bloom/lib/commands/bloom/EXISTS.ts
+++ b/packages/bloom/lib/commands/bloom/EXISTS.ts
@@ -1,9 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['BF.EXISTS', key, item];
-}
-
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Checks if an item exists in a Bloom Filter
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter
+   * @param item - The item to check for existence
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('BF.EXISTS');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/INFO.spec.ts b/packages/bloom/lib/commands/bloom/INFO.spec.ts
index 7a5e5724c22..0dbe5cb1f43 100644
--- a/packages/bloom/lib/commands/bloom/INFO.spec.ts
+++ b/packages/bloom/lib/commands/bloom/INFO.spec.ts
@@ -1,24 +1,27 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INFO';
+import INFO from './INFO';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('bloom'),
-            ['BF.INFO', 'bloom']
-        );
-    });
+describe('BF.INFO', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INFO, 'bloom'),
+      ['BF.INFO', 'bloom']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.info', async client => {
-        await client.bf.reserve('key', 0.01, 100);
+  testUtils.testWithClient('client.bf.info', async client => {
+    const [, reply] = await Promise.all([
+      client.bf.reserve('key', 0.01, 100),
+      client.bf.info('key')
+    ]);
 
-        const info = await client.bf.info('key');
-        assert.equal(typeof info, 'object');
-        assert.equal(info.capacity, 100);
-        assert.equal(typeof info.size, 'number');
-        assert.equal(typeof info.numberOfFilters, 'number');
-        assert.equal(typeof info.numberOfInsertedItems, 'number');
-        assert.equal(typeof info.expansionRate, 'number');
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(typeof reply, 'object');
+    assert.equal(reply['Capacity'], 100);
+    assert.equal(typeof reply['Size'], 'number');
+    assert.equal(typeof reply['Number of filters'], 'number');
+    assert.equal(typeof reply['Number of items inserted'], 'number');
+    assert.equal(typeof reply['Expansion rate'], 'number');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts
index 52e97646404..bdf7d0fda9b 100644
--- a/packages/bloom/lib/commands/bloom/INFO.ts
+++ b/packages/bloom/lib/commands/bloom/INFO.ts
@@ -1,38 +1,30 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { transformInfoV2Reply } from './helpers';
 
-export const IS_READ_ONLY = true;
+export type BfInfoReplyMap = TuplesToMapReply<[
+  [SimpleStringReply<'Capacity'>, NumberReply],
+  [SimpleStringReply<'Size'>, NumberReply],
+  [SimpleStringReply<'Number of filters'>, NumberReply],
+  [SimpleStringReply<'Number of items inserted'>, NumberReply],
+  [SimpleStringReply<'Expansion rate'>, NullReply | NumberReply] 
+]>;
 
-export function transformArguments(key: string): Array<string> {
-    return ['BF.INFO', key];
-}
-
-export type InfoRawReply = [
-    _: string,
-    capacity: number,
-    _: string,
-    size: number,
-    _: string,
-    numberOfFilters: number,
-    _: string,
-    numberOfInsertedItems: number,
-    _: string,
-    expansionRate: number,
-];
-
-export interface InfoReply {
-    capacity: number;
-    size: number;
-    numberOfFilters: number;
-    numberOfInsertedItems: number;
-    expansionRate: number;
-}
-
-export function transformReply(reply: InfoRawReply): InfoReply {
-    return {
-        capacity: reply[1],
-        size: reply[3],
-        numberOfFilters: reply[5],
-        numberOfInsertedItems: reply[7],
-        expansionRate: reply[9]
-    };
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about a Bloom Filter, including capacity, size, number of filters, items inserted, and expansion rate
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter to get information about
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('BF.INFO');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<BfInfoReplyMap>>, _, typeMapping?: TypeMapping): BfInfoReplyMap => {
+      return transformInfoV2Reply<BfInfoReplyMap>(reply, typeMapping);
+    },
+    3: undefined as unknown as () => BfInfoReplyMap
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/INSERT.spec.ts b/packages/bloom/lib/commands/bloom/INSERT.spec.ts
index aff9e6e282b..a9b544a51ae 100644
--- a/packages/bloom/lib/commands/bloom/INSERT.spec.ts
+++ b/packages/bloom/lib/commands/bloom/INSERT.spec.ts
@@ -1,69 +1,70 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INSERT';
+import INSERT from './INSERT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF INSERT', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item'),
-                ['BF.INSERT', 'key', 'ITEMS', 'item']
-            );
-        });
+describe('BF.INSERT', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item'),
+        ['BF.INSERT', 'key', 'ITEMS', 'item']
+      );
+    });
 
-        it('with CAPACITY', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item', { CAPACITY: 100 }),
-                ['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item']
-            );
-        });
+    it('with CAPACITY', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item', { CAPACITY: 100 }),
+        ['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item']
+      );
+    });
 
-        it('with ERROR', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item', { ERROR: 0.01 }),
-                ['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item']
-            );
-        });
+    it('with ERROR', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item', { ERROR: 0.01 }),
+        ['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item']
+      );
+    });
 
-        it('with EXPANSION', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item', { EXPANSION: 1 }),
-                ['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item']
-            );
-        });
+    it('with EXPANSION', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item', { EXPANSION: 1 }),
+        ['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item']
+      );
+    });
 
-        it('with NOCREATE', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item', { NOCREATE: true }),
-                ['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item']
-            );
-        });
+    it('with NOCREATE', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item', { NOCREATE: true }),
+        ['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item']
+      );
+    });
 
-        it('with NONSCALING', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item', { NONSCALING: true }),
-                ['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item']
-            );
-        });
+    it('with NONSCALING', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item', { NONSCALING: true }),
+        ['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item']
+      );
+    });
 
-        it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item', {
-                    CAPACITY: 100,
-                    ERROR: 0.01,
-                    EXPANSION: 1,
-                    NOCREATE: true,
-                    NONSCALING: true
-                }),
-                ['BF.INSERT', 'key', 'CAPACITY', '100', 'ERROR', '0.01', 'EXPANSION', '1', 'NOCREATE', 'NONSCALING', 'ITEMS', 'item']
-            );
-        });
+    it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => {
+      assert.deepEqual(
+        parseArgs(INSERT, 'key', 'item', {
+          CAPACITY: 100,
+          ERROR: 0.01,
+          EXPANSION: 1,
+          NOCREATE: true,
+          NONSCALING: true
+        }),
+        ['BF.INSERT', 'key', 'CAPACITY', '100', 'ERROR', '0.01', 'EXPANSION', '1', 'NOCREATE', 'NONSCALING', 'ITEMS', 'item']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.bf.insert', async client => {
-        assert.deepEqual(
-            await client.bf.insert('key', 'item'),
-            [true]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.insert', async client => {
+    assert.deepEqual(
+      await client.bf.insert('key', 'item'),
+      [true]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/INSERT.ts b/packages/bloom/lib/commands/bloom/INSERT.ts
index f6deb7a8612..c607e015694 100644
--- a/packages/bloom/lib/commands/bloom/INSERT.ts
+++ b/packages/bloom/lib/commands/bloom/INSERT.ts
@@ -1,45 +1,61 @@
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface InsertOptions {
-    CAPACITY?: number;
-    ERROR?: number;
-    EXPANSION?: number;
-    NOCREATE?: true;
-    NONSCALING?: true;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
+
+export interface BfInsertOptions {
+  CAPACITY?: number;
+  ERROR?: number;
+  EXPANSION?: number;
+  NOCREATE?: boolean;
+  NONSCALING?: boolean;
 }
 
-export function transformArguments(
-    key: string,
-    items: RedisCommandArgument | Array<RedisCommandArgument>,
-    options?: InsertOptions
-): RedisCommandArguments {
-    const args = ['BF.INSERT', key];
-
-    if (options?.CAPACITY) {
-        args.push('CAPACITY', options.CAPACITY.toString());
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds one or more items to a Bloom Filter, creating it if it does not exist
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter
+   * @param items - One or more items to add to the filter
+   * @param options - Optional parameters for filter creation
+   * @param options.CAPACITY - Desired capacity for a new filter
+   * @param options.ERROR - Desired error rate for a new filter
+   * @param options.EXPANSION - Expansion rate for a new filter
+   * @param options.NOCREATE - If true, prevents automatic filter creation
+   * @param options.NONSCALING - Prevents the filter from creating additional sub-filters
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    items: RedisVariadicArgument,
+    options?: BfInsertOptions
+  ) {
+    parser.push('BF.INSERT');
+    parser.pushKey(key);
+
+    if (options?.CAPACITY !== undefined) {
+      parser.push('CAPACITY', options.CAPACITY.toString());
     }
 
-    if (options?.ERROR) {
-        args.push('ERROR', options.ERROR.toString());
+    if (options?.ERROR !== undefined) {
+      parser.push('ERROR', options.ERROR.toString());
     }
 
-    if (options?.EXPANSION) {
-        args.push('EXPANSION', options.EXPANSION.toString());
+    if (options?.EXPANSION !== undefined) {
+      parser.push('EXPANSION', options.EXPANSION.toString());
     }
 
     if (options?.NOCREATE) {
-        args.push('NOCREATE');
+      parser.push('NOCREATE');
     }
 
     if (options?.NONSCALING) {
-        args.push('NONSCALING');
+      parser.push('NONSCALING');
     }
 
-    args.push('ITEMS');
-    return pushVerdictArguments(args, items);
-}
-
-export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+    parser.push('ITEMS');
+    parser.pushVariadic(items);
+  },
+  transformReply: transformBooleanArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts
index 19634cb4a78..40e24f96c39 100644
--- a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts
+++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts
@@ -1,28 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './LOADCHUNK';
+import LOADCHUNK from './LOADCHUNK';
+import { RESP_TYPES } from '@redis/client';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF LOADCHUNK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, ''),
-            ['BF.LOADCHUNK', 'key', '0', '']
-        );
-    });
+describe('BF.LOADCHUNK', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LOADCHUNK, 'key', 0, ''),
+      ['BF.LOADCHUNK', 'key', '0', '']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.loadChunk', async client => {
-        const [, { iterator, chunk }] = await Promise.all([
-            client.bf.reserve('source', 0.01, 100),
-            client.bf.scanDump(
-                client.commandOptions({ returnBuffers: true }),
-                'source',
-                0
-            )
-        ]);
+  testUtils.testWithClient('client.bf.loadChunk', async client => {
+    const [, { iterator, chunk }] = await Promise.all([
+      client.bf.reserve('source', 0.01, 100),
+      client.bf.scanDump('source', 0)
+    ]);
 
-        assert.equal(
-            await client.bf.loadChunk('destination', iterator, chunk),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(
+      await client.bf.loadChunk('destination', iterator, chunk),
+      'OK'
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      ...GLOBAL.SERVERS.OPEN.clientOptions,
+      commandOptions: {
+        typeMapping: {
+          [RESP_TYPES.BLOB_STRING]: Buffer
+        }
+      }
+    }
+  });
 });
diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts
index 491f572a49e..d0af9d7a644 100644
--- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts
+++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: string,
-    iteretor: number,
-    chunk: RedisCommandArgument
-): RedisCommandArguments {
-    return ['BF.LOADCHUNK', key, iteretor.toString(), chunk];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Restores a Bloom Filter chunk previously saved using SCANDUMP
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter to restore
+   * @param iterator - Iterator value from the SCANDUMP command
+   * @param chunk - Data chunk from the SCANDUMP command
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) {
+    parser.push('BF.LOADCHUNK');
+    parser.pushKey(key);
+    parser.push(iterator.toString(), chunk);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/MADD.spec.ts b/packages/bloom/lib/commands/bloom/MADD.spec.ts
index 784f99926ff..5eb39ee73d4 100644
--- a/packages/bloom/lib/commands/bloom/MADD.spec.ts
+++ b/packages/bloom/lib/commands/bloom/MADD.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './MADD';
+import MADD from './MADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF MADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', ['1', '2']),
-            ['BF.MADD', 'key', '1', '2']
-        );
-    });
+describe('BF.MADD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MADD, 'key', ['1', '2']),
+      ['BF.MADD', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.ts.mAdd', async client => {
-        assert.deepEqual(
-            await client.bf.mAdd('key', ['1', '2']),
-            [true, true]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ts.mAdd', async client => {
+    assert.deepEqual(
+      await client.bf.mAdd('key', ['1', '2']),
+      [true, true]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/MADD.ts b/packages/bloom/lib/commands/bloom/MADD.ts
index 056c4a1c1c2..adef0aee3e2 100644
--- a/packages/bloom/lib/commands/bloom/MADD.ts
+++ b/packages/bloom/lib/commands/bloom/MADD.ts
@@ -1,7 +1,20 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(key: string, items: Array<string>): Array<string> {
-    return ['BF.MADD', key, ...items];
-}
-
-export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds multiple items to a Bloom Filter in a single call
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter
+   * @param items - One or more items to add to the filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) {
+    parser.push('BF.MADD');
+    parser.pushKey(key);
+    parser.pushVariadic(items);
+  },
+  transformReply: transformBooleanArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts
index 027e51d2c43..60c09b00f17 100644
--- a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts
+++ b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './MEXISTS';
+import MEXISTS from './MEXISTS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF MEXISTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', ['1', '2']),
-            ['BF.MEXISTS', 'key', '1', '2']
-        );
-    });
+describe('BF.MEXISTS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MEXISTS, 'key', ['1', '2']),
+      ['BF.MEXISTS', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.mExists', async client => {
-        assert.deepEqual(
-            await client.bf.mExists('key', ['1', '2']),
-            [false, false]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.mExists', async client => {
+    assert.deepEqual(
+      await client.bf.mExists('key', ['1', '2']),
+      [false, false]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.ts b/packages/bloom/lib/commands/bloom/MEXISTS.ts
index fb79410155d..658f9b01229 100644
--- a/packages/bloom/lib/commands/bloom/MEXISTS.ts
+++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts
@@ -1,9 +1,20 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, items: Array<string>): Array<string> {
-    return ['BF.MEXISTS', key, ...items];
-}
-
-export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Checks if multiple items exist in a Bloom Filter in a single call
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter
+   * @param items - One or more items to check for existence
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) {
+    parser.push('BF.MEXISTS');
+    parser.pushKey(key);
+    parser.pushVariadic(items);
+  },
+  transformReply: transformBooleanArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts
index bc872f9c3f7..803577b350b 100644
--- a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts
+++ b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts
@@ -1,49 +1,50 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './RESERVE';
+import RESERVE from './RESERVE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF RESERVE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 0.01, 100),
-                ['BF.RESERVE', 'key', '0.01', '100']
-            );
-        });
+describe('BF.RESERVE', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 0.01, 100),
+        ['BF.RESERVE', 'key', '0.01', '100']
+      );
+    });
 
-        it('with EXPANSION', () => {
-            assert.deepEqual(
-                transformArguments('key', 0.01, 100, {
-                    EXPANSION: 1
-                }),
-                ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1']
-            );
-        });
+    it('with EXPANSION', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 0.01, 100, {
+          EXPANSION: 1
+        }),
+        ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1']
+      );
+    });
 
-        it('with NONSCALING', () => {
-            assert.deepEqual(
-                transformArguments('key', 0.01, 100, {
-                    NONSCALING: true
-                }),
-                ['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING']
-            );
-        });
+    it('with NONSCALING', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 0.01, 100, {
+          NONSCALING: true
+        }),
+        ['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING']
+      );
+    });
 
-        it('with EXPANSION and NONSCALING', () => {
-            assert.deepEqual(
-                transformArguments('key', 0.01, 100, {
-                    EXPANSION: 1,
-                    NONSCALING: true
-                }),
-                ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1', 'NONSCALING']
-            );
-        });
+    it('with EXPANSION and NONSCALING', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 0.01, 100, {
+          EXPANSION: 1,
+          NONSCALING: true
+        }),
+        ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1', 'NONSCALING']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.bf.reserve', async client => {
-        assert.equal(
-            await client.bf.reserve('bloom', 0.01, 100),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.reserve', async client => {
+    assert.equal(
+      await client.bf.reserve('bloom', 0.01, 100),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/RESERVE.ts b/packages/bloom/lib/commands/bloom/RESERVE.ts
index 18d7002f158..d0b3fb906a9 100644
--- a/packages/bloom/lib/commands/bloom/RESERVE.ts
+++ b/packages/bloom/lib/commands/bloom/RESERVE.ts
@@ -1,27 +1,41 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-interface ReserveOptions {
-    EXPANSION?: number;
-    NONSCALING?: true;
+export interface BfReserveOptions {
+  EXPANSION?: number;
+  NONSCALING?: boolean;
 }
 
-export function transformArguments(
-    key: string,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Creates an empty Bloom Filter with a given desired error ratio and initial capacity
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter to create
+   * @param errorRate - The desired probability for false positives (between 0 and 1)
+   * @param capacity - The number of entries intended to be added to the filter
+   * @param options - Optional parameters to tune the filter
+   * @param options.EXPANSION - Expansion rate for the filter
+   * @param options.NONSCALING - Prevents the filter from creating additional sub-filters
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     errorRate: number,
     capacity: number,
-    options?: ReserveOptions
-): Array<string> {
-    const args = ['BF.RESERVE', key, errorRate.toString(), capacity.toString()];
+    options?: BfReserveOptions
+  ) {
+    parser.push('BF.RESERVE');
+    parser.pushKey(key);
+    parser.push(errorRate.toString(), capacity.toString());
 
     if (options?.EXPANSION) {
-        args.push('EXPANSION', options.EXPANSION.toString());
+        parser.push('EXPANSION', options.EXPANSION.toString());
     }
 
     if (options?.NONSCALING) {
-        args.push('NONSCALING');
+        parser.push('NONSCALING');
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts
index 50119590482..a41a6e8e466 100644
--- a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts
+++ b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts
@@ -1,22 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './SCANDUMP';
+import SCANDUMP from './SCANDUMP';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('BF SCANDUMP', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0),
-            ['BF.SCANDUMP', 'key', '0']
-        );
-    });
+describe('BF.SCANDUMP', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SCANDUMP, 'key', 0),
+      ['BF.SCANDUMP', 'key', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.bf.scanDump', async client => {
-        const [, dump] = await Promise.all([
-            client.bf.reserve('key', 0.01, 100),
-            client.bf.scanDump('key', 0)
-        ]);
-        assert.equal(typeof dump, 'object');
-        assert.equal(typeof dump.iterator, 'number');
-        assert.equal(typeof dump.chunk, 'string');
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.bf.scanDump', async client => {
+    const [, dump] = await Promise.all([
+      client.bf.reserve('key', 0.01, 100),
+      client.bf.scanDump('key', 0)
+    ]);
+    assert.equal(typeof dump, 'object');
+    assert.equal(typeof dump.iterator, 'number');
+    assert.equal(typeof dump.chunk, 'string');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.ts
index 04b3edc2a1f..4aebc5c9fc3 100644
--- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts
+++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts
@@ -1,24 +1,23 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, iterator: number): Array<string> {
-    return ['BF.SCANDUMP', key, iterator.toString()];
-}
-
-type ScanDumpRawReply = [
-    iterator: number,
-    chunk: string
-];
-
-interface ScanDumpReply {
-    iterator: number;
-    chunk: string;
-}
-
-export function transformReply([iterator, chunk]: ScanDumpRawReply): ScanDumpReply {
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Begins an incremental save of a Bloom Filter. This is useful for large filters that can't be saved at once
+   * @param parser - The command parser
+   * @param key - The name of the Bloom filter to save
+   * @param iterator - Iterator value; Start at 0, and use the iterator from the response for the next chunk
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) {
+    parser.push('BF.SCANDUMP');
+    parser.pushKey(key);
+    parser.push(iterator.toString());
+  },
+  transformReply(reply: UnwrapReply<TuplesReply<[NumberReply, BlobStringReply]>>) {
     return {
-        iterator,
-        chunk
+      iterator: reply[0],
+      chunk: reply[1]
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/bloom/helpers.ts b/packages/bloom/lib/commands/bloom/helpers.ts
new file mode 100644
index 00000000000..f5b39c71aa8
--- /dev/null
+++ b/packages/bloom/lib/commands/bloom/helpers.ts
@@ -0,0 +1,29 @@
+import { RESP_TYPES, TypeMapping } from "@redis/client";
+
+export function transformInfoV2Reply<T>(reply: Array<any>, typeMapping?: TypeMapping): T {
+  const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined;
+
+  switch (mapType) {
+    case Array: {
+      return reply as unknown as T;
+    }
+    case Map: {
+      const ret = new Map<string, any>();
+
+      for (let i = 0; i < reply.length; i += 2) {
+        ret.set(reply[i].toString(), reply[i + 1]);
+      }
+
+      return ret as unknown as T;
+    }
+    default: {
+      const ret = Object.create(null);
+
+      for (let i = 0; i < reply.length; i += 2) {
+        ret[reply[i].toString()] = reply[i + 1];
+      }
+
+      return ret as unknown as T;
+    }
+  }
+}
\ No newline at end of file
diff --git a/packages/bloom/lib/commands/bloom/index.ts b/packages/bloom/lib/commands/bloom/index.ts
index f18b8f71095..d49ac63b2ea 100644
--- a/packages/bloom/lib/commands/bloom/index.ts
+++ b/packages/bloom/lib/commands/bloom/index.ts
@@ -1,33 +1,37 @@
-import * as ADD from './ADD';
-import * as CARD from './CARD';
-import * as EXISTS from './EXISTS';
-import * as INFO from './INFO';
-import * as INSERT from './INSERT';
-import * as LOADCHUNK from './LOADCHUNK';
-import * as MADD from './MADD';
-import * as MEXISTS from './MEXISTS';
-import * as RESERVE from './RESERVE';
-import * as SCANDUMP from './SCANDUMP';
+import type { RedisCommands } from '@redis/client/dist/lib/RESP/types';
+
+import ADD from './ADD';
+import CARD from './CARD';
+import EXISTS from './EXISTS';
+import INFO from './INFO';
+import INSERT from './INSERT';
+import LOADCHUNK from './LOADCHUNK';
+import MADD from './MADD';
+import MEXISTS from './MEXISTS';
+import RESERVE from './RESERVE';
+import SCANDUMP from './SCANDUMP';
+
+export * from './helpers';
 
 export default {
-    ADD,
-    add: ADD,
-    CARD,
-    card: CARD,
-    EXISTS,
-    exists: EXISTS,
-    INFO,
-    info: INFO,
-    INSERT,
-    insert: INSERT,
-    LOADCHUNK,
-    loadChunk: LOADCHUNK,
-    MADD,
-    mAdd: MADD,
-    MEXISTS,
-    mExists: MEXISTS,
-    RESERVE,
-    reserve: RESERVE,
-    SCANDUMP,
-    scanDump: SCANDUMP
-};
+  ADD,
+  add: ADD,
+  CARD,
+  card: CARD,
+  EXISTS,
+  exists: EXISTS,
+  INFO,
+  info: INFO,
+  INSERT,
+  insert: INSERT,
+  LOADCHUNK,
+  loadChunk: LOADCHUNK,
+  MADD,
+  mAdd: MADD,
+  MEXISTS,
+  mExists: MEXISTS,
+  RESERVE,
+  reserve: RESERVE,
+  SCANDUMP,
+  scanDump: SCANDUMP
+} as const satisfies RedisCommands;
diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts
index 95bb28e88b5..44ccaf6046d 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts
@@ -1,41 +1,43 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INCRBY';
+import INCRBY from './INCRBY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CMS INCRBY', () => {
-    describe('transformArguments', () => {
-        it('single item', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    item: 'item',
-                    incrementBy: 1
-                }),
-                ['CMS.INCRBY', 'key', 'item', '1']
-            );
-        });
+describe('CMS.INCRBY', () => {
+  describe('transformArguments', () => {
+    it('single item', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', {
+          item: 'item',
+          incrementBy: 1
+        }),
+        ['CMS.INCRBY', 'key', 'item', '1']
+      );
+    });
 
-        it('multiple items', () => {
-            assert.deepEqual(
-                transformArguments('key', [{
-                    item: 'a',
-                    incrementBy: 1
-                }, {
-                    item: 'b',
-                    incrementBy: 2
-                }]),
-                ['CMS.INCRBY', 'key', 'a', '1', 'b', '2']
-            );
-        });
+    it('multiple items', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', [{
+          item: 'a',
+          incrementBy: 1
+        }, {
+          item: 'b',
+          incrementBy: 2
+        }]),
+        ['CMS.INCRBY', 'key', 'a', '1', 'b', '2']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.cms.incrBy', async client => {
+    const [, reply] = await Promise.all([
+      client.cms.initByDim('key', 1000, 5),
+      client.cms.incrBy('key', {
+        item: 'item',
+        incrementBy: 1
+      })
+    ]);
 
-    testUtils.testWithClient('client.cms.incrBy', async client => {
-        await client.cms.initByDim('key', 1000, 5);
-        assert.deepEqual(
-            await client.cms.incrBy('key', {
-                item: 'item',
-                incrementBy: 1
-            }),
-            [1]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [1]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts
index e27fb397cdf..145047b207a 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts
@@ -1,29 +1,38 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-interface IncrByItem {
-    item: string;
-    incrementBy: number;
+export interface BfIncrByItem {
+  item: RedisArgument;
+  incrementBy: number;
 }
 
-export function transformArguments(
-    key: string,
-    items: IncrByItem | Array<IncrByItem>
-): Array<string> {
-    const args = ['CMS.INCRBY', key];
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Increases the count of one or more items in a Count-Min Sketch
+   * @param parser - The command parser
+   * @param key - The name of the sketch
+   * @param items - A single item or array of items to increment, each with an item and increment value
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    items: BfIncrByItem | Array<BfIncrByItem>
+  ) {
+    parser.push('CMS.INCRBY');
+    parser.pushKey(key);
 
     if (Array.isArray(items)) {
-        for (const item of items) {
-            pushIncrByItem(args, item);
-        }
+      for (const item of items) {
+        pushIncrByItem(parser, item);
+      }
     } else {
-        pushIncrByItem(args, items);
+      pushIncrByItem(parser, items);
     }
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
 
-    return args;
+function pushIncrByItem(parser: CommandParser, { item, incrementBy }: BfIncrByItem): void {
+  parser.push(item, incrementBy.toString());
 }
-
-function pushIncrByItem(args: Array<string>, { item, incrementBy }: IncrByItem): void {
-    args.push(item, incrementBy.toString());
-}
-
-export declare function transformReply(): Array<number>;
diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts
index 0db8a48447e..cbc8065016a 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts
@@ -1,25 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INFO';
+import INFO from './INFO';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CMS INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['CMS.INFO', 'key']
-        );
-    });
+describe('CMS.INFO', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INFO, 'key'),
+      ['CMS.INFO', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.cms.info', async client => {
-        await client.cms.initByDim('key', 1000, 5);
+  testUtils.testWithClient('client.cms.info', async client => {
+    const width = 1000,
+      depth = 5,
+      [, reply] = await Promise.all([
+        client.cms.initByDim('key', width, depth),
+        client.cms.info('key')
+      ]);
 
-        assert.deepEqual(
-            await client.cms.info('key'),
-            {
-                width: 1000,
-                depth: 5,
-                count: 0
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    const expected = Object.create(null);
+    expected['width'] = width;
+    expected['depth'] = depth;
+    expected['count'] = 0;
+
+    assert.deepEqual(reply, expected);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.ts
index 6dbfffcb0e0..1f188bda013 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts
@@ -1,30 +1,34 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { transformInfoV2Reply } from '../bloom';
 
-export const IS_READ_ONLY = true;
+export type CmsInfoReplyMap = TuplesToMapReply<[
+  [SimpleStringReply<'width'>, NumberReply],
+  [SimpleStringReply<'depth'>, NumberReply],
+  [SimpleStringReply<'count'>, NumberReply]
+]>;
 
-export function transformArguments(key: string): Array<string> {
-    return ['CMS.INFO', key];
-}
-
-export type InfoRawReply = [
-    _: string,
-    width: number,
-    _: string,
-    depth: number,
-    _: string,
-    count: number
-];
-
-export interface InfoReply {
-    width: number;
-    depth: number;
-    count: number;
-}
-
-export function transformReply(reply: InfoRawReply): InfoReply {
-    return {
-        width: reply[1],
-        depth: reply[3],
-        count: reply[5]
-    };
+export interface CmsInfoReply {
+  width: NumberReply;
+  depth: NumberReply;
+  count: NumberReply;
 }
+ 
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns width, depth, and total count of items in a Count-Min Sketch
+   * @param parser - The command parser
+   * @param key - The name of the sketch to get information about
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('CMS.INFO');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<CmsInfoReplyMap>>, _, typeMapping?: TypeMapping): CmsInfoReply => {
+      return transformInfoV2Reply<CmsInfoReply>(reply, typeMapping);
+    },
+    3: undefined as unknown as () => CmsInfoReply
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts
index 2a9014b765a..9fa1652a2e8 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INITBYDIM';
+import INITBYDIM from './INITBYDIM';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CMS INITBYDIM', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1000, 5),
-            ['CMS.INITBYDIM', 'key', '1000', '5']
-        );
-    });
+describe('CMS.INITBYDIM', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INITBYDIM, 'key', 1000, 5),
+      ['CMS.INITBYDIM', 'key', '1000', '5']
+    );
+  });
 
-    testUtils.testWithClient('client.cms.initByDim', async client => {
-        assert.equal(
-            await client.cms.initByDim('key', 1000, 5),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cms.initByDim', async client => {
+    assert.equal(
+      await client.cms.initByDim('key', 1000, 5),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts
index 4ec6cedd9ea..2bf9d97f208 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, width: number, depth: number): Array<string> {
-    return ['CMS.INITBYDIM', key, width.toString(), depth.toString()];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Initialize a Count-Min Sketch using width and depth parameters
+   * @param parser - The command parser
+   * @param key - The name of the sketch
+   * @param width - Number of counters in each array (must be a multiple of 2)
+   * @param depth - Number of counter arrays (determines accuracy of estimates)
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, width: number, depth: number) {
+    parser.push('CMS.INITBYDIM');
+    parser.pushKey(key);
+    parser.push(width.toString(), depth.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts
index 004d3df14ef..b59bc14494f 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INITBYPROB';
+import INITBYPROB from './INITBYPROB';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CMS INITBYPROB', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0.001, 0.01),
-            ['CMS.INITBYPROB', 'key', '0.001', '0.01']
-        );
-    });
+describe('CMS.INITBYPROB', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INITBYPROB, 'key', 0.001, 0.01),
+      ['CMS.INITBYPROB', 'key', '0.001', '0.01']
+    );
+  });
 
-    testUtils.testWithClient('client.cms.initByProb', async client => {
-        assert.equal(
-            await client.cms.initByProb('key', 0.001, 0.01),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cms.initByProb', async client => {
+    assert.equal(
+      await client.cms.initByProb('key', 0.001, 0.01),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts
index 7f0256515fb..180781d91e8 100644
--- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, error: number, probability: number): Array<string> {
-    return ['CMS.INITBYPROB', key, error.toString(), probability.toString()];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Initialize a Count-Min Sketch using error rate and probability parameters
+   * @param parser - The command parser
+   * @param key - The name of the sketch
+   * @param error - Estimate error, as a decimal between 0 and 1
+   * @param probability - The desired probability for inflated count, as a decimal between 0 and 1
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, error: number, probability: number) {
+    parser.push('CMS.INITBYPROB');
+    parser.pushKey(key);
+    parser.push(error.toString(), probability.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts
index cf234e5734f..03e3d5c6364 100644
--- a/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts
@@ -1,36 +1,35 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './MERGE';
+import MERGE from './MERGE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CMS MERGE', () => {
-    describe('transformArguments', () => {
-        it('without WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('dest', ['src']),
-                ['CMS.MERGE', 'dest', '1', 'src']
-            );
-        });
+describe('CMS.MERGE', () => {
+  describe('transformArguments', () => {
+    it('without WEIGHTS', () => {
+      assert.deepEqual(
+        parseArgs(MERGE, 'destination', ['source']),
+        ['CMS.MERGE', 'destination', '1', 'source']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('dest', [{
-                    name: 'src',
-                    weight: 1
-                }]),
-                ['CMS.MERGE', 'dest', '1', 'src', 'WEIGHTS', '1']
-            );
-        });
+    it('with WEIGHTS', () => {
+      assert.deepEqual(
+        parseArgs(MERGE, 'destination', [{
+          name: 'source',
+          weight: 1
+        }]),
+        ['CMS.MERGE', 'destination', '1', 'source', 'WEIGHTS', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.cms.merge', async client => {
-        await Promise.all([
-            client.cms.initByDim('src', 1000, 5),
-            client.cms.initByDim('dest', 1000, 5),
-        ]);
+  testUtils.testWithClient('client.cms.merge', async client => {
+    const [, , reply] = await Promise.all([
+      client.cms.initByDim('source', 1000, 5),
+      client.cms.initByDim('destination', 1000, 5),
+      client.cms.merge('destination', ['source'])
+    ]);
 
-        assert.equal(
-            await client.cms.merge('dest', ['src']),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts
index 6cca4e797cd..e4921c7975f 100644
--- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts
@@ -1,37 +1,45 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-interface Sketch {
-    name: string;
-    weight: number;
+interface BfMergeSketch {
+  name: RedisArgument;
+  weight: number;
 }
 
-type Sketches = Array<string> | Array<Sketch>;
-
-export function transformArguments(dest: string, src: Sketches): Array<string> {
-    const args = [
-        'CMS.MERGE',
-        dest,
-        src.length.toString()
-    ];
-
-    if (isStringSketches(src)) {
-        args.push(...src);
+export type BfMergeSketches = Array<RedisArgument> | Array<BfMergeSketch>;
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Merges multiple Count-Min Sketches into a single sketch, with optional weights
+   * @param parser - The command parser
+   * @param destination - The name of the destination sketch
+   * @param source - Array of sketch names or array of sketches with weights
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    source: BfMergeSketches
+  ) {
+    parser.push('CMS.MERGE');
+    parser.pushKey(destination);
+    parser.push(source.length.toString());
+
+    if (isPlainSketches(source)) {
+      parser.pushVariadic(source);
     } else {
-        for (const sketch of src) {
-            args.push(sketch.name);
-        }
-
-        args.push('WEIGHTS');
-        for (const sketch of src) {
-            args.push(sketch.weight.toString());
-        }
+      for (let i = 0; i < source.length; i++) {
+        parser.push(source[i].name);
+      }
+      parser.push('WEIGHTS');
+      for (let i = 0; i < source.length; i++) {
+        parser.push(source[i].weight.toString())
+      }
     }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-    return args;
+function isPlainSketches(src: BfMergeSketches): src is Array<RedisArgument> {
+  return typeof src[0] === 'string' || src[0] instanceof Buffer;
 }
-
-function isStringSketches(src: Sketches): src is Array<string> {
-    return typeof src[0] === 'string';
-}
-
-export declare function transformReply(): 'OK';
diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts
index d391ab838be..e12a519e962 100644
--- a/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts
@@ -1,22 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './QUERY';
+import QUERY from './QUERY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CMS QUERY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['CMS.QUERY', 'key', 'item']
-        );
-    });
+describe('CMS.QUERY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(QUERY, 'key', 'item'),
+      ['CMS.QUERY', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cms.query', async client => {
-        await client.cms.initByDim('key', 1000, 5);
+  testUtils.testWithClient('client.cms.query', async client => {
+    const [, reply] = await Promise.all([
+      client.cms.initByDim('key', 1000, 5),
+      client.cms.query('key', 'item')
+    ]);
 
-        assert.deepEqual(
-            await client.cms.query('key', 'item'),
-            [0]
-        );
-
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [0]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts
index 13a71c9b1de..4322b0470c0 100644
--- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts
@@ -1,15 +1,19 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: string,
-    items: string | Array<string>
-): RedisCommandArguments {
-    return pushVerdictArguments(['CMS.QUERY', key], items);
-}
-
-export declare function transformReply(): Array<number>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the count for one or more items in a Count-Min Sketch
+   * @param parser - The command parser
+   * @param key - The name of the sketch
+   * @param items - One or more items to get counts for
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) {
+    parser.push('CMS.QUERY');
+    parser.pushKey(key);
+    parser.pushVariadic(items);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/count-min-sketch/index.ts b/packages/bloom/lib/commands/count-min-sketch/index.ts
index 1d61734a8d0..4f0f395ca3d 100644
--- a/packages/bloom/lib/commands/count-min-sketch/index.ts
+++ b/packages/bloom/lib/commands/count-min-sketch/index.ts
@@ -1,21 +1,22 @@
-import * as INCRBY from './INCRBY';
-import * as INFO from './INFO';
-import * as INITBYDIM from './INITBYDIM';
-import * as INITBYPROB from './INITBYPROB';
-import * as MERGE from './MERGE';
-import * as QUERY from './QUERY';
+import type { RedisCommands } from '@redis/client/dist/lib/RESP/types';
+import INCRBY from './INCRBY';
+import INFO from './INFO';
+import INITBYDIM from './INITBYDIM';
+import INITBYPROB from './INITBYPROB';
+import MERGE from './MERGE';
+import QUERY from './QUERY';
 
 export default {
-    INCRBY,
-    incrBy: INCRBY,
-    INFO,
-    info: INFO,
-    INITBYDIM,
-    initByDim: INITBYDIM,
-    INITBYPROB,
-    initByProb: INITBYPROB,
-    MERGE,
-    merge: MERGE,
-    QUERY,
-    query: QUERY
-};
+  INCRBY,
+  incrBy: INCRBY,
+  INFO,
+  info: INFO,
+  INITBYDIM,
+  initByDim: INITBYDIM,
+  INITBYPROB,
+  initByProb: INITBYPROB,
+  MERGE,
+  merge: MERGE,
+  QUERY,
+  query: QUERY
+} as const satisfies RedisCommands;
diff --git a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts
index f2c029fad3d..7fa518fea84 100644
--- a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments, transformReply } from './ADD';
+import ADD from './ADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF ADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['CF.ADD', 'key', 'item']
-        );
-    });
+describe('CF.ADD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ADD, 'key', 'item'),
+      ['CF.ADD', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.add', async client => {
-        assert.equal(
-            await client.cf.add('key', 'item'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.add', async client => {
+    assert.equal(
+      await client.cf.add('key', 'item'),
+      true
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/ADD.ts b/packages/bloom/lib/commands/cuckoo/ADD.ts
index 8d16c0f2ed0..db16acdea5a 100644
--- a/packages/bloom/lib/commands/cuckoo/ADD.ts
+++ b/packages/bloom/lib/commands/cuckoo/ADD.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['CF.ADD', key, item];
-}
-
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds an item to a Cuckoo Filter, creating the filter if it does not exist
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter
+   * @param item - The item to add to the filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('CF.ADD');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts
index ddd9f922b13..c142733ce40 100644
--- a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts
@@ -1,21 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './ADDNX';
+import ADDNX from './ADDNX';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF ADDNX', () => {
-    describe('transformArguments', () => {
-        it('basic add', () => {
-            assert.deepEqual(
-                transformArguments('key', 'item'),
-                ['CF.ADDNX', 'key', 'item']
-            );
-        });
-    });
+describe('CF.ADDNX', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ADDNX, 'key', 'item'),
+      ['CF.ADDNX', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.add', async client => {
-        assert.equal(
-            await client.cf.addNX('key', 'item'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.add', async client => {
+    assert.equal(
+      await client.cf.addNX('key', 'item'),
+      true
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.ts
index 789003a3a57..ef6e1222e7f 100644
--- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts
+++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['CF.ADDNX', key, item];
-}
-
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds an item to a Cuckoo Filter only if it does not exist
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter
+   * @param item - The item to add to the filter if it doesn't exist
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('CF.ADDNX');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts
index 29f5b415935..9393494d852 100644
--- a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './COUNT';
+import COUNT from './COUNT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF COUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['CF.COUNT', 'key', 'item']
-        );
-    });
+describe('CF.COUNT', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(COUNT, 'key', 'item'),
+      ['CF.COUNT', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.count', async client => {
-        assert.equal(
-            await client.cf.count('key', 'item'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.count', async client => {
+    assert.equal(
+      await client.cf.count('key', 'item'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.ts b/packages/bloom/lib/commands/cuckoo/COUNT.ts
index c9f3e28b38a..e06d71f73e9 100644
--- a/packages/bloom/lib/commands/cuckoo/COUNT.ts
+++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts
@@ -1,7 +1,18 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['CF.COUNT', key, item];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of times an item appears in a Cuckoo Filter
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter
+   * @param item - The item to count occurrences of
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('CF.COUNT');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts
index 03da65881c1..41ed653bfc9 100644
--- a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './DEL';
+import DEL from './DEL';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF DEL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['CF.DEL', 'key', 'item']
-        );
-    });
+describe('CF.DEL', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DEL, 'key', 'item'),
+      ['CF.DEL', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.del', async client => {
-        await client.cf.reserve('key', 4);
+  testUtils.testWithClient('client.cf.del', async client => {
+    const [, reply] = await Promise.all([
+      client.cf.reserve('key', 4),
+      client.cf.del('key', 'item')
+    ]);
 
-        assert.equal(
-            await client.cf.del('key', 'item'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, false);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/DEL.ts b/packages/bloom/lib/commands/cuckoo/DEL.ts
index 1c395a515a8..651a5bd8a8a 100644
--- a/packages/bloom/lib/commands/cuckoo/DEL.ts
+++ b/packages/bloom/lib/commands/cuckoo/DEL.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['CF.DEL', key, item];
-}
-
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes an item from a Cuckoo Filter if it exists
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter
+   * @param item - The item to remove from the filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('CF.DEL');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts
index e281bde6d8a..f77a9d69eff 100644
--- a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './EXISTS';
+import EXISTS from './EXISTS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF EXISTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['CF.EXISTS', 'key', 'item']
-        );
-    });
+describe('CF.EXISTS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EXISTS, 'key', 'item'),
+      ['CF.EXISTS', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.exists', async client => {
-        assert.equal(
-            await client.cf.exists('key', 'item'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.exists', async client => {
+    assert.equal(
+      await client.cf.exists('key', 'item'),
+      false
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.ts
index b50a1e25a87..820143ae886 100644
--- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts
+++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts
@@ -1,9 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, item: string): Array<string> {
-    return ['CF.EXISTS', key, item];
-}
-
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Checks if an item exists in a Cuckoo Filter
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter
+   * @param item - The item to check for existence
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) {
+    parser.push('CF.EXISTS');
+    parser.pushKey(key);
+    parser.push(item);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts b/packages/bloom/lib/commands/cuckoo/INFO.spec.ts
index c2ac5de6fe0..c5503ed113b 100644
--- a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/INFO.spec.ts
@@ -1,27 +1,30 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INFO';
+import INFO from './INFO';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('cuckoo'),
-            ['CF.INFO', 'cuckoo']
-        );
-    });
+describe('CF.INFO', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INFO, 'cuckoo'),
+      ['CF.INFO', 'cuckoo']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.info', async client => {
-        await client.cf.reserve('key', 4);
+  testUtils.testWithClient('client.cf.info', async client => {
+    const [, reply] = await Promise.all([
+      client.cf.reserve('key', 4),
+      client.cf.info('key')
+    ]);
 
-        const info = await client.cf.info('key');
-        assert.equal(typeof info, 'object');
-        assert.equal(typeof info.size, 'number');
-        assert.equal(typeof info.numberOfBuckets, 'number');
-        assert.equal(typeof info.numberOfFilters, 'number');
-        assert.equal(typeof info.numberOfInsertedItems, 'number');
-        assert.equal(typeof info.numberOfDeletedItems, 'number');
-        assert.equal(typeof info.bucketSize, 'number');
-        assert.equal(typeof info.expansionRate, 'number');
-        assert.equal(typeof info.maxIteration, 'number');
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(typeof reply, 'object');
+    assert.equal(typeof reply['Size'], 'number');
+    assert.equal(typeof reply['Number of buckets'], 'number');
+    assert.equal(typeof reply['Number of filters'], 'number');
+    assert.equal(typeof reply['Number of items inserted'], 'number');
+    assert.equal(typeof reply['Number of items deleted'], 'number');
+    assert.equal(typeof reply['Bucket size'], 'number');
+    assert.equal(typeof reply['Expansion rate'], 'number');
+    assert.equal(typeof reply['Max iterations'], 'number');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/INFO.ts b/packages/bloom/lib/commands/cuckoo/INFO.ts
index 04d6954e37a..88622b5cb19 100644
--- a/packages/bloom/lib/commands/cuckoo/INFO.ts
+++ b/packages/bloom/lib/commands/cuckoo/INFO.ts
@@ -1,50 +1,33 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { transformInfoV2Reply } from '../bloom';
 
-export const IS_READ_ONLY = true;
+export type CfInfoReplyMap = TuplesToMapReply<[
+  [SimpleStringReply<'Size'>, NumberReply],
+  [SimpleStringReply<'Number of buckets'>, NumberReply],
+  [SimpleStringReply<'Number of filters'>, NumberReply],
+  [SimpleStringReply<'Number of items inserted'>, NumberReply],
+  [SimpleStringReply<'Number of items deleted'>, NumberReply],
+  [SimpleStringReply<'Bucket size'>, NumberReply],
+  [SimpleStringReply<'Expansion rate'>, NumberReply],
+  [SimpleStringReply<'Max iterations'>, NumberReply]
+]>;
 
-export function transformArguments(key: string): Array<string> {
-    return ['CF.INFO', key];
-}
-
-export type InfoRawReply = [
-    _: string,
-    size: number,
-    _: string,
-    numberOfBuckets: number,
-    _: string,
-    numberOfFilters: number,
-    _: string,
-    numberOfInsertedItems: number,
-    _: string,
-    numberOfDeletedItems: number,
-    _: string,
-    bucketSize: number,
-    _: string,
-    expansionRate: number,
-    _: string,
-    maxIteration: number
-];
-
-export interface InfoReply {
-    size: number;
-    numberOfBuckets: number;
-    numberOfFilters: number;
-    numberOfInsertedItems: number;
-    numberOfDeletedItems: number;
-    bucketSize: number;
-    expansionRate: number;
-    maxIteration: number;
-}
-
-export function transformReply(reply: InfoRawReply): InfoReply {
-    return {
-        size: reply[1],
-        numberOfBuckets: reply[3],
-        numberOfFilters: reply[5],
-        numberOfInsertedItems: reply[7],
-        numberOfDeletedItems: reply[9],
-        bucketSize: reply[11],
-        expansionRate: reply[13],
-        maxIteration: reply[15]
-    };
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns detailed information about a Cuckoo Filter including size, buckets, filters count, items statistics and configuration
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter to get information about
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('CF.INFO');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<CfInfoReplyMap>>, _, typeMapping?: TypeMapping): CfInfoReplyMap => {
+      return transformInfoV2Reply<CfInfoReplyMap>(reply, typeMapping);
+    },
+    3: undefined as unknown as () => CfInfoReplyMap
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts
index 9b56b86a6b7..dc2bd574517 100644
--- a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts
@@ -1,22 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INSERT';
+import INSERT from './INSERT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF INSERT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item', {
-                CAPACITY: 100,
-                NOCREATE: true
-             }),
-            ['CF.INSERT', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item']
-        );
-    });
+describe('CF.INSERT', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INSERT, 'key', 'item', {
+        CAPACITY: 100,
+        NOCREATE: true
+      }),
+      ['CF.INSERT', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.insert', async client => {
-        assert.deepEqual(
-            await client.cf.insert('key', 'item'),
-            [true]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.insert', async client => {
+    assert.deepEqual(
+      await client.cf.insert('key', 'item'),
+      [true]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.ts b/packages/bloom/lib/commands/cuckoo/INSERT.ts
index bcfd4f13a88..277c820cbcd 100644
--- a/packages/bloom/lib/commands/cuckoo/INSERT.ts
+++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts
@@ -1,18 +1,46 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { InsertOptions, pushInsertOptions } from ".";
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export interface CfInsertOptions {
+  CAPACITY?: number;
+  NOCREATE?: boolean;
+}
+
+export function parseCfInsertArguments(
+  parser: CommandParser, 
+  key: RedisArgument,
+  items: RedisVariadicArgument,
+  options?: CfInsertOptions
+) {
+  parser.pushKey(key);
+
+  if (options?.CAPACITY !== undefined) {
+    parser.push('CAPACITY', options.CAPACITY.toString());
+  }
+
+  if (options?.NOCREATE) {
+    parser.push('NOCREATE');
+  }
 
-export function transformArguments(
-    key: string,
-    items: string | Array<string>,
-    options?: InsertOptions
-): RedisCommandArguments {
-    return pushInsertOptions(
-        ['CF.INSERT', key],
-        items,
-        options
-    );
+  parser.push('ITEMS');
+  parser.pushVariadic(items);
 }
 
-export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds one or more items to a Cuckoo Filter, creating it if it does not exist
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter
+   * @param items - One or more items to add to the filter
+   * @param options - Optional parameters for filter creation
+   * @param options.CAPACITY - The number of entries intended to be added to the filter
+   * @param options.NOCREATE - If true, prevents automatic filter creation
+   */
+  parseCommand(...args: Parameters<typeof parseCfInsertArguments>) {
+    args[0].push('CF.INSERT');
+    parseCfInsertArguments(...args);
+  },
+  transformReply: transformBooleanArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts
index 7b1d974e5a6..648d9be7ac8 100644
--- a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts
@@ -1,22 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INSERTNX';
+import INSERTNX from './INSERTNX';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF INSERTNX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item', {
-                CAPACITY: 100,
-                NOCREATE: true
-             }),
-            ['CF.INSERTNX', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item']
-        );
-    });
+describe('CF.INSERTNX', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INSERTNX, 'key', 'item', {
+        CAPACITY: 100,
+        NOCREATE: true
+      }),
+      ['CF.INSERTNX', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.insertnx', async client => {
-        assert.deepEqual(
-            await client.cf.insertNX('key', 'item'),
-            [true]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.insertnx', async client => {
+    assert.deepEqual(
+      await client.cf.insertNX('key', 'item'),
+      [true]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts
index 17009e35a42..bf99db6c3f7 100644
--- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts
+++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts
@@ -1,18 +1,20 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { InsertOptions, pushInsertOptions } from ".";
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import INSERT, { parseCfInsertArguments } from './INSERT';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: string,
-    items: string | Array<string>,
-    options?: InsertOptions
-): RedisCommandArguments {
-    return pushInsertOptions(
-        ['CF.INSERTNX', key],
-        items,
-        options
-    );
-}
-
-export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+/**
+ * Adds one or more items to a Cuckoo Filter only if they do not exist yet, creating the filter if needed
+ * @param parser - The command parser
+ * @param key - The name of the Cuckoo filter
+ * @param items - One or more items to add to the filter
+ * @param options - Optional parameters for filter creation
+ * @param options.CAPACITY - The number of entries intended to be added to the filter
+ * @param options.NOCREATE - If true, prevents automatic filter creation
+ */
+export default {
+  IS_READ_ONLY: INSERT.IS_READ_ONLY,
+  parseCommand(...args: Parameters<typeof parseCfInsertArguments>) {
+    args[0].push('CF.INSERTNX');
+    parseCfInsertArguments(...args);
+  },
+  transformReply: INSERT.transformReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts
index ca3d6f2f8f7..5415c787dda 100644
--- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts
@@ -1,31 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './LOADCHUNK';
+import LOADCHUNK from './LOADCHUNK';
+import { RESP_TYPES } from '@redis/client';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF LOADCHUNK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('item', 0, ''),
-            ['CF.LOADCHUNK', 'item', '0', '']
-        );
-    });
+describe('CF.LOADCHUNK', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LOADCHUNK, 'item', 0, ''),
+      ['CF.LOADCHUNK', 'item', '0', '']
+    );
+  });
 
-    testUtils.testWithClient('client.cf.loadChunk', async client => {
-        const [,, { iterator, chunk }] = await Promise.all([
-            client.cf.reserve('source', 4),
-            client.cf.add('source', 'item'),
-            client.cf.scanDump(
-                client.commandOptions({ returnBuffers: true }),
-                'source',
-                0
-            )
-        ]);
+  testUtils.testWithClient('client.cf.loadChunk', async client => {
+    const [, , { iterator, chunk }] = await Promise.all([
+      client.cf.reserve('source', 4),
+      client.cf.add('source', 'item'),
+      client.cf.scanDump('source', 0)
+    ]);
 
-        assert.ok(Buffer.isBuffer(chunk));
-
-        assert.equal(
-            await client.cf.loadChunk('destination', iterator, chunk),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(
+      await client.cf.loadChunk('destination', iterator, chunk!),
+      'OK'
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      ...GLOBAL.SERVERS.OPEN.clientOptions,
+      commandOptions: {
+        typeMapping: {
+          [RESP_TYPES.BLOB_STRING]: Buffer
+        }
+      }
+    }
+  });
 });
diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts
index 6d960c014e2..3a966e5145a 100644
--- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts
+++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: string,
-    iterator: number,
-    chunk: RedisCommandArgument
-): RedisCommandArguments {
-    return ['CF.LOADCHUNK', key, iterator.toString(), chunk];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Restores a Cuckoo Filter chunk previously saved using SCANDUMP
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter to restore
+   * @param iterator - Iterator value from the SCANDUMP command
+   * @param chunk - Data chunk from the SCANDUMP command
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) {
+    parser.push('CF.LOADCHUNK');
+    parser.pushKey(key);
+    parser.push(iterator.toString(), chunk);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts
index 3145a222c5e..53546e4156e 100644
--- a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts
@@ -1,48 +1,49 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './RESERVE';
+import RESERVE from './RESERVE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF RESERVE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 4),
-                ['CF.RESERVE', 'key', '4']
-            );
-        });
+describe('CF.RESERVE', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 4),
+        ['CF.RESERVE', 'key', '4']
+      );
+    });
 
-        it('with EXPANSION', () => {
-            assert.deepEqual(
-                transformArguments('key', 4, {
-                    EXPANSION: 1
-                }),
-                ['CF.RESERVE', 'key', '4', 'EXPANSION', '1']
-            );
-        });
+    it('with EXPANSION', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 4, {
+          EXPANSION: 1
+        }),
+        ['CF.RESERVE', 'key', '4', 'EXPANSION', '1']
+      );
+    });
 
-        it('with BUCKETSIZE', () => {
-            assert.deepEqual(
-                transformArguments('key', 4, {
-                    BUCKETSIZE: 2
-                }),
-                ['CF.RESERVE', 'key', '4', 'BUCKETSIZE', '2']
-            );
-        });
+    it('with BUCKETSIZE', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 4, {
+          BUCKETSIZE: 2
+        }),
+        ['CF.RESERVE', 'key', '4', 'BUCKETSIZE', '2']
+      );
+    });
 
-        it('with MAXITERATIONS', () => {
-            assert.deepEqual(
-                transformArguments('key', 4, {
-                    MAXITERATIONS: 1
-                }),
-                ['CF.RESERVE', 'key', '4', 'MAXITERATIONS', '1']
-            );
-        });
+    it('with MAXITERATIONS', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'key', 4, {
+          MAXITERATIONS: 1
+        }),
+        ['CF.RESERVE', 'key', '4', 'MAXITERATIONS', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.cf.reserve', async client => {
-        assert.equal(
-            await client.cf.reserve('key', 4),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.cf.reserve', async client => {
+    assert.equal(
+      await client.cf.reserve('key', 4),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.ts
index 114c1fdf441..26e31a1c645 100644
--- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts
+++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts
@@ -1,31 +1,45 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-interface ReserveOptions {
-    BUCKETSIZE?: number;
-    MAXITERATIONS?: number;
-    EXPANSION?: number;
+export interface CfReserveOptions {
+  BUCKETSIZE?: number;
+  MAXITERATIONS?: number;
+  EXPANSION?: number;
 }
 
-export function transformArguments(
-    key: string,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Creates an empty Cuckoo Filter with specified capacity and parameters
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter to create
+   * @param capacity - The number of entries intended to be added to the filter
+   * @param options - Optional parameters to tune the filter
+   * @param options.BUCKETSIZE - Number of items in each bucket
+   * @param options.MAXITERATIONS - Maximum number of iterations before declaring filter full
+   * @param options.EXPANSION - Number of additional buckets per expansion
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     capacity: number,
-    options?: ReserveOptions
-): Array<string> {
-    const args = ['CF.RESERVE', key, capacity.toString()];
+    options?: CfReserveOptions
+  ) {
+    parser.push('CF.RESERVE');
+    parser.pushKey(key);
+    parser.push(capacity.toString());
 
-    if (options?.BUCKETSIZE) {
-        args.push('BUCKETSIZE', options.BUCKETSIZE.toString());
+    if (options?.BUCKETSIZE !== undefined) {
+      parser.push('BUCKETSIZE', options.BUCKETSIZE.toString());
     }
 
-    if (options?.MAXITERATIONS) {
-        args.push('MAXITERATIONS', options.MAXITERATIONS.toString());
+    if (options?.MAXITERATIONS !== undefined) {
+      parser.push('MAXITERATIONS', options.MAXITERATIONS.toString());
     }
 
-    if (options?.EXPANSION) {
-        args.push('EXPANSION', options.EXPANSION.toString());
+    if (options?.EXPANSION !== undefined) {
+      parser.push('EXPANSION', options.EXPANSION.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts
index ec269c62aa5..60a57ac46ab 100644
--- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts
+++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts
@@ -1,23 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './SCANDUMP';
+import SCANDUMP from './SCANDUMP';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CF SCANDUMP', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0),
-            ['CF.SCANDUMP', 'key', '0']
-        );
-    });
+describe('CF.SCANDUMP', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SCANDUMP, 'key', 0),
+      ['CF.SCANDUMP', 'key', '0']
+    );
+  });
+
+  testUtils.testWithClient('client.cf.scanDump', async client => {
+    const [, reply] = await Promise.all([
+      client.cf.reserve('key', 4),
+      client.cf.scanDump('key', 0)
+    ]);
 
-    testUtils.testWithClient('client.cf.scanDump', async client => {
-        await client.cf.reserve('key', 4);
-        assert.deepEqual(
-            await client.cf.scanDump('key', 0),
-            {
-                iterator: 0,
-                chunk: null
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {
+      iterator: 0,
+      chunk: null
+    });
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts
index 91476b49a7a..96a036671f4 100644
--- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts
+++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts
@@ -1,22 +1,23 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, iterator: number): Array<string> {
-    return ['CF.SCANDUMP', key, iterator.toString()];
-}
-
-type ScanDumpRawReply = [
-    iterator: number,
-    chunk: string | null
-];
-
-interface ScanDumpReply {
-    iterator: number;
-    chunk: string | null;
-}
-
-export function transformReply([iterator, chunk]: ScanDumpRawReply): ScanDumpReply {
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Begins an incremental save of a Cuckoo Filter. This is useful for large filters that can't be saved at once
+   * @param parser - The command parser
+   * @param key - The name of the Cuckoo filter to save
+   * @param iterator - Iterator value; Start at 0, and use the iterator from the response for the next chunk
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) {
+    parser.push('CF.SCANDUMP');
+    parser.pushKey(key);
+    parser.push(iterator.toString());
+  },
+  transformReply(reply: UnwrapReply<TuplesReply<[NumberReply, BlobStringReply | NullReply]>>) {
     return {
-        iterator,
-        chunk
+      iterator: reply[0],
+      chunk: reply[1]
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/cuckoo/index.spec.ts b/packages/bloom/lib/commands/cuckoo/index.spec.ts
deleted file mode 100644
index 94f3a0ae281..00000000000
--- a/packages/bloom/lib/commands/cuckoo/index.spec.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { strict as assert } from 'assert';
-import { pushInsertOptions } from '.';
-
-describe('pushInsertOptions', () => {
-    describe('single item', () => {
-        it('single item', () => {
-            assert.deepEqual(
-                pushInsertOptions([], 'item'),
-                ['ITEMS', 'item']
-            );
-        });
-
-        it('multiple items', () => {
-            assert.deepEqual(
-                pushInsertOptions([], ['1', '2']),
-                ['ITEMS', '1', '2']
-            );
-        });
-    });
-
-    it('with CAPACITY', () => {
-        assert.deepEqual(
-            pushInsertOptions([], 'item', {
-                CAPACITY: 100
-            }),
-            ['CAPACITY', '100', 'ITEMS', 'item']
-        );
-    });
-
-    it('with NOCREATE', () => {
-        assert.deepEqual(
-            pushInsertOptions([], 'item', {
-                NOCREATE: true
-            }),
-            ['NOCREATE', 'ITEMS', 'item']
-        );
-    });
-
-    it('with CAPACITY and NOCREATE', () => {
-        assert.deepEqual(
-            pushInsertOptions([], 'item', {
-                CAPACITY: 100,
-                NOCREATE: true
-            }),
-            ['CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item']
-        );
-    });
-});
diff --git a/packages/bloom/lib/commands/cuckoo/index.ts b/packages/bloom/lib/commands/cuckoo/index.ts
index 96b4453bc39..62c63fe8d19 100644
--- a/packages/bloom/lib/commands/cuckoo/index.ts
+++ b/packages/bloom/lib/commands/cuckoo/index.ts
@@ -1,62 +1,37 @@
-
-import * as ADD from './ADD';
-import * as ADDNX from './ADDNX';
-import * as COUNT from './COUNT';
-import * as DEL from './DEL';
-import * as EXISTS from './EXISTS';
-import * as INFO from './INFO';
-import * as INSERT from './INSERT';
-import * as INSERTNX from './INSERTNX';
-import * as LOADCHUNK from './LOADCHUNK';
-import * as RESERVE from './RESERVE';
-import * as SCANDUMP from './SCANDUMP';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import type { RedisCommands } from '@redis/client/dist/lib/RESP/types';
+import ADD from './ADD';
+import ADDNX from './ADDNX';
+import COUNT from './COUNT';
+import DEL from './DEL';
+import EXISTS from './EXISTS';
+import INFO from './INFO';
+import INSERT from './INSERT';
+import INSERTNX from './INSERTNX';
+import LOADCHUNK from './LOADCHUNK';
+import RESERVE from './RESERVE';
+import SCANDUMP from './SCANDUMP';
 
 export default {
-    ADD,
-    add: ADD,
-    ADDNX,
-    addNX: ADDNX,
-    COUNT,
-    count: COUNT,
-    DEL,
-    del: DEL,
-    EXISTS,
-    exists: EXISTS,
-    INFO,
-    info: INFO,
-    INSERT,
-    insert: INSERT,
-    INSERTNX,
-    insertNX: INSERTNX,
-    LOADCHUNK,
-    loadChunk: LOADCHUNK,
-    RESERVE,
-    reserve: RESERVE,
-    SCANDUMP,
-    scanDump: SCANDUMP
-};
-
-export interface InsertOptions {
-    CAPACITY?: number;
-    NOCREATE?: true;
-}
-
-export function pushInsertOptions(
-    args: RedisCommandArguments,
-    items: string | Array<string>,
-    options?: InsertOptions
-): RedisCommandArguments {
-    if (options?.CAPACITY) {
-        args.push('CAPACITY');
-        args.push(options.CAPACITY.toString());
-    }
-
-    if (options?.NOCREATE) {
-        args.push('NOCREATE');
-    }
-
-    args.push('ITEMS');
-    return pushVerdictArguments(args, items);
-}
+  ADD,
+  add: ADD,
+  ADDNX,
+  addNX: ADDNX,
+  COUNT,
+  count: COUNT,
+  DEL,
+  del: DEL,
+  EXISTS,
+  exists: EXISTS,
+  INFO,
+  info: INFO,
+  INSERT,
+  insert: INSERT,
+  INSERTNX,
+  insertNX: INSERTNX,
+  LOADCHUNK,
+  loadChunk: LOADCHUNK,
+  RESERVE,
+  reserve: RESERVE,
+  SCANDUMP,
+  scanDump: SCANDUMP
+} as const satisfies RedisCommands;
diff --git a/packages/bloom/lib/commands/index.ts b/packages/bloom/lib/commands/index.ts
index cea55b2a7c0..6f91089460a 100644
--- a/packages/bloom/lib/commands/index.ts
+++ b/packages/bloom/lib/commands/index.ts
@@ -1,3 +1,4 @@
+import { RedisModules } from '@redis/client';
 import bf from './bloom';
 import cms from './count-min-sketch';
 import cf from './cuckoo';
@@ -5,9 +6,9 @@ import tDigest from './t-digest';
 import topK from './top-k';
 
 export default {
-    bf,
-    cms,
-    cf,
-    tDigest,
-    topK
-};
+  bf,
+  cms,
+  cf,
+  tDigest,
+  topK
+} as const satisfies RedisModules;
diff --git a/packages/bloom/lib/commands/t-digest/ADD.spec.ts b/packages/bloom/lib/commands/t-digest/ADD.spec.ts
index 3e1dbff7f27..7578fb9378b 100644
--- a/packages/bloom/lib/commands/t-digest/ADD.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/ADD.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './ADD';
+import ADD from './ADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.ADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.ADD', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ADD, 'key', [1, 2]),
+      ['TDIGEST.ADD', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.add', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.add('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.add', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.add('key', [1])
+    ]);
 
-        assert.equal(reply, 'OK');
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/ADD.ts b/packages/bloom/lib/commands/t-digest/ADD.ts
index 941e8531003..30e745f8f47 100644
--- a/packages/bloom/lib/commands/t-digest/ADD.ts
+++ b/packages/bloom/lib/commands/t-digest/ADD.ts
@@ -1,17 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds one or more observations to a t-digest sketch
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param values - Array of numeric values to add to the sketch
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, values: Array<number>) {
+    parser.push('TDIGEST.ADD');
+    parser.pushKey(key);
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    values: Array<number>
-): RedisCommandArguments {
-    const args = ['TDIGEST.ADD', key];
-    for (const item of values) {
-        args.push(item.toString());
+    for (const value of values) {
+      parser.push(value.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts
index 083f09d22af..81a2c75dff5 100644
--- a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './BYRANK';
+import BYRANK from './BYRANK';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.BYRANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.BYRANK', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(BYRANK, 'key', [1, 2]),
+      ['TDIGEST.BYRANK', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.byRank', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.byRank('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.byRank', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.byRank('key', [1])
+    ]);
 
-        assert.deepEqual(reply, [NaN]);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [NaN]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.ts b/packages/bloom/lib/commands/t-digest/BYRANK.ts
index 5684385b4d3..9ac855bfeab 100644
--- a/packages/bloom/lib/commands/t-digest/BYRANK.ts
+++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts
@@ -1,19 +1,31 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export function transformByRankArguments(
+  parser: CommandParser, 
+  key: RedisArgument,
+  ranks: Array<number>
+) {
+  parser.pushKey(key);
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    ranks: Array<number>
-): RedisCommandArguments {
-    const args = ['TDIGEST.BYRANK', key];
-    for (const rank of ranks) {
-        args.push(rank.toString());
-    }
-
-    return args;
+  for (const rank of ranks) {
+    parser.push(rank.toString());
+  }
 }
 
-export { transformDoublesReply as transformReply } from '.';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns value estimates for one or more ranks in a t-digest sketch
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param ranks - Array of ranks to get value estimates for (ascending order)
+   */
+  parseCommand(...args: Parameters<typeof transformByRankArguments>) {
+    args[0].push('TDIGEST.BYRANK');
+    transformByRankArguments(...args);
+  },
+  transformReply: transformDoubleArrayReply
+} as const satisfies Command;
+
diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts
index c094f36e71d..c8f794bef57 100644
--- a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './BYREVRANK';
+import BYREVRANK from './BYREVRANK';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.BYREVRANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.BYREVRANK', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(BYREVRANK, 'key', [1, 2]),
+      ['TDIGEST.BYREVRANK', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.byRevRank', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.byRevRank('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.byRevRank', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.byRevRank('key', [1])
+    ]);
 
-        assert.deepEqual(reply, [NaN]);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [NaN]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts
index 3dcf3a973c4..a94e5566bb1 100644
--- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts
+++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts
@@ -1,19 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import BYRANK, { transformByRankArguments } from './BYRANK';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    ranks: Array<number>
-): RedisCommandArguments {
-    const args = ['TDIGEST.BYREVRANK', key];
-    for (const rank of ranks) {
-        args.push(rank.toString());
-    }
-
-    return args;
-}
-
-export { transformDoublesReply as transformReply } from '.';
+/**
+ * Returns value estimates for one or more ranks in a t-digest sketch, starting from highest rank
+ * @param parser - The command parser
+ * @param key - The name of the t-digest sketch
+ * @param ranks - Array of ranks to get value estimates for (descending order)
+ */
+export default {
+  IS_READ_ONLY: BYRANK.IS_READ_ONLY,
+  parseCommand(...args: Parameters<typeof transformByRankArguments>) {
+    args[0].push('TDIGEST.BYREVRANK');
+    transformByRankArguments(...args);
+  },
+  transformReply: BYRANK.transformReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/CDF.spec.ts b/packages/bloom/lib/commands/t-digest/CDF.spec.ts
index 36d3564f62c..2689bf2fc9a 100644
--- a/packages/bloom/lib/commands/t-digest/CDF.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/CDF.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './CDF';
+import CDF from './CDF';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.CDF', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.CDF', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CDF, 'key', [1, 2]),
+      ['TDIGEST.CDF', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.cdf', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.cdf('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.cdf', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.cdf('key', [1])
+    ]);
 
-        assert.deepEqual(reply, [NaN]);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [NaN]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/CDF.ts b/packages/bloom/lib/commands/t-digest/CDF.ts
index fe7ece59d76..a3d3b884a34 100644
--- a/packages/bloom/lib/commands/t-digest/CDF.ts
+++ b/packages/bloom/lib/commands/t-digest/CDF.ts
@@ -1,19 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Estimates the cumulative distribution function for values in a t-digest sketch
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param values - Array of values to get CDF estimates for
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, values: Array<number>) {
+    parser.push('TDIGEST.CDF');
+    parser.pushKey(key);
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    values: Array<number>
-): RedisCommandArguments {
-    const args = ['TDIGEST.CDF', key];
     for (const item of values) {
-        args.push(item.toString());
+      parser.push(item.toString());
     }
-
-    return args;
-}
-
-export { transformDoublesReply as transformReply } from '.';
+  },
+  transformReply: transformDoubleArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts
index 4d329cc81ae..0f218e07ab8 100644
--- a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './CREATE';
+import CREATE from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.CREATE', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['TDIGEST.CREATE', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key'),
+        ['TDIGEST.CREATE', 'key']
+      );
+    });
 
-        it('with COMPRESSION', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    COMPRESSION: 100
-                }),
-                ['TDIGEST.CREATE', 'key', 'COMPRESSION', '100']
-            );
-        });
+    it('with COMPRESSION', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          COMPRESSION: 100
+        }),
+        ['TDIGEST.CREATE', 'key', 'COMPRESSION', '100']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.tDigest.create', async client => {
-        assert.equal(
-            await client.tDigest.create('key'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.tDigest.create', async client => {
+    assert.equal(
+      await client.tDigest.create('key'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/CREATE.ts b/packages/bloom/lib/commands/t-digest/CREATE.ts
index 1935d2973dc..25eb9c4f425 100644
--- a/packages/bloom/lib/commands/t-digest/CREATE.ts
+++ b/packages/bloom/lib/commands/t-digest/CREATE.ts
@@ -1,16 +1,26 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { CompressionOption, pushCompressionArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    options?: CompressionOption
-): RedisCommandArguments {
-    return pushCompressionArgument(
-        ['TDIGEST.CREATE', key],
-        options
-    );
+export interface TDigestCreateOptions {
+  COMPRESSION?: number;
 }
 
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Creates a new t-digest sketch for storing distributions
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param options - Optional parameters for sketch creation
+   * @param options.COMPRESSION - Compression parameter that affects performance and accuracy
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: TDigestCreateOptions) {
+    parser.push('TDIGEST.CREATE');
+    parser.pushKey(key);
+    
+    if (options?.COMPRESSION !== undefined) {
+      parser.push('COMPRESSION', options.COMPRESSION.toString());
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/INFO.spec.ts b/packages/bloom/lib/commands/t-digest/INFO.spec.ts
index 992fda6ea05..d5b8b3e13ed 100644
--- a/packages/bloom/lib/commands/t-digest/INFO.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/INFO.spec.ts
@@ -1,25 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INFO';
+import INFO from './INFO';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TDIGEST.INFO', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INFO, 'key'),
+      ['TDIGEST.INFO', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.info', async client => {
-        await client.tDigest.create('key');
+  testUtils.testWithClient('client.tDigest.info', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.info('key')
+    ]);
 
-        const info = await client.tDigest.info('key');
-        assert(typeof info.capacity, 'number');
-        assert(typeof info.mergedNodes, 'number');
-        assert(typeof info.unmergedNodes, 'number');
-        assert(typeof info.mergedWeight, 'number');
-        assert(typeof info.unmergedWeight, 'number');
-        assert(typeof info.totalCompression, 'number');
-        assert(typeof info.totalCompression, 'number');
-    }, GLOBAL.SERVERS.OPEN);
+    assert(typeof reply, 'object');
+    assert(typeof reply['Compression'], 'number');
+    assert(typeof reply['Capacity'], 'number');
+    assert(typeof reply['Merged nodes'], 'number');
+    assert(typeof reply['Unmerged nodes'], 'number');
+    assert(typeof reply['Merged weight'], 'number');
+    assert(typeof reply['Unmerged weight'], 'number');
+    assert(typeof reply['Observations'], 'number');
+    assert(typeof reply['Total compressions'], 'number');
+    assert(typeof reply['Memory usage'], 'number');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/INFO.ts b/packages/bloom/lib/commands/t-digest/INFO.ts
index 44d2083524f..96ae420886c 100644
--- a/packages/bloom/lib/commands/t-digest/INFO.ts
+++ b/packages/bloom/lib/commands/t-digest/INFO.ts
@@ -1,51 +1,34 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { transformInfoV2Reply } from '../bloom';
 
-export const FIRST_KEY_INDEX = 1;
+export type TdInfoReplyMap = TuplesToMapReply<[
+  [SimpleStringReply<'Compression'>, NumberReply],
+  [SimpleStringReply<'Capacity'>, NumberReply],
+  [SimpleStringReply<'Merged nodes'>, NumberReply],
+  [SimpleStringReply<'Unmerged nodes'>, NumberReply],
+  [SimpleStringReply<'Merged weight'>, NumberReply],
+  [SimpleStringReply<'Unmerged weight'>, NumberReply],
+  [SimpleStringReply<'Observations'>, NumberReply],
+  [SimpleStringReply<'Total compressions'>, NumberReply],
+  [SimpleStringReply<'Memory usage'>, NumberReply]
+]>;
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return [
-        'TDIGEST.INFO',
-        key
-    ];
-}
-
-type InfoRawReply = [
-    'Compression',
-    number,
-    'Capacity',
-    number,
-    'Merged nodes',
-    number,
-    'Unmerged nodes',
-    number,
-    'Merged weight',
-    string,
-    'Unmerged weight',
-    string,
-    'Total compressions',
-    number
-];
-
-interface InfoReply {
-    comperssion: number;
-    capacity: number;
-    mergedNodes: number;
-    unmergedNodes: number;
-    mergedWeight: number;
-    unmergedWeight: number;
-    totalCompression: number;
-}
-
-export function transformReply(reply: InfoRawReply): InfoReply {
-    return {
-        comperssion: reply[1],
-        capacity: reply[3],
-        mergedNodes: reply[5],
-        unmergedNodes: reply[7],
-        mergedWeight: Number(reply[9]),
-        unmergedWeight: Number(reply[11]),
-        totalCompression: reply[13]
-    };
-}
\ No newline at end of file
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about a t-digest sketch including compression, capacity, nodes, weights, observations and memory usage
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch to get information about
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TDIGEST.INFO');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<TdInfoReplyMap>>, _, typeMapping?: TypeMapping): TdInfoReplyMap => {
+      return transformInfoV2Reply<TdInfoReplyMap>(reply, typeMapping);
+    },
+    3: undefined as unknown as () => TdInfoReplyMap
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/MAX.spec.ts b/packages/bloom/lib/commands/t-digest/MAX.spec.ts
index bf850cbfd83..920c9d11391 100644
--- a/packages/bloom/lib/commands/t-digest/MAX.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/MAX.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments, transformReply } from './MAX';
+import MAX from './MAX';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.MAX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TDIGEST.MAX', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MAX, 'key'),
+      ['TDIGEST.MAX', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.max', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.max('key')
-        ]);
+  testUtils.testWithClient('client.tDigest.max', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.max('key')
+    ]);
 
-        assert.deepEqual(reply, NaN);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, NaN);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/MAX.ts b/packages/bloom/lib/commands/t-digest/MAX.ts
index 90c42ec6067..2353c60cdc6 100644
--- a/packages/bloom/lib/commands/t-digest/MAX.ts
+++ b/packages/bloom/lib/commands/t-digest/MAX.ts
@@ -1,14 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return [
-        'TDIGEST.MAX',
-        key
-    ];
-}
-
-export { transformDoubleReply as transformReply } from '.';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the maximum value from a t-digest sketch
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TDIGEST.MAX');
+    parser.pushKey(key);
+  },
+  transformReply: transformDoubleReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts
index 1205cdd9216..f2a7c1a1192 100644
--- a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts
@@ -1,50 +1,51 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments, transformReply } from './MERGE';
+import MERGE from './MERGE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.MERGE', () => {
-    describe('transformArguments', () => {
-        describe('srcKeys', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('dest', 'src'),
-                    ['TDIGEST.MERGE', 'dest', '1', 'src']
-                );
-            });
+  describe('transformArguments', () => {
+    describe('source', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(MERGE, 'destination', 'source'),
+          ['TDIGEST.MERGE', 'destination', '1', 'source']
+        );
+      });
 
-            it('Array', () => {
-                assert.deepEqual(
-                    transformArguments('dest', ['1', '2']),
-                    ['TDIGEST.MERGE', 'dest', '2', '1', '2']
-                );
-            });
-        });
+      it('Array', () => {
+        assert.deepEqual(
+          parseArgs(MERGE, 'destination', ['1', '2']),
+          ['TDIGEST.MERGE', 'destination', '2', '1', '2']
+        );
+      });
+    });
 
-        it('with COMPRESSION', () => {
-            assert.deepEqual(
-                transformArguments('dest', 'src', {
-                    COMPRESSION: 100
-                }),
-                ['TDIGEST.MERGE', 'dest', '1', 'src', 'COMPRESSION', '100']
-            );
-        });
+    it('with COMPRESSION', () => {
+      assert.deepEqual(
+        parseArgs(MERGE, 'destination', 'source', {
+          COMPRESSION: 100
+        }),
+        ['TDIGEST.MERGE', 'destination', '1', 'source', 'COMPRESSION', '100']
+      );
+    });
 
-        it('with OVERRIDE', () => {
-            assert.deepEqual(
-                transformArguments('dest', 'src', {
-                    OVERRIDE: true
-                }),
-                ['TDIGEST.MERGE', 'dest', '1', 'src', 'OVERRIDE']
-            );
-        });
+    it('with OVERRIDE', () => {
+      assert.deepEqual(
+        parseArgs(MERGE, 'destination', 'source', {
+          OVERRIDE: true
+        }),
+        ['TDIGEST.MERGE', 'destination', '1', 'source', 'OVERRIDE']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.tDigest.merge', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('src'),
-            client.tDigest.merge('dest', 'src')
-        ]);
+  testUtils.testWithClient('client.tDigest.merge', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('source'),
+      client.tDigest.merge('destination', 'source')
+    ]);
 
-        assert.equal(reply, 'OK');
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/MERGE.ts b/packages/bloom/lib/commands/t-digest/MERGE.ts
index 5119d0b9e18..f34d30a7cef 100644
--- a/packages/bloom/lib/commands/t-digest/MERGE.ts
+++ b/packages/bloom/lib/commands/t-digest/MERGE.ts
@@ -1,30 +1,40 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers';
-import { CompressionOption, pushCompressionArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface MergeOptions extends CompressionOption {
-    OVERRIDE?: boolean;
+export interface TDigestMergeOptions {
+  COMPRESSION?: number;
+  OVERRIDE?: boolean;
 }
 
-export function transformArguments(
-    destKey: RedisCommandArgument,
-    srcKeys: RedisCommandArgument | Array<RedisCommandArgument>,
-    options?: MergeOptions
-): RedisCommandArguments {
-    const args = pushVerdictArgument(
-        ['TDIGEST.MERGE', destKey],
-        srcKeys
-    );
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Merges multiple t-digest sketches into one, with optional compression and override settings
+   * @param parser - The command parser
+   * @param destination - The name of the destination t-digest sketch
+   * @param source - One or more source sketch names to merge from
+   * @param options - Optional parameters for merge operation
+   * @param options.COMPRESSION - New compression value for merged sketch
+   * @param options.OVERRIDE - If true, override destination sketch if it exists
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    source: RedisVariadicArgument,
+    options?: TDigestMergeOptions
+  ) {
+    parser.push('TDIGEST.MERGE');
+    parser.pushKey(destination);
+    parser.pushKeysLength(source);
 
-    pushCompressionArgument(args, options);
+    if (options?.COMPRESSION !== undefined) {
+      parser.push('COMPRESSION', options.COMPRESSION.toString());
+    }
 
     if (options?.OVERRIDE) {
-        args.push('OVERRIDE');
+      parser.push('OVERRIDE');
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/MIN.spec.ts b/packages/bloom/lib/commands/t-digest/MIN.spec.ts
index d48deaca7fb..278248ea465 100644
--- a/packages/bloom/lib/commands/t-digest/MIN.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/MIN.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments, transformReply } from './MIN';
+import MIN from './MIN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.MIN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TDIGEST.MIN', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MIN, 'key'),
+      ['TDIGEST.MIN', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.min', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.min('key')
-        ]);
+  testUtils.testWithClient('client.tDigest.min', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.min('key')
+    ]);
 
-        assert.equal(reply, NaN);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, NaN);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/MIN.ts b/packages/bloom/lib/commands/t-digest/MIN.ts
index d8be8722b60..9b8ea2a0cfc 100644
--- a/packages/bloom/lib/commands/t-digest/MIN.ts
+++ b/packages/bloom/lib/commands/t-digest/MIN.ts
@@ -1,14 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return [
-        'TDIGEST.MIN',
-        key
-    ];
-}
-
-export { transformDoubleReply as transformReply } from '.';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the minimum value from a t-digest sketch
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TDIGEST.MIN');
+    parser.pushKey(key);
+  },
+  transformReply: transformDoubleReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts
index 7790debf0de..ac7249d12d9 100644
--- a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts
@@ -1,24 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './QUANTILE';
+import QUANTILE from './QUANTILE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.QUANTILE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.QUANTILE', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(QUANTILE, 'key', [1, 2]),
+      ['TDIGEST.QUANTILE', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.quantile', async client => {
-        const [, reply] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.quantile('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.quantile', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.quantile('key', [1])
+    ]);
 
-        assert.deepEqual(
-            reply,
-            [NaN]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(
+      reply,
+      [NaN]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.ts
index 2289ffc6f55..772172b84d5 100644
--- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts
+++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts
@@ -1,23 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    quantiles: Array<number>
-): RedisCommandArguments {
-    const args = [
-        'TDIGEST.QUANTILE',
-        key
-    ];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns value estimates at requested quantiles from a t-digest sketch
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param quantiles - Array of quantiles (between 0 and 1) to get value estimates for
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, quantiles: Array<number>) {
+    parser.push('TDIGEST.QUANTILE');
+    parser.pushKey(key);
 
     for (const quantile of quantiles) {
-        args.push(quantile.toString());
+      parser.push(quantile.toString());
     }
-
-    return args;
-}
-
-export { transformDoublesReply as transformReply } from '.';
+  },
+  transformReply: transformDoubleArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/RANK.spec.ts b/packages/bloom/lib/commands/t-digest/RANK.spec.ts
index 258bedf3491..f1747662f0a 100644
--- a/packages/bloom/lib/commands/t-digest/RANK.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/RANK.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './RANK';
+import RANK from './RANK';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.RANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.RANK', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RANK, 'key', [1, 2]),
+      ['TDIGEST.RANK', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.rank', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.rank('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.rank', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.rank('key', [1])
+    ]);
 
-        assert.deepEqual(reply, [-2]);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [-2]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/RANK.ts b/packages/bloom/lib/commands/t-digest/RANK.ts
index 1a6c84bbd4d..55dd3c636ce 100644
--- a/packages/bloom/lib/commands/t-digest/RANK.ts
+++ b/packages/bloom/lib/commands/t-digest/RANK.ts
@@ -1,19 +1,29 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export function transformRankArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  values: Array<number>
+) {
+  parser.pushKey(key);
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    values: Array<number>
-): RedisCommandArguments {
-    const args = ['TDIGEST.RANK', key];
-    for (const item of values) {
-        args.push(item.toString());
-    }
-
-    return args;
+  for (const value of values) {
+    parser.push(value.toString());
+  }
 }
 
-export declare function transformReply(): Array<number>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the rank of one or more values in a t-digest sketch (number of values that are lower than each value)
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param values - Array of values to get ranks for
+   */
+  parseCommand(...args: Parameters<typeof transformRankArguments>) {
+    args[0].push('TDIGEST.RANK');
+    transformRankArguments(...args);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/RESET.spec.ts b/packages/bloom/lib/commands/t-digest/RESET.spec.ts
index 036fbebc8cc..8e1fc12e6e3 100644
--- a/packages/bloom/lib/commands/t-digest/RESET.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/RESET.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './RESET';
+import RESET from './RESET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.RESET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TDIGEST.RESET', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RESET, 'key'),
+      ['TDIGEST.RESET', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.reset', async client => {
-        const [, reply] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.reset('key')
-        ]);
+  testUtils.testWithClient('client.tDigest.reset', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.reset('key')
+    ]);
 
-        assert.equal(reply, 'OK');
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/RESET.ts b/packages/bloom/lib/commands/t-digest/RESET.ts
index 6c700e6b932..8e1cefe9ff8 100644
--- a/packages/bloom/lib/commands/t-digest/RESET.ts
+++ b/packages/bloom/lib/commands/t-digest/RESET.ts
@@ -1,9 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['TDIGEST.RESET', key];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Resets a t-digest sketch, clearing all previously added observations
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch to reset
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TDIGEST.RESET');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts
index 21d16661dfe..be7b23b2238 100644
--- a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './REVRANK';
+import REVRANK from './REVRANK';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TDIGEST.REVRANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [1, 2]),
-            ['TDIGEST.REVRANK', 'key', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(REVRANK, 'key', [1, 2]),
+      ['TDIGEST.REVRANK', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.revRank', async client => {
-        const [ , reply ] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.revRank('key', [1])
-        ]);
+  testUtils.testWithClient('client.tDigest.revRank', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.revRank('key', [1])
+    ]);
 
-        assert.deepEqual(reply, [-2]);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [-2]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.ts b/packages/bloom/lib/commands/t-digest/REVRANK.ts
index a2465052774..e323e10190a 100644
--- a/packages/bloom/lib/commands/t-digest/REVRANK.ts
+++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts
@@ -1,19 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import RANK, { transformRankArguments } from './RANK';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    values: Array<number>
-): RedisCommandArguments {
-    const args = ['TDIGEST.REVRANK', key];
-    for (const item of values) {
-        args.push(item.toString());
-    }
-
-    return args;
-}
-
-export declare function transformReply(): Array<number>;
+/**
+ * Returns the reverse rank of one or more values in a t-digest sketch (number of values that are higher than each value)
+ * @param parser - The command parser
+ * @param key - The name of the t-digest sketch
+ * @param values - Array of values to get reverse ranks for
+ */
+export default {
+  IS_READ_ONLY: RANK.IS_READ_ONLY,
+  parseCommand(...args: Parameters<typeof transformRankArguments>) {
+    args[0].push('TDIGEST.REVRANK');
+    transformRankArguments(...args);
+  },
+  transformReply: RANK.transformReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts
index dd07f325c8d..8e83c736476 100644
--- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts
+++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments,  transformReply } from './TRIMMED_MEAN';
+import TRIMMED_MEAN from './TRIMMED_MEAN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TDIGEST.RESET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 1),
-            ['TDIGEST.TRIMMED_MEAN', 'key', '0', '1']
-        );
-    });
+describe('TDIGEST.TRIMMED_MEAN', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(TRIMMED_MEAN, 'key', 0, 1),
+      ['TDIGEST.TRIMMED_MEAN', 'key', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.tDigest.trimmedMean', async client => {
-        const [, reply] = await Promise.all([
-            client.tDigest.create('key'),
-            client.tDigest.trimmedMean('key', 0, 1)
-        ]);
+  testUtils.testWithClient('client.tDigest.trimmedMean', async client => {
+    const [, reply] = await Promise.all([
+      client.tDigest.create('key'),
+      client.tDigest.trimmedMean('key', 0, 1)
+    ]);
 
-        assert.equal(reply, NaN);
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, NaN);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts
index 6de80ba7c7c..f65f37a95de 100644
--- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts
+++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts
@@ -1,20 +1,25 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the mean value from a t-digest sketch after trimming values at specified percentiles
+   * @param parser - The command parser
+   * @param key - The name of the t-digest sketch
+   * @param lowCutPercentile - Lower percentile cutoff (between 0 and 100)
+   * @param highCutPercentile - Higher percentile cutoff (between 0 and 100)
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     lowCutPercentile: number,
     highCutPercentile: number
-): RedisCommandArguments {
-    return [
-        'TDIGEST.TRIMMED_MEAN',
-        key,
-        lowCutPercentile.toString(),
-        highCutPercentile.toString()
-    ];
-}
-
-export { transformDoubleReply as transformReply } from '.';
+  ) {
+    parser.push('TDIGEST.TRIMMED_MEAN');
+    parser.pushKey(key);
+    parser.push(lowCutPercentile.toString(), highCutPercentile.toString());
+  },
+  transformReply: transformDoubleReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/t-digest/index.spec.ts b/packages/bloom/lib/commands/t-digest/index.spec.ts
deleted file mode 100644
index 5bef6df04b2..00000000000
--- a/packages/bloom/lib/commands/t-digest/index.spec.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { strict as assert } from 'assert';
-import { pushCompressionArgument, transformDoubleReply, transformDoublesReply } from '.';
-
-describe('pushCompressionArgument', () => {
-    it('undefined', () => {
-        assert.deepEqual(
-            pushCompressionArgument([]),
-            []
-        );
-    });
-
-    it('100', () => {
-        assert.deepEqual(
-            pushCompressionArgument([], { COMPRESSION: 100 }),
-            ['COMPRESSION', '100']
-        );
-    });
-});
-
-describe('transformDoubleReply', () => {
-    it('inf', () => {
-        assert.equal(
-            transformDoubleReply('inf'),
-            Infinity
-        );
-    });
-
-    it('-inf', () => {
-        assert.equal(
-            transformDoubleReply('-inf'),
-            -Infinity
-        );
-    });
-
-    it('nan', () => {
-        assert.equal(
-            transformDoubleReply('nan'),
-            NaN
-        );
-    });
-
-    it('0', () => {
-        assert.equal(
-            transformDoubleReply('0'),
-            0
-        );
-    });
-});
-
-it('transformDoublesReply', () => {
-    assert.deepEqual(
-        transformDoublesReply(['inf', '-inf', 'nan', '0']),
-        [Infinity, -Infinity, NaN, 0]
-    );
-});
diff --git a/packages/bloom/lib/commands/t-digest/index.ts b/packages/bloom/lib/commands/t-digest/index.ts
index da3b37464d2..d180911dbf9 100644
--- a/packages/bloom/lib/commands/t-digest/index.ts
+++ b/packages/bloom/lib/commands/t-digest/index.ts
@@ -1,81 +1,46 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import * as ADD from './ADD';
-import * as BYRANK from './BYRANK';
-import * as BYREVRANK from './BYREVRANK';
-import * as CDF from './CDF';
-import * as CREATE from './CREATE';
-import * as INFO from './INFO';
-import * as MAX from './MAX';
-import * as MERGE from './MERGE';
-import * as MIN from './MIN';
-import * as QUANTILE from './QUANTILE';
-import * as RANK from './RANK';
-import * as RESET from './RESET';
-import * as REVRANK from './REVRANK';
-import * as TRIMMED_MEAN from './TRIMMED_MEAN';
+import type { RedisCommands } from '@redis/client/dist/lib/RESP/types';
+import ADD from './ADD';
+import BYRANK from './BYRANK';
+import BYREVRANK from './BYREVRANK';
+import CDF from './CDF';
+import CREATE from './CREATE';
+import INFO from './INFO';
+import MAX from './MAX';
+import MERGE from './MERGE';
+import MIN from './MIN';
+import QUANTILE from './QUANTILE';
+import RANK from './RANK';
+import RESET from './RESET';
+import REVRANK from './REVRANK';
+import TRIMMED_MEAN from './TRIMMED_MEAN';
 
 export default {
-    ADD,
-    add: ADD,
-    BYRANK,
-    byRank: BYRANK,
-    BYREVRANK,
-    byRevRank: BYREVRANK,
-    CDF,
-    cdf: CDF,
-    CREATE,
-    create: CREATE,
-    INFO,
-    info: INFO,
-    MAX,
-    max: MAX,
-    MERGE,
-    merge: MERGE,
-    MIN,
-    min: MIN,
-    QUANTILE,
-    quantile: QUANTILE,
-    RANK,
-    rank: RANK,
-    RESET,
-    reset: RESET,
-    REVRANK,
-    revRank: REVRANK,
-    TRIMMED_MEAN,
-    trimmedMean: TRIMMED_MEAN
-};
-
-export interface CompressionOption {
-    COMPRESSION?: number;
-}
-
-export function pushCompressionArgument(
-    args: RedisCommandArguments,
-    options?: CompressionOption
-): RedisCommandArguments {
-    if (options?.COMPRESSION) {
-        args.push('COMPRESSION', options.COMPRESSION.toString());
-    }
-
-    return args;
-}
-
-export function transformDoubleReply(reply: string): number {
-    switch (reply) {
-        case 'inf':
-            return Infinity;
-
-        case '-inf':
-            return -Infinity;
-
-        case 'nan':
-            return NaN;
-
-        default:
-            return parseFloat(reply);
-    }
-}
-
-export function transformDoublesReply(reply: Array<string>): Array<number> {
-    return reply.map(transformDoubleReply);
-}
+  ADD,
+  add: ADD,
+  BYRANK,
+  byRank: BYRANK,
+  BYREVRANK,
+  byRevRank: BYREVRANK,
+  CDF,
+  cdf: CDF,
+  CREATE,
+  create: CREATE,
+  INFO,
+  info: INFO,
+  MAX,
+  max: MAX,
+  MERGE,
+  merge: MERGE,
+  MIN,
+  min: MIN,
+  QUANTILE,
+  quantile: QUANTILE,
+  RANK,
+  rank: RANK,
+  RESET,
+  reset: RESET,
+  REVRANK,
+  revRank: REVRANK,
+  TRIMMED_MEAN,
+  trimmedMean: TRIMMED_MEAN
+} as const satisfies RedisCommands;
diff --git a/packages/bloom/lib/commands/top-k/ADD.spec.ts b/packages/bloom/lib/commands/top-k/ADD.spec.ts
index 149007f81d0..15a7a9ce1dd 100644
--- a/packages/bloom/lib/commands/top-k/ADD.spec.ts
+++ b/packages/bloom/lib/commands/top-k/ADD.spec.ts
@@ -1,22 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './ADD';
+import ADD from './ADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK ADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['TOPK.ADD', 'key', 'item']
-        );
-    });
+describe('TOPK.ADD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ADD, 'key', 'item'),
+      ['TOPK.ADD', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.topK.add', async client => {
-        await client.topK.reserve('topK', 3);
+  testUtils.testWithClient('client.topK.add', async client => {
+    const [, reply] = await Promise.all([
+      client.topK.reserve('topK', 3),
+      client.topK.add('topK', 'item')
+    ]);
 
-        assert.deepEqual(
-            await client.topK.add('topK', 'item'),
-            [null]
-        );
-
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [null]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/ADD.ts b/packages/bloom/lib/commands/top-k/ADD.ts
index beee3a2206c..8eab7dd5241 100644
--- a/packages/bloom/lib/commands/top-k/ADD.ts
+++ b/packages/bloom/lib/commands/top-k/ADD.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: string,
-    items: string | Array<string>
-): RedisCommandArguments {
-    return pushVerdictArguments(['TOPK.ADD', key], items);
-}
-
-export declare function transformReply(): Array<null | string>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds one or more items to a Top-K filter and returns items dropped from the top-K list
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   * @param items - One or more items to add to the filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) {
+    parser.push('TOPK.ADD');
+    parser.pushKey(key);
+    parser.pushVariadic(items);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/COUNT.spec.ts b/packages/bloom/lib/commands/top-k/COUNT.spec.ts
index 318fc74c679..a242edfef8a 100644
--- a/packages/bloom/lib/commands/top-k/COUNT.spec.ts
+++ b/packages/bloom/lib/commands/top-k/COUNT.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './COUNT';
+import COUNT from './COUNT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK COUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['TOPK.COUNT', 'key', 'item']
-        );
-    });
+describe('TOPK.COUNT', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(COUNT, 'key', 'item'),
+      ['TOPK.COUNT', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.topK.count', async client => {
-        await client.topK.reserve('key', 3);
+  testUtils.testWithClient('client.topK.count', async client => {
+    const [, reply] = await Promise.all([
+      client.topK.reserve('key', 3),
+      client.topK.count('key', 'item')
+    ]);
 
-        assert.deepEqual(
-            await client.topK.count('key', 'item'),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [0]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/COUNT.ts b/packages/bloom/lib/commands/top-k/COUNT.ts
index fc8cf557dca..b72641471cd 100644
--- a/packages/bloom/lib/commands/top-k/COUNT.ts
+++ b/packages/bloom/lib/commands/top-k/COUNT.ts
@@ -1,15 +1,19 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: string,
-    items: string | Array<string>
-): RedisCommandArguments {
-    return pushVerdictArguments(['TOPK.COUNT', key], items);
-}
-
-export declare function transformReply(): Array<number>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the count of occurrences for one or more items in a Top-K filter
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   * @param items - One or more items to get counts for
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) {
+    parser.push('TOPK.COUNT');
+    parser.pushKey(key);
+    parser.pushVariadic(items);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts
index b23ca6e0ed1..94e5b1d7058 100644
--- a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts
+++ b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts
@@ -1,42 +1,43 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INCRBY';
+import INCRBY from './INCRBY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK INCRBY', () => {
-    describe('transformArguments', () => {
-        it('single item', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    item: 'item',
-                    incrementBy: 1
-                }),
-                ['TOPK.INCRBY', 'key', 'item', '1']
-            );
-        });
+describe('TOPK.INCRBY', () => {
+  describe('transformArguments', () => {
+    it('single item', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', {
+          item: 'item',
+          incrementBy: 1
+        }),
+        ['TOPK.INCRBY', 'key', 'item', '1']
+      );
+    });
 
-        it('multiple items', () => {
-            assert.deepEqual(
-                transformArguments('key', [{
-                    item: 'a',
-                    incrementBy: 1
-                }, {
-                    item: 'b',
-                    incrementBy: 2
-                }]),
-                ['TOPK.INCRBY', 'key', 'a', '1', 'b', '2']
-            );
-        });
+    it('multiple items', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', [{
+          item: 'a',
+          incrementBy: 1
+        }, {
+          item: 'b',
+          incrementBy: 2
+        }]),
+        ['TOPK.INCRBY', 'key', 'a', '1', 'b', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.topK.incrby', async client => {
-        await client.topK.reserve('key', 5);
+  testUtils.testWithClient('client.topK.incrby', async client => {
+    const [, reply] = await Promise.all([
+      client.topK.reserve('key', 5),
+      client.topK.incrBy('key', {
+        item: 'item',
+        incrementBy: 1
+      })
+    ]);
 
-        assert.deepEqual(
-            await client.topK.incrBy('key', {
-                item: 'item',
-                incrementBy: 1
-            }),
-            [null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [null]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/INCRBY.ts b/packages/bloom/lib/commands/top-k/INCRBY.ts
index 2533cb05594..ef93653bd3c 100644
--- a/packages/bloom/lib/commands/top-k/INCRBY.ts
+++ b/packages/bloom/lib/commands/top-k/INCRBY.ts
@@ -1,29 +1,38 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-interface IncrByItem {
-    item: string;
-    incrementBy: number;
+export interface TopKIncrByItem {
+  item: string;
+  incrementBy: number;
 }
 
-export function transformArguments(
-    key: string,
-    items: IncrByItem | Array<IncrByItem>
-): Array<string> {
-    const args = ['TOPK.INCRBY', key];
+function pushIncrByItem(parser: CommandParser, { item, incrementBy }: TopKIncrByItem) {
+  parser.push(item, incrementBy.toString());
+}
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Increases the score of one or more items in a Top-K filter by specified increments
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   * @param items - A single item or array of items to increment, each with an item name and increment value
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    items: TopKIncrByItem | Array<TopKIncrByItem>
+  ) {
+    parser.push('TOPK.INCRBY');
+    parser.pushKey(key);
 
     if (Array.isArray(items)) {
-        for (const item of items) {
-            pushIncrByItem(args, item);
-        }
+      for (const item of items) {
+        pushIncrByItem(parser, item);
+      }
     } else {
-        pushIncrByItem(args, items);
+      pushIncrByItem(parser, items);
     }
-
-    return args;
-}
-
-function pushIncrByItem(args: Array<string>, { item, incrementBy }: IncrByItem): void {
-    args.push(item, incrementBy.toString());
-}
-
-export declare function transformReply(): Array<string | null>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<SimpleStringReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/INFO.spec.ts b/packages/bloom/lib/commands/top-k/INFO.spec.ts
index 2741a58a8ba..2efbf0bdbef 100644
--- a/packages/bloom/lib/commands/top-k/INFO.spec.ts
+++ b/packages/bloom/lib/commands/top-k/INFO.spec.ts
@@ -1,23 +1,27 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './INFO';
+import INFO from './INFO';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('TOPK INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TOPK.INFO', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INFO, 'key'),
+      ['TOPK.INFO', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.topK.info', async client => {
-        await client.topK.reserve('key', 3);
+  testUtils.testWithClient('client.topK.info', async client => {
+    const k = 3,
+      [, reply] = await Promise.all([
+        client.topK.reserve('key', 3),
+        client.topK.info('key')
+      ]);
 
-        const info = await client.topK.info('key');
-        assert.equal(typeof info, 'object');
-        assert.equal(info.k, 3);
-        assert.equal(typeof info.width, 'number');
-        assert.equal(typeof info.depth, 'number');
-        assert.equal(typeof info.decay, 'number');
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(typeof reply, 'object');
+    assert.equal(reply.k, k);
+    assert.equal(typeof reply.width, 'number');
+    assert.equal(typeof reply.depth, 'number');
+    assert.equal(typeof reply.decay, 'number');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/INFO.ts b/packages/bloom/lib/commands/top-k/INFO.ts
index 8c9e8d432b3..60ee3939324 100644
--- a/packages/bloom/lib/commands/top-k/INFO.ts
+++ b/packages/bloom/lib/commands/top-k/INFO.ts
@@ -1,34 +1,32 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, TuplesToMapReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
+import { transformInfoV2Reply } from '../bloom';
 
-export const IS_READ_ONLY = true;
+export type TopKInfoReplyMap = TuplesToMapReply<[
+  [SimpleStringReply<'k'>, NumberReply],
+  [SimpleStringReply<'width'>, NumberReply],
+  [SimpleStringReply<'depth'>, NumberReply],
+  [SimpleStringReply<'decay'>, DoubleReply]
+]>;
 
-export function transformArguments(key: string): Array<string> {
-    return ['TOPK.INFO', key];
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns configuration and statistics of a Top-K filter, including k, width, depth, and decay parameters
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter to get information about
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TOPK.INFO');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<TopKInfoReplyMap>>, preserve?: any, typeMapping?: TypeMapping): TopKInfoReplyMap => {
+      reply[7] = transformDoubleReply[2](reply[7], preserve, typeMapping) as any;
 
-export type InfoRawReply = [
-    _: string,
-    k: number,
-    _: string,
-    width: number,
-    _: string,
-    depth: number,
-    _: string,
-    decay: string
-];
-
-export interface InfoReply {
-    k: number,
-    width: number;
-    depth: number;
-    decay: number;
-}
-
-export function transformReply(reply: InfoRawReply): InfoReply {
-    return {
-        k: reply[1],
-        width: reply[3],
-        depth: reply[5],
-        decay: Number(reply[7])
-    };
-}
+      return transformInfoV2Reply<TopKInfoReplyMap>(reply, typeMapping);
+    },
+    3: undefined as unknown as () => TopKInfoReplyMap
+  }
+} as const satisfies Command
diff --git a/packages/bloom/lib/commands/top-k/LIST.spec.ts b/packages/bloom/lib/commands/top-k/LIST.spec.ts
index 709ac7ffc39..8f5d0efa4db 100644
--- a/packages/bloom/lib/commands/top-k/LIST.spec.ts
+++ b/packages/bloom/lib/commands/top-k/LIST.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './LIST';
+import LIST from './LIST';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK LIST', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TOPK.LIST', 'key']
-        );
-    });
+describe('TOPK.LIST', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LIST, 'key'),
+      ['TOPK.LIST', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.topK.list', async client => {
-        await client.topK.reserve('key', 3);
+  testUtils.testWithClient('client.topK.list', async client => {
+    const [, reply] = await Promise.all([
+      client.topK.reserve('key', 3),
+      client.topK.list('key')
+    ]);
 
-        assert.deepEqual(
-            await client.topK.list('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, []);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/LIST.ts b/packages/bloom/lib/commands/top-k/LIST.ts
index 8837b86f830..e030ff02efa 100644
--- a/packages/bloom/lib/commands/top-k/LIST.ts
+++ b/packages/bloom/lib/commands/top-k/LIST.ts
@@ -1,9 +1,16 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string): Array<string> {
-    return ['TOPK.LIST', key];
-}
-
-export declare function transformReply(): Array<string | null>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns all items in a Top-K filter
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TOPK.LIST');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts
index 1e55239c243..852170e8cd3 100644
--- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts
+++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts
@@ -1,30 +1,28 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './LIST_WITHCOUNT';
+import LIST_WITHCOUNT from './LIST_WITHCOUNT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK LIST WITHCOUNT', () => {
-    testUtils.isVersionGreaterThanHook([2, 2, 9]);
-    
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TOPK.LIST', 'key', 'WITHCOUNT']
-        );
-    });
+describe('TOPK.LIST WITHCOUNT', () => {
+  testUtils.isVersionGreaterThanHook([2, 2, 9]);
 
-    testUtils.testWithClient('client.topK.listWithCount', async client => {
-        const [,, list] = await Promise.all([
-            client.topK.reserve('key', 3),
-            client.topK.add('key', 'item'),
-            client.topK.listWithCount('key')
-        ]);
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LIST_WITHCOUNT, 'key'),
+      ['TOPK.LIST', 'key', 'WITHCOUNT']
+    );
+  });
 
-        assert.deepEqual(
-            list,
-            [{
-                item: 'item',
-                count: 1
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.topK.listWithCount', async client => {
+    const [, , list] = await Promise.all([
+      client.topK.reserve('key', 3),
+      client.topK.add('key', 'item'),
+      client.topK.listWithCount('key')
+    ]);
+
+    assert.deepEqual(list, [{
+      item: 'item',
+      count: 1
+    }]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts
index 47b7d3848ed..bed58a36b70 100644
--- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts
+++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts
@@ -1,26 +1,31 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string): Array<string> {
-    return ['TOPK.LIST', key, 'WITHCOUNT'];
-}
-
-type ListWithCountRawReply = Array<string | number>;
-
-type ListWithCountReply = Array<{
-    item: string,
-    count: number
-}>;
-
-export function transformReply(rawReply: ListWithCountRawReply): ListWithCountReply {
-    const reply: ListWithCountReply = [];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns all items in a Top-K filter with their respective counts
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TOPK.LIST');
+    parser.pushKey(key);
+    parser.push('WITHCOUNT');
+  },
+  transformReply(rawReply: UnwrapReply<ArrayReply<BlobStringReply | NumberReply>>) {
+    const reply: Array<{
+      item: BlobStringReply;
+      count: NumberReply;
+    }> = [];
+    
     for (let i = 0; i < rawReply.length; i++) {
-        reply.push({
-            item: rawReply[i] as string,
-            count: rawReply[++i] as number
-        });
+      reply.push({
+        item: rawReply[i] as BlobStringReply,
+        count: rawReply[++i] as NumberReply
+      });
     }
 
     return reply;
-}
\ No newline at end of file
+  }
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/QUERY.spec.ts b/packages/bloom/lib/commands/top-k/QUERY.spec.ts
index ada9e7e2e39..3651ec5d37b 100644
--- a/packages/bloom/lib/commands/top-k/QUERY.spec.ts
+++ b/packages/bloom/lib/commands/top-k/QUERY.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './QUERY';
+import QUERY from './QUERY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK QUERY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'item'),
-            ['TOPK.QUERY', 'key', 'item']
-        );
-    });
+describe('TOPK.QUERY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(QUERY, 'key', 'item'),
+      ['TOPK.QUERY', 'key', 'item']
+    );
+  });
 
-    testUtils.testWithClient('client.cms.query', async client => {
-        await client.topK.reserve('key', 3);
+  testUtils.testWithClient('client.topK.query', async client => {
+    const [, reply] = await Promise.all([
+      client.topK.reserve('key', 3),
+      client.topK.query('key', 'item')
+    ]);
 
-        assert.deepEqual(
-            await client.topK.query('key', 'item'),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [false]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/QUERY.ts b/packages/bloom/lib/commands/top-k/QUERY.ts
index 94943a26fd7..8976e211979 100644
--- a/packages/bloom/lib/commands/top-k/QUERY.ts
+++ b/packages/bloom/lib/commands/top-k/QUERY.ts
@@ -1,15 +1,19 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: string,
-    items: string | Array<string>
-): RedisCommandArguments {
-    return pushVerdictArguments(['TOPK.QUERY', key], items);
-}
-
-export declare function transformReply(): Array<number>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Checks if one or more items are in the Top-K list
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   * @param items - One or more items to check in the filter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) {
+    parser.push('TOPK.QUERY');
+    parser.pushKey(key);
+    parser.pushVariadic(items);
+  },
+  transformReply: transformBooleanArrayReply
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts
index 54600c0e4f5..aa8d194f940 100644
--- a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts
+++ b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts
@@ -1,32 +1,33 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../../test-utils';
-import { transformArguments } from './RESERVE';
+import RESERVE from './RESERVE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TOPK RESERVE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('topK', 3),
-                ['TOPK.RESERVE', 'topK', '3']
-            );
-        });
+describe('TOPK.RESERVE', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'topK', 3),
+        ['TOPK.RESERVE', 'topK', '3']
+      );
+    });
 
-        it('with options', () => {
-            assert.deepEqual(
-                transformArguments('topK', 3, {
-                    width: 8,
-                    depth: 7,
-                    decay: 0.9
-                }),
-                ['TOPK.RESERVE', 'topK', '3', '8', '7', '0.9']
-            );
-        });
+    it('with options', () => {
+      assert.deepEqual(
+        parseArgs(RESERVE, 'topK', 3, {
+          width: 8,
+          depth: 7,
+          decay: 0.9
+        }),
+        ['TOPK.RESERVE', 'topK', '3', '8', '7', '0.9']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.topK.reserve', async client => {
-        assert.equal(
-            await client.topK.reserve('topK', 3),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.topK.reserve', async client => {
+    assert.equal(
+      await client.topK.reserve('topK', 3),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/bloom/lib/commands/top-k/RESERVE.ts b/packages/bloom/lib/commands/top-k/RESERVE.ts
index 350d4cd8339..e31c2ceb99c 100644
--- a/packages/bloom/lib/commands/top-k/RESERVE.ts
+++ b/packages/bloom/lib/commands/top-k/RESERVE.ts
@@ -1,29 +1,36 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
-
-interface ReserveOptions {
-    width: number;
-    depth: number;
-    decay: number;
+export interface TopKReserveOptions {
+  width: number;
+  depth: number;
+  decay: number;
 }
 
-export function transformArguments(
-    key: string,
-    topK: number,
-    options?: ReserveOptions
-): Array<string> {
-    const args = ['TOPK.RESERVE', key, topK.toString()];
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Creates a new Top-K filter with specified parameters
+   * @param parser - The command parser
+   * @param key - The name of the Top-K filter
+   * @param topK - Number of top occurring items to keep
+   * @param options - Optional parameters for filter configuration
+   * @param options.width - Number of counters in each array
+   * @param options.depth - Number of counter-arrays
+   * @param options.decay - Counter decay factor
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, topK: number, options?: TopKReserveOptions) {
+    parser.push('TOPK.RESERVE');
+    parser.pushKey(key);
+    parser.push(topK.toString());
 
     if (options) {
-        args.push(
-            options.width.toString(),
-            options.depth.toString(),
-            options.decay.toString()
-        );
+      parser.push(
+        options.width.toString(),
+        options.depth.toString(),
+        options.decay.toString()
+      );
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/bloom/lib/commands/top-k/index.ts b/packages/bloom/lib/commands/top-k/index.ts
index 750c91dfa88..fb5de543cab 100644
--- a/packages/bloom/lib/commands/top-k/index.ts
+++ b/packages/bloom/lib/commands/top-k/index.ts
@@ -1,27 +1,28 @@
-import * as ADD from './ADD';
-import * as COUNT from './COUNT';
-import * as INCRBY from './INCRBY';
-import * as INFO from './INFO';
-import * as LIST_WITHCOUNT from './LIST_WITHCOUNT';
-import * as LIST from './LIST';
-import * as QUERY from './QUERY';
-import * as RESERVE from './RESERVE';
+import type { RedisCommands } from '@redis/client/dist/lib/RESP/types';
+import ADD from './ADD';
+import COUNT from './COUNT';
+import INCRBY from './INCRBY';
+import INFO from './INFO';
+import LIST_WITHCOUNT from './LIST_WITHCOUNT';
+import LIST from './LIST';
+import QUERY from './QUERY';
+import RESERVE from './RESERVE';
 
 export default {
-    ADD,
-    add: ADD,
-    COUNT,
-    count: COUNT,
-    INCRBY,
-    incrBy: INCRBY,
-    INFO,
-    info: INFO,
-    LIST_WITHCOUNT,
-    listWithCount: LIST_WITHCOUNT,
-    LIST,
-    list: LIST,
-    QUERY,
-    query: QUERY,
-    RESERVE,
-    reserve: RESERVE
-};
+  ADD,
+  add: ADD,
+  COUNT,
+  count: COUNT,
+  INCRBY,
+  incrBy: INCRBY,
+  INFO,
+  info: INFO,
+  LIST_WITHCOUNT,
+  listWithCount: LIST_WITHCOUNT,
+  LIST,
+  list: LIST,
+  QUERY,
+  query: QUERY,
+  RESERVE,
+  reserve: RESERVE
+} as const satisfies RedisCommands;
diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts
index a2e059b3b99..268ebca8cb9 100644
--- a/packages/bloom/lib/test-utils.ts
+++ b/packages/bloom/lib/test-utils.ts
@@ -1,19 +1,19 @@
 import TestUtils from '@redis/test-utils';
 import RedisBloomModules from '.';
 
-export default new TestUtils({
-    dockerImageName: 'redislabs/rebloom',
-    dockerImageVersionArgument: 'redisbloom-version',
-    defaultDockerVersion: 'edge'
+export default  TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '8.2-rc1'
 });
 
 export const GLOBAL = {
-    SERVERS: {
-        OPEN: {
-            serverArguments: ['--loadmodule /usr/lib/redis/modules/redisbloom.so'],
-            clientOptions: {
-                modules: RedisBloomModules
-            }
-        }
+  SERVERS: {
+    OPEN: {
+      serverArguments: [],
+      clientOptions: {
+        modules: RedisBloomModules
+      }
     }
+  }
 };
diff --git a/packages/bloom/package.json b/packages/bloom/package.json
index 8a9d9f7a87a..edc106214e0 100644
--- a/packages/bloom/package.json
+++ b/packages/bloom/package.json
@@ -1,30 +1,25 @@
 {
   "name": "@redis/bloom",
-  "version": "1.2.0",
+  "version": "5.7.0",
   "license": "MIT",
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
+  "main": "./dist/lib/index.js",
+  "types": "./dist/lib/index.d.ts",
   "files": [
-    "dist/"
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
   ],
   "scripts": {
-    "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
-    "build": "tsc",
-    "documentation": "typedoc"
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
+    "release": "release-it"
   },
   "peerDependencies": {
-    "@redis/client": "^1.0.0"
+    "@redis/client": "^5.7.0"
   },
   "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
-    "@redis/test-utils": "*",
-    "@types/node": "^20.6.2",
-    "nyc": "^15.1.0",
-    "release-it": "^16.1.5",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typedoc": "^0.25.1",
-    "typescript": "^5.2.2"
+    "@redis/test-utils": "*"
+  },
+  "engines": {
+    "node": ">= 18"
   },
   "repository": {
     "type": "git",
diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json
deleted file mode 100644
index 4536bc31338..00000000000
--- a/packages/client/.eslintrc.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-    "root": true,
-    "parser": "@typescript-eslint/parser",
-    "plugins": [
-      "@typescript-eslint"
-    ],
-    "extends": [
-      "eslint:recommended",
-      "plugin:@typescript-eslint/eslint-recommended",
-      "plugin:@typescript-eslint/recommended"
-    ],
-    "rules": {
-      "semi": [2, "always"]
-    }
-  }
diff --git a/packages/client/.npmignore b/packages/client/.npmignore
deleted file mode 100644
index d064e1d0db6..00000000000
--- a/packages/client/.npmignore
+++ /dev/null
@@ -1,10 +0,0 @@
-.nyc_output/
-coverage/
-documentation/
-lib/
-.eslintrc.json
-.nycrc.json
-.release-it.json
-dump.rdb
-index.ts
-tsconfig.json
diff --git a/packages/client/.release-it.json b/packages/client/.release-it.json
index 035124348ca..fe1a6ad0d0d 100644
--- a/packages/client/.release-it.json
+++ b/packages/client/.release-it.json
@@ -1,10 +1,13 @@
 {
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
   "git": {
     "tagName": "client@${version}",
+    "tagMatch": "client@*",
     "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
-  },
-  "npm": {
-    "publishArgs": ["--access", "public"]
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
   }
 }
diff --git a/packages/client/index.ts b/packages/client/index.ts
index 8b21c5d5a32..2deb0c39b47 100644
--- a/packages/client/index.ts
+++ b/packages/client/index.ts
@@ -1,24 +1,39 @@
-import RedisClient from './lib/client';
-import RedisCluster from './lib/cluster';
-
-export { RedisClientType, RedisClientOptions } from './lib/client';
-
-export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands';
+export {
+  /* CommandPolicies, */
+  RedisArgument,
+  RedisFunctions,
+  RedisModules,
+  RedisScripts,
+  RespVersions,
+  TypeMapping,
+} from './lib/RESP/types';
+export { RESP_TYPES } from './lib/RESP/decoder';
+export { VerbatimString } from './lib/RESP/verbatim-string';
+export { defineScript } from './lib/lua-script';
+export * from './lib/errors';
 
+import RedisClient, { RedisClientOptions, RedisClientType } from './lib/client';
+export { RedisClientOptions, RedisClientType };
 export const createClient = RedisClient.create;
+export { CommandParser } from './lib/client/parser';
 
-export const commandOptions = RedisClient.commandOptions;
-
-export { RedisClusterType, RedisClusterOptions } from './lib/cluster';
+import { RedisClientPool, RedisPoolOptions, RedisClientPoolType } from './lib/client/pool';
+export { RedisClientPoolType, RedisPoolOptions };
+export const createClientPool = RedisClientPool.create;
 
+import RedisCluster, { RedisClusterOptions, RedisClusterType } from './lib/cluster';
+export { RedisClusterType, RedisClusterOptions };
 export const createCluster = RedisCluster.create;
 
-export { defineScript } from './lib/lua-script';
-
-export * from './lib/errors';
+import RedisSentinel from './lib/sentinel';
+export { RedisSentinelOptions, RedisSentinelType } from './lib/sentinel/types';
+export const createSentinel = RedisSentinel.create;
 
-export { GeoReplyWith } from './lib/commands/generic-transformers';
+export { GEO_REPLY_WITH, GeoReplyWith } from './lib/commands/GEOSEARCH_WITH';
 
 export { SetOptions } from './lib/commands/SET';
 
-export { RedisFlushModes } from './lib/commands/FLUSHALL';
+export { REDIS_FLUSH_MODES } from './lib/commands/FLUSHALL';
+
+export { BasicClientSideCache, BasicPooledClientSideCache } from './lib/client/cache';
+
diff --git a/packages/client/lib/RESP/decoder.spec.ts b/packages/client/lib/RESP/decoder.spec.ts
new file mode 100644
index 00000000000..c034815c9cd
--- /dev/null
+++ b/packages/client/lib/RESP/decoder.spec.ts
@@ -0,0 +1,426 @@
+import { strict as assert } from 'node:assert';
+import { SinonSpy, spy } from 'sinon';
+import { Decoder, RESP_TYPES } from './decoder';
+import { BlobError, SimpleError } from '../errors';
+import { TypeMapping } from './types';
+import { VerbatimString } from './verbatim-string';
+
+interface Test {
+  toWrite: Buffer;
+  typeMapping?: TypeMapping;
+  replies?: Array<unknown>;
+  errorReplies?: Array<unknown>;
+  pushReplies?: Array<unknown>;
+}
+
+function test(name: string, config: Test) {
+  describe(name, () => {
+    it('single chunk', () => {
+      const setup = setupTest(config);
+      setup.decoder.write(config.toWrite);
+      assertSpiesCalls(config, setup);
+    });
+
+    it('byte by byte', () => {
+      const setup = setupTest(config);
+      for (let i = 0; i < config.toWrite.length; i++) {
+        setup.decoder.write(config.toWrite.subarray(i, i + 1));
+      }
+      assertSpiesCalls(config, setup);
+    });
+  })
+}
+
+function setupTest(config: Test) {
+  const onReplySpy = spy(),
+    onErrorReplySpy = spy(),
+    onPushSpy = spy();
+
+  return {
+    decoder: new Decoder({
+      getTypeMapping: () => config.typeMapping ?? {},
+      onReply: onReplySpy,
+      onErrorReply: onErrorReplySpy,
+      onPush: onPushSpy
+    }),
+    onReplySpy,
+    onErrorReplySpy,
+    onPushSpy
+  };
+}
+
+function assertSpiesCalls(config: Test, spies: ReturnType<typeof setupTest>) {
+  assertSpyCalls(spies.onReplySpy, config.replies);
+  assertSpyCalls(spies.onErrorReplySpy, config.errorReplies);
+  assertSpyCalls(spies.onPushSpy, config.pushReplies);
+}
+
+function assertSpyCalls(spy: SinonSpy, replies?: Array<unknown>) {
+  if (!replies) {
+    assert.equal(spy.callCount, 0);
+    return;
+  }
+
+  assert.equal(spy.callCount, replies.length);
+  for (const [i, reply] of replies.entries()) {
+    assert.deepEqual(
+      spy.getCall(i).args,
+      [reply]
+    );
+  }
+}
+
+describe('RESP Decoder', () => {
+  test('Null', {
+    toWrite: Buffer.from('_\r\n'),
+    replies: [null]
+  });
+  
+  describe('Boolean', () => {
+    test('true', {
+      toWrite: Buffer.from('#t\r\n'),
+      replies: [true]
+    });
+  
+    test('false', {
+      toWrite: Buffer.from('#f\r\n'),
+      replies: [false]
+    });
+  });
+
+  describe('Number', () => {
+    test('0', {
+      toWrite: Buffer.from(':0\r\n'),
+      replies: [0]
+    });
+
+    test('1', {
+      toWrite: Buffer.from(':+1\r\n'),
+      replies: [1]
+    });
+
+    test('+1', {
+      toWrite: Buffer.from(':+1\r\n'),
+      replies: [1]
+    });
+
+    test('-1', {
+      toWrite: Buffer.from(':-1\r\n'),
+      replies: [-1]
+    });
+
+    test('1 as string', {
+      typeMapping: {
+        [RESP_TYPES.NUMBER]: String
+      },
+      toWrite: Buffer.from(':1\r\n'),
+      replies: ['1']
+    });
+  });
+
+  describe('BigNumber', () => {
+    test('0', {
+      toWrite: Buffer.from('(0\r\n'),
+      replies: [0n]
+    });
+
+    test('1', {
+      toWrite: Buffer.from('(1\r\n'),
+      replies: [1n]
+    });
+
+    test('+1', {
+      toWrite: Buffer.from('(+1\r\n'),
+      replies: [1n]
+    });
+
+    test('-1', {
+      toWrite: Buffer.from('(-1\r\n'),
+      replies: [-1n]
+    });
+
+    test('1 as string', {
+      typeMapping: {
+        [RESP_TYPES.BIG_NUMBER]: String
+      },
+      toWrite: Buffer.from('(1\r\n'),
+      replies: ['1']
+    });
+  });
+
+  describe('Double', () => {
+    test('0', {
+      toWrite: Buffer.from(',0\r\n'),
+      replies: [0]
+    });
+
+    test('1', {
+      toWrite: Buffer.from(',1\r\n'),
+      replies: [1]
+    });
+
+    test('+1', {
+      toWrite: Buffer.from(',+1\r\n'),
+      replies: [1]
+    });
+
+    test('-1', {
+      toWrite: Buffer.from(',-1\r\n'),
+      replies: [-1]
+    });
+
+    test('1.1', {
+      toWrite: Buffer.from(',1.1\r\n'),
+      replies: [1.1]
+    });
+
+    test('nan', {
+      toWrite: Buffer.from(',nan\r\n'),
+      replies: [NaN]
+    });
+
+    test('inf', {
+      toWrite: Buffer.from(',inf\r\n'),
+      replies: [Infinity]
+    });
+
+    test('+inf', {
+      toWrite: Buffer.from(',+inf\r\n'),
+      replies: [Infinity]
+    });
+
+    test('-inf', {
+      toWrite: Buffer.from(',-inf\r\n'),
+      replies: [-Infinity]
+    });
+
+    test('1e1', {
+      toWrite: Buffer.from(',1e1\r\n'),
+      replies: [1e1]
+    });
+
+    test('-1.1E+1', {
+      toWrite: Buffer.from(',-1.1E+1\r\n'),
+      replies: [-1.1E+1]
+    });
+
+    test('1 as string', {
+      typeMapping: {
+        [RESP_TYPES.DOUBLE]: String
+      },
+      toWrite: Buffer.from(',1\r\n'),
+      replies: ['1']
+    });
+  });
+
+  describe('SimpleString', () => {
+    test("'OK'", {
+      toWrite: Buffer.from('+OK\r\n'),
+      replies: ['OK']
+    });
+
+    test("'OK' as Buffer", {
+      typeMapping: {
+        [RESP_TYPES.SIMPLE_STRING]: Buffer
+      },
+      toWrite: Buffer.from('+OK\r\n'),
+      replies: [Buffer.from('OK')]
+    });
+  });
+
+  describe('BlobString', () => {
+    test("''", {
+      toWrite: Buffer.from('$0\r\n\r\n'),
+      replies: ['']
+    });
+
+    test("'1234567890'", {
+      toWrite: Buffer.from('$10\r\n1234567890\r\n'),
+      replies: ['1234567890']
+    });
+
+    test('null (RESP2 backwards compatibility)', {
+      toWrite: Buffer.from('$-1\r\n'),
+      replies: [null]
+    });
+
+    test("'OK' as Buffer", {
+      typeMapping: {
+        [RESP_TYPES.BLOB_STRING]: Buffer
+      },
+      toWrite: Buffer.from('$2\r\nOK\r\n'),
+      replies: [Buffer.from('OK')]
+    });
+  });
+
+  describe('VerbatimString', () => {
+    test("''", {
+      toWrite: Buffer.from('=4\r\ntxt:\r\n'),
+      replies: ['']
+    });
+
+    test("'123456'", {
+      toWrite: Buffer.from('=10\r\ntxt:123456\r\n'),
+      replies: ['123456']
+    });
+
+    test("'OK' as VerbatimString", {
+      typeMapping: {
+        [RESP_TYPES.VERBATIM_STRING]: VerbatimString
+      },
+      toWrite: Buffer.from('=6\r\ntxt:OK\r\n'),
+      replies: [new VerbatimString('txt', 'OK')]
+    });
+
+    test("'OK' as Buffer", {
+      typeMapping: {
+        [RESP_TYPES.VERBATIM_STRING]: Buffer
+      },
+      toWrite: Buffer.from('=6\r\ntxt:OK\r\n'),
+      replies: [Buffer.from('OK')]
+    });
+  });
+
+  test('SimpleError', {
+    toWrite: Buffer.from('-ERROR\r\n'),
+    errorReplies: [new SimpleError('ERROR')]
+  });
+
+  test('BlobError', {
+    toWrite: Buffer.from('!5\r\nERROR\r\n'),
+    errorReplies: [new BlobError('ERROR')]
+  });
+
+  describe('Array', () => {
+    test('[]', {
+      toWrite: Buffer.from('*0\r\n'),
+      replies: [[]]
+    });
+
+    test('[0..9]', {
+      toWrite: Buffer.from(`*10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
+      replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
+    });
+
+    test('with all types', {
+      toWrite: Buffer.from([
+        '*13\r\n',
+        '_\r\n',
+        '#f\r\n',
+        ':0\r\n',
+        '(0\r\n',
+        ',0\r\n',
+        '+\r\n',
+        '$0\r\n\r\n',
+        '=4\r\ntxt:\r\n',
+        '-\r\n',
+        '!0\r\n\r\n',
+        '*0\r\n',
+        '~0\r\n',
+        '%0\r\n'
+      ].join('')),
+      replies: [[
+        null,
+        false,
+        0,
+        0n,
+        0,
+        '',
+        '',
+        '',
+        new SimpleError(''),
+        new BlobError(''),
+        [],
+        [],
+        Object.create(null)
+      ]]
+    });
+
+    test('null (RESP2 backwards compatibility)', {
+      toWrite: Buffer.from('*-1\r\n'),
+      replies: [null]
+    });
+  });
+
+  describe('Set', () => {
+    test('empty', {
+      toWrite: Buffer.from('~0\r\n'),
+      replies: [[]]
+    });
+
+    test('of 0..9', {
+      toWrite: Buffer.from(`~10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
+      replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
+    });
+
+    test('0..9 as Set', {
+      typeMapping: {
+        [RESP_TYPES.SET]: Set
+      },
+      toWrite: Buffer.from(`~10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
+      replies: [new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])]
+    });
+  });
+
+  describe('Map', () => {
+    test('{}', {
+      toWrite: Buffer.from('%0\r\n'),
+      replies: [Object.create(null)]
+    });
+
+    test("{ '0'..'9': <key> }", {
+      toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`),
+      replies: [Object.create(null, {
+        0: { value: '0', enumerable: true },
+        1: { value: '1', enumerable: true },
+        2: { value: '2', enumerable: true },
+        3: { value: '3', enumerable: true },
+        4: { value: '4', enumerable: true },
+        5: { value: '5', enumerable: true },
+        6: { value: '6', enumerable: true },
+        7: { value: '7', enumerable: true },
+        8: { value: '8', enumerable: true },
+        9: { value: '9', enumerable: true }
+      })]
+    });
+
+    test("{ '0'..'9': <key> } as Map", {
+      typeMapping: {
+        [RESP_TYPES.MAP]: Map
+      },
+      toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`),
+      replies: [new Map([
+        ['0', '0'],
+        ['1', '1'],
+        ['2', '2'],
+        ['3', '3'],
+        ['4', '4'],
+        ['5', '5'],
+        ['6', '6'],
+        ['7', '7'],
+        ['8', '8'],
+        ['9', '9']
+      ])]
+    });
+
+    test("{ '0'..'9': <key> } as Array", {
+      typeMapping: {
+        [RESP_TYPES.MAP]: Array
+      },
+      toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`),
+      replies: [['0', '0', '1', '1', '2', '2', '3', '3', '4', '4', '5', '5', '6', '6', '7', '7', '8', '8', '9', '9']]
+    });
+  });
+
+  describe('Push', () => {
+    test('[]', {
+      toWrite: Buffer.from('>0\r\n'),
+      pushReplies: [[]]
+    });
+
+    test('[0..9]', {
+      toWrite: Buffer.from(`>10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
+      pushReplies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
+    });
+  });
+});
diff --git a/packages/client/lib/RESP/decoder.ts b/packages/client/lib/RESP/decoder.ts
new file mode 100644
index 00000000000..2485ea23b37
--- /dev/null
+++ b/packages/client/lib/RESP/decoder.ts
@@ -0,0 +1,1178 @@
+// @ts-nocheck
+import { VerbatimString } from './verbatim-string';
+import { SimpleError, BlobError, ErrorReply } from '../errors';
+import { TypeMapping } from './types';
+
+// https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md
+export const RESP_TYPES = {
+  NULL: 95, // _
+  BOOLEAN: 35, // #
+  NUMBER: 58, // :
+  BIG_NUMBER: 40, // (
+  DOUBLE: 44, // ,
+  SIMPLE_STRING: 43, // +
+  BLOB_STRING: 36, // $
+  VERBATIM_STRING: 61, // =
+  SIMPLE_ERROR: 45, // -
+  BLOB_ERROR: 33, // !
+  ARRAY: 42, // *
+  SET: 126, // ~
+  MAP: 37, // %
+  PUSH: 62 // >
+} as const;
+
+const ASCII = {
+  '\r': 13,
+  't': 116,
+  '+': 43,
+  '-': 45,
+  '0': 48,
+  '.': 46,
+  'i': 105,
+  'n': 110,
+  'E': 69,
+  'e': 101
+} as const;
+
+export const PUSH_TYPE_MAPPING = {
+  [RESP_TYPES.BLOB_STRING]: Buffer
+};
+
+// this was written with performance in mind, so it's not very readable... sorry :(
+
+interface DecoderOptions {
+  onReply(reply: any): unknown;
+  onErrorReply(err: ErrorReply): unknown;
+  onPush(push: Array<any>): unknown;
+  getTypeMapping(): TypeMapping;
+}
+
+export class Decoder {
+  onReply;
+  onErrorReply;
+  onPush;
+  getTypeMapping;
+  #cursor = 0;
+  #next;
+
+  constructor(config: DecoderOptions) {
+    this.onReply = config.onReply;
+    this.onErrorReply = config.onErrorReply;
+    this.onPush = config.onPush;
+    this.getTypeMapping = config.getTypeMapping;
+  }
+
+  reset() {
+    this.#cursor = 0;
+    this.#next = undefined;
+  }
+
+  write(chunk) {
+    if (this.#cursor >= chunk.length) {
+      this.#cursor -= chunk.length;
+      return;
+    }
+
+    if (this.#next) {
+      if (this.#next(chunk) || this.#cursor >= chunk.length) {
+        this.#cursor -= chunk.length;
+        return;
+      }
+    }
+
+    do {
+      const type = chunk[this.#cursor];
+      if (++this.#cursor === chunk.length) {
+        this.#next = this.#continueDecodeTypeValue.bind(this, type);
+        break;
+      }
+
+      if (this.#decodeTypeValue(type, chunk)) {
+        break;
+      }
+    } while (this.#cursor < chunk.length);
+    this.#cursor -= chunk.length;
+  }
+
+  #continueDecodeTypeValue(type, chunk) {
+    this.#next = undefined;
+    return this.#decodeTypeValue(type, chunk);
+  }
+    
+  #decodeTypeValue(type, chunk) {
+    switch (type) {
+      case RESP_TYPES.NULL:
+        this.onReply(this.#decodeNull());
+        return false;
+
+      case RESP_TYPES.BOOLEAN:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeBoolean(chunk)
+        );
+
+      case RESP_TYPES.NUMBER:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeNumber(
+            this.getTypeMapping()[RESP_TYPES.NUMBER],
+            chunk
+          )
+        );
+
+      case RESP_TYPES.BIG_NUMBER:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeBigNumber(
+            this.getTypeMapping()[RESP_TYPES.BIG_NUMBER],
+            chunk
+          )
+        );
+      
+      case RESP_TYPES.DOUBLE:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeDouble(
+            this.getTypeMapping()[RESP_TYPES.DOUBLE],
+            chunk
+          )
+        );
+      
+      case RESP_TYPES.SIMPLE_STRING:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeSimpleString(
+            this.getTypeMapping()[RESP_TYPES.SIMPLE_STRING],
+            chunk
+          )
+        );
+      
+      case RESP_TYPES.BLOB_STRING:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeBlobString(
+            this.getTypeMapping()[RESP_TYPES.BLOB_STRING],
+            chunk
+          )
+        );
+
+      case RESP_TYPES.VERBATIM_STRING:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeVerbatimString(
+            this.getTypeMapping()[RESP_TYPES.VERBATIM_STRING],
+            chunk
+          )
+        );
+
+      case RESP_TYPES.SIMPLE_ERROR:
+        return this.#handleDecodedValue(
+          this.onErrorReply,
+          this.#decodeSimpleError(chunk)
+        );
+      
+      case RESP_TYPES.BLOB_ERROR:
+        return this.#handleDecodedValue(
+          this.onErrorReply,
+          this.#decodeBlobError(chunk)
+        );
+
+      case RESP_TYPES.ARRAY:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeArray(this.getTypeMapping(), chunk)
+        );
+
+      case RESP_TYPES.SET:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeSet(this.getTypeMapping(), chunk)
+        );
+      
+      case RESP_TYPES.MAP:
+        return this.#handleDecodedValue(
+          this.onReply,
+          this.#decodeMap(this.getTypeMapping(), chunk)
+        );
+
+      case RESP_TYPES.PUSH:
+        return this.#handleDecodedValue(
+          this.onPush,
+          this.#decodeArray(PUSH_TYPE_MAPPING, chunk)
+        );
+
+      default:
+        throw new Error(`Unknown RESP type ${type} "${String.fromCharCode(type)}"`);
+    }
+  }
+
+  #handleDecodedValue(cb, value) {
+    if (typeof value === 'function') {
+      this.#next = this.#continueDecodeValue.bind(this, cb, value);
+      return true;
+    }
+
+    cb(value);
+    return false;
+  }
+
+  #continueDecodeValue(cb, next, chunk) {
+    this.#next = undefined;
+    return this.#handleDecodedValue(cb, next(chunk));
+  }
+
+  #decodeNull() {
+    this.#cursor += 2; // skip \r\n
+    return null;
+  }
+
+  #decodeBoolean(chunk) {
+    const boolean = chunk[this.#cursor] === ASCII.t;
+    this.#cursor += 3; // skip {t | f}\r\n
+    return boolean;
+  }
+
+  #decodeNumber(type, chunk) {
+    if (type === String) {
+      return this.#decodeSimpleString(String, chunk);
+    }
+
+    switch (chunk[this.#cursor]) {
+      case ASCII['+']:
+        return this.#maybeDecodeNumberValue(false, chunk);
+
+      case ASCII['-']:
+        return this.#maybeDecodeNumberValue(true, chunk);
+
+      default:
+        return this.#decodeNumberValue(
+          false,
+          this.#decodeUnsingedNumber.bind(this, 0),
+          chunk
+        );
+    }
+  }
+
+  #maybeDecodeNumberValue(isNegative, chunk) {
+    const cb = this.#decodeUnsingedNumber.bind(this, 0);
+    return ++this.#cursor === chunk.length ?
+      this.#decodeNumberValue.bind(this, isNegative, cb) :
+      this.#decodeNumberValue(isNegative, cb, chunk);
+  }
+
+  #decodeNumberValue(isNegative, numberCb, chunk) {
+    const number = numberCb(chunk);
+    return typeof number === 'function' ?
+      this.#decodeNumberValue.bind(this, isNegative, number) :
+      isNegative ? -number : number;
+  }
+
+  #decodeUnsingedNumber(number, chunk) {
+    let cursor = this.#cursor;
+    do {
+      const byte = chunk[cursor];
+      if (byte === ASCII['\r']) {
+        this.#cursor = cursor + 2; // skip \r\n
+        return number;
+      }
+      number = number * 10 + byte - ASCII['0'];
+    } while (++cursor < chunk.length);
+
+    this.#cursor = cursor;
+    return this.#decodeUnsingedNumber.bind(this, number);
+  }
+
+  #decodeBigNumber(type, chunk) {
+    if (type === String) {
+      return this.#decodeSimpleString(String, chunk);
+    }
+
+    switch (chunk[this.#cursor]) {
+      case ASCII['+']:
+        return this.#maybeDecodeBigNumberValue(false, chunk);
+
+      case ASCII['-']:
+        return this.#maybeDecodeBigNumberValue(true, chunk);
+
+      default:
+        return this.#decodeBigNumberValue(
+          false,
+          this.#decodeUnsingedBigNumber.bind(this, 0n),
+          chunk
+        );
+    }
+  }
+
+  #maybeDecodeBigNumberValue(isNegative, chunk) {
+    const cb = this.#decodeUnsingedBigNumber.bind(this, 0n);
+    return ++this.#cursor === chunk.length ?
+      this.#decodeBigNumberValue.bind(this, isNegative, cb) :
+      this.#decodeBigNumberValue(isNegative, cb, chunk);
+  }
+
+  #decodeBigNumberValue(isNegative, bigNumberCb, chunk) {
+    const bigNumber = bigNumberCb(chunk);
+    return typeof bigNumber === 'function' ?
+      this.#decodeBigNumberValue.bind(this, isNegative, bigNumber) :
+      isNegative ? -bigNumber : bigNumber;
+  }
+
+  #decodeUnsingedBigNumber(bigNumber, chunk) {
+    let cursor = this.#cursor;
+    do {
+      const byte = chunk[cursor];
+      if (byte === ASCII['\r']) {
+        this.#cursor = cursor + 2; // skip \r\n
+        return bigNumber;
+      }
+      bigNumber = bigNumber * 10n + BigInt(byte - ASCII['0']);
+    } while (++cursor < chunk.length);
+
+    this.#cursor = cursor;
+    return this.#decodeUnsingedBigNumber.bind(this, bigNumber);
+  }
+
+  #decodeDouble(type, chunk) {
+    if (type === String) {
+      return this.#decodeSimpleString(String, chunk);
+    }
+
+    switch (chunk[this.#cursor]) {
+      case ASCII.n:
+        this.#cursor += 5; // skip nan\r\n
+        return NaN;
+
+      case ASCII['+']:
+        return this.#maybeDecodeDoubleInteger(false, chunk);
+
+      case ASCII['-']:
+        return this.#maybeDecodeDoubleInteger(true, chunk);
+
+      default:
+        return this.#decodeDoubleInteger(false, 0, chunk);
+    }
+  }
+
+  #maybeDecodeDoubleInteger(isNegative, chunk) {
+    return ++this.#cursor === chunk.length ?
+      this.#decodeDoubleInteger.bind(this, isNegative, 0) :
+      this.#decodeDoubleInteger(isNegative, 0, chunk);
+  }
+
+  #decodeDoubleInteger(isNegative, integer, chunk) {
+    if (chunk[this.#cursor] === ASCII.i) {
+      this.#cursor += 5; // skip inf\r\n
+      return isNegative ? -Infinity : Infinity;
+    }
+
+    return this.#continueDecodeDoubleInteger(isNegative, integer, chunk);
+  }
+
+  #continueDecodeDoubleInteger(isNegative, integer, chunk) {
+    let cursor = this.#cursor;
+    do {
+      const byte = chunk[cursor];
+      switch (byte) {
+        case ASCII['.']:
+          this.#cursor = cursor + 1; // skip .
+          return this.#cursor < chunk.length ?
+            this.#decodeDoubleDecimal(isNegative, 0, integer, chunk) :
+            this.#decodeDoubleDecimal.bind(this, isNegative, 0, integer);
+
+        case ASCII.E:
+        case ASCII.e:
+          this.#cursor = cursor + 1; // skip E/e
+          const i = isNegative ? -integer : integer;
+          return this.#cursor < chunk.length ?
+            this.#decodeDoubleExponent(i, chunk) :
+            this.#decodeDoubleExponent.bind(this, i);
+
+        case ASCII['\r']:
+          this.#cursor = cursor + 2; // skip \r\n
+          return isNegative ? -integer : integer;
+
+        default:
+          integer = integer * 10 + byte - ASCII['0'];
+      }
+    } while (++cursor < chunk.length);
+
+    this.#cursor = cursor;
+    return this.#continueDecodeDoubleInteger.bind(this, isNegative, integer);
+  }
+
+  // Precalculated multipliers for decimal points to improve performance
+  // "... about 15 to 17 decimal places ..."
+  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#:~:text=about%2015%20to%2017%20decimal%20places
+  static #DOUBLE_DECIMAL_MULTIPLIERS = [
+    1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6,
+    1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12,
+    1e-13, 1e-14, 1e-15, 1e-16, 1e-17
+  ];
+
+  #decodeDoubleDecimal(isNegative, decimalIndex, double, chunk) {
+    let cursor = this.#cursor;
+    do {
+      const byte = chunk[cursor];
+      switch (byte) {
+        case ASCII.E:
+        case ASCII.e:
+          this.#cursor = cursor + 1; // skip E/e
+          const d = isNegative ? -double : double;
+          return this.#cursor === chunk.length ?
+            this.#decodeDoubleExponent.bind(this, d) :
+            this.#decodeDoubleExponent(d, chunk);
+        
+        case ASCII['\r']:
+          this.#cursor = cursor + 2; // skip \r\n
+          return isNegative ? -double : double;
+      }
+      
+      if (decimalIndex < Decoder.#DOUBLE_DECIMAL_MULTIPLIERS.length) {
+        double += (byte - ASCII['0']) * Decoder.#DOUBLE_DECIMAL_MULTIPLIERS[decimalIndex++];
+      }
+    } while (++cursor < chunk.length);
+    
+    this.#cursor = cursor;
+    return this.#decodeDoubleDecimal.bind(this, isNegative, decimalIndex, double);
+  }
+
+  #decodeDoubleExponent(double, chunk) {
+    switch (chunk[this.#cursor]) {
+      case ASCII['+']:
+        return ++this.#cursor === chunk.length ?
+          this.#continueDecodeDoubleExponent.bind(this, false, double, 0) :
+          this.#continueDecodeDoubleExponent(false, double, 0, chunk);
+
+      case ASCII['-']:
+        return ++this.#cursor === chunk.length ?
+          this.#continueDecodeDoubleExponent.bind(this, true, double, 0) :
+          this.#continueDecodeDoubleExponent(true, double, 0, chunk);
+    }
+
+    return this.#continueDecodeDoubleExponent(false, double, 0, chunk);
+  }
+
+  #continueDecodeDoubleExponent(isNegative, double, exponent, chunk) {
+    let cursor = this.#cursor;
+    do {
+      const byte = chunk[cursor];
+      if (byte === ASCII['\r']) {
+        this.#cursor = cursor + 2; // skip \r\n
+        return double * 10 ** (isNegative ? -exponent : exponent);
+      }
+
+      exponent = exponent * 10 + byte - ASCII['0'];
+    } while (++cursor < chunk.length);
+
+    this.#cursor = cursor;
+    return this.#continueDecodeDoubleExponent.bind(this, isNegative, double, exponent);
+  }
+
+  #findCRLF(chunk, cursor) {
+    while (chunk[cursor] !== ASCII['\r']) {
+      if (++cursor === chunk.length) {
+        this.#cursor = chunk.length;
+        return -1;
+      }
+    }
+
+    this.#cursor = cursor + 2; // skip \r\n
+    return cursor;
+  }
+
+  #decodeSimpleString(type, chunk) {
+    const start = this.#cursor,
+      crlfIndex = this.#findCRLF(chunk, start);
+    if (crlfIndex === -1) {
+      return this.#continueDecodeSimpleString.bind(
+        this,
+        [chunk.subarray(start)],
+        type
+      );
+    }
+
+    const slice = chunk.subarray(start, crlfIndex);
+    return type === Buffer ?
+      slice :
+      slice.toString();
+  }
+
+  #continueDecodeSimpleString(chunks, type, chunk) {
+    const start = this.#cursor,
+      crlfIndex = this.#findCRLF(chunk, start);
+    if (crlfIndex === -1) {
+      chunks.push(chunk.subarray(start));
+      return this.#continueDecodeSimpleString.bind(this, chunks, type);
+    }
+
+    chunks.push(chunk.subarray(start, crlfIndex));
+    return type === Buffer ?
+      Buffer.concat(chunks) :
+      chunks.join('');
+  }
+
+  #decodeBlobString(type, chunk) {
+    // RESP 2 bulk string null
+    // https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md#resp-bulk-strings
+    if (chunk[this.#cursor] === ASCII['-']) {
+      this.#cursor += 4; // skip -1\r\n
+      return null;
+    }
+
+    const length = this.#decodeUnsingedNumber(0, chunk);
+    if (typeof length === 'function') {
+      return this.#continueDecodeBlobStringLength.bind(this, length, type);
+    } else if (this.#cursor >= chunk.length) {
+      return this.#decodeBlobStringWithLength.bind(this, length, type);
+    }
+
+    return this.#decodeBlobStringWithLength(length, type, chunk);
+  }
+
+  #continueDecodeBlobStringLength(lengthCb, type, chunk) {
+    const length = lengthCb(chunk);
+    if (typeof length === 'function') {
+      return this.#continueDecodeBlobStringLength.bind(this, length, type);
+    } else if (this.#cursor >= chunk.length) {
+      return this.#decodeBlobStringWithLength.bind(this, length, type);
+    }
+
+    return this.#decodeBlobStringWithLength(length, type, chunk);
+  }
+
+  #decodeStringWithLength(length, skip, type, chunk) {
+    const end = this.#cursor + length;
+    if (end >= chunk.length) {
+      const slice = chunk.subarray(this.#cursor);
+      this.#cursor = chunk.length;
+      return this.#continueDecodeStringWithLength.bind(
+        this,
+        length - slice.length,
+        [slice],
+        skip,
+        type
+      );
+    }
+
+    const slice = chunk.subarray(this.#cursor, end);
+    this.#cursor = end + skip;
+    return type === Buffer ?
+      slice :
+      slice.toString();
+  }
+
+  #continueDecodeStringWithLength(length, chunks, skip, type, chunk) {
+    const end = this.#cursor + length;
+    if (end >= chunk.length) {
+      const slice = chunk.subarray(this.#cursor);
+      chunks.push(slice);
+      this.#cursor = chunk.length;
+      return this.#continueDecodeStringWithLength.bind(
+        this,
+        length - slice.length,
+        chunks,
+        skip,
+        type
+      );
+    }
+
+    chunks.push(chunk.subarray(this.#cursor, end));
+    this.#cursor = end + skip;
+    return type === Buffer ?
+      Buffer.concat(chunks) :
+      chunks.join('');
+  }
+
+  #decodeBlobStringWithLength(length, type, chunk) {
+    return this.#decodeStringWithLength(length, 2, type, chunk);
+  }
+
+  #decodeVerbatimString(type, chunk) {
+    return this.#continueDecodeVerbatimStringLength(
+      this.#decodeUnsingedNumber.bind(this, 0),
+      type,
+      chunk
+    );
+  }
+
+  #continueDecodeVerbatimStringLength(lengthCb, type, chunk) {
+    const length = lengthCb(chunk);
+    return typeof length === 'function' ?
+      this.#continueDecodeVerbatimStringLength.bind(this, length, type) :
+      this.#decodeVerbatimStringWithLength(length, type, chunk);
+  }
+
+  #decodeVerbatimStringWithLength(length, type, chunk) {
+    const stringLength = length - 4; // skip <format>:
+    if (type === VerbatimString) {
+      return this.#decodeVerbatimStringFormat(stringLength, chunk);
+    }
+
+    this.#cursor += 4; // skip <format>:
+    return this.#cursor >= chunk.length ?
+      this.#decodeBlobStringWithLength.bind(this, stringLength, type) :
+      this.#decodeBlobStringWithLength(stringLength, type, chunk);
+  }
+
+  #decodeVerbatimStringFormat(stringLength, chunk) {
+    const formatCb = this.#decodeStringWithLength.bind(this, 3, 1, String); 
+    return this.#cursor >= chunk.length ?
+      this.#continueDecodeVerbatimStringFormat.bind(this, stringLength, formatCb) :
+      this.#continueDecodeVerbatimStringFormat(stringLength, formatCb, chunk);
+  }
+
+  #continueDecodeVerbatimStringFormat(stringLength, formatCb, chunk) {
+    const format = formatCb(chunk);
+    return typeof format === 'function' ?
+      this.#continueDecodeVerbatimStringFormat.bind(this, stringLength, format) :
+      this.#decodeVerbatimStringWithFormat(stringLength, format, chunk);
+  }
+
+  #decodeVerbatimStringWithFormat(stringLength, format, chunk) {
+    return this.#continueDecodeVerbatimStringWithFormat(
+      format,
+      this.#decodeBlobStringWithLength.bind(this, stringLength, String),
+      chunk
+    );
+  }
+
+  #continueDecodeVerbatimStringWithFormat(format, stringCb, chunk) {
+    const string = stringCb(chunk);
+    return typeof string === 'function' ?
+      this.#continueDecodeVerbatimStringWithFormat.bind(this, format, string) :
+      new VerbatimString(format, string);
+  }
+
+  #decodeSimpleError(chunk) {
+    const string = this.#decodeSimpleString(String, chunk);
+    return typeof string === 'function' ?
+      this.#continueDecodeSimpleError.bind(this, string) :
+      new SimpleError(string);
+  }
+
+  #continueDecodeSimpleError(stringCb, chunk) {
+    const string = stringCb(chunk);
+    return typeof string === 'function' ?
+      this.#continueDecodeSimpleError.bind(this, string) :
+      new SimpleError(string);
+  }
+
+  #decodeBlobError(chunk) {
+    const string = this.#decodeBlobString(String, chunk);
+    return typeof string === 'function' ?
+      this.#continueDecodeBlobError.bind(this, string) :
+      new BlobError(string);
+  }
+
+  #continueDecodeBlobError(stringCb, chunk) {
+    const string = stringCb(chunk);
+    return typeof string === 'function' ?
+      this.#continueDecodeBlobError.bind(this, string) :
+      new BlobError(string);
+  }
+
+  #decodeNestedType(typeMapping, chunk) {
+    const type = chunk[this.#cursor];
+    return ++this.#cursor === chunk.length ?
+      this.#decodeNestedTypeValue.bind(this, type, typeMapping) :
+      this.#decodeNestedTypeValue(type, typeMapping, chunk);
+  }
+
+  #decodeNestedTypeValue(type, typeMapping, chunk) {
+    switch (type) {
+      case RESP_TYPES.NULL:
+        return this.#decodeNull();
+
+      case RESP_TYPES.BOOLEAN:
+        return this.#decodeBoolean(chunk);
+
+      case RESP_TYPES.NUMBER:
+        return this.#decodeNumber(typeMapping[RESP_TYPES.NUMBER], chunk);
+
+      case RESP_TYPES.BIG_NUMBER:
+        return this.#decodeBigNumber(typeMapping[RESP_TYPES.BIG_NUMBER], chunk);
+      
+      case RESP_TYPES.DOUBLE:
+        return this.#decodeDouble(typeMapping[RESP_TYPES.DOUBLE], chunk);
+      
+      case RESP_TYPES.SIMPLE_STRING:
+        return this.#decodeSimpleString(typeMapping[RESP_TYPES.SIMPLE_STRING], chunk);
+      
+      case RESP_TYPES.BLOB_STRING:
+        return this.#decodeBlobString(typeMapping[RESP_TYPES.BLOB_STRING], chunk);
+
+      case RESP_TYPES.VERBATIM_STRING:
+        return this.#decodeVerbatimString(typeMapping[RESP_TYPES.VERBATIM_STRING], chunk);
+
+      case RESP_TYPES.SIMPLE_ERROR:
+        return this.#decodeSimpleError(chunk);
+      
+      case RESP_TYPES.BLOB_ERROR:
+        return this.#decodeBlobError(chunk);
+
+      case RESP_TYPES.ARRAY:
+        return this.#decodeArray(typeMapping, chunk);
+
+      case RESP_TYPES.SET:
+        return this.#decodeSet(typeMapping, chunk);
+      
+      case RESP_TYPES.MAP:
+        return this.#decodeMap(typeMapping, chunk);
+
+      default:
+        throw new Error(`Unknown RESP type ${type} "${String.fromCharCode(type)}"`);
+    }
+  }
+
+  #decodeArray(typeMapping, chunk) {
+    // RESP 2 null
+    // https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md#resp-arrays
+    if (chunk[this.#cursor] === ASCII['-']) {
+      this.#cursor += 4; // skip -1\r\n
+      return null;
+    }
+
+    return this.#decodeArrayWithLength(
+      this.#decodeUnsingedNumber(0, chunk),
+      typeMapping,
+      chunk
+    );
+  }
+
+  #decodeArrayWithLength(length, typeMapping, chunk) {
+    return typeof length === 'function' ?
+      this.#continueDecodeArrayLength.bind(this, length, typeMapping) :
+      this.#decodeArrayItems(
+        new Array(length),
+        0,
+        typeMapping,
+        chunk
+      );
+  }
+
+  #continueDecodeArrayLength(lengthCb, typeMapping, chunk) {
+    return this.#decodeArrayWithLength(
+      lengthCb(chunk),
+      typeMapping,
+      chunk
+    );
+  }
+
+  #decodeArrayItems(array, filled, typeMapping, chunk) {
+    for (let i = filled; i < array.length; i++) {
+      if (this.#cursor >= chunk.length) {
+        return this.#decodeArrayItems.bind(
+          this,
+          array,
+          i,
+          typeMapping
+        );
+      }
+
+      const item = this.#decodeNestedType(typeMapping, chunk);
+      if (typeof item === 'function') {
+        return this.#continueDecodeArrayItems.bind(
+          this,
+          array,
+          i,
+          item,
+          typeMapping
+        );
+      }
+
+      array[i] = item;
+    }
+
+    return array;
+  }
+
+  #continueDecodeArrayItems(array, filled, itemCb, typeMapping, chunk) {
+    const item = itemCb(chunk);
+    if (typeof item === 'function') {
+      return this.#continueDecodeArrayItems.bind(
+        this,
+        array,
+        filled,
+        item,
+        typeMapping
+      );
+    }
+
+    array[filled++] = item;
+
+    return this.#decodeArrayItems(array, filled, typeMapping, chunk);
+  }
+
+  #decodeSet(typeMapping, chunk) {
+    const length = this.#decodeUnsingedNumber(0, chunk);
+    if (typeof length === 'function') {
+      return this.#continueDecodeSetLength.bind(this, length, typeMapping);
+    }
+
+    return this.#decodeSetItems(
+      length,
+      typeMapping,
+      chunk
+    );
+  }
+
+  #continueDecodeSetLength(lengthCb, typeMapping, chunk) {
+    const length = lengthCb(chunk);
+    return typeof length === 'function' ?
+      this.#continueDecodeSetLength.bind(this, length, typeMapping) :
+      this.#decodeSetItems(length, typeMapping, chunk);
+  }
+
+  #decodeSetItems(length, typeMapping, chunk) {
+    return typeMapping[RESP_TYPES.SET] === Set ?
+      this.#decodeSetAsSet(
+        new Set(),
+        length,
+        typeMapping,
+        chunk
+      ) :
+      this.#decodeArrayItems(
+        new Array(length),
+        0,
+        typeMapping,
+        chunk
+      );
+  }
+
+  #decodeSetAsSet(set, remaining, typeMapping, chunk) {
+    // using `remaining` instead of `length` & `set.size` to make it work even if the set contains duplicates
+    while (remaining > 0) {
+      if (this.#cursor >= chunk.length) {
+        return this.#decodeSetAsSet.bind(
+          this,
+          set,
+          remaining,
+          typeMapping
+        );
+      }
+
+      const item = this.#decodeNestedType(typeMapping, chunk);
+      if (typeof item === 'function') {
+        return this.#continueDecodeSetAsSet.bind(
+          this,
+          set,
+          remaining,
+          item,
+          typeMapping
+        );
+      }
+
+      set.add(item);
+      --remaining;
+    }
+
+    return set;
+  }
+
+  #continueDecodeSetAsSet(set, remaining, itemCb, typeMapping, chunk) {
+    const item = itemCb(chunk);
+    if (typeof item === 'function') {
+      return this.#continueDecodeSetAsSet.bind(
+        this,
+        set,
+        remaining,
+        item,
+        typeMapping
+      );
+    }
+
+    set.add(item);
+
+    return this.#decodeSetAsSet(set, remaining - 1, typeMapping, chunk);
+  }
+
+  #decodeMap(typeMapping, chunk) {
+    const length = this.#decodeUnsingedNumber(0, chunk);
+    if (typeof length === 'function') {
+      return this.#continueDecodeMapLength.bind(this, length, typeMapping);
+    }
+
+    return this.#decodeMapItems(
+      length,
+      typeMapping,
+      chunk
+    );
+  }
+
+  #continueDecodeMapLength(lengthCb, typeMapping, chunk) {
+    const length = lengthCb(chunk);
+    return typeof length === 'function' ?
+      this.#continueDecodeMapLength.bind(this, length, typeMapping) :
+      this.#decodeMapItems(length, typeMapping, chunk);
+  }
+
+  #decodeMapItems(length, typeMapping, chunk) {
+    switch (typeMapping[RESP_TYPES.MAP]) {
+      case Map:
+        return this.#decodeMapAsMap(
+          new Map(),
+          length,
+          typeMapping,
+          chunk
+        );
+
+      case Array:
+        return this.#decodeArrayItems(
+          new Array(length * 2),
+          0,
+          typeMapping,
+          chunk
+        );
+
+      default:
+        return this.#decodeMapAsObject(
+          Object.create(null),
+          length,
+          typeMapping,
+          chunk
+        );
+    }
+  }
+
+  #decodeMapAsMap(map, remaining, typeMapping, chunk) {
+    // using `remaining` instead of `length` & `map.size` to make it work even if the map contains duplicate keys
+    while (remaining > 0) {
+      if (this.#cursor >= chunk.length) {
+        return this.#decodeMapAsMap.bind(
+          this,
+          map,
+          remaining,
+          typeMapping
+        );
+      }
+
+      const key = this.#decodeMapKey(typeMapping, chunk);
+      if (typeof key === 'function') {
+        return this.#continueDecodeMapKey.bind(
+          this,
+          map,
+          remaining,
+          key,
+          typeMapping
+        );
+      }
+
+      if (this.#cursor >= chunk.length) {
+        return this.#continueDecodeMapValue.bind(
+          this,
+          map,
+          remaining,
+          key,
+          this.#decodeNestedType.bind(this, typeMapping),
+          typeMapping
+        );
+      }
+
+      const value = this.#decodeNestedType(typeMapping, chunk);
+      if (typeof value === 'function') {
+        return this.#continueDecodeMapValue.bind(
+          this,
+          map,
+          remaining,
+          key,
+          value,
+          typeMapping
+        );
+      }
+
+      map.set(key, value);
+      --remaining;
+    }
+
+    return map;
+  }
+
+  #decodeMapKey(typeMapping, chunk) {
+    const type = chunk[this.#cursor];
+    return ++this.#cursor === chunk.length ?
+      this.#decodeMapKeyValue.bind(this, type, typeMapping) :
+      this.#decodeMapKeyValue(type, typeMapping, chunk);
+  }
+
+  #decodeMapKeyValue(type, typeMapping, chunk) {
+    switch (type) {
+      // decode simple string map key as string (and not as buffer)
+      case RESP_TYPES.SIMPLE_STRING:
+        return this.#decodeSimpleString(String, chunk);
+      
+      // decode blob string map key as string (and not as buffer)
+      case RESP_TYPES.BLOB_STRING:
+        return this.#decodeBlobString(String, chunk);
+
+      default:
+        return this.#decodeNestedTypeValue(type, typeMapping, chunk);
+    }
+  }
+
+  #continueDecodeMapKey(map, remaining, keyCb, typeMapping, chunk) {
+    const key = keyCb(chunk);
+    if (typeof key === 'function') {
+      return this.#continueDecodeMapKey.bind(
+        this,
+        map,
+        remaining,
+        key,
+        typeMapping
+      );
+    }
+
+    if (this.#cursor >= chunk.length) {
+      return this.#continueDecodeMapValue.bind(
+        this,
+        map,
+        remaining,
+        key,
+        this.#decodeNestedType.bind(this, typeMapping),
+        typeMapping
+      );
+    }      
+
+    const value = this.#decodeNestedType(typeMapping, chunk);
+    if (typeof value === 'function') {
+      return this.#continueDecodeMapValue.bind(
+        this,
+        map,
+        remaining,
+        key,
+        value,
+        typeMapping
+      );
+    }
+
+    map.set(key, value);
+    return this.#decodeMapAsMap(map, remaining - 1, typeMapping, chunk);
+  }
+
+  #continueDecodeMapValue(map, remaining, key, valueCb, typeMapping, chunk) {
+    const value = valueCb(chunk);
+    if (typeof value === 'function') {
+      return this.#continueDecodeMapValue.bind(
+        this,
+        map,
+        remaining,
+        key,
+        value,
+        typeMapping
+      );
+    }
+
+    map.set(key, value);
+
+    return this.#decodeMapAsMap(map, remaining - 1, typeMapping, chunk);
+  }
+
+  #decodeMapAsObject(object, remaining, typeMapping, chunk) {
+    while (remaining > 0) {
+      if (this.#cursor >= chunk.length) {
+        return this.#decodeMapAsObject.bind(
+          this,
+          object,
+          remaining,
+          typeMapping
+        );
+      }
+
+      const key = this.#decodeMapKey(typeMapping, chunk);
+      if (typeof key === 'function') {
+        return this.#continueDecodeMapAsObjectKey.bind(
+          this,
+          object,
+          remaining,
+          key,
+          typeMapping
+        );
+      }
+
+      if (this.#cursor >= chunk.length) {
+        return this.#continueDecodeMapAsObjectValue.bind(
+          this,
+          object,
+          remaining,
+          key,
+          this.#decodeNestedType.bind(this, typeMapping),
+          typeMapping
+        );
+      }
+
+      const value = this.#decodeNestedType(typeMapping, chunk);
+      if (typeof value === 'function') {
+        return this.#continueDecodeMapAsObjectValue.bind(
+          this,
+          object,
+          remaining,
+          key,
+          value,
+          typeMapping
+        );
+      }
+
+      object[key] = value;
+      --remaining;
+    }
+
+    return object;
+  }
+
+  #continueDecodeMapAsObjectKey(object, remaining, keyCb, typeMapping, chunk) {
+    const key = keyCb(chunk);
+    if (typeof key === 'function') {
+      return this.#continueDecodeMapAsObjectKey.bind(
+        this,
+        object,
+        remaining,
+        key,
+        typeMapping
+      );
+    }
+
+    if (this.#cursor >= chunk.length) {
+      return this.#continueDecodeMapAsObjectValue.bind(
+        this,
+        object,
+        remaining,
+        key,
+        this.#decodeNestedType.bind(this, typeMapping),
+        typeMapping
+      );
+    }
+
+    const value = this.#decodeNestedType(typeMapping, chunk);
+    if (typeof value === 'function') {
+      return this.#continueDecodeMapAsObjectValue.bind(
+        this,
+        object,
+        remaining,
+        key,
+        value,
+        typeMapping
+      );
+    }
+
+    object[key] = value;
+
+    return this.#decodeMapAsObject(object, remaining - 1, typeMapping, chunk);
+  }
+
+  #continueDecodeMapAsObjectValue(object, remaining, key, valueCb, typeMapping, chunk) {
+    const value = valueCb(chunk);
+    if (typeof value === 'function') {
+      return this.#continueDecodeMapAsObjectValue.bind(
+        this,
+        object,
+        remaining,
+        key,
+        value,
+        typeMapping
+      );
+    }
+
+    object[key] = value;
+
+    return this.#decodeMapAsObject(object, remaining - 1, typeMapping, chunk);
+  }
+}
diff --git a/packages/client/lib/RESP/encoder.spec.ts b/packages/client/lib/RESP/encoder.spec.ts
new file mode 100644
index 00000000000..2cbdc7d0b24
--- /dev/null
+++ b/packages/client/lib/RESP/encoder.spec.ts
@@ -0,0 +1,33 @@
+import { strict as assert } from 'node:assert';
+import { describe } from 'mocha';
+import encodeCommand from './encoder';
+
+describe('RESP Encoder', () => {
+  it('1 byte', () => {
+    assert.deepEqual(
+      encodeCommand(['a', 'z']),
+      ['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
+    );
+  });
+
+  it('2 bytes', () => {
+    assert.deepEqual(
+      encodeCommand(['א', 'ת']),
+      ['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
+    );
+  });
+
+  it('4 bytes', () => {
+    assert.deepEqual(
+      [...encodeCommand(['🐣', '🐤'])],
+      ['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
+    );
+  });
+
+  it('buffer', () => {
+    assert.deepEqual(
+      encodeCommand([Buffer.from('string')]),
+      ['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
+    );
+  });
+});
diff --git a/packages/client/lib/RESP/encoder.ts b/packages/client/lib/RESP/encoder.ts
new file mode 100644
index 00000000000..995650627f1
--- /dev/null
+++ b/packages/client/lib/RESP/encoder.ts
@@ -0,0 +1,28 @@
+import { RedisArgument } from './types';
+
+const CRLF = '\r\n';
+
+export default function encodeCommand(args: ReadonlyArray<RedisArgument>): ReadonlyArray<RedisArgument> {
+  const toWrite: Array<RedisArgument> = [];
+
+  let strings = '*' + args.length + CRLF;
+
+  for (let i = 0; i < args.length; i++) {
+    const arg = args[i];
+    if (typeof arg === 'string') {
+      strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF;
+    } else if (arg instanceof Buffer) {
+      toWrite.push(
+        strings + '$' + arg.length.toString() + CRLF,
+        arg
+      );
+      strings = CRLF;
+    } else {
+      throw new TypeError(`"arguments[${i}]" must be of type "string | Buffer", got ${typeof arg} instead.`);
+    }
+  }
+
+  toWrite.push(strings);
+
+  return toWrite;
+}
diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts
new file mode 100644
index 00000000000..8749bbdc7b0
--- /dev/null
+++ b/packages/client/lib/RESP/types.ts
@@ -0,0 +1,407 @@
+import { CommandParser } from '../client/parser';
+import { Tail } from '../commands/generic-transformers';
+import { BlobError, SimpleError } from '../errors';
+import { RedisScriptConfig, SHA1 } from '../lua-script';
+import { RESP_TYPES } from './decoder';
+import { VerbatimString } from './verbatim-string';
+
+export type RESP_TYPES = typeof RESP_TYPES;
+
+export type RespTypes = RESP_TYPES[keyof RESP_TYPES];
+
+// using interface(s) to allow circular references
+// type X = BlobStringReply | ArrayReply<X>;
+
+export interface RespType<
+  RESP_TYPE extends RespTypes,
+  DEFAULT,
+  TYPES = never,
+  TYPE_MAPPING = DEFAULT | TYPES
+> {
+  RESP_TYPE: RESP_TYPE;
+  DEFAULT: DEFAULT;
+  TYPES: TYPES;
+  TYPE_MAPPING: MappedType<TYPE_MAPPING>;
+}
+
+export interface NullReply extends RespType<
+  RESP_TYPES['NULL'],
+  null
+> {}
+
+export interface BooleanReply<
+  T extends boolean = boolean
+> extends RespType<
+  RESP_TYPES['BOOLEAN'],
+  T
+> {}
+
+export interface NumberReply<
+  T extends number = number
+> extends RespType<
+  RESP_TYPES['NUMBER'],
+  T,
+  `${T}`,
+  number | string
+> {}
+
+export interface BigNumberReply<
+  T extends bigint = bigint
+> extends RespType<
+  RESP_TYPES['BIG_NUMBER'],
+  T,
+  number | `${T}`,
+  bigint | number | string
+> {}
+
+export interface DoubleReply<
+  T extends number = number
+> extends RespType<
+  RESP_TYPES['DOUBLE'],
+  T,
+  `${T}`,
+  number | string
+> {}
+
+export interface SimpleStringReply<
+  T extends string = string
+> extends RespType<
+  RESP_TYPES['SIMPLE_STRING'],
+  T,
+  Buffer,
+  string | Buffer
+> {}
+
+export interface BlobStringReply<
+  T extends string = string
+> extends RespType<
+  RESP_TYPES['BLOB_STRING'],
+  T,
+  Buffer,
+  string | Buffer
+> {
+  toString(): string
+}
+
+export interface VerbatimStringReply<
+  T extends string = string
+> extends RespType<
+  RESP_TYPES['VERBATIM_STRING'],
+  T,
+  Buffer | VerbatimString,
+  string | Buffer | VerbatimString
+> {}
+
+export interface SimpleErrorReply extends RespType<
+  RESP_TYPES['SIMPLE_ERROR'],
+  SimpleError,
+  Buffer
+> {}
+
+export interface BlobErrorReply extends RespType<
+  RESP_TYPES['BLOB_ERROR'],
+  BlobError,
+  Buffer
+> {}
+
+export interface ArrayReply<T> extends RespType<
+  RESP_TYPES['ARRAY'],
+  Array<T>,
+  never,
+  Array<any>
+> {}
+
+export interface TuplesReply<T extends [...Array<unknown>]> extends RespType<
+  RESP_TYPES['ARRAY'],
+  T,
+  never,
+  Array<any>
+> {}
+
+export interface SetReply<T> extends RespType<
+  RESP_TYPES['SET'],
+  Array<T>,
+  Set<T>,
+  Array<any> | Set<any>
+> {}
+
+export interface MapReply<K, V> extends RespType<
+  RESP_TYPES['MAP'],
+  { [key: string]: V },
+  Map<K, V> | Array<K | V>,
+  Map<any, any> | Array<any>
+> {}
+
+type MapKeyValue = [key: BlobStringReply | SimpleStringReply, value: unknown];
+
+type MapTuples = Array<MapKeyValue>;
+
+type ExtractMapKey<T> = (
+    T extends BlobStringReply<infer S> ? S :
+    T extends SimpleStringReply<infer S> ? S :
+    never
+);
+
+export interface TuplesToMapReply<T extends MapTuples> extends RespType<
+  RESP_TYPES['MAP'],
+  {
+    [P in T[number] as ExtractMapKey<P[0]>]: P[1];
+  },
+  Map<ExtractMapKey<T[number][0]>, T[number][1]> | FlattenTuples<T>
+> {}
+
+type FlattenTuples<T> = (
+  T extends [] ? [] :
+  T extends [MapKeyValue] ? T[0] :
+  T extends [MapKeyValue, ...infer R] ? [
+    ...T[0],
+    ...FlattenTuples<R>
+  ] :
+  never
+);
+
+export type ReplyUnion = (
+  NullReply |
+  BooleanReply |
+  NumberReply |
+  BigNumberReply |
+  DoubleReply |
+  SimpleStringReply |
+  BlobStringReply |
+  VerbatimStringReply |
+  SimpleErrorReply |
+  BlobErrorReply |
+  ArrayReply<ReplyUnion> |
+  SetReply<ReplyUnion> |
+  MapReply<ReplyUnion, ReplyUnion>
+);
+
+export type MappedType<T> = ((...args: any) => T) | (new (...args: any) => T);
+
+type InferTypeMapping<T> = T extends RespType<RespTypes, unknown, unknown, infer FLAG_TYPES> ? FLAG_TYPES : never;
+
+export type TypeMapping = {
+  [P in RespTypes]?: MappedType<InferTypeMapping<Extract<ReplyUnion, RespType<P, any, any, any>>>>;
+};
+
+type MapKey<
+  T,
+  TYPE_MAPPING extends TypeMapping
+> = ReplyWithTypeMapping<T, TYPE_MAPPING & {
+  // simple and blob strings as map keys decoded as strings
+  [RESP_TYPES.SIMPLE_STRING]: StringConstructor;
+  [RESP_TYPES.BLOB_STRING]: StringConstructor;
+}>;
+
+export type UnwrapReply<REPLY extends RespType<any, any, any, any>> = REPLY['DEFAULT' | 'TYPES'];
+
+export type ReplyWithTypeMapping<
+  REPLY,
+  TYPE_MAPPING extends TypeMapping
+> = (
+  // if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type
+  REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ? 
+    TYPE_MAPPING[RESP_TYPE] extends MappedType<infer T> ?
+      ReplyWithTypeMapping<Extract<DEFAULT | TYPES, T>, TYPE_MAPPING> :
+      ReplyWithTypeMapping<DEFAULT, TYPE_MAPPING>
+  : (
+    // if REPLY is a known generic type, convert its generic arguments
+    // TODO: tuples?
+    REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
+    REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
+    REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
+    // `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
+    REPLY extends Date | Buffer | Error ? REPLY :
+    REPLY extends Record<PropertyKey, any> ? {
+      [P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
+    } :
+    // otherwise, just return the REPLY as is
+    REPLY
+  )
+);
+
+export type TransformReply = (this: void, reply: any, preserve?: any, typeMapping?: TypeMapping) => any; // TODO;
+
+export type RedisArgument = string | Buffer;
+
+export type CommandArguments = Array<RedisArgument> & { preserve?: unknown };
+
+// export const REQUEST_POLICIES = {
+//   /**
+//    * TODO
+//    */
+//   ALL_NODES: 'all_nodes',
+//   /**
+//    * TODO
+//    */
+//   ALL_SHARDS: 'all_shards',
+//   /**
+//    * TODO
+//    */
+//   SPECIAL: 'special'
+// } as const;
+
+// export type REQUEST_POLICIES = typeof REQUEST_POLICIES;
+
+// export type RequestPolicies = REQUEST_POLICIES[keyof REQUEST_POLICIES];
+
+// export const RESPONSE_POLICIES = {
+//   /**
+//    * TODO
+//    */
+//   ONE_SUCCEEDED: 'one_succeeded',
+//   /**
+//    * TODO
+//    */
+//   ALL_SUCCEEDED: 'all_succeeded',
+//   /**
+//    * TODO
+//    */
+//   LOGICAL_AND: 'agg_logical_and',
+//   /**
+//    * TODO
+//    */
+//   SPECIAL: 'special'
+// } as const;
+
+// export type RESPONSE_POLICIES = typeof RESPONSE_POLICIES;
+
+// export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES];
+
+// export type CommandPolicies = {
+//   request?: RequestPolicies | null;
+//   response?: ResponsePolicies | null;
+// };
+
+export type Command = {
+  CACHEABLE?: boolean;
+  IS_READ_ONLY?: boolean;
+  /**
+   * @internal
+   * TODO: remove once `POLICIES` is implemented
+   */
+  IS_FORWARD_COMMAND?: boolean;
+  NOT_KEYED_COMMAND?: true;
+  // POLICIES?: CommandPolicies;
+  parseCommand(this: void, parser: CommandParser, ...args: Array<any>): void;
+  TRANSFORM_LEGACY_REPLY?: boolean;
+  transformReply: TransformReply | Record<RespVersions, TransformReply>;
+  unstableResp3?: boolean;
+};
+
+export type RedisCommands = Record<string, Command>;
+
+export type RedisModules = Record<string, RedisCommands>;
+
+export interface RedisFunction extends Command {
+  NUMBER_OF_KEYS?: number;
+}
+
+export type RedisFunctions = Record<string, Record<string, RedisFunction>>;
+
+export type RedisScript = RedisScriptConfig & SHA1;
+
+export type RedisScripts = Record<string, RedisScript>;
+
+// TODO: move to Commander?
+export interface CommanderConfig<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions
+> {
+  modules?: M;
+  functions?: F;
+  scripts?: S;
+  /**
+   * Specifies the Redis Serialization Protocol version to use.
+   * RESP2 is the default (value 2), while RESP3 (value 3) provides
+   * additional data types and features introduced in Redis 6.0.
+   */
+  RESP?: RESP;
+  /**
+   * When set to true, enables commands that have unstable RESP3 implementations.
+   * When using RESP3 protocol, commands marked as having unstable RESP3 support
+   * will throw an error unless this flag is explicitly set to true.
+   * This primarily affects modules like Redis Search where response formats
+   * in RESP3 mode may change in future versions.
+   */
+  unstableResp3?: boolean;
+}
+
+type Resp2Array<T> = (
+  T extends [] ? [] :
+  T extends [infer ITEM] ? [Resp2Reply<ITEM>] :
+  T extends [infer ITEM, ...infer REST] ? [
+    Resp2Reply<ITEM>,
+    ...Resp2Array<REST>
+  ] :
+  T extends Array<infer ITEM> ? Array<Resp2Reply<ITEM>> :
+  never
+);
+
+export type Resp2Reply<RESP3REPLY> = (
+  RESP3REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
+    // TODO: RESP3 only scalar types
+    RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
+    RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
+      RESP_TYPE,
+      Resp2Array<DEFAULT>
+    > :
+    RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
+      RESP_TYPES['ARRAY'],
+      Resp2Array<Extract<TYPES, Array<any>>>
+    > :
+    RESP3REPLY :
+  RESP3REPLY
+);
+
+export type RespVersions = 2 | 3;
+
+export type CommandReply<
+  COMMAND extends Command,
+  RESP extends RespVersions
+> = (
+  // if transformReply is a function, use its return type
+  COMMAND['transformReply'] extends (...args: any) => infer T ? T :
+  // if transformReply[RESP] is a function, use its return type
+  COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
+  // otherwise use the generic reply type
+  ReplyUnion
+);
+
+export type CommandSignature<
+  COMMAND extends Command,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (...args: Tail<Parameters<COMMAND['parseCommand']>>) => Promise<ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>>;
+
+// export type CommandWithPoliciesSignature<
+//   COMMAND extends Command,
+//   RESP extends RespVersions,
+//   TYPE_MAPPING extends TypeMapping,
+//   POLICIES extends CommandPolicies
+// > = (...args: Parameters<COMMAND['transformArguments']>) => Promise<
+//   ReplyWithPolicy<
+//     ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>,
+//     MergePolicies<COMMAND, POLICIES>
+//   >
+// >;
+
+// export type MergePolicies<
+//   COMMAND extends Command,
+//   POLICIES extends CommandPolicies
+// > = Omit<COMMAND['POLICIES'], keyof POLICIES> & POLICIES;
+
+// type ReplyWithPolicy<
+//   REPLY,
+//   POLICIES extends CommandPolicies,
+// > = (
+//   POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never :
+//   POLICIES['request'] extends null | undefined ? REPLY :
+//   unknown extends POLICIES['request'] ? REPLY :
+//   POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never :
+//   POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY :
+//   // otherwise, return array of replies
+//   Array<REPLY>
+// );
diff --git a/packages/client/lib/RESP/verbatim-string.ts b/packages/client/lib/RESP/verbatim-string.ts
new file mode 100644
index 00000000000..92ff4fe3fb1
--- /dev/null
+++ b/packages/client/lib/RESP/verbatim-string.ts
@@ -0,0 +1,8 @@
+export class VerbatimString extends String {
+  constructor(
+    public format: string,
+    value: string
+  ) {
+    super(value);
+  }
+}
diff --git a/packages/client/lib/authx/credentials-provider.ts b/packages/client/lib/authx/credentials-provider.ts
new file mode 100644
index 00000000000..667795be9b3
--- /dev/null
+++ b/packages/client/lib/authx/credentials-provider.ts
@@ -0,0 +1,102 @@
+import { Disposable } from './disposable';
+/**
+ * Provides credentials asynchronously.
+ */
+export interface AsyncCredentialsProvider {
+  readonly type: 'async-credentials-provider';
+  credentials: () => Promise<BasicAuth>
+}
+
+/**
+ * Provides credentials asynchronously with support for continuous updates via a subscription model.
+ * This is useful for environments where credentials are frequently rotated or updated or can be revoked.
+ */
+export interface StreamingCredentialsProvider {
+  readonly type: 'streaming-credentials-provider';
+
+  /**
+   * Provides initial credentials and subscribes to subsequent updates. This is used internally by the node-redis client
+   * to handle credential rotation and re-authentication.
+   *
+   * Note: The node-redis client manages the subscription lifecycle automatically. Users only need to implement
+   * onReAuthenticationError if they want to be notified about authentication failures.
+   *
+   * Error handling:
+   * - Errors received via onError indicate a fatal issue with the credentials stream
+   * - The stream is automatically closed(disposed) when onError occurs
+   * - onError typically mean the provider failed to fetch new credentials after retrying
+   *
+   * @example
+   * ```ts
+   * const provider = getStreamingProvider();
+   * const [initialCredentials, disposable] = await provider.subscribe({
+   *   onNext: (newCredentials) => {
+   *     // Handle credential update
+   *   },
+   *   onError: (error) => {
+   *     // Handle fatal stream error
+   *   }
+   * });
+   *
+   * @param listener - Callbacks to handle credential updates and errors
+   * @returns A Promise resolving to [initial credentials, cleanup function]
+   */
+  subscribe: (listener: StreamingCredentialsListener<BasicAuth>) => Promise<[BasicAuth, Disposable]>
+
+  /**
+   * Called when authentication fails or credentials cannot be renewed in time.
+   * Implement this to handle authentication errors in your application.
+   *
+   * @param error - Either a CredentialsError (invalid/expired credentials) or
+   *                UnableToObtainNewCredentialsError (failed to fetch new credentials on time)
+   */
+  onReAuthenticationError: (error: ReAuthenticationError) => void;
+
+}
+
+/**
+ * Type representing basic authentication credentials.
+ */
+export type BasicAuth = { username?: string, password?: string }
+
+/**
+ * Callback to handle credential updates and errors.
+ */
+export type StreamingCredentialsListener<T> = {
+  onNext: (credentials: T) => void;
+  onError: (e: Error) => void;
+}
+
+
+/**
+ * Providers that can supply authentication credentials
+ */
+export type CredentialsProvider = AsyncCredentialsProvider | StreamingCredentialsProvider
+
+/**
+ * Errors that can occur during re-authentication.
+ */
+export type ReAuthenticationError = CredentialsError | UnableToObtainNewCredentialsError
+
+/**
+ * Thrown when re-authentication fails with provided credentials .
+ * e.g. when the credentials are invalid, expired or revoked.
+ *
+ */
+export class CredentialsError extends Error {
+  constructor(message: string) {
+    super(`Re-authentication with latest credentials failed: ${message}`);
+    this.name = 'CredentialsError';
+  }
+
+}
+
+/**
+ * Thrown when new credentials cannot be obtained before current ones expire
+ */
+export class UnableToObtainNewCredentialsError extends Error {
+  constructor(message: string) {
+    super(`Unable to obtain new credentials : ${message}`);
+    this.name = 'UnableToObtainNewCredentialsError';
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/disposable.ts b/packages/client/lib/authx/disposable.ts
new file mode 100644
index 00000000000..ee4526a37bd
--- /dev/null
+++ b/packages/client/lib/authx/disposable.ts
@@ -0,0 +1,6 @@
+/**
+ * Represents a resource that can be disposed.
+ */
+export interface Disposable {
+  dispose(): void;
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/identity-provider.ts b/packages/client/lib/authx/identity-provider.ts
new file mode 100644
index 00000000000..a2d25c8f9db
--- /dev/null
+++ b/packages/client/lib/authx/identity-provider.ts
@@ -0,0 +1,22 @@
+/**
+ * An identity provider is responsible for providing a token that can be used to authenticate with a service.
+ */
+
+/**
+ * The response from an identity provider when requesting a token.
+ *
+ * note: "native" refers to the type of the token that the actual identity provider library is using.
+ *
+ * @type T The type of the native idp token.
+ * @property token The token.
+ * @property ttlMs The time-to-live of the token in epoch milliseconds extracted from the native token in local time.
+ */
+export type TokenResponse<T> = { token: T, ttlMs: number };
+
+export interface IdentityProvider<T> {
+  /**
+   * Request a token from the identity provider.
+   * @returns A promise that resolves to an object containing the token and the time-to-live in epoch milliseconds.
+   */
+  requestToken(): Promise<TokenResponse<T>>;
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/index.ts b/packages/client/lib/authx/index.ts
new file mode 100644
index 00000000000..ce611e1497f
--- /dev/null
+++ b/packages/client/lib/authx/index.ts
@@ -0,0 +1,15 @@
+export { TokenManager, TokenManagerConfig, TokenStreamListener, RetryPolicy, IDPError } from './token-manager';
+export {
+  CredentialsProvider,
+  StreamingCredentialsProvider,
+  UnableToObtainNewCredentialsError,
+  CredentialsError,
+  StreamingCredentialsListener,
+  AsyncCredentialsProvider,
+  ReAuthenticationError,
+  BasicAuth
+} from './credentials-provider';
+export { Token } from './token';
+export { IdentityProvider, TokenResponse } from './identity-provider';
+
+export { Disposable } from './disposable'
\ No newline at end of file
diff --git a/packages/client/lib/authx/token-manager.spec.ts b/packages/client/lib/authx/token-manager.spec.ts
new file mode 100644
index 00000000000..1cc2a207edc
--- /dev/null
+++ b/packages/client/lib/authx/token-manager.spec.ts
@@ -0,0 +1,588 @@
+import { strict as assert } from 'node:assert';
+import { Token } from './token';
+import { IDPError, RetryPolicy, TokenManager, TokenManagerConfig, TokenStreamListener } from './token-manager';
+import { IdentityProvider, TokenResponse } from './identity-provider';
+import { setTimeout } from 'timers/promises';
+
+describe('TokenManager', () => {
+
+  /**
+   * Helper function to delay execution for a given number of milliseconds.
+   * @param ms
+   */
+  const delay = (ms: number) => {
+    return setTimeout(ms);
+  }
+
+  /**
+   * IdentityProvider that returns a fixed test token for testing and doesn't handle TTL.
+   */
+  class TestIdentityProvider implements IdentityProvider<string> {
+    requestToken(): Promise<TokenResponse<string>> {
+      return Promise.resolve({ token: 'test-token 1', ttlMs: 1000 });
+    }
+  }
+
+  /**
+   * Helper function to create a test token with a given TTL .
+   * @param ttlMs Time-to-live in milliseconds
+   */
+  const createToken = (ttlMs: number): Token<string> => {
+    return new Token('test-token', ttlMs, 0);
+  };
+
+  /**
+   * Listener that records received tokens and errors for testing.
+   */
+  class TestListener implements TokenStreamListener<string> {
+
+    public readonly receivedTokens: Token<string>[] = [];
+    public readonly errors: IDPError[] = [];
+
+    onNext(token: Token<string>): void {
+      this.receivedTokens.push(token);
+    }
+
+    onError(error: IDPError): void {
+      this.errors.push(error);
+    }
+  }
+
+  /**
+   * IdentityProvider that returns a sequence of tokens with a fixed delay simulating network latency.
+   * Used for testing token refresh scenarios.
+   */
+  class ControlledIdentityProvider implements IdentityProvider<string> {
+    private tokenIndex = 0;
+    private readonly delayMs: number;
+    private readonly ttlMs: number;
+
+    constructor(
+      private readonly tokens: string[],
+      delayMs: number = 0,
+      tokenTTlMs: number = 100
+    ) {
+      this.delayMs = delayMs;
+      this.ttlMs = tokenTTlMs;
+    }
+
+    async requestToken(): Promise<TokenResponse<string>> {
+
+      if (this.tokenIndex >= this.tokens.length) {
+        throw new Error('No more test tokens available');
+      }
+
+      if (this.delayMs > 0) {
+        await setTimeout(this.delayMs);
+      }
+
+      return { token: this.tokens[this.tokenIndex++], ttlMs: this.ttlMs };
+    }
+
+  }
+
+  /**
+   * IdentityProvider that simulates various error scenarios with configurable behavior
+   */
+  class ErrorSimulatingProvider implements IdentityProvider<string> {
+    private requestCount = 0;
+
+    constructor(
+      private readonly errorSequence: Array<Error | string>,
+      private readonly delayMs: number = 0,
+      private readonly ttlMs: number = 100
+    ) {}
+
+    async requestToken(): Promise<TokenResponse<string>> {
+
+      if (this.delayMs > 0) {
+        await delay(this.delayMs);
+      }
+
+      const result = this.errorSequence[this.requestCount];
+      this.requestCount++;
+
+      if (result instanceof Error) {
+        throw result;
+      } else if (typeof result === 'string') {
+        return { token: result, ttlMs: this.ttlMs };
+      } else {
+        throw new Error('No more responses configured');
+      }
+    }
+
+    getRequestCount(): number {
+      return this.requestCount;
+    }
+  }
+
+  describe('constructor validation', () => {
+    it('should throw error if ratio is greater than 1', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 1.1
+      };
+
+      assert.throws(
+        () => new TokenManager(new TestIdentityProvider(), config),
+        /expirationRefreshRatio must be less than or equal to 1/
+      );
+    });
+
+    it('should throw error if ratio is negative', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: -0.1
+      };
+
+      assert.throws(
+        () => new TokenManager(new TestIdentityProvider(), config),
+        /expirationRefreshRatio must be greater or equal to 0/
+      );
+    });
+
+    it('should accept ratio of 1', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 1
+      };
+
+      assert.doesNotThrow(
+        () => new TokenManager(new TestIdentityProvider(), config)
+      );
+    });
+
+    it('should accept ratio of 0', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0
+      };
+
+      assert.doesNotThrow(
+        () => new TokenManager(new TestIdentityProvider(), config)
+      );
+    });
+  });
+
+  describe('calculateRefreshTime', () => {
+    it('should calculate correct refresh time with 0.8 ratio', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(1000);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      // With 1000s TTL and 0.8 ratio, should refresh at 800s
+      assert.equal(refreshTime, 800);
+    });
+
+    it('should return 0 for ratio of 0', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(1000);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      assert.equal(refreshTime, 0);
+    });
+
+    it('should refresh at expiration time with ratio of 1', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 1
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(1000);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      assert.equal(refreshTime, 1000);
+    });
+
+    it('should handle short TTL tokens', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(5);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      assert.equal(refreshTime, 4);
+    });
+
+    it('should handle expired tokens', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      // Create token that expired 100s ago
+      const token = createToken(-100);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      // Should return refresh time of 0 for expired tokens
+      assert.equal(refreshTime, 0);
+    });
+    describe('token refresh scenarios', () => {
+
+      describe('token refresh', () => {
+        it('should handle token refresh', async () => {
+          const networkDelay = 20;
+          const tokenTtl = 100;
+
+          const config: TokenManagerConfig = {
+            expirationRefreshRatio: 0.8
+          };
+
+          const identityProvider = new ControlledIdentityProvider(['token1', 'token2', 'token3'], networkDelay, tokenTtl);
+          const manager = new TokenManager(identityProvider, config);
+          const listener = new TestListener();
+          const disposable = manager.start(listener);
+
+          assert.equal(manager.getCurrentToken(), null, 'Should not have token yet');
+          // Wait for the first token request to complete ( it should be immediate, and we should wait only for the network delay)
+          await delay(networkDelay)
+
+          assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+          assert.equal(listener.receivedTokens[0].value, 'token1', 'Should have correct token value');
+          assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs,
+            tokenTtl, 'Should have correct TTL');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors: ' + listener.errors);
+          assert.equal(manager.getCurrentToken().value, 'token1', 'Should have current token');
+
+          await delay(80);
+
+          assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+          await delay(networkDelay);
+
+          assert.equal(listener.receivedTokens.length, 2, 'Should receive second token');
+          assert.equal(listener.receivedTokens[1].value, 'token2', 'Should have correct token value');
+          assert.equal(listener.receivedTokens[1].expiresAtMs - listener.receivedTokens[1].receivedAtMs,
+            tokenTtl, 'Should have correct TTL');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+          assert.equal(manager.getCurrentToken().value, 'token2', 'Should have current token');
+
+          await delay(80);
+
+          assert.equal(listener.receivedTokens.length, 2, 'Should not receive new token yet');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+          await delay(networkDelay);
+
+          assert.equal(listener.receivedTokens.length, 3, 'Should receive third token');
+          assert.equal(listener.receivedTokens[2].value, 'token3', 'Should have correct token value');
+          assert.equal(listener.receivedTokens[2].expiresAtMs - listener.receivedTokens[2].receivedAtMs,
+            tokenTtl, 'Should have correct TTL');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+          assert.equal(manager.getCurrentToken().value, 'token3', 'Should have current token');
+
+          disposable?.dispose();
+        });
+      });
+    });
+  });
+
+  describe('TokenManager error handling', () => {
+
+    describe('error scenarios', () => {
+      it('should not recover if retries are not enabled', async () => {
+
+        const networkDelay = 20;
+        const tokenTtl = 100;
+
+        const config: TokenManagerConfig = {
+          expirationRefreshRatio: 0.8
+        };
+
+        const identityProvider = new ErrorSimulatingProvider(
+          [
+            'token1',
+            new Error('Fatal error'),
+            'token3'
+          ],
+          networkDelay,
+          tokenTtl
+        );
+
+        const manager = new TokenManager(identityProvider, config);
+        const listener = new TestListener();
+        const disposable = manager.start(listener);
+
+        await delay(networkDelay);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+        assert.equal(listener.receivedTokens[0].value, 'token1', 'Should have correct initial token');
+        assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs,
+          tokenTtl, 'Should have correct TTL');
+        assert.equal(listener.errors.length, 0, 'Should not have errors yet');
+
+        await delay(80);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+        assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+        await delay(networkDelay);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token after failure');
+        assert.equal(listener.errors.length, 1, 'Should receive error');
+        assert.equal(listener.errors[0].message, 'Fatal error', 'Should have correct error message');
+        assert.equal(listener.errors[0].isRetryable, false, 'Should be a fatal error');
+
+        // verify that the token manager is stopped and no more requests are made after the error and expected refresh time
+        await delay(80);
+
+        assert.equal(identityProvider.getRequestCount(), 2, 'Should not make more requests after error');
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token after error');
+        assert.equal(listener.errors.length, 1, 'Should not receive more errors after error');
+        assert.equal(manager.isRunning(), false, 'Should stop token manager after error');
+
+        disposable?.dispose();
+      });
+
+      it('should handle retries with exponential backoff', async () => {
+        const networkDelay = 20;
+        const tokenTtl = 100;
+
+        const config: TokenManagerConfig = {
+          expirationRefreshRatio: 0.8,
+          retry: {
+            maxAttempts: 3,
+            initialDelayMs: 100,
+            maxDelayMs: 1000,
+            backoffMultiplier: 2,
+            isRetryable: (error: unknown) => error instanceof Error && error.message === 'Temporary failure'
+          }
+        };
+
+        const identityProvider = new ErrorSimulatingProvider(
+          [
+            'initial-token',
+            new Error('Temporary failure'),  // First attempt fails
+            new Error('Temporary failure'),  // First retry fails
+            'recovery-token'                 // Second retry succeeds
+          ],
+          networkDelay,
+          tokenTtl
+        );
+
+        const manager = new TokenManager(identityProvider, config);
+        const listener = new TestListener();
+        const disposable = manager.start(listener);
+
+        // Wait for initial token
+        await delay(networkDelay);
+        assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+        assert.equal(listener.receivedTokens[0].value, 'initial-token', 'Should have correct initial token');
+        assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs,
+          tokenTtl, 'Should have correct TTL');
+        assert.equal(listener.errors.length, 0, 'Should not have errors yet');
+
+        await delay(80);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+        assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+        await delay(networkDelay);
+
+        // Should have first error but not stop due to retry config
+        assert.equal(listener.errors.length, 1, 'Should have first error');
+        assert.ok(listener.errors[0].message.includes('attempt 1'), 'Error should indicate first attempt');
+        assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error');
+        assert.equal(manager.isRunning(), true, 'Should continue running during retries');
+
+        // Advance past first retry (delay: 100ms due to backoff)
+        await delay(100);
+
+        assert.equal(listener.errors.length, 1, 'Should not have the second error yet');
+
+        await delay(networkDelay);
+
+        assert.equal(listener.errors.length, 2, 'Should have second error');
+        assert.ok(listener.errors[1].message.includes('attempt 2'), 'Error should indicate second attempt');
+        assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error');
+        assert.equal(manager.isRunning(), true, 'Should continue running during retries');
+
+        // Advance past second retry (delay: 200ms due to backoff)
+        await delay(200);
+
+        assert.equal(listener.errors.length, 2, 'Should not have another error');
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+
+        await delay(networkDelay);
+
+        // Should have recovered
+        assert.equal(listener.receivedTokens.length, 2, 'Should receive recovery token');
+        assert.equal(listener.receivedTokens[1].value, 'recovery-token', 'Should have correct recovery token');
+        assert.equal(listener.receivedTokens[1].expiresAtMs - listener.receivedTokens[1].receivedAtMs,
+          tokenTtl, 'Should have correct TTL');
+        assert.equal(manager.isRunning(), true, 'Should continue running after recovery');
+        assert.equal(identityProvider.getRequestCount(), 4, 'Should have made exactly 4 requests');
+
+        disposable?.dispose();
+      });
+
+      it('should stop after max retries exceeded', async () => {
+        const networkDelay = 20;
+        const tokenTtl = 100;
+
+        const config: TokenManagerConfig = {
+          expirationRefreshRatio: 0.8,
+          retry: {
+            maxAttempts: 2,  // Only allow 2 retries
+            initialDelayMs: 100,
+            maxDelayMs: 1000,
+            backoffMultiplier: 2,
+            jitterPercentage: 0,
+            isRetryable: (error: unknown) => error instanceof Error && error.message === 'Temporary failure'
+          }
+        };
+
+        // All attempts must fail
+        const identityProvider = new ErrorSimulatingProvider(
+          [
+            'initial-token',
+            new Error('Temporary failure'),
+            new Error('Temporary failure'),
+            new Error('Temporary failure')
+          ],
+          networkDelay,
+          tokenTtl
+        );
+
+        const manager = new TokenManager(identityProvider, config);
+        const listener = new TestListener();
+        const disposable = manager.start(listener);
+
+        // Wait for initial token
+        await delay(networkDelay);
+        assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+
+        await delay(80);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+        assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+        //wait for the "network call" to complete
+        await delay(networkDelay);
+
+        // First error
+        assert.equal(listener.errors.length, 1, 'Should have first error');
+        assert.equal(manager.isRunning(), true, 'Should continue running after first error');
+        assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error');
+
+        // Advance past first retry
+        await delay(100);
+
+        assert.equal(listener.errors.length, 1, 'Should not have second error yet');
+
+        //wait for the "network call" to complete
+        await delay(networkDelay);
+
+        // Second error
+        assert.equal(listener.errors.length, 2, 'Should have second error');
+        assert.equal(manager.isRunning(), true, 'Should continue running after second error');
+        assert.equal(listener.errors[1].isRetryable, true, 'Should not be a fatal error');
+
+        // Advance past second retry
+        await delay(200);
+
+        assert.equal(listener.errors.length, 2, 'Should not have third error yet');
+
+        //wait for the "network call" to complete
+        await delay(networkDelay);
+
+        // Should stop after max retries
+        assert.equal(listener.errors.length, 3, 'Should have final error');
+        assert.equal(listener.errors[2].isRetryable, false, 'Should be a fatal error');
+        assert.equal(manager.isRunning(), false, 'Should stop after max retries exceeded');
+        assert.equal(identityProvider.getRequestCount(), 4, 'Should have made exactly 4 requests');
+
+        disposable?.dispose();
+
+      });
+    });
+  });
+
+  describe('TokenManager retry delay calculations', () => {
+    const createManager = (retryConfig: Partial<RetryPolicy>) => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8,
+        retry: {
+          maxAttempts: 3,
+          initialDelayMs: 100,
+          maxDelayMs: 1000,
+          backoffMultiplier: 2,
+          ...retryConfig
+        }
+      };
+      return new TokenManager(new TestIdentityProvider(), config);
+    };
+
+    describe('calculateRetryDelay', () => {
+
+      it('should apply exponential backoff', () => {
+        const manager = createManager({
+          initialDelayMs: 100,
+          backoffMultiplier: 2,
+          jitterPercentage: 0
+        });
+
+        // Test multiple retry attempts
+        const expectedDelays = [
+          [1, 100],    // First attempt: initialDelay * (2^0) = 100
+          [2, 200],    // Second attempt: initialDelay * (2^1) = 200
+          [3, 400],    // Third attempt: initialDelay * (2^2) = 400
+          [4, 800],    // Fourth attempt: initialDelay * (2^3) = 800
+          [5, 1000]    // Fifth attempt: would be 1600, but capped at maxDelay (1000)
+        ];
+
+        for (const [attempt, expectedDelay] of expectedDelays) {
+          manager['retryAttempt'] = attempt;
+          assert.equal(
+            manager.calculateRetryDelay(),
+            expectedDelay,
+            `Incorrect delay for attempt ${attempt}`
+          );
+        }
+      });
+
+      it('should respect maxDelayMs', () => {
+        const manager = createManager({
+          initialDelayMs: 100,
+          maxDelayMs: 300,
+          backoffMultiplier: 2,
+          jitterPercentage: 0
+        });
+
+        // Test that delays are capped at maxDelayMs
+        const expectedDelays = [
+          [1, 100],    // First attempt: 100
+          [2, 200],    // Second attempt: 200
+          [3, 300],    // Third attempt: would be 400, capped at 300
+          [4, 300],    // Fourth attempt: would be 800, capped at 300
+          [5, 300]     // Fifth attempt: would be 1600, capped at 300
+        ];
+
+        for (const [attempt, expectedDelay] of expectedDelays) {
+          manager['retryAttempt'] = attempt;
+          assert.equal(
+            manager.calculateRetryDelay(),
+            expectedDelay,
+            `Incorrect delay for attempt ${attempt}`
+          );
+        }
+      });
+
+      it('should return 0 when no retry config is present', () => {
+        const manager = new TokenManager(new TestIdentityProvider(), {
+          expirationRefreshRatio: 0.8
+        });
+        manager['retryAttempt'] = 1;
+        assert.equal(manager.calculateRetryDelay(), 0);
+      });
+    });
+  });
+});
+
diff --git a/packages/client/lib/authx/token-manager.ts b/packages/client/lib/authx/token-manager.ts
new file mode 100644
index 00000000000..6532d88317b
--- /dev/null
+++ b/packages/client/lib/authx/token-manager.ts
@@ -0,0 +1,318 @@
+import { IdentityProvider, TokenResponse } from './identity-provider';
+import { Token } from './token';
+import {Disposable} from './disposable';
+
+/**
+ * The configuration for retrying token refreshes.
+ */
+export interface RetryPolicy {
+  /**
+   * The maximum number of attempts to retry token refreshes.
+   */
+  maxAttempts: number;
+
+  /**
+   * The initial delay in milliseconds before the first retry.
+   */
+  initialDelayMs: number;
+
+  /**
+   * The maximum delay in milliseconds between retries.
+   * The calculated delay will be capped at this value.
+   */
+  maxDelayMs: number;
+
+  /**
+   * The multiplier for exponential backoff between retries.
+   * @example
+   * A value of 2 will double the delay each time:
+   * - 1st retry: initialDelayMs
+   * - 2nd retry: initialDelayMs * 2
+   * - 3rd retry: initialDelayMs * 4
+   */
+  backoffMultiplier: number;
+
+  /**
+   * The percentage of jitter to apply to the delay.
+   * @example
+   * A value of 0.1 will add or subtract up to 10% of the delay.
+   */
+  jitterPercentage?: number;
+
+  /**
+   * Function to classify errors from the identity provider as retryable or non-retryable.
+   * Used to determine if a token refresh failure should be retried based on the type of error.
+   *
+   * The default behavior is to retry all types of errors if no function is provided.
+   *
+   * Common use cases:
+   * - Network errors that may be transient (should retry)
+   * - Invalid credentials (should not retry)
+   * - Rate limiting responses (should retry)
+   *
+   * @param error - The error from the identity provider3
+   * @param attempt - Current retry attempt (0-based)
+   * @returns `true` if the error is considered transient and the operation should be retried
+   *
+   * @example
+   * ```typescript
+   * const retryPolicy: RetryPolicy = {
+   *   maxAttempts: 3,
+   *   initialDelayMs: 1000,
+   *   maxDelayMs: 5000,
+   *   backoffMultiplier: 2,
+   *   isRetryable: (error) => {
+   *     // Retry on network errors or rate limiting
+   *     return error instanceof NetworkError ||
+   *            error instanceof RateLimitError;
+   *   }
+   * };
+   * ```
+   */
+  isRetryable?: (error: unknown, attempt: number) => boolean;
+}
+
+/**
+ * the configuration for the TokenManager.
+ */
+export interface TokenManagerConfig {
+
+  /**
+   * Represents the ratio of a token's lifetime at which a refresh should be triggered.
+   * For example, a value of 0.75 means the token should be refreshed when 75% of its lifetime has elapsed (or when
+   * 25% of its lifetime remains).
+   */
+  expirationRefreshRatio: number;
+
+  // The retry policy for token refreshes. If not provided, no retries will be attempted.
+  retry?: RetryPolicy;
+}
+
+/**
+ * IDPError indicates a failure from the identity provider.
+ *
+ * The `isRetryable` flag is determined by the RetryPolicy's error classification function - if an error is
+ * classified as retryable, it will be marked as transient and the token manager will attempt to recover.
+ */
+export class IDPError extends Error {
+  constructor(public readonly message: string, public readonly isRetryable: boolean) {
+    super(message);
+    this.name = 'IDPError';
+  }
+}
+
+/**
+ * TokenStreamListener is an interface for objects that listen to token changes.
+ */
+export type TokenStreamListener<T> = {
+  /**
+   * Called each time a new token is received.
+   * @param token
+   */
+  onNext: (token: Token<T>) => void;
+
+  /**
+   * Called when an error occurs while calling the underlying IdentityProvider. The error can be
+   * transient and the token manager will attempt to obtain a token again if retry policy is configured.
+   *
+   * Only fatal errors will terminate the stream and stop the token manager.
+   *
+   * @param error
+   */
+  onError: (error: IDPError) => void;
+
+}
+
+/**
+ * TokenManager is responsible for obtaining/refreshing tokens and notifying listeners about token changes.
+ * It uses an IdentityProvider to request tokens. The token refresh is scheduled based on the token's TTL and
+ * the expirationRefreshRatio configuration.
+ *
+ * The TokenManager should be disposed when it is no longer needed by calling the dispose method on the Disposable
+ * returned by start.
+ */
+export class TokenManager<T> {
+  private currentToken: Token<T> | null = null;
+  private refreshTimeout: NodeJS.Timeout | null = null;
+  private listener: TokenStreamListener<T> | null = null;
+  private retryAttempt: number = 0;
+
+  constructor(
+    private readonly identityProvider: IdentityProvider<T>,
+    private readonly config: TokenManagerConfig
+  ) {
+    if (this.config.expirationRefreshRatio > 1) {
+      throw new Error('expirationRefreshRatio must be less than or equal to 1');
+    }
+    if (this.config.expirationRefreshRatio < 0) {
+      throw new Error('expirationRefreshRatio must be greater or equal to 0');
+    }
+  }
+
+  /**
+   * Starts the token manager and returns a Disposable that can be used to stop the token manager.
+   *
+   * @param listener The listener that will receive token updates.
+   * @param initialDelayMs The initial delay in milliseconds before the first token refresh.
+   */
+  public start(listener: TokenStreamListener<T>, initialDelayMs: number = 0): Disposable {
+    if (this.listener) {
+      this.stop();
+    }
+
+    this.listener = listener;
+    this.retryAttempt = 0;
+
+    this.scheduleNextRefresh(initialDelayMs);
+
+    return {
+      dispose: () => this.stop()
+    };
+  }
+
+  public calculateRetryDelay(): number {
+    if (!this.config.retry) return 0;
+
+    const { initialDelayMs, maxDelayMs, backoffMultiplier, jitterPercentage } = this.config.retry;
+
+    let delay = initialDelayMs * Math.pow(backoffMultiplier, this.retryAttempt - 1);
+
+    delay = Math.min(delay, maxDelayMs);
+
+    if (jitterPercentage) {
+      const jitterRange = delay * (jitterPercentage / 100);
+      const jitterAmount = Math.random() * jitterRange - (jitterRange / 2);
+      delay += jitterAmount;
+    }
+
+    let result = Math.max(0, Math.floor(delay));
+
+    return result;
+  }
+
+  private shouldRetry(error: unknown): boolean {
+    if (!this.config.retry) return false;
+
+    const { maxAttempts, isRetryable } = this.config.retry;
+
+    if (this.retryAttempt >= maxAttempts) {
+      return false;
+    }
+
+    if (isRetryable) {
+      return isRetryable(error, this.retryAttempt);
+    }
+
+    return false;
+  }
+
+  public isRunning(): boolean {
+    return this.listener !== null;
+  }
+
+  private async refresh(): Promise<void> {
+    if (!this.listener) {
+      throw new Error('TokenManager is not running, but refresh was called');
+    }
+
+    try {
+      await this.identityProvider.requestToken().then(this.handleNewToken);
+      this.retryAttempt = 0;
+    } catch (error) {
+
+      if (this.shouldRetry(error)) {
+        this.retryAttempt++;
+        const retryDelay = this.calculateRetryDelay();
+        this.notifyError(`Token refresh failed (attempt ${this.retryAttempt}), retrying in ${retryDelay}ms: ${error}`, true)
+        this.scheduleNextRefresh(retryDelay);
+      } else {
+        this.notifyError(error, false);
+        this.stop();
+      }
+    }
+  }
+
+  private handleNewToken = async ({ token: nativeToken, ttlMs }: TokenResponse<T>): Promise<void> => {
+    if (!this.listener) {
+      throw new Error('TokenManager is not running, but a new token was received');
+    }
+    const token = this.wrapAndSetCurrentToken(nativeToken, ttlMs);
+    this.listener.onNext(token);
+
+    this.scheduleNextRefresh(this.calculateRefreshTime(token));
+  }
+
+  /**
+   * Creates a Token object from a native token and sets it as the current token.
+   *
+   * @param nativeToken - The raw token received from the identity provider
+   * @param ttlMs - Time-to-live in milliseconds for the token
+   *
+   * @returns A new Token instance containing the wrapped native token and expiration details
+   *
+   */
+  public wrapAndSetCurrentToken(nativeToken: T, ttlMs: number): Token<T> {
+    const now = Date.now();
+    const token = new Token(
+      nativeToken,
+      now + ttlMs,
+      now
+    );
+    this.currentToken = token;
+    return token;
+  }
+
+  private scheduleNextRefresh(delayMs: number): void {
+    if (this.refreshTimeout) {
+      clearTimeout(this.refreshTimeout);
+      this.refreshTimeout = null;
+    }
+    if (delayMs === 0) {
+      this.refresh();
+    } else {
+      this.refreshTimeout = setTimeout(() => this.refresh(), delayMs);
+    }
+
+  }
+
+  /**
+   * Calculates the time in milliseconds when the token should be refreshed
+   * based on the token's TTL and the expirationRefreshRatio configuration.
+   *
+   * @param token The token to calculate the refresh time for.
+   * @param now The current time in milliseconds. Defaults to Date.now().
+   */
+  public calculateRefreshTime(token: Token<T>, now: number = Date.now()): number {
+    const ttlMs = token.getTtlMs(now);
+    return Math.floor(ttlMs * this.config.expirationRefreshRatio);
+  }
+
+  private stop(): void {
+
+    if (this.refreshTimeout) {
+      clearTimeout(this.refreshTimeout);
+      this.refreshTimeout = null;
+    }
+
+    this.listener = null;
+    this.currentToken = null;
+    this.retryAttempt = 0;
+  }
+
+  /**
+   * Returns the current token or null if no token is available.
+   */
+  public getCurrentToken(): Token<T> | null {
+    return this.currentToken;
+  }
+
+  private notifyError(error: unknown, isRetryable: boolean): void {
+    const errorMessage = error instanceof Error ? error.message : String(error);
+
+    if (!this.listener) {
+      throw new Error(`TokenManager is not running but received an error: ${errorMessage}`);
+    }
+
+    this.listener.onError(new IDPError(errorMessage, isRetryable));
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/token.ts b/packages/client/lib/authx/token.ts
new file mode 100644
index 00000000000..3d6e6867d84
--- /dev/null
+++ b/packages/client/lib/authx/token.ts
@@ -0,0 +1,23 @@
+/**
+ * A token that can be used to authenticate with a service.
+ */
+export class Token<T> {
+  constructor(
+    public readonly value: T,
+    //represents the token deadline - the time in milliseconds since the Unix epoch at which the token expires
+    public readonly expiresAtMs: number,
+    //represents the time in milliseconds since the Unix epoch at which the token was received
+    public readonly receivedAtMs: number
+  ) {}
+
+  /**
+   * Returns the time-to-live of the token in milliseconds.
+   * @param now The current time in milliseconds since the Unix epoch.
+   */
+  getTtlMs(now: number): number {
+    if (this.expiresAtMs < now) {
+      return 0;
+    }
+    return this.expiresAtMs - now;
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/client/RESP2/composers/buffer.spec.ts b/packages/client/lib/client/RESP2/composers/buffer.spec.ts
deleted file mode 100644
index f57c369fecb..00000000000
--- a/packages/client/lib/client/RESP2/composers/buffer.spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { strict as assert } from 'assert';
-import BufferComposer from './buffer';
-
-describe('Buffer Composer', () => {
-    const composer = new BufferComposer();
-
-    it('should compose two buffers', () => {
-        composer.write(Buffer.from([0]));
-        assert.deepEqual(
-            composer.end(Buffer.from([1])),
-            Buffer.from([0, 1])
-        );
-    });
-});
diff --git a/packages/client/lib/client/RESP2/composers/buffer.ts b/packages/client/lib/client/RESP2/composers/buffer.ts
deleted file mode 100644
index 4affb4283e0..00000000000
--- a/packages/client/lib/client/RESP2/composers/buffer.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Composer } from './interface';
-
-export default class BufferComposer implements Composer<Buffer> {
-    private chunks: Array<Buffer> = [];
-
-    write(buffer: Buffer): void {
-        this.chunks.push(buffer);
-    }
-
-    end(buffer: Buffer): Buffer {
-        this.write(buffer);
-        return Buffer.concat(this.chunks.splice(0));
-    }
-
-    reset() {
-        this.chunks = [];
-    }
-}
diff --git a/packages/client/lib/client/RESP2/composers/interface.ts b/packages/client/lib/client/RESP2/composers/interface.ts
deleted file mode 100644
index 0fc8f031414..00000000000
--- a/packages/client/lib/client/RESP2/composers/interface.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface Composer<T> {
-    write(buffer: Buffer): void;
-
-    end(buffer: Buffer): T;
-
-    reset(): void;
-}
diff --git a/packages/client/lib/client/RESP2/composers/string.spec.ts b/packages/client/lib/client/RESP2/composers/string.spec.ts
deleted file mode 100644
index 9dd26aae021..00000000000
--- a/packages/client/lib/client/RESP2/composers/string.spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { strict as assert } from 'assert';
-import StringComposer from './string';
-
-describe('String Composer', () => {
-    const composer = new StringComposer();
-
-    it('should compose two strings', () => {
-        composer.write(Buffer.from([0]));
-        assert.deepEqual(
-            composer.end(Buffer.from([1])),
-            Buffer.from([0, 1]).toString()
-        );
-    });
-});
diff --git a/packages/client/lib/client/RESP2/composers/string.ts b/packages/client/lib/client/RESP2/composers/string.ts
deleted file mode 100644
index 0cd8f00e95c..00000000000
--- a/packages/client/lib/client/RESP2/composers/string.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { StringDecoder } from 'string_decoder';
-import { Composer } from './interface';
-
-export default class StringComposer implements Composer<string> {
-    private decoder = new StringDecoder();
-
-    private string = '';
-
-    write(buffer: Buffer): void {
-        this.string += this.decoder.write(buffer);
-    }
-
-    end(buffer: Buffer): string {
-        const string = this.string + this.decoder.end(buffer);
-        this.string = '';
-        return string;
-    }
-
-    reset() {
-        this.string = '';
-    }
-}
diff --git a/packages/client/lib/client/RESP2/decoder.spec.ts b/packages/client/lib/client/RESP2/decoder.spec.ts
deleted file mode 100644
index dcce9f60115..00000000000
--- a/packages/client/lib/client/RESP2/decoder.spec.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import { strict as assert } from 'assert';
-import { SinonSpy, spy } from 'sinon';
-import RESP2Decoder from './decoder';
-import { ErrorReply } from '../../errors';
-
-interface DecoderAndSpies {
-    decoder: RESP2Decoder;
-    returnStringsAsBuffersSpy: SinonSpy;
-    onReplySpy: SinonSpy;
-}
-
-function createDecoderAndSpies(returnStringsAsBuffers: boolean): DecoderAndSpies {
-    const returnStringsAsBuffersSpy = spy(() => returnStringsAsBuffers),
-        onReplySpy = spy();
-
-    return {
-        decoder: new RESP2Decoder({
-            returnStringsAsBuffers: returnStringsAsBuffersSpy,
-            onReply: onReplySpy
-        }),
-        returnStringsAsBuffersSpy,
-        onReplySpy
-    };
-}
-
-function writeChunks(stream: RESP2Decoder, buffer: Buffer) {
-    let i = 0;
-    while (i < buffer.length) {
-        stream.write(buffer.slice(i, ++i));
-    }
-}
-
-type Replies = Array<Array<unknown>>;
-
-interface TestsOptions {
-    toWrite: Buffer;
-    returnStringsAsBuffers: boolean;
-    replies: Replies;
-}
-
-function generateTests({
-    toWrite,
-    returnStringsAsBuffers,
-    replies
-}: TestsOptions): void {
-    it('single chunk', () => {
-        const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
-            createDecoderAndSpies(returnStringsAsBuffers);
-        decoder.write(toWrite);
-        assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
-        testReplies(onReplySpy, replies);
-    });
-
-    it('multiple chunks', () => {
-        const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
-            createDecoderAndSpies(returnStringsAsBuffers);
-        writeChunks(decoder, toWrite);
-        assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
-        testReplies(onReplySpy, replies);
-    });
-}
-
-function testReplies(spy: SinonSpy, replies: Replies): void {
-    if (!replies) {
-        assert.equal(spy.callCount, 0);
-        return;
-    }
-
-    assert.equal(spy.callCount, replies.length);
-    for (const [i, reply] of replies.entries()) {
-        assert.deepEqual(
-            spy.getCall(i).args,
-            reply
-        );
-    }
-}
-
-describe('RESP2Parser', () => {
-    describe('Simple String', () => {
-        describe('as strings', () => {
-            generateTests({
-                toWrite: Buffer.from('+OK\r\n'),
-                returnStringsAsBuffers: false,
-                replies: [['OK']]
-            });
-        });
-
-        describe('as buffers', () => {
-            generateTests({
-                toWrite: Buffer.from('+OK\r\n'),
-                returnStringsAsBuffers: true,
-                replies: [[Buffer.from('OK')]]
-            });
-        });
-    });
-
-    describe('Error', () => {
-        generateTests({
-            toWrite: Buffer.from('-ERR\r\n'),
-            returnStringsAsBuffers: false,
-            replies: [[new ErrorReply('ERR')]]
-        });
-    });
-
-    describe('Integer', () => {
-        describe('-1', () => {
-            generateTests({
-                toWrite: Buffer.from(':-1\r\n'),
-                returnStringsAsBuffers: false,
-                replies: [[-1]]
-            });
-        });
-
-        describe('0', () => {
-            generateTests({
-                toWrite: Buffer.from(':0\r\n'),
-                returnStringsAsBuffers: false,
-                replies: [[0]]
-            });
-        });
-    });
-
-    describe('Bulk String', () => {
-        describe('null', () => {
-            generateTests({
-                toWrite: Buffer.from('$-1\r\n'),
-                returnStringsAsBuffers: false,
-                replies: [[null]]
-            });
-        });
-
-        describe('as strings', () => {
-            generateTests({
-                toWrite: Buffer.from('$2\r\naa\r\n'),
-                returnStringsAsBuffers: false,
-                replies: [['aa']]
-            });
-        });
-
-        describe('as buffers', () => {
-            generateTests({
-                toWrite: Buffer.from('$2\r\naa\r\n'),
-                returnStringsAsBuffers: true,
-                replies: [[Buffer.from('aa')]]
-            });
-        });
-    });
-
-    describe('Array', () => {
-        describe('null', () => {
-            generateTests({
-                toWrite: Buffer.from('*-1\r\n'),
-                returnStringsAsBuffers: false,
-                replies: [[null]]
-            });
-        });
-
-        const arrayBuffer = Buffer.from(
-            '*5\r\n' +
-            '+OK\r\n' +
-            '-ERR\r\n' +
-            ':0\r\n' +
-            '$1\r\na\r\n' +
-            '*0\r\n'
-        );
-
-        describe('as strings', () => {
-            generateTests({
-                toWrite: arrayBuffer,
-                returnStringsAsBuffers: false,
-                replies: [[[
-                    'OK',
-                    new ErrorReply('ERR'),
-                    0,
-                    'a',
-                    []
-                ]]]
-            });
-        });
-
-        describe('as buffers', () => {
-            generateTests({
-                toWrite: arrayBuffer,
-                returnStringsAsBuffers: true,
-                replies: [[[
-                    Buffer.from('OK'),
-                    new ErrorReply('ERR'),
-                    0,
-                    Buffer.from('a'),
-                    []
-                ]]]
-            });
-        });
-    });
-});
diff --git a/packages/client/lib/client/RESP2/decoder.ts b/packages/client/lib/client/RESP2/decoder.ts
deleted file mode 100644
index 525f118bf30..00000000000
--- a/packages/client/lib/client/RESP2/decoder.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import { ErrorReply } from '../../errors';
-import { Composer } from './composers/interface';
-import BufferComposer from './composers/buffer';
-import StringComposer from './composers/string';
-
-// RESP2 specification
-// https://redis.io/topics/protocol
-
-enum Types {
-    SIMPLE_STRING = 43, // +
-    ERROR = 45, // -
-    INTEGER = 58, // :
-    BULK_STRING = 36, // $
-    ARRAY = 42 // *
-}
-
-enum ASCII {
-    CR = 13, // \r
-    ZERO = 48,
-    MINUS = 45
-}
-
-export type Reply = string | Buffer | ErrorReply | number | null | Array<Reply>;
-
-type ArrayReply = Array<Reply> | null;
-
-export type ReturnStringsAsBuffers = () => boolean;
-
-interface RESP2Options {
-    returnStringsAsBuffers: ReturnStringsAsBuffers;
-    onReply(reply: Reply): unknown;
-}
-
-interface ArrayInProcess {
-    array: Array<Reply>;
-    pushCounter: number;
-}
-
-// Using TypeScript `private` and not the build-in `#` to avoid __classPrivateFieldGet and __classPrivateFieldSet
-
-export default class RESP2Decoder {
-    constructor(private options: RESP2Options) {}
-
-    private cursor = 0;
-
-    private type?: Types;
-
-    private bufferComposer = new BufferComposer();
-
-    private stringComposer = new StringComposer();
-
-    private currentStringComposer: BufferComposer | StringComposer = this.stringComposer;
-
-    reset() {
-        this.cursor = 0;
-        this.type = undefined;
-        this.bufferComposer.reset();
-        this.stringComposer.reset();
-        this.currentStringComposer = this.stringComposer;
-    }
-
-    write(chunk: Buffer): void {
-        while (this.cursor < chunk.length) {
-            if (!this.type) {
-                this.currentStringComposer = this.options.returnStringsAsBuffers() ?
-                    this.bufferComposer :
-                    this.stringComposer;
-
-                this.type = chunk[this.cursor];
-                if (++this.cursor >= chunk.length) break;
-            }
-
-            const reply = this.parseType(chunk, this.type);
-            if (reply === undefined) break;
-
-            this.type = undefined;
-            this.options.onReply(reply);
-        }
-
-        this.cursor -= chunk.length;
-    }
-
-    private parseType(chunk: Buffer, type: Types, arraysToKeep?: number): Reply | undefined {
-        switch (type) {
-            case Types.SIMPLE_STRING:
-                return this.parseSimpleString(chunk);
-
-            case Types.ERROR:
-                return this.parseError(chunk);
-
-            case Types.INTEGER:
-                return this.parseInteger(chunk);
-
-            case Types.BULK_STRING:
-                return this.parseBulkString(chunk);
-
-            case Types.ARRAY:
-                return this.parseArray(chunk, arraysToKeep);
-        }
-    }
-
-    private compose<
-        C extends Composer<T>,
-        T = C extends Composer<infer TT> ? TT : never
-    >(
-        chunk: Buffer,
-        composer: C
-    ): T | undefined {
-        for (let i = this.cursor; i < chunk.length; i++) {
-            if (chunk[i] === ASCII.CR) {
-                const reply = composer.end(
-                    chunk.subarray(this.cursor, i)
-                );
-                this.cursor = i + 2;
-                return reply;
-            }
-        }
-
-        const toWrite = chunk.subarray(this.cursor);
-        composer.write(toWrite);
-        this.cursor = chunk.length;
-    }
-
-    private parseSimpleString(chunk: Buffer): string | Buffer | undefined {
-        return this.compose(chunk, this.currentStringComposer);
-    }
-
-    private parseError(chunk: Buffer): ErrorReply | undefined {
-        const message = this.compose(chunk, this.stringComposer);
-        if (message !== undefined) {
-            return new ErrorReply(message);
-        }
-    }
-
-    private integer = 0;
-
-    private isNegativeInteger?: boolean;
-
-    private parseInteger(chunk: Buffer): number | undefined {
-        if (this.isNegativeInteger === undefined) {
-            this.isNegativeInteger = chunk[this.cursor] === ASCII.MINUS;
-            if (this.isNegativeInteger && ++this.cursor === chunk.length) return;
-        }
-
-        do {
-            const byte = chunk[this.cursor];
-            if (byte === ASCII.CR) {
-                const integer = this.isNegativeInteger ? -this.integer : this.integer;
-                this.integer = 0;
-                this.isNegativeInteger = undefined;
-                this.cursor += 2;
-                return integer;
-            }
-
-            this.integer = this.integer * 10 + byte - ASCII.ZERO;
-        } while (++this.cursor < chunk.length);
-    }
-
-    private bulkStringRemainingLength?: number;
-
-    private parseBulkString(chunk: Buffer): string | Buffer | null | undefined {
-        if (this.bulkStringRemainingLength === undefined) {
-            const length = this.parseInteger(chunk);
-            if (length === undefined) return;
-            if (length === -1) return null;
-
-            this.bulkStringRemainingLength = length;
-
-            if (this.cursor >= chunk.length) return;
-        }
-
-        const end = this.cursor + this.bulkStringRemainingLength;
-        if (chunk.length >= end) {
-            const reply = this.currentStringComposer.end(
-                chunk.subarray(this.cursor, end)
-            );
-            this.bulkStringRemainingLength = undefined;
-            this.cursor = end + 2;
-            return reply;
-        }
-
-        const toWrite = chunk.subarray(this.cursor);
-        this.currentStringComposer.write(toWrite);
-        this.bulkStringRemainingLength -= toWrite.length;
-        this.cursor = chunk.length;
-    }
-
-    private arraysInProcess: Array<ArrayInProcess> = [];
-
-    private initializeArray = false;
-
-    private arrayItemType?: Types;
-
-    private parseArray(chunk: Buffer, arraysToKeep = 0): ArrayReply | undefined {
-        if (this.initializeArray || this.arraysInProcess.length === arraysToKeep) {
-            const length = this.parseInteger(chunk);
-            if (length === undefined) {
-                this.initializeArray = true;
-                return undefined;
-            }
-
-            this.initializeArray = false;
-            this.arrayItemType = undefined;
-
-            if (length === -1) {
-                return this.returnArrayReply(null, arraysToKeep, chunk);
-            } else if (length === 0) {
-                return this.returnArrayReply([], arraysToKeep, chunk);
-            }
-
-            this.arraysInProcess.push({
-                array: new Array(length),
-                pushCounter: 0
-            });
-        }
-
-        while (this.cursor < chunk.length) {
-            if (!this.arrayItemType) {
-                this.arrayItemType = chunk[this.cursor];
-
-                if (++this.cursor >= chunk.length) break;
-            }
-
-            const item = this.parseType(
-                chunk,
-                this.arrayItemType,
-                arraysToKeep + 1
-            );
-            if (item === undefined) break;
-
-            this.arrayItemType = undefined;
-
-            const reply = this.pushArrayItem(item, arraysToKeep);
-            if (reply !== undefined) return reply;
-        }
-    }
-
-    private returnArrayReply(reply: ArrayReply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
-        if (this.arraysInProcess.length <= arraysToKeep) return reply;
-
-        return this.pushArrayItem(reply, arraysToKeep, chunk);
-    }
-
-    private pushArrayItem(item: Reply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
-        const to = this.arraysInProcess[this.arraysInProcess.length - 1]!;
-        to.array[to.pushCounter] = item;
-        if (++to.pushCounter === to.array.length) {
-            return this.returnArrayReply(
-                this.arraysInProcess.pop()!.array,
-                arraysToKeep,
-                chunk
-            );
-        } else if (chunk && chunk.length > this.cursor) {
-            return this.parseArray(chunk, arraysToKeep);
-        }
-    }
-}
diff --git a/packages/client/lib/client/RESP2/encoder.spec.ts b/packages/client/lib/client/RESP2/encoder.spec.ts
deleted file mode 100644
index 486259472a4..00000000000
--- a/packages/client/lib/client/RESP2/encoder.spec.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { strict as assert } from 'assert';
-import { describe } from 'mocha';
-import encodeCommand from './encoder';
-
-describe('RESP2 Encoder', () => {
-    it('1 byte', () => {
-        assert.deepEqual(
-            encodeCommand(['a', 'z']),
-            ['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
-        );
-    });
-
-    it('2 bytes', () => {
-        assert.deepEqual(
-            encodeCommand(['א', 'ת']),
-            ['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
-        );
-    });
-
-    it('4 bytes', () => {
-        assert.deepEqual(
-            [...encodeCommand(['🐣', '🐤'])],
-            ['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
-        );
-    });
-
-    it('buffer', () => {
-        assert.deepEqual(
-            encodeCommand([Buffer.from('string')]),
-            ['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
-        );
-    });
-});
diff --git a/packages/client/lib/client/RESP2/encoder.ts b/packages/client/lib/client/RESP2/encoder.ts
deleted file mode 100644
index 217fbc714bb..00000000000
--- a/packages/client/lib/client/RESP2/encoder.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { RedisCommandArgument, RedisCommandArguments } from '../../commands';
-
-const CRLF = '\r\n';
-
-export default function encodeCommand(args: RedisCommandArguments): Array<RedisCommandArgument> {
-    const toWrite: Array<RedisCommandArgument> = [];
-
-    let strings = '*' + args.length + CRLF;
-
-    for (let i = 0; i < args.length; i++) {
-        const arg = args[i];
-        if (typeof arg === 'string') {
-            strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF;
-        } else if (arg instanceof Buffer) {
-            toWrite.push(
-                strings + '$' + arg.length.toString() + CRLF,
-                arg
-            );
-            strings = CRLF;
-        } else {
-            throw new TypeError('Invalid argument type');
-        }
-    }
-
-    toWrite.push(strings);
-
-    return toWrite;
-}
diff --git a/packages/client/lib/client/cache.spec.ts b/packages/client/lib/client/cache.spec.ts
new file mode 100644
index 00000000000..55f2672c26c
--- /dev/null
+++ b/packages/client/lib/client/cache.spec.ts
@@ -0,0 +1,700 @@
+import assert from "assert";
+import testUtils, { GLOBAL } from "../test-utils"
+import { BasicClientSideCache, BasicPooledClientSideCache, CacheStats } from "./cache"
+import { REDIS_FLUSH_MODES } from "../commands/FLUSHALL";
+import { once } from 'events';
+
+describe("Client Side Cache", () => {
+  describe('Basic Cache', () => {
+    const csc = new BasicClientSideCache({ maxEntries: 10 });
+
+    testUtils.testWithClient('Basic Cache Miss', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      await client.get("x");
+
+      assert.equal(csc.stats().missCount, 1, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('Basic Cache Hit', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1');
+      assert.equal(await client.get("x"), '1');
+
+      assert.equal(csc.stats().missCount, 1, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 1, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('Max Cache Entries', async client => {
+      csc.clear();
+
+      await client.set('1', 1);
+      assert.equal(await client.get('1'), '1');
+      assert.equal(await client.get('2'), null);
+      assert.equal(await client.get('3'), null);
+      assert.equal(await client.get('4'), null);
+      assert.equal(await client.get('5'), null);
+      assert.equal(await client.get('6'), null);
+      assert.equal(await client.get('7'), null);
+      assert.equal(await client.get('8'), null);
+      assert.equal(await client.get('9'), null);
+      assert.equal(await client.get('10'), null);
+      assert.equal(await client.get('11'), null);
+      assert.equal(await client.get('1'), '1');
+
+      assert.equal(csc.stats().missCount, 12, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('LRU works correctly', async client => {
+      csc.clear();
+
+      await client.set('1', 1);
+      assert.equal(await client.get('1'), '1');
+      assert.equal(await client.get('2'), null);
+      assert.equal(await client.get('3'), null);
+      assert.equal(await client.get('4'), null);
+      assert.equal(await client.get('5'), null);
+      assert.equal(await client.get('1'), '1');
+      assert.equal(await client.get('6'), null);
+      assert.equal(await client.get('7'), null);
+      assert.equal(await client.get('8'), null);
+      assert.equal(await client.get('9'), null);
+      assert.equal(await client.get('10'), null);
+      assert.equal(await client.get('11'), null);
+      assert.equal(await client.get('1'), '1');
+
+      assert.equal(csc.stats().missCount, 11, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 2, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('Basic Cache Clear', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      await client.get("x");
+      csc.clear();
+      await client.get("x");
+
+      assert.equal(csc.stats().missCount, 1, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('Null Invalidate acts as clear', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      await client.get("x");
+      csc.invalidate(null);
+      await client.get("x");
+
+      assert.equal(2, csc.stats().missCount, "Cache Misses");
+      assert.equal(0, csc.stats().hitCount, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('flushdb causes an invalidate null', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1');
+      await client.flushDb(REDIS_FLUSH_MODES.SYNC);
+      assert.equal(await client.get("x"), null);
+
+      assert.equal(csc.stats().missCount, 2, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient('Basic Cache Invalidate', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1', 'first get');
+      await client.set("x", 2);
+      assert.equal(await client.get("x"), '2', 'second get');
+      await client.set("x", 3);
+      assert.equal(await client.get("x"), '3', 'third get');
+
+      assert.equal(csc.stats().missCount, 3, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient("Cached Replies Don't Mutate", async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      await client.set('y', 2);
+      const ret1 = await client.mGet(['x', 'y']);
+      assert.deepEqual(ret1, ['1', '2'], 'first mGet');
+      ret1[0] = '4';
+      const ret2 = await client.mGet(['x', 'y']);
+      assert.deepEqual(ret2, ['1', '2'], 'second mGet');
+      ret2[0] = '8';
+      const ret3 = await client.mGet(['x', 'y']);
+      assert.deepEqual(ret3, ['1', '2'], 'third mGet');
+
+      assert.equal(csc.stats().missCount, 1, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 2, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClient("Cached cleared on disconnect", async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      await client.set('y', 2);
+      const ret1 = await client.mGet(['x', 'y']);
+      assert.deepEqual(ret1, ['1', '2'], 'first mGet');
+
+      assert.equal(csc.stats().missCount, 1, "first Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "first Cache Hits");
+
+      await client.close();
+
+      await client.connect();
+
+      const ret2 = await client.mGet(['x', 'y']);
+      assert.deepEqual(ret2, ['1', '2'], 'second mGet');
+
+      assert.equal(csc.stats().missCount, 1, "second Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "second Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+  });
+
+  describe("Pooled Cache", () => {
+    const csc = new BasicPooledClientSideCache();
+
+    testUtils.testWithClient('Virtual Pool Disconnect', async client1 => {
+      const client2 = client1.duplicate();
+      await client2.connect()
+
+      assert.equal(await client2.get("x"), null);
+      assert.equal(await client1.get("x"), null);
+
+      assert.equal(1, csc.stats().missCount, "Cache Misses");
+      assert.equal(1, csc.stats().hitCount, "Cache Hits");
+
+      await client2.close();
+
+      assert.equal(await client1.get("x"), null);
+      assert.equal(await client1.get("x"), null);
+
+      assert.equal(2, csc.stats().missCount, "Cache Misses");
+      assert.equal(2, csc.stats().hitCount, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+
+    testUtils.testWithClientPool('Basic Cache Miss and Clear', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1');
+
+      assert.equal(1, csc.stats().missCount, "Cache Misses");
+      assert.equal(0, csc.stats().hitCount, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+      },
+      poolOptions: {
+        minimum: 5,
+        maximum: 5,
+        acquireTimeout: 0,
+        cleanupDelay: 1,
+        clientSideCache: csc
+      }
+    })
+
+    testUtils.testWithClientPool('Basic Cache Hit', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1');
+      assert.equal(await client.get("x"), '1');
+      assert.equal(await client.get("x"), '1');
+
+      assert.equal(csc.stats().missCount, 1, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 2, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+      },
+      poolOptions: {
+        minimum: 5,
+        maximum: 5,
+        acquireTimeout: 0,
+        cleanupDelay: 1,
+        clientSideCache: csc
+      }
+    })
+
+    testUtils.testWithClientPool('Basic Cache Manually Invalidate', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+
+      assert.equal(await client.get("x"), '1', 'first get');
+
+      let p: Promise<Array<string>> = once(csc, 'invalidate');
+      await client.set("x", 2);
+      let [i] = await p;
+
+      assert.equal(await client.get("x"), '2', 'second get');
+
+      p = once(csc, 'invalidate');
+      await client.set("x", 3);
+      [i] = await p;
+
+      assert.equal(await client.get("x"), '3');
+
+      assert.equal(csc.stats().missCount, 3, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+      },
+      poolOptions: {
+        minimum: 5,
+        maximum: 5,
+        acquireTimeout: 0,
+        cleanupDelay: 1,
+        clientSideCache: csc
+      }
+    })
+
+    testUtils.testWithClientPool('Basic Cache Invalidate via message', async client => {
+      csc.clear();
+
+      await client.set('x', 1);
+      await client.set('y', 2);
+
+      assert.deepEqual(await client.mGet(['x', 'y']), ['1', '2'], 'first mGet');
+
+      assert.equal(csc.stats().missCount, 1, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+
+      let p: Promise<Array<string>> = once(csc, 'invalidate');
+      await client.set("x", 3);
+      let [i] = await p;
+
+      assert.equal(i, 'x');
+
+      assert.deepEqual(await client.mGet(['x', 'y']), ['3', '2'], 'second mGet');
+
+      assert.equal(csc.stats().missCount, 2, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+
+      p = once(csc, 'invalidate');
+      await client.set("y", 4);
+      [i] = await p;
+
+      assert.equal(i, 'y');
+
+      assert.deepEqual(await client.mGet(['x', 'y']), ['3', '4'], 'second mGet');
+
+      assert.equal(csc.stats().missCount, 3, "Cache Misses");
+      assert.equal(csc.stats().hitCount, 0, "Cache Hits");
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+      },
+      poolOptions: {
+        minimum: 5,
+        maximum: 5,
+        acquireTimeout: 0,
+        cleanupDelay: 1,
+        clientSideCache: csc
+      }
+    })
+  });
+
+  describe('Cluster Caching', () => {
+    const csc = new BasicPooledClientSideCache();
+
+    testUtils.testWithCluster('Basic Cache Miss and Clear', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      await client.get("x");
+      await client.set("y", 1);
+      await client.get("y");
+
+      assert.equal(2, csc.stats().missCount, "Cache Misses");
+      assert.equal(0, csc.stats().hitCount, "Cache Hits");
+    }, {
+      ...GLOBAL.CLUSTERS.OPEN,
+      clusterConfiguration: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    })
+
+    testUtils.testWithCluster('Basic Cache Hit', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1');
+      assert.equal(await client.get("x"), '1');
+      assert.equal(await client.get("x"), '1');
+      await client.set("y", 1);
+      assert.equal(await client.get("y"), '1');
+      assert.equal(await client.get("y"), '1');
+      assert.equal(await client.get("y"), '1');
+
+      assert.equal(2, csc.stats().missCount, "Cache Misses");
+      assert.equal(4, csc.stats().hitCount, "Cache Hits");
+    }, {
+      ...GLOBAL.CLUSTERS.OPEN,
+      clusterConfiguration: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    })
+
+    testUtils.testWithCluster('Basic Cache Invalidate', async client => {
+      csc.clear();
+
+      await client.set("x", 1);
+      assert.equal(await client.get("x"), '1');
+      await client.set("x", 2);
+      assert.equal(await client.get("x"), '2');
+      await client.set("x", 3);
+      assert.equal(await client.get("x"), '3');
+
+      await client.set("y", 1);
+      assert.equal(await client.get("y"), '1');
+      await client.set("y", 2);
+      assert.equal(await client.get("y"), '2');
+      await client.set("y", 3);
+      assert.equal(await client.get("y"), '3');
+
+      assert.equal(6, csc.stats().missCount, "Cache Misses");
+      assert.equal(0, csc.stats().hitCount, "Cache Hits");
+    }, {
+      ...GLOBAL.CLUSTERS.OPEN,
+      clusterConfiguration: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    })
+  });
+  describe("CacheStats", () => {
+    describe("CacheStats.of()", () => {
+      it("should correctly initialize stats and calculate derived values", () => {
+        const stats = CacheStats.of(10, 5, 8, 2, 100, 3);
+        assert.strictEqual(stats.hitCount, 10, "hitCount should be 10");
+        assert.strictEqual(stats.missCount, 5, "missCount should be 5");
+        assert.strictEqual(stats.loadSuccessCount, 8, "loadSuccessCount should be 8");
+        assert.strictEqual(stats.loadFailureCount, 2, "loadFailureCount should be 2");
+        assert.strictEqual(stats.totalLoadTime, 100, "totalLoadTime should be 100");
+        assert.strictEqual(stats.evictionCount, 3, "evictionCount should be 3");
+
+        assert.strictEqual(stats.requestCount(), 15, "requestCount should be 15 (10 hits + 5 misses)");
+        assert.strictEqual(stats.hitRate(), 10 / 15, "hitRate should be 10/15");
+        assert.strictEqual(stats.missRate(), 5 / 15, "missRate should be 5/15");
+        assert.strictEqual(stats.loadCount(), 10, "loadCount should be 10 (8 success + 2 failure)");
+        assert.strictEqual(stats.loadFailureRate(), 2 / 10, "loadFailureRate should be 2/10");
+        assert.strictEqual(stats.averageLoadPenalty(), 100 / 10, "averageLoadPenalty should be 10 (100 time / 10 loads)");
+      });
+
+      it("should handle zero values and division by zero for derived values", () => {
+        const stats = CacheStats.of(0, 0, 0, 0, 0, 0);
+        assert.strictEqual(stats.hitCount, 0, "hitCount");
+        assert.strictEqual(stats.missCount, 0, "missCount");
+        assert.strictEqual(stats.loadSuccessCount, 0, "loadSuccessCount");
+        assert.strictEqual(stats.loadFailureCount, 0, "loadFailureCount");
+        assert.strictEqual(stats.totalLoadTime, 0, "totalLoadTime");
+        assert.strictEqual(stats.evictionCount, 0, "evictionCount");
+
+        assert.strictEqual(stats.requestCount(), 0, "requestCount should be 0");
+        assert.strictEqual(stats.hitRate(), 1, "hitRate should be 1 for 0 requests");
+        assert.strictEqual(stats.missRate(), 0, "missRate should be 0 for 0 requests");
+        assert.strictEqual(stats.loadCount(), 0, "loadCount should be 0");
+        assert.strictEqual(stats.loadFailureRate(), 0, "loadFailureRate should be 0 for 0 loads");
+        assert.strictEqual(stats.averageLoadPenalty(), 0, "averageLoadPenalty should be 0 for 0 loads");
+      });
+    });
+
+    describe("CacheStats.empty()", () => {
+      it("should return stats with all zero counts and 0 for rates/penalties", () => {
+        const stats = CacheStats.empty();
+        assert.strictEqual(stats.hitCount, 0, "empty.hitCount");
+        assert.strictEqual(stats.missCount, 0, "empty.missCount");
+        assert.strictEqual(stats.loadSuccessCount, 0, "empty.loadSuccessCount");
+        assert.strictEqual(stats.loadFailureCount, 0, "empty.loadFailureCount");
+        assert.strictEqual(stats.totalLoadTime, 0, "empty.totalLoadTime");
+        assert.strictEqual(stats.evictionCount, 0, "empty.evictionCount");
+
+        assert.strictEqual(stats.requestCount(), 0, "empty.requestCount");
+        assert.strictEqual(stats.hitRate(), 1, "empty.hitRate should be 1");
+        assert.strictEqual(stats.missRate(), 0, "empty.missRate should be 0");
+        assert.strictEqual(stats.loadCount(), 0, "empty.loadCount");
+        assert.strictEqual(stats.loadFailureRate(), 0, "empty.loadFailureRate should be 0");
+        assert.strictEqual(stats.averageLoadPenalty(), 0, "empty.averageLoadPenalty should be 0");
+      });
+    });
+
+    describe("instance methods", () => {
+      const stats1 = CacheStats.of(10, 5, 8, 2, 100, 3);
+      const stats2 = CacheStats.of(20, 10, 12, 3, 200, 5);
+
+      describe("plus()", () => {
+        it("should correctly add two CacheStats instances", () => {
+          const sum = stats1.plus(stats2);
+          assert.strictEqual(sum.hitCount, 30);
+          assert.strictEqual(sum.missCount, 15);
+          assert.strictEqual(sum.loadSuccessCount, 20);
+          assert.strictEqual(sum.loadFailureCount, 5);
+          assert.strictEqual(sum.totalLoadTime, 300);
+          assert.strictEqual(sum.evictionCount, 8);
+        });
+
+        it("should correctly sum large numbers", () => {
+          const statsC = CacheStats.of(Number.MAX_VALUE, 1, 1, 1, 1, 1);
+          const statsD = CacheStats.of(Number.MAX_VALUE, 1, 1, 1, 1, 1);
+          const sum = statsC.plus(statsD);
+          assert.strictEqual(sum.hitCount, Infinity, "Summing MAX_VALUE should result in Infinity");
+        });
+      });
+
+      describe("minus()", () => {
+        it("should correctly subtract one CacheStats instance from another, flooring at 0", () => {
+          const diff = stats2.minus(stats1);
+          assert.strictEqual(diff.hitCount, 10);
+          assert.strictEqual(diff.missCount, 5);
+          assert.strictEqual(diff.loadSuccessCount, 4);
+          assert.strictEqual(diff.loadFailureCount, 1);
+          assert.strictEqual(diff.totalLoadTime, 100);
+          assert.strictEqual(diff.evictionCount, 2);
+        });
+
+        it("should floor results at 0 if minuend is smaller than subtrahend", () => {
+          const sSmall = CacheStats.of(5, 2, 1, 0, 10, 1);
+          const sLarge = CacheStats.of(10, 5, 2, 1, 20, 2);
+          const diff = sSmall.minus(sLarge);
+          assert.strictEqual(diff.hitCount, 0, "hitCount should be floored at 0 (5 - 10)");
+          assert.strictEqual(diff.missCount, 0, "missCount should be floored at 0 (2 - 5)");
+          assert.strictEqual(diff.loadSuccessCount, 0, "loadSuccessCount should be floored at 0 (1 - 2)");
+          assert.strictEqual(diff.loadFailureCount, 0, "loadFailureCount should be floored at 0 (0 - 1)");
+          assert.strictEqual(diff.totalLoadTime, 0, "totalLoadTime should be floored at 0 (10 - 20)");
+          assert.strictEqual(diff.evictionCount, 0, "evictionCount should be floored at 0 (1 - 2)");
+        });
+      });
+
+      describe("hitRate()", () => {
+        it("should return 0 if requestCount is 0", () => {
+          const stats = CacheStats.of(0, 0, 0, 0, 0, 0);
+          assert.strictEqual(stats.hitRate(), 1);
+        });
+        it("should return 0 if hitCount is 0 but missCount > 0", () => {
+          const stats = CacheStats.of(0, 1, 0, 0, 0, 0);
+          assert.strictEqual(stats.hitRate(), 0);
+        });
+        it("should return 1 if missCount is 0 but hitCount > 0", () => {
+          const stats = CacheStats.of(1, 0, 0, 0, 0, 0);
+          assert.strictEqual(stats.hitRate(), 1);
+        });
+      });
+
+      describe("missRate()", () => {
+        it("should return 0 if requestCount is 0", () => {
+          const stats = CacheStats.of(0, 0, 0, 0, 0, 0);
+          assert.strictEqual(stats.missRate(), 0);
+        });
+        it("should return 1 if hitCount is 0 but missCount > 0", () => {
+          const stats = CacheStats.of(0, 1, 0, 0, 0, 0);
+          assert.strictEqual(stats.missRate(), 1);
+        });
+        it("should return 0 if missCount is 0 but hitCount > 0", () => {
+          const stats = CacheStats.of(1, 0, 0, 0, 0, 0);
+          assert.strictEqual(stats.missRate(), 0);
+        });
+      });
+
+      describe("loadFailureRate()", () => {
+        it("should return 0 if loadCount is 0", () => {
+          const stats = CacheStats.of(0, 0, 0, 0, 0, 0);
+          assert.strictEqual(stats.loadFailureRate(), 0);
+        });
+        it("should return 0 if loadFailureCount is 0 but loadSuccessCount > 0", () => {
+          const stats = CacheStats.of(0, 0, 1, 0, 10, 0);
+          assert.strictEqual(stats.loadFailureRate(), 0);
+        });
+        it("should return 1 if loadSuccessCount is 0 but loadFailureCount > 0", () => {
+          const stats = CacheStats.of(0, 0, 0, 1, 10, 0);
+          assert.strictEqual(stats.loadFailureRate(), 1);
+        });
+      });
+
+      describe("averageLoadPenalty()", () => {
+        it("should return 0 if loadCount is 0, even if totalLoadTime > 0", () => {
+          const stats = CacheStats.of(0, 0, 0, 0, 100, 0);
+          assert.strictEqual(stats.averageLoadPenalty(), 0);
+        });
+        it("should return 0 if totalLoadTime is 0 and loadCount > 0", () => {
+          const stats = CacheStats.of(0, 0, 1, 1, 0, 0);
+          assert.strictEqual(stats.averageLoadPenalty(), 0);
+        });
+      });
+    });
+  });
+  it('should reflect comprehensive cache operations in stats via BasicClientSideCache', async function () {
+
+    const csc = new BasicClientSideCache({
+      maxEntries: 2, // Small size to easily trigger evictions
+    });
+
+    testUtils.testWithClient('comprehensive_stats_run', async client => {
+
+      // --- Phase 1: Initial misses and loads ---
+      await client.set('keyA', 'valueA_1');
+      assert.strictEqual(await client.get('keyA'), 'valueA_1', "Get keyA first time");
+      assert.strictEqual(csc.stats().missCount, 1);
+      assert.strictEqual(csc.stats().loadSuccessCount, 1);
+
+      await client.set('keyB', 'valueB_1');
+      assert.strictEqual(await client.get('keyB'), 'valueB_1', "Get keyB first time");
+      assert.strictEqual(csc.stats().missCount, 2);
+      assert.strictEqual(csc.stats().loadSuccessCount, 2);
+
+      // --- Phase 2: Cache hits ---
+      assert.strictEqual(await client.get('keyA'), 'valueA_1', "Get keyA second time (hit)");
+      assert.strictEqual(csc.stats().hitCount, 1);
+
+      assert.strictEqual(await client.get('keyB'), 'valueB_1', "Get keyB second time (hit)");
+      assert.strictEqual(csc.stats().hitCount, 2);
+
+
+      // --- Phase 3: Trigger evictions and more misses/loads ---
+      await client.set('keyC', 'valueC_1');
+      assert.strictEqual(await client.get('keyC'), 'valueC_1', "Get keyC first time (evicts keyA)");
+      assert.strictEqual(csc.stats().missCount, 3);
+      assert.strictEqual(csc.stats().loadSuccessCount, 3);
+      assert.strictEqual(csc.stats().evictionCount, 1);
+
+
+      assert.strictEqual(await client.get('keyA'), 'valueA_1', "Get keyA again (miss after eviction)");
+      assert.strictEqual(csc.stats().missCount, 4);
+      assert.strictEqual(csc.stats().loadSuccessCount, 4);
+      assert.strictEqual(csc.stats().evictionCount, 2);
+
+
+      // --- Phase 4: More hits ---
+      assert.strictEqual(await client.get('keyC'), 'valueC_1', "Get keyC again (hit)");
+      assert.strictEqual(csc.stats().hitCount, 3);
+
+      // --- Phase 5: Update a key (results in invalidation, then miss/load on next GET) ---
+      // Note: A SET operation on an existing cached key should invalidate it.
+      // The invalidation itself isn't directly a "hit" or "miss" for stats,
+      // but the *next* GET will be a miss.
+      await client.set('keyA', 'valueA_2');
+      assert.strictEqual(await client.get('keyA'), 'valueA_2', "Get keyA after SET (miss due to invalidation)");
+
+      assert.strictEqual(csc.stats().hitCount, 3);
+      assert.strictEqual(csc.stats().loadSuccessCount, 5);
+
+
+
+      const stats = csc.stats()
+
+      assert.strictEqual(stats.hitCount, 3, "Final hitCount");
+      assert.strictEqual(stats.missCount, 5, "Final missCount");
+      assert.strictEqual(stats.loadSuccessCount, 5, "Final loadSuccessCount");
+      assert.strictEqual(stats.loadFailureCount, 0, "Final loadFailureCount (expected 0 for this test)");
+      assert.strictEqual(stats.evictionCount, 2, "Final evictionCount");
+      assert.ok(stats.totalLoadTime >= 0, "Final totalLoadTime should be non-negative");
+
+      assert.strictEqual(stats.requestCount(), 8, "Final requestCount (5 misses + 3 hits)");
+      assert.strictEqual(stats.hitRate(), 3 / 8, "Final hitRate");
+      assert.strictEqual(stats.missRate(), 5 / 8, "Final missRate");
+
+      assert.strictEqual(stats.loadCount(), 5, "Final loadCount (5 success + 0 failure)");
+      assert.strictEqual(stats.loadFailureRate(), 0, "Final loadFailureRate (0 failures / 5 loads)");
+
+      if (stats.loadCount() > 0) {
+        assert.ok(stats.averageLoadPenalty() >= 0, "Final averageLoadPenalty should be non-negative");
+        assert.strictEqual(stats.averageLoadPenalty(), stats.totalLoadTime / stats.loadCount(), "Average load penalty calculation");
+      } else {
+        assert.strictEqual(stats.averageLoadPenalty(), 0, "Final averageLoadPenalty should be 0 if no loads");
+      }
+
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3,
+        clientSideCache: csc
+      }
+    });
+  });
+});
diff --git a/packages/client/lib/client/cache.ts b/packages/client/lib/client/cache.ts
new file mode 100644
index 00000000000..7254352ee8f
--- /dev/null
+++ b/packages/client/lib/client/cache.ts
@@ -0,0 +1,870 @@
+import { EventEmitter } from 'stream';
+import RedisClient from '.';
+import { RedisArgument, ReplyUnion, TransformReply, TypeMapping } from '../RESP/types';
+import { BasicCommandParser } from './parser';
+
+/**
+ * A snapshot of cache statistics.
+ *
+ * This class provides an immutable view of the cache's operational statistics at a particular
+ * point in time. It is heavily inspired by the statistics reporting capabilities found in
+ * Ben Manes's Caffeine cache (https://github.com/ben-manes/caffeine).
+ *
+ * Instances of `CacheStats` are typically obtained from a {@link StatsCounter} and can be used
+ * for performance monitoring, debugging, or logging. It includes metrics such as hit rate,
+ * miss rate, load success/failure rates, average load penalty, and eviction counts.
+ *
+ * All statistics are non-negative. Rates and averages are typically in the range `[0.0, 1.0]`,
+ * or `0` if the an operation has not occurred (e.g. hit rate is 0 if there are no requests).
+ *
+ * Cache statistics are incremented according to specific rules:
+ * - When a cache lookup encounters an existing entry, hitCount is incremented.
+ * - When a cache lookup encounters a missing entry, missCount is incremented.
+ * - When a new entry is successfully loaded, loadSuccessCount is incremented and the
+ *   loading time is added to totalLoadTime.
+ * - When an entry fails to load, loadFailureCount is incremented and the
+ *   loading time is added to totalLoadTime.
+ * - When an entry is evicted due to size constraints or expiration,
+ *   evictionCount is incremented.
+ */
+export class CacheStats {
+  /**
+   * Creates a new CacheStats instance with the specified statistics.
+   */
+  private constructor(
+    public readonly hitCount: number,
+    public readonly missCount: number,
+    public readonly loadSuccessCount: number,
+    public readonly loadFailureCount: number,
+    public readonly totalLoadTime: number,
+    public readonly evictionCount: number
+  ) {
+    if (
+      hitCount < 0 ||
+      missCount < 0 ||
+      loadSuccessCount < 0 ||
+      loadFailureCount < 0 ||
+      totalLoadTime < 0 ||
+      evictionCount < 0
+    ) {
+      throw new Error('All statistics values must be non-negative');
+    }
+  }
+
+  /**
+   * Creates a new CacheStats instance with the specified statistics.
+   *
+   * @param hitCount - Number of cache hits
+   * @param missCount - Number of cache misses
+   * @param loadSuccessCount - Number of successful cache loads
+   * @param loadFailureCount - Number of failed cache loads
+   * @param totalLoadTime - Total load time in milliseconds
+   * @param evictionCount - Number of cache evictions
+   */
+  static of(
+    hitCount = 0,
+    missCount = 0,
+    loadSuccessCount = 0,
+    loadFailureCount = 0,
+    totalLoadTime = 0,
+    evictionCount = 0
+  ): CacheStats {
+    return new CacheStats(
+      hitCount,
+      missCount,
+      loadSuccessCount,
+      loadFailureCount,
+      totalLoadTime,
+      evictionCount
+    );
+  }
+
+  /**
+   * Returns a statistics instance where no cache events have been recorded.
+   *
+   * @returns An empty statistics instance
+   */
+  static empty(): CacheStats {
+    return CacheStats.EMPTY_STATS;
+  }
+
+  /**
+   * An empty stats instance with all counters set to zero.
+   */
+  private static readonly EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0);
+
+  /**
+  * Returns the total number of times cache lookup methods have returned
+  * either a cached or uncached value.
+  *
+  * @returns Total number of requests (hits + misses)
+  */
+  requestCount(): number {
+    return this.hitCount + this.missCount;
+  }
+
+  /**
+   * Returns the hit rate of the cache.
+   * This is defined as hitCount / requestCount, or 1.0 when requestCount is 0.
+   *
+   * @returns The ratio of cache requests that were hits (between 0.0 and 1.0)
+   */
+  hitRate(): number {
+    const requestCount = this.requestCount();
+    return requestCount === 0 ? 1.0 : this.hitCount / requestCount;
+  }
+
+  /**
+   * Returns the miss rate of the cache.
+   * This is defined as missCount / requestCount, or 0.0 when requestCount is 0.
+   *
+   * @returns The ratio of cache requests that were misses (between 0.0 and 1.0)
+   */
+  missRate(): number {
+    const requestCount = this.requestCount();
+    return requestCount === 0 ? 0.0 : this.missCount / requestCount;
+  }
+
+  /**
+  * Returns the total number of load operations (successful + failed).
+  *
+  * @returns Total number of load operations
+  */
+  loadCount(): number {
+    return this.loadSuccessCount + this.loadFailureCount;
+  }
+
+  /**
+   * Returns the ratio of cache loading attempts that failed.
+   * This is defined as loadFailureCount / loadCount, or 0.0 when loadCount is 0.
+   *
+   * @returns Ratio of load operations that failed (between 0.0 and 1.0)
+   */
+  loadFailureRate(): number {
+    const loadCount = this.loadCount();
+    return loadCount === 0 ? 0.0 : this.loadFailureCount / loadCount;
+  }
+
+  /**
+   * Returns the average time spent loading new values, in milliseconds.
+   * This is defined as totalLoadTime / loadCount, or 0.0 when loadCount is 0.
+   *
+   * @returns Average load time in milliseconds
+   */
+  averageLoadPenalty(): number {
+    const loadCount = this.loadCount();
+    return loadCount === 0 ? 0.0 : this.totalLoadTime / loadCount;
+  }
+
+  /**
+  * Returns a new CacheStats representing the difference between this CacheStats
+  * and another. Negative values are rounded up to zero.
+  *
+  * @param other - The statistics to subtract from this instance
+  * @returns The difference between this instance and other
+  */
+  minus(other: CacheStats): CacheStats {
+    return CacheStats.of(
+      Math.max(0, this.hitCount - other.hitCount),
+      Math.max(0, this.missCount - other.missCount),
+      Math.max(0, this.loadSuccessCount - other.loadSuccessCount),
+      Math.max(0, this.loadFailureCount - other.loadFailureCount),
+      Math.max(0, this.totalLoadTime - other.totalLoadTime),
+      Math.max(0, this.evictionCount - other.evictionCount)
+    );
+  }
+
+  /**
+   * Returns a new CacheStats representing the sum of this CacheStats and another.
+   *
+   * @param other - The statistics to add to this instance
+   * @returns The sum of this instance and other
+   */
+  plus(other: CacheStats): CacheStats {
+    return CacheStats.of(
+      this.hitCount + other.hitCount,
+      this.missCount + other.missCount,
+      this.loadSuccessCount + other.loadSuccessCount,
+      this.loadFailureCount + other.loadFailureCount,
+      this.totalLoadTime + other.totalLoadTime,
+      this.evictionCount + other.evictionCount
+    );
+  }
+}
+
+/**
+ * An accumulator for cache statistics.
+ *
+ * This interface defines the contract for objects that record cache-related events
+ * such as hits, misses, loads (successes and failures), and evictions. The design
+ * is inspired by the statistics collection mechanisms in Ben Manes's Caffeine cache
+ * (https://github.com/ben-manes/caffeine).
+ *
+ * Implementations of this interface are responsible for aggregating these events.
+ * A snapshot of the current statistics can be obtained by calling the `snapshot()`
+ * method, which returns an immutable {@link CacheStats} object.
+ *
+ * Common implementations include `DefaultStatsCounter` for active statistics collection
+ * and `DisabledStatsCounter` for a no-op version when stats are not needed.
+ */
+export interface StatsCounter {
+  /**
+   * Records cache hits. This should be called when a cache request returns a cached value.
+   *
+   * @param count - The number of hits to record
+   */
+  recordHits(count: number): void;
+
+  /**
+   * Records cache misses. This should be called when a cache request returns a value that was not
+   * found in the cache.
+   *
+   * @param count - The number of misses to record
+   */
+  recordMisses(count: number): void;
+
+  /**
+   * Records the successful load of a new entry. This method should be called when a cache request
+   * causes an entry to be loaded and the loading completes successfully.
+   *
+   * @param loadTime - The number of milliseconds the cache spent computing or retrieving the new value
+   */
+  recordLoadSuccess(loadTime: number): void;
+
+  /**
+   * Records the failed load of a new entry. This method should be called when a cache request
+   * causes an entry to be loaded, but an exception is thrown while loading the entry.
+   *
+   * @param loadTime - The number of milliseconds the cache spent computing or retrieving the new value
+   *                   prior to the failure
+   */
+  recordLoadFailure(loadTime: number): void;
+
+  /**
+   * Records the eviction of an entry from the cache. This should only be called when an entry is
+   * evicted due to the cache's eviction strategy, and not as a result of manual invalidations.
+   *
+   * @param count - The number of evictions to record
+   */
+  recordEvictions(count: number): void;
+
+  /**
+   * Returns a snapshot of this counter's values. Note that this may be an inconsistent view, as it
+   * may be interleaved with update operations.
+   *
+   * @return A snapshot of this counter's values
+   */
+  snapshot(): CacheStats;
+}
+
+/**
+ * A StatsCounter implementation that does nothing and always returns empty stats.
+ */
+class DisabledStatsCounter implements StatsCounter {
+  static readonly INSTANCE = new DisabledStatsCounter();
+
+  private constructor() { }
+
+  recordHits(count: number): void { }
+  recordMisses(count: number): void { }
+  recordLoadSuccess(loadTime: number): void { }
+  recordLoadFailure(loadTime: number): void { }
+  recordEvictions(count: number): void { }
+  snapshot(): CacheStats { return CacheStats.empty(); }
+}
+
+/**
+ * Returns a StatsCounter that does not record any cache events.
+ *
+ * @return A StatsCounter that does not record metrics
+ */
+function disabledStatsCounter(): StatsCounter {
+  return DisabledStatsCounter.INSTANCE;
+}
+
+/**
+ * A StatsCounter implementation that maintains cache statistics.
+ */
+class DefaultStatsCounter implements StatsCounter {
+  #hitCount = 0;
+  #missCount = 0;
+  #loadSuccessCount = 0;
+  #loadFailureCount = 0;
+  #totalLoadTime = 0;
+  #evictionCount = 0;
+
+  /**
+   * Records cache hits.
+   *
+   * @param count - The number of hits to record
+   */
+  recordHits(count: number): void {
+    this.#hitCount += count;
+  }
+
+  /**
+   * Records cache misses.
+   *
+   * @param count - The number of misses to record
+   */
+  recordMisses(count: number): void {
+    this.#missCount += count;
+  }
+
+  /**
+   * Records the successful load of a new entry.
+   *
+   * @param loadTime - The number of milliseconds spent loading the entry
+   */
+  recordLoadSuccess(loadTime: number): void {
+    this.#loadSuccessCount++;
+    this.#totalLoadTime += loadTime;
+  }
+
+  /**
+   * Records the failed load of a new entry.
+   *
+   * @param loadTime - The number of milliseconds spent attempting to load the entry
+   */
+  recordLoadFailure(loadTime: number): void {
+    this.#loadFailureCount++;
+    this.#totalLoadTime += loadTime;
+  }
+
+  /**
+   * Records cache evictions.
+   *
+   * @param count - The number of evictions to record
+   */
+  recordEvictions(count: number): void {
+    this.#evictionCount += count;
+  }
+
+  /**
+   * Returns a snapshot of the current statistics.
+   *
+   * @returns A snapshot of the current statistics
+   */
+  snapshot(): CacheStats {
+    return CacheStats.of(
+      this.#hitCount,
+      this.#missCount,
+      this.#loadSuccessCount,
+      this.#loadFailureCount,
+      this.#totalLoadTime,
+      this.#evictionCount
+    );
+  }
+
+  /**
+   * Creates a new DefaultStatsCounter.
+   *
+   * @returns A new DefaultStatsCounter instance
+   */
+  static create(): DefaultStatsCounter {
+    return new DefaultStatsCounter();
+  }
+}
+
+type CachingClient = RedisClient<any, any, any, any, any>;
+type CmdFunc = () => Promise<ReplyUnion>;
+
+type EvictionPolicy = "LRU" | "FIFO"
+
+/**
+ * Configuration options for Client Side Cache
+ */
+export interface ClientSideCacheConfig {
+  /**
+   * Time-to-live in milliseconds for cached entries.
+   * Use 0 for no expiration.
+   * @default 0
+   */
+  ttl?: number;
+
+  /**
+   * Maximum number of entries to store in the cache.
+   * Use 0 for unlimited entries.
+   * @default 0
+   */
+  maxEntries?: number;
+
+  /**
+   * Eviction policy to use when the cache reaches its capacity.
+   * - "LRU" (Least Recently Used): Evicts least recently accessed entries first
+   * - "FIFO" (First In First Out): Evicts oldest entries first
+   * @default "LRU"
+   */
+  evictPolicy?: EvictionPolicy;
+
+  /**
+   * Whether to collect statistics about cache operations.
+   * @default true
+   */
+  recordStats?: boolean;
+}
+
+interface CacheCreator {
+  epoch: number;
+  client: CachingClient;
+}
+
+interface ClientSideCacheEntry {
+  invalidate(): void;
+  validate(): boolean;
+}
+
+/**
+ * Generates a unique cache key from Redis command arguments
+ *
+ * @param redisArgs - Array of Redis command arguments
+ * @returns A unique string key for caching
+ */
+function generateCacheKey(redisArgs: ReadonlyArray<RedisArgument>): string {
+  const tmp = new Array(redisArgs.length * 2);
+
+  for (let i = 0; i < redisArgs.length; i++) {
+    tmp[i] = redisArgs[i].length;
+    tmp[i + redisArgs.length] = redisArgs[i];
+  }
+
+  return tmp.join('_');
+}
+
+abstract class ClientSideCacheEntryBase implements ClientSideCacheEntry {
+  #invalidated = false;
+  readonly #expireTime: number;
+
+  constructor(ttl: number) {
+    if (ttl == 0) {
+      this.#expireTime = 0;
+    } else {
+      this.#expireTime = Date.now() + ttl;
+    }
+  }
+
+  invalidate(): void {
+    this.#invalidated = true;
+  }
+
+  validate(): boolean {
+    return !this.#invalidated && (this.#expireTime == 0 || (Date.now() < this.#expireTime))
+  }
+}
+
+class ClientSideCacheEntryValue extends ClientSideCacheEntryBase {
+  readonly #value: any;
+
+  get value() {
+    return this.#value;
+  }
+
+  constructor(ttl: number, value: any) {
+    super(ttl);
+    this.#value = value;
+  }
+}
+
+class ClientSideCacheEntryPromise extends ClientSideCacheEntryBase {
+  readonly #sendCommandPromise: Promise<ReplyUnion>;
+
+  get promise() {
+    return this.#sendCommandPromise;
+  }
+
+  constructor(ttl: number, sendCommandPromise: Promise<ReplyUnion>) {
+    super(ttl);
+    this.#sendCommandPromise = sendCommandPromise;
+  }
+}
+
+export abstract class ClientSideCacheProvider extends EventEmitter {
+  abstract handleCache(client: CachingClient, parser: BasicCommandParser, fn: CmdFunc, transformReply: TransformReply | undefined, typeMapping: TypeMapping | undefined): Promise<any>;
+  abstract trackingOn(): Array<RedisArgument>;
+  abstract invalidate(key: RedisArgument | null): void;
+  abstract clear(): void;
+  abstract stats(): CacheStats;
+  abstract onError(): void;
+  abstract onClose(): void;
+}
+
+export class BasicClientSideCache extends ClientSideCacheProvider {
+  #cacheKeyToEntryMap: Map<string, ClientSideCacheEntry>;
+  #keyToCacheKeySetMap: Map<string, Set<string>>;
+  readonly ttl: number;
+  readonly maxEntries: number;
+  readonly lru: boolean;
+  #statsCounter: StatsCounter;
+
+
+  recordEvictions(count: number): void {
+    this.#statsCounter.recordEvictions(count);
+  }
+
+  recordHits(count: number): void {
+    this.#statsCounter.recordHits(count);
+  }
+
+  recordMisses(count: number): void {
+    this.#statsCounter.recordMisses(count);
+  }
+
+  constructor(config?: ClientSideCacheConfig) {
+    super();
+
+    this.#cacheKeyToEntryMap = new Map<string, ClientSideCacheEntry>();
+    this.#keyToCacheKeySetMap = new Map<string, Set<string>>();
+    this.ttl = config?.ttl ?? 0;
+    this.maxEntries = config?.maxEntries ?? 0;
+    this.lru = config?.evictPolicy !== "FIFO";
+
+    const recordStats = config?.recordStats !== false;
+    this.#statsCounter = recordStats ? DefaultStatsCounter.create() : disabledStatsCounter();
+  }
+
+  /* logic of how caching works:
+
+  1. commands use a CommandParser
+    it enables us to define/retrieve
+      cacheKey - a unique key that corresponds to this command and its arguments
+      redisKeys - an array of redis keys as strings that if the key is modified, will cause redis to invalidate this result when cached
+  2. check if cacheKey is in our cache
+    2b1. if its a value cacheEntry - return it
+    2b2. if it's a promise cache entry - wait on promise and then go to 3c.
+  3. if cacheEntry is not in cache
+    3a. send the command save the promise into a a cacheEntry and then wait on result
+    3b. transform reply (if required) based on transformReply
+    3b. check the cacheEntry is still valid - in cache and hasn't been deleted)
+    3c. if valid - overwrite with value entry
+  4. return previously non cached result
+  */
+  override async handleCache(
+    client: CachingClient,
+    parser: BasicCommandParser,
+    fn: CmdFunc,
+    transformReply?: TransformReply,
+    typeMapping?: TypeMapping
+  ) {
+    let reply: ReplyUnion;
+
+    const cacheKey = generateCacheKey(parser.redisArgs);
+
+    // "2"
+    let cacheEntry = this.get(cacheKey);
+    if (cacheEntry) {
+      // If instanceof is "too slow", can add a "type" and then use an "as" cast to call proper getters.
+      if (cacheEntry instanceof ClientSideCacheEntryValue) { // "2b1"
+        this.#statsCounter.recordHits(1);
+
+        return structuredClone(cacheEntry.value);
+      } else if (cacheEntry instanceof ClientSideCacheEntryPromise) { // 2b2
+        // This counts as a miss since the value hasn't been fully loaded yet.
+        this.#statsCounter.recordMisses(1);
+        reply = await cacheEntry.promise;
+      } else {
+        throw new Error("unknown cache entry type");
+      }
+    } else { // 3/3a
+      this.#statsCounter.recordMisses(1);
+
+      const startTime = performance.now();
+      const promise = fn();
+
+      cacheEntry = this.createPromiseEntry(client, promise);
+      this.set(cacheKey, cacheEntry, parser.keys);
+
+      try {
+        reply = await promise;
+        const loadTime = performance.now() - startTime;
+        this.#statsCounter.recordLoadSuccess(loadTime);
+      } catch (err) {
+        const loadTime = performance.now() - startTime;
+        this.#statsCounter.recordLoadFailure(loadTime);
+
+        if (cacheEntry.validate()) {
+          this.delete(cacheKey!);
+        }
+
+        throw err;
+      }
+    }
+
+    // 3b
+    let val;
+    if (transformReply) {
+      val = transformReply(reply, parser.preserve, typeMapping);
+    } else {
+      val = reply;
+    }
+
+    // 3c
+    if (cacheEntry.validate()) { // revalidating promise entry (dont save value, if promise entry has been invalidated)
+      // 3d
+      cacheEntry = this.createValueEntry(client, val);
+      this.set(cacheKey, cacheEntry, parser.keys);
+      this.emit("cached-key", cacheKey);
+    } else {
+      //   cache entry for key got invalidated between execution and saving, so not saving
+    }
+
+    return structuredClone(val);
+  }
+
+  override trackingOn() {
+    return ['CLIENT', 'TRACKING', 'ON'];
+  }
+
+  override invalidate(key: RedisArgument | null) {
+    if (key === null) {
+      this.clear(false);
+      this.emit("invalidate", key);
+
+      return;
+    }
+
+    const keySet = this.#keyToCacheKeySetMap.get(key.toString());
+    if (keySet) {
+      for (const cacheKey of keySet) {
+        const entry = this.#cacheKeyToEntryMap.get(cacheKey);
+        if (entry) {
+          entry.invalidate();
+        }
+        this.#cacheKeyToEntryMap.delete(cacheKey);
+      }
+      this.#keyToCacheKeySetMap.delete(key.toString());
+    }
+
+    this.emit('invalidate', key);
+  }
+
+  override clear(resetStats = true) {
+    const oldSize = this.#cacheKeyToEntryMap.size;
+    this.#cacheKeyToEntryMap.clear();
+    this.#keyToCacheKeySetMap.clear();
+
+    if (resetStats) {
+      if (!(this.#statsCounter instanceof DisabledStatsCounter)) {
+        this.#statsCounter = DefaultStatsCounter.create();
+      }
+    } else {
+      // If old entries were evicted due to clear, record them as evictions
+      if (oldSize > 0) {
+        this.#statsCounter.recordEvictions(oldSize);
+      }
+    }
+  }
+
+  get(cacheKey: string) {
+    const val = this.#cacheKeyToEntryMap.get(cacheKey);
+
+    if (val && !val.validate()) {
+      this.delete(cacheKey);
+      this.#statsCounter.recordEvictions(1);
+      this.emit("cache-evict", cacheKey);
+
+      return undefined;
+    }
+
+    if (val !== undefined && this.lru) {
+      this.#cacheKeyToEntryMap.delete(cacheKey);
+      this.#cacheKeyToEntryMap.set(cacheKey, val);
+    }
+
+    return val;
+  }
+
+  delete(cacheKey: string) {
+    const entry = this.#cacheKeyToEntryMap.get(cacheKey);
+    if (entry) {
+      entry.invalidate();
+      this.#cacheKeyToEntryMap.delete(cacheKey);
+    }
+  }
+
+  has(cacheKey: string) {
+    return this.#cacheKeyToEntryMap.has(cacheKey);
+  }
+
+  set(cacheKey: string, cacheEntry: ClientSideCacheEntry, keys: Array<RedisArgument>) {
+    let count = this.#cacheKeyToEntryMap.size;
+    const oldEntry = this.#cacheKeyToEntryMap.get(cacheKey);
+
+    if (oldEntry) {
+      count--; // overwriting, so not incrementig
+      oldEntry.invalidate();
+    }
+
+    if (this.maxEntries > 0 && count >= this.maxEntries) {
+      this.deleteOldest();
+      this.#statsCounter.recordEvictions(1);
+    }
+
+    this.#cacheKeyToEntryMap.set(cacheKey, cacheEntry);
+
+    for (const key of keys) {
+      if (!this.#keyToCacheKeySetMap.has(key.toString())) {
+        this.#keyToCacheKeySetMap.set(key.toString(), new Set<string>());
+      }
+
+      const cacheKeySet = this.#keyToCacheKeySetMap.get(key.toString());
+      cacheKeySet!.add(cacheKey);
+    }
+  }
+
+  size() {
+    return this.#cacheKeyToEntryMap.size;
+  }
+
+  createValueEntry(client: CachingClient, value: any): ClientSideCacheEntryValue {
+    return new ClientSideCacheEntryValue(this.ttl, value);
+  }
+
+  createPromiseEntry(client: CachingClient, sendCommandPromise: Promise<ReplyUnion>): ClientSideCacheEntryPromise {
+    return new ClientSideCacheEntryPromise(this.ttl, sendCommandPromise);
+  }
+
+  override stats(): CacheStats {
+    return this.#statsCounter.snapshot();
+  }
+
+  override onError(): void {
+    this.clear();
+  }
+
+  override onClose() {
+    this.clear();
+  }
+
+  /**
+   * @internal
+   */
+  deleteOldest() {
+    const it = this.#cacheKeyToEntryMap[Symbol.iterator]();
+    const n = it.next();
+    if (!n.done) {
+      const key = n.value[0];
+      const entry = this.#cacheKeyToEntryMap.get(key);
+      if (entry) {
+        entry.invalidate();
+      }
+      this.#cacheKeyToEntryMap.delete(key);
+    }
+  }
+
+  /**
+   * Get cache entries for debugging
+   * @internal
+   */
+  entryEntries(): IterableIterator<[string, ClientSideCacheEntry]> {
+    return this.#cacheKeyToEntryMap.entries();
+  }
+
+  /**
+   * Get key set entries for debugging
+   * @internal
+   */
+  keySetEntries(): IterableIterator<[string, Set<string>]> {
+    return this.#keyToCacheKeySetMap.entries();
+  }
+}
+
+export abstract class PooledClientSideCacheProvider extends BasicClientSideCache {
+  #disabled = false;
+
+  disable(): void {
+    this.#disabled = true;
+  }
+
+  enable(): void {
+    this.#disabled = false;
+  }
+
+  override get(cacheKey: string): ClientSideCacheEntry | undefined {
+    if (this.#disabled) {
+      return undefined;
+    }
+
+    return super.get(cacheKey);
+  }
+
+  override has(cacheKey: string): boolean {
+    if (this.#disabled) {
+      return false;
+    }
+
+    return super.has(cacheKey);
+  }
+
+  onPoolClose(): void {
+    this.clear();
+  }
+}
+
+export class BasicPooledClientSideCache extends PooledClientSideCacheProvider {
+  override onError() {
+    this.clear(false);
+  }
+
+  override onClose() {
+    this.clear(false);
+  }
+}
+
+class PooledClientSideCacheEntryValue extends ClientSideCacheEntryValue {
+  #creator: CacheCreator;
+
+  constructor(ttl: number, creator: CacheCreator, value: any) {
+    super(ttl, value);
+
+    this.#creator = creator;
+  }
+
+  override validate(): boolean {
+    let ret = super.validate();
+    if (this.#creator) {
+      ret = ret && this.#creator.client.isReady && this.#creator.client.socketEpoch == this.#creator.epoch
+    }
+
+    return ret;
+  }
+}
+
+class PooledClientSideCacheEntryPromise extends ClientSideCacheEntryPromise {
+  #creator: CacheCreator;
+
+  constructor(ttl: number, creator: CacheCreator, sendCommandPromise: Promise<ReplyUnion>) {
+    super(ttl, sendCommandPromise);
+
+    this.#creator = creator;
+  }
+
+  override validate(): boolean {
+    let ret = super.validate();
+
+    return ret && this.#creator.client.isReady && this.#creator.client.socketEpoch == this.#creator.epoch
+  }
+}
+
+export class PooledNoRedirectClientSideCache extends BasicPooledClientSideCache {
+  override createValueEntry(client: CachingClient, value: any): ClientSideCacheEntryValue {
+    const creator = {
+      epoch: client.socketEpoch,
+      client: client
+    };
+
+    return new PooledClientSideCacheEntryValue(this.ttl, creator, value);
+  }
+
+  override createPromiseEntry(client: CachingClient, sendCommandPromise: Promise<ReplyUnion>): ClientSideCacheEntryPromise {
+    const creator = {
+      epoch: client.socketEpoch,
+      client: client
+    };
+
+    return new PooledClientSideCacheEntryPromise(this.ttl, creator, sendCommandPromise);
+  }
+
+  override onError() { }
+
+  override onClose() { }
+}
diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts
index 7fffed86580..52a07a7e3b5 100644
--- a/packages/client/lib/client/commands-queue.ts
+++ b/packages/client/lib/client/commands-queue.ts
@@ -1,263 +1,483 @@
-import * as LinkedList from 'yallist';
-import { AbortError, ErrorReply } from '../errors';
-import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
-import RESP2Decoder from './RESP2/decoder';
-import encodeCommand from './RESP2/encoder';
+import { SinglyLinkedList, DoublyLinkedNode, DoublyLinkedList } from './linked-list';
+import encodeCommand from '../RESP/encoder';
+import { Decoder, PUSH_TYPE_MAPPING, RESP_TYPES } from '../RESP/decoder';
+import { TypeMapping, ReplyUnion, RespVersions, RedisArgument } from '../RESP/types';
 import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub';
-
-export interface QueueCommandOptions {
-    asap?: boolean;
-    chainId?: symbol;
-    signal?: AbortSignal;
-    returnBuffers?: boolean;
+import { AbortError, ErrorReply, TimeoutError } from '../errors';
+import { MonitorCallback } from '.';
+
+export interface CommandOptions<T = TypeMapping> {
+  chainId?: symbol;
+  asap?: boolean;
+  abortSignal?: AbortSignal;
+  /**
+   * Maps between RESP and JavaScript types
+   */
+  typeMapping?: T;
+  /**
+   * Timeout for the command in milliseconds
+   */
+  timeout?: number;
 }
 
-export interface CommandWaitingToBeSent extends CommandWaitingForReply {
-    args: RedisCommandArguments;
-    chainId?: symbol;
-    abort?: {
-        signal: AbortSignal;
-        listener(): void;
-    };
+export interface CommandToWrite extends CommandWaitingForReply {
+  args: ReadonlyArray<RedisArgument>;
+  chainId: symbol | undefined;
+  abort: {
+    signal: AbortSignal;
+    listener: () => unknown;
+  } | undefined;
+  timeout: {
+    signal: AbortSignal;
+    listener: () => unknown;
+  } | undefined;
 }
 
 interface CommandWaitingForReply {
-    resolve(reply?: unknown): void;
-    reject(err: unknown): void;
-    channelsCounter?: number;
-    returnBuffers?: boolean;
+  resolve(reply?: unknown): void;
+  reject(err: unknown): void;
+  channelsCounter: number | undefined;
+  typeMapping: TypeMapping | undefined;
 }
 
-const PONG = Buffer.from('pong');
-
 export type OnShardedChannelMoved = (channel: string, listeners: ChannelListeners) => void;
 
-export default class RedisCommandsQueue {
-    static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
-        while (queue.length) {
-            queue.shift()!.reject(err);
-        }
-    }
-
-    readonly #maxLength: number | null | undefined;
-    readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
-    readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
-    readonly #onShardedChannelMoved: OnShardedChannelMoved;
+const PONG = Buffer.from('pong'),
+  RESET = Buffer.from('RESET');
 
-    readonly #pubSub = new PubSub();
+const RESP2_PUSH_TYPE_MAPPING = {
+  ...PUSH_TYPE_MAPPING,
+  [RESP_TYPES.SIMPLE_STRING]: Buffer
+};
 
-    get isPubSubActive() {
-        return this.#pubSub.isActive;
+export default class RedisCommandsQueue {
+  readonly #respVersion;
+  readonly #maxLength;
+  readonly #toWrite = new DoublyLinkedList<CommandToWrite>();
+  readonly #waitingForReply = new SinglyLinkedList<CommandWaitingForReply>();
+  readonly #onShardedChannelMoved;
+  #chainInExecution: symbol | undefined;
+  readonly decoder;
+  readonly #pubSub = new PubSub();
+
+  get isPubSubActive() {
+    return this.#pubSub.isActive;
+  }
+
+  #invalidateCallback?: (key: RedisArgument | null) => unknown;
+
+  constructor(
+    respVersion: RespVersions,
+    maxLength: number | null | undefined,
+    onShardedChannelMoved: OnShardedChannelMoved
+  ) {
+    this.#respVersion = respVersion;
+    this.#maxLength = maxLength;
+    this.#onShardedChannelMoved = onShardedChannelMoved;
+    this.decoder = this.#initiateDecoder();
+  }
+
+  #onReply(reply: ReplyUnion) {
+    this.#waitingForReply.shift()!.resolve(reply);
+  }
+
+  #onErrorReply(err: ErrorReply) {
+    this.#waitingForReply.shift()!.reject(err);
+  }
+
+  #onPush(push: Array<any>) {
+    // TODO: type
+    if (this.#pubSub.handleMessageReply(push)) return true;
+
+    const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(push);
+    if (isShardedUnsubscribe && !this.#waitingForReply.length) {
+      const channel = push[1].toString();
+      this.#onShardedChannelMoved(
+        channel,
+        this.#pubSub.removeShardedListeners(channel)
+      );
+      return true;
+    } else if (isShardedUnsubscribe || PubSub.isStatusReply(push)) {
+      const head = this.#waitingForReply.head!.value;
+      if (
+        (Number.isNaN(head.channelsCounter!) && push[2] === 0) ||
+        --head.channelsCounter! === 0
+      ) {
+        this.#waitingForReply.shift()!.resolve();
+      }
+      return true;
     }
-
-    #chainInExecution: symbol | undefined;
-
-    #decoder = new RESP2Decoder({
-        returnStringsAsBuffers: () => {
-            return !!this.#waitingForReply.head?.value.returnBuffers ||
-                this.#pubSub.isActive;
-        },
-        onReply: reply => {
-            if (this.#pubSub.isActive && Array.isArray(reply)) {
-                if (this.#pubSub.handleMessageReply(reply as Array<Buffer>)) return;
-                
-                const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(reply as Array<Buffer>);
-                if (isShardedUnsubscribe && !this.#waitingForReply.length) {
-                    const channel = (reply[1] as Buffer).toString();
-                    this.#onShardedChannelMoved(
-                        channel,
-                        this.#pubSub.removeShardedListeners(channel)
-                    );
-                    return;
-                } else if (isShardedUnsubscribe || PubSub.isStatusReply(reply as Array<Buffer>)) {
-                    const head = this.#waitingForReply.head!.value;
-                    if (
-                        (Number.isNaN(head.channelsCounter!) && reply[2] === 0) ||
-                        --head.channelsCounter! === 0
-                    ) {
-                        this.#waitingForReply.shift()!.resolve();
-                    }
-                    return;
+  }
+
+  #getTypeMapping() {
+    return this.#waitingForReply.head!.value.typeMapping ?? {};
+  }
+
+  #initiateDecoder() {
+    return new Decoder({
+      onReply: reply => this.#onReply(reply),
+      onErrorReply: err => this.#onErrorReply(err),
+      //TODO: we can shave off a few cycles by not adding onPush handler at all if CSC is not used
+      onPush: push => {
+        if (!this.#onPush(push)) {
+          // currently only supporting "invalidate" over RESP3 push messages
+          switch (push[0].toString()) {
+            case "invalidate": {
+              if (this.#invalidateCallback) {
+                if (push[1] !== null) {
+                  for (const key of push[1]) {
+                    this.#invalidateCallback(key);
+                  }
+                } else {
+                  this.#invalidateCallback(null);
                 }
-                if (PONG.equals(reply[0] as Buffer)) {
-                    const { resolve, returnBuffers } = this.#waitingForReply.shift()!,
-                        buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
-                    resolve(returnBuffers ? buffer : buffer.toString());
-                    return;
-                }
-            }
-            
-            const { resolve, reject } = this.#waitingForReply.shift()!;
-            if (reply instanceof ErrorReply) {
-                reject(reply);
-            } else {
-                resolve(reply);
+              }
+              break;
             }
+          }
         }
+      },
+      getTypeMapping: () => this.#getTypeMapping()
     });
-
-    constructor(
-        maxLength: number | null | undefined,
-        onShardedChannelMoved: OnShardedChannelMoved
-    ) {
-        this.#maxLength = maxLength;
-        this.#onShardedChannelMoved = onShardedChannelMoved;
+  }
+
+  setInvalidateCallback(callback?: (key: RedisArgument | null) => unknown) {
+    this.#invalidateCallback = callback;
+  }
+
+  addCommand<T>(
+    args: ReadonlyArray<RedisArgument>,
+    options?: CommandOptions
+  ): Promise<T> {
+    if (this.#maxLength && this.#toWrite.length + this.#waitingForReply.length >= this.#maxLength) {
+      return Promise.reject(new Error('The queue is full'));
+    } else if (options?.abortSignal?.aborted) {
+      return Promise.reject(new AbortError());
     }
 
-    addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions): Promise<T> {
-        if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
-            return Promise.reject(new Error('The queue is full'));
-        } else if (options?.signal?.aborted) {
-            return Promise.reject(new AbortError());
-        }
+    return new Promise((resolve, reject) => {
+      let node: DoublyLinkedNode<CommandToWrite>;
+      const value: CommandToWrite = {
+        args,
+        chainId: options?.chainId,
+        abort: undefined,
+        timeout: undefined,
+        resolve,
+        reject,
+        channelsCounter: undefined,
+        typeMapping: options?.typeMapping
+      };
+
+      const timeout = options?.timeout;
+      if (timeout) {
+        const signal = AbortSignal.timeout(timeout);
+        value.timeout = {
+          signal,
+          listener: () => {
+            this.#toWrite.remove(node);
+            value.reject(new TimeoutError());
+          }
+        };
+        signal.addEventListener('abort', value.timeout.listener, { once: true });
+      }
+
+      const signal = options?.abortSignal;
+      if (signal) {
+        value.abort = {
+          signal,
+          listener: () => {
+            this.#toWrite.remove(node);
+            value.reject(new AbortError());
+          }
+        };
+        signal.addEventListener('abort', value.abort.listener, { once: true });
+      }
+
+      node = this.#toWrite.add(value, options?.asap);
+    });
+  }
+
+  #addPubSubCommand(command: PubSubCommand, asap = false, chainId?: symbol) {
+    return new Promise<void>((resolve, reject) => {
+      this.#toWrite.add({
+        args: command.args,
+        chainId,
+        abort: undefined,
+        timeout: undefined,
+        resolve() {
+          command.resolve();
+          resolve();
+        },
+        reject(err) {
+          command.reject?.();
+          reject(err);
+        },
+        channelsCounter: command.channelsCounter,
+        typeMapping: PUSH_TYPE_MAPPING
+      }, asap);
+    });
+  }
 
-        return new Promise((resolve, reject) => {
-            const node = new LinkedList.Node<CommandWaitingToBeSent>({
-                args,
-                chainId: options?.chainId,
-                returnBuffers: options?.returnBuffers,
-                resolve,
-                reject
-            });
-
-            if (options?.signal) {
-                const listener = () => {
-                    this.#waitingToBeSent.removeNode(node);
-                    node.value.reject(new AbortError());
-                };
-                node.value.abort = {
-                    signal: options.signal,
-                    listener
-                };
-                // AbortSignal type is incorrent
-                (options.signal as any).addEventListener('abort', listener, {
-                    once: true
-                });
-            }
+  #setupPubSubHandler() {
+    // RESP3 uses `onPush` to handle PubSub, so no need to modify `onReply`
+    if (this.#respVersion !== 2) return;
 
-            if (options?.asap) {
-                this.#waitingToBeSent.unshiftNode(node);
-            } else {
-                this.#waitingToBeSent.pushNode(node);
-            }
-        });
-    }
+    this.decoder.onReply = (reply => {
+      if (Array.isArray(reply)) {
+        if (this.#onPush(reply)) return;
 
-    subscribe<T extends boolean>(
-        type: PubSubType,
-        channels: string | Array<string>,
-        listener: PubSubListener<T>,
-        returnBuffers?: T
-    ) {
-        return this.#pushPubSubCommand(
-            this.#pubSub.subscribe(type, channels, listener, returnBuffers)
-        );
-    }
+        if (PONG.equals(reply[0] as Buffer)) {
+          const { resolve, typeMapping } = this.#waitingForReply.shift()!,
+            buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
+          resolve(typeMapping?.[RESP_TYPES.SIMPLE_STRING] === Buffer ? buffer : buffer.toString());
+          return;
+        }
+      }
+
+      return this.#onReply(reply);
+    }) as Decoder['onReply'];
+    this.decoder.getTypeMapping = () => RESP2_PUSH_TYPE_MAPPING;
+  }
+
+  subscribe<T extends boolean>(
+    type: PubSubType,
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    returnBuffers?: T
+  ) {
+    const command = this.#pubSub.subscribe(type, channels, listener, returnBuffers);
+    if (!command) return;
+
+    this.#setupPubSubHandler();
+    return this.#addPubSubCommand(command);
+  }
+
+  #resetDecoderCallbacks() {
+    this.decoder.onReply = (reply => this.#onReply(reply)) as Decoder['onReply'];
+    this.decoder.getTypeMapping = () => this.#getTypeMapping();
+  }
+
+  unsubscribe<T extends boolean>(
+    type: PubSubType,
+    channels?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    returnBuffers?: T
+  ) {
+    const command = this.#pubSub.unsubscribe(type, channels, listener, returnBuffers);
+    if (!command) return;
+
+    if (command && this.#respVersion === 2) {
+      // RESP2 modifies `onReply` to handle PubSub (see #setupPubSubHandler)
+      const { resolve } = command;
+      command.resolve = () => {
+        if (!this.#pubSub.isActive) {
+          this.#resetDecoderCallbacks();
+        }
 
-    unsubscribe<T extends boolean>(
-        type: PubSubType,
-        channels?: string | Array<string>,
-        listener?: PubSubListener<T>,
-        returnBuffers?: T
-    ) {
-        return this.#pushPubSubCommand(
-            this.#pubSub.unsubscribe(type, channels, listener, returnBuffers)
-        );
+        resolve();
+      };
     }
 
-    resubscribe(): Promise<any> | undefined {
-        const commands = this.#pubSub.resubscribe();
-        if (!commands.length) return;
+    return this.#addPubSubCommand(command);
+  }
+
+  resubscribe(chainId?: symbol) {
+    const commands = this.#pubSub.resubscribe();
+    if (!commands.length) return;
+
+    this.#setupPubSubHandler();
+    return Promise.all(
+      commands.map(command => this.#addPubSubCommand(command, true, chainId))
+    );
+  }
+
+  extendPubSubChannelListeners(
+    type: PubSubType,
+    channel: string,
+    listeners: ChannelListeners
+  ) {
+    const command = this.#pubSub.extendChannelListeners(type, channel, listeners);
+    if (!command) return;
+
+    this.#setupPubSubHandler();
+    return this.#addPubSubCommand(command);
+  }
+
+  extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
+    const command = this.#pubSub.extendTypeListeners(type, listeners);
+    if (!command) return;
+
+    this.#setupPubSubHandler();
+    return this.#addPubSubCommand(command);
+  }
+
+  getPubSubListeners(type: PubSubType) {
+    return this.#pubSub.listeners[type];
+  }
+
+  monitor(callback: MonitorCallback, options?: CommandOptions) {
+    return new Promise<void>((resolve, reject) => {
+      const typeMapping = options?.typeMapping ?? {};
+      this.#toWrite.add({
+        args: ['MONITOR'],
+        chainId: options?.chainId,
+        abort: undefined,
+        timeout: undefined,
+        // using `resolve` instead of using `.then`/`await` to make sure it'll be called before processing the next reply
+        resolve: () => {
+          // after running `MONITOR` only `MONITOR` and `RESET` replies are expected
+          // any other command should cause an error
+
+          // if `RESET` already overrides `onReply`, set monitor as it's fallback
+          if (this.#resetFallbackOnReply) {
+            this.#resetFallbackOnReply = callback;
+          } else {
+            this.decoder.onReply = callback;
+          }
+
+          this.decoder.getTypeMapping = () => typeMapping;
+          resolve();
+        },
+        reject,
+        channelsCounter: undefined,
+        typeMapping
+      }, options?.asap);
+    });
+  }
+
+  resetDecoder() {
+    this.#resetDecoderCallbacks();
+    this.decoder.reset();
+  }
+
+  #resetFallbackOnReply?: Decoder['onReply'];
+
+  async reset<T extends TypeMapping>(chainId: symbol, typeMapping?: T) {
+    return new Promise((resolve, reject) => {
+      // overriding onReply to handle `RESET` while in `MONITOR` or PubSub mode
+      this.#resetFallbackOnReply = this.decoder.onReply;
+      this.decoder.onReply = (reply => {
+        if (
+          (typeof reply === 'string' && reply === 'RESET') ||
+          (reply instanceof Buffer && RESET.equals(reply))
+        ) {
+          this.#resetDecoderCallbacks();
+          this.#resetFallbackOnReply = undefined;
+          this.#pubSub.reset();
+
+          this.#waitingForReply.shift()!.resolve(reply);
+          return;
+        }
 
-        return Promise.all(
-            commands.map(command => this.#pushPubSubCommand(command))
-        );
+        this.#resetFallbackOnReply!(reply);
+      }) as Decoder['onReply'];
+
+      this.#toWrite.push({
+        args: ['RESET'],
+        chainId,
+        abort: undefined,
+        timeout: undefined,
+        resolve,
+        reject,
+        channelsCounter: undefined,
+        typeMapping
+      });
+    });
+  }
+
+  isWaitingToWrite() {
+    return this.#toWrite.length > 0;
+  }
+
+  *commandsToWrite() {
+    let toSend = this.#toWrite.shift();
+    while (toSend) {
+      let encoded: ReadonlyArray<RedisArgument>
+      try {
+        encoded = encodeCommand(toSend.args);
+      } catch (err) {
+        toSend.reject(err);
+        toSend = this.#toWrite.shift();
+        continue;
+      }
+
+      // TODO reuse `toSend` or create new object?
+      (toSend as any).args = undefined;
+      if (toSend.abort) {
+        RedisCommandsQueue.#removeAbortListener(toSend);
+        toSend.abort = undefined;
+      }
+      if (toSend.timeout) {
+        RedisCommandsQueue.#removeTimeoutListener(toSend);
+        toSend.timeout = undefined;
+      }
+      this.#chainInExecution = toSend.chainId;
+      toSend.chainId = undefined;
+      this.#waitingForReply.push(toSend);
+
+      yield encoded;
+      toSend = this.#toWrite.shift();
     }
+  }
 
-    extendPubSubChannelListeners(
-        type: PubSubType,
-        channel: string,
-        listeners: ChannelListeners
-    ) {
-        return this.#pushPubSubCommand(
-            this.#pubSub.extendChannelListeners(type, channel, listeners)
-        );
+  #flushWaitingForReply(err: Error): void {
+    for (const node of this.#waitingForReply) {
+      node.reject(err);
     }
+    this.#waitingForReply.reset();
+  }
 
-    extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
-        return this.#pushPubSubCommand(
-            this.#pubSub.extendTypeListeners(type, listeners)
-        );
-    }
+  static #removeAbortListener(command: CommandToWrite) {
+    command.abort!.signal.removeEventListener('abort', command.abort!.listener);
+  }
 
-    getPubSubListeners(type: PubSubType) {
-        return this.#pubSub.getTypeListeners(type);
-    }
+  static #removeTimeoutListener(command: CommandToWrite) {
+    command.timeout!.signal.removeEventListener('abort', command.timeout!.listener);
+  }
 
-    #pushPubSubCommand(command: PubSubCommand) {
-        if (command === undefined) return;
-
-        return new Promise<void>((resolve, reject) => {
-            this.#waitingToBeSent.push({
-                args: command.args,
-                channelsCounter: command.channelsCounter,
-                returnBuffers: true,
-                resolve: () => {
-                    command.resolve();
-                    resolve();
-                },
-                reject: err => {
-                    command.reject?.();
-                    reject(err);
-                }
-            });
-        });
+  static #flushToWrite(toBeSent: CommandToWrite, err: Error) {
+    if (toBeSent.abort) {
+      RedisCommandsQueue.#removeAbortListener(toBeSent);
     }
-
-    getCommandToSend(): RedisCommandArguments | undefined {
-        const toSend = this.#waitingToBeSent.shift();
-        if (!toSend) return;
-
-        let encoded: RedisCommandArguments;
-        try {
-            encoded = encodeCommand(toSend.args);
-        } catch (err) {
-            toSend.reject(err);
-            return;
-        }
-
-        this.#waitingForReply.push({
-            resolve: toSend.resolve,
-            reject: toSend.reject,
-            channelsCounter: toSend.channelsCounter,
-            returnBuffers: toSend.returnBuffers
-        });
-        this.#chainInExecution = toSend.chainId;
-        return encoded;
+    if (toBeSent.timeout) {
+      RedisCommandsQueue.#removeTimeoutListener(toBeSent);
     }
 
-    onReplyChunk(chunk: Buffer): void {
-        this.#decoder.write(chunk);
-    }
+    toBeSent.reject(err);
+  }
 
-    flushWaitingForReply(err: Error): void {
-        this.#decoder.reset();
-        this.#pubSub.reset();
-        RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
+  flushWaitingForReply(err: Error): void {
+    this.resetDecoder();
+    this.#pubSub.reset();
 
-        if (!this.#chainInExecution) return;
+    this.#flushWaitingForReply(err);
 
-        while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
-            this.#waitingToBeSent.shift();
-        }
+    if (!this.#chainInExecution) return;
 
-        this.#chainInExecution = undefined;
+    while (this.#toWrite.head?.value.chainId === this.#chainInExecution) {
+      RedisCommandsQueue.#flushToWrite(
+        this.#toWrite.shift()!,
+        err
+      );
     }
 
-    flushAll(err: Error): void {
-        this.#decoder.reset();
-        this.#pubSub.reset();
-        RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
-        RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
+    this.#chainInExecution = undefined;
+  }
+
+  flushAll(err: Error): void {
+    this.resetDecoder();
+    this.#pubSub.reset();
+    this.#flushWaitingForReply(err);
+    for (const node of this.#toWrite) {
+      RedisCommandsQueue.#flushToWrite(node, err);
     }
+    this.#toWrite.reset();
+  }
+
+  isEmpty() {
+    return (
+      this.#toWrite.length === 0 &&
+      this.#waitingForReply.length === 0
+    );
+  }
 }
diff --git a/packages/client/lib/client/commands.ts b/packages/client/lib/client/commands.ts
deleted file mode 100644
index 76ae5d73735..00000000000
--- a/packages/client/lib/client/commands.ts
+++ /dev/null
@@ -1,374 +0,0 @@
-import CLUSTER_COMMANDS from '../cluster/commands';
-import * as ACL_CAT from '../commands/ACL_CAT';
-import * as ACL_DELUSER from '../commands/ACL_DELUSER';
-import * as ACL_DRYRUN from '../commands/ACL_DRYRUN';
-import * as ACL_GENPASS from '../commands/ACL_GENPASS';
-import * as ACL_GETUSER from '../commands/ACL_GETUSER';
-import * as ACL_LIST from '../commands/ACL_LIST';
-import * as ACL_LOAD from '../commands/ACL_LOAD';
-import * as ACL_LOG_RESET from '../commands/ACL_LOG_RESET';
-import * as ACL_LOG from '../commands/ACL_LOG';
-import * as ACL_SAVE from '../commands/ACL_SAVE';
-import * as ACL_SETUSER from '../commands/ACL_SETUSER';
-import * as ACL_USERS from '../commands/ACL_USERS';
-import * as ACL_WHOAMI from '../commands/ACL_WHOAMI';
-import * as ASKING from '../commands/ASKING';
-import * as AUTH from '../commands/AUTH';
-import * as BGREWRITEAOF from '../commands/BGREWRITEAOF';
-import * as BGSAVE from '../commands/BGSAVE';
-import * as CLIENT_CACHING from '../commands/CLIENT_CACHING';
-import * as CLIENT_GETNAME from '../commands/CLIENT_GETNAME';
-import * as CLIENT_GETREDIR from '../commands/CLIENT_GETREDIR';
-import * as CLIENT_ID from '../commands/CLIENT_ID';
-import * as CLIENT_KILL from '../commands/CLIENT_KILL';
-import * as CLIENT_LIST from '../commands/CLIENT_LIST';
-import * as CLIENT_NO_EVICT from '../commands/CLIENT_NO-EVICT';
-import * as CLIENT_NO_TOUCH from '../commands/CLIENT_NO-TOUCH';
-import * as CLIENT_PAUSE from '../commands/CLIENT_PAUSE';
-import * as CLIENT_SETNAME from '../commands/CLIENT_SETNAME';
-import * as CLIENT_TRACKING from '../commands/CLIENT_TRACKING';
-import * as CLIENT_TRACKINGINFO from '../commands/CLIENT_TRACKINGINFO';
-import * as CLIENT_UNPAUSE from '../commands/CLIENT_UNPAUSE';
-import * as CLIENT_INFO from '../commands/CLIENT_INFO';
-import * as CLUSTER_ADDSLOTS from '../commands/CLUSTER_ADDSLOTS';
-import * as CLUSTER_ADDSLOTSRANGE from '../commands/CLUSTER_ADDSLOTSRANGE';
-import * as CLUSTER_BUMPEPOCH from '../commands/CLUSTER_BUMPEPOCH';
-import * as CLUSTER_COUNT_FAILURE_REPORTS from '../commands/CLUSTER_COUNT-FAILURE-REPORTS';
-import * as CLUSTER_COUNTKEYSINSLOT from '../commands/CLUSTER_COUNTKEYSINSLOT';
-import * as CLUSTER_DELSLOTS from '../commands/CLUSTER_DELSLOTS';
-import * as CLUSTER_DELSLOTSRANGE from '../commands/CLUSTER_DELSLOTSRANGE';
-import * as CLUSTER_FAILOVER from '../commands/CLUSTER_FAILOVER';
-import * as CLUSTER_FLUSHSLOTS from '../commands/CLUSTER_FLUSHSLOTS';
-import * as CLUSTER_FORGET from '../commands/CLUSTER_FORGET';
-import * as CLUSTER_GETKEYSINSLOT from '../commands/CLUSTER_GETKEYSINSLOT';
-import * as CLUSTER_INFO from '../commands/CLUSTER_INFO';
-import * as CLUSTER_KEYSLOT from '../commands/CLUSTER_KEYSLOT';
-import * as CLUSTER_LINKS from '../commands/CLUSTER_LINKS';
-import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
-import * as CLUSTER_MYID from '../commands/CLUSTER_MYID';
-import * as CLUSTER_MYSHARDID from '../commands/CLUSTER_MYSHARDID';
-import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
-import * as CLUSTER_REPLICAS from '../commands/CLUSTER_REPLICAS';
-import * as CLUSTER_REPLICATE from '../commands/CLUSTER_REPLICATE';
-import * as CLUSTER_RESET from '../commands/CLUSTER_RESET';
-import * as CLUSTER_SAVECONFIG from '../commands/CLUSTER_SAVECONFIG';
-import * as CLUSTER_SET_CONFIG_EPOCH from '../commands/CLUSTER_SET-CONFIG-EPOCH';
-import * as CLUSTER_SETSLOT from '../commands/CLUSTER_SETSLOT';
-import * as CLUSTER_SLOTS from '../commands/CLUSTER_SLOTS';
-import * as COMMAND_COUNT from '../commands/COMMAND_COUNT';
-import * as COMMAND_GETKEYS from '../commands/COMMAND_GETKEYS';
-import * as COMMAND_GETKEYSANDFLAGS from '../commands/COMMAND_GETKEYSANDFLAGS';
-import * as COMMAND_INFO from '../commands/COMMAND_INFO';
-import * as COMMAND_LIST from '../commands/COMMAND_LIST';
-import * as COMMAND from '../commands/COMMAND';
-import * as CONFIG_GET from '../commands/CONFIG_GET';
-import * as CONFIG_RESETASTAT from '../commands/CONFIG_RESETSTAT';
-import * as CONFIG_REWRITE from '../commands/CONFIG_REWRITE';
-import * as CONFIG_SET from '../commands/CONFIG_SET';
-import * as DBSIZE from '../commands/DBSIZE';
-import * as DISCARD from '../commands/DISCARD';
-import * as ECHO from '../commands/ECHO';
-import * as FAILOVER from '../commands/FAILOVER';
-import * as FLUSHALL from '../commands/FLUSHALL';
-import * as FLUSHDB from '../commands/FLUSHDB';
-import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE';
-import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP';
-import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH';
-import * as FUNCTION_KILL from '../commands/FUNCTION_KILL';
-import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE';
-import * as FUNCTION_LIST from '../commands/FUNCTION_LIST';
-import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD';
-import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE';
-import * as FUNCTION_STATS from '../commands/FUNCTION_STATS';
-import * as HELLO from '../commands/HELLO';
-import * as INFO from '../commands/INFO';
-import * as KEYS from '../commands/KEYS';
-import * as LASTSAVE from '../commands/LASTSAVE';
-import * as LATENCY_DOCTOR from '../commands/LATENCY_DOCTOR';
-import * as LATENCY_GRAPH from '../commands/LATENCY_GRAPH';
-import * as LATENCY_HISTORY from '../commands/LATENCY_HISTORY';
-import * as LATENCY_LATEST from '../commands/LATENCY_LATEST';
-import * as LOLWUT from '../commands/LOLWUT';
-import * as MEMORY_DOCTOR from '../commands/MEMORY_DOCTOR';
-import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
-import * as MEMORY_PURGE from '../commands/MEMORY_PURGE';
-import * as MEMORY_STATS from '../commands/MEMORY_STATS';
-import * as MEMORY_USAGE from '../commands/MEMORY_USAGE';
-import * as MODULE_LIST from '../commands/MODULE_LIST';
-import * as MODULE_LOAD from '../commands/MODULE_LOAD';
-import * as MODULE_UNLOAD from '../commands/MODULE_UNLOAD';
-import * as MOVE from '../commands/MOVE';
-import * as PING from '../commands/PING';
-import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
-import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
-import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
-import * as PUBSUB_SHARDCHANNELS from '../commands/PUBSUB_SHARDCHANNELS';
-import * as PUBSUB_SHARDNUMSUB from '../commands/PUBSUB_SHARDNUMSUB';
-import * as RANDOMKEY from '../commands/RANDOMKEY';
-import * as READONLY from '../commands/READONLY';
-import * as READWRITE from '../commands/READWRITE';
-import * as REPLICAOF from '../commands/REPLICAOF';
-import * as RESTORE_ASKING from '../commands/RESTORE-ASKING';
-import * as ROLE from '../commands/ROLE';
-import * as SAVE from '../commands/SAVE';
-import * as SCAN from '../commands/SCAN';
-import * as SCRIPT_DEBUG from '../commands/SCRIPT_DEBUG';
-import * as SCRIPT_EXISTS from '../commands/SCRIPT_EXISTS';
-import * as SCRIPT_FLUSH from '../commands/SCRIPT_FLUSH';
-import * as SCRIPT_KILL from '../commands/SCRIPT_KILL';
-import * as SCRIPT_LOAD from '../commands/SCRIPT_LOAD';
-import * as SHUTDOWN from '../commands/SHUTDOWN';
-import * as SWAPDB from '../commands/SWAPDB';
-import * as TIME from '../commands/TIME';
-import * as UNWATCH from '../commands/UNWATCH';
-import * as WAIT from '../commands/WAIT';
-
-export default {
-    ...CLUSTER_COMMANDS,
-    ACL_CAT,
-    aclCat: ACL_CAT,
-    ACL_DELUSER,
-    aclDelUser: ACL_DELUSER,
-    ACL_DRYRUN,
-    aclDryRun: ACL_DRYRUN,
-    ACL_GENPASS,
-    aclGenPass: ACL_GENPASS,
-    ACL_GETUSER,
-    aclGetUser: ACL_GETUSER,
-    ACL_LIST,
-    aclList: ACL_LIST,
-    ACL_LOAD,
-    aclLoad: ACL_LOAD,
-    ACL_LOG_RESET,
-    aclLogReset: ACL_LOG_RESET,
-    ACL_LOG,
-    aclLog: ACL_LOG,
-    ACL_SAVE,
-    aclSave: ACL_SAVE,
-    ACL_SETUSER,
-    aclSetUser: ACL_SETUSER,
-    ACL_USERS,
-    aclUsers: ACL_USERS,
-    ACL_WHOAMI,
-    aclWhoAmI: ACL_WHOAMI,
-    ASKING,
-    asking: ASKING,
-    AUTH,
-    auth: AUTH,
-    BGREWRITEAOF,
-    bgRewriteAof: BGREWRITEAOF,
-    BGSAVE,
-    bgSave: BGSAVE,
-    CLIENT_CACHING,
-    clientCaching: CLIENT_CACHING,
-    CLIENT_GETNAME,
-    clientGetName: CLIENT_GETNAME,
-    CLIENT_GETREDIR,
-    clientGetRedir: CLIENT_GETREDIR,
-    CLIENT_ID,
-    clientId: CLIENT_ID,
-    CLIENT_KILL,
-    clientKill: CLIENT_KILL,
-    'CLIENT_NO-EVICT': CLIENT_NO_EVICT,
-    clientNoEvict: CLIENT_NO_EVICT,
-    'CLIENT_NO-TOUCH': CLIENT_NO_TOUCH,
-    clientNoTouch: CLIENT_NO_TOUCH,
-    CLIENT_LIST,
-    clientList: CLIENT_LIST,
-    CLIENT_PAUSE,
-    clientPause: CLIENT_PAUSE,
-    CLIENT_SETNAME,
-    clientSetName: CLIENT_SETNAME,
-    CLIENT_TRACKING,
-    clientTracking: CLIENT_TRACKING,
-    CLIENT_TRACKINGINFO,
-    clientTrackingInfo: CLIENT_TRACKINGINFO,
-    CLIENT_UNPAUSE,
-    clientUnpause: CLIENT_UNPAUSE,
-    CLIENT_INFO,
-    clientInfo: CLIENT_INFO,
-    CLUSTER_ADDSLOTS,
-    clusterAddSlots: CLUSTER_ADDSLOTS,
-    CLUSTER_ADDSLOTSRANGE,
-    clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE,
-    CLUSTER_BUMPEPOCH,
-    clusterBumpEpoch: CLUSTER_BUMPEPOCH,
-    CLUSTER_COUNT_FAILURE_REPORTS,
-    clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS,
-    CLUSTER_COUNTKEYSINSLOT,
-    clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT,
-    CLUSTER_DELSLOTS,
-    clusterDelSlots: CLUSTER_DELSLOTS,
-    CLUSTER_DELSLOTSRANGE,
-    clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE,
-    CLUSTER_FAILOVER,
-    clusterFailover: CLUSTER_FAILOVER,
-    CLUSTER_FLUSHSLOTS,
-    clusterFlushSlots: CLUSTER_FLUSHSLOTS,
-    CLUSTER_FORGET,
-    clusterForget: CLUSTER_FORGET,
-    CLUSTER_GETKEYSINSLOT,
-    clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT,
-    CLUSTER_INFO,
-    clusterInfo: CLUSTER_INFO,
-    CLUSTER_KEYSLOT,
-    clusterKeySlot: CLUSTER_KEYSLOT,
-    CLUSTER_LINKS,
-    clusterLinks: CLUSTER_LINKS,
-    CLUSTER_MEET,
-    clusterMeet: CLUSTER_MEET,
-    CLUSTER_MYID,
-    clusterMyId: CLUSTER_MYID,
-    CLUSTER_MYSHARDID,
-    clusterMyShardId: CLUSTER_MYSHARDID,
-    CLUSTER_NODES,
-    clusterNodes: CLUSTER_NODES,
-    CLUSTER_REPLICAS,
-    clusterReplicas: CLUSTER_REPLICAS,
-    CLUSTER_REPLICATE,
-    clusterReplicate: CLUSTER_REPLICATE,
-    CLUSTER_RESET,
-    clusterReset: CLUSTER_RESET,
-    CLUSTER_SAVECONFIG,
-    clusterSaveConfig: CLUSTER_SAVECONFIG,
-    CLUSTER_SET_CONFIG_EPOCH,
-    clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH,
-    CLUSTER_SETSLOT,
-    clusterSetSlot: CLUSTER_SETSLOT,
-    CLUSTER_SLOTS,
-    clusterSlots: CLUSTER_SLOTS,
-    COMMAND_COUNT,
-    commandCount: COMMAND_COUNT,
-    COMMAND_GETKEYS,
-    commandGetKeys: COMMAND_GETKEYS,
-    COMMAND_GETKEYSANDFLAGS,
-    commandGetKeysAndFlags: COMMAND_GETKEYSANDFLAGS,
-    COMMAND_INFO,
-    commandInfo: COMMAND_INFO,
-    COMMAND_LIST,
-    commandList: COMMAND_LIST,
-    COMMAND,
-    command: COMMAND,
-    CONFIG_GET,
-    configGet: CONFIG_GET,
-    CONFIG_RESETASTAT,
-    configResetStat: CONFIG_RESETASTAT,
-    CONFIG_REWRITE,
-    configRewrite: CONFIG_REWRITE,
-    CONFIG_SET,
-    configSet: CONFIG_SET,
-    DBSIZE,
-    dbSize: DBSIZE,
-    DISCARD,
-    discard: DISCARD,
-    ECHO,
-    echo: ECHO,
-    FAILOVER,
-    failover: FAILOVER,
-    FLUSHALL,
-    flushAll: FLUSHALL,
-    FLUSHDB,
-    flushDb: FLUSHDB,
-    FUNCTION_DELETE,
-    functionDelete: FUNCTION_DELETE,
-    FUNCTION_DUMP,
-    functionDump: FUNCTION_DUMP,
-    FUNCTION_FLUSH,
-    functionFlush: FUNCTION_FLUSH,
-    FUNCTION_KILL,
-    functionKill: FUNCTION_KILL,
-    FUNCTION_LIST_WITHCODE,
-    functionListWithCode: FUNCTION_LIST_WITHCODE,
-    FUNCTION_LIST,
-    functionList: FUNCTION_LIST,
-    FUNCTION_LOAD,
-    functionLoad: FUNCTION_LOAD,
-    FUNCTION_RESTORE,
-    functionRestore: FUNCTION_RESTORE,
-    FUNCTION_STATS,
-    functionStats: FUNCTION_STATS,
-    HELLO,
-    hello: HELLO,
-    INFO,
-    info: INFO,
-    KEYS,
-    keys: KEYS,
-    LASTSAVE,
-    lastSave: LASTSAVE,
-    LATENCY_DOCTOR,
-    latencyDoctor: LATENCY_DOCTOR,
-    LATENCY_GRAPH,
-    latencyGraph: LATENCY_GRAPH,
-    LATENCY_HISTORY,
-    latencyHistory: LATENCY_HISTORY,
-    LATENCY_LATEST,
-    latencyLatest: LATENCY_LATEST,
-    LOLWUT,
-    lolwut: LOLWUT,
-    MEMORY_DOCTOR,
-    memoryDoctor: MEMORY_DOCTOR,
-    'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS,
-    memoryMallocStats: MEMORY_MALLOC_STATS,
-    MEMORY_PURGE,
-    memoryPurge: MEMORY_PURGE,
-    MEMORY_STATS,
-    memoryStats: MEMORY_STATS,
-    MEMORY_USAGE,
-    memoryUsage: MEMORY_USAGE,
-    MODULE_LIST,
-    moduleList: MODULE_LIST,
-    MODULE_LOAD,
-    moduleLoad: MODULE_LOAD,
-    MODULE_UNLOAD,
-    moduleUnload: MODULE_UNLOAD,
-    MOVE,
-    move: MOVE,
-    PING,
-    ping: PING,
-    PUBSUB_CHANNELS,
-    pubSubChannels: PUBSUB_CHANNELS,
-    PUBSUB_NUMPAT,
-    pubSubNumPat: PUBSUB_NUMPAT,
-    PUBSUB_NUMSUB,
-    pubSubNumSub: PUBSUB_NUMSUB,
-    PUBSUB_SHARDCHANNELS,
-    pubSubShardChannels: PUBSUB_SHARDCHANNELS,
-    PUBSUB_SHARDNUMSUB,
-    pubSubShardNumSub: PUBSUB_SHARDNUMSUB,
-    RANDOMKEY,
-    randomKey: RANDOMKEY,
-    READONLY,
-    readonly: READONLY,
-    READWRITE,
-    readwrite: READWRITE,
-    REPLICAOF,
-    replicaOf: REPLICAOF,
-    'RESTORE-ASKING': RESTORE_ASKING,
-    restoreAsking: RESTORE_ASKING,
-    ROLE,
-    role: ROLE,
-    SAVE,
-    save: SAVE,
-    SCAN,
-    scan: SCAN,
-    SCRIPT_DEBUG,
-    scriptDebug: SCRIPT_DEBUG,
-    SCRIPT_EXISTS,
-    scriptExists: SCRIPT_EXISTS,
-    SCRIPT_FLUSH,
-    scriptFlush: SCRIPT_FLUSH,
-    SCRIPT_KILL,
-    scriptKill: SCRIPT_KILL,
-    SCRIPT_LOAD,
-    scriptLoad: SCRIPT_LOAD,
-    SHUTDOWN,
-    shutdown: SHUTDOWN,
-    SWAPDB,
-    swapDb: SWAPDB,
-    TIME,
-    time: TIME,
-    UNWATCH,
-    unwatch: UNWATCH,
-    WAIT,
-    wait: WAIT
-};
diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts
index 7f93efaa1c3..f04d6467062 100644
--- a/packages/client/lib/client/index.spec.ts
+++ b/packages/client/lib/client/index.spec.ts
@@ -1,1076 +1,976 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
-import RedisClient, { RedisClientType } from '.';
-import { RedisClientMultiCommandType } from './multi-command';
-import { RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands';
-import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors';
+import RedisClient, { RedisClientOptions, RedisClientType } from '.';
+import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, TimeoutError, WatchError } from '../errors';
 import { defineScript } from '../lua-script';
-import { spy } from 'sinon';
-import { once } from 'events';
-import { ClientKillFilters } from '../commands/CLIENT_KILL';
-import { promisify } from 'util';
-
-import {version} from '../../package.json';
+import { spy, stub } from 'sinon';
+import { once } from 'node:events';
+import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec';
+import { RESP_TYPES } from '../RESP/decoder';
+import { BlobStringReply, NumberReply } from '../RESP/types';
+import { SortedSetMember } from '../commands/generic-transformers';
+import { CommandParser } from './parser';
 
 export const SQUARE_SCRIPT = defineScript({
-    SCRIPT: 'return ARGV[1] * ARGV[1];',
-    NUMBER_OF_KEYS: 0,
-    transformArguments(number: number): Array<string> {
-        return [number.toString()];
-    }
+  SCRIPT:
+    `local number = redis.call('GET', KEYS[1])
+    return number * number`,
+  NUMBER_OF_KEYS: 1,
+  FIRST_KEY_INDEX: 0,
+  parseCommand(parser: CommandParser, key: string) {
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
 });
 
-export const MATH_FUNCTION = {
-    name: 'math',
-    engine: 'LUA',
-    code: `#!LUA name=math
-        redis.register_function{
-            function_name = "square",
-            callback = function(keys, args) return args[1] * args[1] end,
-            flags = { "no-writes" }
-        }`,
-    library: {
-        square: {
-            NAME: 'square',
-            IS_READ_ONLY: true,
-            NUMBER_OF_KEYS: 0,
-            transformArguments(number: number): Array<string> {
-                return [number.toString()];
-            }
-        }
-    }
-};
-
-export async function loadMathFunction(
-    client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>
-): Promise<void> {
-    await client.functionLoad(
-        MATH_FUNCTION.code,
-        { REPLACE: true }
-    );
-}
-
 describe('Client', () => {
-    describe('parseURL', () => {
-        it('redis://user:secret@localhost:6379/0', () => {
-            assert.deepEqual(
-                RedisClient.parseURL('redis://user:secret@localhost:6379/0'),
-                {
-                    socket: {
-                        host: 'localhost',
-                        port: 6379
-                    },
-                    username: 'user',
-                    password: 'secret',
-                    database: 0
-                }
-            );
-        });
-
-        it('rediss://user:secret@localhost:6379/0', () => {
-            assert.deepEqual(
-                RedisClient.parseURL('rediss://user:secret@localhost:6379/0'),
-                {
-                    socket: {
-                        host: 'localhost',
-                        port: 6379,
-                        tls: true
-                    },
-                    username: 'user',
-                    password: 'secret',
-                    database: 0
-                }
-            );
-        });
-
-        it('Invalid protocol', () => {
-            assert.throws(
-                () => RedisClient.parseURL('redi://user:secret@localhost:6379/0'),
-                TypeError
-            );
-        });
-
-        it('Invalid pathname', () => {
-            assert.throws(
-                () => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'),
-                TypeError
-            );
-        });
-
-        it('redis://localhost', () => {
-            assert.deepEqual(
-                RedisClient.parseURL('redis://localhost'),
-                {
-                    socket: {
-                        host: 'localhost',
-                    }
-                }
-            );
-        });
+  describe('initialization', () => {
+    describe('clientSideCache validation', () => {
+      const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
+
+      it('should throw error when clientSideCache is enabled with RESP 2', () => {
+        assert.throws(
+          () => new RedisClient({
+            clientSideCache: clientSideCacheConfig,
+            RESP: 2,
+          }),
+          new Error('Client Side Caching is only supported with RESP3')
+        );
+      });
+
+      it('should throw error when clientSideCache is enabled with RESP undefined', () => {
+        assert.throws(
+          () => new RedisClient({
+            clientSideCache: clientSideCacheConfig,
+          }),
+          new Error('Client Side Caching is only supported with RESP3')
+        );
+      });
+
+      it('should not throw when clientSideCache is enabled with RESP 3', () => {
+        assert.doesNotThrow(() =>
+          new RedisClient({
+            clientSideCache: clientSideCacheConfig,
+            RESP: 3,
+          })
+        );
+      });
     });
+  });
+
+  describe('parseURL', () => {
+    it('redis://user:secret@localhost:6379/0', async () => {
+      const result = RedisClient.parseURL('redis://user:secret@localhost:6379/0');
+      const expected: RedisClientOptions = {
+        socket: {
+          host: 'localhost',
+          port: 6379
+        },
+        username: 'user',
+        password: 'secret',
+        database: 0,
+        credentialsProvider: {
+          type: 'async-credentials-provider',
+          credentials: async () => ({
+            password: 'secret',
+            username: 'user'
+          })
+        }
+      };
 
-    describe('connect', () => {
-        testUtils.testWithClient('connect should return the client instance', async client => {
-            try {
-                assert.equal(await client.connect(), client);
-            } finally {
-                if (client.isOpen) await client.disconnect();
-            }
-        }, {
-            ...GLOBAL.SERVERS.PASSWORD,
-            disableClientSetup: true
-        });
-
-        testUtils.testWithClient('should set default lib name and version', async client => {
-            const clientInfo = await client.clientInfo();
-
-            assert.equal(clientInfo.libName, 'node-redis');
-            assert.equal(clientInfo.libVer, version);
-        }, {
-            ...GLOBAL.SERVERS.PASSWORD,
-            minimumDockerVersion: [7, 2]
-        });
-
-        testUtils.testWithClient('disable sending lib name and version', async client => {
-            const clientInfo = await client.clientInfo();
-
-            assert.equal(clientInfo.libName, '');
-            assert.equal(clientInfo.libVer, '');
-        }, {
-            ...GLOBAL.SERVERS.PASSWORD,
-            clientOptions: {
-                ...GLOBAL.SERVERS.PASSWORD.clientOptions,
-                disableClientInfo: true
-            },
-            minimumDockerVersion: [7, 2]
-        });
-
-        testUtils.testWithClient('send client name tag', async client => {
-            const clientInfo = await client.clientInfo();
-
-            assert.equal(clientInfo.libName, 'node-redis(test)');
-            assert.equal(clientInfo.libVer, version);
-        }, {
-            ...GLOBAL.SERVERS.PASSWORD,
-            clientOptions: {
-                ...GLOBAL.SERVERS.PASSWORD.clientOptions,
-                clientInfoTag: "test"
-            },
-            minimumDockerVersion: [7, 2]
-        });
-    });
+      // Compare everything except the credentials function
+      const { credentialsProvider: resultCredProvider, ...resultRest } = result;
+      const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected;
+
+      // Compare non-function properties
+      assert.deepEqual(resultRest, expectedRest);
+
+      if (result?.credentialsProvider?.type === 'async-credentials-provider'
+        && expected?.credentialsProvider?.type === 'async-credentials-provider') {
+
+        // Compare the actual output of the credentials functions
+        const resultCreds = await result.credentialsProvider?.credentials();
+        const expectedCreds = await expected.credentialsProvider?.credentials();
+        assert.deepEqual(resultCreds, expectedCreds);
+      } else {
+        assert.fail('Credentials provider type mismatch');
+      }
 
-    describe('authentication', () => {
-        testUtils.testWithClient('Client should be authenticated', async client => {
-            assert.equal(
-                await client.ping(),
-                'PONG'
-            );
-        }, GLOBAL.SERVERS.PASSWORD);
-
-        testUtils.testWithClient('should execute AUTH before SELECT', async client => {
-            assert.equal(
-                (await client.clientInfo()).db,
-                2
-            );
-        }, {
-            ...GLOBAL.SERVERS.PASSWORD,
-            clientOptions: {
-                ...GLOBAL.SERVERS.PASSWORD.clientOptions,
-                database: 2
-            },
-            minimumDockerVersion: [6, 2]
-        });
-    });
 
-    testUtils.testWithClient('should set connection name', async client => {
-        assert.equal(
-            await client.clientGetName(),
-            'name'
-        );
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        clientOptions: {
-            name: 'name'
-        }
     });
 
-    describe('legacyMode', () => {
-        testUtils.testWithClient('client.sendCommand should call the callback', async client => {
-            assert.equal(
-                await promisify(client.sendCommand).call(client, 'PING'),
-                'PONG'
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.sendCommand should work without callback', async client => {
-            client.sendCommand(['PING']);
-            await client.v4.ping(); // make sure the first command was replied
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.sendCommand should reply with error', async client => {
-            await assert.rejects(
-                promisify(client.sendCommand).call(client, '1', '2')
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.hGetAll should reply with error', async client => {
-            await assert.rejects(
-                promisify(client.hGetAll).call(client)
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
-            assert.equal(
-                await client.v4.sendCommand(['PING']),
-                'PONG'
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.v4.{command} should return a promise', async client => {
-            assert.equal(
-                await client.v4.ping(),
-                'PONG'
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.{command} should accept vardict arguments', async client => {
-            assert.equal(
-                await promisify(client.set).call(client, 'a', 'b'),
-                'OK'
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.{command} should accept arguments array', async client => {
-            assert.equal(
-                await promisify(client.set).call(client, ['a', 'b']),
-                'OK'
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.{command} should accept mix of arrays and arguments', async client => {
-            assert.equal(
-                await promisify(client.set).call(client, ['a'], 'b', ['EX', 1]),
-                'OK'
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.hGetAll should return object', async client => {
-            await client.v4.hSet('key', 'field', 'value');
-            
-            assert.deepEqual(
-                await promisify(client.hGetAll).call(client, 'key'),
-                Object.create(null, {
-                    field: {
-                        value: 'value',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        function multiExecAsync<
-            M extends RedisModules,
-            F extends RedisFunctions,
-            S extends RedisScripts
-        >(multi: RedisClientMultiCommandType<M, F, S>): Promise<Array<RedisCommandRawReply>> {
-            return new Promise((resolve, reject) => {
-                (multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
-                    if (err) return reject(err);
-
-                    resolve(replies);
-                });
-            });
+    it('rediss://user:secret@localhost:6379/0', async () => {
+      const result = RedisClient.parseURL('rediss://user:secret@localhost:6379/0');
+      const expected: RedisClientOptions = {
+        socket: {
+          host: 'localhost',
+          port: 6379,
+          tls: true
+        },
+        username: 'user',
+        password: 'secret',
+        database: 0,
+        credentialsProvider: {
+          credentials: async () => ({
+            password: 'secret',
+            username: 'user'
+          }),
+          type: 'async-credentials-provider'
         }
+      };
 
-        testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
-            assert.deepEqual(
-                await multiExecAsync(
-                    client.multi().ping()
-                ),
-                ['PONG']
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
-            client.multi()
-                .ping()
-                .exec();
-            await client.v4.ping(); // make sure the first command was replied
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => {
-            assert.deepEqual(
-                await client.multi()
-                    .ping()
-                    .v4.ping()
-                    .v4.exec(),
-                ['PONG', 'PONG']
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.{script} should return a promise', async client => {
-            assert.equal(
-                await client.square(2),
-                4
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true,
-                scripts: {
-                    square: SQUARE_SCRIPT
-                }
-            }
-        });
-
-        testUtils.testWithClient('client.multi.{command}.exec should flatten array arguments', async client => {
-            assert.deepEqual(
-                await client.multi()
-                    .sAdd('a', ['b', 'c'])
-                    .v4.exec(),
-                [2]
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-
-        testUtils.testWithClient('client.multi.hGetAll should return object', async client => { 
-            assert.deepEqual(
-                await multiExecAsync(
-                    client.multi()
-                        .hSet('key', 'field', 'value')
-                        .hGetAll('key')
-                ),
-                [
-                    1,
-                    Object.create(null, {
-                        field: {
-                            value: 'value',
-                            configurable: true,
-                            enumerable: true
-                        }
-                    })
-                ]
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                legacyMode: true
-            }
-        });
-    });
+      // Compare everything except the credentials function
+      const { credentialsProvider: resultCredProvider, ...resultRest } = result;
+      const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected;
 
-    describe('events', () => {
-        testUtils.testWithClient('connect, ready, end', async client => {
-            await Promise.all([
-                once(client, 'connect'),
-                once(client, 'ready'),
-                client.connect()
-            ]);
-
-            await Promise.all([
-                once(client, 'end'),
-                client.disconnect()
-            ]);
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            disableClientSetup: true
-        });
-    });
+      // Compare non-function properties
+      assert.deepEqual(resultRest, expectedRest);
+      assert.equal(resultCredProvider?.type, expectedCredProvider?.type);
+
+      if (result?.credentialsProvider?.type === 'async-credentials-provider' &&
+        expected?.credentialsProvider?.type === 'async-credentials-provider') {
+
+        // Compare the actual output of the credentials functions
+        const resultCreds = await result.credentialsProvider.credentials();
+        const expectedCreds = await expected.credentialsProvider.credentials();
+        assert.deepEqual(resultCreds, expectedCreds);
+
+      } else {
+        assert.fail('Credentials provider type mismatch');
+      }
+
+    })
 
-    describe('sendCommand', () => {
-        testUtils.testWithClient('PING', async client => {
-            assert.equal(await client.sendCommand(['PING']), 'PONG');
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('returnBuffers', async client => {
-            assert.deepEqual(
-                await client.sendCommand(['PING'], {
-                    returnBuffers: true
-                }),
-                Buffer.from('PONG')
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        describe('AbortController', () => {
-            before(function () {
-                if (!global.AbortController) {
-                    this.skip();
-                }
-            });
-
-            testUtils.testWithClient('success', async client => {
-                await client.sendCommand(['PING'], {
-                    signal: new AbortController().signal
-                });
-            }, GLOBAL.SERVERS.OPEN);
-
-            testUtils.testWithClient('AbortError', client => {
-                const controller = new AbortController();
-                controller.abort();
-
-                return assert.rejects(
-                    client.sendCommand(['PING'], {
-                        signal: controller.signal
-                    }),
-                    AbortError
-                );
-            }, GLOBAL.SERVERS.OPEN);
-        });
-
-        testUtils.testWithClient('undefined and null should not break the client', async client => {
-            await assert.rejects(
-                client.sendCommand([null as any, undefined as any]),
-                TypeError
-            );
-
-            assert.equal(
-                await client.ping(),
-                'PONG'
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('Invalid protocol', () => {
+      assert.throws(
+        () => RedisClient.parseURL('redi://user:secret@localhost:6379/0'),
+        TypeError
+      );
     });
 
-    describe('multi', () => {
-        testUtils.testWithClient('simple', async client => {
-            assert.deepEqual(
-                await client.multi()
-                    .ping()
-                    .set('key', 'value')
-                    .get('key')
-                    .exec(),
-                ['PONG', 'OK', 'value']
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should reject the whole chain on error', client => {
-            return assert.rejects(
-                client.multi()
-                    .ping()
-                    .addCommand(['INVALID COMMAND'])
-                    .ping()
-                    .exec()
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should reject the whole chain upon client disconnect', async client => {
-            await client.disconnect();
-
-            return assert.rejects(
-                client.multi()
-                    .ping()
-                    .set('key', 'value')
-                    .get('key')
-                    .exec(),
-                ClientClosedError
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('with script', async client => {
-            assert.deepEqual(
-                await client.multi()
-                    .square(2)
-                    .exec(),
-                [4]
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            clientOptions: {
-                scripts: {
-                    square: SQUARE_SCRIPT
-                }
-            }
-        });
-
-        testUtils.testWithClient('WatchError', async client => {
-            await client.watch('key');
-
-            await client.set(
-                RedisClient.commandOptions({
-                    isolated: true
-                }),
-                'key',
-                '1'
-            );
-
-            await assert.rejects(
-                client.multi()
-                    .decr('key')
-                    .exec(),
-                WatchError
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        describe('execAsPipeline', () => {
-            testUtils.testWithClient('exec(true)', async client => {
-                assert.deepEqual(
-                    await client.multi()
-                        .ping()
-                        .exec(true),
-                    ['PONG']
-                );
-            }, GLOBAL.SERVERS.OPEN);
-
-            testUtils.testWithClient('empty execAsPipeline', async client => {
-                assert.deepEqual(
-                    await client.multi().execAsPipeline(),
-                    []
-                );
-            }, GLOBAL.SERVERS.OPEN);
-        });
-
-        testUtils.testWithClient('should remember selected db', async client => {
-            await client.multi()
-                .select(1)
-                .exec();
-            await killClient(client);
-            assert.equal(
-                (await client.clientInfo()).db,
-                1
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            minimumDockerVersion: [6, 2] // CLIENT INFO
-        });
-
-        testUtils.testWithClient('should handle error replies (#2665)', async client => {
-            await assert.rejects(
-                client.multi()
-                    .set('key', 'value')
-                    .hGetAll('key')
-                    .exec(),
-                err => {
-                    assert.ok(err instanceof MultiErrorReply);
-                    assert.equal(err.replies.length, 2);
-                    assert.deepEqual(err.errorIndexes, [1]);
-                    assert.ok(err.replies[1] instanceof ErrorReply);
-                    assert.deepEqual([...err.errors()], [err.replies[1]]);
-                    return true;
-                }
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('Invalid pathname', () => {
+      assert.throws(
+        () => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'),
+        TypeError
+      );
     });
 
-    testUtils.testWithClient('scripts', async client => {
+    it('redis://localhost', () => {
+      assert.deepEqual(
+        RedisClient.parseURL('redis://localhost'),
+        {
+          socket: {
+            host: 'localhost',
+          }
+        }
+      );
+    });
+  });
+
+  describe('authentication', () => {
+    testUtils.testWithClient('Client should be authenticated', async client => {
+      assert.equal(
+        await client.ping(),
+        'PONG'
+      );
+    }, GLOBAL.SERVERS.PASSWORD);
+
+    testUtils.testWithClient('Client can authenticate asynchronously ', async client => {
+      assert.equal(
+        await client.ping(),
+        'PONG'
+      );
+    }, GLOBAL.SERVERS.ASYNC_BASIC_AUTH);
+
+    testUtils.testWithClient('Client can authenticate using the streaming credentials provider for initial token acquisition',
+      async client => {
         assert.equal(
-            await client.square(2),
-            4
+          await client.ping(),
+          'PONG'
         );
+      }, GLOBAL.SERVERS.STREAMING_AUTH);
+
+    testUtils.testWithClient('should execute AUTH before SELECT', async client => {
+      assert.equal(
+        (await client.clientInfo()).db,
+        2
+      );
     }, {
-        ...GLOBAL.SERVERS.OPEN,
-        clientOptions: {
-            scripts: {
-                square: SQUARE_SCRIPT
-            }
-        }
+      ...GLOBAL.SERVERS.PASSWORD,
+      clientOptions: {
+        ...GLOBAL.SERVERS.PASSWORD.clientOptions,
+        database: 2
+      },
+      minimumDockerVersion: [6, 2]
     });
+  });
 
-    const module = {
-        echo: {
-            transformArguments(message: string): Array<string> {
-                return ['ECHO', message];
-            },
-            transformReply(reply: string): string {
-                return reply;
-            }
-        }
-    };
+  testUtils.testWithClient('should set connection name', async client => {
+    assert.equal(
+      await client.clientGetName(),
+      'name'
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      name: 'name'
+    }
+  });
+
+  // TODO: fix & uncomment
+  // testUtils.testWithClient('connect, ready and end events', async client => {
+  //   await Promise.all([
+  //     once(client, 'connect'),
+  //     once(client, 'ready'),
+  //     client.connect()
+  //   ]);
+
+  //   await Promise.all([
+  //     once(client, 'end'),
+  //     client.close()
+  //   ]);
+  // }, {
+  //   ...GLOBAL.SERVERS.OPEN,
+  //   disableClientSetup: true
+  // });
+
+  describe('sendCommand', () => {
+    testUtils.testWithClient('PING', async client => {
+      assert.equal(await client.sendCommand(['PING']), 'PONG');
+    }, GLOBAL.SERVERS.OPEN);
 
-    testUtils.testWithClient('modules', async client => {
-        assert.equal(
-            await client.module.echo('message'),
-            'message'
-        );
+    testUtils.testWithClient('Unactivated AbortController should not abort', async client => {
+      await client.sendCommand(['PING'], {
+        abortSignal: new AbortController().signal
+      });
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('AbortError', async client => {
+        await blockSetImmediate(async () => {
+          await assert.rejects(client.sendCommand(['PING'], {
+            abortSignal: AbortSignal.timeout(5)
+          }), AbortError);
+        })
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('Timeout with custom timeout config', async client => {
+      await blockSetImmediate(async () => {
+        await assert.rejects(client.sendCommand(['PING'], {
+          timeout: 5
+        }), TimeoutError);
+      })
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithCluster('Timeout with custom timeout config (cluster)', async cluster => {
+      await blockSetImmediate(async () => {
+        await assert.rejects(cluster.sendCommand(undefined, true, ['PING'], {
+          timeout: 5
+        }), TimeoutError);
+      })
+    }, GLOBAL.CLUSTERS.OPEN);
+
+    testUtils.testWithClientSentinel('Timeout with custom timeout config (sentinel)', async sentinel => {
+      await blockSetImmediate(async () => {
+        await assert.rejects(sentinel.sendCommand(true, ['PING'], {
+          timeout: 5
+        }), TimeoutError);
+      })
+    }, GLOBAL.CLUSTERS.OPEN);
+
+    testUtils.testWithClient('Timeout with global timeout config', async client => {
+      await blockSetImmediate(async () => {
+        await assert.rejects(client.ping(), TimeoutError);
+        await assert.rejects(client.sendCommand(['PING']), TimeoutError);
+      });
     }, {
-        ...GLOBAL.SERVERS.OPEN,
-        clientOptions: {
-            modules: {
-                module
-            }
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        commandOptions: {
+          timeout: 5
         }
+      }
     });
 
-    testUtils.testWithClient('functions', async client => {
-        await loadMathFunction(client);
+    testUtils.testWithCluster('Timeout with global timeout config (cluster)', async cluster => {
+      await blockSetImmediate(async () => {
+        await assert.rejects(cluster.HSET('key', 'foo', 'value'), TimeoutError);
+        await assert.rejects(cluster.sendCommand(undefined, true, ['PING']), TimeoutError);
+      });
+    }, {
+      ...GLOBAL.CLUSTERS.OPEN,
+      clusterConfiguration: {
+        commandOptions: {
+          timeout: 5
+        }
+      }
+    });
 
-        assert.equal(
-            await client.math.square(2),
-            4
-        );
+    testUtils.testWithClientSentinel('Timeout with global timeout config (sentinel)', async sentinel => {
+      await blockSetImmediate(async () => {
+        await assert.rejects(sentinel.HSET('key', 'foo', 'value'), TimeoutError);
+        await assert.rejects(sentinel.sendCommand(true, ['PING']), TimeoutError);
+      });
     }, {
-        ...GLOBAL.SERVERS.OPEN,
-        minimumDockerVersion: [7, 0],
-        clientOptions: {
-            functions: {
-                math: MATH_FUNCTION.library
-            }
+      ...GLOBAL.SENTINEL.OPEN,
+      clientOptions: {
+        commandOptions: {
+          timeout: 5
         }
+      }
     });
 
-    describe('isolationPool', () => {
-        testUtils.testWithClient('executeIsolated', async client => {
-            const id = await client.clientId(),
-                isolatedId = await client.executeIsolated(isolatedClient => isolatedClient.clientId());
-            assert.ok(id !== isolatedId);
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should be able to use pool even before connect', async client => {
-            await client.executeIsolated(() => Promise.resolve());
-            // make sure to destroy isolation pool
-            await client.connect();
-            await client.disconnect();
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            disableClientSetup: true
-        });
-
-        testUtils.testWithClient('should work after reconnect (#2406)', async client => {
-            await client.disconnect();
-            await client.connect();
-            await client.executeIsolated(() => Promise.resolve());
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should throw ClientClosedError after disconnect', async client => {
-            await client.connect();
-            await client.disconnect();
-            await assert.rejects(
-                client.executeIsolated(() => Promise.resolve()),
-                ClientClosedError
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            disableClientSetup: true
-        });
+    testUtils.testWithClient('undefined and null should not break the client', async client => {
+      await assert.rejects(
+        client.sendCommand([null as any, undefined as any]),
+        TypeError
+      );
+
+      assert.equal(
+        await client.ping(),
+        'PONG'
+      );
+    }, GLOBAL.SERVERS.OPEN);
+  });
+
+  describe('multi', () => {
+    testUtils.testWithClient('simple', async client => {
+      assert.deepEqual(
+        await client.multi()
+          .ping()
+          .set('key', 'value')
+          .get('key')
+          .exec(),
+        ['PONG', 'OK', 'value']
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('should reject the whole chain on error', client => {
+      return assert.rejects(
+        client.multi()
+          .ping()
+          .addCommand(['INVALID COMMAND'])
+          .ping()
+          .exec()
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('should reject the whole chain upon client disconnect', async client => {
+      await client.close();
+
+      return assert.rejects(
+        client.multi()
+          .ping()
+          .set('key', 'value')
+          .get('key')
+          .exec(),
+        ClientClosedError
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('with script', async client => {
+      assert.deepEqual(
+        await client.multi()
+          .set('key', '2')
+          .square('key')
+          .exec(),
+        ['OK', 4]
+      );
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        scripts: {
+          square: SQUARE_SCRIPT
+        }
+      }
     });
 
-    async function killClient<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(
-        client: RedisClientType<M, F, S>,
-        errorClient: RedisClientType<M, F, S> = client
-    ): Promise<void> {
-        const onceErrorPromise = once(errorClient, 'error');
-        await client.sendCommand(['QUIT']);
-        await Promise.all([
-            onceErrorPromise,
-            assert.rejects(client.ping(), SocketClosedUnexpectedlyError)
-        ]);
-    }
+    testUtils.testWithClient('WatchError', async client => {
+      await client.watch('key');
 
-    testUtils.testWithClient('should reconnect when socket disconnects', async client => {
-        await killClient(client);
-        await assert.doesNotReject(client.ping());
+      const duplicate = await client.duplicate().connect();
+      try {
+        await client.set(
+          'key',
+          '1'
+        );
+      } finally {
+        duplicate.destroy();
+      }
+
+      await assert.rejects(
+        client.multi()
+          .decr('key')
+          .exec(),
+        WatchError
+      );
     }, GLOBAL.SERVERS.OPEN);
 
-    testUtils.testWithClient('should remember selected db', async client => {
-        await client.select(1);
-        await killClient(client);
-        assert.equal(
-            (await client.clientInfo()).db,
-            1
+    describe('execAsPipeline', () => {
+      testUtils.testWithClient('exec(true)', async client => {
+        assert.deepEqual(
+          await client.multi()
+            .ping()
+            .exec(true),
+          ['PONG']
+        );
+      }, GLOBAL.SERVERS.OPEN);
+
+      testUtils.testWithClient('empty execAsPipeline', async client => {
+        assert.deepEqual(
+          await client.multi().execAsPipeline(),
+          []
         );
+      }, GLOBAL.SERVERS.OPEN);
+    });
+
+    testUtils.testWithClient('should remember selected db', async client => {
+      await client.multi()
+        .select(1)
+        .exec();
+      await killClient(client);
+      assert.equal(
+        (await client.clientInfo()).db,
+        1
+      );
     }, {
-        ...GLOBAL.SERVERS.OPEN,
-        minimumDockerVersion: [6, 2] // CLIENT INFO
+      ...GLOBAL.SERVERS.OPEN,
+      minimumDockerVersion: [6, 2] // CLIENT INFO
     });
 
-    testUtils.testWithClient('should propagated errors from "isolated" clients', client => {
-        client.on('error', () => {
-            // ignore errors
-        });
-        return client.executeIsolated(isolated => killClient(isolated, client));
+    testUtils.testWithClient('should handle error replies (#2665)', async client => {
+      await assert.rejects(
+        client.multi()
+          .set('key', 'value')
+          .hGetAll('key')
+          .exec(),
+        err => {
+          assert.ok(err instanceof MultiErrorReply);
+          assert.equal(err.replies.length, 2);
+          assert.deepEqual(err.errorIndexes, [1]);
+          assert.ok(err.replies[1] instanceof ErrorReply);
+          // @ts-ignore TS2802
+          assert.deepEqual([...err.errors()], [err.replies[1]]);
+          return true;
+        }
+      );
     }, GLOBAL.SERVERS.OPEN);
+  });
+
+  testUtils.testWithClient('scripts', async client => {
+    const [, reply] = await Promise.all([
+      client.set('key', '2'),
+      client.square('key')
+    ]);
+
+    assert.equal(reply, 4);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      scripts: {
+        square: SQUARE_SCRIPT
+      }
+    }
+  });
+
+  const module = {
+    echo: {
+      parseCommand(parser: CommandParser, message: string) {
+        parser.push('ECHO', message);
+      },
+      transformReply: undefined as unknown as () => BlobStringReply
+    }
+  };
 
-    testUtils.testWithClient('scanIterator', async client => {
-        const promises = [],
-            keys = new Set();
-        for (let i = 0; i < 100; i++) {
-            const key = i.toString();
-            keys.add(key);
-            promises.push(client.set(key, ''));
-        }
+  testUtils.testWithClient('modules', async client => {
+    assert.equal(
+      await client.module.echo('message'),
+      'message'
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      modules: {
+        module
+      }
+    }
+  });
+
+  testUtils.testWithClient('functions', async client => {
+    const [, , reply] = await Promise.all([
+      loadMathFunction(client),
+      client.set('key', '2'),
+      client.math.square('key')
+    ]);
+
+    assert.equal(reply, 4);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    minimumDockerVersion: [7, 0],
+    clientOptions: {
+      functions: {
+        math: MATH_FUNCTION.library
+      }
+    }
+  });
+
+  testUtils.testWithClient('duplicate should reuse command options', async client => {
+    const duplicate = client.duplicate();
 
-        await Promise.all(promises);
+    await duplicate.connect();
 
-        const results = new Set();
-        for await (const key of client.scanIterator()) {
-            results.add(key);
+    try {
+      assert.deepEqual(
+        await duplicate.ping(),
+        Buffer.from('PONG')
+      );
+    } finally {
+      duplicate.close();
+    }
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      commandOptions: {
+        typeMapping: {
+          [RESP_TYPES.SIMPLE_STRING]: Buffer
         }
+      }
+    },
+    disableClientSetup: true,
+  });
+
+  async function killClient(
+    client: RedisClientType<any, any, any, any, any>,
+    errorClient: RedisClientType<any, any, any, any, any> = client
+  ): Promise<void> {
+    const onceErrorPromise = once(errorClient, 'error');
+    await client.sendCommand(['QUIT']);
+    await Promise.all([
+      onceErrorPromise,
+      assert.rejects(client.ping())
+    ]);
+  }
+
+  testUtils.testWithClient('should reconnect when socket disconnects', async client => {
+    await killClient(client);
+    await assert.doesNotReject(client.ping());
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('should remember selected db', async client => {
+    await client.select(1);
+    await killClient(client);
+    assert.equal(
+      (await client.clientInfo()).db,
+      1
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    minimumDockerVersion: [6, 2] // CLIENT INFO
+  });
+
+  testUtils.testWithClient('scanIterator', async client => {
+    const entries: Array<string> = [],
+      keys = new Set<string>();
+    for (let i = 0; i < 100; i++) {
+      const key = i.toString();
+      keys.add(key);
+      entries.push(key, '');
+    }
 
-        assert.deepEqual(keys, results);
-    }, GLOBAL.SERVERS.OPEN);
+    await client.mSet(entries);
 
-    testUtils.testWithClient('hScanIterator', async client => {
-        const hash: Record<string, string> = {};
-        for (let i = 0; i < 100; i++) {
-            hash[i.toString()] = i.toString();
-        }
+    const results = new Set();
+    for await (const keys of client.scanIterator()) {
+      for (const key of keys) {
+        results.add(key);
+      }
+    }
 
-        await client.hSet('key', hash);
+    assert.deepEqual(keys, results);
+  }, GLOBAL.SERVERS.OPEN);
 
-        const results: Record<string, string> = {};
-        for await (const { field, value } of client.hScanIterator('key')) {
-            results[field] = value;
-        }
+  testUtils.testWithClient('hScanIterator', async client => {
+    const hash: Record<string, string> = {};
+    for (let i = 0; i < 100; i++) {
+      hash[i.toString()] = i.toString();
+    }
 
-        assert.deepEqual(hash, results);
-    }, GLOBAL.SERVERS.OPEN);
+    await client.hSet('key', hash);
 
-    testUtils.testWithClient('hScanNoValuesIterator', async client => {
-        const hash: Record<string, string> = {};
-        const expectedKeys: Array<string> = [];
-        for (let i = 0; i < 100; i++) {
-            hash[i.toString()] = i.toString();
-            expectedKeys.push(i.toString());
-        }
+    const results: Record<string, string> = {};
+    for await (const entries of client.hScanIterator('key')) {
+      for (const { field, value } of entries) {
+        results[field] = value;
+      }
+    }
 
-        await client.hSet('key', hash);
+    assert.deepEqual(hash, results);
+  }, GLOBAL.SERVERS.OPEN);
 
-        const keys: Array<string> = [];
-        for await (const key of client.hScanNoValuesIterator('key')) {
-            keys.push(key);
-        }
+  testUtils.testWithClient('hScanNoValuesIterator', async client => {
+    const hash: Record<string, string> = {};
+    const expectedFields: Array<string> = [];
+    for (let i = 0; i < 100; i++) {
+      hash[i.toString()] = i.toString();
+      expectedFields.push(i.toString());
+    }
 
-        function sort(a: string, b: string) {
-            return Number(a) - Number(b);
-        }
+    await client.hSet('key', hash);
 
-        assert.deepEqual(keys.sort(sort), expectedKeys);
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        minimumDockerVersion: [7, 4]
-    });
+    const actualFields: Array<string> = [];
+    for await (const fields of client.hScanNoValuesIterator('key')) {
+      for (const field of fields) {
+        actualFields.push(field);
+      }
+    }
 
-    testUtils.testWithClient('sScanIterator', async client => {
-        const members = new Set<string>();
-        for (let i = 0; i < 100; i++) {
-            members.add(i.toString());
-        }
+    function sort(a: string, b: string) {
+      return Number(a) - Number(b);
+    }
 
-        await client.sAdd('key', Array.from(members));
+    assert.deepEqual(actualFields.sort(sort), expectedFields);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    minimumDockerVersion: [7, 4]
+  });
 
-        const results = new Set<string>();
-        for await (const key of client.sScanIterator('key')) {
-            results.add(key);
-        }
+  testUtils.testWithClient('sScanIterator', async client => {
+    const members = new Set<string>();
+    for (let i = 0; i < 100; i++) {
+      members.add(i.toString());
+    }
 
-        assert.deepEqual(members, results);
-    }, GLOBAL.SERVERS.OPEN);
+    await client.sAdd('key', Array.from(members));
 
-    testUtils.testWithClient('zScanIterator', async client => {
-        const members = [];
-        for (let i = 0; i < 100; i++) {
-            members.push({
-                score: 1,
-                value: i.toString()
-            });
-        }
+    const results = new Set<string>();
+    for await (const members of client.sScanIterator('key')) {
+      for (const member of members) {
+        results.add(member);
+      }
+    }
 
-        await client.zAdd('key', members);
+    assert.deepEqual(members, results);
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('zScanIterator', async client => {
+    const members: Array<SortedSetMember> = [],
+      map = new Map<string, number>();
+    for (let i = 0; i < 100; i++) {
+      const member = {
+        value: i.toString(),
+        score: 1
+      };
+      map.set(member.value, member.score);
+      members.push(member);
+    }
 
-        const map = new Map<string, number>();
-        for await (const member of client.zScanIterator('key')) {
-            map.set(member.value, member.score);
-        }
+    await client.zAdd('key', members);
 
-        type MemberTuple = [string, number];
+    const results = new Map<string, number>();
+    for await (const members of client.zScanIterator('key')) {
+      for (const { value, score } of members) {
+        results.set(value, score);
+      }
+    }
 
-        function sort(a: MemberTuple, b: MemberTuple) {
-            return Number(b[0]) - Number(a[0]);
-        }
+    assert.deepEqual(map, results);
+  }, GLOBAL.SERVERS.OPEN);
 
-        assert.deepEqual(
-            [...map.entries()].sort(sort),
-            members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
-        );
+  describe('PubSub', () => {
+    testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => {
+      function assertStringListener(message: string, channel: string) {
+        assert.equal(typeof message, 'string');
+        assert.equal(typeof channel, 'string');
+      }
+
+      function assertBufferListener(message: Buffer, channel: Buffer) {
+        assert.ok(message instanceof Buffer);
+        assert.ok(channel instanceof Buffer);
+      }
+
+      const subscriber = await publisher.duplicate().connect();
+
+      try {
+        const channelListener1 = spy(assertBufferListener),
+          channelListener2 = spy(assertStringListener),
+          patternListener = spy(assertStringListener);
+
+        await Promise.all([
+          subscriber.subscribe('channel', channelListener1, true),
+          subscriber.subscribe('channel', channelListener2),
+          subscriber.pSubscribe('channel*', patternListener)
+        ]);
+        await Promise.all([
+          waitTillBeenCalled(channelListener1),
+          waitTillBeenCalled(channelListener2),
+          waitTillBeenCalled(patternListener),
+          publisher.publish(Buffer.from('channel'), Buffer.from('message'))
+        ]);
+        assert.ok(channelListener1.calledOnceWithExactly(Buffer.from('message'), Buffer.from('channel')));
+        assert.ok(channelListener2.calledOnceWithExactly('message', 'channel'));
+        assert.ok(patternListener.calledOnceWithExactly('message', 'channel'));
+
+        await subscriber.unsubscribe('channel', channelListener1, true);
+        await Promise.all([
+          waitTillBeenCalled(channelListener2),
+          waitTillBeenCalled(patternListener),
+          publisher.publish('channel', 'message')
+        ]);
+        assert.ok(channelListener1.calledOnce);
+        assert.ok(channelListener2.calledTwice);
+        assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel'));
+        assert.ok(patternListener.calledTwice);
+        assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel'));
+        await subscriber.unsubscribe('channel');
+        await Promise.all([
+          waitTillBeenCalled(patternListener),
+          publisher.publish('channel', 'message')
+        ]);
+        assert.ok(channelListener1.calledOnce);
+        assert.ok(channelListener2.calledTwice);
+        assert.ok(patternListener.calledThrice);
+        assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel'));
+
+        await subscriber.pUnsubscribe();
+        await publisher.publish('channel', 'message');
+        assert.ok(channelListener1.calledOnce);
+        assert.ok(channelListener2.calledTwice);
+        assert.ok(patternListener.calledThrice);
+
+        // should be able to send commands when unsubsribed from all channels (see #1652)
+        await assert.doesNotReject(subscriber.ping());
+      } finally {
+        subscriber.destroy();
+      }
     }, GLOBAL.SERVERS.OPEN);
-    
-    describe('PubSub', () => {
-        testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => {
-            function assertStringListener(message: string, channel: string) {
-                assert.equal(typeof message, 'string');
-                assert.equal(typeof channel, 'string');
-            }
-
-            function assertBufferListener(message: Buffer, channel: Buffer) {
-                assert.ok(Buffer.isBuffer(message));
-                assert.ok(Buffer.isBuffer(channel));
-            }
-
-            const subscriber = publisher.duplicate();
-
-            await subscriber.connect();
-
-            try {
-                const channelListener1 = spy(assertBufferListener),
-                    channelListener2 = spy(assertStringListener),
-                    patternListener = spy(assertStringListener);
-
-                await Promise.all([
-                    subscriber.subscribe('channel', channelListener1, true),
-                    subscriber.subscribe('channel', channelListener2),
-                    subscriber.pSubscribe('channel*', patternListener)
-                ]);
-                await Promise.all([
-                    waitTillBeenCalled(channelListener1),
-                    waitTillBeenCalled(channelListener2),
-                    waitTillBeenCalled(patternListener),
-                    publisher.publish(Buffer.from('channel'), Buffer.from('message'))
-                ]);
-
-                assert.ok(channelListener1.calledOnceWithExactly(Buffer.from('message'), Buffer.from('channel')));
-                assert.ok(channelListener2.calledOnceWithExactly('message', 'channel'));
-                assert.ok(patternListener.calledOnceWithExactly('message', 'channel'));
-
-                await subscriber.unsubscribe('channel', channelListener1, true);
-                await Promise.all([
-                    waitTillBeenCalled(channelListener2),
-                    waitTillBeenCalled(patternListener),
-                    publisher.publish('channel', 'message')
-                ]);
-                assert.ok(channelListener1.calledOnce);
-                assert.ok(channelListener2.calledTwice);
-                assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel'));
-                assert.ok(patternListener.calledTwice);
-                assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel'));
-                await subscriber.unsubscribe('channel');
-                await Promise.all([
-                    waitTillBeenCalled(patternListener),
-                    publisher.publish('channel', 'message')
-                ]);
-                assert.ok(channelListener1.calledOnce);
-                assert.ok(channelListener2.calledTwice);
-                assert.ok(patternListener.calledThrice);
-                assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel'));
-                await subscriber.pUnsubscribe();
-                await publisher.publish('channel', 'message');
-                assert.ok(channelListener1.calledOnce);
-                assert.ok(channelListener2.calledTwice);
-                assert.ok(patternListener.calledThrice);
-                // should be able to send commands when unsubsribed from all channels (see #1652)
-                await assert.doesNotReject(subscriber.ping());
-            } finally {
-                await subscriber.disconnect();
-            }
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should resubscribe', async publisher => {
-            const subscriber = publisher.duplicate();
-
-            await subscriber.connect();
-
-            try {
-                const channelListener = spy();
-                await subscriber.subscribe('channel', channelListener);
-
-                const patternListener = spy();
-                await subscriber.pSubscribe('channe*', patternListener);
-
-                await Promise.all([
-                    once(subscriber, 'error'),
-                    publisher.clientKill({
-                        filter: ClientKillFilters.SKIP_ME,
-                        skipMe: true
-                    })
-                ]);
-
-                await once(subscriber, 'ready');
-
-                await Promise.all([
-                    waitTillBeenCalled(channelListener),
-                    waitTillBeenCalled(patternListener),
-                    publisher.publish('channel', 'message')
-                ]);
-            } finally {
-                await subscriber.disconnect();
-            }
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should not fail when message arrives right after subscribe', async publisher => {
-            const subscriber = publisher.duplicate();
-
-            await subscriber.connect();
-
-            try {
-                await assert.doesNotReject(Promise.all([
-                    subscriber.subscribe('channel', () => {
-                        // noop
-                    }),
-                    publisher.publish('channel', 'message')
-                ]));
-            } finally {
-                await subscriber.disconnect();
-            }
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('should be able to quit in PubSub mode', async client => {
-            await client.subscribe('channel', () => {
-                // noop
-            });
-
-            await assert.doesNotReject(client.quit());
-
-            assert.equal(client.isOpen, false);
-        }, GLOBAL.SERVERS.OPEN);
-    });
 
-    testUtils.testWithClient('ConnectionTimeoutError', async client => {
-        const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
-            start = process.hrtime.bigint();
+    testUtils.testWithClient('should resubscribe', async publisher => {
+      const subscriber = await publisher.duplicate().connect();
 
-        while (process.hrtime.bigint() - start < 1_000_000) {
-            // block the event loop for 1ms, to make sure the connection will timeout
-        }
+      try {
+        const channelListener = spy();
+        await subscriber.subscribe('channel', channelListener);
 
-        await promise;
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        clientOptions: {
-            socket: {
-                connectTimeout: 1
-            }
-        },
-        disableClientSetup: true
-    });
+        const patternListener = spy();
+        await subscriber.pSubscribe('channe*', patternListener);
 
-    testUtils.testWithClient('client.quit', async client => {
-        await client.connect();
+        await Promise.all([
+          once(subscriber, 'error'),
+          publisher.clientKill({
+            filter: 'SKIPME',
+            skipMe: true
+          })
+        ]);
 
-        const pingPromise = client.ping(),
-            quitPromise = client.quit();
-        assert.equal(client.isOpen, false);
+        await once(subscriber, 'ready');
 
-        const [ping, quit] = await Promise.all([
-            pingPromise,
-            quitPromise,
-            assert.rejects(client.ping(), ClientClosedError)
+        await Promise.all([
+          waitTillBeenCalled(channelListener),
+          waitTillBeenCalled(patternListener),
+          publisher.publish('channel', 'message')
         ]);
+      } finally {
+        subscriber.destroy();
+      }
+    }, GLOBAL.SERVERS.OPEN);
 
-        assert.equal(ping, 'PONG');
-        assert.equal(quit, 'OK');
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        disableClientSetup: true
-    });
+    testUtils.testWithClient('should not fail when message arrives right after subscribe', async publisher => {
+      const subscriber = await publisher.duplicate().connect();
+
+      try {
+        await assert.doesNotReject(Promise.all([
+          subscriber.subscribe('channel', () => {
+            // noop
+          }),
+          publisher.publish('channel', 'message')
+        ]));
+      } finally {
+        subscriber.destroy();
+      }
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('should be able to quit in PubSub mode', async client => {
+      await client.subscribe('channel', () => {
+        // noop
+      });
 
-    testUtils.testWithClient('client.disconnect', async client => {
-        const pingPromise = client.ping(),
-            disconnectPromise = client.disconnect();
-        assert.equal(client.isOpen, false);
+      await assert.doesNotReject(client.quit());
+
+      assert.equal(client.isOpen, false);
+    }, GLOBAL.SERVERS.OPEN);
+  });
+
+  testUtils.testWithClient('ConnectionTimeoutError', async client => {
+    const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
+      start = process.hrtime.bigint();
+
+    while (process.hrtime.bigint() - start < 1_000_000) {
+      // block the event loop for 1ms, to make sure the connection will timeout
+    }
+
+    await promise;
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      socket: {
+        connectTimeout: 1
+      }
+    },
+    disableClientSetup: true
+  });
+
+  testUtils.testWithClient('client.quit', async client => {
+    await client.connect();
+
+    const pingPromise = client.ping(),
+      quitPromise = client.quit();
+    assert.equal(client.isOpen, false);
+
+    const [ping, quit] = await Promise.all([
+      pingPromise,
+      quitPromise,
+      assert.rejects(client.ping(), ClientClosedError)
+    ]);
+
+    assert.equal(ping, 'PONG');
+    assert.equal(quit, 'OK');
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    disableClientSetup: true
+  });
+
+  testUtils.testWithClient('client.disconnect', async client => {
+    const pingPromise = client.ping(),
+      disconnectPromise = client.disconnect();
+    assert.equal(client.isOpen, false);
+    await Promise.all([
+      assert.rejects(pingPromise, DisconnectsClientError),
+      assert.doesNotReject(disconnectPromise),
+      assert.rejects(client.ping(), ClientClosedError)
+    ]);
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('should be able to connect after disconnect (see #1801)', async client => {
+    await client.disconnect();
+    await client.connect();
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('should be able to use ref and unref', client => {
+    client.unref();
+    client.ref();
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('pingInterval', async client => {
+    assert.deepEqual(
+      await once(client, 'ping-interval'),
+      ['PONG']
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      pingInterval: 1
+    }
+  });
+
+  testUtils.testWithClient('should reject commands in connect phase when `disableOfflineQueue`', async client => {
+    const connectPromise = client.connect();
+    await assert.rejects(
+      client.ping(),
+      ClientOfflineError
+    );
+    await connectPromise;
+    await client.disconnect();
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      disableOfflineQueue: true
+    },
+    disableClientSetup: true
+  });
+
+  describe('MONITOR', () => {
+    testUtils.testWithClient('should be able to monitor commands', async client => {
+      const duplicate = await client.duplicate().connect(),
+        listener = spy(message => assert.equal(typeof message, 'string'));
+      await duplicate.monitor(listener);
+
+      try {
         await Promise.all([
-            assert.rejects(pingPromise, DisconnectsClientError),
-            assert.doesNotReject(disconnectPromise),
-            assert.rejects(client.ping(), ClientClosedError)
+          waitTillBeenCalled(listener),
+          client.ping()
         ]);
+      } finally {
+        duplicate.destroy();
+      }
     }, GLOBAL.SERVERS.OPEN);
 
-    testUtils.testWithClient('should be able to connect after disconnect (see #1801)', async client => {
-        await client.disconnect();
-        await client.connect();
+    testUtils.testWithClient('should keep monitoring after reconnection', async client => {
+      const duplicate = await client.duplicate().connect(),
+        listener = spy(message => assert.equal(typeof message, 'string'));
+      await duplicate.monitor(listener);
+
+      try {
+        await Promise.all([
+          once(duplicate, 'error'),
+          client.clientKill({
+            filter: 'SKIPME',
+            skipMe: true
+          })
+        ]);
+
+        await once(duplicate, 'ready');
+
+        await Promise.all([
+          waitTillBeenCalled(listener),
+          client.ping()
+        ]);
+      } finally {
+        duplicate.destroy();
+      }
     }, GLOBAL.SERVERS.OPEN);
 
-    testUtils.testWithClient('should be able to use ref and unref', client => {
-        client.unref();
-        client.ref();
+    testUtils.testWithClient('should be able to go back to "normal mode"', async client => {
+      await Promise.all([
+        client.monitor(() => { }),
+        client.reset()
+      ]);
+      await assert.doesNotReject(client.ping());
     }, GLOBAL.SERVERS.OPEN);
 
-    testUtils.testWithClient('pingInterval', async client => {
-        assert.deepEqual(
-            await once(client, 'ping-interval'),
-            ['PONG']
-        );
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        clientOptions: {
-            pingInterval: 1
-        }
-    });
+    testUtils.testWithClient('should respect type mapping', async client => {
+      const duplicate = await client.duplicate().connect(),
+        listener = spy(message => assert.ok(message instanceof Buffer));
+      await duplicate.withTypeMapping({
+        [RESP_TYPES.SIMPLE_STRING]: Buffer
+      }).monitor(listener);
 
-    testUtils.testWithClient('should reject commands in connect phase when `disableOfflineQueue`', async client => {
-        const connectPromise = client.connect();
-        await assert.rejects(
-            client.ping(),
-            ClientOfflineError
-        );
-        await connectPromise;
-        await client.disconnect();
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        clientOptions: {
-            disableOfflineQueue: true
-        },
-        disableClientSetup: true
-    });
+      try {
+        await Promise.all([
+          waitTillBeenCalled(listener),
+          client.ping()
+        ]);
+      } finally {
+        duplicate.destroy();
+      }
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
+
+/**
+ * Executes the provided function in a context where setImmediate is stubbed to not do anything.
+ * This blocks setImmediate callbacks from executing
+ */
+async function blockSetImmediate(fn: () => Promise<unknown>) {
+  let setImmediateStub: any;
+
+  try {
+    setImmediateStub = stub(global, 'setImmediate');
+    setImmediateStub.callsFake(() => {
+      //Dont call the callback, effectively blocking execution
+    });
+    await fn();
+  } finally {
+    if (setImmediateStub) {
+      setImmediateStub.restore();
+    }
+  }
+}
diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts
index d7f33e97b16..128dc599677 100644
--- a/packages/client/lib/client/index.ts
+++ b/packages/client/lib/client/index.ts
@@ -1,883 +1,1383 @@
-import COMMANDS from './commands';
-import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands';
-import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
-import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue';
+import COMMANDS from '../commands';
+import RedisSocket, { RedisSocketOptions } from './socket';
+import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, Disposable } from '../authx';
+import RedisCommandsQueue, { CommandOptions } from './commands-queue';
+import { EventEmitter } from 'node:events';
+import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
+import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors';
+import { URL } from 'node:url';
+import { TcpSocketConnectOpts } from 'node:net';
+import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub';
+import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply, TransformReply, CommandArguments } from '../RESP/types';
 import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
 import { RedisMultiQueuedCommand } from '../multi-command';
-import { EventEmitter } from 'events';
-import { CommandOptions, commandOptions, isCommandOptions } from '../command-options';
-import { ScanOptions, ZMember } from '../commands/generic-transformers';
-import { ScanCommandOptions } from '../commands/SCAN';
-import { HScanTuple } from '../commands/HSCAN';
-import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
-import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
-import { ClientClosedError, ClientOfflineError, DisconnectsClientError, ErrorReply } from '../errors';
-import { URL } from 'url';
-import { TcpSocketConnectOpts } from 'net';
-import { PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub';
-
-import {version} from '../../package.json';
+import HELLO, { HelloOptions } from '../commands/HELLO';
+import { ScanOptions, ScanCommonOptions } from '../commands/SCAN';
+import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode';
+import { RedisPoolOptions, RedisClientPool } from './pool';
+import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../commands/generic-transformers';
+import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider } from './cache';
+import { BasicCommandParser, CommandParser } from './parser';
+import SingleEntryCache from '../single-entry-cache';
+import { version } from '../../package.json'
 
 export interface RedisClientOptions<
-    M extends RedisModules = RedisModules,
-    F extends RedisFunctions = RedisFunctions,
-    S extends RedisScripts = RedisScripts
-> extends RedisExtensions<M, F, S> {
-    /**
-     * `redis[s]://[[username][:password]@][host][:port][/db-number]`
-     * See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details
-     */
-    url?: string;
-    /**
-     * Socket connection properties
-     */
-    socket?: RedisSocketOptions;
-    /**
-     * ACL username ([see ACL guide](https://redis.io/topics/acl))
-     */
-    username?: string;
-    /**
-     * ACL password or the old "--requirepass" password
-     */
-    password?: string;
-    /**
-     * Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))
-     */
-    name?: string;
-    /**
-     * Redis database number (see [`SELECT`](https://redis.io/commands/select) command)
-     */
-    database?: number;
-    /**
-     * Maximum length of the client's internal command queue
-     */
-    commandsQueueMaxLength?: number;
-    /**
-     * When `true`, commands are rejected when the client is reconnecting.
-     * When `false`, commands are queued for execution after reconnection.
-     */
-    disableOfflineQueue?: boolean;
-    /**
-     * Connect in [`READONLY`](https://redis.io/commands/readonly) mode
-     */
-    readonly?: boolean;
-    legacyMode?: boolean;
-    isolationPoolOptions?: PoolOptions;
-    /**
-     * Send `PING` command at interval (in ms).
-     * Useful with Redis deployments that do not use TCP Keep-Alive.
-     */
-    pingInterval?: number;
-    /**
-     * If set to true, disables sending client identifier (user-agent like message) to the redis server
-     */
-    disableClientInfo?: boolean;
-    /**
-     * Tag to append to library name that is sent to the Redis server
-     */
-    clientInfoTag?: string;
+  M extends RedisModules = RedisModules,
+  F extends RedisFunctions = RedisFunctions,
+  S extends RedisScripts = RedisScripts,
+  RESP extends RespVersions = RespVersions,
+  TYPE_MAPPING extends TypeMapping = TypeMapping,
+  SocketOptions extends RedisSocketOptions = RedisSocketOptions
+> extends CommanderConfig<M, F, S, RESP> {
+  /**
+   * `redis[s]://[[username][:password]@][host][:port][/db-number]`
+   * See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details
+   */
+  url?: string;
+  /**
+   * Socket connection properties
+   */
+  socket?: SocketOptions;
+  /**
+   * ACL username ([see ACL guide](https://redis.io/topics/acl))
+   */
+  username?: string;
+  /**
+   * ACL password or the old "--requirepass" password
+   */
+  password?: string;
+
+  /**
+   * Provides credentials for authentication. Can be set directly or will be created internally
+   * if username/password are provided instead. If both are supplied, this credentialsProvider
+   * takes precedence over username/password.
+   */
+  credentialsProvider?: CredentialsProvider;
+  /**
+   * Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))
+   */
+  name?: string;
+  /**
+   * Redis database number (see [`SELECT`](https://redis.io/commands/select) command)
+   */
+  database?: number;
+  /**
+   * Maximum length of the client's internal command queue
+   */
+  commandsQueueMaxLength?: number;
+  /**
+   * When `true`, commands are rejected when the client is reconnecting.
+   * When `false`, commands are queued for execution after reconnection.
+   */
+  disableOfflineQueue?: boolean;
+  /**
+   * Connect in [`READONLY`](https://redis.io/commands/readonly) mode
+   */
+  readonly?: boolean;
+  /**
+   * Send `PING` command at interval (in ms).
+   * Useful with Redis deployments that do not honor TCP Keep-Alive.
+   */
+  pingInterval?: number;
+  /**
+   * Default command options to be applied to all commands executed through this client.
+   *
+   * These options can be overridden on a per-command basis when calling specific commands.
+   *
+   * @property {symbol} [chainId] - Identifier for chaining commands together
+   * @property {boolean} [asap] - When true, the command is executed as soon as possible
+   * @property {AbortSignal} [abortSignal] - AbortSignal to cancel the command
+   * @property {TypeMapping} [typeMapping] - Custom type mappings between RESP and JavaScript types
+   *
+   * @example Setting default command options
+   * ```
+   * const client = createClient({
+   *   commandOptions: {
+   *     asap: true,
+   *     typeMapping: {
+   *       // Custom type mapping configuration
+   *     }
+   *   }
+   * });
+   * ```
+   */
+  commandOptions?: CommandOptions<TYPE_MAPPING>;
+  /**
+   * Client Side Caching configuration.
+   *
+   * Enables Redis Servers and Clients to work together to cache results from commands
+   * sent to a server. The server will notify the client when cached results are no longer valid.
+   *
+   * Note: Client Side Caching is only supported with RESP3.
+   *
+   * @example Anonymous cache configuration
+   * ```
+   * const client = createClient({
+   *   RESP: 3,
+   *   clientSideCache: {
+   *     ttl: 0,
+   *     maxEntries: 0,
+   *     evictPolicy: "LRU"
+   *   }
+   * });
+   * ```
+   *
+   * @example Using a controllable cache
+   * ```
+   * const cache = new BasicClientSideCache({
+   *   ttl: 0,
+   *   maxEntries: 0,
+   *   evictPolicy: "LRU"
+   * });
+   * const client = createClient({
+   *   RESP: 3,
+   *   clientSideCache: cache
+   * });
+   * ```
+   */
+  clientSideCache?: ClientSideCacheProvider | ClientSideCacheConfig;
+  /**
+   * If set to true, disables sending client identifier (user-agent like message) to the redis server
+   */
+  disableClientInfo?: boolean;
+  /**
+   * Tag to append to library name that is sent to the Redis server
+   */
+  clientInfoTag?: string;
 }
 
-type WithCommands = {
-    [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
-};
-
-export type WithModules<M extends RedisModules> = {
-    [P in keyof M as ExcludeMappedString<P>]: {
-        [C in keyof M[P] as ExcludeMappedString<C>]: RedisCommandSignature<M[P][C]>;
+type WithCommands<
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+    [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
+  };
+
+type WithModules<
+  M extends RedisModules,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+    [P in keyof M]: {
+      [C in keyof M[P]]: CommandSignature<M[P][C], RESP, TYPE_MAPPING>;
     };
-};
-
-export type WithFunctions<F extends RedisFunctions> = {
-    [P in keyof F as ExcludeMappedString<P>]: {
-        [FF in keyof F[P] as ExcludeMappedString<FF>]: RedisCommandSignature<F[P][FF]>;
+  };
+
+type WithFunctions<
+  F extends RedisFunctions,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+    [L in keyof F]: {
+      [C in keyof F[L]]: CommandSignature<F[L][C], RESP, TYPE_MAPPING>;
     };
-};
-
-export type WithScripts<S extends RedisScripts> = {
-    [P in keyof S as ExcludeMappedString<P>]: RedisCommandSignature<S[P]>;
-};
+  };
+
+type WithScripts<
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+    [P in keyof S]: CommandSignature<S[P], RESP, TYPE_MAPPING>;
+  };
+
+export type RedisClientExtensions<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> = (
+    WithCommands<RESP, TYPE_MAPPING> &
+    WithModules<M, RESP, TYPE_MAPPING> &
+    WithFunctions<F, RESP, TYPE_MAPPING> &
+    WithScripts<S, RESP, TYPE_MAPPING>
+  );
 
 export type RedisClientType<
-    M extends RedisModules = Record<string, never>,
-    F extends RedisFunctions = Record<string, never>,
-    S extends RedisScripts = Record<string, never>
-> = RedisClient<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
-
-export type InstantiableRedisClient<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = new (options?: RedisClientOptions<M, F, S>) => RedisClientType<M, F, S>;
-
-export interface ClientCommandOptions extends QueueCommandOptions {
-    isolated?: boolean;
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> = (
+    RedisClient<M, F, S, RESP, TYPE_MAPPING> &
+    RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
+  );
+
+type ProxyClient = RedisClient<any, any, any, any, any>;
+
+type NamespaceProxyClient = { _self: ProxyClient };
+
+interface ScanIteratorOptions {
+  cursor?: RedisArgument;
 }
 
-type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
+export type MonitorCallback<TYPE_MAPPING extends TypeMapping = TypeMapping> = (reply: ReplyWithTypeMapping<SimpleStringReply, TYPE_MAPPING>) => unknown;
 
 export default class RedisClient<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > extends EventEmitter {
-    static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
-        return commandOptions(options);
-    }
-
-    commandOptions = RedisClient.commandOptions;
-
-    static extend<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClient<M, F, S> {
-        const Client = attachExtensions({
-            BaseClass: RedisClient,
-            modulesExecutor: RedisClient.prototype.commandsExecutor,
-            modules: extensions?.modules,
-            functionsExecutor: RedisClient.prototype.functionsExecuter,
-            functions: extensions?.functions,
-            scriptsExecutor: RedisClient.prototype.scriptsExecuter,
-            scripts: extensions?.scripts
-        });
+  static #createCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
 
-        if (Client !== RedisClient) {
-            Client.prototype.Multi = RedisClientMultiCommand.extend(extensions);
-        }
-
-        return Client;
-    }
+    return async function (this: ProxyClient, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
 
-    static create<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(options?: RedisClientOptions<M, F, S>): RedisClientType<M, F, S> {
-        return new (RedisClient.extend(options))(options);
+      return this._self._executeCommand(command, parser, this._commandOptions, transformReply);
     }
+  }
 
-    static parseURL(url: string): RedisClientOptions {
-        // https://www.iana.org/assignments/uri-schemes/prov/redis
-        const { hostname, port, protocol, username, password, pathname } = new URL(url),
-            parsed: RedisClientOptions = {
-                socket: {
-                    host: hostname
-                }
-            };
+  static #createModuleCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
 
-        if (protocol === 'rediss:') {
-            (parsed.socket as RedisTlsSocketOptions).tls = true;
-        } else if (protocol !== 'redis:') {
-            throw new TypeError('Invalid protocol');
-        }
-
-        if (port) {
-            (parsed.socket as TcpSocketConnectOpts).port = Number(port);
-        }
+    return async function (this: NamespaceProxyClient, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
 
-        if (username) {
-            parsed.username = decodeURIComponent(username);
-        }
+      return this._self._executeCommand(command, parser, this._self._commandOptions, transformReply);
+    };
+  }
 
-        if (password) {
-            parsed.password = decodeURIComponent(password);
-        }
+  static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
+    const prefix = functionArgumentsPrefix(name, fn);
+    const transformReply = getTransformReply(fn, resp);
 
-        if (pathname.length > 1) {
-            const database = Number(pathname.substring(1));
-            if (isNaN(database)) {
-                throw new TypeError('Invalid pathname');
-            }
+    return async function (this: NamespaceProxyClient, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      fn.parseCommand(parser, ...args);
 
-            parsed.database = database;
-        }
+      return this._self._executeCommand(fn, parser, this._self._commandOptions, transformReply);
+    };
+  }
 
-        return parsed;
-    }
+  static #createScriptCommand(script: RedisScript, resp: RespVersions) {
+    const prefix = scriptArgumentsPrefix(script);
+    const transformReply = getTransformReply(script, resp);
 
-    readonly #options?: RedisClientOptions<M, F, S>;
-    readonly #socket: RedisSocket;
-    readonly #queue: RedisCommandsQueue;
-    #isolationPool?: Pool<RedisClientType<M, F, S>>;
-    readonly #v4: Record<string, any> = {};
-    #selectedDB = 0;
+    return async function (this: ProxyClient, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      script.parseCommand(parser, ...args)
 
-    get options(): RedisClientOptions<M, F, S> | undefined {
-        return this.#options;
+      return this._executeScript(script, parser, this._commandOptions, transformReply);
     }
+  }
 
-    get isOpen(): boolean {
-        return this.#socket.isOpen;
-    }
+  static #SingleEntryCache = new SingleEntryCache<any, any>()
 
-    get isReady(): boolean {
-        return this.#socket.isReady;
-    }
+  static factory<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2
+  >(config?: CommanderConfig<M, F, S, RESP>) {
 
-    get isPubSubActive() {
-        return this.#queue.isPubSubActive;
-    }
 
-    get v4(): Record<string, any> {
-        if (!this.#options?.legacyMode) {
-            throw new Error('the client is not in "legacy mode"');
-        }
+    let Client = RedisClient.#SingleEntryCache.get(config);
+    if (!Client) {
+      Client = attachConfig({
+        BaseClass: RedisClient,
+        commands: COMMANDS,
+        createCommand: RedisClient.#createCommand,
+        createModuleCommand: RedisClient.#createModuleCommand,
+        createFunctionCommand: RedisClient.#createFunctionCommand,
+        createScriptCommand: RedisClient.#createScriptCommand,
+        config
+      });
 
-        return this.#v4;
-    }
+      Client.prototype.Multi = RedisClientMultiCommand.extend(config);
 
-    constructor(options?: RedisClientOptions<M, F, S>) {
-        super();
-        this.#options = this.#initiateOptions(options);
-        this.#queue = this.#initiateQueue();
-        this.#socket = this.#initiateSocket();
-        // should be initiated in connect, not here
-        // TODO: consider breaking in v5
-        this.#isolationPool = this.#initiateIsolationPool();
-        this.#legacyMode();
+      RedisClient.#SingleEntryCache.set(config, Client);
     }
 
-    #initiateOptions(options?: RedisClientOptions<M, F, S>): RedisClientOptions<M, F, S> | undefined {
-        if (options?.url) {
-            const parsed = RedisClient.parseURL(options.url);
-            if (options.socket) {
-                parsed.socket = Object.assign(options.socket, parsed.socket);
-            }
-
-            Object.assign(options, parsed);
-        }
-
-        if (options?.database) {
-            this.#selectedDB = options.database;
+    return <TYPE_MAPPING extends TypeMapping = {}>(
+      options?: Omit<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>, keyof Exclude<typeof config, undefined>>
+    ) => {
+      // returning a "proxy" to prevent the namespaces._self to leak between "proxies"
+      return Object.create(new Client(options)) as RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+    };
+  }
+
+  static create<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(this: void, options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    return RedisClient.factory(options)(options);
+  }
+
+  static parseURL(url: string): RedisClientOptions {
+    // https://www.iana.org/assignments/uri-schemes/prov/redis
+    const { hostname, port, protocol, username, password, pathname } = new URL(url),
+      parsed: RedisClientOptions = {
+        socket: {
+          host: hostname
         }
+      };
 
-        return options;
+    if (protocol === 'rediss:') {
+      parsed!.socket!.tls = true;
+    } else if (protocol !== 'redis:') {
+      throw new TypeError('Invalid protocol');
     }
 
-    #initiateQueue(): RedisCommandsQueue {
-        return new RedisCommandsQueue(
-            this.#options?.commandsQueueMaxLength,
-            (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners)
-        );
+    if (port) {
+      (parsed.socket as TcpSocketConnectOpts).port = Number(port);
     }
 
-    #initiateSocket(): RedisSocket {
-        const socketInitiator = async (): Promise<void> => {
-            const promises = [];
-
-            if (this.#selectedDB !== 0) {
-                promises.push(
-                    this.#queue.addCommand(
-                        ['SELECT', this.#selectedDB.toString()],
-                        { asap: true }
-                    )
-                );
-            }
-
-            if (this.#options?.readonly) {
-                promises.push(
-                    this.#queue.addCommand(
-                        COMMANDS.READONLY.transformArguments(),
-                        { asap: true }
-                    )
-                );
-            }
-
-            if (!this.#options?.disableClientInfo) {
-                promises.push(
-                    this.#queue.addCommand(
-                        [  'CLIENT', 'SETINFO', 'LIB-VER', version],
-                        { asap: true }
-                    ).catch(err => {
-                        if (!(err instanceof ErrorReply)) {
-                            throw err;
-                        }
-                    })
-                );
-
-                promises.push(
-                    this.#queue.addCommand(
-                        [
-                           'CLIENT', 'SETINFO', 'LIB-NAME',
-                            this.#options?.clientInfoTag ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis'
-                        ],
-                        { asap: true }
-                    ).catch(err => {
-                        if (!(err instanceof ErrorReply)) {
-                            throw err;
-                        }
-                    })
-                );
-              }
-
-            if (this.#options?.name) {
-                promises.push(
-                    this.#queue.addCommand(
-                        COMMANDS.CLIENT_SETNAME.transformArguments(this.#options.name),
-                        { asap: true }
-                    )
-                );
-            }
-
-            if (this.#options?.username || this.#options?.password) {
-                promises.push(
-                    this.#queue.addCommand(
-                        COMMANDS.AUTH.transformArguments({
-                            username: this.#options.username,
-                            password: this.#options.password ?? ''
-                        }),
-                        { asap: true }
-                    )
-                );
-            }
-
-            const resubscribePromise = this.#queue.resubscribe();
-            if (resubscribePromise) {
-                promises.push(resubscribePromise);
-            }
-
-            if (promises.length) {
-                this.#tick(true);
-                await Promise.all(promises);
-            }
-        };
-
-        return new RedisSocket(socketInitiator, this.#options?.socket)
-            .on('data', chunk => this.#queue.onReplyChunk(chunk))
-            .on('error', err => {
-                this.emit('error', err);
-                if (this.#socket.isOpen && !this.#options?.disableOfflineQueue) {
-                    this.#queue.flushWaitingForReply(err);
-                } else {
-                    this.#queue.flushAll(err);
-                }
-            })
-            .on('connect', () => {
-                this.emit('connect');
-            })
-            .on('ready', () => {
-                this.emit('ready');
-                this.#setPingTimer();
-                this.#tick();
-            })
-            .on('reconnecting', () => this.emit('reconnecting'))
-            .on('drain', () => this.#tick())
-            .on('end', () => this.emit('end'));
-    }
-
-    #initiateIsolationPool() {
-        return createPool({
-            create: async () => {
-                const duplicate = this.duplicate({
-                    isolationPoolOptions: undefined
-                }).on('error', err => this.emit('error', err));
-                await duplicate.connect();
-                return duplicate;
-            },
-            destroy: client => client.disconnect()
-        }, this.#options?.isolationPoolOptions);
-    }
-
-    #legacyMode(): void {
-        if (!this.#options?.legacyMode) return;
-
-        (this as any).#v4.sendCommand = this.#sendCommand.bind(this);
-        (this as any).sendCommand = (...args: Array<any>): void => {
-            const result = this.#legacySendCommand(...args);
-            if (result) {
-                result.promise
-                    .then(reply => result.callback(null, reply))
-                    .catch(err => result.callback(err));
-            }
-        };
-
-        for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
-            this.#defineLegacyCommand(name, command);
-            (this as any)[name.toLowerCase()] ??= (this as any)[name];
-        }
-
-        // hard coded commands
-        this.#defineLegacyCommand('SELECT');
-        this.#defineLegacyCommand('select');
-        this.#defineLegacyCommand('SUBSCRIBE');
-        this.#defineLegacyCommand('subscribe');
-        this.#defineLegacyCommand('PSUBSCRIBE');
-        this.#defineLegacyCommand('pSubscribe');
-        this.#defineLegacyCommand('UNSUBSCRIBE');
-        this.#defineLegacyCommand('unsubscribe');
-        this.#defineLegacyCommand('PUNSUBSCRIBE');
-        this.#defineLegacyCommand('pUnsubscribe');
-        this.#defineLegacyCommand('QUIT');
-        this.#defineLegacyCommand('quit');
-    }
-
-    #legacySendCommand(...args: Array<any>) {
-        const callback = typeof args[args.length - 1] === 'function' ?
-            args.pop() as ClientLegacyCallback :
-            undefined;
-
-        const promise = this.#sendCommand(transformLegacyCommandArguments(args));
-        if (callback) return {
-            promise,
-            callback
-        };
-        promise.catch(err => this.emit('error', err));
-    }
-
-    #defineLegacyCommand(name: string, command?: RedisCommand): void {
-        this.#v4[name] = (this as any)[name].bind(this);
-        (this as any)[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
-            (...args: Array<unknown>) => {
-                const result = this.#legacySendCommand(name, ...args);
-                if (result) {
-                    result.promise
-                        .then(reply => result.callback(null, command.transformReply!(reply)))
-                        .catch(err => result.callback(err));
-                }
-            } :
-            (...args: Array<unknown>) => (this as any).sendCommand(name, ...args);
-    }
-
-    #pingTimer?: NodeJS.Timeout;
-
-    #setPingTimer(): void {
-        if (!this.#options?.pingInterval || !this.#socket.isReady) return;
-        clearTimeout(this.#pingTimer);
-
-        this.#pingTimer = setTimeout(() => {
-            if (!this.#socket.isReady) return;
-
-            // using #sendCommand to support legacy mode
-            this.#sendCommand(['PING'])
-                .then(reply => this.emit('ping-interval', reply))
-                .catch(err => this.emit('error', err))
-                .finally(() => this.#setPingTimer());
-        }, this.#options.pingInterval);
-    }
-
-    duplicate(overrides?: Partial<RedisClientOptions<M, F, S>>): RedisClientType<M, F, S> {
-        return new (Object.getPrototypeOf(this).constructor)({
-            ...this.#options,
-            ...overrides
-        });
+    if (username) {
+      parsed.username = decodeURIComponent(username);
     }
 
-    async connect() {
-        // see comment in constructor
-        this.#isolationPool ??= this.#initiateIsolationPool();
-        await this.#socket.connect();
-        return this as unknown as RedisClientType<M, F, S>;
+    if (password) {
+      parsed.password = decodeURIComponent(password);
     }
 
-    async commandsExecutor<C extends RedisCommand>(
-        command: C,
-        args: Array<unknown>
-    ): Promise<RedisCommandReply<C>> {
-        const { args: redisArgs, options } = transformCommandArguments(command, args);
-        return transformCommandReply(
-            command,
-            await this.#sendCommand(redisArgs, options),
-            redisArgs.preserve
-        );
+    if (username || password) {
+      parsed.credentialsProvider = {
+        type: 'async-credentials-provider',
+        credentials: async () => (
+          {
+            username: username ? decodeURIComponent(username) : undefined,
+            password: password ? decodeURIComponent(password) : undefined
+          })
+      };
     }
 
-    sendCommand<T = RedisCommandRawReply>(
-        args: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<T> {
-        return this.#sendCommand(args, options);
-    }
-
-    // using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
-    #sendCommand<T = RedisCommandRawReply>(
-        args: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<T> {
-        if (!this.#socket.isOpen) {
-            return Promise.reject(new ClientClosedError());
-        } else if (options?.isolated) {
-            return this.executeIsolated(isolatedClient =>
-                isolatedClient.sendCommand(args, {
-                    ...options,
-                    isolated: false
-                })
-            );
-        } else if (!this.#socket.isReady && this.#options?.disableOfflineQueue) {
-            return Promise.reject(new ClientOfflineError());
-        }
+    if (pathname.length > 1) {
+      const database = Number(pathname.substring(1));
+      if (isNaN(database)) {
+        throw new TypeError('Invalid pathname');
+      }
 
-        const promise = this.#queue.addCommand<T>(args, options);
-        this.#tick();
-        return promise;
-    }
-
-    async functionsExecuter<F extends RedisFunction>(
-        fn: F,
-        args: Array<unknown>,
-        name: string
-    ): Promise<RedisCommandReply<F>> {
-        const { args: redisArgs, options } = transformCommandArguments(fn, args);
-        return transformCommandReply(
-            fn,
-            await this.executeFunction(name, fn, redisArgs, options),
-            redisArgs.preserve
-        );
+      parsed.database = database;
     }
 
-    executeFunction(
-        name: string,
-        fn: RedisFunction,
-        args: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<RedisCommandRawReply> {
-        return this.#sendCommand(
-            fCallArguments(name, fn, args),
-            options
-        );
+    return parsed;
+  }
+
+  readonly #options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>;
+  readonly #socket: RedisSocket;
+  readonly #queue: RedisCommandsQueue;
+  #selectedDB = 0;
+  #monitorCallback?: MonitorCallback<TYPE_MAPPING>;
+  private _self = this;
+  private _commandOptions?: CommandOptions<TYPE_MAPPING>;
+  // flag used to annotate that the client
+  // was in a watch transaction when
+  // a topology change occured
+  #dirtyWatch?: string;
+  #watchEpoch?: number;
+  #clientSideCache?: ClientSideCacheProvider;
+  #credentialsSubscription: Disposable | null = null;
+  get clientSideCache() {
+    return this._self.#clientSideCache;
+  }
+
+
+  get options(): RedisClientOptions<M, F, S, RESP> | undefined {
+    return this._self.#options;
+  }
+
+  get isOpen(): boolean {
+    return this._self.#socket.isOpen;
+  }
+
+  get isReady(): boolean {
+    return this._self.#socket.isReady;
+  }
+
+  get isPubSubActive() {
+    return this._self.#queue.isPubSubActive;
+  }
+
+  get socketEpoch() {
+    return this._self.#socket.socketEpoch;
+  }
+
+  get isWatching() {
+    return this._self.#watchEpoch !== undefined;
+  }
+
+  /**
+   * Indicates whether the client's WATCH command has been invalidated by a topology change.
+   * When this returns true, any transaction using WATCH will fail with a WatchError.
+   * @returns true if the watched keys have been modified, false otherwise
+   */
+  get isDirtyWatch(): boolean {
+    return this._self.#dirtyWatch !== undefined
+  }
+
+  /**
+   * Marks the client's WATCH command as invalidated due to a topology change.
+   * This will cause any subsequent EXEC in a transaction to fail with a WatchError.
+   * @param msg - The error message explaining why the WATCH is dirty
+   */
+  setDirtyWatch(msg: string) {
+    this._self.#dirtyWatch = msg;
+  }
+
+  constructor(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    super();
+    this.#validateOptions(options)
+    this.#options = this.#initiateOptions(options);
+    this.#queue = this.#initiateQueue();
+    this.#socket = this.#initiateSocket();
+
+    if (options?.clientSideCache) {
+      if (options.clientSideCache instanceof ClientSideCacheProvider) {
+        this.#clientSideCache = options.clientSideCache;
+      } else {
+        const cscConfig = options.clientSideCache;
+        this.#clientSideCache = new BasicClientSideCache(cscConfig);
+      }
+      this.#queue.setInvalidateCallback(this.#clientSideCache.invalidate.bind(this.#clientSideCache));
     }
+  }
 
-    async scriptsExecuter<S extends RedisScript>(
-        script: S,
-        args: Array<unknown>
-    ): Promise<RedisCommandReply<S>> {
-        const { args: redisArgs, options } = transformCommandArguments(script, args);
-        return transformCommandReply(
-            script,
-            await this.executeScript(script, redisArgs, options),
-            redisArgs.preserve
-        );
+  #validateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    if (options?.clientSideCache && options?.RESP !== 3) {
+      throw new Error('Client Side Caching is only supported with RESP3');
     }
 
-    async executeScript(
-        script: RedisScript,
-        args: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<RedisCommandRawReply> {
-        const redisArgs: RedisCommandArguments = ['EVALSHA', script.SHA1];
+  }
+  #initiateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> | undefined {
 
-        if (script.NUMBER_OF_KEYS !== undefined) {
-            redisArgs.push(script.NUMBER_OF_KEYS.toString());
-        }
-
-        redisArgs.push(...args);
+    // Convert username/password to credentialsProvider if no credentialsProvider is already in place
+    if (!options?.credentialsProvider && (options?.username || options?.password)) {
 
-        try {
-            return await this.#sendCommand(redisArgs, options);
-        } catch (err: any) {
-            if (!err?.message?.startsWith?.('NOSCRIPT')) {
-                throw err;
-            }
-
-            redisArgs[0] = 'EVAL';
-            redisArgs[1] = script.SCRIPT;
-            return this.#sendCommand(redisArgs, options);
-        }
+      options.credentialsProvider = {
+        type: 'async-credentials-provider',
+        credentials: async () => ({
+          username: options.username,
+          password: options.password
+        })
+      };
     }
 
-    async SELECT(db: number): Promise<void>;
-    async SELECT(options: CommandOptions<ClientCommandOptions>, db: number): Promise<void>;
-    async SELECT(options?: any, db?: any): Promise<void> {
-        if (!isCommandOptions(options)) {
-            db = options;
-            options = null;
-        }
+    if (options?.url) {
+      const parsed = RedisClient.parseURL(options.url);
+      if (options.socket) {
+        parsed.socket = Object.assign(options.socket, parsed.socket);
+      }
 
-        await this.#sendCommand(['SELECT', db.toString()], options);
-        this.#selectedDB = db;
+      Object.assign(options, parsed);
     }
 
-    select = this.SELECT;
-
-    #pubSubCommand(promise: Promise<void> | undefined) {
-        if (promise === undefined) return Promise.resolve();
-
-        this.#tick();
-        return promise;
+    if (options?.database) {
+      this._self.#selectedDB = options.database;
     }
 
-    SUBSCRIBE<T extends boolean = false>(
-        channels: string | Array<string>,
-        listener: PubSubListener<T>,
-        bufferMode?: T
-    ): Promise<void> {
-        return this.#pubSubCommand(
-            this.#queue.subscribe(
-                PubSubType.CHANNELS,
-                channels,
-                listener,
-                bufferMode
-            )
-        );
+    if (options?.commandOptions) {
+      this._commandOptions = options.commandOptions;
     }
 
-    subscribe = this.SUBSCRIBE;
-
-
-    UNSUBSCRIBE<T extends boolean = false>(
-        channels?: string | Array<string>,
-        listener?: PubSubListener<T>,
-        bufferMode?: T
-    ): Promise<void> {
-        return this.#pubSubCommand(
-            this.#queue.unsubscribe(
-                PubSubType.CHANNELS,
-                channels,
-                listener,
-                bufferMode
-            )
-        );
+    return options;
+  }
+
+  #initiateQueue(): RedisCommandsQueue {
+    return new RedisCommandsQueue(
+      this.#options?.RESP ?? 2,
+      this.#options?.commandsQueueMaxLength,
+      (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners)
+    );
+  }
+
+  /**
+   * @param credentials
+   */
+  private reAuthenticate = async (credentials: BasicAuth) => {
+    // Re-authentication is not supported on RESP2 with PubSub active
+    if (!(this.isPubSubActive && !this.#options?.RESP)) {
+      await this.sendCommand(
+        parseArgs(COMMANDS.AUTH, {
+          username: credentials.username,
+          password: credentials.password ?? ''
+        })
+      );
     }
+  }
+
+  #subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> {
+    return cp.subscribe({
+      onNext: credentials => {
+        this.reAuthenticate(credentials).catch(error => {
+          const errorMessage = error instanceof Error ? error.message : String(error);
+          cp.onReAuthenticationError(new CredentialsError(errorMessage));
+        });
 
-    unsubscribe = this.UNSUBSCRIBE;
-
-    PSUBSCRIBE<T extends boolean = false>(
-        patterns: string | Array<string>,
-        listener: PubSubListener<T>,
-        bufferMode?: T
-    ): Promise<void> {
-        return this.#pubSubCommand(
-            this.#queue.subscribe(
-                PubSubType.PATTERNS,
-                patterns,
-                listener,
-                bufferMode
-            )
-        );
+      },
+      onError: (e: Error) => {
+        const errorMessage = `Error from streaming credentials provider: ${e.message}`;
+        cp.onReAuthenticationError(new UnableToObtainNewCredentialsError(errorMessage));
+      }
+    });
+  }
+
+  async #handshake(chainId: symbol, asap: boolean) {
+    const promises = [];
+    const commandsWithErrorHandlers = await this.#getHandshakeCommands();
+
+    if (asap) commandsWithErrorHandlers.reverse()
+
+    for (const { cmd, errorHandler } of commandsWithErrorHandlers) {
+      promises.push(
+        this.#queue
+          .addCommand(cmd, {
+            chainId,
+            asap
+          })
+          .catch(errorHandler)
+      );
     }
+    return promises;
+  }
+
+  async #getHandshakeCommands(): Promise<
+    Array<{ cmd: CommandArguments } & { errorHandler?: (err: Error) => void }>
+  > {
+    const commands = [];
+    const cp = this.#options?.credentialsProvider;
+
+    if (this.#options?.RESP) {
+      const hello: HelloOptions = {};
+
+      if (cp && cp.type === 'async-credentials-provider') {
+        const credentials = await cp.credentials();
+        if (credentials.password) {
+          hello.AUTH = {
+            username: credentials.username ?? 'default',
+            password: credentials.password
+          };
+        }
+      }
+
+      if (cp && cp.type === 'streaming-credentials-provider') {
+        const [credentials, disposable] =
+          await this.#subscribeForStreamingCredentials(cp);
+        this.#credentialsSubscription = disposable;
+
+        if (credentials.password) {
+          hello.AUTH = {
+            username: credentials.username ?? 'default',
+            password: credentials.password
+          };
+        }
+      }
+
+      if (this.#options.name) {
+        hello.SETNAME = this.#options.name;
+      }
+
+      commands.push({ cmd: parseArgs(HELLO, this.#options.RESP, hello) });
+    } else {
+      if (cp && cp.type === 'async-credentials-provider') {
+        const credentials = await cp.credentials();
+
+        if (credentials.username || credentials.password) {
+          commands.push({
+            cmd: parseArgs(COMMANDS.AUTH, {
+              username: credentials.username,
+              password: credentials.password ?? ''
+            })
+          });
+        }
+      }
+
+      if (cp && cp.type === 'streaming-credentials-provider') {
+        const [credentials, disposable] =
+          await this.#subscribeForStreamingCredentials(cp);
+        this.#credentialsSubscription = disposable;
+
+        if (credentials.username || credentials.password) {
+          commands.push({
+            cmd: parseArgs(COMMANDS.AUTH, {
+              username: credentials.username,
+              password: credentials.password ?? ''
+            })
+          });
+        }
+      }
 
-    pSubscribe = this.PSUBSCRIBE;
-
-    PUNSUBSCRIBE<T extends boolean = false>(
-        patterns?: string | Array<string>,
-        listener?: PubSubListener<T>,
-        bufferMode?: T
-    ): Promise<void> {
-        return this.#pubSubCommand(
-            this.#queue.unsubscribe(
-                PubSubType.PATTERNS,
-                patterns,
-                listener,
-                bufferMode
-            )
-        );
+      if (this.#options?.name) {
+        commands.push({
+          cmd: parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name)
+        });
+      }
     }
 
-    pUnsubscribe = this.PUNSUBSCRIBE;
-
-    SSUBSCRIBE<T extends boolean = false>(
-        channels: string | Array<string>,
-        listener: PubSubListener<T>,
-        bufferMode?: T
-    ): Promise<void> {
-        return this.#pubSubCommand(
-            this.#queue.subscribe(
-                PubSubType.SHARDED,
-                channels,
-                listener,
-                bufferMode
-            )
-        );
+    if (this.#selectedDB !== 0) {
+      commands.push({ cmd: ['SELECT', this.#selectedDB.toString()] });
     }
 
-    sSubscribe = this.SSUBSCRIBE;
-
-    SUNSUBSCRIBE<T extends boolean = false>(
-        channels?: string | Array<string>,
-        listener?: PubSubListener<T>,
-        bufferMode?: T
-    ): Promise<void> {
-        return this.#pubSubCommand(
-            this.#queue.unsubscribe(
-                PubSubType.SHARDED,
-                channels,
-                listener,
-                bufferMode
-            )
-        );
+    if (this.#options?.readonly) {
+      commands.push({ cmd: parseArgs(COMMANDS.READONLY) });
     }
 
-    sUnsubscribe = this.SUNSUBSCRIBE;
-
-    getPubSubListeners(type: PubSubType) {
-        return this.#queue.getPubSubListeners(type);
+    if (!this.#options?.disableClientInfo) {
+      commands.push({
+        cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version],
+        errorHandler: () => {
+          // Client libraries are expected to pipeline this command
+          // after authentication on all connections and ignore failures
+          // since they could be connected to an older version that doesn't support them.
+        }
+      });
+
+      commands.push({
+        cmd: [
+          'CLIENT',
+          'SETINFO',
+          'LIB-NAME',
+          this.#options?.clientInfoTag
+            ? `node-redis(${this.#options.clientInfoTag})`
+            : 'node-redis'
+        ],
+        errorHandler: () => {
+          // Client libraries are expected to pipeline this command
+          // after authentication on all connections and ignore failures
+          // since they could be connected to an older version that doesn't support them.
+        }
+      });
     }
 
-    extendPubSubChannelListeners(
-        type: PubSubType,
-        channel: string,
-        listeners: ChannelListeners
-    ) {
-        return this.#pubSubCommand(
-            this.#queue.extendPubSubChannelListeners(type, channel, listeners)
-        );
+    if (this.#clientSideCache) {
+      commands.push({cmd: this.#clientSideCache.trackingOn()});
     }
 
-    extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
-        return this.#pubSubCommand(
-            this.#queue.extendPubSubListeners(type, listeners)
+    return commands;
+  }
+
+  #initiateSocket(): RedisSocket {
+    const socketInitiator = async () => {
+      const promises = [],
+        chainId = Symbol('Socket Initiator');
+
+      const resubscribePromise = this.#queue.resubscribe(chainId);
+      if (resubscribePromise) {
+        promises.push(resubscribePromise);
+      }
+
+      if (this.#monitorCallback) {
+        promises.push(
+          this.#queue.monitor(
+            this.#monitorCallback,
+            {
+              typeMapping: this._commandOptions?.typeMapping,
+              chainId,
+              asap: true
+            }
+          )
         );
-    }
+      }
 
-    QUIT(): Promise<string> {
-        return this.#socket.quit(async () => {
-            if (this.#pingTimer) clearTimeout(this.#pingTimer);
-            const quitPromise = this.#queue.addCommand<string>(['QUIT']);
-            this.#tick();
-            const [reply] = await Promise.all([
-                quitPromise,
-                this.#destroyIsolationPool()
-            ]);
-            return reply;
-        });
-    }
+      promises.push(...(await this.#handshake(chainId, true)));
 
-    quit = this.QUIT;
+      if (promises.length) {
+        this.#write();
+        return Promise.all(promises);
+      }
+    };
 
-    #tick(force = false): void {
-        if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) {
-            return;
+    return new RedisSocket(socketInitiator, this.#options?.socket)
+      .on('data', chunk => {
+        try {
+          this.#queue.decoder.write(chunk);
+        } catch (err) {
+          this.#queue.resetDecoder();
+          this.emit('error', err);
         }
-
-        this.#socket.cork();
-
-        while (!this.#socket.writableNeedDrain) {
-            const args = this.#queue.getCommandToSend();
-            if (args === undefined) break;
-
-            this.#socket.writeCommand(args);
+      })
+      .on('error', err => {
+        this.emit('error', err);
+        this.#clientSideCache?.onError();
+        if (this.#socket.isOpen && !this.#options?.disableOfflineQueue) {
+          this.#queue.flushWaitingForReply(err);
+        } else {
+          this.#queue.flushAll(err);
         }
+      })
+      .on('connect', () => this.emit('connect'))
+      .on('ready', () => {
+        this.emit('ready');
+        this.#setPingTimer();
+        this.#maybeScheduleWrite();
+      })
+      .on('reconnecting', () => this.emit('reconnecting'))
+      .on('drain', () => this.#maybeScheduleWrite())
+      .on('end', () => this.emit('end'));
+  }
+
+  #pingTimer?: NodeJS.Timeout;
+
+  #setPingTimer(): void {
+    if (!this.#options?.pingInterval || !this.#socket.isReady) return;
+    clearTimeout(this.#pingTimer);
+
+    this.#pingTimer = setTimeout(() => {
+      if (!this.#socket.isReady) return;
+
+      this.sendCommand(['PING'])
+        .then(reply => this.emit('ping-interval', reply))
+        .catch(err => this.emit('error', err))
+        .finally(() => this.#setPingTimer());
+    }, this.#options.pingInterval);
+  }
+
+  withCommandOptions<
+    OPTIONS extends CommandOptions<TYPE_MAPPING>,
+    TYPE_MAPPING extends TypeMapping
+  >(options: OPTIONS) {
+    const proxy = Object.create(this._self);
+    proxy._commandOptions = options;
+    return proxy as RedisClientType<
+      M,
+      F,
+      S,
+      RESP,
+      TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
+    >;
+  }
+
+  private _commandOptionsProxy<
+    K extends keyof CommandOptions,
+    V extends CommandOptions[K]
+  >(
+    key: K,
+    value: V
+  ) {
+    const proxy = Object.create(this._self);
+    proxy._commandOptions = Object.create(this._commandOptions ?? null);
+    proxy._commandOptions[key] = value;
+    return proxy as RedisClientType<
+      M,
+      F,
+      S,
+      RESP,
+      K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
+    >;
+  }
+
+  /**
+   * Override the `typeMapping` command option
+   */
+  withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
+    return this._commandOptionsProxy('typeMapping', typeMapping);
+  }
+
+  /**
+   * Override the `abortSignal` command option
+   */
+  withAbortSignal(abortSignal: AbortSignal) {
+    return this._commandOptionsProxy('abortSignal', abortSignal);
+  }
+
+  /**
+   * Override the `asap` command option to `true`
+   */
+  asap() {
+    return this._commandOptionsProxy('asap', true);
+  }
+
+  /**
+   * Create the "legacy" (v3/callback) interface
+   */
+  legacy(): RedisLegacyClientType {
+    return new RedisLegacyClient(
+      this as unknown as RedisClientType<M, F, S>
+    ) as RedisLegacyClientType;
+  }
+
+  /**
+   * Create {@link RedisClientPool `RedisClientPool`} using this client as a prototype
+   */
+  createPool(options?: Partial<RedisPoolOptions>) {
+    return RedisClientPool.create(
+      this._self.#options,
+      options
+    );
+  }
+
+  duplicate<
+    _M extends RedisModules = M,
+    _F extends RedisFunctions = F,
+    _S extends RedisScripts = S,
+    _RESP extends RespVersions = RESP,
+    _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING
+  >(overrides?: Partial<RedisClientOptions<_M, _F, _S, _RESP, _TYPE_MAPPING>>) {
+    return new (Object.getPrototypeOf(this).constructor)({
+      ...this._self.#options,
+      commandOptions: this._commandOptions,
+      ...overrides
+    }) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>;
+  }
+
+  async connect() {
+    await this._self.#socket.connect();
+    return this as unknown as RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  /**
+   * @internal
+   */
+  async _executeCommand(
+    command: Command,
+    parser: CommandParser,
+    commandOptions: CommandOptions<TYPE_MAPPING> | undefined,
+    transformReply: TransformReply | undefined,
+  ) {
+    const csc = this._self.#clientSideCache;
+    const defaultTypeMapping = this._self.#options?.commandOptions === commandOptions;
+
+    const fn = () => { return this.sendCommand(parser.redisArgs, commandOptions) };
+
+    if (csc && command.CACHEABLE && defaultTypeMapping) {
+      return await csc.handleCache(this._self, parser as BasicCommandParser, fn, transformReply, commandOptions?.typeMapping);
+    } else {
+      const reply = await fn();
+
+      if (transformReply) {
+        return transformReply(reply, parser.preserve, commandOptions?.typeMapping);
+      }
+      return reply;
     }
-
-    executeIsolated<T>(fn: (client: RedisClientType<M, F, S>) => T | Promise<T>): Promise<T> {
-        if (!this.#isolationPool) return Promise.reject(new ClientClosedError());
-        return this.#isolationPool.use(fn);
+  }
+
+  /**
+   * @internal
+   */
+  async _executeScript(
+    script: RedisScript,
+    parser: CommandParser,
+    options: CommandOptions | undefined,
+    transformReply: TransformReply | undefined,
+  ) {
+    const args = parser.redisArgs as Array<RedisArgument>;
+
+    let reply: ReplyUnion;
+    try {
+      reply = await this.sendCommand(args, options);
+    } catch (err) {
+      if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err;
+
+      args[0] = 'EVAL';
+      args[1] = script.SCRIPT;
+      reply = await this.sendCommand(args, options);
     }
 
-    MULTI(): RedisClientMultiCommandType<M, F, S> {
-        return new (this as any).Multi(
-            this.multiExecutor.bind(this),
-            this.#options?.legacyMode
-        );
+    return transformReply ?
+      transformReply(reply, parser.preserve, options?.typeMapping) :
+      reply;
+  }
+
+  sendCommand<T = ReplyUnion>(
+    args: ReadonlyArray<RedisArgument>,
+    options?: CommandOptions
+  ): Promise<T> {
+    if (!this._self.#socket.isOpen) {
+      return Promise.reject(new ClientClosedError());
+    } else if (!this._self.#socket.isReady && this._self.#options?.disableOfflineQueue) {
+      return Promise.reject(new ClientOfflineError());
     }
 
-    multi = this.MULTI;
-
-    async multiExecutor(
-        commands: Array<RedisMultiQueuedCommand>,
-        selectedDB?: number,
-        chainId?: symbol
-    ): Promise<Array<RedisCommandRawReply>> {
-        if (!this.#socket.isOpen) {
-            return Promise.reject(new ClientClosedError());
-        }
-
-        const promise = chainId ?
-            // if `chainId` has a value, it's a `MULTI` (and not "pipeline") - need to add the `MULTI` and `EXEC` commands
-            Promise.all([
-                this.#queue.addCommand(['MULTI'], { chainId }),
-                this.#addMultiCommands(commands, chainId),
-                this.#queue.addCommand(['EXEC'], { chainId })
-            ]) :
-            this.#addMultiCommands(commands);
-
-        this.#tick();
+    // Merge global options with provided options
+    const opts = {
+      ...this._self._commandOptions,
+      ...options
+    }
 
-        const results = await promise;
+    const promise = this._self.#queue.addCommand<T>(args, opts);
+    this._self.#scheduleWrite();
+    return promise;
+  }
+
+  async SELECT(db: number): Promise<void> {
+    await this.sendCommand(['SELECT', db.toString()]);
+    this._self.#selectedDB = db;
+  }
+
+  select = this.SELECT;
+
+  #pubSubCommand<T>(promise: Promise<T> | undefined) {
+    if (promise === undefined) return Promise.resolve();
+
+    this.#scheduleWrite();
+    return promise;
+  }
+
+  SUBSCRIBE<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ): Promise<void> {
+    return this._self.#pubSubCommand(
+      this._self.#queue.subscribe(
+        PUBSUB_TYPE.CHANNELS,
+        channels,
+        listener,
+        bufferMode
+      )
+    );
+  }
+
+  subscribe = this.SUBSCRIBE;
+
+  UNSUBSCRIBE<T extends boolean = false>(
+    channels?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ): Promise<void> {
+    return this._self.#pubSubCommand(
+      this._self.#queue.unsubscribe(
+        PUBSUB_TYPE.CHANNELS,
+        channels,
+        listener,
+        bufferMode
+      )
+    );
+  }
+
+  unsubscribe = this.UNSUBSCRIBE;
+
+  PSUBSCRIBE<T extends boolean = false>(
+    patterns: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ): Promise<void> {
+    return this._self.#pubSubCommand(
+      this._self.#queue.subscribe(
+        PUBSUB_TYPE.PATTERNS,
+        patterns,
+        listener,
+        bufferMode
+      )
+    );
+  }
+
+  pSubscribe = this.PSUBSCRIBE;
+
+  PUNSUBSCRIBE<T extends boolean = false>(
+    patterns?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ): Promise<void> {
+    return this._self.#pubSubCommand(
+      this._self.#queue.unsubscribe(
+        PUBSUB_TYPE.PATTERNS,
+        patterns,
+        listener,
+        bufferMode
+      )
+    );
+  }
+
+  pUnsubscribe = this.PUNSUBSCRIBE;
+
+  SSUBSCRIBE<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ): Promise<void> {
+    return this._self.#pubSubCommand(
+      this._self.#queue.subscribe(
+        PUBSUB_TYPE.SHARDED,
+        channels,
+        listener,
+        bufferMode
+      )
+    );
+  }
+
+  sSubscribe = this.SSUBSCRIBE;
+
+  SUNSUBSCRIBE<T extends boolean = false>(
+    channels?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ): Promise<void> {
+    return this._self.#pubSubCommand(
+      this._self.#queue.unsubscribe(
+        PUBSUB_TYPE.SHARDED,
+        channels,
+        listener,
+        bufferMode
+      )
+    );
+  }
+
+  sUnsubscribe = this.SUNSUBSCRIBE;
+
+  async WATCH(key: RedisVariadicArgument) {
+    const reply = await this._self.sendCommand(
+      pushVariadicArguments(['WATCH'], key)
+    );
+    this._self.#watchEpoch ??= this._self.socketEpoch;
+    return reply as unknown as ReplyWithTypeMapping<SimpleStringReply<'OK'>, TYPE_MAPPING>;
+  }
+
+  watch = this.WATCH;
+
+  async UNWATCH() {
+    const reply = await this._self.sendCommand(['UNWATCH']);
+    this._self.#watchEpoch = undefined;
+    return reply as unknown as ReplyWithTypeMapping<SimpleStringReply<'OK'>, TYPE_MAPPING>;
+  }
+
+  unwatch = this.UNWATCH;
+
+  getPubSubListeners(type: PubSubType) {
+    return this._self.#queue.getPubSubListeners(type);
+  }
+
+  extendPubSubChannelListeners(
+    type: PubSubType,
+    channel: string,
+    listeners: ChannelListeners
+  ) {
+    return this._self.#pubSubCommand(
+      this._self.#queue.extendPubSubChannelListeners(type, channel, listeners)
+    );
+  }
+
+  extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
+    return this._self.#pubSubCommand(
+      this._self.#queue.extendPubSubListeners(type, listeners)
+    );
+  }
+
+  #write() {
+    this.#socket.write(this.#queue.commandsToWrite());
+  }
+
+  #scheduledWrite?: NodeJS.Immediate;
+
+  #scheduleWrite() {
+    if (!this.#socket.isReady || this.#scheduledWrite) return;
+
+    this.#scheduledWrite = setImmediate(() => {
+      this.#write();
+      this.#scheduledWrite = undefined;
+    });
+  }
+
+  #maybeScheduleWrite() {
+    if (!this.#queue.isWaitingToWrite()) return;
+
+    this.#scheduleWrite();
+  }
+
+  /**
+   * @internal
+   */
+  async _executePipeline(
+    commands: Array<RedisMultiQueuedCommand>,
+    selectedDB?: number
+  ) {
+    if (!this._self.#socket.isOpen) {
+      return Promise.reject(new ClientClosedError());
+    }
 
-        if (selectedDB !== undefined) {
-            this.#selectedDB = selectedDB;
-        }
+    const chainId = Symbol('Pipeline Chain'),
+      promise = Promise.all(
+        commands.map(({ args }) => this._self.#queue.addCommand(args, {
+          chainId,
+          typeMapping: this._commandOptions?.typeMapping
+        }))
+      );
+    this._self.#scheduleWrite();
+    const result = await promise;
+
+    if (selectedDB !== undefined) {
+      this._self.#selectedDB = selectedDB;
+    }
 
-        return results;
+    return result;
+  }
+
+  /**
+   * @internal
+   */
+  async _executeMulti(
+    commands: Array<RedisMultiQueuedCommand>,
+    selectedDB?: number
+  ) {
+    const dirtyWatch = this._self.#dirtyWatch;
+    this._self.#dirtyWatch = undefined;
+    const watchEpoch = this._self.#watchEpoch;
+    this._self.#watchEpoch = undefined;
+
+    if (!this._self.#socket.isOpen) {
+      throw new ClientClosedError();
     }
 
-    #addMultiCommands(commands: Array<RedisMultiQueuedCommand>, chainId?: symbol) {
-        return Promise.all(
-            commands.map(({ args }) => this.#queue.addCommand(args, { chainId }))
-        );
+    if (dirtyWatch) {
+      throw new WatchError(dirtyWatch);
     }
 
-    async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
-        let cursor = 0;
-        do {
-            const reply = await (this as any).scan(cursor, options);
-            cursor = reply.cursor;
-            for (const key of reply.keys) {
-                yield key;
-            }
-        } while (cursor !== 0);
+    if (watchEpoch && watchEpoch !== this._self.socketEpoch) {
+      throw new WatchError('Client reconnected after WATCH');
     }
 
-    async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<HScanTuple, string>> {
-        let cursor = 0;
-        do {
-            const reply = await (this as any).hScan(key, cursor, options);
-            cursor = reply.cursor;
-            for (const tuple of reply.tuples) {
-                yield tuple;
-            }
-        } while (cursor !== 0);
+    const typeMapping = this._commandOptions?.typeMapping;
+    const chainId = Symbol('MULTI Chain');
+    const promises = [
+      this._self.#queue.addCommand(['MULTI'], { chainId }),
+    ];
+
+    for (const { args } of commands) {
+      promises.push(
+        this._self.#queue.addCommand(args, {
+          chainId,
+          typeMapping
+        })
+      );
     }
 
-    async* hScanNoValuesIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<RedisCommandArgument, string>> {
-        let cursor = 0;
-        do {
-            const reply = await (this as any).hScanNoValues(key, cursor, options);
-            cursor = reply.cursor;
-            for (const k of reply.keys) {
-                yield k;
-            }
-        } while (cursor !== 0);
+    promises.push(
+      this._self.#queue.addCommand(['EXEC'], { chainId })
+    );
+
+    this._self.#scheduleWrite();
+
+    const results = await Promise.all(promises),
+      execResult = results[results.length - 1];
+
+    if (execResult === null) {
+      throw new WatchError();
     }
 
-    async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
-        let cursor = 0;
-        do {
-            const reply = await (this as any).sScan(key, cursor, options);
-            cursor = reply.cursor;
-            for (const member of reply.members) {
-                yield member;
-            }
-        } while (cursor !== 0);
+    if (selectedDB !== undefined) {
+      this._self.#selectedDB = selectedDB;
     }
 
-    async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<ZMember, string>> {
-        let cursor = 0;
-        do {
-            const reply = await (this as any).zScan(key, cursor, options);
-            cursor = reply.cursor;
-            for (const member of reply.members) {
-                yield member;
-            }
-        } while (cursor !== 0);
+    return execResult as Array<unknown>;
+  }
+
+  MULTI() {
+    type Multi = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>;
+    return new ((this as any).Multi as Multi)(
+      this._executeMulti.bind(this),
+      this._executePipeline.bind(this),
+      this._commandOptions?.typeMapping
+    );
+  }
+
+  multi = this.MULTI;
+
+  async* scanIterator(
+    this: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
+    options?: ScanOptions & ScanIteratorOptions
+  ) {
+    let cursor = options?.cursor ?? '0';
+    do {
+      const reply = await this.scan(cursor, options);
+      cursor = reply.cursor;
+      yield reply.keys;
+    } while (cursor !== '0');
+  }
+
+  async* hScanIterator(
+    this: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
+    key: RedisArgument,
+    options?: ScanCommonOptions & ScanIteratorOptions
+  ) {
+    let cursor = options?.cursor ?? '0';
+    do {
+      const reply = await this.hScan(key, cursor, options);
+      cursor = reply.cursor;
+      yield reply.entries;
+    } while (cursor !== '0');
+  }
+
+  async* hScanValuesIterator(
+    this: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
+    key: RedisArgument,
+    options?: ScanCommonOptions & ScanIteratorOptions
+  ) {
+    let cursor = options?.cursor ?? '0';
+    do {
+      const reply = await this.hScanNoValues(key, cursor, options);
+      cursor = reply.cursor;
+      yield reply.fields;
+    } while (cursor !== '0');
+  }
+
+  async* hScanNoValuesIterator(
+    this: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
+    key: RedisArgument,
+    options?: ScanCommonOptions & ScanIteratorOptions
+  ) {
+    let cursor = options?.cursor ?? '0';
+    do {
+      const reply = await this.hScanNoValues(key, cursor, options);
+      cursor = reply.cursor;
+      yield reply.fields;
+    } while (cursor !== '0');
+  }
+
+  async* sScanIterator(
+    this: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
+    key: RedisArgument,
+    options?: ScanCommonOptions & ScanIteratorOptions
+  ) {
+    let cursor = options?.cursor ?? '0';
+    do {
+      const reply = await this.sScan(key, cursor, options);
+      cursor = reply.cursor;
+      yield reply.members;
+    } while (cursor !== '0');
+  }
+
+  async* zScanIterator(
+    this: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
+    key: RedisArgument,
+    options?: ScanCommonOptions & ScanIteratorOptions
+  ) {
+    let cursor = options?.cursor ?? '0';
+    do {
+      const reply = await this.zScan(key, cursor, options);
+      cursor = reply.cursor;
+      yield reply.members;
+    } while (cursor !== '0');
+  }
+
+  async MONITOR(callback: MonitorCallback<TYPE_MAPPING>) {
+    const promise = this._self.#queue.monitor(callback, {
+      typeMapping: this._commandOptions?.typeMapping
+    });
+    this._self.#scheduleWrite();
+    await promise;
+    this._self.#monitorCallback = callback;
+  }
+
+  monitor = this.MONITOR;
+
+  /**
+   * Reset the client to its default state (i.e. stop PubSub, stop monitoring, select default DB, etc.)
+   */
+  async reset() {
+    const chainId = Symbol('Reset Chain'),
+      promises = [this._self.#queue.reset(chainId)],
+      selectedDB = this._self.#options?.database ?? 0;
+    this._self.#credentialsSubscription?.dispose();
+    this._self.#credentialsSubscription = null;
+    promises.push(...(await this._self.#handshake(chainId, false)));
+    this._self.#scheduleWrite();
+    await Promise.all(promises);
+    this._self.#selectedDB = selectedDB;
+    this._self.#monitorCallback = undefined;
+    this._self.#dirtyWatch = undefined;
+    this._self.#watchEpoch = undefined;
+  }
+
+  /**
+   * If the client has state, reset it.
+   * An internal function to be used by wrapper class such as `RedisClientPool`.
+   * @internal
+   */
+  resetIfDirty() {
+    let shouldReset = false;
+    if (this._self.#selectedDB !== (this._self.#options?.database ?? 0)) {
+      console.warn('Returning a client with a different selected DB');
+      shouldReset = true;
     }
 
-    async disconnect(): Promise<void> {
-        if (this.#pingTimer) clearTimeout(this.#pingTimer);
-        this.#queue.flushAll(new DisconnectsClientError());
-        this.#socket.disconnect();
-        await this.#destroyIsolationPool();
+    if (this._self.#monitorCallback) {
+      console.warn('Returning a client with active MONITOR');
+      shouldReset = true;
     }
 
-    async #destroyIsolationPool(): Promise<void> {
-        await this.#isolationPool!.drain();
-        await this.#isolationPool!.clear();
-        this.#isolationPool = undefined;
+    if (this._self.#queue.isPubSubActive) {
+      console.warn('Returning a client with active PubSub');
+      shouldReset = true;
     }
 
-    ref(): void {
-        this.#socket.ref();
+    if (this._self.#dirtyWatch || this._self.#watchEpoch) {
+      console.warn('Returning a client with active WATCH');
+      shouldReset = true;
     }
 
-    unref(): void {
-        this.#socket.unref();
+    if (shouldReset) {
+      return this.reset();
     }
+  }
+
+  /**
+   * @deprecated use .close instead
+   */
+  QUIT(): Promise<string> {
+    this._self.#credentialsSubscription?.dispose();
+    this._self.#credentialsSubscription = null;
+    return this._self.#socket.quit(async () => {
+      clearTimeout(this._self.#pingTimer);
+      const quitPromise = this._self.#queue.addCommand<string>(['QUIT']);
+      this._self.#scheduleWrite();
+      return quitPromise;
+    });
+  }
+
+  quit = this.QUIT;
+
+  /**
+   * @deprecated use .destroy instead
+   */
+  disconnect() {
+    return Promise.resolve(this.destroy());
+  }
+
+  /**
+   * Close the client. Wait for pending commands.
+   */
+  close() {
+    return new Promise<void>(resolve => {
+      clearTimeout(this._self.#pingTimer);
+      this._self.#socket.close();
+      this._self.#clientSideCache?.onClose();
+
+      if (this._self.#queue.isEmpty()) {
+        this._self.#socket.destroySocket();
+        return resolve();
+      }
+
+      const maybeClose = () => {
+        if (!this._self.#queue.isEmpty()) return;
+
+        this._self.#socket.off('data', maybeClose);
+        this._self.#socket.destroySocket();
+        resolve();
+      };
+      this._self.#socket.on('data', maybeClose);
+      this._self.#credentialsSubscription?.dispose();
+      this._self.#credentialsSubscription = null;
+    });
+  }
+
+  /**
+   * Destroy the client. Rejects all commands immediately.
+   */
+  destroy() {
+    clearTimeout(this._self.#pingTimer);
+    this._self.#queue.flushAll(new DisconnectsClientError());
+    this._self.#socket.destroy();
+    this._self.#clientSideCache?.onClose();
+    this._self.#credentialsSubscription?.dispose();
+    this._self.#credentialsSubscription = null;
+  }
+
+  ref() {
+    this._self.#socket.ref();
+  }
+
+  unref() {
+    this._self.#socket.unref();
+  }
 }
-
-attachCommands({
-    BaseClass: RedisClient,
-    commands: COMMANDS,
-    executor: RedisClient.prototype.commandsExecutor
-});
-(RedisClient.prototype as any).Multi = RedisClientMultiCommand;
diff --git a/packages/client/lib/client/legacy-mode.spec.ts b/packages/client/lib/client/legacy-mode.spec.ts
new file mode 100644
index 00000000000..306ea7f3353
--- /dev/null
+++ b/packages/client/lib/client/legacy-mode.spec.ts
@@ -0,0 +1,111 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import { promisify } from 'node:util';
+import { RedisLegacyClientType } from './legacy-mode';
+import { ErrorReply } from '../errors';
+import { RedisClientType } from '.';
+import { once } from 'node:events';
+
+function testWithLegacyClient(title: string, fn: (legacy: RedisLegacyClientType, client: RedisClientType) => Promise<unknown>) {
+  testUtils.testWithClient(title, client => fn(client.legacy(), client), GLOBAL.SERVERS.OPEN);
+}
+
+describe('Legacy Mode', () => {
+  describe('client.sendCommand', () => {
+    testWithLegacyClient('resolve', async client => {
+      assert.equal(
+        await promisify(client.sendCommand).call(client, 'PING'),
+        'PONG'
+      );
+    });
+
+    testWithLegacyClient('reject', async client => {
+      await assert.rejects(
+        promisify(client.sendCommand).call(client, 'ERROR'),
+        ErrorReply
+      );
+    });
+
+    testWithLegacyClient('reject without a callback', async (legacy, client) => {
+      legacy.sendCommand('ERROR');
+      const [err] = await once(client, 'error');
+      assert.ok(err instanceof ErrorReply);
+    });
+  });
+
+  describe('hGetAll (TRANSFORM_LEGACY_REPLY)', () => {  
+    testWithLegacyClient('resolve', async client => {
+      await promisify(client.hSet).call(client, 'key', 'field', 'value');
+      assert.deepEqual(
+        await promisify(client.hGetAll).call(client, 'key'),
+        Object.create(null, {
+          field: {
+            value: 'value',
+            configurable: true,
+            enumerable: true
+          }
+        })
+      );
+    });
+
+    testWithLegacyClient('reject', async client => {
+      await assert.rejects(
+        promisify(client.hGetAll).call(client),
+        ErrorReply
+      );
+    });
+  });
+
+  describe('client.set', () => {
+    testWithLegacyClient('vardict', async client => {
+      assert.equal(
+        await promisify(client.set).call(client, 'a', 'b'),
+        'OK'
+      );
+    });
+
+    testWithLegacyClient('array', async client => {
+      assert.equal(
+        await promisify(client.set).call(client, ['a', 'b']),
+        'OK'
+      );
+    });
+
+    testWithLegacyClient('vardict & arrays', async client => {
+      assert.equal(
+        await promisify(client.set).call(client, ['a'], 'b', ['EX', 1]),
+        'OK'
+      );
+    });
+
+    testWithLegacyClient('reject without a callback', async (legacy, client) => {
+      legacy.set('ERROR');
+      const [err] = await once(client, 'error');
+      assert.ok(err instanceof ErrorReply);
+    });
+  });
+
+  describe('client.multi', () => {
+    testWithLegacyClient('resolve', async client => {
+      const multi = client.multi().ping().sendCommand('PING');
+      assert.deepEqual(
+        await promisify(multi.exec).call(multi),
+        ['PONG', 'PONG']
+      );
+    });
+    
+    testWithLegacyClient('reject', async client => {
+      const multi = client.multi().sendCommand('ERROR');
+      await assert.rejects(
+        promisify(multi.exec).call(multi),
+        ErrorReply
+      );
+    });
+
+    testWithLegacyClient('reject without a callback', async (legacy, client) => {
+      legacy.multi().sendCommand('ERROR').exec();
+      const [err] = await once(client, 'error');
+      assert.ok(err instanceof ErrorReply);
+    });
+  });
+});
diff --git a/packages/client/lib/client/legacy-mode.ts b/packages/client/lib/client/legacy-mode.ts
new file mode 100644
index 00000000000..03e7cf4efe1
--- /dev/null
+++ b/packages/client/lib/client/legacy-mode.ts
@@ -0,0 +1,177 @@
+import { RedisModules, RedisFunctions, RedisScripts, RespVersions, Command, CommandArguments, ReplyUnion } from '../RESP/types';
+import { RedisClientType } from '.';
+import { getTransformReply } from '../commander';
+import { ErrorReply } from '../errors';
+import COMMANDS from '../commands';
+import RedisMultiCommand from '../multi-command';
+
+type LegacyArgument = string | Buffer | number | Date;
+
+type LegacyArguments = Array<LegacyArgument | LegacyArguments>;
+
+type LegacyCallback = (err: ErrorReply | null, reply?: ReplyUnion) => unknown
+
+type LegacyCommandArguments = LegacyArguments | [
+  ...args: LegacyArguments,
+  callback: LegacyCallback
+];
+
+type WithCommands = {
+  [P in keyof typeof COMMANDS]: (...args: LegacyCommandArguments) => void;
+};
+
+export type RedisLegacyClientType = RedisLegacyClient & WithCommands;
+
+export class RedisLegacyClient {
+  static #transformArguments(redisArgs: CommandArguments, args: LegacyCommandArguments) {
+    let callback: LegacyCallback | undefined;
+    if (typeof args[args.length - 1] === 'function') {
+      callback = args.pop() as LegacyCallback;
+    }
+
+    RedisLegacyClient.pushArguments(redisArgs, args as LegacyArguments);
+
+    return callback;
+  }
+
+  static pushArguments(redisArgs: CommandArguments, args: LegacyArguments) {
+    for (let i = 0; i < args.length; ++i) {
+      const arg = args[i];
+      if (Array.isArray(arg)) {
+        RedisLegacyClient.pushArguments(redisArgs, arg);
+      } else {
+        redisArgs.push(
+          typeof arg === 'number' || arg instanceof Date ?
+            arg.toString() :
+            arg
+        );
+      }
+    }
+  }
+
+  static getTransformReply(command: Command, resp: RespVersions) {
+    return command.TRANSFORM_LEGACY_REPLY ?
+      getTransformReply(command, resp) :
+      undefined;
+  }
+
+  static #createCommand(name: string, command: Command, resp: RespVersions) {
+    const transformReply = RedisLegacyClient.getTransformReply(command, resp);
+    return function (this: RedisLegacyClient, ...args: LegacyCommandArguments) {
+      const redisArgs = [name],
+        callback = RedisLegacyClient.#transformArguments(redisArgs, args),
+        promise = this.#client.sendCommand(redisArgs);
+
+      if (!callback) {
+        promise.catch(err => this.#client.emit('error', err));
+        return;
+      }
+
+      promise
+        .then(reply => callback(null, transformReply ? transformReply(reply) : reply))
+        .catch(err => callback(err));
+    };
+  }
+
+  #client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
+  #Multi: ReturnType<typeof LegacyMultiCommand['factory']>;
+
+  constructor(
+    client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>
+  ) {
+    this.#client = client;
+
+    const RESP = client.options?.RESP ?? 2;
+    for (const [name, command] of Object.entries(COMMANDS)) {
+      // TODO: as any?
+      (this as any)[name] = RedisLegacyClient.#createCommand(
+        name,
+        command,
+        RESP
+      );
+    }
+
+    this.#Multi = LegacyMultiCommand.factory(RESP);
+  }
+
+  sendCommand(...args: LegacyCommandArguments) {
+    const redisArgs: CommandArguments = [],
+      callback = RedisLegacyClient.#transformArguments(redisArgs, args),
+      promise = this.#client.sendCommand(redisArgs);
+
+    if (!callback) {
+      promise.catch(err => this.#client.emit('error', err));
+      return;
+    }
+  
+    promise
+      .then(reply => callback(null, reply))
+      .catch(err => callback(err));
+  }
+
+  multi() {
+    return this.#Multi(this.#client);
+  }
+}
+
+type MultiWithCommands = {
+  [P in keyof typeof COMMANDS]: (...args: LegacyCommandArguments) => RedisLegacyMultiType;
+};
+
+export type RedisLegacyMultiType = LegacyMultiCommand & MultiWithCommands;
+
+class LegacyMultiCommand {
+  static #createCommand(name: string, command: Command, resp: RespVersions) {
+    const transformReply = RedisLegacyClient.getTransformReply(command, resp);
+    return function (this: LegacyMultiCommand, ...args: LegacyArguments) {
+      const redisArgs = [name];
+      RedisLegacyClient.pushArguments(redisArgs, args);
+      this.#multi.addCommand(redisArgs, transformReply);
+      return this;
+    };
+  }
+
+  static factory(resp: RespVersions) {
+    const Multi = class extends LegacyMultiCommand {};
+
+    for (const [name, command] of Object.entries(COMMANDS)) {
+      // TODO: as any?
+      (Multi as any).prototype[name] = LegacyMultiCommand.#createCommand(
+        name,
+        command,
+        resp
+      );
+    }
+
+    return (client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>) => {
+      return new Multi(client) as unknown as RedisLegacyMultiType;
+    };
+  }
+
+  readonly #multi = new RedisMultiCommand();
+  readonly #client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
+
+  constructor(client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>) {
+    this.#client = client;
+  }
+
+  sendCommand(...args: LegacyArguments) {
+    const redisArgs: CommandArguments = [];
+    RedisLegacyClient.pushArguments(redisArgs, args);
+    this.#multi.addCommand(redisArgs);
+    return this;
+  }
+
+  exec(cb?: (err: ErrorReply | null, replies?: Array<unknown>) => unknown) {
+    const promise = this.#client._executeMulti(this.#multi.queue);
+
+    if (!cb) {
+      promise.catch(err => this.#client.emit('error', err));
+      return;
+    }
+
+    promise
+      .then(results => cb(null, this.#multi.transformReplies(results)))
+      .catch(err => cb?.(err));
+  }
+}
diff --git a/packages/client/lib/client/linked-list.spec.ts b/packages/client/lib/client/linked-list.spec.ts
new file mode 100644
index 00000000000..9547fb81c7c
--- /dev/null
+++ b/packages/client/lib/client/linked-list.spec.ts
@@ -0,0 +1,138 @@
+import { SinglyLinkedList, DoublyLinkedList } from './linked-list';
+import { equal, deepEqual } from 'assert/strict';
+
+describe('DoublyLinkedList', () => {
+  const list = new DoublyLinkedList();
+
+  it('should start empty', () => {
+    equal(list.length, 0);
+    equal(list.head, undefined);
+    equal(list.tail, undefined);
+    deepEqual(Array.from(list), []);
+  });
+
+  it('shift empty', () => {
+    equal(list.shift(), undefined);
+    equal(list.length, 0);
+    deepEqual(Array.from(list), []);
+  });
+
+  it('push 1', () => {
+    list.push(1);
+    equal(list.length, 1);
+    deepEqual(Array.from(list), [1]);
+  });
+
+  it('push 2', () => {
+    list.push(2);
+    equal(list.length, 2);
+    deepEqual(Array.from(list), [1, 2]);
+  });
+
+  it('unshift 0', () => {
+    list.unshift(0);
+    equal(list.length, 3);
+    deepEqual(Array.from(list), [0, 1, 2]);
+  });
+
+  it('remove middle node', () => {
+    list.remove(list.head!.next!);
+    equal(list.length, 2);
+    deepEqual(Array.from(list), [0, 2]);
+  });
+
+  it('remove head', () => {
+    list.remove(list.head!);
+    equal(list.length, 1);
+    deepEqual(Array.from(list), [2]);
+  });
+
+  it('remove tail', () => {
+    list.remove(list.tail!);
+    equal(list.length, 0);
+    deepEqual(Array.from(list), []);
+  });
+
+  it('unshift empty queue', () => {
+    list.unshift(0);
+    equal(list.length, 1);
+    deepEqual(Array.from(list), [0]);
+  });
+
+  it('push 1', () => {
+    list.push(1);
+    equal(list.length, 2);
+    deepEqual(Array.from(list), [0, 1]);
+  });
+
+  it('shift', () => {
+    equal(list.shift(), 0);
+    equal(list.length, 1);
+    deepEqual(Array.from(list), [1]);
+  });
+
+  it('shift last element', () => {
+    equal(list.shift(), 1);
+    equal(list.length, 0);
+    deepEqual(Array.from(list), []);
+  });
+});
+
+describe('SinglyLinkedList', () => {
+  const list = new SinglyLinkedList();
+
+  it('should start empty', () => {
+    equal(list.length, 0);
+    equal(list.head, undefined);
+    equal(list.tail, undefined);
+    deepEqual(Array.from(list), []);
+  });
+
+  it('shift empty', () => {
+    equal(list.shift(), undefined);
+    equal(list.length, 0);
+    deepEqual(Array.from(list), []);
+  });
+
+  it('push 1', () => {
+    list.push(1);
+    equal(list.length, 1);
+    deepEqual(Array.from(list), [1]);
+  });
+
+  it('push 2', () => {
+    list.push(2);
+    equal(list.length, 2);
+    deepEqual(Array.from(list), [1, 2]);
+  });
+
+  it('push 3', () => {
+    list.push(3);
+    equal(list.length, 3);
+    deepEqual(Array.from(list), [1, 2, 3]);
+  });
+
+  it('shift 1', () => {
+    equal(list.shift(), 1);
+    equal(list.length, 2);
+    deepEqual(Array.from(list), [2, 3]);
+  });
+
+  it('shift 2', () => {
+    equal(list.shift(), 2);
+    equal(list.length, 1);
+    deepEqual(Array.from(list), [3]);
+  });
+
+  it('shift 3', () => {
+    equal(list.shift(), 3);
+    equal(list.length, 0);
+    deepEqual(Array.from(list), []);
+  });
+
+  it('should be empty', () => {
+    equal(list.length, 0);
+    equal(list.head, undefined);
+    equal(list.tail, undefined);
+  });
+});
diff --git a/packages/client/lib/client/linked-list.ts b/packages/client/lib/client/linked-list.ts
new file mode 100644
index 00000000000..29678f027b5
--- /dev/null
+++ b/packages/client/lib/client/linked-list.ts
@@ -0,0 +1,203 @@
+export interface DoublyLinkedNode<T> {
+  value: T;
+  previous: DoublyLinkedNode<T> | undefined;
+  next: DoublyLinkedNode<T> | undefined;
+}
+
+export class DoublyLinkedList<T> {
+  #length = 0;
+
+  get length() {
+    return this.#length;
+  }
+
+  #head?: DoublyLinkedNode<T>;
+
+  get head() {
+    return this.#head;
+  }
+
+  #tail?: DoublyLinkedNode<T>;
+
+  get tail() {
+    return this.#tail;
+  }
+
+  push(value: T) {
+    ++this.#length;
+
+    if (this.#tail === undefined) {
+      return this.#tail = this.#head = {
+        previous: this.#head,
+        next: undefined,
+        value
+      };
+    } 
+
+    return this.#tail = this.#tail.next = {
+      previous: this.#tail,
+      next: undefined,
+      value
+    };
+  }
+
+  unshift(value: T) {
+    ++this.#length;
+
+    if (this.#head === undefined) {
+      return this.#head = this.#tail = {
+        previous: undefined,
+        next: undefined,
+        value
+      };
+    }
+
+    return this.#head = this.#head.previous = {
+      previous: undefined,
+      next: this.#head,
+      value
+    };
+  }
+
+  add(value: T, prepend = false) {
+    return prepend ?
+      this.unshift(value) :
+      this.push(value);
+  }
+
+  shift() {
+    if (this.#head === undefined) return undefined;
+
+    --this.#length;
+    const node = this.#head;
+    if (node.next) {
+      node.next.previous = node.previous;
+      this.#head = node.next;
+      node.next = undefined;
+    } else {
+      this.#head = this.#tail = undefined;
+    }
+    return node.value;
+  }
+
+  remove(node: DoublyLinkedNode<T>) {
+    --this.#length;
+
+    if (this.#tail === node) {
+      this.#tail = node.previous;
+    }
+
+    if (this.#head === node) {
+      this.#head = node.next;
+    } else {
+      node.previous!.next = node.next;
+      node.previous = undefined;
+    }
+    
+    node.next = undefined;
+  }
+
+  reset() {
+    this.#length = 0;
+    this.#head = this.#tail = undefined;
+  }
+
+  *[Symbol.iterator]() {
+    let node = this.#head;
+    while (node !== undefined) {
+      yield node.value;
+      node = node.next;
+    }
+  }
+}
+
+export interface SinglyLinkedNode<T> {
+  value: T;
+  next: SinglyLinkedNode<T> | undefined;
+  removed: boolean;
+}
+
+export class SinglyLinkedList<T> {
+  #length = 0;
+
+  get length() {
+    return this.#length;
+  }
+
+  #head?: SinglyLinkedNode<T>;
+
+  get head() {
+    return this.#head;
+  }
+
+  #tail?: SinglyLinkedNode<T>;
+
+  get tail() {
+    return this.#tail;
+  }
+
+  push(value: T) {
+    ++this.#length;
+
+    const node = {
+      value,
+      next: undefined,
+      removed: false
+    };
+
+    if (this.#head === undefined) {
+      return this.#head = this.#tail = node;
+    }
+
+    return this.#tail!.next = this.#tail = node;
+  }
+
+  remove(node: SinglyLinkedNode<T>, parent: SinglyLinkedNode<T> | undefined) {
+    if (node.removed) {
+      throw new Error("node already removed");
+    }
+    --this.#length;
+
+    if (this.#head === node) {
+      if (this.#tail === node) {
+        this.#head = this.#tail = undefined;
+      } else {
+        this.#head = node.next;
+      }
+    } else if (this.#tail === node) {
+      this.#tail = parent;
+      parent!.next = undefined;
+    } else {
+      parent!.next = node.next;
+    }
+
+    node.removed = true;
+  }
+
+  shift() {
+    if (this.#head === undefined) return undefined;
+
+    const node = this.#head;
+    if (--this.#length === 0) {
+      this.#head = this.#tail = undefined;
+    } else {
+      this.#head = node.next;
+    }
+
+    node.removed = true;
+    return node.value;
+  }
+
+  reset() {
+    this.#length = 0;
+    this.#head = this.#tail = undefined;
+  }
+
+  *[Symbol.iterator]() {
+    let node = this.#head;
+    while (node !== undefined) {
+      yield node.value;
+      node = node.next;
+    }
+  }
+}
diff --git a/packages/client/lib/client/multi-command.ts b/packages/client/lib/client/multi-command.ts
index e347667bf2c..a687655b60a 100644
--- a/packages/client/lib/client/multi-command.ts
+++ b/packages/client/lib/client/multi-command.ts
@@ -1,200 +1,239 @@
-import COMMANDS from './commands';
-import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction, RedisCommands } from '../commands';
-import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
-import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander';
+import COMMANDS from '../commands';
+import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command';
+import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types';
+import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander';
+import { BasicCommandParser } from './parser';
+import { Tail } from '../commands/generic-transformers';
 
 type CommandSignature<
-    C extends RedisCommand,
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, F, S>;
+  REPLIES extends Array<unknown>,
+  C extends Command,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (...args: Tail<Parameters<C['parseCommand']>>) => RedisClientMultiCommandType<
+  [...REPLIES, ReplyWithTypeMapping<CommandReply<C, RESP>, TYPE_MAPPING>],
+  M,
+  F,
+  S,
+  RESP,
+  TYPE_MAPPING
+>;
 
 type WithCommands<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>;
+  [P in keyof typeof COMMANDS]: CommandSignature<REPLIES, (typeof COMMANDS)[P], M, F, S, RESP, TYPE_MAPPING>;
 };
 
 type WithModules<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof M as ExcludeMappedString<P>]: {
-        [C in keyof M[P] as ExcludeMappedString<C>]: CommandSignature<M[P][C], M, F, S>;
-    };
+  [P in keyof M]: {
+    [C in keyof M[P]]: CommandSignature<REPLIES, M[P][C], M, F, S, RESP, TYPE_MAPPING>;
+  };
 };
 
 type WithFunctions<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof F as ExcludeMappedString<P>]: {
-        [FF in keyof F[P] as ExcludeMappedString<FF>]: CommandSignature<F[P][FF], M, F, S>;
-    };
+  [L in keyof F]: {
+    [C in keyof F[L]]: CommandSignature<REPLIES, F[L][C], M, F, S, RESP, TYPE_MAPPING>;
+  };
 };
 
 type WithScripts<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof S as ExcludeMappedString<P>]: CommandSignature<S[P], M, F, S>;
+  [P in keyof S]: CommandSignature<REPLIES, S[P], M, F, S, RESP, TYPE_MAPPING>;
 };
 
 export type RedisClientMultiCommandType<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = RedisClientMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
-
-type InstantiableRedisMultiCommand<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<M, F, S>;
-
-export type RedisClientMultiExecutor = (
-    queue: Array<RedisMultiQueuedCommand>,
-    selectedDB?: number,
-    chainId?: symbol
-) => Promise<Array<RedisCommandRawReply>>;
-
-export default class RedisClientMultiCommand {
-    static extend<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(extensions?: RedisExtensions<M, F, S>): InstantiableRedisMultiCommand<M, F, S> {
-        return attachExtensions({
-            BaseClass: RedisClientMultiCommand,
-            modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
-            modules: extensions?.modules,
-            functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor,
-            functions: extensions?.functions,
-            scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor,
-            scripts: extensions?.scripts
-        });
-    }
-
-    readonly #multi = new RedisMultiCommand();
-    readonly #executor: RedisClientMultiExecutor;
-    readonly v4: Record<string, any> = {};
-    #selectedDB?: number;
-
-    constructor(executor: RedisClientMultiExecutor, legacyMode = false) {
-        this.#executor = executor;
-        if (legacyMode) {
-            this.#legacyMode();
-        }
-    }
-
-    #legacyMode(): void {
-        this.v4.addCommand = this.addCommand.bind(this);
-        (this as any).addCommand = (...args: Array<any>): this => {
-            this.#multi.addCommand(transformLegacyCommandArguments(args));
-            return this;
-        };
-        this.v4.exec = this.exec.bind(this);
-        (this as any).exec = (callback?: (err: Error | null, replies?: Array<unknown>) => unknown): void => {
-            this.v4.exec()
-                .then((reply: Array<unknown>) => {
-                    if (!callback) return;
-
-                    callback(null, reply);
-                })
-                .catch((err: Error) => {
-                    if (!callback) {
-                        // this.emit('error', err);
-                        return;
-                    }
-
-                    callback(err);
-                });
-        };
-
-        for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
-            this.#defineLegacyCommand(name, command);
-            (this as any)[name.toLowerCase()] ??= (this as any)[name];
-        }
-    }
-
-    #defineLegacyCommand(this: any, name: string, command?: RedisCommand): void {
-        this.v4[name] = this[name].bind(this.v4);
-        this[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
-            (...args: Array<unknown>) => {
-                this.#multi.addCommand(
-                    [name, ...transformLegacyCommandArguments(args)],
-                    command.transformReply
-                );
-                return this;
-            } :
-            (...args: Array<unknown>) => this.addCommand(name, ...args);
-    }
-
-    commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
-        return this.addCommand(
-            command.transformArguments(...args),
-            command.transformReply
-        );
-    }
-
-    SELECT(db: number, transformReply?: RedisCommand['transformReply']): this {
-        this.#selectedDB = db;
-        return this.addCommand(['SELECT', db.toString()], transformReply);
-    }
-
-    select = this.SELECT;
-
-    addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): this {
-        this.#multi.addCommand(args, transformReply);
-        return this;
-    }
-
-    functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
-        this.#multi.addFunction(name, fn, args);
-        return this;
-    }
-
-    scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
-        this.#multi.addScript(script, args);
-        return this;
-    }
-
-    async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
-        if (execAsPipeline) {
-            return this.execAsPipeline();
-        }
-
-        return this.#multi.handleExecReplies(
-            await this.#executor(
-                this.#multi.queue,
-                this.#selectedDB,
-                RedisMultiCommand.generateChainId()
-            )
-        );
-    }
-
-    EXEC = this.exec;
-
-    async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
-        if (this.#multi.queue.length === 0) return [];
-        
-        return this.#multi.transformReplies(
-            await this.#executor(
-                this.#multi.queue,
-                this.#selectedDB
-            )
-        );
-    }
-}
+  REPLIES extends Array<any>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (
+  RedisClientMultiCommand<REPLIES> &
+  WithCommands<REPLIES, M, F, S, RESP, TYPE_MAPPING> & 
+  WithModules<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
+  WithFunctions<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
+  WithScripts<REPLIES, M, F, S, RESP, TYPE_MAPPING>
+);
+
+type ExecuteMulti = (commands: Array<RedisMultiQueuedCommand>, selectedDB?: number) => Promise<Array<unknown>>;
+
+export default class RedisClientMultiCommand<REPLIES = []> {
+  static #createCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return function (this: RedisClientMultiCommand, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this.addCommand(
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static #createModuleCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return function (this: { _self: RedisClientMultiCommand }, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this._self.addCommand(
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
+    const prefix = functionArgumentsPrefix(name, fn);
+    const transformReply = getTransformReply(fn, resp);
 
-attachCommands({
-    BaseClass: RedisClientMultiCommand,
-    commands: COMMANDS,
-    executor: RedisClientMultiCommand.prototype.commandsExecutor
-});
+    return function (this: { _self: RedisClientMultiCommand }, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      fn.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this._self.addCommand(
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static #createScriptCommand(script: RedisScript, resp: RespVersions) {
+    const transformReply = getTransformReply(script, resp);
+
+    return function (this: RedisClientMultiCommand, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      script.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this.#addScript(
+        script,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static extend<
+    M extends RedisModules = Record<string, never>,
+    F extends RedisFunctions = Record<string, never>,
+    S extends RedisScripts = Record<string, never>,
+    RESP extends RespVersions = 2
+  >(config?: CommanderConfig<M, F, S, RESP>) {
+    return attachConfig({
+      BaseClass: RedisClientMultiCommand,
+      commands: COMMANDS,
+      createCommand: RedisClientMultiCommand.#createCommand,
+      createModuleCommand: RedisClientMultiCommand.#createModuleCommand,
+      createFunctionCommand: RedisClientMultiCommand.#createFunctionCommand,
+      createScriptCommand: RedisClientMultiCommand.#createScriptCommand,
+      config
+    });
+  }
+
+  readonly #multi: RedisMultiCommand
+  readonly #executeMulti: ExecuteMulti;
+  readonly #executePipeline: ExecuteMulti;
+
+  #selectedDB?: number;
+
+  constructor(executeMulti: ExecuteMulti, executePipeline: ExecuteMulti, typeMapping?: TypeMapping) {
+    this.#multi = new RedisMultiCommand(typeMapping);
+    this.#executeMulti = executeMulti;
+    this.#executePipeline = executePipeline;
+  }
+
+  SELECT(db: number, transformReply?: TransformReply): this {
+    this.#selectedDB = db;
+    this.#multi.addCommand(['SELECT', db.toString()], transformReply);
+    return this;
+  }
+
+  select = this.SELECT;
+
+  addCommand(args: CommandArguments, transformReply?: TransformReply) {
+    this.#multi.addCommand(args, transformReply);
+    return this;
+  }
+
+  #addScript(
+    script: RedisScript,
+    args: CommandArguments,
+    transformReply?: TransformReply
+  ) {
+    this.#multi.addScript(script, args, transformReply);
+
+    return this;
+  }
+
+  async exec<T extends MultiReply = MULTI_REPLY['GENERIC']>(execAsPipeline = false): Promise<MultiReplyType<T, REPLIES>> {
+    if (execAsPipeline) return this.execAsPipeline<T>();
+
+    return this.#multi.transformReplies(
+      await this.#executeMulti(this.#multi.queue, this.#selectedDB)
+    ) as MultiReplyType<T, REPLIES>;
+  }
+
+  EXEC = this.exec;
+
+  execTyped(execAsPipeline = false) {
+    return this.exec<MULTI_REPLY['TYPED']>(execAsPipeline);
+  }
+
+  async execAsPipeline<T extends MultiReply = MULTI_REPLY['GENERIC']>(): Promise<MultiReplyType<T, REPLIES>> {
+    if (this.#multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
+
+    return this.#multi.transformReplies(
+      await this.#executePipeline(this.#multi.queue, this.#selectedDB)
+    ) as MultiReplyType<T, REPLIES>;
+  }
+
+  execAsPipelineTyped() {
+    return this.execAsPipeline<MULTI_REPLY['TYPED']>();
+  }
+}
diff --git a/packages/client/lib/client/parser.ts b/packages/client/lib/client/parser.ts
new file mode 100644
index 00000000000..3e820230429
--- /dev/null
+++ b/packages/client/lib/client/parser.ts
@@ -0,0 +1,103 @@
+import { RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from '../commands/generic-transformers';
+
+export interface CommandParser {
+  redisArgs: ReadonlyArray<RedisArgument>;
+  keys: ReadonlyArray<RedisArgument>;
+  firstKey: RedisArgument | undefined;
+  preserve: unknown;
+
+  push: (...arg: Array<RedisArgument>) => unknown;
+  pushVariadic: (vals: RedisVariadicArgument) => unknown;
+  pushVariadicWithLength: (vals: RedisVariadicArgument) => unknown;
+  pushVariadicNumber: (vals: number | Array<number>) => unknown;
+  pushKey: (key: RedisArgument) => unknown; // normal push of keys
+  pushKeys: (keys: RedisVariadicArgument) => unknown; // push multiple keys at a time
+  pushKeysLength: (keys: RedisVariadicArgument) => unknown; // push multiple keys at a time
+}
+
+export class BasicCommandParser implements CommandParser {
+  #redisArgs: Array<RedisArgument> = [];
+  #keys: Array<RedisArgument> = [];
+  preserve: unknown;
+
+  get redisArgs() {
+    return this.#redisArgs;
+  }
+
+  get keys() {
+    return this.#keys;
+  }
+
+  get firstKey() {
+    return this.#keys[0];
+  }
+
+  get cacheKey() {
+    const tmp = new Array(this.#redisArgs.length*2);
+
+    for (let i = 0; i < this.#redisArgs.length; i++) {
+      tmp[i] = this.#redisArgs[i].length;
+      tmp[i+this.#redisArgs.length] = this.#redisArgs[i];
+    }
+
+    return tmp.join('_');
+  }
+
+  push(...arg: Array<RedisArgument>) {
+    this.#redisArgs.push(...arg);
+  };
+
+  pushVariadic(vals: RedisVariadicArgument) {
+    if (Array.isArray(vals)) {
+      for (const val of vals) {
+        this.push(val);
+      }
+    } else {
+      this.push(vals);
+    }
+  }
+
+  pushVariadicWithLength(vals: RedisVariadicArgument) {
+    if (Array.isArray(vals)) {
+      this.#redisArgs.push(vals.length.toString());
+    } else {
+      this.#redisArgs.push('1');
+    }
+    this.pushVariadic(vals);
+  }
+
+  pushVariadicNumber(vals: number | number[]) {
+    if (Array.isArray(vals)) {
+      for (const val of vals) {
+        this.push(val.toString());
+      }
+    } else {
+      this.push(vals.toString());
+    }
+  }
+
+  pushKey(key: RedisArgument) {
+    this.#keys.push(key);
+    this.#redisArgs.push(key);
+  }
+
+  pushKeysLength(keys: RedisVariadicArgument) {
+    if (Array.isArray(keys)) {
+      this.#redisArgs.push(keys.length.toString());
+    } else {
+      this.#redisArgs.push('1');
+    }
+    this.pushKeys(keys);
+  }
+
+  pushKeys(keys: RedisVariadicArgument) {
+    if (Array.isArray(keys)) {
+      this.#keys.push(...keys);
+      this.#redisArgs.push(...keys);
+    } else {
+      this.#keys.push(keys);
+      this.#redisArgs.push(keys);
+    }
+  }
+}
diff --git a/packages/client/lib/client/pool.spec.ts b/packages/client/lib/client/pool.spec.ts
new file mode 100644
index 00000000000..f292dc171c7
--- /dev/null
+++ b/packages/client/lib/client/pool.spec.ts
@@ -0,0 +1,42 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+
+describe('RedisClientPool', () => {
+  testUtils.testWithClientPool('sendCommand', async pool => {
+    assert.equal(
+      await pool.sendCommand(['PING']),
+      'PONG'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientPool(
+    'proper error propagation in sequential operations',
+    async (pool) => {
+      let hasUnhandledRejection = false;
+
+      process.once('unhandledRejection', () => {
+        hasUnhandledRejection = true;
+      });
+
+      const groupName = 'test-group';
+      const streamName = 'test-stream';
+
+      // First attempt - should succeed
+      await pool.xGroupCreate(streamName, groupName, '0', {
+        MKSTREAM: true,
+      });
+
+      // Subsequent attempts - should all throw BUSYGROUP errors and be handled properly
+      for (let i = 0; i < 3; i++) {
+        await assert.rejects(
+          pool.xGroupCreate(streamName, groupName, '0', {
+            MKSTREAM: true,
+          })
+        );
+      }
+
+      assert.equal(hasUnhandledRejection, false);
+    },
+    GLOBAL.SERVERS.OPEN
+  );
+});
diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts
new file mode 100644
index 00000000000..b53bb2c7e61
--- /dev/null
+++ b/packages/client/lib/client/pool.ts
@@ -0,0 +1,546 @@
+import COMMANDS from '../commands';
+import { Command, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
+import RedisClient, { RedisClientType, RedisClientOptions, RedisClientExtensions } from '.';
+import { EventEmitter } from 'node:events';
+import { DoublyLinkedNode, DoublyLinkedList, SinglyLinkedList } from './linked-list';
+import { TimeoutError } from '../errors';
+import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
+import { CommandOptions } from './commands-queue';
+import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
+import { BasicPooledClientSideCache, ClientSideCacheConfig, PooledClientSideCacheProvider } from './cache';
+import { BasicCommandParser } from './parser';
+import SingleEntryCache from '../single-entry-cache';
+
+export interface RedisPoolOptions {
+  /**
+   * The minimum number of clients to keep in the pool (>= 1).
+   */
+  minimum: number;
+  /**
+   * The maximum number of clients to keep in the pool (>= {@link RedisPoolOptions.minimum} >= 1).
+   */
+  maximum: number;
+  /**
+   * The maximum time a task can wait for a client to become available (>= 0).
+   */
+  acquireTimeout: number;
+  /**
+   * The delay in milliseconds before a cleanup operation is performed on idle clients.
+   * 
+   * After this delay, the pool will check if there are too many idle clients and destroy 
+   * excess ones to maintain optimal pool size.
+   */
+  cleanupDelay: number;
+  /**
+   * Client Side Caching configuration for the pool.
+   * 
+   * Enables Redis Servers and Clients to work together to cache results from commands 
+   * sent to a server. The server will notify the client when cached results are no longer valid.
+   * In pooled mode, the cache is shared across all clients in the pool.
+   * 
+   * Note: Client Side Caching is only supported with RESP3.
+   * 
+   * @example Anonymous cache configuration
+   * ```
+   * const client = createClientPool({RESP: 3}, {
+   *   clientSideCache: {
+   *     ttl: 0,
+   *     maxEntries: 0,
+   *     evictPolicy: "LRU"
+   *   },
+   *   minimum: 5
+   * });
+   * ```
+   * 
+   * @example Using a controllable cache
+   * ```
+   * const cache = new BasicPooledClientSideCache({
+   *   ttl: 0,
+   *   maxEntries: 0,
+   *   evictPolicy: "LRU"
+   * });
+   * const client = createClientPool({RESP: 3}, {
+   *   clientSideCache: cache,
+   *   minimum: 5
+   * });
+   * ```
+   */
+  clientSideCache?: PooledClientSideCacheProvider | ClientSideCacheConfig;
+  /**
+   * Enable experimental support for RESP3 module commands.
+   * 
+   * When enabled, allows the use of module commands that have been adapted 
+   * for the RESP3 protocol. This is an unstable feature and may change in 
+   * future versions.
+   * 
+   * @default false
+   */
+  unstableResp3Modules?: boolean;
+}
+
+export type PoolTask<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping,
+  T = unknown
+> = (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => T;
+
+export type RedisClientPoolType<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> = (
+  RedisClientPool<M, F, S, RESP, TYPE_MAPPING> &
+  RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
+);
+
+type ProxyPool = RedisClientPoolType<any, any, any, any, any>;
+
+type NamespaceProxyPool = { _self: ProxyPool };
+
+export class RedisClientPool<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> extends EventEmitter {
+  static #createCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return async function (this: ProxyPool, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      return this.execute(client => client._executeCommand(command, parser, this._commandOptions, transformReply))
+    };
+  }
+
+  static #createModuleCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return async function (this: NamespaceProxyPool, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      return this._self.execute(client => client._executeCommand(command, parser, this._self._commandOptions, transformReply))
+    };
+  }
+
+  static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
+    const prefix = functionArgumentsPrefix(name, fn);
+    const transformReply = getTransformReply(fn, resp);
+
+    return async function (this: NamespaceProxyPool, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      fn.parseCommand(parser, ...args);
+
+      return this._self.execute(client => client._executeCommand(fn, parser, this._self._commandOptions, transformReply))    };
+  }
+
+  static #createScriptCommand(script: RedisScript, resp: RespVersions) {
+    const prefix = scriptArgumentsPrefix(script);
+    const transformReply = getTransformReply(script, resp);
+
+    return async function (this: ProxyPool, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.pushVariadic(prefix);
+      script.parseCommand(parser, ...args);
+
+      return this.execute(client => client._executeScript(script, parser, this._commandOptions, transformReply))
+    };
+  }
+
+  static #SingleEntryCache = new SingleEntryCache<any, any>();
+
+  static create<
+    M extends RedisModules,
+    F extends RedisFunctions,
+    S extends RedisScripts,
+    RESP extends RespVersions,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(
+    clientOptions?: Omit<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>, "clientSideCache">,
+    options?: Partial<RedisPoolOptions>
+  ) {
+
+    let Pool = RedisClientPool.#SingleEntryCache.get(clientOptions);
+    if(!Pool) {
+      Pool = attachConfig({
+        BaseClass: RedisClientPool,
+        commands: COMMANDS,
+        createCommand: RedisClientPool.#createCommand,
+        createModuleCommand: RedisClientPool.#createModuleCommand,
+        createFunctionCommand: RedisClientPool.#createFunctionCommand,
+        createScriptCommand: RedisClientPool.#createScriptCommand,
+        config: clientOptions
+      });
+      Pool.prototype.Multi = RedisClientMultiCommand.extend(clientOptions);
+      RedisClientPool.#SingleEntryCache.set(clientOptions, Pool);
+    }
+
+    // returning a "proxy" to prevent the namespaces._self to leak between "proxies"
+    return Object.create(
+      new Pool(
+        clientOptions,
+        options
+      )
+    ) as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  // TODO: defaults
+  static #DEFAULTS = {
+    minimum: 1,
+    maximum: 100,
+    acquireTimeout: 3000,
+    cleanupDelay: 3000
+  } satisfies RedisPoolOptions;
+
+  readonly #clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+  readonly #options: RedisPoolOptions;
+
+  readonly #idleClients = new SinglyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>();
+
+  /**
+   * The number of idle clients.
+   */
+  get idleClients() {
+    return this._self.#idleClients.length;
+  }
+
+  readonly #clientsInUse = new DoublyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>();
+
+  /**
+   * The number of clients in use.
+   */
+  get clientsInUse() {
+    return this._self.#clientsInUse.length;
+  }
+
+  /**
+   * The total number of clients in the pool (including connecting, idle, and in use).
+   */
+  get totalClients() {
+    return this._self.#idleClients.length + this._self.#clientsInUse.length;
+  }
+
+  readonly #tasksQueue = new SinglyLinkedList<{
+    timeout: NodeJS.Timeout | undefined;
+    resolve: (value: unknown) => unknown;
+    reject: (reason?: unknown) => unknown;
+    fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>;
+  }>();
+
+  /**
+   * The number of tasks waiting for a client to become available.
+   */
+  get tasksQueueLength() {
+    return this._self.#tasksQueue.length;
+  }
+
+  #isOpen = false;
+
+  /**
+   * Whether the pool is open (either connecting or connected).
+   */
+  get isOpen() {
+    return this._self.#isOpen;
+  }
+
+  #isClosing = false;
+
+  /**
+   * Whether the pool is closing (*not* closed).
+   */
+  get isClosing() {
+    return this._self.#isClosing;
+  }
+
+  #clientSideCache?: PooledClientSideCacheProvider;
+  get clientSideCache() {
+    return this._self.#clientSideCache;
+  }
+
+  /**
+   * You are probably looking for {@link RedisClient.createPool `RedisClient.createPool`},
+   * {@link RedisClientPool.fromClient `RedisClientPool.fromClient`},
+   * or {@link RedisClientPool.fromOptions `RedisClientPool.fromOptions`}...
+   */
+  constructor(
+    clientOptions?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>,
+    options?: Partial<RedisPoolOptions>
+  ) {
+    super();
+
+    this.#options = {
+      ...RedisClientPool.#DEFAULTS,
+      ...options
+    };
+    if (options?.clientSideCache) {
+      if (clientOptions === undefined) {
+        clientOptions = {};
+      }
+
+      if (options.clientSideCache instanceof PooledClientSideCacheProvider) {
+        this.#clientSideCache = clientOptions.clientSideCache = options.clientSideCache;
+      } else {
+        const cscConfig = options.clientSideCache;
+        this.#clientSideCache = clientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig);
+//        this.#clientSideCache = clientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig);
+      }
+    }
+
+    this.#clientFactory = RedisClient.factory(clientOptions).bind(undefined, clientOptions) as () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  private _self = this;
+  private _commandOptions?: CommandOptions<TYPE_MAPPING>;
+
+  withCommandOptions<
+    OPTIONS extends CommandOptions<TYPE_MAPPING>,
+    TYPE_MAPPING extends TypeMapping
+  >(options: OPTIONS) {
+    const proxy = Object.create(this._self);
+    proxy._commandOptions = options;
+    return proxy as RedisClientPoolType<
+      M,
+      F,
+      S,
+      RESP,
+      TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
+    >;
+  }
+
+  #commandOptionsProxy<
+    K extends keyof CommandOptions,
+    V extends CommandOptions[K]
+  >(
+    key: K,
+    value: V
+  ) {
+    const proxy = Object.create(this._self);
+    proxy._commandOptions = Object.create(this._commandOptions ?? null);
+    proxy._commandOptions[key] = value;
+    return proxy as RedisClientPoolType<
+      M,
+      F,
+      S,
+      RESP,
+      K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
+    >;
+  }
+
+  /**
+   * Override the `typeMapping` command option
+   */
+  withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
+    return this._self.#commandOptionsProxy('typeMapping', typeMapping);
+  }
+
+  /**
+   * Override the `abortSignal` command option
+   */
+  withAbortSignal(abortSignal: AbortSignal) {
+    return this._self.#commandOptionsProxy('abortSignal', abortSignal);
+  }
+
+  /**
+   * Override the `asap` command option to `true`
+   * TODO: remove?
+   */
+  asap() {
+    return this._self.#commandOptionsProxy('asap', true);
+  }
+
+  async connect() {
+    if (this._self.#isOpen) return; // TODO: throw error?
+    this._self.#isOpen = true;
+
+    const promises = [];
+    while (promises.length < this._self.#options.minimum) {
+      promises.push(this._self.#create());
+    }
+
+    try {
+      await Promise.all(promises);
+    } catch (err) {
+      this.destroy();
+      throw err;
+    }
+
+    return this as unknown as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  async #create() {
+    const node = this._self.#clientsInUse.push(
+      this._self.#clientFactory()
+        .on('error', (err: Error) => this.emit('error', err))
+    );
+
+    try {
+      const client = node.value;
+      await client.connect();
+    } catch (err) {
+      this._self.#clientsInUse.remove(node);
+      throw err;
+    }
+
+    this._self.#returnClient(node);
+  }
+  
+  execute<T>(fn: PoolTask<M, F, S, RESP, TYPE_MAPPING, T>) {
+    return new Promise<Awaited<T>>((resolve, reject) => {
+      const client = this._self.#idleClients.shift(),
+        { tail } = this._self.#tasksQueue;
+      if (!client) {
+        let timeout;
+        if (this._self.#options.acquireTimeout > 0) {
+          timeout = setTimeout(
+            () => {
+              this._self.#tasksQueue.remove(task, tail);
+              reject(new TimeoutError('Timeout waiting for a client')); // TODO: message
+            },
+            this._self.#options.acquireTimeout
+          );
+        }
+
+        const task = this._self.#tasksQueue.push({
+          timeout,
+          // @ts-ignore
+          resolve,
+          reject,
+          fn
+        });
+
+        if (this.totalClients < this._self.#options.maximum) {
+          this._self.#create();
+        }
+
+        return;
+      }
+
+      const node = this._self.#clientsInUse.push(client);
+      // @ts-ignore
+      this._self.#executeTask(node, resolve, reject, fn);
+    });
+  }
+
+  #executeTask(
+    node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>,
+    resolve: <T>(value: T | PromiseLike<T>) => void,
+    reject: (reason?: unknown) => void,
+    fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>
+  ) {
+    const result = fn(node.value);
+    if (result instanceof Promise) {
+      result
+      .then(resolve, reject)
+      .finally(() => this.#returnClient(node))
+    } else {
+      resolve(result);
+      this.#returnClient(node);
+    }
+  }
+
+  #returnClient(node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>) {
+    const task = this.#tasksQueue.shift();
+    if (task) {
+      clearTimeout(task.timeout);
+      this.#executeTask(node, task.resolve, task.reject, task.fn);
+      return;
+    }
+
+    this.#clientsInUse.remove(node);
+    this.#idleClients.push(node.value);
+
+    this.#scheduleCleanup();
+  }
+
+  cleanupTimeout?: NodeJS.Timeout;
+
+  #scheduleCleanup() {
+    if (this.totalClients <= this.#options.minimum) return;
+
+    clearTimeout(this.cleanupTimeout);
+    this.cleanupTimeout = setTimeout(() => this.#cleanup(), this.#options.cleanupDelay);
+  }
+
+  #cleanup() {
+    const toDestroy = Math.min(this.#idleClients.length, this.totalClients - this.#options.minimum);
+    for (let i = 0; i < toDestroy; i++) {
+      // TODO: shift vs pop
+      const client = this.#idleClients.shift()!
+      client.destroy();
+    }
+  }
+
+  sendCommand(
+    args: Array<RedisArgument>,
+    options?: CommandOptions
+  ) {
+    return this.execute(client => client.sendCommand(args, options));
+  }
+
+  MULTI() {
+    type Multi = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>;
+    return new ((this as any).Multi as Multi)(
+      (commands, selectedDB) => this.execute(client => client._executeMulti(commands, selectedDB)),
+      commands => this.execute(client => client._executePipeline(commands)),
+      this._commandOptions?.typeMapping
+    );
+  }
+
+  multi = this.MULTI;
+
+  async close() {
+    if (this._self.#isClosing) return; // TODO: throw err?
+    if (!this._self.#isOpen) return; // TODO: throw err?
+
+    this._self.#isClosing = true;
+    
+    try {
+      const promises = [];
+
+      for (const client of this._self.#idleClients) {
+        promises.push(client.close());
+      }
+  
+      for (const client of this._self.#clientsInUse) {
+        promises.push(client.close());
+      }
+
+      await Promise.all(promises);
+
+      this.#clientSideCache?.onPoolClose();
+  
+      this._self.#idleClients.reset();
+      this._self.#clientsInUse.reset();
+    } catch (err) {
+      
+    } finally {
+      this._self.#isClosing = false;
+    } 
+  }
+
+  destroy() {
+    for (const client of this._self.#idleClients) {
+      client.destroy();
+    }
+    this._self.#idleClients.reset();
+
+    for (const client of this._self.#clientsInUse) {
+      client.destroy();
+    }
+
+    this._self.#clientSideCache?.onPoolClose();
+
+    this._self.#clientsInUse.reset();
+
+    this._self.#isOpen = false;
+  }
+}
diff --git a/packages/client/lib/client/pub-sub.spec.ts b/packages/client/lib/client/pub-sub.spec.ts
index 8b9f16732cb..74bd85c1831 100644
--- a/packages/client/lib/client/pub-sub.spec.ts
+++ b/packages/client/lib/client/pub-sub.spec.ts
@@ -1,151 +1,151 @@
-import { strict as assert } from 'assert';
-import { PubSub, PubSubType } from './pub-sub';
+import { strict as assert } from 'node:assert';
+import { PubSub, PUBSUB_TYPE } from './pub-sub';
 
 describe('PubSub', () => {
-    const TYPE = PubSubType.CHANNELS,
-        CHANNEL = 'channel',
-        LISTENER = () => {};
-
-    describe('subscribe to new channel', () => {
-        function createAndSubscribe() {
-            const pubSub = new PubSub(),
-                command = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-    
-            assert.equal(pubSub.isActive, true);
-            assert.ok(command);
-            assert.equal(command.channelsCounter, 1);
-            
-            return {
-                pubSub,
-                command
-            };
-        }
-
-        it('resolve', () => {
-            const { pubSub, command } = createAndSubscribe();
-    
-            command.resolve();
-
-            assert.equal(pubSub.isActive, true);
-        });
-
-        it('reject', () => {
-            const { pubSub, command } = createAndSubscribe();
-    
-            assert.ok(command.reject);
-            command.reject();
-
-            assert.equal(pubSub.isActive, false);
-        });
+  const TYPE = PUBSUB_TYPE.CHANNELS,
+    CHANNEL = 'channel',
+    LISTENER = () => {};
+
+  describe('subscribe to new channel', () => {
+    function createAndSubscribe() {
+      const pubSub = new PubSub(),
+        command = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+
+      assert.equal(pubSub.isActive, true);
+      assert.ok(command);
+      assert.equal(command.channelsCounter, 1);
+
+      return {
+        pubSub,
+        command
+      };
+    }
+
+    it('resolve', () => {
+      const { pubSub, command } = createAndSubscribe();
+
+      command.resolve();
+
+      assert.equal(pubSub.isActive, true);
     });
 
-    it('subscribe to already subscribed channel', () => {
-        const pubSub = new PubSub(),
-            firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-        assert.ok(firstSubscribe);
+    it('reject', () => {
+      const { pubSub, command } = createAndSubscribe();
 
-        const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-        assert.ok(secondSubscribe);
+      assert.ok(command.reject);
+      command.reject();
 
-        firstSubscribe.resolve();
+      assert.equal(pubSub.isActive, false);
+    });
+  });
+
+  it('subscribe to already subscribed channel', () => {
+    const pubSub = new PubSub(),
+      firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+    assert.ok(firstSubscribe);
+
+    const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+    assert.ok(secondSubscribe);
+
+    firstSubscribe.resolve();
+
+    assert.equal(
+      pubSub.subscribe(TYPE, CHANNEL, LISTENER),
+      undefined
+    );
+  });
+
+  it('unsubscribe all', () => {
+    const pubSub = new PubSub();
+
+    const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+    assert.ok(subscribe);
+    subscribe.resolve();
+    assert.equal(pubSub.isActive, true);
+
+    const unsubscribe = pubSub.unsubscribe(TYPE);
+    assert.equal(pubSub.isActive, true);
+    assert.ok(unsubscribe);
+    unsubscribe.resolve();
+    assert.equal(pubSub.isActive, false);
+  });
+
+  describe('unsubscribe from channel', () => {
+    it('when not subscribed', () => {
+      const pubSub = new PubSub(),
+        unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
+      assert.ok(unsubscribe);
+      unsubscribe.resolve();
+      assert.equal(pubSub.isActive, false);
+    });
 
-        assert.equal(
-            pubSub.subscribe(TYPE, CHANNEL, LISTENER),
-            undefined
-        );
+    it('when already subscribed', () => {
+      const pubSub = new PubSub(),
+        subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+      assert.ok(subscribe);
+      subscribe.resolve();
+      assert.equal(pubSub.isActive, true);
+
+      const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
+      assert.equal(pubSub.isActive, true);
+      assert.ok(unsubscribe);
+      unsubscribe.resolve();
+      assert.equal(pubSub.isActive, false);
+    });
+  });
+
+  describe('unsubscribe from listener', () => {
+    it('when it\'s the only listener', () => {
+      const pubSub = new PubSub(),
+        subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+      assert.ok(subscribe);
+      subscribe.resolve();
+      assert.equal(pubSub.isActive, true);
+
+      const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER);
+      assert.ok(unsubscribe);
+      unsubscribe.resolve();
+      assert.equal(pubSub.isActive, false);
     });
 
-    it('unsubscribe all', () => {
-        const pubSub = new PubSub();
-        
-        const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+    it('when there are more listeners', () => {
+      const pubSub = new PubSub(),
+        subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
+      assert.ok(subscribe);
+      subscribe.resolve();
+      assert.equal(pubSub.isActive, true);
+
+      assert.equal(
+        pubSub.subscribe(TYPE, CHANNEL, () => { }),
+        undefined
+      );
+
+      assert.equal(
+        pubSub.unsubscribe(TYPE, CHANNEL, LISTENER),
+        undefined
+      );
+    });
+
+    describe('non-existing listener', () => {
+      it('on subscribed channel', () => {
+        const pubSub = new PubSub(),
+          subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
         assert.ok(subscribe);
         subscribe.resolve();
         assert.equal(pubSub.isActive, true);
 
-        const unsubscribe = pubSub.unsubscribe(TYPE);
+        assert.equal(
+          pubSub.unsubscribe(TYPE, CHANNEL, () => { }),
+          undefined
+        );
         assert.equal(pubSub.isActive, true);
-        assert.ok(unsubscribe);
-        unsubscribe.resolve();
-        assert.equal(pubSub.isActive, false);
-    });
-
-    describe('unsubscribe from channel', () => {
-        it('when not subscribed', () => {
-            const pubSub = new PubSub(),
-                unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
-            assert.ok(unsubscribe);
-            unsubscribe.resolve();
-            assert.equal(pubSub.isActive, false);
-        });
-
-        it('when already subscribed', () => {
-            const pubSub = new PubSub(),
-                subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-            assert.ok(subscribe);
-            subscribe.resolve();
-            assert.equal(pubSub.isActive, true);
-    
-            const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
-            assert.equal(pubSub.isActive, true);
-            assert.ok(unsubscribe);
-            unsubscribe.resolve();
-            assert.equal(pubSub.isActive, false);
-        });
-    });
+      });
 
-    describe('unsubscribe from listener', () => {
-        it('when it\'s the only listener', () => {
-            const pubSub = new PubSub(),
-                subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-            assert.ok(subscribe);
-            subscribe.resolve();
-            assert.equal(pubSub.isActive, true);
-
-            const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER);
-            assert.ok(unsubscribe);
-            unsubscribe.resolve();
-            assert.equal(pubSub.isActive, false);
-        });
-
-        it('when there are more listeners', () => {
-            const pubSub = new PubSub(),
-                subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-            assert.ok(subscribe);
-            subscribe.resolve();
-            assert.equal(pubSub.isActive, true);
-
-            assert.equal(
-                pubSub.subscribe(TYPE, CHANNEL, () => {}),
-                undefined
-            );
-
-            assert.equal(
-                pubSub.unsubscribe(TYPE, CHANNEL, LISTENER),
-                undefined
-            );
-        });
-
-        describe('non-existing listener', () => {
-            it('on subscribed channel', () => {
-                const pubSub = new PubSub(),
-                    subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
-                assert.ok(subscribe);
-                subscribe.resolve();
-                assert.equal(pubSub.isActive, true);
-
-                assert.equal(
-                    pubSub.unsubscribe(TYPE, CHANNEL, () => {}),
-                    undefined
-                );
-                assert.equal(pubSub.isActive, true);
-            });
-
-            it('on unsubscribed channel', () => {
-                const pubSub = new PubSub();
-                assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => {}));
-                assert.equal(pubSub.isActive, false);
-            });
-        });
+      it('on unsubscribed channel', () => {
+        const pubSub = new PubSub();
+        assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => { }));
+        assert.equal(pubSub.isActive, false);
+      });
     });
+  });
 });
diff --git a/packages/client/lib/client/pub-sub.ts b/packages/client/lib/client/pub-sub.ts
index a8a909e0252..1387aea8417 100644
--- a/packages/client/lib/client/pub-sub.ts
+++ b/packages/client/lib/client/pub-sub.ts
@@ -1,408 +1,409 @@
-import { RedisCommandArgument } from "../commands";
+import { RedisArgument } from '../RESP/types';
+import { CommandToWrite } from './commands-queue';
 
-export enum PubSubType {
-    CHANNELS = 'CHANNELS',
-    PATTERNS = 'PATTERNS',
-    SHARDED = 'SHARDED'
-}
+export const PUBSUB_TYPE = {
+  CHANNELS: 'CHANNELS',
+  PATTERNS: 'PATTERNS',
+  SHARDED: 'SHARDED'
+} as const;
+
+export type PUBSUB_TYPE = typeof PUBSUB_TYPE;
+
+export type PubSubType = PUBSUB_TYPE[keyof PUBSUB_TYPE];
 
 const COMMANDS = {
-    [PubSubType.CHANNELS]: {
-        subscribe: Buffer.from('subscribe'),
-        unsubscribe: Buffer.from('unsubscribe'),
-        message: Buffer.from('message')
-    },
-    [PubSubType.PATTERNS]: {
-        subscribe: Buffer.from('psubscribe'),
-        unsubscribe: Buffer.from('punsubscribe'),
-        message: Buffer.from('pmessage')
-    },
-    [PubSubType.SHARDED]: {
-        subscribe: Buffer.from('ssubscribe'),
-        unsubscribe: Buffer.from('sunsubscribe'),
-        message: Buffer.from('smessage')
-    }
+  [PUBSUB_TYPE.CHANNELS]: {
+    subscribe: Buffer.from('subscribe'),
+    unsubscribe: Buffer.from('unsubscribe'),
+    message: Buffer.from('message')
+  },
+  [PUBSUB_TYPE.PATTERNS]: {
+    subscribe: Buffer.from('psubscribe'),
+    unsubscribe: Buffer.from('punsubscribe'),
+    message: Buffer.from('pmessage')
+  },
+  [PUBSUB_TYPE.SHARDED]: {
+    subscribe: Buffer.from('ssubscribe'),
+    unsubscribe: Buffer.from('sunsubscribe'),
+    message: Buffer.from('smessage')
+  }
 };
 
 export type PubSubListener<
-    RETURN_BUFFERS extends boolean = false
+  RETURN_BUFFERS extends boolean = false
 > = <T extends RETURN_BUFFERS extends true ? Buffer : string>(message: T, channel: T) => unknown;
 
 export interface ChannelListeners {
-    unsubscribing: boolean;
-    buffers: Set<PubSubListener<true>>;
-    strings: Set<PubSubListener<false>>;
+  unsubscribing: boolean;
+  buffers: Set<PubSubListener<true>>;
+  strings: Set<PubSubListener<false>>;
 }
 
 export type PubSubTypeListeners = Map<string, ChannelListeners>;
 
-type Listeners = Record<PubSubType, PubSubTypeListeners>;
+export type PubSubListeners = Record<PubSubType, PubSubTypeListeners>;
 
-export type PubSubCommand = ReturnType<
-    typeof PubSub.prototype.subscribe |
-    typeof PubSub.prototype.unsubscribe | 
-    typeof PubSub.prototype.extendTypeListeners
->;
+export type PubSubCommand = (
+  Required<Pick<CommandToWrite, 'args' | 'channelsCounter' | 'resolve'>> & {
+    reject: undefined | (() => unknown);
+  }
+);
 
 export class PubSub {
-    static isStatusReply(reply: Array<Buffer>): boolean {
-        return (
-            COMMANDS[PubSubType.CHANNELS].subscribe.equals(reply[0]) ||
-            COMMANDS[PubSubType.CHANNELS].unsubscribe.equals(reply[0]) ||
-            COMMANDS[PubSubType.PATTERNS].subscribe.equals(reply[0]) ||
-            COMMANDS[PubSubType.PATTERNS].unsubscribe.equals(reply[0]) ||
-            COMMANDS[PubSubType.SHARDED].subscribe.equals(reply[0])
-        );
-    }
-
-    static isShardedUnsubscribe(reply: Array<Buffer>): boolean {
-        return COMMANDS[PubSubType.SHARDED].unsubscribe.equals(reply[0]);
+  static isStatusReply(reply: Array<Buffer>): boolean {
+    return (
+      COMMANDS[PUBSUB_TYPE.CHANNELS].subscribe.equals(reply[0]) ||
+      COMMANDS[PUBSUB_TYPE.CHANNELS].unsubscribe.equals(reply[0]) ||
+      COMMANDS[PUBSUB_TYPE.PATTERNS].subscribe.equals(reply[0]) ||
+      COMMANDS[PUBSUB_TYPE.PATTERNS].unsubscribe.equals(reply[0]) ||
+      COMMANDS[PUBSUB_TYPE.SHARDED].subscribe.equals(reply[0])
+    );
+  }
+
+  static isShardedUnsubscribe(reply: Array<Buffer>): boolean {
+    return COMMANDS[PUBSUB_TYPE.SHARDED].unsubscribe.equals(reply[0]);
+  }
+
+  static #channelsArray(channels: string | Array<string>) {
+    return (Array.isArray(channels) ? channels : [channels]);
+  }
+
+  static #listenersSet<T extends boolean>(
+    listeners: ChannelListeners,
+    returnBuffers?: T
+  ) {
+    return (returnBuffers ? listeners.buffers : listeners.strings);
+  }
+
+  #subscribing = 0;
+
+  #isActive = false;
+
+  get isActive() {
+    return this.#isActive;
+  }
+
+  readonly listeners: PubSubListeners = {
+    [PUBSUB_TYPE.CHANNELS]: new Map(),
+    [PUBSUB_TYPE.PATTERNS]: new Map(),
+    [PUBSUB_TYPE.SHARDED]: new Map()
+  };
+
+  subscribe<T extends boolean>(
+    type: PubSubType,
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    returnBuffers?: T
+  ) {
+    const args: Array<RedisArgument> = [COMMANDS[type].subscribe],
+      channelsArray = PubSub.#channelsArray(channels);
+    for (const channel of channelsArray) {
+      let channelListeners = this.listeners[type].get(channel);
+      if (!channelListeners || channelListeners.unsubscribing) {
+        args.push(channel);
+      }
     }
-    
-    static #channelsArray(channels: string | Array<string>) {
-        return (Array.isArray(channels) ? channels : [channels]);
-    }
-
-    static #listenersSet<T extends boolean>(
-        listeners: ChannelListeners,
-        returnBuffers?: T
-    ) {
-        return (returnBuffers ? listeners.buffers : listeners.strings);
-    }
-
-    #subscribing = 0;
-
-    #isActive = false;
 
-    get isActive() {
-        return this.#isActive;
+    if (args.length === 1) {
+      // all channels are already subscribed, add listeners without issuing a command
+      for (const channel of channelsArray) {
+        PubSub.#listenersSet(
+          this.listeners[type].get(channel)!,
+          returnBuffers
+        ).add(listener);
+      }
+      return;
     }
 
-    #listeners: Listeners = {
-        [PubSubType.CHANNELS]: new Map(),
-        [PubSubType.PATTERNS]: new Map(),
-        [PubSubType.SHARDED]: new Map()
-    };
-
-    subscribe<T extends boolean>(
-        type: PubSubType,
-        channels: string | Array<string>,
-        listener: PubSubListener<T>,
-        returnBuffers?: T
-    ) {
-        const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe],
-            channelsArray = PubSub.#channelsArray(channels);
+    this.#isActive = true;
+    this.#subscribing++;
+    return {
+      args,
+      channelsCounter: args.length - 1,
+      resolve: () => {
+        this.#subscribing--;
         for (const channel of channelsArray) {
-            let channelListeners = this.#listeners[type].get(channel);
-            if (!channelListeners || channelListeners.unsubscribing) {
-                args.push(channel);
-            }
+          let listeners = this.listeners[type].get(channel);
+          if (!listeners) {
+            listeners = {
+              unsubscribing: false,
+              buffers: new Set(),
+              strings: new Set()
+            };
+            this.listeners[type].set(channel, listeners);
+          }
+
+          PubSub.#listenersSet(listeners, returnBuffers).add(listener);
         }
-
-        if (args.length === 1) {
-            // all channels are already subscribed, add listeners without issuing a command
-            for (const channel of channelsArray) {
-                PubSub.#listenersSet(
-                    this.#listeners[type].get(channel)!,
-                    returnBuffers
-                ).add(listener);
-            }
-            return;
-        }
-
-        this.#isActive = true;
-        this.#subscribing++;
-        return {
-            args,
-            channelsCounter: args.length - 1,
-            resolve: () => {
-                this.#subscribing--;
-                for (const channel of channelsArray) {
-                    let listeners = this.#listeners[type].get(channel);
-                    if (!listeners) {
-                        listeners = {
-                            unsubscribing: false,
-                            buffers: new Set(),
-                            strings: new Set()
-                        };
-                        this.#listeners[type].set(channel, listeners);
-                    }
-
-                    PubSub.#listenersSet(listeners, returnBuffers).add(listener);
-                }
-            },
-            reject: () => {
-                this.#subscribing--;
-                this.#updateIsActive();
-            }
-        };
+      },
+      reject: () => {
+        this.#subscribing--;
+        this.#updateIsActive();
+      }
+    } satisfies PubSubCommand;
+  }
+
+  extendChannelListeners(
+    type: PubSubType,
+    channel: string,
+    listeners: ChannelListeners
+  ) {
+    if (!this.#extendChannelListeners(type, channel, listeners)) return;
+
+    this.#isActive = true;
+    this.#subscribing++;
+    return {
+      args: [
+        COMMANDS[type].subscribe,
+        channel
+      ],
+      channelsCounter: 1,
+      resolve: () => this.#subscribing--,
+      reject: () => {
+        this.#subscribing--;
+        this.#updateIsActive();
+      }
+    } satisfies PubSubCommand;
+  }
+
+  #extendChannelListeners(
+    type: PubSubType,
+    channel: string,
+    listeners: ChannelListeners
+  ) {
+    const existingListeners = this.listeners[type].get(channel);
+    if (!existingListeners) {
+      this.listeners[type].set(channel, listeners);
+      return true;
     }
 
-    extendChannelListeners(
-        type: PubSubType,
-        channel: string,
-        listeners: ChannelListeners
-    ) {
-        if (!this.#extendChannelListeners(type, channel, listeners)) return;
-
-        this.#isActive = true;
-        this.#subscribing++;
-        return {
-            args: [
-                COMMANDS[type].subscribe,
-                channel
-            ],
-            channelsCounter: 1,
-            resolve: () => this.#subscribing--,
-            reject: () => {
-                this.#subscribing--;
-                this.#updateIsActive();
-            }
-        };
+    for (const listener of listeners.buffers) {
+      existingListeners.buffers.add(listener);
     }
 
-    #extendChannelListeners(
-        type: PubSubType,
-        channel: string,
-        listeners: ChannelListeners
-    ) {
-        const existingListeners = this.#listeners[type].get(channel);
-        if (!existingListeners) {
-            this.#listeners[type].set(channel, listeners);
-            return true;
-        }
-
-        for (const listener of listeners.buffers) {
-            existingListeners.buffers.add(listener);
-        }
-
-        for (const listener of listeners.strings) {
-            existingListeners.strings.add(listener);
-        }
-
-        return false;
+    for (const listener of listeners.strings) {
+      existingListeners.strings.add(listener);
     }
 
-    extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) {
-        const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe];
-        for (const [channel, channelListeners] of listeners) {
-            if (this.#extendChannelListeners(type, channel, channelListeners)) {
-                args.push(channel);
-            }
-        }
+    return false;
+  }
 
-        if (args.length === 1) return;
-
-        this.#isActive = true;
-        this.#subscribing++;
-        return {
-            args,
-            channelsCounter: args.length - 1,
-            resolve: () => this.#subscribing--,
-            reject: () => {
-                this.#subscribing--;
-                this.#updateIsActive();
-            }
-        };
+  extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) {
+    const args: Array<RedisArgument> = [COMMANDS[type].subscribe];
+    for (const [channel, channelListeners] of listeners) {
+      if (this.#extendChannelListeners(type, channel, channelListeners)) {
+        args.push(channel);
+      }
     }
 
-    unsubscribe<T extends boolean>(
-        type: PubSubType,
-        channels?: string | Array<string>,
-        listener?: PubSubListener<T>,
-        returnBuffers?: T
-    ) {
-        const listeners = this.#listeners[type];
-        if (!channels) {
-            return this.#unsubscribeCommand(
-                [COMMANDS[type].unsubscribe],
-                // cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue
-                // cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail
-                NaN,
-                () => listeners.clear()
-            );
-        }
+    if (args.length === 1) return;
 
-        const channelsArray = PubSub.#channelsArray(channels);
-        if (!listener) {
-            return this.#unsubscribeCommand(
-                [COMMANDS[type].unsubscribe, ...channelsArray],
-                channelsArray.length,
-                () => {
-                    for (const channel of channelsArray) {
-                        listeners.delete(channel);
-                    }
-                }
-            );
-        }
+    this.#isActive = true;
+    this.#subscribing++;
+    return {
+      args,
+      channelsCounter: args.length - 1,
+      resolve: () => this.#subscribing--,
+      reject: () => {
+        this.#subscribing--;
+        this.#updateIsActive();
+      }
+    } satisfies PubSubCommand;
+  }
+
+  unsubscribe<T extends boolean>(
+    type: PubSubType,
+    channels?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    returnBuffers?: T
+  ) {
+    const listeners = this.listeners[type];
+    if (!channels) {
+      return this.#unsubscribeCommand(
+        [COMMANDS[type].unsubscribe],
+        // cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue
+        // cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail
+        NaN,
+        () => listeners.clear()
+      );
+    }
 
-        const args: Array<RedisCommandArgument> = [COMMANDS[type].unsubscribe];
-        for (const channel of channelsArray) {
-            const sets = listeners.get(channel);
-            if (sets) {
-                let current,
-                    other;
-                if (returnBuffers) {
-                    current = sets.buffers;
-                    other = sets.strings;
-                } else {
-                    current = sets.strings;
-                    other = sets.buffers;
-                }
-
-                const currentSize = current.has(listener) ? current.size - 1 : current.size;
-                if (currentSize !== 0 || other.size !== 0) continue;
-                sets.unsubscribing = true;
-            }
-
-            args.push(channel);
+    const channelsArray = PubSub.#channelsArray(channels);
+    if (!listener) {
+      return this.#unsubscribeCommand(
+        [COMMANDS[type].unsubscribe, ...channelsArray],
+        channelsArray.length,
+        () => {
+          for (const channel of channelsArray) {
+            listeners.delete(channel);
+          }
         }
+      );
+    }
 
-        if (args.length === 1) {
-            // all channels has other listeners,
-            // delete the listeners without issuing a command
-            for (const channel of channelsArray) {
-                PubSub.#listenersSet(
-                    listeners.get(channel)!,
-                    returnBuffers
-                ).delete(listener);
-            }
-            return;
+    const args: Array<RedisArgument> = [COMMANDS[type].unsubscribe];
+    for (const channel of channelsArray) {
+      const sets = listeners.get(channel);
+      if (sets) {
+        let current,
+          other;
+        if (returnBuffers) {
+          current = sets.buffers;
+          other = sets.strings;
+        } else {
+          current = sets.strings;
+          other = sets.buffers;
         }
 
-        return this.#unsubscribeCommand(
-            args,
-            args.length - 1,
-            () => {
-                for (const channel of channelsArray) {
-                    const sets = listeners.get(channel);
-                    if (!sets) continue;
-
-                    (returnBuffers ? sets.buffers : sets.strings).delete(listener);
-                    if (sets.buffers.size === 0 && sets.strings.size === 0) {
-                        listeners.delete(channel);
-                    }
-                }
-            }
-        );
-    }
+        const currentSize = current.has(listener) ? current.size - 1 : current.size;
+        if (currentSize !== 0 || other.size !== 0) continue;
+        sets.unsubscribing = true;
+      }
 
-    #unsubscribeCommand(
-        args: Array<RedisCommandArgument>,
-        channelsCounter: number,
-        removeListeners: () => void
-    ) {
-        return {
-            args,
-            channelsCounter,
-            resolve: () => {
-                removeListeners();
-                this.#updateIsActive();
-            },
-            reject: undefined // use the same structure as `subscribe`
-        };
+      args.push(channel);
     }
 
-    #updateIsActive() {
-        this.#isActive = (
-            this.#listeners[PubSubType.CHANNELS].size !== 0 ||
-            this.#listeners[PubSubType.PATTERNS].size !== 0 ||
-            this.#listeners[PubSubType.SHARDED].size !== 0 ||
-            this.#subscribing !== 0
-        );
+    if (args.length === 1) {
+      // all channels has other listeners,
+      // delete the listeners without issuing a command
+      for (const channel of channelsArray) {
+        PubSub.#listenersSet(
+          listeners.get(channel)!,
+          returnBuffers
+        ).delete(listener);
+      }
+      return;
     }
 
-    reset() {
-        this.#isActive = false;
-        this.#subscribing = 0;
-    }
+    return this.#unsubscribeCommand(
+      args,
+      args.length - 1,
+      () => {
+        for (const channel of channelsArray) {
+          const sets = listeners.get(channel);
+          if (!sets) continue;
 
-    resubscribe(): Array<PubSubCommand> {
-        const commands = [];
-        for (const [type, listeners] of Object.entries(this.#listeners)) {
-            if (!listeners.size) continue;
-
-            this.#isActive = true;
-            this.#subscribing++;
-            const callback = () => this.#subscribing--;
-            commands.push({
-                args: [
-                    COMMANDS[type as PubSubType].subscribe,
-                    ...listeners.keys()
-                ],
-                channelsCounter: listeners.size,
-                resolve: callback,
-                reject: callback
-            });
+          (returnBuffers ? sets.buffers : sets.strings).delete(listener);
+          if (sets.buffers.size === 0 && sets.strings.size === 0) {
+            listeners.delete(channel);
+          }
         }
-
-        return commands;
+      }
+    );
+  }
+
+  #unsubscribeCommand(
+    args: Array<RedisArgument>,
+    channelsCounter: number,
+    removeListeners: () => void
+  ) {
+    return {
+      args,
+      channelsCounter,
+      resolve: () => {
+        removeListeners();
+        this.#updateIsActive();
+      },
+      reject: undefined
+    } satisfies PubSubCommand;
+  }
+
+  #updateIsActive() {
+    this.#isActive = (
+      this.listeners[PUBSUB_TYPE.CHANNELS].size !== 0 ||
+      this.listeners[PUBSUB_TYPE.PATTERNS].size !== 0 ||
+      this.listeners[PUBSUB_TYPE.SHARDED].size !== 0 ||
+      this.#subscribing !== 0
+    );
+  }
+
+  reset() {
+    this.#isActive = false;
+    this.#subscribing = 0;
+  }
+
+  resubscribe() {
+    const commands = [];
+    for (const [type, listeners] of Object.entries(this.listeners)) {
+      if (!listeners.size) continue;
+
+      this.#isActive = true;
+      this.#subscribing++;
+      const callback = () => this.#subscribing--;
+      commands.push({
+        args: [
+          COMMANDS[type as PubSubType].subscribe,
+          ...listeners.keys()
+        ],
+        channelsCounter: listeners.size,
+        resolve: callback,
+        reject: callback
+      } satisfies PubSubCommand);
     }
 
-    handleMessageReply(reply: Array<Buffer>): boolean {
-        if (COMMANDS[PubSubType.CHANNELS].message.equals(reply[0])) {
-            this.#emitPubSubMessage(
-                PubSubType.CHANNELS,
-                reply[2],
-                reply[1]
-            );
-            return true;
-        } else if (COMMANDS[PubSubType.PATTERNS].message.equals(reply[0])) {
-            this.#emitPubSubMessage(
-                PubSubType.PATTERNS,
-                reply[3],
-                reply[2],
-                reply[1]
-            );
-            return true;
-        } else if (COMMANDS[PubSubType.SHARDED].message.equals(reply[0])) {
-            this.#emitPubSubMessage(
-                PubSubType.SHARDED,
-                reply[2],
-                reply[1]
-            );
-            return true;
-        }
-
-        return false;
+    return commands;
+  }
+
+  handleMessageReply(reply: Array<Buffer>): boolean {
+    if (COMMANDS[PUBSUB_TYPE.CHANNELS].message.equals(reply[0])) {
+      this.#emitPubSubMessage(
+        PUBSUB_TYPE.CHANNELS,
+        reply[2],
+        reply[1]
+      );
+      return true;
+    } else if (COMMANDS[PUBSUB_TYPE.PATTERNS].message.equals(reply[0])) {
+      this.#emitPubSubMessage(
+        PUBSUB_TYPE.PATTERNS,
+        reply[3],
+        reply[2],
+        reply[1]
+      );
+      return true;
+    } else if (COMMANDS[PUBSUB_TYPE.SHARDED].message.equals(reply[0])) {
+      this.#emitPubSubMessage(
+        PUBSUB_TYPE.SHARDED,
+        reply[2],
+        reply[1]
+      );
+      return true;
     }
 
-    removeShardedListeners(channel: string): ChannelListeners {
-        const listeners = this.#listeners[PubSubType.SHARDED].get(channel)!;
-        this.#listeners[PubSubType.SHARDED].delete(channel);
-        this.#updateIsActive();
-        return listeners;
+    return false;
+  }
+
+  removeShardedListeners(channel: string): ChannelListeners {
+    const listeners = this.listeners[PUBSUB_TYPE.SHARDED].get(channel)!;
+    this.listeners[PUBSUB_TYPE.SHARDED].delete(channel);
+    this.#updateIsActive();
+    return listeners;
+  }
+
+  #emitPubSubMessage(
+    type: PubSubType,
+    message: Buffer,
+    channel: Buffer,
+    pattern?: Buffer
+  ): void {
+    const keyString = (pattern ?? channel).toString(),
+      listeners = this.listeners[type].get(keyString);
+
+    if (!listeners) return;
+
+    for (const listener of listeners.buffers) {
+      listener(message, channel);
     }
-    
-    #emitPubSubMessage(
-        type: PubSubType,
-        message: Buffer,
-        channel: Buffer,
-        pattern?: Buffer
-    ): void {
-        const keyString = (pattern ?? channel).toString(),
-            listeners = this.#listeners[type].get(keyString);
-
-        if (!listeners) return;
-
-        for (const listener of listeners.buffers) {
-            listener(message, channel);
-        }
-
-        if (!listeners.strings.size) return;
 
-        const channelString = pattern ? channel.toString() : keyString,
-            messageString = channelString === '__redis__:invalidate' ?
-                // https://github.com/redis/redis/pull/7469
-                // https://github.com/redis/redis/issues/7463
-                (message === null ? null : (message as any as Array<Buffer>).map(x => x.toString())) as any :
-                message.toString();
-        for (const listener of listeners.strings) {
-            listener(messageString, channelString);
-        }
-    }
+    if (!listeners.strings.size) return;
 
-    getTypeListeners(type: PubSubType): PubSubTypeListeners {
-        return this.#listeners[type];
+    const channelString = pattern ? channel.toString() : keyString,
+      messageString = channelString === '__redis__:invalidate' ?
+        // https://github.com/redis/redis/pull/7469
+        // https://github.com/redis/redis/issues/7463
+        (message === null ? null : (message as any as Array<Buffer>).map(x => x.toString())) as any :
+        message.toString();
+    for (const listener of listeners.strings) {
+      listener(messageString, channelString);
     }
+  }
 }
diff --git a/packages/client/lib/client/socket.spec.ts b/packages/client/lib/client/socket.spec.ts
index eb555351ac4..5117cc4f49d 100644
--- a/packages/client/lib/client/socket.spec.ts
+++ b/packages/client/lib/client/socket.spec.ts
@@ -1,87 +1,148 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import { spy } from 'sinon';
-import { once } from 'events';
+import { once } from 'node:events';
 import RedisSocket, { RedisSocketOptions } from './socket';
+import testUtils, { GLOBAL } from '../test-utils';
+import { setTimeout } from 'timers/promises';
 
 describe('Socket', () => {
-    function createSocket(options: RedisSocketOptions): RedisSocket {
-        const socket = new RedisSocket(
-            () => Promise.resolve(),
-            options
-        );
-
-        socket.on('error', () => {
-            // ignore errors
-        });
+  function createSocket(options: RedisSocketOptions): RedisSocket {
+    const socket = new RedisSocket(() => Promise.resolve(), options);
 
-        return socket;
-    }
+    socket.on('error', () => {
+      // ignore errors
+    });
 
-    describe('reconnectStrategy', () => {
-        it('false', async () => {
-            const socket = createSocket({
-                host: 'error',
-                connectTimeout: 1,
-                reconnectStrategy: false
-            });
+    return socket;
+  }
 
-            await assert.rejects(socket.connect());
+  describe('reconnectStrategy', () => {
+    it('false', async () => {
+      const socket = createSocket({
+        host: 'error',
+        connectTimeout: 1,
+        reconnectStrategy: false
+      });
 
-            assert.equal(socket.isOpen, false);
-        });
+      await assert.rejects(socket.connect());
 
-        it('0', async () => {
-            const socket = createSocket({
-                host: 'error',
-                connectTimeout: 1,
-                reconnectStrategy: 0
-            });
-
-            socket.connect();
-            await once(socket, 'error');
-            assert.equal(socket.isOpen, true);
-            assert.equal(socket.isReady, false);
-            socket.disconnect();
-            assert.equal(socket.isOpen, false);
-        });
+      assert.equal(socket.isOpen, false);
+    });
 
-        it('custom strategy', async () => {
-            const numberOfRetries = 3;
+    it('0', async () => {
+      const socket = createSocket({
+        host: 'error',
+        connectTimeout: 1,
+        reconnectStrategy: 0
+      });
+
+      socket.connect();
+      await once(socket, 'error');
+      assert.equal(socket.isOpen, true);
+      assert.equal(socket.isReady, false);
+      socket.destroy();
+      assert.equal(socket.isOpen, false);
+    });
 
-            const reconnectStrategy = spy((retries: number) => {
-                assert.equal(retries + 1, reconnectStrategy.callCount);
+    it('custom strategy', async () => {
+      const numberOfRetries = 3;
 
-                if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
+      const reconnectStrategy = spy((retries: number) => {
+        assert.equal(retries + 1, reconnectStrategy.callCount);
 
-                return 0;
-            });
+        if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
 
-            const socket = createSocket({
-                host: 'error',
-                connectTimeout: 1,
-                reconnectStrategy
-            });
+        return 0;
+      });
 
-            await assert.rejects(socket.connect(), {
-                message: `${numberOfRetries}`
-            });
+      const socket = createSocket({
+        host: 'error',
+        connectTimeout: 1,
+        reconnectStrategy
+      });
 
-            assert.equal(socket.isOpen, false);
-        });
+      await assert.rejects(socket.connect(), {
+        message: `${numberOfRetries}`
+      });
 
-        it('should handle errors', async () => {
-            const socket = createSocket({
-                host: 'error',
-                connectTimeout: 1,
-                reconnectStrategy(retries: number) {
-                    if (retries === 1) return new Error('done');
-                    throw new Error();
-                }
-            });
+      assert.equal(socket.isOpen, false);
+    });
 
-            await assert.rejects(socket.connect());
+    it('should handle errors', async () => {
+      const socket = createSocket({
+        host: 'error',
+        connectTimeout: 1,
+        reconnectStrategy(retries: number) {
+          if (retries === 1) return new Error('done');
+          throw new Error();
+        }
+      });
 
-            assert.equal(socket.isOpen, false);
-        });
+      await assert.rejects(socket.connect());
+
+      assert.equal(socket.isOpen, false);
     });
+  });
+
+  describe('socketTimeout', () => {
+    const timeout = 50;
+    testUtils.testWithClient(
+      'should timeout with positive socketTimeout values',
+      async client => {
+        let timedOut = false;
+
+        assert.equal(client.isReady, true, 'client.isReady');
+        assert.equal(client.isOpen, true, 'client.isOpen');
+
+        client.on('error', err => {
+          assert.equal(
+            err.message,
+            `Socket timeout timeout. Expecting data, but didn't receive any in ${timeout}ms.`
+          );
+
+          assert.equal(client.isReady, false, 'client.isReady');
+
+          // This is actually a bug with the onSocketError implementation,
+          // the client should be closed before the error is emitted
+          process.nextTick(() => {
+            assert.equal(client.isOpen, false, 'client.isOpen');
+          });
+
+          timedOut = true;
+        });
+        await setTimeout(timeout * 2);
+        if (!timedOut) assert.fail('Should have timed out by now');
+      },
+      {
+        ...GLOBAL.SERVERS.OPEN,
+        clientOptions: {
+          socket: {
+            socketTimeout: timeout
+          }
+        }
+      }
+    );
+
+    testUtils.testWithClient(
+      'should not timeout with undefined socketTimeout',
+      async client => {
+
+        assert.equal(client.isReady, true, 'client.isReady');
+        assert.equal(client.isOpen, true, 'client.isOpen');
+
+        client.on('error', err => {
+          assert.fail('Should not have timed out or errored in any way');
+        });
+        await setTimeout(100);
+      },
+      {
+        ...GLOBAL.SERVERS.OPEN,
+        clientOptions: {
+          socket: {
+            socketTimeout: undefined
+          }
+        }
+      }
+    );
+  });
 });
diff --git a/packages/client/lib/client/socket.ts b/packages/client/lib/client/socket.ts
index b701f6ea979..5f0bcc44929 100644
--- a/packages/client/lib/client/socket.ts
+++ b/packages/client/lib/client/socket.ts
@@ -1,310 +1,369 @@
-import { EventEmitter } from 'events';
-import * as net from 'net';
-import * as tls from 'tls';
-import { RedisCommandArguments } from '../commands';
-import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors';
-import { promiseTimeout } from '../utils';
-
-export interface RedisSocketCommonOptions {
-    /**
-     * Connection Timeout (in milliseconds)
-     */
-    connectTimeout?: number;
-    /**
-     * Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay)
-     */
-    noDelay?: boolean;
-    /**
-     * Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay)
-     */
-    keepAlive?: number | false;
-    /**
-     * When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
-     * 1. `false` -> do not reconnect, close the client and flush the command queue.
-     * 2. `number` -> wait for `X` milliseconds before reconnecting.
-     * 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
-     * Defaults to `retries => Math.min(retries * 50, 500)`
-     */
-    reconnectStrategy?: false | number | ((retries: number, cause: Error) => false | Error | number);
+import { EventEmitter, once } from 'node:events';
+import net from 'node:net';
+import tls from 'node:tls';
+import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError, SocketTimeoutError } from '../errors';
+import { setTimeout } from 'node:timers/promises';
+import { RedisArgument } from '../RESP/types';
+
+type NetOptions = {
+  tls?: false;
+};
+
+type ReconnectStrategyFunction = (retries: number, cause: Error) => false | Error | number;
+
+type RedisSocketOptionsCommon = {
+  /**
+   * Connection timeout (in milliseconds)
+   */
+  connectTimeout?: number;
+  /**
+   * When the socket closes unexpectedly (without calling `.close()`/`.destroy()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
+   * 1. `false` -> do not reconnect, close the client and flush the command queue.
+   * 2. `number` -> wait for `X` milliseconds before reconnecting.
+   * 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
+   */
+  reconnectStrategy?: false | number | ReconnectStrategyFunction;
+  /**
+   * The timeout (in milliseconds) after which the socket will be closed. `undefined` means no timeout.
+   */
+  socketTimeout?: number;
 }
 
-type RedisNetSocketOptions = Partial<net.SocketConnectOpts> & {
-    tls?: false;
+type RedisTcpOptions = RedisSocketOptionsCommon & NetOptions & Omit<
+  net.TcpNetConnectOpts,
+  'timeout' | 'onread' | 'readable' | 'writable' | 'port'
+> & {
+  port?: number;
 };
 
-export interface RedisTlsSocketOptions extends tls.ConnectionOptions {
-    tls: true;
+type RedisTlsOptions = RedisSocketOptionsCommon & tls.ConnectionOptions & {
+  tls: true;
 }
 
-export type RedisSocketOptions = RedisSocketCommonOptions & (RedisNetSocketOptions | RedisTlsSocketOptions);
-
-interface CreateSocketReturn<T> {
-    connectEvent: string;
-    socket: T;
+type RedisIpcOptions = RedisSocketOptionsCommon & Omit<
+  net.IpcNetConnectOpts,
+  'timeout' | 'onread' | 'readable' | 'writable'
+> & {
+  tls: false;
 }
 
-export type RedisSocketInitiator = () => Promise<void>;
+export type RedisTcpSocketOptions = RedisTcpOptions | RedisTlsOptions;
 
-export default class RedisSocket extends EventEmitter {
-    static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
-        options ??= {};
-        if (!(options as net.IpcSocketConnectOpts).path) {
-            (options as net.TcpSocketConnectOpts).port ??= 6379;
-            (options as net.TcpSocketConnectOpts).host ??= 'localhost';
-        }
+export type RedisSocketOptions = RedisTcpSocketOptions | RedisIpcOptions;
 
-        options.connectTimeout ??= 5000;
-        options.keepAlive ??= 5000;
-        options.noDelay ??= true;
+export type RedisSocketInitiator = () => void | Promise<unknown>;
 
-        return options;
-    }
+export default class RedisSocket extends EventEmitter {
+  readonly #initiator;
+  readonly #connectTimeout;
+  readonly #reconnectStrategy;
+  readonly #socketFactory;
+  readonly #socketTimeout;
 
-    static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions {
-        return (options as RedisTlsSocketOptions).tls === true;
-    }
+  #socket?: net.Socket | tls.TLSSocket;
 
-    readonly #initiator: RedisSocketInitiator;
+  #isOpen = false;
 
-    readonly #options: RedisSocketOptions;
+  get isOpen() {
+    return this.#isOpen;
+  }
 
-    #socket?: net.Socket | tls.TLSSocket;
+  #isReady = false;
 
-    #isOpen = false;
+  get isReady() {
+    return this.#isReady;
+  }
 
-    get isOpen(): boolean {
-        return this.#isOpen;
-    }
+  #isSocketUnrefed = false;
 
-    #isReady = false;
+  #socketEpoch = 0;
 
-    get isReady(): boolean {
-        return this.#isReady;
-    }
+  get socketEpoch() {
+    return this.#socketEpoch;
+  }
 
-    // `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used
-    // https://nodejs.org/api/stream.html#stream_writable_writableneeddrain
-    #writableNeedDrain = false;
+  constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) {
+    super();
 
-    get writableNeedDrain(): boolean {
-        return this.#writableNeedDrain;
-    }
+    this.#initiator = initiator;
+    this.#connectTimeout = options?.connectTimeout ?? 5000;
+    this.#reconnectStrategy = this.#createReconnectStrategy(options);
+    this.#socketFactory = this.#createSocketFactory(options);
+    this.#socketTimeout = options?.socketTimeout;
+  }
 
-    #isSocketUnrefed = false;
+  #createReconnectStrategy(options?: RedisSocketOptions): ReconnectStrategyFunction {
+    const strategy = options?.reconnectStrategy;
+    if (strategy === false || typeof strategy === 'number') {
+      return () => strategy;
+    }
 
-    constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) {
-        super();
+    if (strategy) {
+      return (retries, cause) => {
+        try {
+          const retryIn = strategy(retries, cause);
+          if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') {
+            throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`);
+          }
+          return retryIn;
+        } catch (err) {
+          this.emit('error', err);
+          return this.defaultReconnectStrategy(retries, err);
+        }
+      };
+    }
 
-        this.#initiator = initiator;
-        this.#options = RedisSocket.#initiateOptions(options);
+    return this.defaultReconnectStrategy;
+  }
+
+  #createSocketFactory(options?: RedisSocketOptions) {
+    // TLS
+    if (options?.tls === true) {
+      const withDefaults: tls.ConnectionOptions = {
+        ...options,
+        port: options?.port ?? 6379,
+        // https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
+        // @types/node is... incorrect...
+        // @ts-expect-error
+        noDelay: options?.noDelay ?? true,
+        // https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
+        // @types/node is... incorrect...
+        // @ts-expect-error
+        keepAlive: options?.keepAlive ?? true,
+        // https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
+        // @types/node is... incorrect...
+        // @ts-expect-error
+        keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000,
+        timeout: undefined,
+        onread: undefined,
+        readable: true,
+        writable: true
+      };
+      return {
+        create() {
+          return tls.connect(withDefaults);
+        },
+        event: 'secureConnect'
+      };
     }
 
-    #reconnectStrategy(retries: number, cause: Error) {
-        if (this.#options.reconnectStrategy === false) {
-            return false;
-        } else if (typeof this.#options.reconnectStrategy === 'number') {
-            return this.#options.reconnectStrategy;
-        } else if (this.#options.reconnectStrategy) {
-            try {
-                const retryIn = this.#options.reconnectStrategy(retries, cause);
-                if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') {
-                    throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`);
-                }
-
-                return retryIn;
-            } catch (err) {
-                this.emit('error', err);    
-            }
-        }
+    // IPC
+    if (options && 'path' in options) {
+      const withDefaults: net.IpcNetConnectOpts = {
+        ...options,
+        timeout: undefined,
+        onread: undefined,
+        readable: true,
+        writable: true
+      };
+      return {
+        create() {
+          return net.createConnection(withDefaults);
+        },
+        event: 'connect'
+      };
+    }
 
-        return Math.min(retries * 50, 500);
+    // TCP
+    const withDefaults: net.TcpNetConnectOpts = {
+      ...options,
+      port: options?.port ?? 6379,
+      noDelay: options?.noDelay ?? true,
+      keepAlive: options?.keepAlive ?? true,
+      keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000,
+      timeout: undefined,
+      onread: undefined,
+      readable: true,
+      writable: true
+    };
+    return {
+      create() {
+        return net.createConnection(withDefaults);
+      },
+      event: 'connect'
+    };
+  }
+
+  #shouldReconnect(retries: number, cause: Error) {
+    const retryIn = this.#reconnectStrategy(retries, cause);
+    if (retryIn === false) {
+      this.#isOpen = false;
+      this.emit('error', cause);
+      return cause;
+    } else if (retryIn instanceof Error) {
+      this.#isOpen = false;
+      this.emit('error', cause);
+      return new ReconnectStrategyError(retryIn, cause);
     }
 
-    #shouldReconnect(retries: number, cause: Error) {
-        const retryIn = this.#reconnectStrategy(retries, cause);
-        if (retryIn === false) {
-            this.#isOpen = false;
-            this.emit('error', cause);
-            return cause;
-        } else if (retryIn instanceof Error) {
-            this.#isOpen = false;
-            this.emit('error', cause);
-            return new ReconnectStrategyError(retryIn, cause);
-        }
+    return retryIn;
+  }
 
-        return retryIn;
+  async connect(): Promise<void> {
+    if (this.#isOpen) {
+      throw new Error('Socket already opened');
     }
 
-    async connect(): Promise<void> {
-        if (this.#isOpen) {
-            throw new Error('Socket already opened');
+    this.#isOpen = true;
+    return this.#connect();
+  }
+
+  async #connect(): Promise<void> {
+    let retries = 0;
+    do {
+      try {
+        this.#socket = await this.#createSocket();
+        this.emit('connect');
+
+        try {
+          await this.#initiator();
+        } catch (err) {
+          this.#socket.destroy();
+          this.#socket = undefined;
+          throw err;
+        }
+        this.#isReady = true;
+        this.#socketEpoch++;
+        this.emit('ready');
+      } catch (err) {
+        const retryIn = this.#shouldReconnect(retries++, err as Error);
+        if (typeof retryIn !== 'number') {
+          throw retryIn;
         }
 
-        this.#isOpen = true;
-        return this.#connect();
+        this.emit('error', err);
+        await setTimeout(retryIn);
+        this.emit('reconnecting');
+      }
+    } while (this.#isOpen && !this.#isReady);
+  }
+
+  async #createSocket(): Promise<net.Socket | tls.TLSSocket> {
+    const socket = this.#socketFactory.create();
+
+    let onTimeout;
+    if (this.#connectTimeout !== undefined) {
+      onTimeout = () => socket.destroy(new ConnectionTimeoutError());
+      socket.once('timeout', onTimeout);
+      socket.setTimeout(this.#connectTimeout);
     }
 
-    async #connect(): Promise<void> {
-        let retries = 0;
-        do {
-            try {
-                this.#socket = await this.#createSocket();
-                this.#writableNeedDrain = false;
-                this.emit('connect');
-
-                try {
-                    await this.#initiator();
-                } catch (err) {
-                    this.#socket.destroy();
-                    this.#socket = undefined;
-                    throw err;
-                }
-                this.#isReady = true;
-                this.emit('ready');
-            } catch (err) {
-                const retryIn = this.#shouldReconnect(retries++, err as Error);
-                if (typeof retryIn !== 'number') {
-                    throw retryIn;
-                }
-
-                this.emit('error', err);
-                await promiseTimeout(retryIn);
-                this.emit('reconnecting');
-            }
-        } while (this.#isOpen && !this.#isReady);
+    if (this.#isSocketUnrefed) {
+      socket.unref();
     }
 
-    #createSocket(): Promise<net.Socket | tls.TLSSocket> {
-        return new Promise((resolve, reject) => {
-            const { connectEvent, socket } = RedisSocket.#isTlsSocket(this.#options) ?
-                this.#createTlsSocket() :
-                this.#createNetSocket();
-
-            if (this.#options.connectTimeout) {
-                socket.setTimeout(this.#options.connectTimeout, () => socket.destroy(new ConnectionTimeoutError()));
-            }
-
-            if (this.#isSocketUnrefed) {
-                socket.unref();
-            }
-
-            socket
-                .setNoDelay(this.#options.noDelay)
-                .once('error', reject)
-                .once(connectEvent, () => {
-                    socket
-                        .setTimeout(0)
-                        // https://github.com/nodejs/node/issues/31663
-                        .setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0)
-                        .off('error', reject)
-                        .once('error', (err: Error) => this.#onSocketError(err))
-                        .once('close', hadError => {
-                            if (!hadError && this.#isOpen && this.#socket === socket) {
-                                this.#onSocketError(new SocketClosedUnexpectedlyError());
-                            }
-                        })
-                        .on('drain', () => {
-                            this.#writableNeedDrain = false;
-                            this.emit('drain');
-                        })
-                        .on('data', data => this.emit('data', data));
-
-                    resolve(socket);
-                });
-        });
-    }
+    await once(socket, this.#socketFactory.event);
 
-    #createNetSocket(): CreateSocketReturn<net.Socket> {
-        return {
-            connectEvent: 'connect',
-            socket: net.connect(this.#options as net.NetConnectOpts) // TODO
-        };
+    if (onTimeout) {
+      socket.removeListener('timeout', onTimeout);
     }
 
-    #createTlsSocket(): CreateSocketReturn<tls.TLSSocket> {
-        return {
-            connectEvent: 'secureConnect',
-            socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO
-        };
+    if (this.#socketTimeout) {
+      socket.once('timeout', () => {
+        socket.destroy(new SocketTimeoutError(this.#socketTimeout!));
+      });
+      socket.setTimeout(this.#socketTimeout);
     }
 
-    #onSocketError(err: Error): void {
-        const wasReady = this.#isReady;
-        this.#isReady = false;
-        this.emit('error', err);
-
-        if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
-        
-        this.emit('reconnecting');
-        this.#connect().catch(() => {
-            // the error was already emitted, silently ignore it
-        });
+    socket
+      .once('error', err => this.#onSocketError(err))
+      .once('close', hadError => {
+        if (hadError || !this.#isOpen || this.#socket !== socket) return;
+        this.#onSocketError(new SocketClosedUnexpectedlyError());
+      })
+      .on('drain', () => this.emit('drain'))
+      .on('data', data => this.emit('data', data));
+
+    return socket;
+  }
+
+  #onSocketError(err: Error): void {
+    const wasReady = this.#isReady;
+    this.#isReady = false;
+    this.emit('error', err);
+
+    if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
+
+    this.emit('reconnecting');
+    this.#connect().catch(() => {
+      // the error was already emitted, silently ignore it
+    });
+  }
+
+  write(iterable: Iterable<ReadonlyArray<RedisArgument>>) {
+    if (!this.#socket) return;
+
+    this.#socket.cork();
+    for (const args of iterable) {
+      for (const toWrite of args) {
+        this.#socket.write(toWrite);
+      }
+
+      if (this.#socket.writableNeedDrain) break;
     }
+    this.#socket.uncork();
+  }
 
-    writeCommand(args: RedisCommandArguments): void {
-        if (!this.#socket) {
-            throw new ClientClosedError();
-        }
-
-        for (const toWrite of args) {
-            this.#writableNeedDrain = !this.#socket.write(toWrite);
-        }
+  async quit<T>(fn: () => Promise<T>): Promise<T> {
+    if (!this.#isOpen) {
+      throw new ClientClosedError();
     }
 
-    disconnect(): void {
-        if (!this.#isOpen) {
-            throw new ClientClosedError();
-        }
+    this.#isOpen = false;
+    const reply = await fn();
+    this.destroySocket();
+    return reply;
+  }
 
-        this.#isOpen = false;
-        this.#disconnect();
+  close() {
+    if (!this.#isOpen) {
+      throw new ClientClosedError();
     }
 
-    #disconnect(): void {
-        this.#isReady = false;
+    this.#isOpen = false;
+  }
 
-        if (this.#socket) {
-            this.#socket.destroy();
-            this.#socket = undefined;
-        }
-        
-        this.emit('end');
+  destroy() {
+    if (!this.#isOpen) {
+      throw new ClientClosedError();
     }
 
-    async quit<T>(fn: () => Promise<T>): Promise<T> {
-        if (!this.#isOpen) {
-            throw new ClientClosedError();
-        }
+    this.#isOpen = false;
+    this.destroySocket();
+  }
 
-        this.#isOpen = false;
-        const reply = await fn();
-        this.#disconnect();
-        return reply;
+  destroySocket() {
+    this.#isReady = false;
+
+    if (this.#socket) {
+      this.#socket.destroy();
+      this.#socket = undefined;
     }
 
-    #isCorked = false;
+    this.emit('end');
+  }
 
-    cork(): void {
-        if (!this.#socket || this.#isCorked) {
-            return;
-        }
+  ref() {
+    this.#isSocketUnrefed = false;
+    this.#socket?.ref();
+  }
 
-        this.#socket.cork();
-        this.#isCorked = true;
+  unref() {
+    this.#isSocketUnrefed = true;
+    this.#socket?.unref();
+  }
 
-        setImmediate(() => {
-            this.#socket?.uncork();
-            this.#isCorked = false;
-        });
+  defaultReconnectStrategy(retries: number, cause: unknown) {
+    // By default, do not reconnect on socket timeout.
+    if (cause instanceof SocketTimeoutError) {
+      return false;
     }
 
-    ref(): void {
-        this.#isSocketUnrefed = false;
-        this.#socket?.ref();
-    }
+    // Generate a random jitter between 0 – 200 ms:
+    const jitter = Math.floor(Math.random() * 200);
+    // Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:
+    const delay = Math.min(Math.pow(2, retries) * 50, 2000);
 
-    unref(): void {
-        this.#isSocketUnrefed = true;
-        this.#socket?.unref();
-    }
+    return delay + jitter;
+  }
 }
diff --git a/packages/client/lib/cluster/cluster-slots.spec.ts b/packages/client/lib/cluster/cluster-slots.spec.ts
new file mode 100644
index 00000000000..bea1453037a
--- /dev/null
+++ b/packages/client/lib/cluster/cluster-slots.spec.ts
@@ -0,0 +1,48 @@
+import { strict as assert } from 'node:assert';
+import { EventEmitter } from 'node:events';
+import { RedisClusterOptions, RedisClusterClientOptions } from './index';
+import RedisClusterSlots from './cluster-slots';
+
+describe('RedisClusterSlots', () => {
+  describe('initialization', () => {
+
+    describe('clientSideCache validation', () => {
+      const mockEmit = ((_event: string | symbol, ..._args: any[]): boolean => true) as EventEmitter['emit'];
+      const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
+      const rootNodes: Array<RedisClusterClientOptions> = [
+        { socket: { host: 'localhost', port: 30001 } }
+      ];
+
+      it('should throw error when clientSideCache is enabled with RESP 2', () => {
+        assert.throws(
+          () => new RedisClusterSlots({
+            rootNodes,
+            clientSideCache: clientSideCacheConfig,
+            RESP: 2 as const,
+          }, mockEmit),
+          new Error('Client Side Caching is only supported with RESP3')
+        );
+      });
+
+      it('should throw error when clientSideCache is enabled with RESP undefined', () => {
+        assert.throws(
+          () => new RedisClusterSlots({
+            rootNodes,
+            clientSideCache: clientSideCacheConfig,
+          }, mockEmit),
+          new Error('Client Side Caching is only supported with RESP3')
+        );
+      });
+
+      it('should not throw when clientSideCache is enabled with RESP 3', () => {
+        assert.doesNotThrow(() =>
+          new RedisClusterSlots({
+            rootNodes,
+            clientSideCache: clientSideCacheConfig,
+            RESP: 3 as const,
+          }, mockEmit)
+        );
+      });
+    });
+  });
+});
diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts
index 45c96a80b50..84486112320 100644
--- a/packages/client/lib/cluster/cluster-slots.ts
+++ b/packages/client/lib/cluster/cluster-slots.ts
@@ -1,621 +1,638 @@
-import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
 import { RedisClusterClientOptions, RedisClusterOptions } from '.';
-import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands';
 import { RootNodesUnavailableError } from '../errors';
-import { ClusterSlotsNode } from '../commands/CLUSTER_SLOTS';
-import { types } from 'util';
-import { ChannelListeners, PubSubType, PubSubTypeListeners } from '../client/pub-sub';
-import { EventEmitter } from 'stream';
-
-// We need to use 'require', because it's not possible with Typescript to import
-// function that are exported as 'module.exports = function`, without esModuleInterop
-// set to true.
-const calculateSlot = require('cluster-key-slot');
+import RedisClient, { RedisClientOptions, RedisClientType } from '../client';
+import { EventEmitter } from 'node:stream';
+import { ChannelListeners, PUBSUB_TYPE, PubSubTypeListeners } from '../client/pub-sub';
+import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
+import calculateSlot from 'cluster-key-slot';
+import { RedisSocketOptions } from '../client/socket';
+import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../client/cache';
 
 interface NodeAddress {
-    host: string;
-    port: number;
+  host: string;
+  port: number;
 }
 
 export type NodeAddressMap = {
-    [address: string]: NodeAddress;
+  [address: string]: NodeAddress;
 } | ((address: string) => NodeAddress | undefined);
 
-type ValueOrPromise<T> = T | Promise<T>;
-
-type ClientOrPromise<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = ValueOrPromise<RedisClientType<M, F, S>>;
-
 export interface Node<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > {
-    address: string;
-    client?: ClientOrPromise<M, F, S>;
+  address: string;
+  client?: RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+  connectPromise?: Promise<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>;
 }
 
 export interface ShardNode<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> extends Node<M, F, S> {
-    id: string;
-    host: string;
-    port: number;
-    readonly: boolean;
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> extends Node<M, F, S, RESP, TYPE_MAPPING>, NodeAddress {
+  id: string;
+  readonly: boolean;
 }
 
 export interface MasterNode<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> extends ShardNode<M, F, S> {
-    pubSubClient?: ClientOrPromise<M, F, S>;
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> extends ShardNode<M, F, S, RESP, TYPE_MAPPING> {
+  pubSub?: {
+    connectPromise?: Promise<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>;
+    client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+  };
 }
 
 export interface Shard<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > {
-    master: MasterNode<M, F, S>;
-    replicas?: Array<ShardNode<M, F, S>>;
-    nodesIterator?: IterableIterator<ShardNode<M, F, S>>;
+  master: MasterNode<M, F, S, RESP, TYPE_MAPPING>;
+  replicas?: Array<ShardNode<M, F, S, RESP, TYPE_MAPPING>>;
+  nodesIterator?: IterableIterator<ShardNode<M, F, S, RESP, TYPE_MAPPING>>;
 }
 
 type ShardWithReplicas<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = Shard<M, F, S> & Required<Pick<Shard<M, F, S>, 'replicas'>>;
-
-export type PubSubNode<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = Required<Node<M, F, S>>;
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = Shard<M, F, S, RESP, TYPE_MAPPING> & Required<Pick<Shard<M, F, S, RESP, TYPE_MAPPING>, 'replicas'>>;
+
+type PubSubNode<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (
+  Omit<Node<M, F, S, RESP, TYPE_MAPPING>, 'client'> &
+  Required<Pick<Node<M, F, S, RESP, TYPE_MAPPING>, 'client'>>
+);
 
 type PubSubToResubscribe = Record<
-    PubSubType.CHANNELS | PubSubType.PATTERNS,
-    PubSubTypeListeners
+  PUBSUB_TYPE['CHANNELS'] | PUBSUB_TYPE['PATTERNS'],
+  PubSubTypeListeners
 >;
 
 export type OnShardedChannelMovedError = (
-    err: unknown,
-    channel: string,
-    listeners?: ChannelListeners
+  err: unknown,
+  channel: string,
+  listeners?: ChannelListeners
 ) => void;
 
 export default class RedisClusterSlots<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > {
-    static #SLOTS = 16384;
-
-    readonly #options: RedisClusterOptions<M, F, S>;
-    readonly #Client: InstantiableRedisClient<M, F, S>;
-    readonly #emit: EventEmitter['emit'];
-    slots = new Array<Shard<M, F, S>>(RedisClusterSlots.#SLOTS);
-    shards = new Array<Shard<M, F, S>>();
-    masters = new Array<ShardNode<M, F, S>>();
-    replicas = new Array<ShardNode<M, F, S>>();
-    readonly nodeByAddress = new Map<string, MasterNode<M, F, S> | ShardNode<M, F, S>>();
-    pubSubNode?: PubSubNode<M, F, S>;
-
-    #isOpen = false;
-
-    get isOpen() {
-        return this.#isOpen;
+  static #SLOTS = 16384;
+
+  readonly #options;
+  readonly #clientFactory;
+  readonly #emit: EventEmitter['emit'];
+  slots = new Array<Shard<M, F, S, RESP, TYPE_MAPPING>>(RedisClusterSlots.#SLOTS);
+  masters = new Array<MasterNode<M, F, S, RESP, TYPE_MAPPING>>();
+  replicas = new Array<ShardNode<M, F, S, RESP, TYPE_MAPPING>>();
+  readonly nodeByAddress = new Map<string, MasterNode<M, F, S, RESP, TYPE_MAPPING> | ShardNode<M, F, S, RESP, TYPE_MAPPING>>();
+  pubSubNode?: PubSubNode<M, F, S, RESP, TYPE_MAPPING>;
+  clientSideCache?: PooledClientSideCacheProvider;
+
+  #isOpen = false;
+
+  get isOpen() {
+    return this.#isOpen;
+  }
+
+  #validateOptions(options?: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    if (options?.clientSideCache && options?.RESP !== 3) {
+      throw new Error('Client Side Caching is only supported with RESP3');
     }
-
-    constructor(
-        options: RedisClusterOptions<M, F, S>,
-        emit: EventEmitter['emit']
-    ) {
-        this.#options = options;
-        this.#Client = RedisClient.extend(options);
-        this.#emit = emit;
+  }
+
+  constructor(
+    options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING>,
+    emit: EventEmitter['emit']
+  ) {
+    this.#validateOptions(options);
+    this.#options = options;
+
+    if (options?.clientSideCache) {
+      if (options.clientSideCache instanceof PooledClientSideCacheProvider) {
+        this.clientSideCache = options.clientSideCache;
+      } else {
+        this.clientSideCache = new BasicPooledClientSideCache(options.clientSideCache)
+      }
     }
 
-    async connect() {
-        if (this.#isOpen) {
-            throw new Error('Cluster already open');
-        }
+    this.#clientFactory = RedisClient.factory(this.#options);
+    this.#emit = emit;
+  }
 
-        this.#isOpen = true;
-        try {
-            await this.#discoverWithRootNodes();
-        } catch (err) {
-            this.#isOpen = false;
-            throw err;
-        }
+  async connect() {
+    if (this.#isOpen) {
+      throw new Error('Cluster already open');
     }
 
-    async #discoverWithRootNodes() {
-        let start = Math.floor(Math.random() * this.#options.rootNodes.length);
-        for (let i = start; i < this.#options.rootNodes.length; i++) {    
-            if (await this.#discover(this.#options.rootNodes[i])) return;
-        }
-
-        for (let i = 0; i < start; i++) {
-            if (await this.#discover(this.#options.rootNodes[i])) return;
-        }
+    this.#isOpen = true;
+    try {
+      await this.#discoverWithRootNodes();
+    } catch (err) {
+      this.#isOpen = false;
+      throw err;
+    }
+  }
 
-        throw new RootNodesUnavailableError();
+  async #discoverWithRootNodes() {
+    let start = Math.floor(Math.random() * this.#options.rootNodes.length);
+    for (let i = start; i < this.#options.rootNodes.length; i++) {
+      if (!this.#isOpen) throw new Error('Cluster closed');
+      if (await this.#discover(this.#options.rootNodes[i])) return;
     }
 
-    #resetSlots() {
-        this.slots = new Array(RedisClusterSlots.#SLOTS);
-        this.shards = [];
-        this.masters = [];
-        this.replicas = [];
-        this.#randomNodeIterator = undefined;
+    for (let i = 0; i < start; i++) {
+      if (!this.#isOpen) throw new Error('Cluster closed');
+      if (await this.#discover(this.#options.rootNodes[i])) return;
     }
 
-    async #discover(rootNode?: RedisClusterClientOptions) {
-        const addressesInUse = new Set<string>();
+    throw new RootNodesUnavailableError();
+  }
+
+  #resetSlots() {
+    this.slots = new Array(RedisClusterSlots.#SLOTS);
+    this.masters = [];
+    this.replicas = [];
+    this._randomNodeIterator = undefined;
+  }
+
+  async #discover(rootNode: RedisClusterClientOptions) {
+    this.clientSideCache?.clear();
+    this.clientSideCache?.disable();
+    try {
+      const addressesInUse = new Set<string>(),
+        promises: Array<Promise<unknown>> = [],
+        eagerConnect = this.#options.minimizeConnections !== true;
+
+      const shards = await this.#getShards(rootNode);
+      this.#resetSlots(); // Reset slots AFTER shards have been fetched to prevent a race condition
+      for (const { from, to, master, replicas } of shards) {
+        const shard: Shard<M, F, S, RESP, TYPE_MAPPING> = {
+          master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises)
+        };
 
-        try {
-            const shards = await this.#getShards(rootNode),
-                promises: Array<Promise<unknown>> = [],
-                eagerConnect = this.#options.minimizeConnections !== true;
-            this.#resetSlots();
-            for (const { from, to, master, replicas } of shards) {
-                const shard: Shard<M, F, S> = {
-                    master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises)
-                };
-
-                if (this.#options.useReplicas) {
-                    shard.replicas = replicas.map(replica =>
-                        this.#initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises)
-                    );
-                }
-
-                this.shards.push(shard);
-
-                for (let i = from; i <= to; i++) {
-                    this.slots[i] = shard;
-                }
-            }
-
-            if (this.pubSubNode && !addressesInUse.has(this.pubSubNode.address)) {
-                if (types.isPromise(this.pubSubNode.client)) {
-                    promises.push(
-                        this.pubSubNode.client.then(client => client.disconnect())
-                    );
-                    this.pubSubNode = undefined;
-                } else {
-                    promises.push(this.pubSubNode.client.disconnect());
-                    
-                    const channelsListeners = this.pubSubNode.client.getPubSubListeners(PubSubType.CHANNELS),
-                        patternsListeners = this.pubSubNode.client.getPubSubListeners(PubSubType.PATTERNS);
-
-                    if (channelsListeners.size || patternsListeners.size) {
-                        promises.push(
-                            this.#initiatePubSubClient({
-                                [PubSubType.CHANNELS]: channelsListeners,
-                                [PubSubType.PATTERNS]: patternsListeners
-                            })
-                        );
-                    }
-                }
-            }
-
-            for (const [address, node] of this.nodeByAddress.entries()) {
-                if (addressesInUse.has(address)) continue;
-
-                if (node.client) {
-                    promises.push(
-                        this.#execOnNodeClient(node.client, client => client.disconnect())
-                    );
-                }
-
-                const { pubSubClient } = node as MasterNode<M, F, S>;
-                if (pubSubClient) {
-                    promises.push(
-                        this.#execOnNodeClient(pubSubClient, client => client.disconnect())
-                    );
-                }
-
-                this.nodeByAddress.delete(address);
-            }
-
-            await Promise.all(promises);
-
-            return true;
-        } catch (err) {
-            this.#emit('error', err);
-            return false;
+        if (this.#options.useReplicas) {
+          shard.replicas = replicas.map(replica =>
+            this.#initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises)
+          );
         }
-    }
 
-    async #getShards(rootNode?: RedisClusterClientOptions) {
-        const client = new this.#Client(
-            this.#clientOptionsDefaults(rootNode, true)
-        );
+        for (let i = from; i <= to; i++) {
+          this.slots[i] = shard;
+        }
+      }
 
-        client.on('error', err => this.#emit('error', err));
+      if (this.pubSubNode && !addressesInUse.has(this.pubSubNode.address)) {
+        const channelsListeners = this.pubSubNode.client.getPubSubListeners(PUBSUB_TYPE.CHANNELS),
+          patternsListeners = this.pubSubNode.client.getPubSubListeners(PUBSUB_TYPE.PATTERNS);
 
-        await client.connect();
+        this.pubSubNode.client.destroy();
 
-        try {
-            // using `CLUSTER SLOTS` and not `CLUSTER SHARDS` to support older versions
-            return await client.clusterSlots();
-        } finally {
-            await client.disconnect();
+        if (channelsListeners.size || patternsListeners.size) {
+          promises.push(
+            this.#initiatePubSubClient({
+              [PUBSUB_TYPE.CHANNELS]: channelsListeners,
+              [PUBSUB_TYPE.PATTERNS]: patternsListeners
+            })
+          );
         }
-    }
+      }
 
-    #getNodeAddress(address: string): NodeAddress | undefined {
-        switch (typeof this.#options.nodeAddressMap) {
-            case 'object':
-                return this.#options.nodeAddressMap[address];
+      for (const [address, node] of this.nodeByAddress.entries()) {
+        if (addressesInUse.has(address)) continue;
 
-            case 'function':
-                return this.#options.nodeAddressMap(address);
+        if (node.client) {
+          node.client.destroy();
         }
-    }
 
-    #clientOptionsDefaults(
-        options?: RedisClusterClientOptions,
-        disableReconnect?: boolean
-    ): RedisClusterClientOptions | undefined {
-        let result: RedisClusterClientOptions | undefined;
-        if (this.#options.defaults) {
-            let socket;
-            if (this.#options.defaults.socket) {
-                socket = {
-                    ...this.#options.defaults.socket,
-                    ...options?.socket
-                };
-            } else {
-                socket = options?.socket;
-            }
-
-            result = {
-                ...this.#options.defaults,
-                ...options,
-                socket
-            };
-        } else {
-            result = options;
-        }
-        
-        if (disableReconnect) {
-            result ??= {};
-            result.socket ??= {};
-            result.socket.reconnectStrategy = false;
+        const { pubSub } = node as MasterNode<M, F, S, RESP, TYPE_MAPPING>;
+        if (pubSub) {
+          pubSub.client.destroy();
         }
 
-        return result;
-    }
-
-    #initiateSlotNode(
-        { id, ip, port }: ClusterSlotsNode,
-        readonly: boolean,
-        eagerConnent: boolean,
-        addressesInUse: Set<string>,
-        promises: Array<Promise<unknown>>
-    ) {
-        const address = `${ip}:${port}`;
-        addressesInUse.add(address);
-
-        let node = this.nodeByAddress.get(address);
-        if (!node) {
-            node = {
-                id,
-                host: ip,
-                port,
-                address,
-                readonly,
-                client: undefined
-            };
-            
-            if (eagerConnent) {
-                promises.push(this.#createNodeClient(node));
-            }
-            
-            this.nodeByAddress.set(address, node);
-        }
+        this.nodeByAddress.delete(address);
+      }
 
-        (readonly ? this.replicas : this.masters).push(node);
+      await Promise.all(promises);
+      this.clientSideCache?.enable();
 
-        return node;
+      return true;
+    } catch (err) {
+      this.#emit('error', err);
+      return false;
     }
-
-    async #createClient(
-        node: ShardNode<M, F, S>,
-        readonly = node.readonly
-    ) {
-        const client = new this.#Client(
-            this.#clientOptionsDefaults({
-                socket: this.#getNodeAddress(node.address) ?? {
-                    host: node.host,
-                    port: node.port
-                },
-                readonly
-            })
-        );
-        client.on('error', err => this.#emit('error', err));
-
-        await client.connect();
-
-        return client;
+  }
+
+  async #getShards(rootNode: RedisClusterClientOptions) {
+    const options = this.#clientOptionsDefaults(rootNode)!;
+    options.socket ??= {};
+    options.socket.reconnectStrategy = false;
+    options.RESP = this.#options.RESP;
+    options.commandOptions = undefined;
+
+    // TODO: find a way to avoid type casting
+    const client = await this.#clientFactory(options as RedisClientOptions<M, F, S, RESP, {}>)
+      .on('error', err => this.#emit('error', err))
+      .connect();
+
+    try {
+      // switch to `CLUSTER SHARDS` when Redis 7.0 will be the minimum supported version
+      return await client.clusterSlots();
+    } finally {
+      client.destroy();
     }
+  }
 
-    #createNodeClient(node: ShardNode<M, F, S>) {
-        const promise = this.#createClient(node)
-            .then(client => {
-                node.client = client;
-                return client;
-            })
-            .catch(err => {
-                node.client = undefined;
-                throw err;
-            });
-        node.client = promise;
-        return promise;
-    }
+  #getNodeAddress(address: string): NodeAddress | undefined {
+    switch (typeof this.#options.nodeAddressMap) {
+      case 'object':
+        return this.#options.nodeAddressMap[address];
 
-    nodeClient(node: ShardNode<M, F, S>) {
-        return node.client ?? this.#createNodeClient(node);
+      case 'function':
+        return this.#options.nodeAddressMap(address);
     }
-
-    #runningRediscoverPromise?: Promise<void>;
-
-    async rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
-        this.#runningRediscoverPromise ??= this.#rediscover(startWith)
-            .finally(() => this.#runningRediscoverPromise = undefined);
-        return this.#runningRediscoverPromise;
+  }
+
+  #clientOptionsDefaults(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    if (!this.#options.defaults) return options;
+
+    let socket;
+    if (this.#options.defaults.socket) {
+      socket = {
+        ...this.#options.defaults.socket,
+        ...options?.socket
+      };
+    } else {
+      socket = options?.socket;
     }
 
-    async #rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
-        if (await this.#discover(startWith.options)) return;
-
-        return this.#discoverWithRootNodes();
+    return {
+      ...this.#options.defaults,
+      ...options,
+      socket: socket as RedisSocketOptions
+    };
+  }
+
+  #initiateSlotNode(
+    shard: NodeAddress & { id: string; },
+    readonly: boolean,
+    eagerConnent: boolean,
+    addressesInUse: Set<string>,
+    promises: Array<Promise<unknown>>
+  ) {
+    const address = `${shard.host}:${shard.port}`;
+
+    let node = this.nodeByAddress.get(address);
+    if (!node) {
+      node = {
+        ...shard,
+        address,
+        readonly,
+        client: undefined,
+        connectPromise: undefined
+      };
+
+      if (eagerConnent) {
+        promises.push(this.#createNodeClient(node));
+      }
+
+      this.nodeByAddress.set(address, node);
     }
 
-    quit(): Promise<void> {
-        return this.#destroy(client => client.quit());
+    if (!addressesInUse.has(address)) {
+      addressesInUse.add(address);
+      (readonly ? this.replicas : this.masters).push(node);
     }
 
-    disconnect(): Promise<void> {
-        return this.#destroy(client => client.disconnect());
+    return node;
+  }
+
+  #createClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly = node.readonly) {
+    return this.#clientFactory(
+      this.#clientOptionsDefaults({
+        clientSideCache: this.clientSideCache,
+        RESP: this.#options.RESP,
+        socket: this.#getNodeAddress(node.address) ?? {
+          host: node.host,
+          port: node.port
+        },
+        readonly
+      })
+    ).on('error', err => console.error(err));
+  }
+
+  #createNodeClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly?: boolean) {
+    const client = node.client = this.#createClient(node, readonly);
+    return node.connectPromise = client.connect()
+      .finally(() => node.connectPromise = undefined);
+  }
+
+  nodeClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>) {
+    return (
+      node.connectPromise ?? // if the node is connecting
+      node.client ?? // if the node is connected
+      this.#createNodeClient(node) // if the not is disconnected
+    );
+  }
+
+  #runningRediscoverPromise?: Promise<void>;
+
+  async rediscover(startWith: RedisClientType<M, F, S, RESP>): Promise<void> {
+    this.#runningRediscoverPromise ??= this.#rediscover(startWith)
+      .finally(() => this.#runningRediscoverPromise = undefined);
+    return this.#runningRediscoverPromise;
+  }
+
+  async #rediscover(startWith: RedisClientType<M, F, S, RESP>): Promise<void> {
+    if (await this.#discover(startWith.options!)) return;
+
+    return this.#discoverWithRootNodes();
+  }
+
+  /**
+   * @deprecated Use `close` instead.
+   */
+  quit(): Promise<void> {
+    return this.#destroy(client => client.quit());
+  }
+
+  /**
+   * @deprecated Use `destroy` instead.
+   */
+  disconnect(): Promise<void> {
+    return this.#destroy(client => client.disconnect());
+  }
+
+  close() {
+    return this.#destroy(client => client.close());
+  }
+
+  destroy() {
+    this.#isOpen = false;
+
+    for (const client of this.#clients()) {
+      client.destroy();
     }
 
-    async #destroy(fn: (client: RedisClientType<M, F, S>) => Promise<unknown>): Promise<void> {
-        this.#isOpen = false;
-
-        const promises = [];
-        for (const { master, replicas } of this.shards) {
-            if (master.client) {
-                promises.push(
-                    this.#execOnNodeClient(master.client, fn)
-                );
-            }
-
-            if (master.pubSubClient) {
-                promises.push(
-                    this.#execOnNodeClient(master.pubSubClient, fn)
-                );
-            }
-
-            if (replicas) {
-                for (const { client } of replicas) {
-                    if (client) {
-                        promises.push(
-                            this.#execOnNodeClient(client, fn)
-                        );
-                    }
-                }
-            }
-        }
+    if (this.pubSubNode) {
+      this.pubSubNode.client.destroy();
+      this.pubSubNode = undefined;
+    }
 
-        if (this.pubSubNode) {
-            promises.push(this.#execOnNodeClient(this.pubSubNode.client, fn));
-            this.pubSubNode = undefined;
-        }
+    this.#resetSlots();
+    this.nodeByAddress.clear();
+  }
 
-        this.#resetSlots();
-        this.nodeByAddress.clear();
+  *#clients() {
+    for (const master of this.masters) {
+      if (master.client) {
+        yield master.client;
+      }
 
-        await Promise.allSettled(promises);
+      if (master.pubSub) {
+        yield master.pubSub.client;
+      }
     }
 
-    #execOnNodeClient(
-        client: ClientOrPromise<M, F, S>,
-        fn: (client: RedisClientType<M, F, S>) => Promise<unknown>
-    ) {
-        return types.isPromise(client) ?
-            client.then(fn) :
-            fn(client);
+    for (const replica of this.replicas) {
+      if (replica.client) {
+        yield replica.client;
+      }
     }
+  }
 
-    getClient(
-        firstKey: RedisCommandArgument | undefined,
-        isReadonly: boolean | undefined
-    ): ClientOrPromise<M, F, S> {
-        if (!firstKey) {
-            return this.nodeClient(this.getRandomNode());
-        }
+  async #destroy(fn: (client: RedisClientType<M, F, S, RESP>) => Promise<unknown>): Promise<void> {
+    this.#isOpen = false;
 
-        const slotNumber = calculateSlot(firstKey);
-        if (!isReadonly) {
-            return this.nodeClient(this.slots[slotNumber].master);
-        }
+    const promises = [];
+    for (const client of this.#clients()) {
+      promises.push(fn(client));
+    }
 
-        return this.nodeClient(this.getSlotRandomNode(slotNumber));
+    if (this.pubSubNode) {
+      promises.push(fn(this.pubSubNode.client));
+      this.pubSubNode = undefined;
     }
 
-    *#iterateAllNodes() {
-        let i = Math.floor(Math.random() * (this.masters.length + this.replicas.length));
-        if (i < this.masters.length) {
-            do {
-                yield this.masters[i];
-            } while (++i < this.masters.length);
-
-            for (const replica of this.replicas) {
-                yield replica;
-            }
-        } else {
-            i -= this.masters.length;
-            do {
-                yield this.replicas[i];
-            } while (++i < this.replicas.length);
-        }
+    this.#resetSlots();
+    this.nodeByAddress.clear();
 
-        while (true) {
-            for (const master of this.masters) {
-                yield master;
-            }
+    await Promise.allSettled(promises);
+  }
 
-            for (const replica of this.replicas) {
-                yield replica;
-            }
-        }
+  getClient(
+    firstKey: RedisArgument | undefined,
+    isReadonly: boolean | undefined
+  ) {
+    if (!firstKey) {
+      return this.nodeClient(this.getRandomNode());
     }
 
-    #randomNodeIterator?: IterableIterator<ShardNode<M, F, S>>;
-
-    getRandomNode() {
-        this.#randomNodeIterator ??= this.#iterateAllNodes();
-        return this.#randomNodeIterator.next().value as ShardNode<M, F, S>;
+    const slotNumber = calculateSlot(firstKey);
+    if (!isReadonly) {
+      return this.nodeClient(this.slots[slotNumber].master);
     }
 
-    *#slotNodesIterator(slot: ShardWithReplicas<M, F, S>) {
-        let i = Math.floor(Math.random() * (1 + slot.replicas.length));
-        if (i < slot.replicas.length) {
-            do {
-                yield slot.replicas[i];
-            } while (++i < slot.replicas.length);
-        }
-
-        while (true) {
-            yield slot.master;
-            
-            for (const replica of slot.replicas) {
-                yield replica;
-            }
-        }
+    return this.nodeClient(this.getSlotRandomNode(slotNumber));
+  }
+
+  *#iterateAllNodes() {
+    let i = Math.floor(Math.random() * (this.masters.length + this.replicas.length));
+    if (i < this.masters.length) {
+      do {
+        yield this.masters[i];
+      } while (++i < this.masters.length);
+
+      for (const replica of this.replicas) {
+        yield replica;
+      }
+    } else {
+      i -= this.masters.length;
+      do {
+        yield this.replicas[i];
+      } while (++i < this.replicas.length);
     }
 
-    getSlotRandomNode(slotNumber: number) {
-        const slot = this.slots[slotNumber];
-        if (!slot.replicas?.length) {
-            return slot.master;
-        }
+    while (true) {
+      for (const master of this.masters) {
+        yield master;
+      }
 
-        slot.nodesIterator ??= this.#slotNodesIterator(slot as ShardWithReplicas<M, F, S>);
-        return slot.nodesIterator.next().value as ShardNode<M, F, S>;
+      for (const replica of this.replicas) {
+        yield replica;
+      }
     }
+  }
 
-    getMasterByAddress(address: string) {
-        const master = this.nodeByAddress.get(address);
-        if (!master) return;
-
-        return this.nodeClient(master);
-    }
+  _randomNodeIterator?: IterableIterator<ShardNode<M, F, S, RESP, TYPE_MAPPING>>;
 
-    getPubSubClient() {
-        return this.pubSubNode ?
-            this.pubSubNode.client :
-            this.#initiatePubSubClient();
-    }
+  getRandomNode() {
+    this._randomNodeIterator ??= this.#iterateAllNodes();
+    return this._randomNodeIterator.next().value as ShardNode<M, F, S, RESP, TYPE_MAPPING>;
+  }
 
-    async #initiatePubSubClient(toResubscribe?: PubSubToResubscribe) {
-        const index = Math.floor(Math.random() * (this.masters.length + this.replicas.length)),
-            node = index < this.masters.length ?
-                this.masters[index] :
-                this.replicas[index - this.masters.length];
-    
-        this.pubSubNode = {
-            address: node.address,
-            client: this.#createClient(node, true)
-                .then(async client => {
-                    if (toResubscribe) {
-                        await Promise.all([
-                            client.extendPubSubListeners(PubSubType.CHANNELS, toResubscribe[PubSubType.CHANNELS]),
-                            client.extendPubSubListeners(PubSubType.PATTERNS, toResubscribe[PubSubType.PATTERNS])
-                        ]);
-                    }
-                    
-                    this.pubSubNode!.client = client;
-                    return client;
-                })
-                .catch(err => {
-                    this.pubSubNode = undefined;
-                    throw err;
-                })
-        };
-        
-        return this.pubSubNode.client as Promise<RedisClientType<M, F, S>>;
+  *#slotNodesIterator(slot: ShardWithReplicas<M, F, S, RESP, TYPE_MAPPING>) {
+    let i = Math.floor(Math.random() * (1 + slot.replicas.length));
+    if (i < slot.replicas.length) {
+      do {
+        yield slot.replicas[i];
+      } while (++i < slot.replicas.length);
     }
 
-    async executeUnsubscribeCommand(
-        unsubscribe: (client: RedisClientType<M, F, S>) => Promise<void>
-    ): Promise<void> {
-        const client = await this.getPubSubClient();
-        await unsubscribe(client);
+    while (true) {
+      yield slot.master;
 
-        if (!client.isPubSubActive && client.isOpen) {
-            await client.disconnect();
-            this.pubSubNode = undefined;
-        }
+      for (const replica of slot.replicas) {
+        yield replica;
+      }
     }
+  }
 
-    getShardedPubSubClient(channel: string) {
-        const { master } = this.slots[calculateSlot(channel)];
-        return master.pubSubClient ?? this.#initiateShardedPubSubClient(master);
+  getSlotRandomNode(slotNumber: number) {
+    const slot = this.slots[slotNumber];
+    if (!slot.replicas?.length) {
+      return slot.master;
     }
 
-    #initiateShardedPubSubClient(master: MasterNode<M, F, S>) {
-        const promise = this.#createClient(master, true)
-            .then(client => {
-                client.on('server-sunsubscribe', async (channel, listeners) => {
-                    try {
-                        await this.rediscover(client);
-                        const redirectTo = await this.getShardedPubSubClient(channel);
-                        redirectTo.extendPubSubChannelListeners(
-                            PubSubType.SHARDED,
-                            channel,
-                            listeners
-                        );
-                    } catch (err) {
-                        this.#emit('sharded-shannel-moved-error', err, channel, listeners);
-                    }
-                });
-
-                master.pubSubClient = client;
-                return client;
-            })
-            .catch(err => {
-                master.pubSubClient = undefined;
-                throw err;
-            });
+    slot.nodesIterator ??= this.#slotNodesIterator(slot as ShardWithReplicas<M, F, S, RESP, TYPE_MAPPING>);
+    return slot.nodesIterator.next().value as ShardNode<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  getMasterByAddress(address: string) {
+    const master = this.nodeByAddress.get(address);
+    if (!master) return;
+
+    return this.nodeClient(master);
+  }
+
+  getPubSubClient() {
+    if (!this.pubSubNode) return this.#initiatePubSubClient();
+
+    return this.pubSubNode.connectPromise ?? this.pubSubNode.client;
+  }
+
+  async #initiatePubSubClient(toResubscribe?: PubSubToResubscribe) {
+    const index = Math.floor(Math.random() * (this.masters.length + this.replicas.length)),
+      node = index < this.masters.length ?
+        this.masters[index] :
+        this.replicas[index - this.masters.length],
+        client = this.#createClient(node, false);
+      
+    this.pubSubNode = {
+      address: node.address,
+      client,
+      connectPromise: client.connect()
+        .then(async client => {
+          if (toResubscribe) {
+            await Promise.all([
+              client.extendPubSubListeners(PUBSUB_TYPE.CHANNELS, toResubscribe[PUBSUB_TYPE.CHANNELS]),
+              client.extendPubSubListeners(PUBSUB_TYPE.PATTERNS, toResubscribe[PUBSUB_TYPE.PATTERNS])
+            ]);
+          }
+
+          this.pubSubNode!.connectPromise = undefined;
+          return client;
+        })
+        .catch(err => {
+          this.pubSubNode = undefined;
+          throw err;
+        })
+    };
+
+    return this.pubSubNode.connectPromise!;
+  }
+
+  async executeUnsubscribeCommand(
+    unsubscribe: (client: RedisClientType<M, F, S, RESP>) => Promise<void>
+  ): Promise<void> {
+    const client = await this.getPubSubClient();
+    await unsubscribe(client);
+
+    if (!client.isPubSubActive) {
+      client.destroy();
+      this.pubSubNode = undefined;
+    }
+  }
 
-        master.pubSubClient = promise;
+  getShardedPubSubClient(channel: string) {
+    const { master } = this.slots[calculateSlot(channel)];
+    if (!master.pubSub) return this.#initiateShardedPubSubClient(master);
+    return master.pubSub.connectPromise ?? master.pubSub.client;
+  }
 
-        return promise;
-    }
+  async #initiateShardedPubSubClient(master: MasterNode<M, F, S, RESP, TYPE_MAPPING>) {
+    const client = this.#createClient(master, false)
+      .on('server-sunsubscribe', async (channel, listeners) => {
+        try {
+          await this.rediscover(client);
+          const redirectTo = await this.getShardedPubSubClient(channel);
+          await redirectTo.extendPubSubChannelListeners(
+            PUBSUB_TYPE.SHARDED,
+            channel,
+            listeners
+          );
+        } catch (err) {
+          this.#emit('sharded-shannel-moved-error', err, channel, listeners);
+        }
+      });
+
+    master.pubSub = {
+      client,
+      connectPromise: client.connect()
+        .then(client => {
+          master.pubSub!.connectPromise = undefined;
+          return client;
+        })
+        .catch(err => {
+          master.pubSub = undefined;
+          throw err;
+        })
+    };
+
+    return master.pubSub.connectPromise!;
+  }
+
+  async executeShardedUnsubscribeCommand(
+    channel: string,
+    unsubscribe: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => Promise<void>
+  ) {
+    const { master } = this.slots[calculateSlot(channel)];
+    if (!master.pubSub) return;
 
-    async executeShardedUnsubscribeCommand(
-        channel: string,
-        unsubscribe: (client: RedisClientType<M, F, S>) => Promise<void>
-    ): Promise<void> {
-        const { master } = this.slots[calculateSlot(channel)];
-        if (!master.pubSubClient) return Promise.resolve();
+    const client = master.pubSub.connectPromise ?
+      await master.pubSub.connectPromise :
+      master.pubSub.client;
 
-        const client = await master.pubSubClient;
-        await unsubscribe(client);
+    await unsubscribe(client);
 
-        if (!client.isPubSubActive && client.isOpen) {
-            await client.disconnect();
-            master.pubSubClient = undefined;
-        }
+    if (!client.isPubSubActive) {
+      client.destroy();
+      master.pubSub = undefined;
     }
+  }
 }
diff --git a/packages/client/lib/cluster/commands.ts b/packages/client/lib/cluster/commands.ts
deleted file mode 100644
index 9027c5c0b5e..00000000000
--- a/packages/client/lib/cluster/commands.ts
+++ /dev/null
@@ -1,670 +0,0 @@
-
-import * as APPEND from '../commands/APPEND';
-import * as BITCOUNT from '../commands/BITCOUNT';
-import * as BITFIELD_RO from '../commands/BITFIELD_RO';
-import * as BITFIELD from '../commands/BITFIELD';
-import * as BITOP from '../commands/BITOP';
-import * as BITPOS from '../commands/BITPOS';
-import * as BLMOVE from '../commands/BLMOVE';
-import * as BLMPOP from '../commands/BLMPOP';
-import * as BLPOP from '../commands/BLPOP';
-import * as BRPOP from '../commands/BRPOP';
-import * as BRPOPLPUSH from '../commands/BRPOPLPUSH';
-import * as BZMPOP from '../commands/BZMPOP';
-import * as BZPOPMAX from '../commands/BZPOPMAX';
-import * as BZPOPMIN from '../commands/BZPOPMIN';
-import * as COPY from '../commands/COPY';
-import * as DECR from '../commands/DECR';
-import * as DECRBY from '../commands/DECRBY';
-import * as DEL from '../commands/DEL';
-import * as DUMP from '../commands/DUMP';
-import * as EVAL_RO from '../commands/EVAL_RO';
-import * as EVAL from '../commands/EVAL';
-import * as EVALSHA_RO from '../commands/EVALSHA_RO';
-import * as EVALSHA from '../commands/EVALSHA';
-import * as EXISTS from '../commands/EXISTS';
-import * as EXPIRE from '../commands/EXPIRE';
-import * as EXPIREAT from '../commands/EXPIREAT';
-import * as EXPIRETIME from '../commands/EXPIRETIME';
-import * as FCALL_RO from '../commands/FCALL_RO';
-import * as FCALL from '../commands/FCALL';
-import * as GEOADD from '../commands/GEOADD';
-import * as GEODIST from '../commands/GEODIST';
-import * as GEOHASH from '../commands/GEOHASH';
-import * as GEOPOS from '../commands/GEOPOS';
-import * as GEORADIUS_RO_WITH from '../commands/GEORADIUS_RO_WITH';
-import * as GEORADIUS_RO from '../commands/GEORADIUS_RO';
-import * as GEORADIUS_WITH from '../commands/GEORADIUS_WITH';
-import * as GEORADIUS from '../commands/GEORADIUS';
-import * as GEORADIUSBYMEMBER_RO_WITH from '../commands/GEORADIUSBYMEMBER_RO_WITH';
-import * as GEORADIUSBYMEMBER_RO from '../commands/GEORADIUSBYMEMBER_RO';
-import * as GEORADIUSBYMEMBER_WITH from '../commands/GEORADIUSBYMEMBER_WITH';
-import * as GEORADIUSBYMEMBER from '../commands/GEORADIUSBYMEMBER';
-import * as GEORADIUSBYMEMBERSTORE from '../commands/GEORADIUSBYMEMBERSTORE';
-import * as GEORADIUSSTORE from '../commands/GEORADIUSSTORE';
-import * as GEOSEARCH_WITH from '../commands/GEOSEARCH_WITH';
-import * as GEOSEARCH from '../commands/GEOSEARCH';
-import * as GEOSEARCHSTORE from '../commands/GEOSEARCHSTORE';
-import * as GET from '../commands/GET';
-import * as GETBIT from '../commands/GETBIT';
-import * as GETDEL from '../commands/GETDEL';
-import * as GETEX from '../commands/GETEX';
-import * as GETRANGE from '../commands/GETRANGE';
-import * as GETSET from '../commands/GETSET';
-import * as HDEL from '../commands/HDEL';
-import * as HEXISTS from '../commands/HEXISTS';
-import * as HEXPIRE from '../commands/HEXPIRE';
-import * as HEXPIREAT from '../commands/HEXPIREAT';
-import * as HEXPIRETIME from '../commands/HEXPIRETIME';
-import * as HGET from '../commands/HGET';
-import * as HGETALL from '../commands/HGETALL';
-import * as HINCRBY from '../commands/HINCRBY';
-import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT';
-import * as HKEYS from '../commands/HKEYS';
-import * as HLEN from '../commands/HLEN';
-import * as HMGET from '../commands/HMGET';
-import * as HPERSIST from '../commands/HPERSIST';
-import * as HPEXPIRE from '../commands/HPEXPIRE';
-import * as HPEXPIREAT from '../commands/HPEXPIREAT';
-import * as HPEXPIRETIME from '../commands/HPEXPIRETIME';
-import * as HPTTL from '../commands/HPTTL';
-import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
-import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
-import * as HRANDFIELD from '../commands/HRANDFIELD';
-import * as HSCAN from '../commands/HSCAN';
-import * as HSCAN_NOVALUES from '../commands/HSCAN_NOVALUES';
-import * as HSET from '../commands/HSET';
-import * as HSETNX from '../commands/HSETNX';
-import * as HSTRLEN from '../commands/HSTRLEN';
-import * as HTTL from '../commands/HTTL';
-import * as HVALS from '../commands/HVALS';
-import * as INCR from '../commands/INCR';
-import * as INCRBY from '../commands/INCRBY';
-import * as INCRBYFLOAT from '../commands/INCRBYFLOAT';
-import * as LCS_IDX_WITHMATCHLEN from '../commands/LCS_IDX_WITHMATCHLEN';
-import * as LCS_IDX from '../commands/LCS_IDX';
-import * as LCS_LEN from '../commands/LCS_LEN';
-import * as LCS from '../commands/LCS';
-import * as LINDEX from '../commands/LINDEX';
-import * as LINSERT from '../commands/LINSERT';
-import * as LLEN from '../commands/LLEN';
-import * as LMOVE from '../commands/LMOVE';
-import * as LMPOP from '../commands/LMPOP';
-import * as LPOP_COUNT from '../commands/LPOP_COUNT';
-import * as LPOP from '../commands/LPOP';
-import * as LPOS_COUNT from '../commands/LPOS_COUNT';
-import * as LPOS from '../commands/LPOS';
-import * as LPUSH from '../commands/LPUSH';
-import * as LPUSHX from '../commands/LPUSHX';
-import * as LRANGE from '../commands/LRANGE';
-import * as LREM from '../commands/LREM';
-import * as LSET from '../commands/LSET';
-import * as LTRIM from '../commands/LTRIM';
-import * as MGET from '../commands/MGET';
-import * as MIGRATE from '../commands/MIGRATE';
-import * as MSET from '../commands/MSET';
-import * as MSETNX from '../commands/MSETNX';
-import * as OBJECT_ENCODING from '../commands/OBJECT_ENCODING';
-import * as OBJECT_FREQ from '../commands/OBJECT_FREQ';
-import * as OBJECT_IDLETIME from '../commands/OBJECT_IDLETIME';
-import * as OBJECT_REFCOUNT from '../commands/OBJECT_REFCOUNT';
-import * as PERSIST from '../commands/PERSIST';
-import * as PEXPIRE from '../commands/PEXPIRE';
-import * as PEXPIREAT from '../commands/PEXPIREAT';
-import * as PEXPIRETIME from '../commands/PEXPIRETIME';
-import * as PFADD from '../commands/PFADD';
-import * as PFCOUNT from '../commands/PFCOUNT';
-import * as PFMERGE from '../commands/PFMERGE';
-import * as PSETEX from '../commands/PSETEX';
-import * as PTTL from '../commands/PTTL';
-import * as PUBLISH from '../commands/PUBLISH';
-import * as RENAME from '../commands/RENAME';
-import * as RENAMENX from '../commands/RENAMENX';
-import * as RESTORE from '../commands/RESTORE';
-import * as RPOP_COUNT from '../commands/RPOP_COUNT';
-import * as RPOP from '../commands/RPOP';
-import * as RPOPLPUSH from '../commands/RPOPLPUSH';
-import * as RPUSH from '../commands/RPUSH';
-import * as RPUSHX from '../commands/RPUSHX';
-import * as SADD from '../commands/SADD';
-import * as SCARD from '../commands/SCARD';
-import * as SDIFF from '../commands/SDIFF';
-import * as SDIFFSTORE from '../commands/SDIFFSTORE';
-import * as SET from '../commands/SET';
-import * as SETBIT from '../commands/SETBIT';
-import * as SETEX from '../commands/SETEX';
-import * as SETNX from '../commands/SETNX';
-import * as SETRANGE from '../commands/SETRANGE';
-import * as SINTER from '../commands/SINTER';
-import * as SINTERCARD from '../commands/SINTERCARD';
-import * as SINTERSTORE from '../commands/SINTERSTORE';
-import * as SISMEMBER from '../commands/SISMEMBER';
-import * as SMEMBERS from '../commands/SMEMBERS';
-import * as SMISMEMBER from '../commands/SMISMEMBER';
-import * as SMOVE from '../commands/SMOVE';
-import * as SORT_RO from '../commands/SORT_RO';
-import * as SORT_STORE from '../commands/SORT_STORE';
-import * as SORT from '../commands/SORT';
-import * as SPOP from '../commands/SPOP';
-import * as SPUBLISH from '../commands/SPUBLISH';
-import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
-import * as SRANDMEMBER from '../commands/SRANDMEMBER';
-import * as SREM from '../commands/SREM';
-import * as SSCAN from '../commands/SSCAN';
-import * as STRLEN from '../commands/STRLEN';
-import * as SUNION from '../commands/SUNION';
-import * as SUNIONSTORE from '../commands/SUNIONSTORE';
-import * as TOUCH from '../commands/TOUCH';
-import * as TTL from '../commands/TTL';
-import * as TYPE from '../commands/TYPE';
-import * as UNLINK from '../commands/UNLINK';
-import * as WATCH from '../commands/WATCH';
-import * as XACK from '../commands/XACK';
-import * as XADD from '../commands/XADD';
-import * as XAUTOCLAIM_JUSTID from '../commands/XAUTOCLAIM_JUSTID';
-import * as XAUTOCLAIM from '../commands/XAUTOCLAIM';
-import * as XCLAIM_JUSTID from '../commands/XCLAIM_JUSTID';
-import * as XCLAIM from '../commands/XCLAIM';
-import * as XDEL from '../commands/XDEL';
-import * as XGROUP_CREATE from '../commands/XGROUP_CREATE';
-import * as XGROUP_CREATECONSUMER from '../commands/XGROUP_CREATECONSUMER';
-import * as XGROUP_DELCONSUMER from '../commands/XGROUP_DELCONSUMER';
-import * as XGROUP_DESTROY from '../commands/XGROUP_DESTROY';
-import * as XGROUP_SETID from '../commands/XGROUP_SETID';
-import * as XINFO_CONSUMERS from '../commands/XINFO_CONSUMERS';
-import * as XINFO_GROUPS from '../commands/XINFO_GROUPS';
-import * as XINFO_STREAM from '../commands/XINFO_STREAM';
-import * as XLEN from '../commands/XLEN';
-import * as XPENDING_RANGE from '../commands/XPENDING_RANGE';
-import * as XPENDING from '../commands/XPENDING';
-import * as XRANGE from '../commands/XRANGE';
-import * as XREAD from '../commands/XREAD';
-import * as XREADGROUP from '../commands/XREADGROUP';
-import * as XREVRANGE from '../commands/XREVRANGE';
-import * as XSETID from '../commands/XSETID';
-import * as XTRIM from '../commands/XTRIM';
-import * as ZADD from '../commands/ZADD';
-import * as ZCARD from '../commands/ZCARD';
-import * as ZCOUNT from '../commands/ZCOUNT';
-import * as ZDIFF_WITHSCORES from '../commands/ZDIFF_WITHSCORES';
-import * as ZDIFF from '../commands/ZDIFF';
-import * as ZDIFFSTORE from '../commands/ZDIFFSTORE';
-import * as ZINCRBY from '../commands/ZINCRBY';
-import * as ZINTER_WITHSCORES from '../commands/ZINTER_WITHSCORES';
-import * as ZINTER from '../commands/ZINTER';
-import * as ZINTERCARD from '../commands/ZINTERCARD';
-import * as ZINTERSTORE from '../commands/ZINTERSTORE';
-import * as ZLEXCOUNT from '../commands/ZLEXCOUNT';
-import * as ZMPOP from '../commands/ZMPOP';
-import * as ZMSCORE from '../commands/ZMSCORE';
-import * as ZPOPMAX_COUNT from '../commands/ZPOPMAX_COUNT';
-import * as ZPOPMAX from '../commands/ZPOPMAX';
-import * as ZPOPMIN_COUNT from '../commands/ZPOPMIN_COUNT';
-import * as ZPOPMIN from '../commands/ZPOPMIN';
-import * as ZRANDMEMBER_COUNT_WITHSCORES from '../commands/ZRANDMEMBER_COUNT_WITHSCORES';
-import * as ZRANDMEMBER_COUNT from '../commands/ZRANDMEMBER_COUNT';
-import * as ZRANDMEMBER from '../commands/ZRANDMEMBER';
-import * as ZRANGE_WITHSCORES from '../commands/ZRANGE_WITHSCORES';
-import * as ZRANGE from '../commands/ZRANGE';
-import * as ZRANGEBYLEX from '../commands/ZRANGEBYLEX';
-import * as ZRANGEBYSCORE_WITHSCORES from '../commands/ZRANGEBYSCORE_WITHSCORES';
-import * as ZRANGEBYSCORE from '../commands/ZRANGEBYSCORE';
-import * as ZRANGESTORE from '../commands/ZRANGESTORE';
-import * as ZRANK from '../commands/ZRANK';
-import * as ZREM from '../commands/ZREM';
-import * as ZREMRANGEBYLEX from '../commands/ZREMRANGEBYLEX';
-import * as ZREMRANGEBYRANK from '../commands/ZREMRANGEBYRANK';
-import * as ZREMRANGEBYSCORE from '../commands/ZREMRANGEBYSCORE';
-import * as ZREVRANK from '../commands/ZREVRANK';
-import * as ZSCAN from '../commands/ZSCAN';
-import * as ZSCORE from '../commands/ZSCORE';
-import * as ZUNION_WITHSCORES from '../commands/ZUNION_WITHSCORES';
-import * as ZUNION from '../commands/ZUNION';
-import * as ZUNIONSTORE from '../commands/ZUNIONSTORE';
-
-export default {
-    APPEND,
-    append: APPEND,
-    BITCOUNT,
-    bitCount: BITCOUNT,
-    BITFIELD_RO,
-    bitFieldRo: BITFIELD_RO,
-    BITFIELD,
-    bitField: BITFIELD,
-    BITOP,
-    bitOp: BITOP,
-    BITPOS,
-    bitPos: BITPOS,
-    BLMOVE,
-    blMove: BLMOVE,
-    BLMPOP,
-    blmPop: BLMPOP,
-    BLPOP,
-    blPop: BLPOP,
-    BRPOP,
-    brPop: BRPOP,
-    BRPOPLPUSH,
-    brPopLPush: BRPOPLPUSH,
-    BZMPOP,
-    bzmPop: BZMPOP,
-    BZPOPMAX,
-    bzPopMax: BZPOPMAX,
-    BZPOPMIN,
-    bzPopMin: BZPOPMIN,
-    COPY,
-    copy: COPY,
-    DECR,
-    decr: DECR,
-    DECRBY,
-    decrBy: DECRBY,
-    DEL,
-    del: DEL,
-    DUMP,
-    dump: DUMP,
-    EVAL_RO,
-    evalRo: EVAL_RO,
-    EVAL,
-    eval: EVAL,
-    EVALSHA,
-    evalSha: EVALSHA,
-    EVALSHA_RO,
-    evalShaRo: EVALSHA_RO,
-    EXISTS,
-    exists: EXISTS,
-    EXPIRE,
-    expire: EXPIRE,
-    EXPIREAT,
-    expireAt: EXPIREAT,
-    EXPIRETIME,
-    expireTime: EXPIRETIME,
-    FCALL_RO,
-    fCallRo: FCALL_RO,
-    FCALL,
-    fCall: FCALL,
-    GEOADD,
-    geoAdd: GEOADD,
-    GEODIST,
-    geoDist: GEODIST,
-    GEOHASH,
-    geoHash: GEOHASH,
-    GEOPOS,
-    geoPos: GEOPOS,
-    GEORADIUS_RO_WITH,
-    geoRadiusRoWith: GEORADIUS_RO_WITH,
-    GEORADIUS_RO,
-    geoRadiusRo: GEORADIUS_RO,
-    GEORADIUS_WITH,
-    geoRadiusWith: GEORADIUS_WITH,
-    GEORADIUS,
-    geoRadius: GEORADIUS,
-    GEORADIUSBYMEMBER_RO_WITH,
-    geoRadiusByMemberRoWith: GEORADIUSBYMEMBER_RO_WITH,
-    GEORADIUSBYMEMBER_RO,
-    geoRadiusByMemberRo: GEORADIUSBYMEMBER_RO,
-    GEORADIUSBYMEMBER_WITH,
-    geoRadiusByMemberWith: GEORADIUSBYMEMBER_WITH,
-    GEORADIUSBYMEMBER,
-    geoRadiusByMember: GEORADIUSBYMEMBER,
-    GEORADIUSBYMEMBERSTORE,
-    geoRadiusByMemberStore: GEORADIUSBYMEMBERSTORE,
-    GEORADIUSSTORE,
-    geoRadiusStore: GEORADIUSSTORE,
-    GEOSEARCH_WITH,
-    geoSearchWith: GEOSEARCH_WITH,
-    GEOSEARCH,
-    geoSearch: GEOSEARCH,
-    GEOSEARCHSTORE,
-    geoSearchStore: GEOSEARCHSTORE,
-    GET,
-    get: GET,
-    GETBIT,
-    getBit: GETBIT,
-    GETDEL,
-    getDel: GETDEL,
-    GETEX,
-    getEx: GETEX,
-    GETRANGE,
-    getRange: GETRANGE,
-    GETSET,
-    getSet: GETSET,
-    HDEL,
-    hDel: HDEL,
-    HEXISTS,
-    hExists: HEXISTS,
-    HEXPIRE,
-    hExpire: HEXPIRE,
-    HEXPIREAT,
-    hExpireAt: HEXPIREAT,
-    HEXPIRETIME,
-    hExpireTime: HEXPIRETIME,
-    HGET,
-    hGet: HGET,
-    HGETALL,
-    hGetAll: HGETALL,
-    HINCRBY,
-    hIncrBy: HINCRBY,
-    HINCRBYFLOAT,
-    hIncrByFloat: HINCRBYFLOAT,
-    HKEYS,
-    hKeys: HKEYS,
-    HLEN,
-    hLen: HLEN,
-    HMGET,
-    hmGet: HMGET,
-    HPERSIST,
-    hPersist: HPERSIST,
-    HPEXPIRE,
-    hpExpire: HPEXPIRE,
-    HPEXPIREAT,
-    hpExpireAt: HPEXPIREAT,
-    HPEXPIRETIME,
-    hpExpireTime: HPEXPIRETIME,
-    HPTTL,
-    hpTTL: HPTTL,
-    HRANDFIELD_COUNT_WITHVALUES,
-    hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
-    HRANDFIELD_COUNT,
-    hRandFieldCount: HRANDFIELD_COUNT,
-    HRANDFIELD,
-    hRandField: HRANDFIELD,
-    HSCAN,
-    hScan: HSCAN,
-    HSCAN_NOVALUES,
-    hScanNoValues: HSCAN_NOVALUES,
-    HSET,
-    hSet: HSET,
-    HSETNX,
-    hSetNX: HSETNX,
-    HSTRLEN,
-    hStrLen: HSTRLEN,
-    HTTL,
-    hTTL: HTTL,
-    HVALS,
-    hVals: HVALS,
-    INCR,
-    incr: INCR,
-    INCRBY,
-    incrBy: INCRBY,
-    INCRBYFLOAT,
-    incrByFloat: INCRBYFLOAT,
-    LCS_IDX_WITHMATCHLEN,
-    lcsIdxWithMatchLen: LCS_IDX_WITHMATCHLEN,
-    LCS_IDX,
-    lcsIdx: LCS_IDX,
-    LCS_LEN,
-    lcsLen: LCS_LEN,
-    LCS,
-    lcs: LCS,
-    LINDEX,
-    lIndex: LINDEX,
-    LINSERT,
-    lInsert: LINSERT,
-    LLEN,
-    lLen: LLEN,
-    LMOVE,
-    lMove: LMOVE,
-    LMPOP,
-    lmPop: LMPOP,
-    LPOP_COUNT,
-    lPopCount: LPOP_COUNT,
-    LPOP,
-    lPop: LPOP,
-    LPOS_COUNT,
-    lPosCount: LPOS_COUNT,
-    LPOS,
-    lPos: LPOS,
-    LPUSH,
-    lPush: LPUSH,
-    LPUSHX,
-    lPushX: LPUSHX,
-    LRANGE,
-    lRange: LRANGE,
-    LREM,
-    lRem: LREM,
-    LSET,
-    lSet: LSET,
-    LTRIM,
-    lTrim: LTRIM,
-    MGET,
-    mGet: MGET,
-    MIGRATE,
-    migrate: MIGRATE,
-    MSET,
-    mSet: MSET,
-    MSETNX,
-    mSetNX: MSETNX,
-    OBJECT_ENCODING,
-    objectEncoding: OBJECT_ENCODING,
-    OBJECT_FREQ,
-    objectFreq: OBJECT_FREQ,
-    OBJECT_IDLETIME,
-    objectIdleTime: OBJECT_IDLETIME,
-    OBJECT_REFCOUNT,
-    objectRefCount: OBJECT_REFCOUNT,
-    PERSIST,
-    persist: PERSIST,
-    PEXPIRE,
-    pExpire: PEXPIRE,
-    PEXPIREAT,
-    pExpireAt: PEXPIREAT,
-    PEXPIRETIME,
-    pExpireTime: PEXPIRETIME,
-    PFADD,
-    pfAdd: PFADD,
-    PFCOUNT,
-    pfCount: PFCOUNT,
-    PFMERGE,
-    pfMerge: PFMERGE,
-    PSETEX,
-    pSetEx: PSETEX,
-    PTTL,
-    pTTL: PTTL,
-    PUBLISH,
-    publish: PUBLISH,
-    RENAME,
-    rename: RENAME,
-    RENAMENX,
-    renameNX: RENAMENX,
-    RESTORE,
-    restore: RESTORE,
-    RPOP_COUNT,
-    rPopCount: RPOP_COUNT,
-    RPOP,
-    rPop: RPOP,
-    RPOPLPUSH,
-    rPopLPush: RPOPLPUSH,
-    RPUSH,
-    rPush: RPUSH,
-    RPUSHX,
-    rPushX: RPUSHX,
-    SADD,
-    sAdd: SADD,
-    SCARD,
-    sCard: SCARD,
-    SDIFF,
-    sDiff: SDIFF,
-    SDIFFSTORE,
-    sDiffStore: SDIFFSTORE,
-    SINTER,
-    sInter: SINTER,
-    SINTERCARD,
-    sInterCard: SINTERCARD,
-    SINTERSTORE,
-    sInterStore: SINTERSTORE,
-    SET,
-    set: SET,
-    SETBIT,
-    setBit: SETBIT,
-    SETEX,
-    setEx: SETEX,
-    SETNX,
-    setNX: SETNX,
-    SETRANGE,
-    setRange: SETRANGE,
-    SISMEMBER,
-    sIsMember: SISMEMBER,
-    SMEMBERS,
-    sMembers: SMEMBERS,
-    SMISMEMBER,
-    smIsMember: SMISMEMBER,
-    SMOVE,
-    sMove: SMOVE,
-    SORT_RO,
-    sortRo: SORT_RO,
-    SORT_STORE,
-    sortStore: SORT_STORE,
-    SORT,
-    sort: SORT,
-    SPOP,
-    sPop: SPOP,
-    SPUBLISH,
-    sPublish: SPUBLISH,
-    SRANDMEMBER_COUNT,
-    sRandMemberCount: SRANDMEMBER_COUNT,
-    SRANDMEMBER,
-    sRandMember: SRANDMEMBER,
-    SREM,
-    sRem: SREM,
-    SSCAN,
-    sScan: SSCAN,
-    STRLEN,
-    strLen: STRLEN,
-    SUNION,
-    sUnion: SUNION,
-    SUNIONSTORE,
-    sUnionStore: SUNIONSTORE,
-    TOUCH,
-    touch: TOUCH,
-    TTL,
-    ttl: TTL,
-    TYPE,
-    type: TYPE,
-    UNLINK,
-    unlink: UNLINK,
-    WATCH,
-    watch: WATCH,
-    XACK,
-    xAck: XACK,
-    XADD,
-    xAdd: XADD,
-    XAUTOCLAIM_JUSTID,
-    xAutoClaimJustId: XAUTOCLAIM_JUSTID,
-    XAUTOCLAIM,
-    xAutoClaim: XAUTOCLAIM,
-    XCLAIM,
-    xClaim: XCLAIM,
-    XCLAIM_JUSTID,
-    xClaimJustId: XCLAIM_JUSTID,
-    XDEL,
-    xDel: XDEL,
-    XGROUP_CREATE,
-    xGroupCreate: XGROUP_CREATE,
-    XGROUP_CREATECONSUMER,
-    xGroupCreateConsumer: XGROUP_CREATECONSUMER,
-    XGROUP_DELCONSUMER,
-    xGroupDelConsumer: XGROUP_DELCONSUMER,
-    XGROUP_DESTROY,
-    xGroupDestroy: XGROUP_DESTROY,
-    XGROUP_SETID,
-    xGroupSetId: XGROUP_SETID,
-    XINFO_CONSUMERS,
-    xInfoConsumers: XINFO_CONSUMERS,
-    XINFO_GROUPS,
-    xInfoGroups: XINFO_GROUPS,
-    XINFO_STREAM,
-    xInfoStream: XINFO_STREAM,
-    XLEN,
-    xLen: XLEN,
-    XPENDING_RANGE,
-    xPendingRange: XPENDING_RANGE,
-    XPENDING,
-    xPending: XPENDING,
-    XRANGE,
-    xRange: XRANGE,
-    XREAD,
-    xRead: XREAD,
-    XREADGROUP,
-    xReadGroup: XREADGROUP,
-    XREVRANGE,
-    xRevRange: XREVRANGE,
-    XSETID,
-    xSetId: XSETID,
-    XTRIM,
-    xTrim: XTRIM,
-    ZADD,
-    zAdd: ZADD,
-    ZCARD,
-    zCard: ZCARD,
-    ZCOUNT,
-    zCount: ZCOUNT,
-    ZDIFF_WITHSCORES,
-    zDiffWithScores: ZDIFF_WITHSCORES,
-    ZDIFF,
-    zDiff: ZDIFF,
-    ZDIFFSTORE,
-    zDiffStore: ZDIFFSTORE,
-    ZINCRBY,
-    zIncrBy: ZINCRBY,
-    ZINTER_WITHSCORES,
-    zInterWithScores: ZINTER_WITHSCORES,
-    ZINTER,
-    zInter: ZINTER,
-    ZINTERCARD,
-    zInterCard: ZINTERCARD,
-    ZINTERSTORE,
-    zInterStore: ZINTERSTORE,
-    ZLEXCOUNT,
-    zLexCount: ZLEXCOUNT,
-    ZMPOP,
-    zmPop: ZMPOP,
-    ZMSCORE,
-    zmScore: ZMSCORE,
-    ZPOPMAX_COUNT,
-    zPopMaxCount: ZPOPMAX_COUNT,
-    ZPOPMAX,
-    zPopMax: ZPOPMAX,
-    ZPOPMIN_COUNT,
-    zPopMinCount: ZPOPMIN_COUNT,
-    ZPOPMIN,
-    zPopMin: ZPOPMIN,
-    ZRANDMEMBER_COUNT_WITHSCORES,
-    zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES,
-    ZRANDMEMBER_COUNT,
-    zRandMemberCount: ZRANDMEMBER_COUNT,
-    ZRANDMEMBER,
-    zRandMember: ZRANDMEMBER,
-    ZRANGE_WITHSCORES,
-    zRangeWithScores: ZRANGE_WITHSCORES,
-    ZRANGE,
-    zRange: ZRANGE,
-    ZRANGEBYLEX,
-    zRangeByLex: ZRANGEBYLEX,
-    ZRANGEBYSCORE_WITHSCORES,
-    zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES,
-    ZRANGEBYSCORE,
-    zRangeByScore: ZRANGEBYSCORE,
-    ZRANGESTORE,
-    zRangeStore: ZRANGESTORE,
-    ZRANK,
-    zRank: ZRANK,
-    ZREM,
-    zRem: ZREM,
-    ZREMRANGEBYLEX,
-    zRemRangeByLex: ZREMRANGEBYLEX,
-    ZREMRANGEBYRANK,
-    zRemRangeByRank: ZREMRANGEBYRANK,
-    ZREMRANGEBYSCORE,
-    zRemRangeByScore: ZREMRANGEBYSCORE,
-    ZREVRANK,
-    zRevRank: ZREVRANK,
-    ZSCAN,
-    zScan: ZSCAN,
-    ZSCORE,
-    zScore: ZSCORE,
-    ZUNION_WITHSCORES,
-    zUnionWithScores: ZUNION_WITHSCORES,
-    ZUNION,
-    zUnion: ZUNION,
-    ZUNIONSTORE,
-    zUnionStore: ZUNIONSTORE
-};
diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts
index 569d716272a..4db5f32e853 100644
--- a/packages/client/lib/cluster/index.spec.ts
+++ b/packages/client/lib/cluster/index.spec.ts
@@ -1,389 +1,342 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
 import RedisCluster from '.';
-import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
-import { commandOptions } from '../command-options';
 import { SQUARE_SCRIPT } from '../client/index.spec';
 import { RootNodesUnavailableError } from '../errors';
 import { spy } from 'sinon';
-import { promiseTimeout } from '../utils';
 import RedisClient from '../client';
 
 describe('Cluster', () => {
-    testUtils.testWithCluster('sendCommand', async cluster => {
-        assert.equal(
-            await cluster.sendCommand(undefined, true, ['PING']),
-            'PONG'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('sendCommand', async cluster => {
+    assert.equal(
+      await cluster.sendCommand(undefined, true, ['PING']),
+      'PONG'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
+
+  testUtils.testWithCluster('isOpen', async cluster => {
+    assert.equal(cluster.isOpen, true);
+    await cluster.destroy();
+    assert.equal(cluster.isOpen, false);
+  }, GLOBAL.CLUSTERS.OPEN);
+
+  testUtils.testWithCluster('connect should throw if already connected', async cluster => {
+    await assert.rejects(cluster.connect());
+  }, GLOBAL.CLUSTERS.OPEN);
+
+  testUtils.testWithCluster('multi', async cluster => {
+    const key = 'key';
+    assert.deepEqual(
+      await cluster.multi()
+        .set(key, 'value')
+        .get(key)
+        .exec(),
+      ['OK', 'value']
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
+
+  testUtils.testWithCluster('scripts', async cluster => {
+    const [, reply] = await Promise.all([
+      cluster.set('key', '2'),
+      cluster.square('key')
+    ]);
+
+    assert.equal(reply, 4);
+  }, {
+    ...GLOBAL.CLUSTERS.OPEN,
+    clusterConfiguration: {
+      scripts: {
+        square: SQUARE_SCRIPT
+      }
+    }
+  });
+
+  it('should throw RootNodesUnavailableError', async () => {
+    const cluster = RedisCluster.create({
+      rootNodes: []
+    });
 
-    testUtils.testWithCluster('isOpen', async cluster => {
-        assert.equal(cluster.isOpen, true);
-        await cluster.disconnect();
-        assert.equal(cluster.isOpen, false);
-    }, GLOBAL.CLUSTERS.OPEN);
+    try {
+      await assert.rejects(
+        cluster.connect(),
+        RootNodesUnavailableError
+      );
+    } catch (err) {
+      await cluster.disconnect();
+      throw err;
+    }
+  });
+
+  testUtils.testWithCluster('should handle live resharding', async cluster => {
+    const slot = 12539,
+      key = 'key',
+      value = 'value';
+    await cluster.set(key, value);
+
+    const importing = cluster.slots[0].master,
+      migrating = cluster.slots[slot].master,
+      [importingClient, migratingClient] = await Promise.all([
+        cluster.nodeClient(importing),
+        cluster.nodeClient(migrating)
+      ]);
+
+    await Promise.all([
+      importingClient.clusterSetSlot(slot, 'IMPORTING', migrating.id),
+      migratingClient.clusterSetSlot(slot, 'MIGRATING', importing.id)
+    ]);
+
+    // should be able to get the key from the migrating node
+    assert.equal(
+      await cluster.get(key),
+      value
+    );
+
+    await migratingClient.migrate(
+      importing.host,
+      importing.port,
+      key,
+      0,
+      10
+    );
+
+    // should be able to get the key from the importing node using `ASKING`
+    assert.equal(
+      await cluster.get(key),
+      value
+    );
+
+    await Promise.all([
+      importingClient.clusterSetSlot(slot, 'NODE', importing.id),
+      migratingClient.clusterSetSlot(slot, 'NODE', importing.id),
+    ]);
+
+    // should handle `MOVED` errors
+    assert.equal(
+      await cluster.get(key),
+      value
+    );
+  }, {
+    serverArguments: [],
+    numberOfMasters: 2
+  });
+
+  testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
+    const totalNodes = cluster.masters.length + cluster.replicas.length,
+      ids = new Set<string>();
+    for (let i = 0; i < totalNodes; i++) {
+      ids.add(cluster.getRandomNode().id);
+    }
+
+    assert.equal(ids.size, totalNodes);
+  }, GLOBAL.CLUSTERS.WITH_REPLICAS);
+
+  testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
+    const totalNodes = 1 + cluster.slots[0].replicas!.length,
+      ids = new Set<string>();
+    for (let i = 0; i < totalNodes; i++) {
+      ids.add(cluster.getSlotRandomNode(0).id);
+    }
+
+    assert.equal(ids.size, totalNodes);
+  }, GLOBAL.CLUSTERS.WITH_REPLICAS);
+
+  testUtils.testWithCluster('cluster topology', async cluster => {
+    assert.equal(cluster.slots.length, 16384);
+    const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
+    assert.equal(cluster.masters.length, numberOfMasters);
+    assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
+    assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
+  }, GLOBAL.CLUSTERS.WITH_REPLICAS);
+
+  testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
+    const masters = cluster.getMasters();
+    assert.ok(Array.isArray(masters));
+    for (const master of masters) {
+      assert.equal(typeof master.id, 'string');
+      assert.ok(master.client instanceof RedisClient);
+    }
+  }, {
+    ...GLOBAL.CLUSTERS.OPEN,
+    clusterConfiguration: {
+      minimizeConnections: undefined // reset to default
+    }
+  });
+
+  testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
+    const master = cluster.getSlotMaster(0);
+    assert.equal(typeof master.id, 'string');
+    assert.ok(master.client instanceof RedisClient);
+  }, {
+    ...GLOBAL.CLUSTERS.OPEN,
+    clusterConfiguration: {
+      minimizeConnections: undefined // reset to default
+    }
+  });
+
+  testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
+    await assert.rejects(cluster.mGet(['a', 'b']));
+  }, GLOBAL.CLUSTERS.OPEN);
+
+  describe('minimizeConnections', () => {
+    testUtils.testWithCluster('false', async cluster => {
+      for (const master of cluster.masters) {
+        assert.ok(master.client instanceof RedisClient);
+      }
+    }, {
+      ...GLOBAL.CLUSTERS.OPEN,
+      clusterConfiguration: {
+        minimizeConnections: false
+      }
+    });
 
-    testUtils.testWithCluster('connect should throw if already connected', async cluster => {
-        await assert.rejects(cluster.connect());
-    }, GLOBAL.CLUSTERS.OPEN);
+    testUtils.testWithCluster('true', async cluster => {
+      for (const master of cluster.masters) {
+        assert.equal(master.client, undefined);
+      }
+    }, {
+      ...GLOBAL.CLUSTERS.OPEN,
+      clusterConfiguration: {
+        minimizeConnections: true
+      }
+    });
+  });
 
-    testUtils.testWithCluster('multi', async cluster => {
-        const key = 'key';
-        assert.deepEqual(
-            await cluster.multi()
-                .set(key, 'value')
-                .get(key)
-                .exec(),
-            ['OK', 'value']
-        );
+  describe('PubSub', () => {
+    testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
+      const listener = spy();
+
+      await cluster.subscribe('channel', listener);
+
+      await Promise.all([
+        waitTillBeenCalled(listener),
+        cluster.publish('channel', 'message')
+      ]);
+
+      assert.ok(listener.calledOnceWithExactly('message', 'channel'));
+
+      await cluster.unsubscribe('channel', listener);
+
+      assert.equal(cluster.pubSubNode, undefined);
     }, GLOBAL.CLUSTERS.OPEN);
 
-    testUtils.testWithCluster('scripts', async cluster => {
-        assert.equal(
-            await cluster.square(2),
-            4
-        );
-    }, {
-        ...GLOBAL.CLUSTERS.OPEN,
-        clusterConfiguration: {
-            scripts: {
-                square: SQUARE_SCRIPT
-            }
-        }
-    });
+    testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
+      const listener = spy();
 
-    it('should throw RootNodesUnavailableError', async () => {
-        const cluster = RedisCluster.create({
-            rootNodes: []
-        });
-
-        try {
-            await assert.rejects(
-                cluster.connect(),
-                RootNodesUnavailableError
-            );
-        } catch (err) {
-            await cluster.disconnect();
-            throw err;
-        }
-    });
+      await cluster.pSubscribe('channe*', listener);
 
-    testUtils.testWithCluster('should handle live resharding', async cluster => {
-        const slot = 12539,
-            key = 'key',
-            value = 'value';
-        await cluster.set(key, value);
-
-        const importing = cluster.slots[0].master,
-            migrating = cluster.slots[slot].master,
-            [ importingClient, migratingClient ] = await Promise.all([
-                cluster.nodeClient(importing),
-                cluster.nodeClient(migrating)
-            ]);
-
-        await Promise.all([
-            importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id),
-            migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id)
-        ]);
+      await Promise.all([
+        waitTillBeenCalled(listener),
+        cluster.publish('channel', 'message')
+      ]);
 
-        // should be able to get the key from the migrating node
-        assert.equal(
-            await cluster.get(key),
-            value
-        );
+      assert.ok(listener.calledOnceWithExactly('message', 'channel'));
 
-        await migratingClient.migrate(
-            importing.host,
-            importing.port,
-            key,
-            0,
-            10
-        );
+      await cluster.pUnsubscribe('channe*', listener);
 
-        // should be able to get the key from the importing node using `ASKING`
-        assert.equal(
-            await cluster.get(key),
-            value
-        );
+      assert.equal(cluster.pubSubNode, undefined);
+    }, GLOBAL.CLUSTERS.OPEN);
 
-        await Promise.all([
-            importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
-            migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
+    testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
+      const listener = spy();
+      await cluster.subscribe('channel', listener);
+
+      assert.ok(cluster.pubSubNode);
+      const [migrating, importing] = cluster.masters[0].address === cluster.pubSubNode.address ?
+        cluster.masters :
+        [cluster.masters[1], cluster.masters[0]],
+        [migratingClient, importingClient] = await Promise.all([
+          cluster.nodeClient(migrating),
+          cluster.nodeClient(importing)
         ]);
 
-        // should handle `MOVED` errors
-        assert.equal(
-            await cluster.get(key),
-            value
+      const range = cluster.slots[0].master === migrating ? {
+        key: 'bar', // 5061
+        start: 0,
+        end: 8191
+      } : {
+        key: 'foo', // 12182
+        start: 8192,
+        end: 16383
+      };
+
+      // TODO: is there a better way to migrate slots without causing CLUSTERDOWN?
+      const promises: Array<Promise<unknown>> = [];
+      for (let i = range.start; i <= range.end; i++) {
+        promises.push(
+          migratingClient.clusterSetSlot(i, 'NODE', importing.id),
+          importingClient.clusterSetSlot(i, 'NODE', importing.id)
         );
-    }, {
-        serverArguments: [],
-        numberOfMasters: 2
-    });
+      }
+      await Promise.all(promises);
 
-    testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
-        const totalNodes = cluster.masters.length + cluster.replicas.length,
-            ids = new Set<string>();
-        for (let i = 0; i < totalNodes; i++) {
-            ids.add(cluster.getRandomNode().id);
-        }
-        
-        assert.equal(ids.size, totalNodes);
-    }, GLOBAL.CLUSTERS.WITH_REPLICAS);
-
-    testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
-        const totalNodes = 1 + cluster.slots[0].replicas!.length,
-            ids = new Set<string>();
-        for (let i = 0; i < totalNodes; i++) {
-            ids.add(cluster.getSlotRandomNode(0).id);
-        }
-        
-        assert.equal(ids.size, totalNodes);
-    }, GLOBAL.CLUSTERS.WITH_REPLICAS);
-
-    testUtils.testWithCluster('cluster topology', async cluster => {
-        assert.equal(cluster.slots.length, 16384);
-        const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
-        assert.equal(cluster.shards.length, numberOfMasters);
-        assert.equal(cluster.masters.length, numberOfMasters);
-        assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
-        assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
-    }, GLOBAL.CLUSTERS.WITH_REPLICAS);
-
-    testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
-        const masters = cluster.getMasters();
-        assert.ok(Array.isArray(masters));
-        for (const master of masters) {
-            assert.equal(typeof master.id, 'string');
-            assert.ok(master.client instanceof RedisClient);
-        }
-    }, {
-        ...GLOBAL.CLUSTERS.OPEN,
-        clusterConfiguration: {
-            minimizeConnections: undefined // reset to default
-        }
-    });
+      // make sure to cause `MOVED` error
+      await cluster.get(range.key);
 
-    testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
-        const master = cluster.getSlotMaster(0);
-        assert.equal(typeof master.id, 'string');
-        assert.ok(master.client instanceof RedisClient);
+      await Promise.all([
+        cluster.publish('channel', 'message'),
+        waitTillBeenCalled(listener)
+      ]);
+
+      assert.ok(listener.calledOnceWithExactly('message', 'channel'));
     }, {
-        ...GLOBAL.CLUSTERS.OPEN,
-        clusterConfiguration: {
-            minimizeConnections: undefined // reset to default
-        }
+      serverArguments: [],
+      numberOfMasters: 2,
+      minimumDockerVersion: [7]
     });
 
-    testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
-        await assert.rejects(cluster.mGet(['a', 'b']));
-    }, GLOBAL.CLUSTERS.OPEN);
+    testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
+      const listener = spy();
 
-    testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => {
-        // 'a' and 'b' hash to different cluster slots (see previous unit test)
-        // -> maxCommandRedirections 0: rejects on MOVED/ASK reply
-        await cluster.set(commandOptions({ isolated: true }), 'a', '1'),
-        await cluster.set(commandOptions({ isolated: true }), 'b', '2'),
+      await cluster.sSubscribe('channel', listener);
 
-        assert.equal(await cluster.get('a'), '1');
-        assert.equal(await cluster.get('b'), '2');
+      await Promise.all([
+        waitTillBeenCalled(listener),
+        cluster.sPublish('channel', 'message')
+      ]);
+
+      assert.ok(listener.calledOnceWithExactly('message', 'channel'));
+
+      await cluster.sUnsubscribe('channel', listener);
+
+      // 10328 is the slot of `channel`
+      assert.equal(cluster.slots[10328].master.pubSub, undefined);
     }, {
-        ...GLOBAL.CLUSTERS.OPEN,
-        clusterConfiguration: {
-            maxCommandRedirections: 0
-        }
+      ...GLOBAL.CLUSTERS.OPEN,
+      minimumDockerVersion: [7]
     });
 
-    describe('minimizeConnections', () => {
-        testUtils.testWithCluster('false', async cluster => {
-            for (const master of cluster.masters) {
-                assert.ok(master.client instanceof RedisClient);
-            }
-        }, {
-            ...GLOBAL.CLUSTERS.OPEN,
-            clusterConfiguration: {
-                minimizeConnections: false
-            }
-        });
-
-        testUtils.testWithCluster('true', async cluster => {
-            for (const master of cluster.masters) {
-                assert.equal(master.client, undefined);
-            }
-        }, {
-            ...GLOBAL.CLUSTERS.OPEN,
-            clusterConfiguration: {
-                minimizeConnections: true
-            }
-        });
-    });
+    testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
+      const SLOT = 10328,
+        migrating = cluster.slots[SLOT].master,
+        importing = cluster.masters.find(master => master !== migrating)!,
+        [migratingClient, importingClient] = await Promise.all([
+          cluster.nodeClient(migrating),
+          cluster.nodeClient(importing)
+        ]);
 
-    describe('PubSub', () => {
-        testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
-            const listener = spy();
-
-            await cluster.subscribe('channel', listener);
-
-            await Promise.all([
-                waitTillBeenCalled(listener),
-                cluster.publish('channel', 'message')
-            ]);
-            
-            assert.ok(listener.calledOnceWithExactly('message', 'channel'));
-
-            await cluster.unsubscribe('channel', listener);
-
-            assert.equal(cluster.pubSubNode, undefined);
-        }, GLOBAL.CLUSTERS.OPEN);
-        
-        testUtils.testWithCluster('concurrent UNSUBSCRIBE does not throw an error (#2685)', async cluster => {
-            const listener = spy();
-            await Promise.all([
-                cluster.subscribe('1', listener),
-                cluster.subscribe('2', listener)
-            ]);
-            await Promise.all([
-                cluster.unsubscribe('1', listener),
-                cluster.unsubscribe('2', listener)
-            ]);
-        }, GLOBAL.CLUSTERS.OPEN);
-
-        testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
-            const listener = spy();
-
-            await cluster.pSubscribe('channe*', listener);
-
-            await Promise.all([
-                waitTillBeenCalled(listener),
-                cluster.publish('channel', 'message')
-            ]);
-            
-            assert.ok(listener.calledOnceWithExactly('message', 'channel'));
-
-            await cluster.pUnsubscribe('channe*', listener);
-
-            assert.equal(cluster.pubSubNode, undefined);
-        }, GLOBAL.CLUSTERS.OPEN);        
-
-        testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
-            const listener = spy();
-            await cluster.subscribe('channel', listener);
-
-            assert.ok(cluster.pubSubNode);
-            const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ?
-                    cluster.masters :
-                    [cluster.masters[1], cluster.masters[0]],
-                [ migratingClient, importingClient ] = await Promise.all([
-                    cluster.nodeClient(migrating),
-                    cluster.nodeClient(importing)
-                ]);
-
-            const range = cluster.slots[0].master === migrating ? {
-                key: 'bar', // 5061
-                start: 0,
-                end: 8191
-            } : {
-                key: 'foo', // 12182
-                start: 8192,
-                end: 16383
-            };
-
-            await Promise.all([
-                migratingClient.clusterDelSlotsRange(range),
-                importingClient.clusterDelSlotsRange(range),
-                importingClient.clusterAddSlotsRange(range)
-            ]);
-
-            // wait for migrating node to be notified about the new topology
-            while ((await migratingClient.clusterInfo()).state !== 'ok') {
-                await promiseTimeout(50);
-            }
-
-            // make sure to cause `MOVED` error
-            await cluster.get(range.key);
-
-            await Promise.all([
-                cluster.publish('channel', 'message'),
-                waitTillBeenCalled(listener)
-            ]);
-            
-            assert.ok(listener.calledOnceWithExactly('message', 'channel'));
-        }, {
-            serverArguments: [],
-            numberOfMasters: 2,
-            minimumDockerVersion: [7]
-        });
-
-        testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
-            const listener = spy();
-
-            await cluster.sSubscribe('channel', listener);
-
-            await Promise.all([
-                waitTillBeenCalled(listener),
-                cluster.sPublish('channel', 'message')
-            ]);
-            
-            assert.ok(listener.calledOnceWithExactly('message', 'channel'));
-
-            await cluster.sUnsubscribe('channel', listener);
-
-            // 10328 is the slot of `channel`
-            assert.equal(cluster.slots[10328].master.pubSubClient, undefined);
-        }, {
-            ...GLOBAL.CLUSTERS.OPEN,
-            minimumDockerVersion: [7]
-        });
-
-        testUtils.testWithCluster('concurrent SUNSUBCRIBE does not throw an error (#2685)', async cluster => {
-            const listener = spy();
-            await Promise.all([
-                await cluster.sSubscribe('1', listener),
-                await cluster.sSubscribe('2', listener)
-            ]);
-            await Promise.all([
-                cluster.sUnsubscribe('1', listener),
-                cluster.sUnsubscribe('2', listener)
-            ]);
-        }, {
-            ...GLOBAL.CLUSTERS.OPEN,
-            minimumDockerVersion: [7]
-        });
-
-        testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
-            const SLOT = 10328,
-                migrating = cluster.slots[SLOT].master,
-                importing = cluster.masters.find(master => master !== migrating)!,
-                [ migratingClient, importingClient ] = await Promise.all([
-                    cluster.nodeClient(migrating),
-                    cluster.nodeClient(importing)
-                ]);
-
-            await Promise.all([
-                migratingClient.clusterDelSlots(SLOT),
-                importingClient.clusterDelSlots(SLOT),
-                importingClient.clusterAddSlots(SLOT)
-            ]);
-
-            // wait for migrating node to be notified about the new topology
-            while ((await migratingClient.clusterInfo()).state !== 'ok') {
-                await promiseTimeout(50);
-            }
-
-            const listener = spy();
-
-            // will trigger `MOVED` error
-            await cluster.sSubscribe('channel', listener);
-
-            await Promise.all([
-                waitTillBeenCalled(listener),
-                cluster.sPublish('channel', 'message')
-            ]);
-            
-            assert.ok(listener.calledOnceWithExactly('message', 'channel'));
-        }, {
-            serverArguments: [],
-            minimumDockerVersion: [7]
-        });
+      await Promise.all([
+        migratingClient.clusterDelSlots(SLOT),
+        importingClient.clusterDelSlots(SLOT),
+        importingClient.clusterAddSlots(SLOT),
+        // cause "topology refresh" on both nodes
+        migratingClient.clusterSetSlot(SLOT, 'NODE', importing.id),
+        importingClient.clusterSetSlot(SLOT, 'NODE', importing.id)
+      ]);
+
+      const listener = spy();
+
+      // will trigger `MOVED` error
+      await cluster.sSubscribe('channel', listener);
+
+      await Promise.all([
+        waitTillBeenCalled(listener),
+        cluster.sPublish('channel', 'message')
+      ]);
+
+      assert.ok(listener.calledOnceWithExactly('message', 'channel'));
+    }, {
+      serverArguments: [],
+      minimumDockerVersion: [7]
     });
+  });
 });
diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts
index 49ac293d6cf..6d26ac98c9a 100644
--- a/packages/client/lib/cluster/index.ts
+++ b/packages/client/lib/cluster/index.ts
@@ -1,424 +1,690 @@
-import COMMANDS from './commands';
-import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands';
-import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client';
+import { RedisClientOptions, RedisClientType } from '../client';
+import { CommandOptions } from '../client/commands-queue';
+import { Command, CommandArguments, CommanderConfig, CommandSignature, /*CommandPolicies, CommandWithPoliciesSignature,*/ TypeMapping, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions } from '../RESP/types';
+import COMMANDS from '../commands';
+import { EventEmitter } from 'node:events';
+import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
 import RedisClusterSlots, { NodeAddressMap, ShardNode } from './cluster-slots';
-import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander';
-import { EventEmitter } from 'events';
-import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
-import { RedisMultiQueuedCommand } from '../multi-command';
+import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
 import { PubSubListener } from '../client/pub-sub';
 import { ErrorReply } from '../errors';
+import { RedisTcpSocketOptions } from '../client/socket';
+import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/cache';
+import { BasicCommandParser } from '../client/parser';
+import { ASKING_CMD } from '../commands/ASKING';
+import SingleEntryCache from '../single-entry-cache'
+interface ClusterCommander<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping,
+  // POLICIES extends CommandPolicies
+> extends CommanderConfig<M, F, S, RESP> {
+  commandOptions?: ClusterCommandOptions<TYPE_MAPPING/*, POLICIES*/>;
+}
 
 export type RedisClusterClientOptions = Omit<
-    RedisClientOptions,
-    'modules' | 'functions' | 'scripts' | 'database'
+  RedisClientOptions<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisTcpSocketOptions>,
+  keyof ClusterCommander<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping/*, CommandPolicies*/>
 >;
 
 export interface RedisClusterOptions<
-    M extends RedisModules = Record<string, never>,
-    F extends RedisFunctions = Record<string, never>,
-    S extends RedisScripts = Record<string, never>
-> extends RedisExtensions<M, F, S> {
-    /**
-     * Should contain details for some of the cluster nodes that the client will use to discover
-     * the "cluster topology". We recommend including details for at least 3 nodes here.
-     */
-    rootNodes: Array<RedisClusterClientOptions>;
-    /**
-     * Default values used for every client in the cluster. Use this to specify global values,
-     * for example: ACL credentials, timeouts, TLS configuration etc.
-     */
-    defaults?: Partial<RedisClusterClientOptions>;
-    /**
-     * When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes.
-     * Useful for short-term or PubSub-only connections.
-     */
-    minimizeConnections?: boolean;
-    /**
-     * When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes.
-     */
-    useReplicas?: boolean;
-    /**
-     * The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors.
-     */
-    maxCommandRedirections?: number;
-    /**
-     * Mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to
-     * Useful when the cluster is running on another network
-     *
-     */
-    nodeAddressMap?: NodeAddressMap;
+  M extends RedisModules = RedisModules,
+  F extends RedisFunctions = RedisFunctions,
+  S extends RedisScripts = RedisScripts,
+  RESP extends RespVersions = RespVersions,
+  TYPE_MAPPING extends TypeMapping = TypeMapping,
+  // POLICIES extends CommandPolicies = CommandPolicies
+> extends ClusterCommander<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/> {
+  /**
+   * Should contain details for some of the cluster nodes that the client will use to discover
+   * the "cluster topology". We recommend including details for at least 3 nodes here.
+   */
+  rootNodes: Array<RedisClusterClientOptions>;
+  /**
+   * Default values used for every client in the cluster. Use this to specify global values,
+   * for example: ACL credentials, timeouts, TLS configuration etc.
+   */
+  defaults?: Partial<RedisClusterClientOptions>;
+  /**
+   * When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes.
+   * Useful for short-term or PubSub-only connections.
+   */
+  minimizeConnections?: boolean;
+  /**
+   * When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes.
+   */
+  // TODO: replicas only mode?
+  useReplicas?: boolean;
+  /**
+   * The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors.
+   */
+  maxCommandRedirections?: number;
+  /**
+   * Mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to
+   * Useful when the cluster is running on another network
+   */
+  nodeAddressMap?: NodeAddressMap;
+  /**
+   * Client Side Caching configuration for the pool.
+   *
+   * Enables Redis Servers and Clients to work together to cache results from commands
+   * sent to a server. The server will notify the client when cached results are no longer valid.
+   * In pooled mode, the cache is shared across all clients in the pool.
+   *
+   * Note: Client Side Caching is only supported with RESP3.
+   *
+   * @example Anonymous cache configuration
+   * ```
+   * const client = createCluster({
+   *   clientSideCache: {
+   *     ttl: 0,
+   *     maxEntries: 0,
+   *     evictPolicy: "LRU"
+   *   },
+   *   minimum: 5
+   * });
+   * ```
+   *
+   * @example Using a controllable cache
+   * ```
+   * const cache = new BasicPooledClientSideCache({
+   *   ttl: 0,
+   *   maxEntries: 0,
+   *   evictPolicy: "LRU"
+   * });
+   * const client = createCluster({
+   *   clientSideCache: cache,
+   *   minimum: 5
+   * });
+   * ```
+   */
+  clientSideCache?: PooledClientSideCacheProvider | ClientSideCacheConfig;
 }
 
-type WithCommands = {
-    [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
+// remove once request & response policies are ready
+type ClusterCommand<
+  NAME extends PropertyKey,
+  COMMAND extends Command
+> = COMMAND['NOT_KEYED_COMMAND'] extends true ? (
+  COMMAND['IS_FORWARD_COMMAND'] extends true ? NAME : never
+) : NAME;
+
+// CommandWithPoliciesSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING, POLICIES>
+type WithCommands<
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof typeof COMMANDS as ClusterCommand<P, (typeof COMMANDS)[P]>]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
 };
 
-export type RedisClusterType<
-    M extends RedisModules = Record<string, never>,
-    F extends RedisFunctions = Record<string, never>,
-    S extends RedisScripts = Record<string, never>
-> = RedisCluster<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
-
-export default class RedisCluster<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> extends EventEmitter {
-    static extractFirstKey(
-        command: RedisCommand,
-        originalArgs: Array<unknown>,
-        redisArgs: RedisCommandArguments
-    ): RedisCommandArgument | undefined {
-        if (command.FIRST_KEY_INDEX === undefined) {
-            return undefined;
-        } else if (typeof command.FIRST_KEY_INDEX === 'number') {
-            return redisArgs[command.FIRST_KEY_INDEX];
-        }
-
-        return command.FIRST_KEY_INDEX(...originalArgs);
-    }
-
-    static create<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(options?: RedisClusterOptions<M, F, S>): RedisClusterType<M, F, S> {
-        return new (attachExtensions({
-            BaseClass: RedisCluster,
-            modulesExecutor: RedisCluster.prototype.commandsExecutor,
-            modules: options?.modules,
-            functionsExecutor: RedisCluster.prototype.functionsExecutor,
-            functions: options?.functions,
-            scriptsExecutor: RedisCluster.prototype.scriptsExecutor,
-            scripts: options?.scripts
-        }))(options);
-    }
-
-    readonly #options: RedisClusterOptions<M, F, S>;
-
-    readonly #slots: RedisClusterSlots<M, F, S>;
-
-    get slots() {
-        return this.#slots.slots;
-    }
-
-    get shards() {
-        return this.#slots.shards;
-    }
-
-    get masters() {
-        return this.#slots.masters;
-    }
-
-    get replicas() {
-        return this.#slots.replicas;
-    }
-
-    get nodeByAddress() {
-        return this.#slots.nodeByAddress;
-    }
-
-    get pubSubNode() {
-        return this.#slots.pubSubNode;
-    }
-
-    readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
-
-    get isOpen() {
-        return this.#slots.isOpen;
-    }
-
-    constructor(options: RedisClusterOptions<M, F, S>) {
-        super();
-
-        this.#options = options;
-        this.#slots = new RedisClusterSlots(options, this.emit.bind(this));
-        this.#Multi = RedisClusterMultiCommand.extend(options);
-    }
-
-    duplicate(overrides?: Partial<RedisClusterOptions<M, F, S>>): RedisClusterType<M, F, S> {
-        return new (Object.getPrototypeOf(this).constructor)({
-            ...this.#options,
-            ...overrides
-        });
-    }
-
-    connect() {
-        return this.#slots.connect();
-    }
-
-    async commandsExecutor<C extends RedisCommand>(
-        command: C,
-        args: Array<unknown>
-    ): Promise<RedisCommandReply<C>> {
-        const { jsArgs, args: redisArgs, options } = transformCommandArguments(command, args);
-        return transformCommandReply(
-            command,
-            await this.sendCommand(
-                RedisCluster.extractFirstKey(command, jsArgs, redisArgs),
-                command.IS_READ_ONLY,
-                redisArgs,
-                options
-            ),
-            redisArgs.preserve
-        );
-    }
+type WithModules<
+  M extends RedisModules,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof M]: {
+    [C in keyof M[P] as ClusterCommand<C, M[P][C]>]: CommandSignature<M[P][C], RESP, TYPE_MAPPING>;
+  };
+};
 
-    async sendCommand<T = RedisCommandRawReply>(
-        firstKey: RedisCommandArgument | undefined,
-        isReadonly: boolean | undefined,
-        args: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<T> {
-        return this.#execute(
-            firstKey,
-            isReadonly,
-            client => client.sendCommand<T>(args, options)
-        );
-    }
+type WithFunctions<
+  F extends RedisFunctions,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [L in keyof F]: {
+    [C in keyof F[L] as ClusterCommand<C, F[L][C]>]: CommandSignature<F[L][C], RESP, TYPE_MAPPING>;
+  };
+};
 
-    async functionsExecutor<F extends RedisFunction>(
-        fn: F,
-        args: Array<unknown>,
-        name: string,
-    ): Promise<RedisCommandReply<F>> {
-        const { args: redisArgs, options } = transformCommandArguments(fn, args);
-        return transformCommandReply(
-            fn,
-            await this.executeFunction(
-                name,
-                fn,
-                args,
-                redisArgs,
-                options
-            ),
-            redisArgs.preserve
-        );
-    }
+type WithScripts<
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof S as ClusterCommand<P, S[P]>]: CommandSignature<S[P], RESP, TYPE_MAPPING>;
+};
 
-    async executeFunction(
-        name: string,
-        fn: RedisFunction,
-        originalArgs: Array<unknown>,
-        redisArgs: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<RedisCommandRawReply> {
-        return this.#execute(
-            RedisCluster.extractFirstKey(fn, originalArgs, redisArgs),
-            fn.IS_READ_ONLY,
-            client => client.executeFunction(name, fn, redisArgs, options)
-        );
-    }
+export type RedisClusterType<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {},
+  // POLICIES extends CommandPolicies = {}
+> = (
+  RedisCluster<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/> &
+  WithCommands<RESP, TYPE_MAPPING> &
+  WithModules<M, RESP, TYPE_MAPPING> &
+  WithFunctions<F, RESP, TYPE_MAPPING> &
+  WithScripts<S, RESP, TYPE_MAPPING>
+);
+
+export interface ClusterCommandOptions<
+  TYPE_MAPPING extends TypeMapping = TypeMapping
+  // POLICIES extends CommandPolicies = CommandPolicies
+> extends CommandOptions<TYPE_MAPPING> {
+  // policies?: POLICIES;
+}
 
-    async scriptsExecutor<S extends RedisScript>(script: S, args: Array<unknown>): Promise<RedisCommandReply<S>> {
-        const { args: redisArgs, options } = transformCommandArguments(script, args);
-        return transformCommandReply(
-            script,
-            await this.executeScript(
-                script,
-                args,
-                redisArgs,
-                options
-            ),
-            redisArgs.preserve
-        );
-    }
+type ProxyCluster = RedisCluster<any, any, any, any, any/*, any*/>;
 
-    async executeScript(
-        script: RedisScript,
-        originalArgs: Array<unknown>,
-        redisArgs: RedisCommandArguments,
-        options?: ClientCommandOptions
-    ): Promise<RedisCommandRawReply> {
-        return this.#execute(
-            RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
-            script.IS_READ_ONLY,
-            client => client.executeScript(script, redisArgs, options)
-        );
-    }
+type NamespaceProxyCluster = { _self: ProxyCluster };
 
-    async #execute<Reply>(
-        firstKey: RedisCommandArgument | undefined,
-        isReadonly: boolean | undefined,
-        executor: (client: RedisClientType<M, F, S>) => Promise<Reply>
-    ): Promise<Reply> {
-        const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
-        let client = await this.#slots.getClient(firstKey, isReadonly);
-        for (let i = 0;; i++) {
-            try {
-                return await executor(client);
-            } catch (err) {
-                if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) {
-                    throw err;
-                }
-
-                if (err.message.startsWith('ASK')) {
-                    const address = err.message.substring(err.message.lastIndexOf(' ') + 1);
-                    let redirectTo = await this.#slots.getMasterByAddress(address);
-                    if (!redirectTo) {
-                        await this.#slots.rediscover(client);
-                        redirectTo = await this.#slots.getMasterByAddress(address);
-                    }
-
-                    if (!redirectTo) {
-                        throw new Error(`Cannot find node ${address}`);
-                    }
-
-                    await redirectTo.asking();
-                    client = redirectTo;
-                    continue;
-                } else if (err.message.startsWith('MOVED')) {
-                    await this.#slots.rediscover(client);
-                    client = await this.#slots.getClient(firstKey, isReadonly);
-                    continue;
-                }
-
-                throw err;
-            }
+export default class RedisCluster<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping,
+  // POLICIES extends CommandPolicies
+> extends EventEmitter {
+  static #createCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return async function (this: ProxyCluster, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      return this._self._execute(
+        parser.firstKey,
+        command.IS_READ_ONLY,
+        this._commandOptions,
+        (client, opts) => client._executeCommand(command, parser, opts, transformReply)
+      );
+    };
+  }
+
+  static #createModuleCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return async function (this: NamespaceProxyCluster, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      return this._self._execute(
+        parser.firstKey,
+        command.IS_READ_ONLY,
+        this._self._commandOptions,
+        (client, opts) => client._executeCommand(command, parser, opts, transformReply)
+      );
+    };
+  }
+
+  static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
+    const prefix = functionArgumentsPrefix(name, fn);
+    const transformReply = getTransformReply(fn, resp);
+
+    return async function (this: NamespaceProxyCluster, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      fn.parseCommand(parser, ...args);
+
+      return this._self._execute(
+        parser.firstKey,
+        fn.IS_READ_ONLY,
+        this._self._commandOptions,
+        (client, opts) => client._executeCommand(fn, parser, opts, transformReply)
+      );
+    };
+  }
+
+  static #createScriptCommand(script: RedisScript, resp: RespVersions) {
+    const prefix = scriptArgumentsPrefix(script);
+    const transformReply = getTransformReply(script, resp);
+
+    return async function (this: ProxyCluster, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      script.parseCommand(parser, ...args);
+
+      return this._self._execute(
+        parser.firstKey,
+        script.IS_READ_ONLY,
+        this._commandOptions,
+        (client, opts) => client._executeScript(script, parser, opts, transformReply)
+      );
+    };
+  }
+
+  static #SingleEntryCache = new SingleEntryCache<any, any>();
+
+  static factory<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {},
+    // POLICIES extends CommandPolicies = {}
+  >(config?: ClusterCommander<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) {
+
+    let Cluster = RedisCluster.#SingleEntryCache.get(config);
+    if (!Cluster) {
+      Cluster = attachConfig({
+        BaseClass: RedisCluster,
+        commands: COMMANDS,
+        createCommand: RedisCluster.#createCommand,
+        createModuleCommand: RedisCluster.#createModuleCommand,
+        createFunctionCommand: RedisCluster.#createFunctionCommand,
+        createScriptCommand: RedisCluster.#createScriptCommand,
+        config
+      });
+
+      Cluster.prototype.Multi = RedisClusterMultiCommand.extend(config);
+      RedisCluster.#SingleEntryCache.set(config, Cluster);
+    }
+
+    return (options?: Omit<RedisClusterOptions, keyof Exclude<typeof config, undefined>>) => {
+      // returning a "proxy" to prevent the namespaces._self to leak between "proxies"
+      return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
+    };
+  }
+
+  static create<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {},
+    // POLICIES extends CommandPolicies = {}
+  >(options?: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) {
+    return RedisCluster.factory(options)(options);
+  }
+
+  readonly _options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
+
+  readonly _slots: RedisClusterSlots<M, F, S, RESP, TYPE_MAPPING>;
+
+  private _self = this;
+  private _commandOptions?: ClusterCommandOptions<TYPE_MAPPING/*, POLICIES*/>;
+
+  /**
+   * An array of the cluster slots, each slot contain its `master` and `replicas`.
+   * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica).
+   */
+  get slots() {
+    return this._self._slots.slots;
+  }
+
+  get clientSideCache() {
+    return this._self._slots.clientSideCache;
+  }
+
+  /**
+   * An array of the cluster masters.
+   * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node.
+   */
+  get masters() {
+    return this._self._slots.masters;
+  }
+
+  /**
+   * An array of the cluster replicas.
+   * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific replica node.
+   */
+  get replicas() {
+    return this._self._slots.replicas;
+  }
+
+  /**
+   * A map form a node address (`<host>:<port>`) to its shard, each shard contain its `master` and `replicas`.
+   * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica).
+   */
+  get nodeByAddress() {
+    return this._self._slots.nodeByAddress;
+  }
+
+  /**
+   * The current pub/sub node.
+   */
+  get pubSubNode() {
+    return this._self._slots.pubSubNode;
+  }
+
+  get isOpen() {
+    return this._self._slots.isOpen;
+  }
+
+  constructor(options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) {
+    super();
+
+    this._options = options;
+    this._slots = new RedisClusterSlots(options, this.emit.bind(this));
+
+    if (options?.commandOptions) {
+      this._commandOptions = options.commandOptions;
+    }
+  }
+
+  duplicate<
+    _M extends RedisModules = M,
+    _F extends RedisFunctions = F,
+    _S extends RedisScripts = S,
+    _RESP extends RespVersions = RESP,
+    _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING
+  >(overrides?: Partial<RedisClusterOptions<_M, _F, _S, _RESP, _TYPE_MAPPING>>) {
+    return new (Object.getPrototypeOf(this).constructor)({
+      ...this._self._options,
+      commandOptions: this._commandOptions,
+      ...overrides
+    }) as RedisClusterType<_M, _F, _S, _RESP, _TYPE_MAPPING>;
+  }
+
+  async connect() {
+    await this._self._slots.connect();
+    return this as unknown as RedisClusterType<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  withCommandOptions<
+    OPTIONS extends ClusterCommandOptions<TYPE_MAPPING/*, CommandPolicies*/>,
+    TYPE_MAPPING extends TypeMapping,
+    // POLICIES extends CommandPolicies
+  >(options: OPTIONS) {
+    const proxy = Object.create(this);
+    proxy._commandOptions = options;
+    return proxy as RedisClusterType<
+      M,
+      F,
+      S,
+      RESP,
+      TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
+      // POLICIES extends CommandPolicies ? POLICIES : {}
+    >;
+  }
+
+  private _commandOptionsProxy<
+    K extends keyof ClusterCommandOptions,
+    V extends ClusterCommandOptions[K]
+  >(
+    key: K,
+    value: V
+  ) {
+    const proxy = Object.create(this);
+    proxy._commandOptions = Object.create(this._commandOptions ?? null);
+    proxy._commandOptions[key] = value;
+    return proxy as RedisClusterType<
+      M,
+      F,
+      S,
+      RESP,
+      K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
+      // K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES
+    >;
+  }
+
+  /**
+   * Override the `typeMapping` command option
+   */
+  withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
+    return this._commandOptionsProxy('typeMapping', typeMapping);
+  }
+
+  // /**
+  //  * Override the `policies` command option
+  //  * TODO
+  //  */
+  // withPolicies<POLICIES extends CommandPolicies> (policies: POLICIES) {
+  //   return this._commandOptionsProxy('policies', policies);
+  // }
+
+  _handleAsk<T>(
+    fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>, opts?: ClusterCommandOptions) => Promise<T>
+  ) {
+    return async (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>, options?: ClusterCommandOptions) => {
+      const chainId = Symbol("asking chain");
+      const opts = options ? {...options} : {};
+      opts.chainId = chainId;
+
+
+
+      const ret = await Promise.all(
+        [
+          client.sendCommand([ASKING_CMD], {chainId: chainId}),
+          fn(client, opts)
+        ]
+      );
+
+      return ret[1];
+    };
+  }
+
+  async _execute<T>(
+    firstKey: RedisArgument | undefined,
+    isReadonly: boolean | undefined,
+    options: ClusterCommandOptions | undefined,
+    fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>, opts?: ClusterCommandOptions) => Promise<T>
+  ): Promise<T> {
+    const maxCommandRedirections = this._options.maxCommandRedirections ?? 16;
+    let client = await this._slots.getClient(firstKey, isReadonly);
+    let i = 0;
+
+    let myFn = fn;
+
+    while (true) {
+      try {
+        return await myFn(client, options);
+      } catch (err) {
+        myFn = fn;
+
+        // TODO: error class
+        if (++i > maxCommandRedirections || !(err instanceof Error)) {
+          throw err;
         }
-    }
-
-    MULTI(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, F, S> {
-        return new this.#Multi(
-            (commands: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => {
-                return this.#execute(
-                    firstKey,
-                    false,
-                    client => client.multiExecutor(commands, undefined, chainId)
-                );
-            },
-            routing
-        );
-    }
-
-    multi = this.MULTI;
-
-    async SUBSCRIBE<T extends boolean = false>(
-        channels: string | Array<string>,
-        listener: PubSubListener<T>,
-        bufferMode?: T
-    ) {
-        return (await this.#slots.getPubSubClient())
-            .SUBSCRIBE(channels, listener, bufferMode);
-    }
-
-    subscribe = this.SUBSCRIBE;
-
-    async UNSUBSCRIBE<T extends boolean = false>(
-        channels?: string | Array<string>,
-        listener?: PubSubListener<boolean>,
-        bufferMode?: T
-    ) {
-        return this.#slots.executeUnsubscribeCommand(client =>
-            client.UNSUBSCRIBE(channels, listener, bufferMode)
-        );
-    }
-
-    unsubscribe = this.UNSUBSCRIBE;
-
-    async PSUBSCRIBE<T extends boolean = false>(
-        patterns: string | Array<string>,
-        listener: PubSubListener<T>,
-        bufferMode?: T
-    ) {
-        return (await this.#slots.getPubSubClient())
-            .PSUBSCRIBE(patterns, listener, bufferMode);
-    }
-
-    pSubscribe = this.PSUBSCRIBE;
 
-    async PUNSUBSCRIBE<T extends boolean = false>(
-        patterns?: string | Array<string>,
-        listener?: PubSubListener<T>,
-        bufferMode?: T
-    ) {
-        return this.#slots.executeUnsubscribeCommand(client =>
-            client.PUNSUBSCRIBE(patterns, listener, bufferMode)
-        );
-    }
-
-    pUnsubscribe = this.PUNSUBSCRIBE;
-
-    async SSUBSCRIBE<T extends boolean = false>(
-        channels: string | Array<string>,
-        listener: PubSubListener<T>,
-        bufferMode?: T
-    ) {
-        const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16,
-            firstChannel = Array.isArray(channels) ? channels[0] : channels;
-        let client = await this.#slots.getShardedPubSubClient(firstChannel);
-        for (let i = 0;; i++) {
-            try {
-                return await client.SSUBSCRIBE(channels, listener, bufferMode);
-            } catch (err) {
-                if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) {
-                    throw err;
-                }
-
-                if (err.message.startsWith('MOVED')) {
-                    await this.#slots.rediscover(client);
-                    client = await this.#slots.getShardedPubSubClient(firstChannel);
-                    continue;
-                }
-
-                throw err;
-            }
+        if (err.message.startsWith('ASK')) {
+          const address = err.message.substring(err.message.lastIndexOf(' ') + 1);
+          let redirectTo = await this._slots.getMasterByAddress(address);
+          if (!redirectTo) {
+            await this._slots.rediscover(client);
+            redirectTo = await this._slots.getMasterByAddress(address);
+          }
+
+          if (!redirectTo) {
+            throw new Error(`Cannot find node ${address}`);
+          }
+
+          client = redirectTo;
+          myFn = this._handleAsk(fn);
+          continue;
         }
-    }
-
-    sSubscribe = this.SSUBSCRIBE;
-
-    SUNSUBSCRIBE<T extends boolean = false>(
-        channels: string | Array<string>,
-        listener?: PubSubListener<T>,
-        bufferMode?: T
-    ) {
-        return this.#slots.executeShardedUnsubscribeCommand(
-            Array.isArray(channels) ? channels[0] : channels,
-            client => client.SUNSUBSCRIBE(channels, listener, bufferMode)
-        );
-    }
-
-    sUnsubscribe = this.SUNSUBSCRIBE;
-
-    quit(): Promise<void> {
-        return this.#slots.quit();
-    }
-
-    disconnect(): Promise<void> {
-        return this.#slots.disconnect();
-    }
 
-    nodeClient(node: ShardNode<M, F, S>) {
-        return this.#slots.nodeClient(node);
-    }
-
-    getRandomNode() {
-        return this.#slots.getRandomNode();
-    }
+        if (err.message.startsWith('MOVED')) {
+          await this._slots.rediscover(client);
+          client = await this._slots.getClient(firstKey, isReadonly);
+          continue;
+        }
 
-    getSlotRandomNode(slot: number) {
-        return this.#slots.getSlotRandomNode(slot);
-    }
+        throw err;
+      }
+    }
+  }
+
+  async sendCommand<T = ReplyUnion>(
+    firstKey: RedisArgument | undefined,
+    isReadonly: boolean | undefined,
+    args: CommandArguments,
+    options?: ClusterCommandOptions,
+    // defaultPolicies?: CommandPolicies
+  ): Promise<T> {
+
+    // Merge global options with local options
+    const opts = {
+      ...this._self._commandOptions,
+      ...options
+    }
+    return this._self._execute(
+      firstKey,
+      isReadonly,
+      opts,
+      (client, opts) => client.sendCommand(args, opts)
+    );
+  }
+
+  MULTI(routing?: RedisArgument) {
+    type Multi = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>;
+    return new ((this as any).Multi as Multi)(
+      async (firstKey, isReadonly, commands) => {
+        const client = await this._self._slots.getClient(firstKey, isReadonly);
+        return client._executeMulti(commands);
+      },
+      async (firstKey, isReadonly, commands) => {
+        const client = await this._self._slots.getClient(firstKey, isReadonly);
+        return client._executePipeline(commands);
+      },
+      routing,
+      this._commandOptions?.typeMapping
+    );
+  }
+
+  multi = this.MULTI;
+
+  async SUBSCRIBE<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return (await this._self._slots.getPubSubClient())
+      .SUBSCRIBE(channels, listener, bufferMode);
+  }
+
+  subscribe = this.SUBSCRIBE;
+
+  async UNSUBSCRIBE<T extends boolean = false>(
+    channels?: string | Array<string>,
+    listener?: PubSubListener<boolean>,
+    bufferMode?: T
+  ) {
+    return this._self._slots.executeUnsubscribeCommand(client =>
+      client.UNSUBSCRIBE(channels, listener, bufferMode)
+    );
+  }
+
+  unsubscribe = this.UNSUBSCRIBE;
+
+  async PSUBSCRIBE<T extends boolean = false>(
+    patterns: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return (await this._self._slots.getPubSubClient())
+      .PSUBSCRIBE(patterns, listener, bufferMode);
+  }
+
+  pSubscribe = this.PSUBSCRIBE;
+
+  async PUNSUBSCRIBE<T extends boolean = false>(
+    patterns?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this._self._slots.executeUnsubscribeCommand(client =>
+      client.PUNSUBSCRIBE(patterns, listener, bufferMode)
+    );
+  }
+
+  pUnsubscribe = this.PUNSUBSCRIBE;
+
+  async SSUBSCRIBE<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    const maxCommandRedirections = this._self._options.maxCommandRedirections ?? 16,
+      firstChannel = Array.isArray(channels) ? channels[0] : channels;
+    let client = await this._self._slots.getShardedPubSubClient(firstChannel);
+    for (let i = 0; ; i++) {
+      try {
+        return await client.SSUBSCRIBE(channels, listener, bufferMode);
+      } catch (err) {
+        if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) {
+          throw err;
+        }
 
-    /**
-     * @deprecated use `.masters` instead
-     */
-    getMasters() {
-        return this.masters;
-    }
+        if (err.message.startsWith('MOVED')) {
+          await this._self._slots.rediscover(client);
+          client = await this._self._slots.getShardedPubSubClient(firstChannel);
+          continue;
+        }
 
-    /**
-     * @deprecated use `.slots[<SLOT>]` instead
-     */
-    getSlotMaster(slot: number) {
-        return this.slots[slot].master;
-    }
+        throw err;
+      }
+    }
+  }
+
+  sSubscribe = this.SSUBSCRIBE;
+
+  SUNSUBSCRIBE<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this._self._slots.executeShardedUnsubscribeCommand(
+      Array.isArray(channels) ? channels[0] : channels,
+      client => client.SUNSUBSCRIBE(channels, listener, bufferMode)
+    );
+  }
+
+  sUnsubscribe = this.SUNSUBSCRIBE;
+
+  /**
+   * @deprecated Use `close` instead.
+   */
+  quit() {
+    return this._self._slots.quit();
+  }
+
+  /**
+   * @deprecated Use `destroy` instead.
+   */
+  disconnect() {
+    return this._self._slots.disconnect();
+  }
+
+  close() {
+    this._self._slots.clientSideCache?.onPoolClose();
+    return this._self._slots.close();
+  }
+
+  destroy() {
+    this._self._slots.clientSideCache?.onPoolClose();
+    return this._self._slots.destroy();
+  }
+
+  nodeClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>) {
+    return this._self._slots.nodeClient(node);
+  }
+
+  /**
+   * Returns a random node from the cluster.
+   * Userful for running "forward" commands (like PUBLISH) on a random node.
+   */
+  getRandomNode() {
+    return this._self._slots.getRandomNode();
+  }
+
+  /**
+   * Get a random node from a slot.
+   * Useful for running readonly commands on a slot.
+   */
+  getSlotRandomNode(slot: number) {
+    return this._self._slots.getSlotRandomNode(slot);
+  }
+
+  /**
+   * @deprecated use `.masters` instead
+   * TODO
+   */
+  getMasters() {
+    return this.masters;
+  }
+
+  /**
+   * @deprecated use `.slots[<SLOT>]` instead
+   * TODO
+   */
+  getSlotMaster(slot: number) {
+    return this.slots[slot].master;
+  }
 }
-
-attachCommands({
-    BaseClass: RedisCluster,
-    commands: COMMANDS,
-    executor: RedisCluster.prototype.commandsExecutor
-});
diff --git a/packages/client/lib/cluster/multi-command.ts b/packages/client/lib/cluster/multi-command.ts
index ef3c7590ec7..f370618ff30 100644
--- a/packages/client/lib/cluster/multi-command.ts
+++ b/packages/client/lib/cluster/multi-command.ts
@@ -1,141 +1,279 @@
-import COMMANDS from './commands';
-import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
-import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
-import { attachCommands, attachExtensions } from '../commander';
-import RedisCluster from '.';
-
-type RedisClusterMultiCommandSignature<
-    C extends RedisCommand,
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, F, S>;
+import COMMANDS from '../commands';
+import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command';
+import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping, RedisArgument } from '../RESP/types';
+import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander';
+import { BasicCommandParser } from '../client/parser';
+import { Tail } from '../commands/generic-transformers';
+
+type CommandSignature<
+  REPLIES extends Array<unknown>,
+  C extends Command,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (...args: Tail<Parameters<C['parseCommand']>>) => RedisClusterMultiCommandType<
+  [...REPLIES, ReplyWithTypeMapping<CommandReply<C, RESP>, TYPE_MAPPING>],
+  M,
+  F,
+  S,
+  RESP,
+  TYPE_MAPPING
+>;
 
 type WithCommands<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>;
+  [P in keyof typeof COMMANDS]: CommandSignature<REPLIES, (typeof COMMANDS)[P], M, F, S, RESP, TYPE_MAPPING>;
 };
 
 type WithModules<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof M as ExcludeMappedString<P>]: {
-        [C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, F, S>;
-    };
+  [P in keyof M]: {
+    [C in keyof M[P]]: CommandSignature<REPLIES, M[P][C], M, F, S, RESP, TYPE_MAPPING>;
+  };
 };
 
 type WithFunctions<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof F as ExcludeMappedString<P>]: {
-        [FF in keyof F[P] as ExcludeMappedString<FF>]: RedisClusterMultiCommandSignature<F[P][FF], M, F, S>;
-    };
+  [L in keyof F]: {
+    [C in keyof F[L]]: CommandSignature<REPLIES, F[L][C], M, F, S, RESP, TYPE_MAPPING>;
+  };
 };
 
 type WithScripts<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > = {
-    [P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, F, S>;
+  [P in keyof S]: CommandSignature<REPLIES, S[P], M, F, S, RESP, TYPE_MAPPING>;
 };
 
 export type RedisClusterMultiCommandType<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = RedisClusterMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
-
-export type InstantiableRedisClusterMultiCommandType<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
-> = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, F, S>;
-
-export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
-
-export default class RedisClusterMultiCommand {
-    readonly #multi = new RedisMultiCommand();
-    readonly #executor: RedisClusterMultiExecutor;
-    #firstKey: RedisCommandArgument | undefined;
-
-    static extend<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClusterMultiCommandType<M, F, S> {
-        return attachExtensions({
-            BaseClass: RedisClusterMultiCommand,
-            modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
-            modules: extensions?.modules,
-            functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor,
-            functions: extensions?.functions,
-            scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor,
-            scripts: extensions?.scripts
-        });
-    }
-
-    constructor(executor: RedisClusterMultiExecutor, firstKey?: RedisCommandArgument) {
-        this.#executor = executor;
-        this.#firstKey = firstKey;
-    }
-
-    commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
-        const transformedArguments = command.transformArguments(...args);
-        this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments);
-        return this.addCommand(undefined, transformedArguments, command.transformReply);
-    }
-
-    addCommand(
-        firstKey: RedisCommandArgument | undefined,
-        args: RedisCommandArguments,
-        transformReply?: RedisCommand['transformReply']
-    ): this {
-        this.#firstKey ??= firstKey;
-        this.#multi.addCommand(args, transformReply);
-        return this;
-    }
-
-    functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
-        const transformedArguments = this.#multi.addFunction(name, fn, args);
-        this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments);
-        return this;
-    }
-
-    scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
-        const transformedArguments = this.#multi.addScript(script, args);
-        this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments);
-        return this;
-    }
-
-    async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
-        if (execAsPipeline) {
-            return this.execAsPipeline();
-        }
-
-        return this.#multi.handleExecReplies(
-            await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId())
-        );
-    }
-
-    EXEC = this.exec;
-
-    async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
-        return this.#multi.transformReplies(
-            await this.#executor(this.#multi.queue, this.#firstKey)
-        );
-    }
-}
+  REPLIES extends Array<any>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (
+  RedisClusterMultiCommand<REPLIES> &
+  WithCommands<REPLIES, M, F, S, RESP, TYPE_MAPPING> & 
+  WithModules<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
+  WithFunctions<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
+  WithScripts<REPLIES, M, F, S, RESP, TYPE_MAPPING>
+);
+
+export type ClusterMultiExecute = (
+  firstKey: RedisArgument | undefined,
+  isReadonly: boolean | undefined,
+  commands: Array<RedisMultiQueuedCommand>
+) => Promise<Array<unknown>>;
+
+export default class RedisClusterMultiCommand<REPLIES = []> {
+  static #createCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return function (this: RedisClusterMultiCommand, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+      const firstKey = parser.firstKey;
+      
+      return this.addCommand(
+        firstKey,
+        command.IS_READ_ONLY,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static #createModuleCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return function (this: { _self: RedisClusterMultiCommand }, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+      const firstKey = parser.firstKey;
+
+      return this._self.addCommand(
+        firstKey,
+        command.IS_READ_ONLY,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
+    const prefix = functionArgumentsPrefix(name, fn);
+    const transformReply = getTransformReply(fn, resp);
+
+    return function (this: { _self: RedisClusterMultiCommand }, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      fn.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+      const firstKey = parser.firstKey;
+
+      return this._self.addCommand(
+        firstKey,
+        fn.IS_READ_ONLY,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  static #createScriptCommand(script: RedisScript, resp: RespVersions) {
+    const transformReply = getTransformReply(script, resp);
 
-attachCommands({
-    BaseClass: RedisClusterMultiCommand,
-    commands: COMMANDS,
-    executor: RedisClusterMultiCommand.prototype.commandsExecutor
-});
+    return function (this: RedisClusterMultiCommand, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      script.parseCommand(parser, ...args);
+
+      const scriptArgs: CommandArguments = parser.redisArgs;
+      scriptArgs.preserve = parser.preserve;
+      const firstKey = parser.firstKey;
+
+      return this.#addScript(
+        firstKey,
+        script.IS_READ_ONLY,
+        script,
+        scriptArgs,
+        transformReply
+      );
+    };
+  }
+
+  static extend<
+    M extends RedisModules = Record<string, never>,
+    F extends RedisFunctions = Record<string, never>,
+    S extends RedisScripts = Record<string, never>,
+    RESP extends RespVersions = 2
+  >(config?: CommanderConfig<M, F, S, RESP>) {
+    return attachConfig({
+      BaseClass: RedisClusterMultiCommand,
+      commands: COMMANDS,
+      createCommand: RedisClusterMultiCommand.#createCommand,
+      createModuleCommand: RedisClusterMultiCommand.#createModuleCommand,
+      createFunctionCommand: RedisClusterMultiCommand.#createFunctionCommand,
+      createScriptCommand: RedisClusterMultiCommand.#createScriptCommand,
+      config
+    });
+  }
+
+  readonly #multi: RedisMultiCommand
+
+  readonly #executeMulti: ClusterMultiExecute;
+  readonly #executePipeline: ClusterMultiExecute;
+  #firstKey: RedisArgument | undefined;
+  #isReadonly: boolean | undefined = true;
+
+  constructor(
+    executeMulti: ClusterMultiExecute,
+    executePipeline: ClusterMultiExecute,
+    routing: RedisArgument | undefined,
+    typeMapping?: TypeMapping
+  ) {
+    this.#multi = new RedisMultiCommand(typeMapping);
+    this.#executeMulti = executeMulti;
+    this.#executePipeline = executePipeline;
+    this.#firstKey = routing;
+  }
+
+  #setState(
+    firstKey: RedisArgument | undefined,
+    isReadonly: boolean | undefined,
+  ) {
+    this.#firstKey ??= firstKey;
+    this.#isReadonly &&= isReadonly;
+  }
+
+  addCommand(
+    firstKey: RedisArgument | undefined,
+    isReadonly: boolean | undefined,
+    args: CommandArguments,
+    transformReply?: TransformReply
+  ) {
+    this.#setState(firstKey, isReadonly);
+    this.#multi.addCommand(args, transformReply);
+    return this;
+  }
+
+  #addScript(
+    firstKey: RedisArgument | undefined,
+    isReadonly: boolean | undefined,
+    script: RedisScript,
+    args: CommandArguments,
+    transformReply?: TransformReply
+  ) {
+    this.#setState(firstKey, isReadonly);
+    this.#multi.addScript(script, args, transformReply);
+
+    return this;
+  }
+
+  async exec<T extends MultiReply = MULTI_REPLY['GENERIC']>(execAsPipeline = false) {
+    if (execAsPipeline) return this.execAsPipeline<T>();
+
+    return this.#multi.transformReplies(
+      await this.#executeMulti(
+        this.#firstKey,
+        this.#isReadonly,
+        this.#multi.queue
+      )
+    ) as MultiReplyType<T, REPLIES>;
+  }
+
+  EXEC = this.exec;
+
+  execTyped(execAsPipeline = false) {
+    return this.exec<MULTI_REPLY['TYPED']>(execAsPipeline);
+  }
+
+  async execAsPipeline<T extends MultiReply = MULTI_REPLY['GENERIC']>() {
+    if (this.#multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
+
+    return this.#multi.transformReplies(
+      await this.#executePipeline(
+        this.#firstKey,
+        this.#isReadonly,
+        this.#multi.queue
+      )
+    ) as MultiReplyType<T, REPLIES>;
+  }
+
+  execAsPipelineTyped() {
+    return this.execAsPipeline<MULTI_REPLY['TYPED']>();
+  }
+}
diff --git a/packages/client/lib/command-options.ts b/packages/client/lib/command-options.ts
deleted file mode 100644
index 8f91130b557..00000000000
--- a/packages/client/lib/command-options.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-const symbol = Symbol('Command Options');
-
-export type CommandOptions<T> = T & {
-    readonly [symbol]: true;
-};
-
-export function commandOptions<T>(options: T): CommandOptions<T> {
-    (options as any)[symbol] = true;
-    return options as CommandOptions<T>;
-}
-
-export function isCommandOptions<T>(options: any): options is CommandOptions<T> {
-    return options?.[symbol] === true;
-}
diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts
index c04f41e1eb0..cfdf39526cc 100644
--- a/packages/client/lib/commander.ts
+++ b/packages/client/lib/commander.ts
@@ -1,165 +1,128 @@
-
-import { ClientCommandOptions } from './client';
-import { CommandOptions, isCommandOptions } from './command-options';
-import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands';
-
-type Instantiable<T = any> = new (...args: Array<any>) => T;
-
-type CommandsExecutor<C extends RedisCommand = RedisCommand> =
-    (command: C, args: Array<unknown>, name: string) => unknown;
-
-interface AttachCommandsConfig<C extends RedisCommand> {
-    BaseClass: Instantiable;
-    commands: Record<string, C>;
-    executor: CommandsExecutor<C>;
-}
-
-export function attachCommands<C extends RedisCommand>({
-    BaseClass,
-    commands,
-    executor
-}: AttachCommandsConfig<C>): void {
-    for (const [name, command] of Object.entries(commands)) {
-        BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
-            return executor.call(this, command, args, name);
-        };
-    }
+import { Command, CommanderConfig, RedisArgument, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TransformReply } from './RESP/types';
+
+interface AttachConfigOptions<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions
+> {
+  BaseClass: new (...args: any) => any;
+  commands: RedisCommands;
+  createCommand(command: Command, resp: RespVersions): (...args: any) => any;
+  createModuleCommand(command: Command, resp: RespVersions): (...args: any) => any;
+  createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions): (...args: any) => any;
+  createScriptCommand(script: RedisScript, resp: RespVersions): (...args: any) => any;
+  config?: CommanderConfig<M, F, S, RESP>;
 }
 
-interface AttachExtensionsConfig<T extends Instantiable = Instantiable> {
-    BaseClass: T;
-    modulesExecutor: CommandsExecutor;
-    modules?: RedisModules;
-    functionsExecutor: CommandsExecutor<RedisFunction>;
-    functions?: RedisFunctions;
-    scriptsExecutor: CommandsExecutor<RedisScript>;
-    scripts?: RedisScripts;
+/* FIXME: better error message / link */
+function throwResp3SearchModuleUnstableError() {
+  throw new Error('Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance');
 }
 
-export function attachExtensions(config: AttachExtensionsConfig): any {
-    let Commander;
-
-    if (config.modules) {
-        Commander = attachWithNamespaces({
-            BaseClass: config.BaseClass,
-            namespaces: config.modules,
-            executor: config.modulesExecutor
-        });
+export function attachConfig<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions
+>({
+  BaseClass,
+  commands,
+  createCommand,
+  createModuleCommand,
+  createFunctionCommand,
+  createScriptCommand,
+  config
+}: AttachConfigOptions<M, F, S, RESP>) {
+  const RESP = config?.RESP ?? 2,
+    Class: any = class extends BaseClass {};
+
+  for (const [name, command] of Object.entries(commands)) {
+    if (config?.RESP == 3 && command.unstableResp3 && !config.unstableResp3) {
+      Class.prototype[name] = throwResp3SearchModuleUnstableError;
+    } else {
+      Class.prototype[name] = createCommand(command, RESP);
     }
+  }
+
+  if (config?.modules) {
+    for (const [moduleName, module] of Object.entries(config.modules)) {
+      const fns = Object.create(null);
+      for (const [name, command] of Object.entries(module)) {
+        if (config.RESP == 3 && command.unstableResp3 && !config.unstableResp3) {
+          fns[name] = throwResp3SearchModuleUnstableError;
+        } else {
+          fns[name] = createModuleCommand(command, RESP);
+        }
+      }
 
-    if (config.functions) {
-        Commander = attachWithNamespaces({
-            BaseClass: Commander ?? config.BaseClass,
-            namespaces: config.functions,
-            executor: config.functionsExecutor
-        });
+      attachNamespace(Class.prototype, moduleName, fns);
     }
+  }
+
+  if (config?.functions) {
+    for (const [library, commands] of Object.entries(config.functions)) {
+      const fns = Object.create(null);
+      for (const [name, command] of Object.entries(commands)) {
+        fns[name] = createFunctionCommand(name, command, RESP);
+      }
 
-    if (config.scripts) {
-        Commander ??= class extends config.BaseClass {};
-        attachCommands({
-            BaseClass: Commander,
-            commands: config.scripts,
-            executor: config.scriptsExecutor
-        });
+      attachNamespace(Class.prototype, library, fns);
     }
+  }
 
-    return Commander ?? config.BaseClass;
-}
+  if (config?.scripts) {
+    for (const [name, script] of Object.entries(config.scripts)) {
+      Class.prototype[name] = createScriptCommand(script, RESP);
+    }
+  }
 
-interface AttachWithNamespacesConfig<C extends RedisCommand> {
-    BaseClass: Instantiable;
-    namespaces: Record<string, Record<string, C>>;
-    executor: CommandsExecutor<C>;
+  return Class;
 }
 
-function attachWithNamespaces<C extends RedisCommand>({
-    BaseClass,
-    namespaces,
-    executor
-}: AttachWithNamespacesConfig<C>): any {
-    const Commander = class extends BaseClass {
-        constructor(...args: Array<any>) {
-            super(...args);
-
-            for (const namespace of Object.keys(namespaces)) {
-                this[namespace] = Object.create(this[namespace], {
-                    self: {
-                        value: this
-                    }
-                });
-            }
-        }
-    };
-
-    for (const [namespace, commands] of Object.entries(namespaces)) {
-        Commander.prototype[namespace] = {};
-        for (const [name, command] of Object.entries(commands)) {
-            Commander.prototype[namespace][name] = function (...args: Array<unknown>): unknown {
-                return executor.call(this.self, command, args, name);
-            };
-        }
+function attachNamespace(prototype: any, name: PropertyKey, fns: any) {
+  Object.defineProperty(prototype, name, {
+    get() {
+      const value = Object.create(fns);
+      value._self = this;
+      Object.defineProperty(this, name, { value });
+      return value;
     }
-
-    return Commander;
+  });
 }
 
-export function transformCommandArguments<T = ClientCommandOptions>(
-    command: RedisCommand,
-    args: Array<unknown>
-): {
-    jsArgs: Array<unknown>;
-    args: RedisCommandArguments;
-    options: CommandOptions<T> | undefined;
-} {
-    let options;
-    if (isCommandOptions<T>(args[0])) {
-        options = args[0];
-        args = args.slice(1);
-    }
+export function getTransformReply(command: Command, resp: RespVersions): TransformReply | undefined {
+  switch (typeof command.transformReply) {
+    case 'function':
+      return command.transformReply;
 
-    return {
-        jsArgs: args,
-        args: command.transformArguments(...args),
-        options
-    };
+    case 'object':
+      return command.transformReply[resp];
+  }
 }
 
-export function transformLegacyCommandArguments(args: Array<any>): Array<any> {
-    return args.flat().map(arg => {
-        return typeof arg === 'number' || arg instanceof Date ?
-            arg.toString() :
-            arg;
-    });
-}
+export function functionArgumentsPrefix(name: string, fn: RedisFunction) {
+  const prefix: Array<RedisArgument> = [
+    fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
+    name
+  ];
 
-export function transformCommandReply<C extends RedisCommand>(
-    command: C,
-    rawReply: unknown,
-    preserved: unknown
-): RedisCommandReply<C> {
-    if (!command.transformReply) {
-        return rawReply as RedisCommandReply<C>;
-    }
+  if (fn.NUMBER_OF_KEYS !== undefined) {
+    prefix.push(fn.NUMBER_OF_KEYS.toString());
+  }
 
-    return command.transformReply(rawReply, preserved);
+  return prefix;
 }
 
-export function fCallArguments(
-    name: RedisCommandArgument,
-    fn: RedisFunction,
-    args: RedisCommandArguments
-): RedisCommandArguments {
-    const actualArgs: RedisCommandArguments = [
-        fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
-        name
-    ];
-
-    if (fn.NUMBER_OF_KEYS !== undefined) {
-        actualArgs.push(fn.NUMBER_OF_KEYS.toString());
-    }
+export function scriptArgumentsPrefix(script: RedisScript) {
+  const prefix: Array<string | Buffer> = [
+    script.IS_READ_ONLY ? 'EVALSHA_RO' : 'EVALSHA',
+    script.SHA1
+  ];
 
-    actualArgs.push(...args);
+  if (script.NUMBER_OF_KEYS !== undefined) {
+    prefix.push(script.NUMBER_OF_KEYS.toString());
+  }
 
-    return actualArgs;
+  return prefix;
 }
diff --git a/packages/client/lib/commands/ACL_CAT.spec.ts b/packages/client/lib/commands/ACL_CAT.spec.ts
index 521871a1c6b..09d5ecade5a 100644
--- a/packages/client/lib/commands/ACL_CAT.spec.ts
+++ b/packages/client/lib/commands/ACL_CAT.spec.ts
@@ -1,23 +1,32 @@
-import { strict as assert } from 'assert';
-import testUtils from '../test-utils';
-import { transformArguments } from './ACL_CAT';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import { parseArgs } from './generic-transformers';
+import ACL_CAT from './ACL_CAT';
 
 describe('ACL CAT', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['ACL', 'CAT']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ACL_CAT),
+        ['ACL', 'CAT']
+      );
+    });
 
-        it('with categoryName', () => {
-            assert.deepEqual(
-                transformArguments('dangerous'),
-                ['ACL', 'CAT', 'dangerous']
-            );
-        });
+    it('with categoryName', () => {
+      assert.deepEqual(
+        parseArgs(ACL_CAT, 'dangerous'),
+        ['ACL', 'CAT', 'dangerous']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.aclCat', async client => {
+    const categories = await client.aclCat();
+    assert.ok(Array.isArray(categories));
+    for (const category of categories) {
+      assert.equal(typeof category, 'string');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_CAT.ts b/packages/client/lib/commands/ACL_CAT.ts
index 161546cfbe9..f4ddfacc68d 100644
--- a/packages/client/lib/commands/ACL_CAT.ts
+++ b/packages/client/lib/commands/ACL_CAT.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export function transformArguments(categoryName?: RedisCommandArgument): RedisCommandArguments {
-    const args: RedisCommandArguments = ['ACL', 'CAT'];
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Lists ACL categories or commands in a category
+   * @param parser - The Redis command parser
+   * @param categoryName - Optional category name to filter commands
+   */
+  parseCommand(parser: CommandParser, categoryName?: RedisArgument) {
+    parser.push('ACL', 'CAT');
     if (categoryName) {
-        args.push(categoryName);
+      parser.push(categoryName);
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_DELUSER.spec.ts b/packages/client/lib/commands/ACL_DELUSER.spec.ts
index 5c5ea2fa2a3..45fa3af9fc7 100644
--- a/packages/client/lib/commands/ACL_DELUSER.spec.ts
+++ b/packages/client/lib/commands/ACL_DELUSER.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ACL_DELUSER';
+import ACL_DELUSER from './ACL_DELUSER';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL DELUSER', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('username'),
-                ['ACL', 'DELUSER', 'username']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ACL_DELUSER, 'username'),
+        ['ACL', 'DELUSER', 'username']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ACL', 'DELUSER', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ACL_DELUSER, ['1', '2']),
+        ['ACL', 'DELUSER', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.aclDelUser', async client => {
-        assert.equal(
-            await client.aclDelUser('dosenotexists'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.aclDelUser', async client => {
+    assert.equal(
+      typeof await client.aclDelUser('user'),
+      'number'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_DELUSER.ts b/packages/client/lib/commands/ACL_DELUSER.ts
index 25ed1a10300..404641e0abb 100644
--- a/packages/client/lib/commands/ACL_DELUSER.ts
+++ b/packages/client/lib/commands/ACL_DELUSER.ts
@@ -1,10 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export function transformArguments(
-    username: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['ACL', 'DELUSER'], username);
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Deletes one or more users from the ACL
+   * @param parser - The Redis command parser
+   * @param username - Username(s) to delete
+   */
+  parseCommand(parser: CommandParser, username: RedisVariadicArgument) {
+    parser.push('ACL', 'DELUSER');
+    parser.pushVariadic(username);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_DRYRUN.spec.ts b/packages/client/lib/commands/ACL_DRYRUN.spec.ts
index 3154689c29e..38a4def8361 100644
--- a/packages/client/lib/commands/ACL_DRYRUN.spec.ts
+++ b/packages/client/lib/commands/ACL_DRYRUN.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ACL_DRYRUN';
+import ACL_DRYRUN from './ACL_DRYRUN';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL DRYRUN', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('default', ['GET', 'key']),
-            ['ACL', 'DRYRUN', 'default', 'GET', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_DRYRUN, 'default', ['GET', 'key']),
+      ['ACL', 'DRYRUN', 'default', 'GET', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.aclDryRun', async client => {
-        assert.equal(
-            await client.aclDryRun('default', ['GET', 'key']),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.aclDryRun', async client => {
+    assert.equal(
+      await client.aclDryRun('default', ['GET', 'key']),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_DRYRUN.ts b/packages/client/lib/commands/ACL_DRYRUN.ts
index 95eed95066c..49ac41a859a 100644
--- a/packages/client/lib/commands/ACL_DRYRUN.ts
+++ b/packages/client/lib/commands/ACL_DRYRUN.ts
@@ -1,18 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    username: RedisCommandArgument,
-    command: Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return [
-        'ACL',
-        'DRYRUN',
-        username,
-        ...command
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Simulates ACL operations without executing them
+   * @param parser - The Redis command parser
+   * @param username - Username to simulate ACL operations for
+   * @param command - Command arguments to simulate
+   */
+  parseCommand(parser: CommandParser, username: RedisArgument, command: Array<RedisArgument>) {
+    parser.push('ACL', 'DRYRUN', username, ...command);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply
+} as const satisfies Command;
 
diff --git a/packages/client/lib/commands/ACL_GENPASS.spec.ts b/packages/client/lib/commands/ACL_GENPASS.spec.ts
index 3b2a022f972..35e161f424f 100644
--- a/packages/client/lib/commands/ACL_GENPASS.spec.ts
+++ b/packages/client/lib/commands/ACL_GENPASS.spec.ts
@@ -1,23 +1,31 @@
-import { strict as assert } from 'assert';
-import testUtils from '../test-utils';
-import { transformArguments } from './ACL_GENPASS';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ACL_GENPASS from './ACL_GENPASS';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL GENPASS', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['ACL', 'GENPASS']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ACL_GENPASS),
+        ['ACL', 'GENPASS']
+      );
+    });
 
-        it('with bits', () => {
-            assert.deepEqual(
-                transformArguments(128),
-                ['ACL', 'GENPASS', '128']
-            );
-        });
+    it('with bits', () => {
+      assert.deepEqual(
+        parseArgs(ACL_GENPASS, 128),
+        ['ACL', 'GENPASS', '128']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.aclGenPass', async client => {
+    assert.equal(
+      typeof await client.aclGenPass(),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_GENPASS.ts b/packages/client/lib/commands/ACL_GENPASS.ts
index 91a71e220e0..d1785839a5c 100644
--- a/packages/client/lib/commands/ACL_GENPASS.ts
+++ b/packages/client/lib/commands/ACL_GENPASS.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export function transformArguments(bits?: number): RedisCommandArguments {
-    const args = ['ACL', 'GENPASS'];
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Generates a secure password for ACL users
+   * @param parser - The Redis command parser
+   * @param bits - Optional number of bits for password entropy
+   */
+  parseCommand(parser: CommandParser, bits?: number) {
+    parser.push('ACL', 'GENPASS');
     if (bits) {
-        args.push(bits.toString());
+      parser.push(bits.toString());
     }
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
 
-    return args;
-}
-
-export declare function transformReply(): RedisCommandArgument;
diff --git a/packages/client/lib/commands/ACL_GETUSER.spec.ts b/packages/client/lib/commands/ACL_GETUSER.spec.ts
index 4cd693db9ce..83776a3473a 100644
--- a/packages/client/lib/commands/ACL_GETUSER.spec.ts
+++ b/packages/client/lib/commands/ACL_GETUSER.spec.ts
@@ -1,34 +1,35 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ACL_GETUSER';
+import ACL_GETUSER from './ACL_GETUSER';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL GETUSER', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('username'),
-            ['ACL', 'GETUSER', 'username']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_GETUSER, 'username'),
+      ['ACL', 'GETUSER', 'username']
+    );
+  });
 
-    testUtils.testWithClient('client.aclGetUser', async client => {
-        const reply = await client.aclGetUser('default');
+  testUtils.testWithClient('client.aclGetUser', async client => {
+    const reply = await client.aclGetUser('default');
 
-        assert.ok(Array.isArray(reply.passwords));
-        assert.equal(typeof reply.commands, 'string');
-        assert.ok(Array.isArray(reply.flags));
+    assert.ok(Array.isArray(reply.passwords));
+    assert.equal(typeof reply.commands, 'string');
+    assert.ok(Array.isArray(reply.flags));
 
-        if (testUtils.isVersionGreaterThan([7])) {
-        assert.equal(typeof reply.keys, 'string');
-        assert.equal(typeof reply.channels, 'string');
-        assert.ok(Array.isArray(reply.selectors));
-        } else {
-        assert.ok(Array.isArray(reply.keys));
+    if (testUtils.isVersionGreaterThan([7])) {
+      assert.equal(typeof reply.keys, 'string');
+      assert.equal(typeof reply.channels, 'string');
+      assert.ok(Array.isArray(reply.selectors));
+    } else {
+      assert.ok(Array.isArray(reply.keys));
 
-        if (testUtils.isVersionGreaterThan([6, 2])) {
-            assert.ok(Array.isArray(reply.channels));
-        }
-        }
-    }, GLOBAL.SERVERS.OPEN);
+      if (testUtils.isVersionGreaterThan([6, 2])) {
+        assert.ok(Array.isArray(reply.channels));
+      }
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_GETUSER.ts b/packages/client/lib/commands/ACL_GETUSER.ts
index 818a945bac1..a1505251c6e 100644
--- a/packages/client/lib/commands/ACL_GETUSER.ts
+++ b/packages/client/lib/commands/ACL_GETUSER.ts
@@ -1,40 +1,49 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-export function transformArguments(username: RedisCommandArgument): RedisCommandArguments {
-    return ['ACL', 'GETUSER', username];
-}
+type AclUser = TuplesToMapReply<[
+  [BlobStringReply<'flags'>, ArrayReply<BlobStringReply>],
+  [BlobStringReply<'passwords'>, ArrayReply<BlobStringReply>],
+  [BlobStringReply<'commands'>, BlobStringReply],
+  /** changed to BlobStringReply in 7.0 */
+  [BlobStringReply<'keys'>, ArrayReply<BlobStringReply> | BlobStringReply],
+  /** added in 6.2, changed to BlobStringReply in 7.0 */
+  [BlobStringReply<'channels'>, ArrayReply<BlobStringReply> | BlobStringReply],
+  /** added in 7.0 */
+  [BlobStringReply<'selectors'>, ArrayReply<TuplesToMapReply<[
+    [BlobStringReply<'commands'>, BlobStringReply],
+    [BlobStringReply<'keys'>, BlobStringReply],
+    [BlobStringReply<'channels'>, BlobStringReply]
+  ]>>],
+]>;
 
-type AclGetUserRawReply = [
-    'flags',
-    Array<RedisCommandArgument>,
-    'passwords',
-    Array<RedisCommandArgument>,
-    'commands',
-    RedisCommandArgument,
-    'keys',
-    Array<RedisCommandArgument> | RedisCommandArgument,
-    'channels',
-    Array<RedisCommandArgument> | RedisCommandArgument,
-    'selectors' | undefined,
-    Array<Array<string>> | undefined
-];
-
-interface AclUser {
-    flags: Array<RedisCommandArgument>;
-    passwords: Array<RedisCommandArgument>;
-    commands: RedisCommandArgument;
-    keys: Array<RedisCommandArgument> | RedisCommandArgument;
-    channels: Array<RedisCommandArgument> | RedisCommandArgument;
-    selectors?: Array<Array<string>>;
-}
-
-export function transformReply(reply: AclGetUserRawReply): AclUser {
-    return {
-        flags: reply[1],
-        passwords: reply[3],
-        commands: reply[5],
-        keys: reply[7],
-        channels: reply[9],
-        selectors: reply[11]
-    };
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns ACL information about a specific user
+   * @param parser - The Redis command parser
+   * @param username - Username to get information for
+   */
+  parseCommand(parser: CommandParser, username: RedisArgument) {
+    parser.push('ACL', 'GETUSER', username);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<AclUser>>) => ({
+      flags: reply[1],
+      passwords: reply[3],
+      commands: reply[5],
+      keys: reply[7],
+      channels: reply[9],
+      selectors: (reply[11] as unknown as UnwrapReply<typeof reply[11]>)?.map(selector => {
+        const inferred = selector as unknown as UnwrapReply<typeof selector>;
+        return {
+          commands: inferred[1],
+          keys: inferred[3],
+          channels: inferred[5]
+        };
+      })
+    }),
+    3: undefined as unknown as () => AclUser
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_LIST.spec.ts b/packages/client/lib/commands/ACL_LIST.spec.ts
index 9f9156db7a2..0f67aaa53e9 100644
--- a/packages/client/lib/commands/ACL_LIST.spec.ts
+++ b/packages/client/lib/commands/ACL_LIST.spec.ts
@@ -1,14 +1,23 @@
-import { strict as assert } from 'assert';
-import testUtils from '../test-utils';
-import { transformArguments } from './ACL_LIST';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ACL_LIST from './ACL_LIST';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL LIST', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ACL', 'LIST']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_LIST),
+      ['ACL', 'LIST']
+    );
+  });
+
+  testUtils.testWithClient('client.aclList', async client => {
+    const users = await client.aclList();
+    assert.ok(Array.isArray(users));
+    for (const user of users) {
+      assert.equal(typeof user, 'string');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_LIST.ts b/packages/client/lib/commands/ACL_LIST.ts
index ae523fe9ce9..4d2ec995cd5 100644
--- a/packages/client/lib/commands/ACL_LIST.ts
+++ b/packages/client/lib/commands/ACL_LIST.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ACL', 'LIST'];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns all configured ACL users and their permissions
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ACL', 'LIST');
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_LOAD.spec.ts b/packages/client/lib/commands/ACL_LOAD.spec.ts
index 703d5eeb252..a41ce45e8a6 100644
--- a/packages/client/lib/commands/ACL_LOAD.spec.ts
+++ b/packages/client/lib/commands/ACL_LOAD.spec.ts
@@ -1,14 +1,15 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './ACL_SAVE';
+import ACL_LOAD from './ACL_LOAD';
+import { parseArgs } from './generic-transformers';
 
-describe('ACL SAVE', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+describe('ACL LOAD', () => {
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ACL', 'SAVE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_LOAD),
+      ['ACL', 'LOAD']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/ACL_LOAD.ts b/packages/client/lib/commands/ACL_LOAD.ts
index 88309102b95..0367904a507 100644
--- a/packages/client/lib/commands/ACL_LOAD.ts
+++ b/packages/client/lib/commands/ACL_LOAD.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ACL', 'LOAD'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Reloads ACL configuration from the ACL file
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ACL', 'LOAD');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_LOG.spec.ts b/packages/client/lib/commands/ACL_LOG.spec.ts
index a8296d31da6..7da61faca37 100644
--- a/packages/client/lib/commands/ACL_LOG.spec.ts
+++ b/packages/client/lib/commands/ACL_LOG.spec.ts
@@ -1,53 +1,51 @@
-import { strict as assert } from 'assert';
-import testUtils from '../test-utils';
-import { transformArguments, transformReply } from './ACL_LOG';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ACL_LOG from './ACL_LOG';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL LOG', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['ACL', 'LOG']
-            );
-        });
-
-        it('with count', () => {
-            assert.deepEqual(
-                transformArguments(10),
-                ['ACL', 'LOG', '10']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ACL_LOG),
+        ['ACL', 'LOG']
+      );
     });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([[
-                'count',
-                1,
-                'reason',
-                'auth',
-                'context',
-                'toplevel',
-                'object',
-                'AUTH',
-                'username',
-                'someuser',
-                'age-seconds',
-                '4.096',
-                'client-info',
-                'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
-            ]]),
-            [{
-                count: 1,
-                reason: 'auth',
-                context: 'toplevel',
-                object: 'AUTH',
-                username: 'someuser',
-                ageSeconds: 4.096,
-                clientInfo: 'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
-            }]
-        );
+    it('with count', () => {
+      assert.deepEqual(
+        parseArgs(ACL_LOG, 10),
+        ['ACL', 'LOG', '10']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.aclLog', async client => {
+    // make sure to create one log
+    await assert.rejects(
+      client.auth({
+        username: 'incorrect',
+        password: 'incorrect'
+      })
+    );
+
+    const logs = await client.aclLog();
+    assert.ok(Array.isArray(logs));
+    for (const log of logs) {
+      assert.equal(typeof log.count, 'number');
+      assert.equal(typeof log.reason, 'string');
+      assert.equal(typeof log.context, 'string');
+      assert.equal(typeof log.object, 'string');
+      assert.equal(typeof log.username, 'string');
+      assert.equal(typeof log['age-seconds'], 'number');
+      assert.equal(typeof log['client-info'], 'string');
+      if (testUtils.isVersionGreaterThan([7, 2])) {
+        assert.equal(typeof log['entry-id'], 'number');
+        assert.equal(typeof log['timestamp-created'], 'number');
+        assert.equal(typeof log['timestamp-last-updated'], 'number');
+      }
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_LOG.ts b/packages/client/lib/commands/ACL_LOG.ts
index 0fd9aa6f19d..a65f85039b1 100644
--- a/packages/client/lib/commands/ACL_LOG.ts
+++ b/packages/client/lib/commands/ACL_LOG.ts
@@ -1,50 +1,55 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types';
+import { transformDoubleReply } from './generic-transformers';
 
-export function transformArguments(count?: number): RedisCommandArguments {
-    const args = ['ACL', 'LOG'];
+export type AclLogReply = ArrayReply<TuplesToMapReply<[
+  [BlobStringReply<'count'>, NumberReply],
+  [BlobStringReply<'reason'>, BlobStringReply],
+  [BlobStringReply<'context'>, BlobStringReply],
+  [BlobStringReply<'object'>, BlobStringReply],
+  [BlobStringReply<'username'>, BlobStringReply],
+  [BlobStringReply<'age-seconds'>, DoubleReply],
+  [BlobStringReply<'client-info'>, BlobStringReply],
+  /** added in 7.0 */
+  [BlobStringReply<'entry-id'>, NumberReply],
+  /** added in 7.0 */
+  [BlobStringReply<'timestamp-created'>, NumberReply],
+  /** added in 7.0 */
+  [BlobStringReply<'timestamp-last-updated'>, NumberReply]
+]>>;
 
-    if (count) {
-        args.push(count.toString());
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns ACL security events log entries
+   * @param parser - The Redis command parser
+   * @param count - Optional maximum number of entries to return
+   */
+  parseCommand(parser: CommandParser, count?: number) {
+    parser.push('ACL', 'LOG');
+    if (count != undefined) {
+      parser.push(count.toString());
     }
-
-    return args;
-}
-
-type AclLogRawReply = [
-    _: RedisCommandArgument,
-    count: number,
-    _: RedisCommandArgument,
-    reason: RedisCommandArgument,
-    _: RedisCommandArgument,
-    context: RedisCommandArgument,
-    _: RedisCommandArgument,
-    object: RedisCommandArgument,
-    _: RedisCommandArgument,
-    username: RedisCommandArgument,
-    _: RedisCommandArgument,
-    ageSeconds: RedisCommandArgument,
-    _: RedisCommandArgument,
-    clientInfo: RedisCommandArgument
-];
-
-interface AclLog {
-    count: number;
-    reason: RedisCommandArgument;
-    context: RedisCommandArgument;
-    object: RedisCommandArgument;
-    username: RedisCommandArgument;
-    ageSeconds: number;
-    clientInfo: RedisCommandArgument;
-}
-
-export function transformReply(reply: Array<AclLogRawReply>): Array<AclLog> {
-    return reply.map(log => ({
-        count: log[1],
-        reason: log[3],
-        context: log[5],
-        object: log[7],
-        username: log[9],
-        ageSeconds: Number(log[11]),
-        clientInfo: log[13]
-    }));
-}
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<AclLogReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      return reply.map(item => {
+        const inferred = item as unknown as UnwrapReply<typeof item>;
+        return {
+          count: inferred[1],
+          reason: inferred[3],
+          context: inferred[5],
+          object: inferred[7],
+          username: inferred[9],
+          'age-seconds': transformDoubleReply[2](inferred[11], preserve, typeMapping),
+          'client-info': inferred[13],
+          'entry-id': inferred[15],
+          'timestamp-created': inferred[17],
+          'timestamp-last-updated': inferred[19]
+        };
+      })
+    },
+    3: undefined as unknown as () => AclLogReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts
index 5d26e45d04f..62d193a132d 100644
--- a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts
+++ b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts
@@ -1,14 +1,22 @@
-import { strict as assert } from 'assert';
-import testUtils from '../test-utils';
-import { transformArguments } from './ACL_LOG_RESET';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ACL_LOG_RESET from './ACL_LOG_RESET';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL LOG RESET', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ACL', 'LOG', 'RESET']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_LOG_RESET),
+      ['ACL', 'LOG', 'RESET']
+    );
+  });
+
+  testUtils.testWithClient('client.aclLogReset', async client => {
+    assert.equal(
+      await client.aclLogReset(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ACL_LOG_RESET.ts b/packages/client/lib/commands/ACL_LOG_RESET.ts
index 8ff0be4f8b9..31897c1458c 100644
--- a/packages/client/lib/commands/ACL_LOG_RESET.ts
+++ b/packages/client/lib/commands/ACL_LOG_RESET.ts
@@ -1,7 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import ACL_LOG from './ACL_LOG';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ACL', 'LOG', 'RESET'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: ACL_LOG.IS_READ_ONLY,
+  /**
+   * Clears the ACL security events log
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ACL', 'LOG', 'RESET');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_SAVE.spec.ts b/packages/client/lib/commands/ACL_SAVE.spec.ts
index f4de312bb7a..98f7c9f183d 100644
--- a/packages/client/lib/commands/ACL_SAVE.spec.ts
+++ b/packages/client/lib/commands/ACL_SAVE.spec.ts
@@ -1,14 +1,15 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './ACL_LOAD';
+import ACL_SAVE from './ACL_SAVE';
+import { parseArgs } from './generic-transformers';
 
-describe('ACL LOAD', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+describe('ACL SAVE', () => {
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ACL', 'LOAD']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_SAVE),
+      ['ACL', 'SAVE']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/ACL_SAVE.ts b/packages/client/lib/commands/ACL_SAVE.ts
index e57cd697297..c53c611ab6f 100644
--- a/packages/client/lib/commands/ACL_SAVE.ts
+++ b/packages/client/lib/commands/ACL_SAVE.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ACL', 'SAVE'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Saves the current ACL configuration to the ACL file
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ACL', 'SAVE');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_SETUSER.spec.ts b/packages/client/lib/commands/ACL_SETUSER.spec.ts
index 9c8ea8a59e0..9f39868e809 100644
--- a/packages/client/lib/commands/ACL_SETUSER.spec.ts
+++ b/packages/client/lib/commands/ACL_SETUSER.spec.ts
@@ -1,23 +1,24 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './ACL_SETUSER';
+import ACL_SETUSER from './ACL_SETUSER';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL SETUSER', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('username', 'allkeys'),
-                ['ACL', 'SETUSER', 'username', 'allkeys']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ACL_SETUSER, 'username', 'allkeys'),
+        ['ACL', 'SETUSER', 'username', 'allkeys']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('username', ['allkeys', 'allchannels']),
-                ['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ACL_SETUSER, 'username', ['allkeys', 'allchannels']),
+        ['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/ACL_SETUSER.ts b/packages/client/lib/commands/ACL_SETUSER.ts
index a12cc8ed24e..dd3d9b770f5 100644
--- a/packages/client/lib/commands/ACL_SETUSER.ts
+++ b/packages/client/lib/commands/ACL_SETUSER.ts
@@ -1,11 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export function transformArguments(
-    username: RedisCommandArgument,
-    rule: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Creates or modifies ACL user with specified rules
+   * @param parser - The Redis command parser
+   * @param username - Username to create or modify
+   * @param rule - ACL rule(s) to apply to the user
+   */
+  parseCommand(parser: CommandParser, username: RedisArgument, rule: RedisVariadicArgument) {
+    parser.push('ACL', 'SETUSER', username);
+    parser.pushVariadic(rule);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_USERS.spec.ts b/packages/client/lib/commands/ACL_USERS.spec.ts
index 35e06ce8494..d897b61e4f3 100644
--- a/packages/client/lib/commands/ACL_USERS.spec.ts
+++ b/packages/client/lib/commands/ACL_USERS.spec.ts
@@ -1,14 +1,15 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './ACL_USERS';
+import ACL_USERS from './ACL_USERS';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL USERS', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ACL', 'USERS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_USERS),
+      ['ACL', 'USERS']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/ACL_USERS.ts b/packages/client/lib/commands/ACL_USERS.ts
index 7970a262e26..92c965b9f93 100644
--- a/packages/client/lib/commands/ACL_USERS.ts
+++ b/packages/client/lib/commands/ACL_USERS.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ACL', 'USERS'];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns a list of all configured ACL usernames
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ACL', 'USERS');
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ACL_WHOAMI.spec.ts b/packages/client/lib/commands/ACL_WHOAMI.spec.ts
index 32eb327beea..f939c657a7a 100644
--- a/packages/client/lib/commands/ACL_WHOAMI.spec.ts
+++ b/packages/client/lib/commands/ACL_WHOAMI.spec.ts
@@ -1,14 +1,15 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './ACL_WHOAMI';
+import ACL_WHOAMI from './ACL_WHOAMI';
+import { parseArgs } from './generic-transformers';
 
 describe('ACL WHOAMI', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ACL', 'WHOAMI']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ACL_WHOAMI),
+      ['ACL', 'WHOAMI']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/ACL_WHOAMI.ts b/packages/client/lib/commands/ACL_WHOAMI.ts
index 3c41171638e..1d58eb2f3d3 100644
--- a/packages/client/lib/commands/ACL_WHOAMI.ts
+++ b/packages/client/lib/commands/ACL_WHOAMI.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ACL', 'WHOAMI'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the username of the current connection
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ACL', 'WHOAMI');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/APPEND.spec.ts b/packages/client/lib/commands/APPEND.spec.ts
index 23353866843..925c16917b9 100644
--- a/packages/client/lib/commands/APPEND.spec.ts
+++ b/packages/client/lib/commands/APPEND.spec.ts
@@ -1,11 +1,23 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './APPEND';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import APPEND from './APPEND';
+import { parseArgs } from './generic-transformers';
 
 describe('APPEND', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'value'),
-            ['APPEND', 'key', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(APPEND, 'key', 'value'),
+      ['APPEND', 'key', 'value']
+    );
+  });
+
+  testUtils.testAll('append', async client => {
+    assert.equal(
+      await client.append('key', 'value'),
+      5
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/APPEND.ts b/packages/client/lib/commands/APPEND.ts
index 66f7fc84798..f7ca6c83578 100644
--- a/packages/client/lib/commands/APPEND.ts
+++ b/packages/client/lib/commands/APPEND.ts
@@ -1,12 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Appends a value to a string key
+   * @param parser - The Redis command parser
+   * @param key - The key to append to
+   * @param value - The value to append
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) {
+    parser.push('APPEND', key, value);
+  },
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return ['APPEND', key, value];
-}
-
-export declare function transformReply(): number;
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ASKING.spec.ts b/packages/client/lib/commands/ASKING.spec.ts
index 3da2015199e..7be4d25d449 100644
--- a/packages/client/lib/commands/ASKING.spec.ts
+++ b/packages/client/lib/commands/ASKING.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './ASKING';
+import { strict as assert } from 'node:assert';
+import ASKING from './ASKING';
+import { parseArgs } from './generic-transformers';
 
 describe('ASKING', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ASKING']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ASKING),
+      ['ASKING']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/ASKING.ts b/packages/client/lib/commands/ASKING.ts
index 8a87806fe62..5484b2ccc62 100644
--- a/packages/client/lib/commands/ASKING.ts
+++ b/packages/client/lib/commands/ASKING.ts
@@ -1,7 +1,17 @@
-import { RedisCommandArguments, RedisCommandArgument } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['ASKING'];
-}
+export const ASKING_CMD = 'ASKING';
 
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Tells a Redis cluster node that the client is ok receiving such redirects
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push(ASKING_CMD);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/AUTH.spec.ts b/packages/client/lib/commands/AUTH.spec.ts
index 1907488346e..762dd24f16a 100644
--- a/packages/client/lib/commands/AUTH.spec.ts
+++ b/packages/client/lib/commands/AUTH.spec.ts
@@ -1,25 +1,26 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './AUTH';
+import { strict as assert } from 'node:assert';
+import AUTH from './AUTH';
+import { parseArgs } from './generic-transformers';
 
 describe('AUTH', () => {
-    describe('transformArguments', () => {
-        it('password only', () => {
-            assert.deepEqual(
-                transformArguments({
-                    password: 'password'
-                }),
-                ['AUTH', 'password']
-            );
-        });
+  describe('transformArguments', () => {
+    it('password only', () => {
+      assert.deepEqual(
+        parseArgs(AUTH, {
+          password: 'password'
+        }),
+        ['AUTH', 'password']
+      );
+    });
 
-        it('username & password', () => {
-            assert.deepEqual(
-                transformArguments({
-                    username: 'username',
-                    password: 'password'
-                }),
-                ['AUTH', 'username', 'password']
-            );
-        });
+    it('username & password', () => {
+      assert.deepEqual(
+        parseArgs(AUTH, {
+          username: 'username',
+          password: 'password'
+        }),
+        ['AUTH', 'username', 'password']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/AUTH.ts b/packages/client/lib/commands/AUTH.ts
index 49b0df6d313..40ed45bc936 100644
--- a/packages/client/lib/commands/AUTH.ts
+++ b/packages/client/lib/commands/AUTH.ts
@@ -1,16 +1,27 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
 export interface AuthOptions {
-    username?: RedisCommandArgument;
-    password: RedisCommandArgument;
+  username?: RedisArgument;
+  password: RedisArgument;
 }
 
-export function transformArguments({ username, password }: AuthOptions): RedisCommandArguments {
-    if (!username) {
-        return ['AUTH', password];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Authenticates the connection using a password or username and password
+   * @param parser - The Redis command parser
+   * @param options - Authentication options containing username and/or password
+   * @param options.username - Optional username for authentication
+   * @param options.password - Password for authentication
+   */
+  parseCommand(parser: CommandParser, { username, password }: AuthOptions) {
+    parser.push('AUTH');
+    if (username !== undefined) {
+      parser.push(username);
     }
-
-    return ['AUTH', username, password];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+    parser.push(password);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BGREWRITEAOF.spec.ts b/packages/client/lib/commands/BGREWRITEAOF.spec.ts
index d0e150e155b..f58ec9a5762 100644
--- a/packages/client/lib/commands/BGREWRITEAOF.spec.ts
+++ b/packages/client/lib/commands/BGREWRITEAOF.spec.ts
@@ -1,11 +1,20 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './BGREWRITEAOF';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import BGREWRITEAOF from './BGREWRITEAOF';
+import { parseArgs } from './generic-transformers';
 
 describe('BGREWRITEAOF', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['BGREWRITEAOF']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(BGREWRITEAOF),
+      ['BGREWRITEAOF']
+    );
+  });
+
+  testUtils.testWithClient('client.bgRewriteAof', async client => {
+    assert.equal(
+      typeof await client.bgRewriteAof(),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/BGREWRITEAOF.ts b/packages/client/lib/commands/BGREWRITEAOF.ts
index be4ec2546ab..2aa4c6b7b89 100644
--- a/packages/client/lib/commands/BGREWRITEAOF.ts
+++ b/packages/client/lib/commands/BGREWRITEAOF.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['BGREWRITEAOF'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Asynchronously rewrites the append-only file
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('BGREWRITEAOF');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BGSAVE.spec.ts b/packages/client/lib/commands/BGSAVE.spec.ts
index 8e4de5eef5b..dcf7b815119 100644
--- a/packages/client/lib/commands/BGSAVE.spec.ts
+++ b/packages/client/lib/commands/BGSAVE.spec.ts
@@ -1,23 +1,33 @@
-import { strict as assert } from 'assert';
-import { describe } from 'mocha';
-import { transformArguments } from './BGSAVE';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import BGSAVE from './BGSAVE';
+import { parseArgs } from './generic-transformers';
 
 describe('BGSAVE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['BGSAVE']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(BGSAVE),
+        ['BGSAVE']
+      );
+    });
 
-        it('with SCHEDULE', () => {
-            assert.deepEqual(
-                transformArguments({
-                    SCHEDULE: true
-                }),
-                ['BGSAVE', 'SCHEDULE']
-            );
-        });
+    it('with SCHEDULE', () => {
+      assert.deepEqual(
+        parseArgs(BGSAVE, {
+          SCHEDULE: true
+        }),
+        ['BGSAVE', 'SCHEDULE']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.bgSave', async client => {
+    assert.equal(
+      typeof await client.bgSave({
+        SCHEDULE: true // using `SCHEDULE` to make sure it won't throw an error
+      }),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/BGSAVE.ts b/packages/client/lib/commands/BGSAVE.ts
index 9c90f3485be..cad8ad5393e 100644
--- a/packages/client/lib/commands/BGSAVE.ts
+++ b/packages/client/lib/commands/BGSAVE.ts
@@ -1,17 +1,24 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-interface BgSaveOptions {
-    SCHEDULE?: true;
+export interface BgSaveOptions {
+  SCHEDULE?: boolean;
 }
 
-export function transformArguments(options?: BgSaveOptions): RedisCommandArguments {
-    const args = ['BGSAVE'];
-
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Asynchronously saves the dataset to disk
+   * @param parser - The Redis command parser
+   * @param options - Optional configuration
+   * @param options.SCHEDULE - Schedule a BGSAVE operation when no BGSAVE is already in progress
+   */
+  parseCommand(parser: CommandParser, options?: BgSaveOptions) {
+    parser.push('BGSAVE');
     if (options?.SCHEDULE) {
-        args.push('SCHEDULE');
+      parser.push('SCHEDULE');
     }
-
-    return args;
-}
-
-export declare function transformReply(): RedisCommandArgument;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BITCOUNT.spec.ts b/packages/client/lib/commands/BITCOUNT.spec.ts
index 76e7b03f7c9..e2990472948 100644
--- a/packages/client/lib/commands/BITCOUNT.spec.ts
+++ b/packages/client/lib/commands/BITCOUNT.spec.ts
@@ -1,44 +1,48 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BITCOUNT';
+import BITCOUNT from './BITCOUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('BITCOUNT', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['BITCOUNT', 'key']
-            );
-        });
+  describe('parseCommand', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(BITCOUNT, 'key'),
+        ['BITCOUNT', 'key']
+      );
+    });
 
-        describe('with range', () => {
-            it('simple', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        start: 0,
-                        end: 1
-                    }),
-                    ['BITCOUNT', 'key', '0', '1']
-                );
-            });
+    describe('with range', () => {
+      it('simple', () => {
+        assert.deepEqual(
+          parseArgs(BITCOUNT, 'key', {
+            start: 0,
+            end: 1
+          }),
+          ['BITCOUNT', 'key', '0', '1']
+        );
+      });
 
-            it('with mode', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        start: 0,
-                        end: 1,
-                        mode: 'BIT'
-                    }),
-                    ['BITCOUNT', 'key', '0', '1', 'BIT']
-                );
-            });
-        });
+      it('with mode', () => {
+        assert.deepEqual(
+          parseArgs(BITCOUNT, 'key', {
+            start: 0,
+            end: 1,
+            mode: 'BIT'
+          }),
+          ['BITCOUNT', 'key', '0', '1', 'BIT']
+        );
+      });
     });
+  });
 
-    testUtils.testWithClient('client.bitCount', async client => {
-        assert.equal(
-            await client.bitCount('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('bitCount', async client => {
+    assert.equal(
+      await client.bitCount('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BITCOUNT.ts b/packages/client/lib/commands/BITCOUNT.ts
index 4bbd4f00911..a8d7295ff3c 100644
--- a/packages/client/lib/commands/BITCOUNT.ts
+++ b/packages/client/lib/commands/BITCOUNT.ts
@@ -1,33 +1,35 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface BitCountRange {
-    start: number;
-    end: number;
-    mode?: 'BYTE' | 'BIT';
+export interface BitCountRange {
+  start: number;
+  end: number;
+  mode?: 'BYTE' | 'BIT';
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    range?: BitCountRange
-): RedisCommandArguments {
-    const args = ['BITCOUNT', key];
-
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the count of set bits in a string key
+   * @param parser - The Redis command parser
+   * @param key - The key to count bits in
+   * @param range - Optional range specification
+   * @param range.start - Start offset in bytes/bits
+   * @param range.end - End offset in bytes/bits
+   * @param range.mode - Optional counting mode: BYTE or BIT
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, range?: BitCountRange) {
+    parser.push('BITCOUNT');
+    parser.pushKey(key);
     if (range) {
-        args.push(
-            range.start.toString(),
-            range.end.toString()
-        );
+      parser.push(range.start.toString());
+      parser.push(range.end.toString());
 
-        if (range.mode) {
-            args.push(range.mode);
-        }
+      if (range.mode) {
+        parser.push(range.mode);
+      }
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BITFIELD.spec.ts b/packages/client/lib/commands/BITFIELD.spec.ts
index aaf0f93e501..5fcc112466b 100644
--- a/packages/client/lib/commands/BITFIELD.spec.ts
+++ b/packages/client/lib/commands/BITFIELD.spec.ts
@@ -1,46 +1,56 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BITFIELD';
+import BITFIELD from './BITFIELD';
+import { parseArgs } from './generic-transformers';
 
 describe('BITFIELD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [{
-                operation: 'OVERFLOW',
-                behavior: 'WRAP'
-            }, {
-                operation: 'GET',
-                encoding: 'i8',
-                offset: 0
-            }, {
-                operation: 'OVERFLOW',
-                behavior: 'SAT'
-            }, {
-                operation: 'SET',
-                encoding: 'i16',
-                offset: 1,
-                value: 0
-            }, {
-                operation: 'OVERFLOW',
-                behavior: 'FAIL'
-            }, {
-                operation: 'INCRBY',
-                encoding: 'i32',
-                offset: 2,
-                increment: 1
-            }]),
-            ['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(BITFIELD, 'key', [{
+        operation: 'OVERFLOW',
+        behavior: 'WRAP'
+      }, {
+        operation: 'GET',
+        encoding: 'i8',
+        offset: 0
+      }, {
+        operation: 'OVERFLOW',
+        behavior: 'SAT'
+      }, {
+        operation: 'SET',
+        encoding: 'i16',
+        offset: 1,
+        value: 0
+      }, {
+        operation: 'OVERFLOW',
+        behavior: 'FAIL'
+      }, {
+        operation: 'INCRBY',
+        encoding: 'i32',
+        offset: 2,
+        increment: 1
+      }]),
+      ['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.bitField', async client => {
-        assert.deepEqual(
-            await client.bitField('key', [{
-                operation: 'GET',
-                encoding: 'i8',
-                offset: 0
-            }]),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('bitField', async client => {
+    const a = client.bitField('key', [{
+      operation: 'GET',
+      encoding: 'i8',
+      offset: 0
+    }]);
+    
+    assert.deepEqual(
+      await client.bitField('key', [{
+        operation: 'GET',
+        encoding: 'i8',
+        offset: 0
+      }]),
+      [0]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BITFIELD.ts b/packages/client/lib/commands/BITFIELD.ts
index 6a477b89f01..984c90e270e 100644
--- a/packages/client/lib/commands/BITFIELD.ts
+++ b/packages/client/lib/commands/BITFIELD.ts
@@ -1,80 +1,92 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '../RESP/types';
 
 export type BitFieldEncoding = `${'i' | 'u'}${number}`;
 
 export interface BitFieldOperation<S extends string> {
-    operation: S;
+  operation: S;
 }
 
 export interface BitFieldGetOperation extends BitFieldOperation<'GET'> {
-    encoding: BitFieldEncoding;
-    offset: number | string;
+  encoding: BitFieldEncoding;
+  offset: number | string;
 }
 
-interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
-    encoding: BitFieldEncoding;
-    offset: number | string;
-    value: number;
+export interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
+  encoding: BitFieldEncoding;
+  offset: number | string;
+  value: number;
 }
 
-interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
-    encoding: BitFieldEncoding;
-    offset: number | string;
-    increment: number;
+export interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
+  encoding: BitFieldEncoding;
+  offset: number | string;
+  increment: number;
 }
 
-interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
-    behavior: string;
+export interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
+  behavior: string;
 }
 
-type BitFieldOperations = Array<
-    BitFieldGetOperation |
-    BitFieldSetOperation |
-    BitFieldIncrByOperation |
-    BitFieldOverflowOperation
+export type BitFieldOperations = Array<
+  BitFieldGetOperation |
+  BitFieldSetOperation |
+  BitFieldIncrByOperation |
+  BitFieldOverflowOperation
 >;
 
-export function transformArguments(key: string, operations: BitFieldOperations): Array<string> {
-    const args = ['BITFIELD', key];
+export type BitFieldRoOperations = Array<
+  Omit<BitFieldGetOperation, 'operation'>
+>;
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Performs arbitrary bitfield integer operations on strings
+   * @param parser - The Redis command parser
+   * @param key - The key holding the string
+   * @param operations - Array of bitfield operations to perform: GET, SET, INCRBY or OVERFLOW
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, operations: BitFieldOperations) {
+    parser.push('BITFIELD');
+    parser.pushKey(key);
 
     for (const options of operations) {
-        switch (options.operation) {
-            case 'GET':
-                args.push(
-                    'GET',
-                    options.encoding,
-                    options.offset.toString()
-                );
-                break;
+      switch (options.operation) {
+        case 'GET':
+          parser.push(
+            'GET',
+            options.encoding,
+            options.offset.toString()
+          );
+          break;
 
-            case 'SET':
-                args.push(
-                    'SET',
-                    options.encoding,
-                    options.offset.toString(),
-                    options.value.toString()
-                );
-                break;
+        case 'SET':
+          parser.push(
+            'SET',
+            options.encoding,
+            options.offset.toString(),
+            options.value.toString()
+          );
+          break;
 
-            case 'INCRBY':
-                args.push(
-                    'INCRBY',
-                    options.encoding,
-                    options.offset.toString(),
-                    options.increment.toString()
-                );
-                break;
+        case 'INCRBY':
+          parser.push(
+            'INCRBY',
+            options.encoding,
+            options.offset.toString(),
+            options.increment.toString()
+          );
+          break;
 
-            case 'OVERFLOW':
-                args.push(
-                    'OVERFLOW',
-                    options.behavior
-                );
-                break;
-        }
+        case 'OVERFLOW':
+          parser.push(
+            'OVERFLOW',
+            options.behavior
+          );
+          break;
+      }
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<number | null>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BITFIELD_RO.spec.ts b/packages/client/lib/commands/BITFIELD_RO.spec.ts
index 98399d5f235..f2c1797412f 100644
--- a/packages/client/lib/commands/BITFIELD_RO.spec.ts
+++ b/packages/client/lib/commands/BITFIELD_RO.spec.ts
@@ -1,27 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BITFIELD_RO';
+import BITFIELD_RO from './BITFIELD_RO';
+import { parseArgs } from './generic-transformers';
 
-describe('BITFIELD RO', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+describe('BITFIELD_RO', () => {
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', [{
-                encoding: 'i8',
-                offset: 0
-            }]),
-            ['BITFIELD_RO', 'key', 'GET', 'i8', '0']
-        );
-    });
+  it('parseCommand', () => {
+    assert.deepEqual(
+      parseArgs(BITFIELD_RO, 'key', [{
+        encoding: 'i8',
+        offset: 0
+      }]),
+      ['BITFIELD_RO', 'key', 'GET', 'i8', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.bitFieldRo', async client => {
-        assert.deepEqual(
-            await client.bitFieldRo('key', [{
-                encoding: 'i8',
-                offset: 0
-            }]),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('bitFieldRo', async client => {
+    assert.deepEqual(
+      await client.bitFieldRo('key', [{
+        encoding: 'i8',
+        offset: 0
+      }]),
+      [0]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BITFIELD_RO.ts b/packages/client/lib/commands/BITFIELD_RO.ts
index efd4eac188e..e81a3ce4fbe 100644
--- a/packages/client/lib/commands/BITFIELD_RO.ts
+++ b/packages/client/lib/commands/BITFIELD_RO.ts
@@ -1,26 +1,29 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { BitFieldGetOperation } from './BITFIELD';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-type BitFieldRoOperations = Array<
-    Omit<BitFieldGetOperation, 'operation'> &
-    Partial<Pick<BitFieldGetOperation, 'operation'>>
+export type BitFieldRoOperations = Array<
+  Omit<BitFieldGetOperation, 'operation'>
 >;
 
-export function transformArguments(key: string, operations: BitFieldRoOperations): Array<string> {
-    const args = ['BITFIELD_RO', key];
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Performs read-only bitfield integer operations on strings
+   * @param parser - The Redis command parser
+   * @param key - The key holding the string
+   * @param operations - Array of GET operations to perform on the bitfield
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, operations: BitFieldRoOperations) {
+    parser.push('BITFIELD_RO');
+    parser.pushKey(key);
 
     for (const operation of operations) {
-        args.push(
-            'GET',
-            operation.encoding,
-            operation.offset.toString()
-        );
+      parser.push('GET');
+      parser.push(operation.encoding);
+      parser.push(operation.offset.toString())
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<number | null>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BITOP.spec.ts b/packages/client/lib/commands/BITOP.spec.ts
index 554530d56f4..65fe6f86338 100644
--- a/packages/client/lib/commands/BITOP.spec.ts
+++ b/packages/client/lib/commands/BITOP.spec.ts
@@ -1,35 +1,96 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BITOP';
+import BITOP, { BitOperations } from './BITOP';
+import { parseArgs } from './generic-transformers';
 
 describe('BITOP', () => {
-    describe('transformArguments', () => {
-        it('single key', () => {
-            assert.deepEqual(
-                transformArguments('AND', 'destKey', 'key'),
-                ['BITOP', 'AND', 'destKey', 'key']
-            );
-        });
-
-        it('multiple keys', () => {
-            assert.deepEqual(
-                transformArguments('AND', 'destKey', ['1', '2']),
-                ['BITOP', 'AND', 'destKey', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single key', () => {
+      assert.deepEqual(
+        parseArgs(BITOP, 'AND', 'destKey', 'key'),
+        ['BITOP', 'AND', 'destKey', 'key']
+      );
     });
 
-    testUtils.testWithClient('client.bitOp', async client => {
+    it('multiple keys', () => {
+      assert.deepEqual(
+        parseArgs(BITOP, 'AND', 'destKey', ['1', '2']),
+        ['BITOP', 'AND', 'destKey', '1', '2']
+      );
+    });
+  });
+
+  for (const op of ['AND', 'OR', 'XOR'] as BitOperations[]) {
+    testUtils.testAll(`bitOp ${op} with non-existing keys`, async client => {
+      assert.equal(
+        await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']),
+        0
+      );
+      }, {
+        client: GLOBAL.SERVERS.OPEN,
+        cluster: GLOBAL.CLUSTERS.OPEN
+    });
+
+    testUtils.testAll(`bitOp ${op} with existing keys`, async client => {
+        await client.set('{tag}key1', 'value1');
+        await client.set('{tag}key2', 'value2');
+
         assert.equal(
-            await client.bitOp('AND', 'destKey', 'key'),
-            0
+          await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']),
+          6
         );
-    }, GLOBAL.SERVERS.OPEN);
+      }, {
+        client: GLOBAL.SERVERS.OPEN,
+        cluster: GLOBAL.CLUSTERS.OPEN
+    });
+  }
+  
+  // NOT operation requires only one key
+  testUtils.testAll('bitOp NOT with non-existing keys', async client => {
+    assert.equal(
+      await client.bitOp('NOT', '{tag}destKey', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('bitOp NOT with existing keys', async client => {
+    await client.set('{tag}key', 'value');
+
+    assert.equal(
+      await client.bitOp('NOT', '{tag}destKey', '{tag}key'),
+      5
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  // newer operations supported since Redis 8.2
+  for (const op of ['DIFF', 'DIFF1', 'ANDOR', 'ONE'] as BitOperations[]) {
+    testUtils.testAll(`bitOp ${op} with non-existing keys`, async client => {
+      assert.equal(
+        await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']),
+        0
+      );
+      }, {
+        client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+        cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    });
+
+    testUtils.testAll(`bitOp ${op} with existing keys`, async client => {
+        await client.set('{tag}key1', 'value1');
+        await client.set('{tag}key2', 'value2');
 
-    testUtils.testWithCluster('cluster.bitOp', async cluster => {
         assert.equal(
-            await cluster.bitOp('AND', '{tag}destKey', '{tag}key'),
-            0
+          await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']),
+          6
         );
-    }, GLOBAL.CLUSTERS.OPEN);
+      }, {
+        client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+        cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    });
+  }
 });
diff --git a/packages/client/lib/commands/BITOP.ts b/packages/client/lib/commands/BITOP.ts
index e2953303d41..da8b97ceda6 100644
--- a/packages/client/lib/commands/BITOP.ts
+++ b/packages/client/lib/commands/BITOP.ts
@@ -1,16 +1,27 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
+export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT' | 'DIFF' | 'DIFF1' | 'ANDOR' | 'ONE';
 
-type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
-
-export function transformArguments(
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Performs bitwise operations between strings
+   * @param parser - The Redis command parser
+   * @param operation - Bitwise operation to perform: AND, OR, XOR, NOT, DIFF, DIFF1, ANDOR, ONE
+   * @param destKey - Destination key to store the result
+   * @param key - Source key(s) to perform operation on
+   */
+  parseCommand(
+    parser: CommandParser,
     operation: BitOperations,
-    destKey: RedisCommandArgument,
-    key: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['BITOP', operation, destKey], key);
-}
-
-export declare function transformReply(): number;
+    destKey: RedisArgument,
+    key: RedisVariadicArgument
+  ) {
+      parser.push('BITOP', operation);
+      parser.pushKey(destKey);
+      parser.pushKeys(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BITPOS.spec.ts b/packages/client/lib/commands/BITPOS.spec.ts
index 2a0758fe5da..c699deab83c 100644
--- a/packages/client/lib/commands/BITPOS.spec.ts
+++ b/packages/client/lib/commands/BITPOS.spec.ts
@@ -1,49 +1,46 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BITPOS';
+import BITPOS from './BITPOS';
+import { parseArgs } from './generic-transformers';
 
 describe('BITPOS', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['BITPOS', 'key', '1']
-            );
-        });
-
-        it('with start', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 1),
-                ['BITPOS', 'key', '1', '1']
-            );
-        });
+  describe('parseCommand', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(BITPOS, 'key', 1),
+        ['BITPOS', 'key', '1']
+      );
+    });
 
-        it('with start and end', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 1, -1),
-                ['BITPOS', 'key', '1', '1', '-1']
-            );
-        });
+    it('with start', () => {
+      assert.deepEqual(
+        parseArgs(BITPOS, 'key', 1, 1),
+        ['BITPOS', 'key', '1', '1']
+      );
+    });
 
-        it('with start, end and mode', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 1, -1, 'BIT'),
-                ['BITPOS', 'key', '1', '1', '-1', 'BIT']
-            );
-        });
+    it('with start and end', () => {
+      assert.deepEqual(
+        parseArgs(BITPOS, 'key', 1, 1, -1),
+        ['BITPOS', 'key', '1', '1', '-1']
+      );
     });
 
-    testUtils.testWithClient('client.bitPos', async client => {
-        assert.equal(
-            await client.bitPos('key', 1, 1),
-            -1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with start, end and mode', () => {
+      assert.deepEqual(
+        parseArgs(BITPOS, 'key', 1, 1, -1, 'BIT'),
+        ['BITPOS', 'key', '1', '1', '-1', 'BIT']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.bitPos', async cluster => {
-        assert.equal(
-            await cluster.bitPos('key', 1, 1),
-            -1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('bitPos', async client => {
+    assert.equal(
+      await client.bitPos('key', 1, 1),
+      -1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BITPOS.ts b/packages/client/lib/commands/BITPOS.ts
index a9a035fd9f2..cbc1aeaf610 100644
--- a/packages/client/lib/commands/BITPOS.ts
+++ b/packages/client/lib/commands/BITPOS.ts
@@ -1,32 +1,41 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 import { BitValue } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the position of first bit set to 0 or 1 in a string
+   * @param parser - The Redis command parser
+   * @param key - The key holding the string
+   * @param bit - The bit value to look for (0 or 1)
+   * @param start - Optional starting position in bytes/bits
+   * @param end - Optional ending position in bytes/bits
+   * @param mode - Optional counting mode: BYTE or BIT
+   */
+  parseCommand(parser: CommandParser,
+    key: RedisArgument,
     bit: BitValue,
     start?: number,
     end?: number,
     mode?: 'BYTE' | 'BIT'
-): RedisCommandArguments {
-    const args = ['BITPOS', key, bit.toString()];
+  ) {
+    parser.push('BITPOS');
+    parser.pushKey(key);
+    parser.push(bit.toString());
 
-    if (typeof start === 'number') {
-        args.push(start.toString());
+    if (start !== undefined) {
+      parser.push(start.toString());
     }
 
-    if (typeof end === 'number') {
-        args.push(end.toString());
+    if (end !== undefined) {
+      parser.push(end.toString());
     }
 
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BLMOVE.spec.ts b/packages/client/lib/commands/BLMOVE.spec.ts
index 3b86c1ec91e..d4e9e024a8c 100644
--- a/packages/client/lib/commands/BLMOVE.spec.ts
+++ b/packages/client/lib/commands/BLMOVE.spec.ts
@@ -1,43 +1,36 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BLMOVE';
-import { commandOptions } from '../../index';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BLMOVE from './BLMOVE';
+import { parseArgs } from './generic-transformers';
 
 describe('BLMOVE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
-            ['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(BLMOVE, 'source', 'destination', 'LEFT', 'RIGHT', 0),
+      ['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.blMove', async client => {
-        const [blMoveReply] = await Promise.all([
-            client.blMove(commandOptions({
-                isolated: true
-            }), 'source', 'destination', 'LEFT', 'RIGHT', 0),
-            client.lPush('source', 'element')
-        ]);
+  testUtils.testAll('blMove - null', async client => {
+    assert.equal(
+      await client.blMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT', BLOCKING_MIN_VALUE),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 
-        assert.equal(
-            blMoveReply,
-            'element'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.blMove', async cluster => {
-        const [blMoveReply] = await Promise.all([
-            cluster.blMove(commandOptions({
-                isolated: true
-            }), '{tag}source', '{tag}destination', 'LEFT', 'RIGHT', 0),
-            cluster.lPush('{tag}source', 'element')
-        ]);
-
-        assert.equal(
-            blMoveReply,
-            'element'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('blMove - with member', async client => {
+    const [, reply] = await Promise.all([
+      client.lPush('{tag}source', 'element'),
+      client.blMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT', BLOCKING_MIN_VALUE)
+    ]);
+    assert.equal(reply, 'element');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BLMOVE.ts b/packages/client/lib/commands/BLMOVE.ts
index ee808e70fcc..c2b04301f84 100644
--- a/packages/client/lib/commands/BLMOVE.ts
+++ b/packages/client/lib/commands/BLMOVE.ts
@@ -1,23 +1,29 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 import { ListSide } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    source: RedisCommandArgument,
-    destination: RedisCommandArgument,
-    sourceDirection: ListSide,
-    destinationDirection: ListSide,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Pop an element from a list, push it to another list and return it; or block until one is available
+   * @param parser - The Redis command parser
+   * @param source - Key of the source list
+   * @param destination - Key of the destination list
+   * @param sourceSide - Side of source list to pop from (LEFT or RIGHT)
+   * @param destinationSide - Side of destination list to push to (LEFT or RIGHT)
+   * @param timeout - Timeout in seconds, 0 to block indefinitely
+   */
+  parseCommand(
+    parser: CommandParser,
+    source: RedisArgument,
+    destination: RedisArgument,
+    sourceSide: ListSide,
+    destinationSide: ListSide,
     timeout: number
-): RedisCommandArguments {
-    return [
-        'BLMOVE',
-        source,
-        destination,
-        sourceDirection,
-        destinationDirection,
-        timeout.toString()
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+  ) {
+    parser.push('BLMOVE');
+    parser.pushKeys([source, destination]);
+    parser.push(sourceSide, destinationSide, timeout.toString())
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BLMPOP.spec.ts b/packages/client/lib/commands/BLMPOP.spec.ts
index 15853a771b0..6cda524b50f 100644
--- a/packages/client/lib/commands/BLMPOP.spec.ts
+++ b/packages/client/lib/commands/BLMPOP.spec.ts
@@ -1,32 +1,50 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BLMPOP';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BLMPOP from './BLMPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('BLMPOP', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(0, 'key', 'LEFT'),
-                ['BLMPOP', '0', '1', 'key', 'LEFT']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(BLMPOP, 0, 'key', 'LEFT'),
+        ['BLMPOP', '0', '1', 'key', 'LEFT']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments(0, 'key', 'LEFT', {
-                    COUNT: 2
-                }),
-                ['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '2']
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(BLMPOP, 0, 'key', 'LEFT', {
+          COUNT: 1
+        }),
+        ['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '1']
+      );
     });
+  });
+
+  testUtils.testAll('blmPop - null', async client => {
+    assert.equal(
+      await client.blmPop(BLOCKING_MIN_VALUE, 'key', 'RIGHT'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 
-    testUtils.testWithClient('client.blmPop', async client => {
-        assert.deepEqual(
-            await client.blmPop(1, 'key', 'RIGHT'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('blmPop - with member', async client => {
+    const [, reply] = await Promise.all([
+      client.lPush('key', 'element'),
+      client.blmPop(BLOCKING_MIN_VALUE, 'key', 'RIGHT')
+    ]);
+    assert.deepEqual(reply, [
+      'key',
+      ['element']
+    ]);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });  
 });
diff --git a/packages/client/lib/commands/BLMPOP.ts b/packages/client/lib/commands/BLMPOP.ts
index 11bfad8b99b..b858218b580 100644
--- a/packages/client/lib/commands/BLMPOP.ts
+++ b/packages/client/lib/commands/BLMPOP.ts
@@ -1,20 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { Command } from '../RESP/types';
+import LMPOP, { LMPopArguments, parseLMPopArguments } from './LMPOP';
 
-export const FIRST_KEY_INDEX = 3;
-
-export function transformArguments(
-    timeout: number,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    side: ListSide,
-    options?: LMPopOptions
-): RedisCommandArguments {
-    return transformLMPopArguments(
-        ['BLMPOP', timeout.toString()],
-        keys,
-        side,
-        options
-    );
-}
-
-export { transformReply } from './LMPOP';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Pops elements from multiple lists; blocks until elements are available
+   * @param parser - The Redis command parser
+   * @param timeout - Timeout in seconds, 0 to block indefinitely
+   * @param args - Additional arguments for LMPOP command
+   */
+  parseCommand(parser: CommandParser, timeout: number, ...args: LMPopArguments) {
+    parser.push('BLMPOP', timeout.toString());
+    parseLMPopArguments(parser, ...args);
+  }, 
+  transformReply: LMPOP.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BLPOP.spec.ts b/packages/client/lib/commands/BLPOP.spec.ts
index 84920c851e1..1bb53a774b7 100644
--- a/packages/client/lib/commands/BLPOP.spec.ts
+++ b/packages/client/lib/commands/BLPOP.spec.ts
@@ -1,79 +1,47 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './BLPOP';
-import { commandOptions } from '../../index';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BLPOP from './BLPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('BLPOP', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['BLPOP', 'key', '0']
-            );
-        });
-
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments(['key1', 'key2'], 0),
-                ['BLPOP', 'key1', 'key2', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(BLPOP, 'key', 0),
+        ['BLPOP', 'key', '0']
+      );
     });
 
-    describe('transformReply', () => {
-        it('null', () => {
-            assert.equal(
-                transformReply(null),
-                null
-            );
-        });
-
-        it('member', () => {
-            assert.deepEqual(
-                transformReply(['key', 'element']),
-                {
-                    key: 'key',
-                    element: 'element'
-                }
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(BLPOP, ['1', '2'], 0),
+        ['BLPOP', '1', '2', '0']
+      );
     });
-
-    testUtils.testWithClient('client.blPop', async client => {
-        const [ blPopReply ] = await Promise.all([
-            client.blPop(
-                commandOptions({ isolated: true }),
-                'key',
-                1
-            ),
-            client.lPush('key', 'element'),
-        ]);
-
-        assert.deepEqual(
-            blPopReply,
-            {
-                key: 'key',
-                element: 'element'
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.blPop', async cluster => {
-        const [ blPopReply ] = await Promise.all([
-            cluster.blPop(
-                commandOptions({ isolated: true }),
-                'key',
-                1
-            ),
-            cluster.lPush('key', 'element')
-        ]);
-
-        assert.deepEqual(
-            blPopReply,
-            {
-                key: 'key',
-                element: 'element'
-            }
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  });
+
+  testUtils.testAll('blPop - null', async client => {
+    assert.equal(
+      await client.blPop('key', BLOCKING_MIN_VALUE),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('blPop - with member', async client => {
+    const [, reply] = await Promise.all([
+      client.lPush('key', 'element'),
+      client.blPop('key', 1)
+    ]);
+
+    assert.deepEqual(reply, {
+      key: 'key',
+      element: 'element'
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BLPOP.ts b/packages/client/lib/commands/BLPOP.ts
index 46ef41ad6f0..6ffccc6417c 100644
--- a/packages/client/lib/commands/BLPOP.ts
+++ b/packages/client/lib/commands/BLPOP.ts
@@ -1,31 +1,26 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    timeout: number
-): RedisCommandArguments {
-    const args = pushVerdictArguments(['BLPOP'], keys);
-
-    args.push(timeout.toString());
-
-    return args;
-}
-
-type BLPopRawReply = null | [RedisCommandArgument, RedisCommandArgument];
-
-type BLPopReply = null | {
-    key: RedisCommandArgument;
-    element: RedisCommandArgument;
-};
-
-export function transformReply(reply: BLPopRawReply): BLPopReply {
+import { CommandParser } from '../client/parser';
+import { UnwrapReply, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Removes and returns the first element in a list, or blocks until one is available
+   * @param parser - The Redis command parser
+   * @param key - Key of the list to pop from, or array of keys to try sequentially
+   * @param timeout - Maximum seconds to block, 0 to block indefinitely
+   */
+  parseCommand(parser: CommandParser, key: RedisVariadicArgument, timeout: number) {
+    parser.push('BLPOP');
+    parser.pushKeys(key);
+    parser.push(timeout.toString());
+  },
+  transformReply(reply: UnwrapReply<NullReply | TuplesReply<[BlobStringReply, BlobStringReply]>>) {
     if (reply === null) return null;
 
     return {
-        key: reply[0],
-        element: reply[1]
+      key: reply[0],
+      element: reply[1]
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BRPOP.spec.ts b/packages/client/lib/commands/BRPOP.spec.ts
index fc203e1abdf..de23bb34a92 100644
--- a/packages/client/lib/commands/BRPOP.spec.ts
+++ b/packages/client/lib/commands/BRPOP.spec.ts
@@ -1,79 +1,47 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './BRPOP';
-import { commandOptions } from '../../index';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BRPOP from './BRPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('BRPOP', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['BRPOP', 'key', '0']
-            );
-        });
-
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments(['key1', 'key2'], 0),
-                ['BRPOP', 'key1', 'key2', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(BRPOP, 'key', 0),
+        ['BRPOP', 'key', '0']
+      );
     });
 
-    describe('transformReply', () => {
-        it('null', () => {
-            assert.equal(
-                transformReply(null),
-                null
-            );
-        });
-
-        it('member', () => {
-            assert.deepEqual(
-                transformReply(['key', 'element']),
-                {
-                    key: 'key',
-                    element: 'element'
-                }
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(BRPOP, ['1', '2'], 0),
+        ['BRPOP', '1', '2', '0']
+      );
     });
-
-    testUtils.testWithClient('client.brPop', async client => {
-        const [ brPopReply ] = await Promise.all([
-            client.brPop(
-                commandOptions({ isolated: true }),
-                'key',
-                1
-            ),
-            client.lPush('key', 'element'),
-        ]);
-
-        assert.deepEqual(
-            brPopReply,
-            {
-                key: 'key',
-                element: 'element'
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.brPop', async cluster => {
-        const [ brPopReply ] = await Promise.all([
-            cluster.brPop(
-                commandOptions({ isolated: true }),
-                'key',
-                1
-            ),
-            cluster.lPush('key', 'element'),
-        ]);
-
-        assert.deepEqual(
-            brPopReply,
-            {
-                key: 'key',
-                element: 'element'
-            }
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  });
+
+  testUtils.testAll('brPop - null', async client => {
+    assert.equal(
+      await client.brPop('key', BLOCKING_MIN_VALUE),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('brPopblPop - with member', async client => {
+    const [, reply] = await Promise.all([
+      client.lPush('key', 'element'),
+      client.brPop('key', 1)
+    ]);
+
+    assert.deepEqual(reply, {
+      key: 'key',
+      element: 'element'
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BRPOP.ts b/packages/client/lib/commands/BRPOP.ts
index b30e7e2cc29..994a60ec902 100644
--- a/packages/client/lib/commands/BRPOP.ts
+++ b/packages/client/lib/commands/BRPOP.ts
@@ -1,17 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
+import BLPOP from './BLPOP';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument | Array<RedisCommandArgument>,
-    timeout: number
-): RedisCommandArguments {
-    const args = pushVerdictArguments(['BRPOP'], key);
-
-    args.push(timeout.toString());
-
-    return args;
-}
-
-export { transformReply } from './BLPOP';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Removes and returns the last element in a list, or blocks until one is available
+   * @param parser - The Redis command parser
+   * @param key - Key of the list to pop from, or array of keys to try sequentially
+   * @param timeout - Maximum seconds to block, 0 to block indefinitely
+   */
+  parseCommand(parser: CommandParser, key: RedisVariadicArgument, timeout: number) {
+    parser.push('BRPOP');
+    parser.pushKeys(key);
+    parser.push(timeout.toString());
+  },
+  transformReply: BLPOP.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BRPOPLPUSH.spec.ts b/packages/client/lib/commands/BRPOPLPUSH.spec.ts
index 214af4553ac..6c2a2a2c900 100644
--- a/packages/client/lib/commands/BRPOPLPUSH.spec.ts
+++ b/packages/client/lib/commands/BRPOPLPUSH.spec.ts
@@ -1,47 +1,43 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BRPOPLPUSH';
-import { commandOptions } from '../../index';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BRPOPLPUSH from './BRPOPLPUSH';
+import { parseArgs } from './generic-transformers';
 
 describe('BRPOPLPUSH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('source', 'destination', 0),
-            ['BRPOPLPUSH', 'source', 'destination', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(BRPOPLPUSH, 'source', 'destination', 0),
+      ['BRPOPLPUSH', 'source', 'destination', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.brPopLPush', async client => {
-        const [ popReply ] = await Promise.all([
-            client.brPopLPush(
-                commandOptions({ isolated: true }),
-                'source',
-                'destination',
-                0
-            ),
-            client.lPush('source', 'element')
-        ]);
+  testUtils.testAll('brPopLPush - null', async client => {
+    assert.equal(
+      await client.brPopLPush(
+        '{tag}source',
+        '{tag}destination',
+        BLOCKING_MIN_VALUE
+      ),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 
-        assert.equal(
-            popReply,
-            'element'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('brPopLPush - with member', async client => {
+    const [, reply] = await Promise.all([
+      client.lPush('{tag}source', 'element'),
+      client.brPopLPush(
+        '{tag}source',
+        '{tag}destination',
+        0
+      )
+    ]);
 
-    testUtils.testWithCluster('cluster.brPopLPush', async cluster => {
-        const [ popReply ] = await Promise.all([
-            cluster.brPopLPush(
-                commandOptions({ isolated: true }),
-                '{tag}source',
-                '{tag}destination',
-                0
-            ),
-            cluster.lPush('{tag}source', 'element')
-        ]);
-
-        assert.equal(
-            popReply,
-            'element'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(reply, 'element');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BRPOPLPUSH.ts b/packages/client/lib/commands/BRPOPLPUSH.ts
index 72c3e4aa5b2..b62d263cfaf 100644
--- a/packages/client/lib/commands/BRPOPLPUSH.ts
+++ b/packages/client/lib/commands/BRPOPLPUSH.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    source: RedisCommandArgument,
-    destination: RedisCommandArgument,
-    timeout: number
-): RedisCommandArguments {
-    return ['BRPOPLPUSH', source, destination, timeout.toString()];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Pops an element from a list, pushes it to another list and returns it; blocks until element is available
+   * @param parser - The Redis command parser
+   * @param source - Key of the source list to pop from
+   * @param destination - Key of the destination list to push to
+   * @param timeout - Maximum seconds to block, 0 to block indefinitely
+   */
+  parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, timeout: number) {
+    parser.push('BRPOPLPUSH');
+    parser.pushKeys([source, destination]);
+    parser.push(timeout.toString());
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BZMPOP.spec.ts b/packages/client/lib/commands/BZMPOP.spec.ts
index 0e381c114f2..8b082a214ee 100644
--- a/packages/client/lib/commands/BZMPOP.spec.ts
+++ b/packages/client/lib/commands/BZMPOP.spec.ts
@@ -1,32 +1,56 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './BZMPOP';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BZMPOP from './BZMPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('BZMPOP', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(0, 'key', 'MIN'),
-                ['BZMPOP', '0', '1', 'key', 'MIN']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(BZMPOP, 0, 'key', 'MIN'),
+        ['BZMPOP', '0', '1', 'key', 'MIN']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments(0, 'key', 'MIN', {
-                    COUNT: 2
-                }),
-                ['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2']
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(BZMPOP, 0, 'key', 'MIN', {
+          COUNT: 2
+        }),
+        ['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2']
+      );
     });
+  });
+
+  testUtils.testAll('bzmPop - null', async client => {
+    assert.equal(
+      await client.bzmPop(BLOCKING_MIN_VALUE, 'key', 'MAX'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 
-    testUtils.testWithClient('client.bzmPop', async client => {
-        assert.deepEqual(
-            await client.bzmPop(1, 'key', 'MAX'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('bzmPop - with member', async client => {
+    const key = 'key',
+      member = {
+        value: 'a',
+        score: 1
+      },
+      [, reply] = await Promise.all([
+        client.zAdd(key, member),
+        client.bzmPop(BLOCKING_MIN_VALUE, key, 'MAX')
+      ]);
+
+    assert.deepEqual(reply, {
+      key,
+      members: [member]
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BZMPOP.ts b/packages/client/lib/commands/BZMPOP.ts
index e4e9699cbd4..1d67adbccbc 100644
--- a/packages/client/lib/commands/BZMPOP.ts
+++ b/packages/client/lib/commands/BZMPOP.ts
@@ -1,20 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { SortedSetSide, transformZMPopArguments, ZMPopOptions } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { Command } from '../RESP/types';
+import ZMPOP, { parseZMPopArguments, ZMPopArguments } from './ZMPOP';
 
-export const FIRST_KEY_INDEX = 3;
-
-export function transformArguments(
-    timeout: number,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    side: SortedSetSide,
-    options?: ZMPopOptions
-): RedisCommandArguments {
-    return transformZMPopArguments(
-        ['BZMPOP', timeout.toString()],
-        keys,
-        side,
-        options
-    );
-}
-
-export { transformReply } from './ZMPOP';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns members from one or more sorted sets in the specified order; blocks until elements are available
+   * @param parser - The Redis command parser
+   * @param timeout - Maximum seconds to block, 0 to block indefinitely
+   * @param args - Additional arguments specifying the keys, min/max count, and order (MIN/MAX)
+   */
+  parseCommand(parser: CommandParser, timeout: number, ...args: ZMPopArguments) {
+    parser.push('BZMPOP', timeout.toString());
+    parseZMPopArguments(parser, ...args);
+  },
+  transformReply: ZMPOP.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/BZPOPMAX.spec.ts b/packages/client/lib/commands/BZPOPMAX.spec.ts
index d5c17437122..fbf60862327 100644
--- a/packages/client/lib/commands/BZPOPMAX.spec.ts
+++ b/packages/client/lib/commands/BZPOPMAX.spec.ts
@@ -1,65 +1,52 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './BZPOPMAX';
-import { commandOptions } from '../../index';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BZPOPMAX from './BZPOPMAX';
+import { parseArgs } from './generic-transformers';
 
 describe('BZPOPMAX', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['BZPOPMAX', 'key', '0']
-            );
-        });
-
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2'], 0),
-                ['BZPOPMAX', '1', '2', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(BZPOPMAX, 'key', 0),
+        ['BZPOPMAX', 'key', '0']
+      );
     });
 
-    describe('transformReply', () => {
-        it('null', () => {
-            assert.equal(
-                transformReply(null),
-                null
-            );
-        });
-
-        it('member', () => {
-            assert.deepEqual(
-                transformReply(['key', 'value', '1']),
-                {
-                    key: 'key',
-                    value: 'value',
-                    score: 1
-                }
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(BZPOPMAX, ['1', '2'], 0),
+        ['BZPOPMAX', '1', '2', '0']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.bzPopMax', async client => {
-        const [ bzPopMaxReply ] = await Promise.all([
-            client.bzPopMax(
-                commandOptions({ isolated: true }),
-                'key',
-                1
-            ),
-            client.zAdd('key', [{
-                value: '1',
-                score: 1
-            }])
-        ]);
+  testUtils.testAll('bzPopMax - null', async client => {
+    assert.equal(
+      await client.bzPopMax('key', BLOCKING_MIN_VALUE),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 
-        assert.deepEqual(
-            bzPopMaxReply,
-            {
-                key: 'key',
-                value: '1',
-                score: 1
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('bzPopMax - with member', async client => {
+    const key = 'key',
+      member = {
+        value: 'a',
+        score: 1
+      },
+      [, reply] = await Promise.all([
+        client.zAdd(key, member),
+        client.bzPopMax(key, BLOCKING_MIN_VALUE)
+      ]);
+
+    assert.deepEqual(reply, {
+      key,
+      ...member
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BZPOPMAX.ts b/packages/client/lib/commands/BZPOPMAX.ts
index 94a30fb8dce..cffbd5e877b 100644
--- a/packages/client/lib/commands/BZPOPMAX.ts
+++ b/packages/client/lib/commands/BZPOPMAX.ts
@@ -1,29 +1,39 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments, transformNumberInfinityReply, ZMember } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument | Array<RedisCommandArgument>,
-    timeout: number
-): RedisCommandArguments {
-    const args = pushVerdictArguments(['BZPOPMAX'], key);
-
-    args.push(timeout.toString());
-
-    return args;
-}
-
-type ZMemberRawReply = [key: RedisCommandArgument, value: RedisCommandArgument, score: RedisCommandArgument] | null;
-
-type BZPopMaxReply = (ZMember & { key: RedisCommandArgument }) | null;
-
-export function transformReply(reply: ZMemberRawReply): BZPopMaxReply | null {
-    if (!reply) return null;
-
-    return {
+import { CommandParser } from '../client/parser';
+import { NullReply, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
+import { RedisVariadicArgument, transformDoubleReply } from './generic-transformers';
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns the member with the highest score in a sorted set, or blocks until one is available
+   * @param parser - The Redis command parser
+   * @param keys - Key of the sorted set, or array of keys to try sequentially
+   * @param timeout - Maximum seconds to block, 0 to block indefinitely
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) {
+    parser.push('BZPOPMAX');
+    parser.pushKeys(keys);
+    parser.push(timeout.toString());
+  },
+  transformReply: {
+    2(
+      reply: UnwrapReply<NullReply | TuplesReply<[BlobStringReply, BlobStringReply, BlobStringReply]>>,
+      preserve?: any,
+      typeMapping?: TypeMapping
+    ) {
+      return reply === null ? null : {
         key: reply[0],
         value: reply[1],
-        score: transformNumberInfinityReply(reply[2])
-    };
-}
+        score: transformDoubleReply[2](reply[2], preserve, typeMapping)
+      };
+    },
+    3(reply: UnwrapReply<NullReply | TuplesReply<[BlobStringReply, BlobStringReply, DoubleReply]>>) {
+      return reply === null ? null : {
+        key: reply[0],
+        value: reply[1],
+        score: reply[2]
+      };
+    }
+  }
+} as const satisfies Command;
+
diff --git a/packages/client/lib/commands/BZPOPMIN.spec.ts b/packages/client/lib/commands/BZPOPMIN.spec.ts
index 0573a4ac898..2f8cab8dedf 100644
--- a/packages/client/lib/commands/BZPOPMIN.spec.ts
+++ b/packages/client/lib/commands/BZPOPMIN.spec.ts
@@ -1,65 +1,52 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './BZPOPMIN';
-import { commandOptions } from '../../index';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
+import BZPOPMIN from './BZPOPMIN';
+import { parseArgs } from './generic-transformers';
 
 describe('BZPOPMIN', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['BZPOPMIN', 'key', '0']
-            );
-        });
-
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2'], 0),
-                ['BZPOPMIN', '1', '2', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(BZPOPMIN, 'key', 0),
+        ['BZPOPMIN', 'key', '0']
+      );
     });
 
-    describe('transformReply', () => {
-        it('null', () => {
-            assert.equal(
-                transformReply(null),
-                null
-            );
-        });
-
-        it('member', () => {
-            assert.deepEqual(
-                transformReply(['key', 'value', '1']),
-                {
-                    key: 'key',
-                    value: 'value',
-                    score: 1
-                }
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(BZPOPMIN, ['1', '2'], 0),
+        ['BZPOPMIN', '1', '2', '0']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.bzPopMin', async client => {
-        const [ bzPopMinReply ] = await Promise.all([
-            client.bzPopMin(
-                commandOptions({ isolated: true }),
-                'key',
-                1
-            ),
-            client.zAdd('key', [{
-                value: '1',
-                score: 1
-            }])
-        ]);
+  testUtils.testAll('bzPopMin - null', async client => {
+    assert.equal(
+      await client.bzPopMin('key', BLOCKING_MIN_VALUE),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 
-        assert.deepEqual(
-            bzPopMinReply,
-            {
-                key: 'key',
-                value: '1',
-                score: 1
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('bzPopMin - with member', async client => {
+    const key = 'key',
+      member = {
+        value: 'a',
+        score: 1
+      },
+      [, reply] = await Promise.all([
+        client.zAdd(key, member),
+        client.bzPopMin(key, BLOCKING_MIN_VALUE)
+      ]);
+
+    assert.deepEqual(reply, {
+      key,
+      ...member
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/BZPOPMIN.ts b/packages/client/lib/commands/BZPOPMIN.ts
index 40cb3d5dc75..911a8654e5a 100644
--- a/packages/client/lib/commands/BZPOPMIN.ts
+++ b/packages/client/lib/commands/BZPOPMIN.ts
@@ -1,17 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
+import BZPOPMAX from './BZPOPMAX';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: BZPOPMAX.IS_READ_ONLY,
+  /**
+   * Removes and returns the member with the lowest score in a sorted set, or blocks until one is available
+   * @param parser - The Redis command parser
+   * @param keys - Key of the sorted set, or array of keys to try sequentially
+   * @param timeout - Maximum seconds to block, 0 to block indefinitely
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) {
+    parser.push('BZPOPMIN');
+    parser.pushKeys(keys);
+    parser.push(timeout.toString());
+  },
+  transformReply: BZPOPMAX.transformReply
+} as const satisfies Command;
 
-export function transformArguments(
-    key: RedisCommandArgument | Array<RedisCommandArgument>,
-    timeout: number
-): RedisCommandArguments {
-    const args = pushVerdictArguments(['BZPOPMIN'], key);
-
-    args.push(timeout.toString());
-
-    return args;
-}
-
-export { transformReply } from './BZPOPMAX';
diff --git a/packages/client/lib/commands/CLIENT_CACHING.spec.ts b/packages/client/lib/commands/CLIENT_CACHING.spec.ts
index d9cb9a3f796..ad3511b3e97 100644
--- a/packages/client/lib/commands/CLIENT_CACHING.spec.ts
+++ b/packages/client/lib/commands/CLIENT_CACHING.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLIENT_CACHING';
+import { strict as assert } from 'node:assert';
+import CLIENT_CACHING from './CLIENT_CACHING';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT CACHING', () => {
-    describe('transformArguments', () => {
-        it('true', () => {
-            assert.deepEqual(
-                transformArguments(true),
-                ['CLIENT', 'CACHING', 'YES']
-            );
-        });
+  describe('transformArguments', () => {
+    it('true', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_CACHING, true),
+        ['CLIENT', 'CACHING', 'YES']
+      );
+    });
 
-        it('false', () => {
-            assert.deepEqual(
-                transformArguments(false),
-                ['CLIENT', 'CACHING', 'NO']
-            );
-        });
+    it('false', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_CACHING, false),
+        ['CLIENT', 'CACHING', 'NO']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLIENT_CACHING.ts b/packages/client/lib/commands/CLIENT_CACHING.ts
index bc2fbe41e9d..f65230b5c24 100644
--- a/packages/client/lib/commands/CLIENT_CACHING.ts
+++ b/packages/client/lib/commands/CLIENT_CACHING.ts
@@ -1,11 +1,20 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(value: boolean): RedisCommandArguments {
-    return [
-        'CLIENT',
-        'CACHING',
-        value ? 'YES' : 'NO'
-    ];
-}
-
-export declare function transformReply(): 'OK' | Buffer;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Instructs the server about tracking or not keys in the next request
+   * @param parser - The Redis command parser
+   * @param value - Whether to enable (true) or disable (false) tracking
+   */
+  parseCommand(parser: CommandParser, value: boolean) {
+    parser.push(
+      'CLIENT',
+      'CACHING',
+      value ? 'YES' : 'NO'
+    );
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts
index 0a09713882f..5b0dfdb8437 100644
--- a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts
+++ b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts
@@ -1,11 +1,20 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLIENT_GETNAME';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CLIENT_GETNAME from './CLIENT_GETNAME';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT GETNAME', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLIENT', 'GETNAME']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_GETNAME),
+      ['CLIENT', 'GETNAME']
+    );
+  });
+  
+  testUtils.testWithClient('client.clientGetName', async client => {
+    assert.equal(
+      await client.clientGetName(),
+      null
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_GETNAME.ts b/packages/client/lib/commands/CLIENT_GETNAME.ts
index da00539d7fb..f61d82d0548 100644
--- a/packages/client/lib/commands/CLIENT_GETNAME.ts
+++ b/packages/client/lib/commands/CLIENT_GETNAME.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['CLIENT', 'GETNAME'];
-}
-
-export declare function transformReply(): string | null;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the name of the current connection
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLIENT', 'GETNAME');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts
index 09dd9677e32..a7c375fec26 100644
--- a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts
+++ b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLIENT_GETREDIR';
+import { strict as assert } from 'node:assert';
+import CLIENT_GETREDIR from './CLIENT_GETREDIR';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT GETREDIR', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLIENT', 'GETREDIR']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_GETREDIR),
+      ['CLIENT', 'GETREDIR']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.ts b/packages/client/lib/commands/CLIENT_GETREDIR.ts
index d192adf284a..ce50e86bf11 100644
--- a/packages/client/lib/commands/CLIENT_GETREDIR.ts
+++ b/packages/client/lib/commands/CLIENT_GETREDIR.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['CLIENT', 'GETREDIR'];
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the ID of the client to which the current client is redirecting tracking notifications
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLIENT', 'GETREDIR');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_ID.spec.ts b/packages/client/lib/commands/CLIENT_ID.spec.ts
index 6792a8c31be..51b308adf2c 100644
--- a/packages/client/lib/commands/CLIENT_ID.spec.ts
+++ b/packages/client/lib/commands/CLIENT_ID.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_ID';
+import CLIENT_ID from './CLIENT_ID';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT ID', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLIENT', 'ID']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_ID),
+      ['CLIENT', 'ID']
+    );
+  });
 
-    testUtils.testWithClient('client.clientId', async client => {
-        assert.equal(
-            typeof (await client.clientId()),
-            'number'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientId', async client => {
+    assert.equal(
+      typeof (await client.clientId()),
+      'number'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_ID.ts b/packages/client/lib/commands/CLIENT_ID.ts
index a57e392ade6..9daa7f90135 100644
--- a/packages/client/lib/commands/CLIENT_ID.ts
+++ b/packages/client/lib/commands/CLIENT_ID.ts
@@ -1,7 +1,15 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['CLIENT', 'ID'];
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the client ID for the current connection
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLIENT', 'ID');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_INFO.spec.ts b/packages/client/lib/commands/CLIENT_INFO.spec.ts
index ccb99017cf3..96881e6c1aa 100644
--- a/packages/client/lib/commands/CLIENT_INFO.spec.ts
+++ b/packages/client/lib/commands/CLIENT_INFO.spec.ts
@@ -1,50 +1,137 @@
-import { strict as assert } from 'assert';
-import { transformArguments, transformReply } from './CLIENT_INFO';
+import { strict as assert } from 'node:assert';
+import CLIENT_INFO from './CLIENT_INFO';
 import testUtils, { GLOBAL } from '../test-utils';
+import { parseArgs } from './generic-transformers';
+import { version } from '../../package.json';
 
 describe('CLIENT INFO', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
-
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLIENT', 'INFO']
-        );
-    });
-
-    testUtils.testWithClient('client.clientInfo', async client => {
-        const reply = await client.clientInfo();
-        assert.equal(typeof reply.id, 'number');
-        assert.equal(typeof reply.addr, 'string');
-        assert.equal(typeof reply.laddr, 'string');
-        assert.equal(typeof reply.fd, 'number');
-        assert.equal(typeof reply.name, 'string');
-        assert.equal(typeof reply.age, 'number');
-        assert.equal(typeof reply.idle, 'number');
-        assert.equal(typeof reply.flags, 'string');
-        assert.equal(typeof reply.db, 'number');
-        assert.equal(typeof reply.sub, 'number');
-        assert.equal(typeof reply.psub, 'number');
-        assert.equal(typeof reply.multi, 'number');
-        assert.equal(typeof reply.qbuf, 'number');
-        assert.equal(typeof reply.qbufFree, 'number');
-        assert.equal(typeof reply.argvMem, 'number');
-        assert.equal(typeof reply.obl, 'number');
-        assert.equal(typeof reply.oll, 'number');
-        assert.equal(typeof reply.omem, 'number');
-        assert.equal(typeof reply.totMem, 'number');
-        assert.equal(typeof reply.events, 'string');
-        assert.equal(typeof reply.cmd, 'string');
-        assert.equal(typeof reply.user, 'string');
-        assert.equal(typeof reply.redir, 'number');
-
-        if (testUtils.isVersionGreaterThan([7, 0])) {
-            assert.equal(typeof reply.multiMem, 'number');
-            assert.equal(typeof reply.resp, 'number');
-        }
-
-        if (testUtils.isVersionGreaterThan([7, 0, 3])) {
-            assert.equal(typeof reply.ssub, 'number');
-        }
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.isVersionGreaterThanHook([6, 2]);
+
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_INFO),
+      ['CLIENT', 'INFO']
+    );
+  });
+
+  testUtils.testWithClient('client.clientInfo', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(typeof reply.id, 'number');
+    assert.equal(typeof reply.addr, 'string');
+    assert.equal(typeof reply.laddr, 'string');
+    assert.equal(typeof reply.fd, 'number');
+    assert.equal(typeof reply.name, 'string');
+    assert.equal(typeof reply.age, 'number');
+    assert.equal(typeof reply.idle, 'number');
+    assert.equal(typeof reply.flags, 'string');
+    assert.equal(typeof reply.db, 'number');
+    assert.equal(typeof reply.sub, 'number');
+    assert.equal(typeof reply.psub, 'number');
+    assert.equal(typeof reply.multi, 'number');
+    assert.equal(typeof reply.qbuf, 'number');
+    assert.equal(typeof reply.qbufFree, 'number');
+    assert.equal(typeof reply.argvMem, 'number');
+    assert.equal(typeof reply.obl, 'number');
+    assert.equal(typeof reply.oll, 'number');
+    assert.equal(typeof reply.omem, 'number');
+    assert.equal(typeof reply.totMem, 'number');
+    assert.equal(typeof reply.events, 'string');
+    assert.equal(typeof reply.cmd, 'string');
+    assert.equal(typeof reply.user, 'string');
+    assert.equal(typeof reply.redir, 'number');
+
+    if (testUtils.isVersionGreaterThan([7, 0])) {
+      assert.equal(typeof reply.multiMem, 'number');
+      assert.equal(typeof reply.resp, 'number');
+
+      if (testUtils.isVersionGreaterThan([7, 0, 3])) {
+        assert.equal(typeof reply.ssub, 'number');
+      }
+    }
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('client.clientInfo Redis < 7', async client => {
+    const reply = await client.clientInfo();
+    if (!testUtils.isVersionGreaterThan([7])) {
+      assert.strictEqual(reply.libName, undefined, 'LibName should be undefined for Redis < 7');
+      assert.strictEqual(reply.libVer, undefined, 'LibVer should be undefined for Redis < 7');
+    }
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 info disabled', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, '');
+    assert.equal(reply.libVer, '');
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      disableClientInfo: true
+    }
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp unset, info enabled, tag set', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, 'node-redis(client1)');
+    assert.equal(reply.libVer, version);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      clientInfoTag: 'client1'
+    }
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp unset, info enabled, tag unset', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, 'node-redis');
+    assert.equal(reply.libVer, version);
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp2 info enabled', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, 'node-redis(client1)');
+    assert.equal(reply.libVer, version);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 2,
+      clientInfoTag: 'client1'
+    }
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp2 info disabled', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, '');
+    assert.equal(reply.libVer, '');
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      disableClientInfo: true,
+      RESP: 2
+    }
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp3 info enabled', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, 'node-redis(client1)');
+    assert.equal(reply.libVer, version);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3,
+      clientInfoTag: 'client1'
+    }
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp3 info disabled', async client => {
+    const reply = await client.clientInfo();
+    assert.equal(reply.libName, '');
+    assert.equal(reply.libVer, '');
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      disableClientInfo: true,
+      RESP: 3
+    }
+  });
+
 });
diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts
index fd823542f86..5fed01c52e1 100644
--- a/packages/client/lib/commands/CLIENT_INFO.ts
+++ b/packages/client/lib/commands/CLIENT_INFO.ts
@@ -1,94 +1,130 @@
-export const IS_READ_ONLY = true;
-
-export function transformArguments(): Array<string> {
-    return ['CLIENT', 'INFO'];
-}
+import { CommandParser } from '../client/parser';
+import { Command, VerbatimStringReply } from '../RESP/types';
 
 export interface ClientInfoReply {
-    id: number;
-    addr: string;
-    laddr?: string; // 6.2
-    fd: number;
-    name: string;
-    age: number;
-    idle: number;
-    flags: string;
-    db: number;
-    sub: number;
-    psub: number;
-    ssub?: number; // 7.0.3
-    multi: number;
-    qbuf: number;
-    qbufFree: number;
-    argvMem?: number; // 6.0
-    multiMem?: number; // 7.0
-    obl: number;
-    oll: number;
-    omem: number;
-    totMem?: number; // 6.0
-    events: string;
-    cmd: string;
-    user?: string; // 6.0
-    redir?: number; // 6.2
-    resp?: number; // 7.0
-    // 7.2
-    libName?: string;
-    libVer?: string;
+  id: number;
+  addr: string;
+  /**
+   * available since 6.2
+   */
+  laddr?: string;
+  fd: number;
+  name: string;
+  age: number;
+  idle: number;
+  flags: string;
+  db: number;
+  sub: number;
+  psub: number;
+  /**
+   * available since 7.0.3
+   */
+  ssub?: number;
+  multi: number;
+  qbuf: number;
+  qbufFree: number;
+  /**
+   * available since 6.0
+   */
+  argvMem?: number;
+  /**
+   * available since 7.0
+   */
+  multiMem?: number;
+  obl: number;
+  oll: number;
+  omem: number;
+  /**
+   * available since 6.0
+   */
+  totMem?: number;
+  events: string;
+  cmd: string;
+  /**
+   * available since 6.0
+   */
+  user?: string;
+  /**
+   * available since 6.2
+   */
+  redir?: number;
+  /**
+   * available since 7.0
+   */
+  resp?: number;
+  /**
+   * available since 7.0
+   */
+  libName?: string;
+  /**
+   * available since 7.0
+   */
+  libVer?: string;
 }
 
 const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g;
 
-export function transformReply(rawReply: string): ClientInfoReply {
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information and statistics about the current client connection
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLIENT', 'INFO');
+  },
+  transformReply(rawReply: VerbatimStringReply) {
     const map: Record<string, string> = {};
-    for (const item of rawReply.matchAll(CLIENT_INFO_REGEX)) {
-        map[item[1]] = item[2];
+    for (const item of rawReply.toString().matchAll(CLIENT_INFO_REGEX)) {
+      map[item[1]] = item[2];
     }
-
     const reply: ClientInfoReply = {
-        id: Number(map.id),
-        addr: map.addr,
-        fd: Number(map.fd),
-        name: map.name,
-        age: Number(map.age),
-        idle: Number(map.idle),
-        flags: map.flags,
-        db: Number(map.db),
-        sub: Number(map.sub),
-        psub: Number(map.psub),
-        multi: Number(map.multi),
-        qbuf: Number(map.qbuf),
-        qbufFree: Number(map['qbuf-free']),
-        argvMem: Number(map['argv-mem']),
-        obl: Number(map.obl),
-        oll: Number(map.oll),
-        omem: Number(map.omem),
-        totMem: Number(map['tot-mem']),
-        events: map.events,
-        cmd: map.cmd,
-        user: map.user,
-        libName: map['lib-name'],
-        libVer: map['lib-ver'],
+      id: Number(map.id),
+      addr: map.addr,
+      fd: Number(map.fd),
+      name: map.name,
+      age: Number(map.age),
+      idle: Number(map.idle),
+      flags: map.flags,
+      db: Number(map.db),
+      sub: Number(map.sub),
+      psub: Number(map.psub),
+      multi: Number(map.multi),
+      qbuf: Number(map.qbuf),
+      qbufFree: Number(map['qbuf-free']),
+      argvMem: Number(map['argv-mem']),
+      obl: Number(map.obl),
+      oll: Number(map.oll),
+      omem: Number(map.omem),
+      totMem: Number(map['tot-mem']),
+      events: map.events,
+      cmd: map.cmd,
+      user: map.user,
+      libName: map['lib-name'],
+      libVer: map['lib-ver']
     };
 
     if (map.laddr !== undefined) {
-        reply.laddr = map.laddr;
+      reply.laddr = map.laddr;
     }
 
     if (map.redir !== undefined) {
-        reply.redir = Number(map.redir);
+      reply.redir = Number(map.redir);
     }
 
     if (map.ssub !== undefined) {
-        reply.ssub = Number(map.ssub);
+      reply.ssub = Number(map.ssub);
     }
 
     if (map['multi-mem'] !== undefined) {
-        reply.multiMem = Number(map['multi-mem']);
+      reply.multiMem = Number(map['multi-mem']);
     }
 
     if (map.resp !== undefined) {
-        reply.resp = Number(map.resp);
+      reply.resp = Number(map.resp);
     }
 
     return reply;
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_KILL.spec.ts b/packages/client/lib/commands/CLIENT_KILL.spec.ts
index 733aaca858b..5078a267516 100644
--- a/packages/client/lib/commands/CLIENT_KILL.spec.ts
+++ b/packages/client/lib/commands/CLIENT_KILL.spec.ts
@@ -1,120 +1,121 @@
-import { strict as assert } from 'assert';
-import { ClientKillFilters, transformArguments } from './CLIENT_KILL';
+import { strict as assert } from 'node:assert';
+import CLIENT_KILL, { CLIENT_KILL_FILTERS } from './CLIENT_KILL';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT KILL', () => {
-    describe('transformArguments', () => {
-        it('ADDRESS', () => {
-            assert.deepEqual(
-                transformArguments({
-                    filter: ClientKillFilters.ADDRESS,
-                    address: 'ip:6379'
-                }),
-                ['CLIENT', 'KILL', 'ADDR', 'ip:6379']
-            );
-        });
+  describe('transformArguments', () => {
+    it('ADDRESS', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_KILL, {
+          filter: CLIENT_KILL_FILTERS.ADDRESS,
+          address: 'ip:6379'
+        }),
+        ['CLIENT', 'KILL', 'ADDR', 'ip:6379']
+      );
+    });
+
+    it('LOCAL_ADDRESS', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_KILL, {
+          filter: CLIENT_KILL_FILTERS.LOCAL_ADDRESS,
+          localAddress: 'ip:6379'
+        }),
+        ['CLIENT', 'KILL', 'LADDR', 'ip:6379']
+      );
+    });
 
-        it('LOCAL_ADDRESS', () => {
-            assert.deepEqual(
-                transformArguments({
-                    filter: ClientKillFilters.LOCAL_ADDRESS,
-                    localAddress: 'ip:6379'
-                }),
-                ['CLIENT', 'KILL', 'LADDR', 'ip:6379']
-            );
-        });
+    describe('ID', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_KILL, {
+            filter: CLIENT_KILL_FILTERS.ID,
+            id: '1'
+          }),
+          ['CLIENT', 'KILL', 'ID', '1']
+        );
+      });
 
-        describe('ID', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filter: ClientKillFilters.ID,
-                        id: '1'
-                    }),
-                    ['CLIENT', 'KILL', 'ID', '1']
-                );
-            });
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_KILL, {
+            filter: CLIENT_KILL_FILTERS.ID,
+            id: 1
+          }),
+          ['CLIENT', 'KILL', 'ID', '1']
+        );
+      });
+    });
 
-            it('number', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filter: ClientKillFilters.ID,
-                        id: 1
-                    }),
-                    ['CLIENT', 'KILL', 'ID', '1']
-                );
-            });
-        });
+    it('TYPE', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_KILL, {
+          filter: CLIENT_KILL_FILTERS.TYPE,
+          type: 'master'
+        }),
+        ['CLIENT', 'KILL', 'TYPE', 'master']
+      );
+    });
 
-        it('TYPE', () => {
-            assert.deepEqual(
-                transformArguments({
-                    filter: ClientKillFilters.TYPE,
-                    type: 'master'
-                }),
-                ['CLIENT', 'KILL', 'TYPE', 'master']
-            );
-        });
+    it('USER', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_KILL, {
+          filter: CLIENT_KILL_FILTERS.USER,
+          username: 'username'
+        }),
+        ['CLIENT', 'KILL', 'USER', 'username']
+      );
+    });
 
-        it('USER', () => {
-            assert.deepEqual(
-                transformArguments({
-                    filter: ClientKillFilters.USER,
-                    username: 'username'
-                }),
-                ['CLIENT', 'KILL', 'USER', 'username']
-            );
-        });
+    it('MAXAGE', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_KILL, {
+          filter: CLIENT_KILL_FILTERS.MAXAGE,
+          maxAge: 10
+        }),
+        ['CLIENT', 'KILL', 'MAXAGE', '10']
+      );
+    });
 
-        it('MAXAGE', () => {
-            assert.deepEqual(
-                transformArguments({
-                    filter: ClientKillFilters.MAXAGE,
-                    maxAge: 10
-                }),
-                ['CLIENT', 'KILL', 'MAXAGE', '10']
-            );
-        });
+    describe('SKIP_ME', () => {
+      it('undefined', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_KILL, CLIENT_KILL_FILTERS.SKIP_ME),
+          ['CLIENT', 'KILL', 'SKIPME']
+        );
+      });
 
-        describe('SKIP_ME', () => {
-            it('undefined', () => {
-                assert.deepEqual(
-                    transformArguments(ClientKillFilters.SKIP_ME),
-                    ['CLIENT', 'KILL', 'SKIPME']
-                );
-            });
+      it('true', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_KILL, {
+            filter: CLIENT_KILL_FILTERS.SKIP_ME,
+            skipMe: true
+          }),
+          ['CLIENT', 'KILL', 'SKIPME', 'yes']
+        );
+      });
 
-            it('true', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filter: ClientKillFilters.SKIP_ME,
-                        skipMe: true
-                    }),
-                    ['CLIENT', 'KILL', 'SKIPME', 'yes']
-                );
-            });
+      it('false', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_KILL, {
+            filter: CLIENT_KILL_FILTERS.SKIP_ME,
+            skipMe: false
+          }),
+          ['CLIENT', 'KILL', 'SKIPME', 'no']
+        );
+      });
+    });
 
-            it('false', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filter: ClientKillFilters.SKIP_ME,
-                        skipMe: false
-                    }),
-                    ['CLIENT', 'KILL', 'SKIPME', 'no']
-                );
-            });
-        });
-        
-        it('TYPE & SKIP_ME', () => {
-            assert.deepEqual(
-                transformArguments([
-                    {
-                        filter: ClientKillFilters.TYPE,
-                        type: 'master'
-                    },
-                    ClientKillFilters.SKIP_ME
-                ]),
-                ['CLIENT', 'KILL', 'TYPE', 'master', 'SKIPME']
-            );
-        });
+    it('TYPE & SKIP_ME', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_KILL, [
+          {
+            filter: CLIENT_KILL_FILTERS.TYPE,
+            type: 'master'
+          },
+          CLIENT_KILL_FILTERS.SKIP_ME
+        ]),
+        ['CLIENT', 'KILL', 'TYPE', 'master', 'SKIPME']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLIENT_KILL.ts b/packages/client/lib/commands/CLIENT_KILL.ts
index b1a53df64d8..d7f0d0e0829 100644
--- a/packages/client/lib/commands/CLIENT_KILL.ts
+++ b/packages/client/lib/commands/CLIENT_KILL.ts
@@ -1,104 +1,114 @@
-import { RedisCommandArguments } from '.';
-
-export enum ClientKillFilters {
-    ADDRESS = 'ADDR',
-    LOCAL_ADDRESS = 'LADDR',
-    ID = 'ID',
-    TYPE = 'TYPE',
-    USER = 'USER',
-    SKIP_ME = 'SKIPME',
-    MAXAGE = 'MAXAGE'
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+
+export const CLIENT_KILL_FILTERS = {
+  ADDRESS: 'ADDR',
+  LOCAL_ADDRESS: 'LADDR',
+  ID: 'ID',
+  TYPE: 'TYPE',
+  USER: 'USER',
+  SKIP_ME: 'SKIPME',
+  MAXAGE: 'MAXAGE'
+} as const;
+
+type CLIENT_KILL_FILTERS = typeof CLIENT_KILL_FILTERS;
+
+export interface ClientKillFilterCommon<T extends CLIENT_KILL_FILTERS[keyof CLIENT_KILL_FILTERS]> {
+  filter: T;
 }
 
-interface KillFilter<T extends ClientKillFilters> {
-    filter: T;
+export interface ClientKillAddress extends ClientKillFilterCommon<CLIENT_KILL_FILTERS['ADDRESS']> {
+  address: `${string}:${number}`;
 }
 
-interface KillAddress extends KillFilter<ClientKillFilters.ADDRESS> {
-    address: `${string}:${number}`;
+export interface ClientKillLocalAddress extends ClientKillFilterCommon<CLIENT_KILL_FILTERS['LOCAL_ADDRESS']> {
+  localAddress: `${string}:${number}`;
 }
 
-interface KillLocalAddress extends KillFilter<ClientKillFilters.LOCAL_ADDRESS> {
-    localAddress: `${string}:${number}`;
+export interface ClientKillId extends ClientKillFilterCommon<CLIENT_KILL_FILTERS['ID']> {
+  id: number | `${number}`;
 }
 
-interface KillId extends KillFilter<ClientKillFilters.ID> {
-    id: number | `${number}`;
+export interface ClientKillType extends ClientKillFilterCommon<CLIENT_KILL_FILTERS['TYPE']> {
+  type: 'normal' | 'master' | 'replica' | 'pubsub';
 }
 
-interface KillType extends KillFilter<ClientKillFilters.TYPE> {
-    type: 'normal' | 'master' | 'replica' | 'pubsub';
+export interface ClientKillUser extends ClientKillFilterCommon<CLIENT_KILL_FILTERS['USER']> {
+  username: string;
 }
 
-interface KillUser extends KillFilter<ClientKillFilters.USER> {
-    username: string;
-}
-
-type KillSkipMe = ClientKillFilters.SKIP_ME | (KillFilter<ClientKillFilters.SKIP_ME> & {
-    skipMe: boolean;
+export type ClientKillSkipMe = CLIENT_KILL_FILTERS['SKIP_ME'] | (ClientKillFilterCommon<CLIENT_KILL_FILTERS['SKIP_ME']> & {
+  skipMe: boolean;
 });
 
-interface KillMaxAge extends KillFilter<ClientKillFilters.MAXAGE> {
-    maxAge: number;
+export interface ClientKillMaxAge extends ClientKillFilterCommon<CLIENT_KILL_FILTERS['MAXAGE']> {
+  maxAge: number;
 }
 
-type KillFilters = KillAddress | KillLocalAddress | KillId | KillType | KillUser | KillSkipMe | KillMaxAge;
+export type ClientKillFilter = ClientKillAddress | ClientKillLocalAddress | ClientKillId | ClientKillType | ClientKillUser | ClientKillSkipMe | ClientKillMaxAge;
 
-export function transformArguments(filters: KillFilters | Array<KillFilters>): RedisCommandArguments {
-    const args = ['CLIENT', 'KILL'];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Closes client connections matching the specified filters
+   * @param parser - The Redis command parser
+   * @param filters - One or more filters to match client connections to kill
+   */
+  parseCommand(parser: CommandParser, filters: ClientKillFilter | Array<ClientKillFilter>) {
+    parser.push('CLIENT', 'KILL');
 
     if (Array.isArray(filters)) {
-        for (const filter of filters) {
-            pushFilter(args, filter);
-        }
+      for (const filter of filters) {
+        pushFilter(parser, filter);
+      }
     } else {
-        pushFilter(args, filters);
+      pushFilter(parser, filters);
     }
 
-    return args;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
+
+function pushFilter(parser: CommandParser, filter: ClientKillFilter): void {
+  if (filter === CLIENT_KILL_FILTERS.SKIP_ME) {
+    parser.push('SKIPME');
+    return;
+  }
+
+  parser.push(filter.filter);
+
+  switch (filter.filter) {
+    case CLIENT_KILL_FILTERS.ADDRESS:
+      parser.push(filter.address);
+      break;
+
+    case CLIENT_KILL_FILTERS.LOCAL_ADDRESS:
+      parser.push(filter.localAddress);
+      break;
+
+    case CLIENT_KILL_FILTERS.ID:
+      parser.push(
+        typeof filter.id === 'number' ?
+          filter.id.toString() :
+          filter.id
+      );
+      break;
+
+    case CLIENT_KILL_FILTERS.TYPE:
+      parser.push(filter.type);
+      break;
+
+    case CLIENT_KILL_FILTERS.USER:
+      parser.push(filter.username);
+      break;
+
+    case CLIENT_KILL_FILTERS.SKIP_ME:
+      parser.push(filter.skipMe ? 'yes' : 'no');
+      break;
+    
+    case CLIENT_KILL_FILTERS.MAXAGE:
+      parser.push(filter.maxAge.toString());
+      break;
+  }
 }
-
-function pushFilter(args: RedisCommandArguments, filter: KillFilters): void {
-    if (filter === ClientKillFilters.SKIP_ME) {
-        args.push('SKIPME');
-        return;
-    }
-
-    args.push(filter.filter);
-
-    switch(filter.filter) {
-        case ClientKillFilters.ADDRESS:
-            args.push(filter.address);
-            break;
-
-        case ClientKillFilters.LOCAL_ADDRESS:
-            args.push(filter.localAddress);
-            break;
-
-        case ClientKillFilters.ID:
-            args.push(
-                typeof filter.id === 'number' ?
-                    filter.id.toString() :
-                    filter.id
-            );
-            break;
-
-        case ClientKillFilters.TYPE:
-            args.push(filter.type);
-            break;
-
-        case ClientKillFilters.USER:
-                args.push(filter.username);
-                break;
-
-        case ClientKillFilters.SKIP_ME:
-            args.push(filter.skipMe ? 'yes' : 'no');
-            break;
-
-        case ClientKillFilters.MAXAGE:
-            args.push(filter.maxAge.toString());
-            break;
-    }
-}
-
-export declare function transformReply(): number;
diff --git a/packages/client/lib/commands/CLIENT_LIST.spec.ts b/packages/client/lib/commands/CLIENT_LIST.spec.ts
index c9c720e12ef..34709c5f14f 100644
--- a/packages/client/lib/commands/CLIENT_LIST.spec.ts
+++ b/packages/client/lib/commands/CLIENT_LIST.spec.ts
@@ -1,78 +1,78 @@
-import { strict as assert } from 'assert';
-import { transformArguments, transformReply } from './CLIENT_LIST';
+import { strict as assert } from 'node:assert';
+import CLIENT_LIST from './CLIENT_LIST';
 import testUtils, { GLOBAL } from '../test-utils';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT LIST', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['CLIENT', 'LIST']
-            );
-        });
-
-        it('with TYPE', () => {
-            assert.deepEqual(
-                transformArguments({
-                    TYPE: 'NORMAL' 
-                }),
-                ['CLIENT', 'LIST', 'TYPE', 'NORMAL']
-            );
-        });
-
-        it('with ID', () => {
-            assert.deepEqual(
-                transformArguments({
-                    ID: ['1', '2']
-                }),
-                ['CLIENT', 'LIST', 'ID', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_LIST),
+        ['CLIENT', 'LIST']
+      );
     });
 
-    testUtils.testWithClient('client.clientList', async client => {
-        const reply = await client.clientList();
-        assert.ok(Array.isArray(reply));
-
-        for (const item of reply) {
-            assert.equal(typeof item.id, 'number');
-            assert.equal(typeof item.addr, 'string');
-            assert.equal(typeof item.fd, 'number');
-            assert.equal(typeof item.name, 'string');
-            assert.equal(typeof item.age, 'number');
-            assert.equal(typeof item.idle, 'number');
-            assert.equal(typeof item.flags, 'string');
-            assert.equal(typeof item.db, 'number');
-            assert.equal(typeof item.sub, 'number');
-            assert.equal(typeof item.psub, 'number');
-            assert.equal(typeof item.multi, 'number');
-            assert.equal(typeof item.qbuf, 'number');
-            assert.equal(typeof item.qbufFree, 'number');
-            assert.equal(typeof item.obl, 'number');
-            assert.equal(typeof item.oll, 'number');
-            assert.equal(typeof item.omem, 'number');
-            assert.equal(typeof item.events, 'string');
-            assert.equal(typeof item.cmd, 'string');
+    it('with TYPE', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_LIST, {
+          TYPE: 'NORMAL'
+        }),
+        ['CLIENT', 'LIST', 'TYPE', 'NORMAL']
+      );
+    });
 
-            if (testUtils.isVersionGreaterThan([6, 0])) {
-                assert.equal(typeof item.argvMem, 'number');
-                assert.equal(typeof item.totMem, 'number');
-                assert.equal(typeof item.user, 'string');
-            }
+    it('with ID', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_LIST, {
+          ID: ['1', '2']
+        }),
+        ['CLIENT', 'LIST', 'ID', '1', '2']
+      );
+    });
+  });
 
-            if (testUtils.isVersionGreaterThan([6, 2])) {
-                assert.equal(typeof item.redir, 'number');
-                assert.equal(typeof item.laddr, 'string');
-            }
+  testUtils.testWithClient('client.clientList', async client => {
+    const reply = await client.clientList();
+    assert.ok(Array.isArray(reply));
+    for (const item of reply) {
+      assert.equal(typeof item.id, 'number');
+      assert.equal(typeof item.addr, 'string');
+      assert.equal(typeof item.fd, 'number');
+      assert.equal(typeof item.name, 'string');
+      assert.equal(typeof item.age, 'number');
+      assert.equal(typeof item.idle, 'number');
+      assert.equal(typeof item.flags, 'string');
+      assert.equal(typeof item.db, 'number');
+      assert.equal(typeof item.sub, 'number');
+      assert.equal(typeof item.psub, 'number');
+      assert.equal(typeof item.multi, 'number');
+      assert.equal(typeof item.qbuf, 'number');
+      assert.equal(typeof item.qbufFree, 'number');
+      assert.equal(typeof item.obl, 'number');
+      assert.equal(typeof item.oll, 'number');
+      assert.equal(typeof item.omem, 'number');
+      assert.equal(typeof item.events, 'string');
+      assert.equal(typeof item.cmd, 'string');
 
-            if (testUtils.isVersionGreaterThan([7, 0])) {
-                assert.equal(typeof item.multiMem, 'number');
-                assert.equal(typeof item.resp, 'number');
-            }
+      if (testUtils.isVersionGreaterThan([6, 0])) {
+        assert.equal(typeof item.argvMem, 'number');
+        assert.equal(typeof item.totMem, 'number');
+        assert.equal(typeof item.user, 'string');
+        
+        if (testUtils.isVersionGreaterThan([6, 2])) {
+          assert.equal(typeof item.redir, 'number');
+          assert.equal(typeof item.laddr, 'string');
+          
+          if (testUtils.isVersionGreaterThan([7, 0])) {
+            assert.equal(typeof item.multiMem, 'number');
+            assert.equal(typeof item.resp, 'number');
 
             if (testUtils.isVersionGreaterThan([7, 0, 3])) {
-                assert.equal(typeof item.ssub, 'number');
+              assert.equal(typeof item.ssub, 'number');
             }
+          }
         }
-    }, GLOBAL.SERVERS.OPEN);
+      }
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_LIST.ts b/packages/client/lib/commands/CLIENT_LIST.ts
index 6f71dc7d999..1c1d9c3ec50 100644
--- a/packages/client/lib/commands/CLIENT_LIST.ts
+++ b/packages/client/lib/commands/CLIENT_LIST.ts
@@ -1,43 +1,46 @@
-import { RedisCommandArguments, RedisCommandArgument } from '.';
-import { pushVerdictArguments } from './generic-transformers';
-import { transformReply as transformClientInfoReply, ClientInfoReply } from './CLIENT_INFO';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types';
+import CLIENT_INFO, { ClientInfoReply } from './CLIENT_INFO';
 
-interface ListFilterType {
-    TYPE: 'NORMAL' | 'MASTER' | 'REPLICA' | 'PUBSUB';
-    ID?: never;
+export interface ListFilterType {
+  TYPE: 'NORMAL' | 'MASTER' | 'REPLICA' | 'PUBSUB';
+  ID?: never;
 }
 
-interface ListFilterId {
-    ID: Array<RedisCommandArgument>;
-    TYPE?: never;
+export interface ListFilterId {
+  ID: Array<RedisArgument>;
+  TYPE?: never;
 }
 
 export type ListFilter = ListFilterType | ListFilterId;
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(filter?: ListFilter): RedisCommandArguments {
-    let args: RedisCommandArguments = ['CLIENT', 'LIST'];
-
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about all client connections. Can be filtered by type or ID
+   * @param parser - The Redis command parser
+   * @param filter - Optional filter to return only specific client types or IDs
+   */
+  parseCommand(parser: CommandParser, filter?: ListFilter) {
+    parser.push('CLIENT', 'LIST');
     if (filter) {
-        if (filter.TYPE !== undefined) {
-            args.push('TYPE', filter.TYPE);
-        } else  {
-            args.push('ID');
-            args = pushVerdictArguments(args, filter.ID);
-        }
+      if (filter.TYPE !== undefined) {
+        parser.push('TYPE', filter.TYPE);
+      } else {
+        parser.push('ID');
+        parser.pushVariadic(filter.ID);
+      }
     }
-
-    return args;
-}
-
-export function transformReply(rawReply: string): Array<ClientInfoReply> {
-    const split = rawReply.split('\n'),
-        length = split.length - 1,
-        reply: Array<ClientInfoReply> = [];
+  },
+  transformReply(rawReply: VerbatimStringReply): Array<ClientInfoReply> {
+    const split = rawReply.toString().split('\n'),
+      length = split.length - 1,
+      reply: Array<ClientInfoReply> = [];
     for (let i = 0; i < length; i++) {
-        reply.push(transformClientInfoReply(split[i]));
+      reply.push(CLIENT_INFO.transformReply(split[i] as unknown as VerbatimStringReply));
     }
-    
+
     return reply;
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts
index df8903f0646..50afd413492 100644
--- a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts
+++ b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_NO-EVICT';
+import CLIENT_NO_EVICT from './CLIENT_NO-EVICT';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT NO-EVICT', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('true', () => {
-            assert.deepEqual(
-                transformArguments(true),
-                ['CLIENT', 'NO-EVICT', 'ON']
-            );
-        });
+  describe('transformArguments', () => {
+    it('true', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_NO_EVICT, true),
+        ['CLIENT', 'NO-EVICT', 'ON']
+      );
+    });
 
-        it('false', () => {
-            assert.deepEqual(
-                transformArguments(false),
-                ['CLIENT', 'NO-EVICT', 'OFF']
-            );
-        });
+    it('false', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_NO_EVICT, false),
+        ['CLIENT', 'NO-EVICT', 'OFF']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.clientNoEvict', async client => {
-        assert.equal(
-            await client.clientNoEvict(true),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientNoEvict', async client => {
+    assert.equal(
+      await client.clientNoEvict(true),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.ts
index 86edbde1d23..116d226d036 100644
--- a/packages/client/lib/commands/CLIENT_NO-EVICT.ts
+++ b/packages/client/lib/commands/CLIENT_NO-EVICT.ts
@@ -1,11 +1,20 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(value: boolean): RedisCommandArguments {
-    return [
-        'CLIENT',
-        'NO-EVICT',
-        value ? 'ON' : 'OFF'
-    ];
-}
-
-export declare function transformReply(): 'OK' | Buffer;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Controls whether to prevent the client's connections from being evicted
+   * @param parser - The Redis command parser
+   * @param value - Whether to enable (true) or disable (false) the no-evict mode
+   */
+  parseCommand(parser: CommandParser, value: boolean) {
+    parser.push(
+      'CLIENT',
+      'NO-EVICT',
+      value ? 'ON' : 'OFF'
+    );
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts
index 80ee0ada1fd..ec5c9f18ae9 100644
--- a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts
+++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts
@@ -1,30 +1,31 @@
 import { strict as assert } from 'assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_NO-TOUCH';
+import CLIENT_NO_TOUCH from './CLIENT_NO-TOUCH';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT NO-TOUCH', () => {
-    testUtils.isVersionGreaterThanHook([7, 2]);
+  testUtils.isVersionGreaterThanHook([7, 2]);
 
-    describe('transformArguments', () => {
-        it('true', () => {
-            assert.deepEqual(
-                transformArguments(true),
-                ['CLIENT', 'NO-TOUCH', 'ON']
-            );
-        });
+  describe('transformArguments', () => {
+    it('true', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_NO_TOUCH, true),
+        ['CLIENT', 'NO-TOUCH', 'ON']
+      );
+    });
 
-        it('false', () => {
-            assert.deepEqual(
-                transformArguments(false),
-                ['CLIENT', 'NO-TOUCH', 'OFF']
-            );
-        });
+    it('false', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_NO_TOUCH, false),
+        ['CLIENT', 'NO-TOUCH', 'OFF']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.clientNoTouch', async client => {
-        assert.equal(
-            await client.clientNoTouch(true),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientNoTouch', async client => {
+    assert.equal(
+      await client.clientNoTouch(true),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts
index d11f693dbab..167b31f3600 100644
--- a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts
+++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts
@@ -1,11 +1,21 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(value: boolean): RedisCommandArguments {
-    return [
-        'CLIENT',
-        'NO-TOUCH',
-        value ? 'ON' : 'OFF'
-    ];
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Controls whether to prevent the client from touching the LRU/LFU of keys
+   * @param parser - The Redis command parser
+   * @param value - Whether to enable (true) or disable (false) the no-touch mode
+   */
+  parseCommand(parser: CommandParser, value: boolean) {
+    parser.push(
+      'CLIENT',
+      'NO-TOUCH',
+      value ? 'ON' : 'OFF'
+    );
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-export declare function transformReply(): 'OK' | Buffer;
diff --git a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts
index 1376ff41eed..e213433afbe 100644
--- a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts
+++ b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_PAUSE';
+import CLIENT_PAUSE from './CLIENT_PAUSE';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT PAUSE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(0),
-                ['CLIENT', 'PAUSE', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_PAUSE, 0),
+        ['CLIENT', 'PAUSE', '0']
+      );
+    });
 
-        it('with mode', () => {
-            assert.deepEqual(
-                transformArguments(0, 'ALL'),
-                ['CLIENT', 'PAUSE', '0', 'ALL']
-            );
-        });
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_PAUSE, 0, 'ALL'),
+        ['CLIENT', 'PAUSE', '0', 'ALL']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.clientPause', async client => {
-        assert.equal(
-            await client.clientPause(0),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientPause', async client => {
+    assert.equal(
+      await client.clientPause(0),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_PAUSE.ts b/packages/client/lib/commands/CLIENT_PAUSE.ts
index 090002272c9..4d0638df89d 100644
--- a/packages/client/lib/commands/CLIENT_PAUSE.ts
+++ b/packages/client/lib/commands/CLIENT_PAUSE.ts
@@ -1,20 +1,20 @@
-import { RedisCommandArguments } from '.';
-
-export function transformArguments(
-    timeout: number,
-    mode?: 'WRITE' | 'ALL'
-): RedisCommandArguments {
-    const args = [
-        'CLIENT',
-        'PAUSE',
-        timeout.toString()
-    ];
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Stops the server from processing client commands for the specified duration
+   * @param parser - The Redis command parser
+   * @param timeout - Time in milliseconds to pause command processing
+   * @param mode - Optional mode: 'WRITE' to pause only write commands, 'ALL' to pause all commands
+   */
+  parseCommand(parser: CommandParser, timeout: number, mode?: 'WRITE' | 'ALL') {
+    parser.push('CLIENT', 'PAUSE', timeout.toString());
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK' | Buffer;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts
index 96618f3f79f..b2b339c3d19 100644
--- a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts
+++ b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts
@@ -1,11 +1,21 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLIENT_SETNAME';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+
+import CLIENT_SETNAME from './CLIENT_SETNAME';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT SETNAME', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('name'),
-            ['CLIENT', 'SETNAME', 'name']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_SETNAME, 'name'),
+      ['CLIENT', 'SETNAME', 'name']
+    );
+  });
+
+  testUtils.testWithClient('client.clientSetName', async client => {
+    assert.equal(
+      await client.clientSetName('name'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_SETNAME.ts b/packages/client/lib/commands/CLIENT_SETNAME.ts
index f5cf1c786fb..7e4fcc17d94 100644
--- a/packages/client/lib/commands/CLIENT_SETNAME.ts
+++ b/packages/client/lib/commands/CLIENT_SETNAME.ts
@@ -1,7 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(name: RedisCommandArgument): RedisCommandArguments {
-    return ['CLIENT', 'SETNAME', name];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Assigns a name to the current connection
+   * @param parser - The Redis command parser
+   * @param name - The name to assign to the connection
+   */
+  parseCommand(parser: CommandParser, name: RedisArgument) {
+    parser.push('CLIENT', 'SETNAME', name);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts
index bbd0b13e777..032725635ee 100644
--- a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts
+++ b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts
@@ -1,101 +1,102 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_TRACKING';
+import CLIENT_TRACKING from './CLIENT_TRACKING';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT TRACKING', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        describe('true', () => {
-            it('simple', () => {
-                assert.deepEqual(
-                    transformArguments(true),
-                    ['CLIENT', 'TRACKING', 'ON']
-                );
-            });
+  describe('transformArguments', () => {
+    describe('true', () => {
+      it('simple', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_TRACKING, true),
+          ['CLIENT', 'TRACKING', 'ON']
+        );
+      });
 
-            it('with REDIRECT', () => {
-                assert.deepEqual(
-                    transformArguments(true, {
-                        REDIRECT: 1
-                    }),
-                    ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1']
-                );
-            });
+      it('with REDIRECT', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_TRACKING, true, {
+            REDIRECT: 1
+          }),
+          ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1']
+        );
+      });
 
-            describe('with BCAST', () => {
-                it('simple', () => {
-                    assert.deepEqual(
-                        transformArguments(true, {
-                            BCAST: true
-                        }),
-                        ['CLIENT', 'TRACKING', 'ON', 'BCAST']
-                    );
-                });
+      describe('with BCAST', () => {
+        it('simple', () => {
+          assert.deepEqual(
+            parseArgs(CLIENT_TRACKING, true, {
+              BCAST: true
+            }),
+            ['CLIENT', 'TRACKING', 'ON', 'BCAST']
+          );
+        });
 
-                describe('with PREFIX', () => {
-                    it('string', () => {
-                        assert.deepEqual(
-                            transformArguments(true, {
-                                BCAST: true,
-                                PREFIX: 'prefix'
-                            }),
-                            ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', 'prefix']
-                        );
-                    });
+        describe('with PREFIX', () => {
+          it('string', () => {
+            assert.deepEqual(
+              parseArgs(CLIENT_TRACKING, true, {
+                BCAST: true,
+                PREFIX: 'prefix'
+              }),
+              ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', 'prefix']
+            );
+          });
 
-                    it('array', () => {
-                        assert.deepEqual(
-                            transformArguments(true, {
-                                BCAST: true,
-                                PREFIX: ['1', '2']
-                            }),
-                            ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', '1', 'PREFIX', '2']
-                        );
-                    });
-                });
-            });
+          it('array', () => {
+            assert.deepEqual(
+              parseArgs(CLIENT_TRACKING, true, {
+                BCAST: true,
+                PREFIX: ['1', '2']
+              }),
+              ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', '1', 'PREFIX', '2']
+            );
+          });
+        });
+      });
 
-            it('with OPTIN', () => {
-                assert.deepEqual(
-                    transformArguments(true, {
-                        OPTIN: true
-                    }),
-                    ['CLIENT', 'TRACKING', 'ON', 'OPTIN']
-                );
-            });
+      it('with OPTIN', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_TRACKING, true, {
+            OPTIN: true
+          }),
+          ['CLIENT', 'TRACKING', 'ON', 'OPTIN']
+        );
+      });
 
-            it('with OPTOUT', () => {
-                assert.deepEqual(
-                    transformArguments(true, {
-                        OPTOUT: true
-                    }),
-                    ['CLIENT', 'TRACKING', 'ON', 'OPTOUT']
-                );
-            });
+      it('with OPTOUT', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_TRACKING, true, {
+            OPTOUT: true
+          }),
+          ['CLIENT', 'TRACKING', 'ON', 'OPTOUT']
+        );
+      });
 
-            it('with NOLOOP', () => {
-                assert.deepEqual(
-                    transformArguments(true, {
-                        NOLOOP: true
-                    }),
-                    ['CLIENT', 'TRACKING', 'ON', 'NOLOOP']
-                );
-            });
-        });
+      it('with NOLOOP', () => {
+        assert.deepEqual(
+          parseArgs(CLIENT_TRACKING, true, {
+            NOLOOP: true
+          }),
+          ['CLIENT', 'TRACKING', 'ON', 'NOLOOP']
+        );
+      });
+    });
 
-        it('false', () => {
-            assert.deepEqual(
-                transformArguments(false),
-                ['CLIENT', 'TRACKING', 'OFF']
-            );
-        });
+    it('false', () => {
+      assert.deepEqual(
+        parseArgs(CLIENT_TRACKING, false),
+        ['CLIENT', 'TRACKING', 'OFF']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.clientTracking', async client => {
-        assert.equal(
-            await client.clientTracking(false),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientTracking', async client => {
+    assert.equal(
+      await client.clientTracking(false),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_TRACKING.ts b/packages/client/lib/commands/CLIENT_TRACKING.ts
index c70702706e4..08bed098c20 100644
--- a/packages/client/lib/commands/CLIENT_TRACKING.ts
+++ b/packages/client/lib/commands/CLIENT_TRACKING.ts
@@ -1,83 +1,93 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
 interface CommonOptions {
-    REDIRECT?: number;
-    NOLOOP?: boolean;
+  REDIRECT?: number;
+  NOLOOP?: boolean;
 }
 
 interface BroadcastOptions {
-    BCAST?: boolean;
-    PREFIX?: RedisCommandArgument | Array<RedisCommandArgument>;
+  BCAST?: boolean;
+  PREFIX?: RedisVariadicArgument;
 }
 
 interface OptInOptions {
-    OPTIN?: boolean;
+  OPTIN?: boolean;
 }
 
 interface OptOutOptions {
-    OPTOUT?: boolean;
+  OPTOUT?: boolean;
 }
 
-type ClientTrackingOptions = CommonOptions & (
-    BroadcastOptions |
-    OptInOptions |
-    OptOutOptions
+export type ClientTrackingOptions = CommonOptions & (
+  BroadcastOptions |
+  OptInOptions |
+  OptOutOptions
 );
 
-export function transformArguments<M extends boolean>(
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Controls server-assisted client side caching for the current connection
+   * @param parser - The Redis command parser
+   * @param mode - Whether to enable (true) or disable (false) tracking
+   * @param options - Optional configuration including REDIRECT, BCAST, PREFIX, OPTIN, OPTOUT, and NOLOOP options
+   */
+  parseCommand<M extends boolean>(
+    parser: CommandParser,
     mode: M,
-    options?: M extends true ? ClientTrackingOptions : undefined
-): RedisCommandArguments {
-    const args: RedisCommandArguments = [
-        'CLIENT',
-        'TRACKING',
-        mode ? 'ON' : 'OFF'
-    ];
+    options?: M extends true ? ClientTrackingOptions : never
+  ) {
+    parser.push(
+      'CLIENT',
+      'TRACKING',
+      mode ? 'ON' : 'OFF'
+    );
 
     if (mode) {
-        if (options?.REDIRECT) {
-            args.push(
-                'REDIRECT',
-                options.REDIRECT.toString()
-            );
-        }
+      if (options?.REDIRECT) {
+        parser.push(
+          'REDIRECT',
+          options.REDIRECT.toString()
+        );
+      }
 
-        if (isBroadcast(options)) {
-            args.push('BCAST');
+      if (isBroadcast(options)) {
+        parser.push('BCAST');
 
-            if (options?.PREFIX) {
-                if (Array.isArray(options.PREFIX)) {
-                    for (const prefix of options.PREFIX) {
-                        args.push('PREFIX', prefix);
-                    }
-                } else {
-                    args.push('PREFIX', options.PREFIX);
-                }
+        if (options?.PREFIX) {
+          if (Array.isArray(options.PREFIX)) {
+            for (const prefix of options.PREFIX) {
+              parser.push('PREFIX', prefix);
             }
-        } else if (isOptIn(options)) {
-            args.push('OPTIN');
-        } else if (isOptOut(options)) {
-            args.push('OPTOUT');
+          } else {
+            parser.push('PREFIX', options.PREFIX);
+          }
         }
+      } else if (isOptIn(options)) {
+        parser.push('OPTIN');
+      } else if (isOptOut(options)) {
+        parser.push('OPTOUT');
+      }
 
-        if (options?.NOLOOP) {
-            args.push('NOLOOP');
-        }
+      if (options?.NOLOOP) {
+        parser.push('NOLOOP');
+      }
     }
-
-    return args;
-}
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
 function isBroadcast(options?: ClientTrackingOptions): options is BroadcastOptions {
-    return (options as BroadcastOptions)?.BCAST === true;
+  return (options as BroadcastOptions)?.BCAST === true;
 }
 
 function isOptIn(options?: ClientTrackingOptions): options is OptInOptions {
-    return (options as OptInOptions)?.OPTIN === true;
+  return (options as OptInOptions)?.OPTIN === true;
 }
 
 function isOptOut(options?: ClientTrackingOptions): options is OptOutOptions {
-    return (options as OptOutOptions)?.OPTOUT === true;
+  return (options as OptOutOptions)?.OPTOUT === true;
 }
-
-export declare function transformReply(): 'OK' | Buffer;
diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts
index 49bffe7612d..d776519df22 100644
--- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts
+++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts
@@ -1,25 +1,26 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_TRACKINGINFO';
+import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT TRACKINGINFO', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLIENT', 'TRACKINGINFO']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_TRACKINGINFO),
+      ['CLIENT', 'TRACKINGINFO']
+    );
+  });
 
-    testUtils.testWithClient('client.clientTrackingInfo', async client => {
-        assert.deepEqual(
-            await client.clientTrackingInfo(),
-            {
-                flags: new Set(['off']),
-                redirect: -1,
-                prefixes: []
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientTrackingInfo', async client => {
+    assert.deepEqual(
+      await client.clientTrackingInfo(),
+      {
+        flags: ['off'],
+        redirect: -1,
+        prefixes: []
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts
index 7c883fc6997..1fb19e9e60b 100644
--- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts
+++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts
@@ -1,28 +1,28 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { TuplesToMapReply, BlobStringReply, SetReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['CLIENT', 'TRACKINGINFO'];
-}
+type TrackingInfo = TuplesToMapReply<[
+  [BlobStringReply<'flags'>, SetReply<BlobStringReply>],
+  [BlobStringReply<'redirect'>, NumberReply],
+  [BlobStringReply<'prefixes'>, ArrayReply<BlobStringReply>]
+]>;
 
-type RawReply = [
-    'flags',
-    Array<string>,
-    'redirect',
-    number,
-    'prefixes',
-    Array<string>
-];
-
-interface Reply {
-    flags: Set<string>;
-    redirect: number;
-    prefixes: Array<string>;
-}
-
-export function transformReply(reply: RawReply): Reply {
-    return {
-        flags: new Set(reply[1]),
-        redirect: reply[3],
-        prefixes: reply[5]
-    };
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about the current connection's key tracking state
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLIENT', 'TRACKINGINFO');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<TrackingInfo>>) => ({
+      flags: reply[1],
+      redirect: reply[3],
+      prefixes: reply[5]
+    }),
+    3: undefined as unknown as () => TrackingInfo
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts
index 73c731ee87f..0b58cf6517e 100644
--- a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts
+++ b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLIENT_UNPAUSE';
+import CLIENT_UNPAUSE from './CLIENT_UNPAUSE';
+import { parseArgs } from './generic-transformers';
 
 describe('CLIENT UNPAUSE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLIENT', 'UNPAUSE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLIENT_UNPAUSE),
+      ['CLIENT', 'UNPAUSE']
+    );
+  });
 
-    testUtils.testWithClient('client.unpause', async client => {
-        assert.equal(
-            await client.clientUnpause(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.clientUnpause', async client => {
+    assert.equal(
+      await client.clientUnpause(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.ts
index e139436d004..2ac4fde0112 100644
--- a/packages/client/lib/commands/CLIENT_UNPAUSE.ts
+++ b/packages/client/lib/commands/CLIENT_UNPAUSE.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['CLIENT', 'UNPAUSE'];
-}
-
-export declare function transformReply(): 'OK' | Buffer;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Resumes processing of client commands after a CLIENT PAUSE
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLIENT', 'UNPAUSE');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
index c16476de436..4a9b1839bb4 100644
--- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_ADDSLOTS';
+import { strict as assert } from 'node:assert';
+import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER ADDSLOTS', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments(0),
-                ['CLUSTER', 'ADDSLOTS', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_ADDSLOTS, 0),
+        ['CLUSTER', 'ADDSLOTS', '0']
+      );
+    });
 
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments([0, 1]),
-                ['CLUSTER', 'ADDSLOTS', '0', '1']
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_ADDSLOTS, [0, 1]),
+        ['CLUSTER', 'ADDSLOTS', '0', '1']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
index 6cd357fb823..f833a42e5ad 100644
--- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
+++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
@@ -1,11 +1,17 @@
-import { RedisCommandArguments } from '.';
-import { pushVerdictNumberArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(slots: number | Array<number>): RedisCommandArguments {
-    return pushVerdictNumberArguments(
-        ['CLUSTER', 'ADDSLOTS'],
-        slots
-    );
-}
-
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Assigns hash slots to the current node in a Redis Cluster
+   * @param parser - The Redis command parser
+   * @param slots - One or more hash slots to be assigned
+   */
+  parseCommand(parser: CommandParser, slots: number | Array<number>) {
+    parser.push('CLUSTER', 'ADDSLOTS');
+    parser.pushVariadicNumber(slots);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts
index ebd1e3445ff..40706968f93 100644
--- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts
@@ -1,29 +1,33 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_ADDSLOTSRANGE';
+import { strict as assert } from 'node:assert';
+import testUtils from '../test-utils';
+import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER ADDSLOTSRANGE', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments({
-                    start: 0,
-                    end: 1
-                }),
-                ['CLUSTER', 'ADDSLOTSRANGE', '0', '1']
-            );
-        });
+  testUtils.isVersionGreaterThanHook([7, 0]);
 
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments([{
-                    start: 0,
-                    end: 1
-                }, {
-                    start: 2,
-                    end: 3
-                }]),
-                ['CLUSTER', 'ADDSLOTSRANGE', '0', '1', '2', '3']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_ADDSLOTSRANGE, {
+          start: 0,
+          end: 1
+        }),
+        ['CLUSTER', 'ADDSLOTSRANGE', '0', '1']
+      );
     });
+
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_ADDSLOTSRANGE, [{
+          start: 0,
+          end: 1
+        }, {
+          start: 2,
+          end: 3
+        }]),
+        ['CLUSTER', 'ADDSLOTSRANGE', '0', '1', '2', '3']
+      );
+    });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts
index 6a8d6dc668f..e3e7c8b4988 100644
--- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts
+++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts
@@ -1,13 +1,18 @@
-import { RedisCommandArguments } from '.';
-import { pushSlotRangesArguments, SlotRange } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import { parseSlotRangesArguments, SlotRange } from './generic-transformers';
 
-export function transformArguments(
-    ranges: SlotRange | Array<SlotRange>
-): RedisCommandArguments {
-    return pushSlotRangesArguments(
-        ['CLUSTER', 'ADDSLOTSRANGE'],
-        ranges
-    );
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Assigns hash slot ranges to the current node in a Redis Cluster
+   * @param parser - The Redis command parser
+   * @param ranges - One or more slot ranges to be assigned, each specified as [start, end]
+   */
+  parseCommand(parser: CommandParser, ranges: SlotRange | Array<SlotRange>) {
+    parser.push('CLUSTER', 'ADDSLOTSRANGE');
+    parseSlotRangesArguments(parser, ranges);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts
index edb68b3b3b0..f3ecde9f6a8 100644
--- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_BUMPEPOCH';
+import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER BUMPEPOCH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'BUMPEPOCH']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_BUMPEPOCH),
+      ['CLUSTER', 'BUMPEPOCH']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.equal(
-            typeof await client.clusterBumpEpoch(),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      typeof await client.clusterBumpEpoch(),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts
index 7f81c8fdc42..3cf9f3fb207 100644
--- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts
+++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts
@@ -1,5 +1,15 @@
-export function transformArguments(): Array<string> {
-    return ['CLUSTER', 'BUMPEPOCH'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): 'BUMPED' | 'STILL';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Advances the cluster config epoch
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'BUMPEPOCH');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'BUMPED' | 'STILL'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts
index 558110d0a28..06a901ef301 100644
--- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts
@@ -1,22 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_COUNT-FAILURE-REPORTS';
+import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER COUNT-FAILURE-REPORTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('0'),
-            ['CLUSTER', 'COUNT-FAILURE-REPORTS', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_COUNT_FAILURE_REPORTS, '0'),
+      ['CLUSTER', 'COUNT-FAILURE-REPORTS', '0']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterCountFailureReports', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.equal(
-            typeof await client.clusterCountFailureReports(
-                await client.clusterMyId()
-            ),
-            'number'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterCountFailureReports', async cluster => {
+    const [master] = cluster.masters,
+      client = await cluster.nodeClient(master);
+    assert.equal(
+      typeof await client.clusterCountFailureReports(master.id),
+      'number'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts
index 3fbc33052f8..b4421b1d8fa 100644
--- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts
+++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts
@@ -1,5 +1,16 @@
-export function transformArguments(nodeId: string): Array<string> {
-    return ['CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId];
-}
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of failure reports for a given node
+   * @param parser - The Redis command parser
+   * @param nodeId - The ID of the node to check
+   */
+  parseCommand(parser: CommandParser, nodeId: RedisArgument) {
+    parser.push('CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts
index 27ecbcfffa3..52848409465 100644
--- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_COUNTKEYSINSLOT';
+import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER COUNTKEYSINSLOT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(0),
-            ['CLUSTER', 'COUNTKEYSINSLOT', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_COUNTKEYSINSLOT, 0),
+      ['CLUSTER', 'COUNTKEYSINSLOT', '0']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterCountKeysInSlot', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.equal(
-            typeof await client.clusterCountKeysInSlot(0),
-            'number'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterCountKeysInSlot', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      typeof await client.clusterCountKeysInSlot(0),
+      'number'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts
index a5ff75e58a9..df97c79161e 100644
--- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts
+++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts
@@ -1,5 +1,16 @@
-export function transformArguments(slot: number): Array<string> {
-    return ['CLUSTER', 'COUNTKEYSINSLOT', slot.toString()];
-}
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of keys in the specified hash slot
+   * @param parser - The Redis command parser
+   * @param slot - The hash slot to check
+   */
+  parseCommand(parser: CommandParser, slot: number) {
+    parser.push('CLUSTER', 'COUNTKEYSINSLOT', slot.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts
index 85d13f4ed3d..2937fdd4d79 100644
--- a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_DELSLOTS';
+import { strict as assert } from 'node:assert';
+import CLUSTER_DELSLOTS from './CLUSTER_DELSLOTS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER DELSLOTS', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments(0),
-                ['CLUSTER', 'DELSLOTS', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_DELSLOTS, 0),
+        ['CLUSTER', 'DELSLOTS', '0']
+      );
+    });
 
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments([0, 1]),
-                ['CLUSTER', 'DELSLOTS', '0', '1']
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_DELSLOTS, [0, 1]),
+        ['CLUSTER', 'DELSLOTS', '0', '1']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts
index bf8d9c18900..eea7bcb699e 100644
--- a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts
+++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts
@@ -1,11 +1,17 @@
-import { RedisCommandArguments } from '.';
-import { pushVerdictNumberArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(slots: number | Array<number>): RedisCommandArguments {
-    return pushVerdictNumberArguments(
-        ['CLUSTER', 'DELSLOTS'],
-        slots
-    );
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Removes hash slots from the current node in a Redis Cluster
+   * @param parser - The Redis command parser
+   * @param slots - One or more hash slots to be removed
+   */
+  parseCommand(parser: CommandParser, slots: number | Array<number>) {
+    parser.push('CLUSTER', 'DELSLOTS');
+    parser.pushVariadicNumber(slots);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts
index 8fd50d01a54..6007421d11b 100644
--- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts
@@ -1,29 +1,30 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_DELSLOTSRANGE';
+import { strict as assert } from 'node:assert';
+import CLUSTER_DELSLOTSRANGE from './CLUSTER_DELSLOTSRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER DELSLOTSRANGE', () => {
-    describe('transformArguments', () => {
-        it('single', () => {
-            assert.deepEqual(
-                transformArguments({
-                    start: 0,
-                    end: 1
-                }),
-                ['CLUSTER', 'DELSLOTSRANGE', '0', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_DELSLOTSRANGE, {
+          start: 0,
+          end: 1
+        }),
+        ['CLUSTER', 'DELSLOTSRANGE', '0', '1']
+      );
+    });
 
-        it('multiple', () => {
-            assert.deepEqual(
-                transformArguments([{
-                    start: 0,
-                    end: 1
-                }, {
-                    start: 2,
-                    end: 3
-                }]),
-                ['CLUSTER', 'DELSLOTSRANGE', '0', '1', '2', '3']
-            );
-        });
+    it('multiple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_DELSLOTSRANGE, [{
+          start: 0,
+          end: 1
+        }, {
+          start: 2,
+          end: 3
+        }]),
+        ['CLUSTER', 'DELSLOTSRANGE', '0', '1', '2', '3']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts
index b136113c65f..32b27d8ea14 100644
--- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts
+++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts
@@ -1,13 +1,18 @@
-import { RedisCommandArguments } from '.';
-import { pushSlotRangesArguments, SlotRange } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import { parseSlotRangesArguments, SlotRange } from './generic-transformers';
 
-export function transformArguments(
-    ranges: SlotRange | Array<SlotRange>
-): RedisCommandArguments {
-    return pushSlotRangesArguments(
-        ['CLUSTER', 'DELSLOTSRANGE'],
-        ranges
-    );
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Removes hash slot ranges from the current node in a Redis Cluster
+   * @param parser - The Redis command parser
+   * @param ranges - One or more slot ranges to be removed, each specified as [start, end]
+   */
+  parseCommand(parser:CommandParser, ranges: SlotRange | Array<SlotRange>) {
+    parser.push('CLUSTER', 'DELSLOTSRANGE');
+    parseSlotRangesArguments(parser, ranges);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts
index 578ff56b9cd..f8e4b986048 100644
--- a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts
@@ -1,20 +1,23 @@
-import { strict as assert } from 'assert';
-import { FailoverModes, transformArguments } from './CLUSTER_FAILOVER';
+import { strict as assert } from 'node:assert';
+import CLUSTER_FAILOVER, { FAILOVER_MODES } from './CLUSTER_FAILOVER';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER FAILOVER', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['CLUSTER', 'FAILOVER']
-            );
-        });
-        
-        it('with mode', () => {
-            assert.deepEqual(
-                transformArguments(FailoverModes.FORCE),
-                ['CLUSTER', 'FAILOVER', 'FORCE']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_FAILOVER),
+        ['CLUSTER', 'FAILOVER']
+      );
     });
+
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_FAILOVER, {
+          mode: FAILOVER_MODES.FORCE
+        }),
+        ['CLUSTER', 'FAILOVER', 'FORCE']
+      );
+    });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.ts
index 9bc4b69f343..8a228c07349 100644
--- a/packages/client/lib/commands/CLUSTER_FAILOVER.ts
+++ b/packages/client/lib/commands/CLUSTER_FAILOVER.ts
@@ -1,16 +1,31 @@
-export enum FailoverModes {
-    FORCE = 'FORCE',
-    TAKEOVER = 'TAKEOVER'
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(mode?: FailoverModes): Array<string> {
-    const args = ['CLUSTER', 'FAILOVER'];
+export const FAILOVER_MODES = {
+  FORCE: 'FORCE',
+  TAKEOVER: 'TAKEOVER'
+} as const;
 
-    if (mode) {
-        args.push(mode);
-    }
+export type FailoverMode = typeof FAILOVER_MODES[keyof typeof FAILOVER_MODES];
 
-    return args;
+export interface ClusterFailoverOptions {
+  mode?: FailoverMode;
 }
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Forces a replica to perform a manual failover of its master
+   * @param parser - The Redis command parser
+   * @param options - Optional configuration with FORCE or TAKEOVER mode
+   */
+  parseCommand(parser:CommandParser, options?: ClusterFailoverOptions) {
+    parser.push('CLUSTER', 'FAILOVER');
+
+    if (options?.mode) {
+      parser.push(options.mode);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
index f91a9a70cfd..43701adfe6a 100644
--- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_FLUSHSLOTS';
+import { strict as assert } from 'node:assert';
+import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER FLUSHSLOTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'FLUSHSLOTS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_FLUSHSLOTS),
+      ['CLUSTER', 'FLUSHSLOTS']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
index dfb1e1ccde8..dac1f24c697 100644
--- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
+++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
@@ -1,5 +1,15 @@
-export function transformArguments(): Array<string> {
-    return ['CLUSTER', 'FLUSHSLOTS'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Deletes all hash slots from the current node in a Redis Cluster
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'FLUSHSLOTS');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts
index cadcdb678f3..8d02374cf87 100644
--- a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_FORGET';
+import { strict as assert } from 'node:assert';
+import CLUSTER_FORGET from './CLUSTER_FORGET';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER FORGET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('0'),
-            ['CLUSTER', 'FORGET', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_FORGET, '0'),
+      ['CLUSTER', 'FORGET', '0']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_FORGET.ts b/packages/client/lib/commands/CLUSTER_FORGET.ts
index fc557073aeb..ff7cb952121 100644
--- a/packages/client/lib/commands/CLUSTER_FORGET.ts
+++ b/packages/client/lib/commands/CLUSTER_FORGET.ts
@@ -1,5 +1,16 @@
-export function transformArguments(nodeId: string): Array<string> {
-    return  ['CLUSTER', 'FORGET', nodeId];
-}
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Removes a node from the cluster
+   * @param parser - The Redis command parser
+   * @param nodeId - The ID of the node to remove
+   */
+  parseCommand(parser: CommandParser, nodeId: RedisArgument) {
+    parser.push('CLUSTER', 'FORGET', nodeId);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
index 957b7de20cb..468eecc74a9 100644
--- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
@@ -1,21 +1,26 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_GETKEYSINSLOT';
+import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER GETKEYSINSLOT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(0, 10),
-            ['CLUSTER', 'GETKEYSINSLOT', '0', '10']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_GETKEYSINSLOT, 0, 10),
+      ['CLUSTER', 'GETKEYSINSLOT', '0', '10']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterGetKeysInSlot', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]),
-            reply = await client.clusterGetKeysInSlot(0, 1);
-        assert.ok(Array.isArray(reply));
-        for (const item of reply) {
-            assert.equal(typeof item, 'string');
-        }
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterGetKeysInSlot', async cluster => {
+    const slot = 12539, // "key" slot
+      client = await cluster.nodeClient(cluster.slots[slot].master),
+      [, reply] = await Promise.all([  
+        client.set('key', 'value'),
+        client.clusterGetKeysInSlot(slot, 1),
+      ])
+    assert.ok(Array.isArray(reply));
+    for (const item of reply) {
+      assert.equal(typeof item, 'string');
+    }
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
index ec75b7b7336..90756cf6302 100644
--- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
+++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
@@ -1,5 +1,17 @@
-export function transformArguments(slot: number, count: number): Array<string> {
-    return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()];
-}
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns a number of keys from the specified hash slot
+   * @param parser - The Redis command parser
+   * @param slot - The hash slot to get keys from
+   * @param count - Maximum number of keys to return
+   */
+  parseCommand(parser: CommandParser, slot: number, count: number) {
+    parser.push('CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_INFO.spec.ts b/packages/client/lib/commands/CLUSTER_INFO.spec.ts
index 69d5c4a8c56..01dafce8d53 100644
--- a/packages/client/lib/commands/CLUSTER_INFO.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_INFO.spec.ts
@@ -1,55 +1,21 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './CLUSTER_INFO';
+import CLUSTER_INFO from './CLUSTER_INFO';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'INFO']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_INFO),
+      ['CLUSTER', 'INFO']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([
-                'cluster_state:ok',
-                'cluster_slots_assigned:16384',
-                'cluster_slots_ok:16384',
-                'cluster_slots_pfail:0',
-                'cluster_slots_fail:0',
-                'cluster_known_nodes:6',
-                'cluster_size:3',
-                'cluster_current_epoch:6',
-                'cluster_my_epoch:2',
-                'cluster_stats_messages_sent:1483972',
-                'cluster_stats_messages_received:1483968'
-            ].join('\r\n')),
-            {
-                state: 'ok',
-                slots: {
-                    assigned: 16384,
-                    ok: 16384,
-                    pfail: 0,
-                    fail: 0
-                },
-                knownNodes: 6,
-                size: 3,
-                currentEpoch: 6,
-                myEpoch: 2,
-                stats: {
-                    messagesSent: 1483972,
-                    messagesReceived: 1483968
-                }
-            }
-        );
-    });
-
-    testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.notEqual(
-            await client.clusterInfo(),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      typeof await client.clusterInfo(),
+      'string'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_INFO.ts b/packages/client/lib/commands/CLUSTER_INFO.ts
index 634515f927c..fa220b9f645 100644
--- a/packages/client/lib/commands/CLUSTER_INFO.ts
+++ b/packages/client/lib/commands/CLUSTER_INFO.ts
@@ -1,47 +1,15 @@
-export function transformArguments(): Array<string> {
-    return ['CLUSTER', 'INFO'];
-}
-
-interface ClusterInfoReply {
-    state: string;
-    slots: {
-        assigned: number;
-        ok: number;
-        pfail: number;
-        fail: number;
-    };
-    knownNodes: number;
-    size: number;
-    currentEpoch: number;
-    myEpoch: number;
-    stats: {
-        messagesSent: number;
-        messagesReceived: number;
-    };
-}
-
-export function transformReply(reply: string): ClusterInfoReply {
-    const lines = reply.split('\r\n');
-
-    return {
-        state: extractLineValue(lines[0]),
-        slots: {
-            assigned: Number(extractLineValue(lines[1])),
-            ok: Number(extractLineValue(lines[2])),
-            pfail: Number(extractLineValue(lines[3])),
-            fail: Number(extractLineValue(lines[4]))
-        },
-        knownNodes: Number(extractLineValue(lines[5])),
-        size: Number(extractLineValue(lines[6])),
-        currentEpoch: Number(extractLineValue(lines[7])),
-        myEpoch: Number(extractLineValue(lines[8])),
-        stats: {
-            messagesSent: Number(extractLineValue(lines[9])),
-            messagesReceived: Number(extractLineValue(lines[10]))
-        }
-    };
-}
-
-export function extractLineValue(line: string): string {
-    return line.substring(line.indexOf(':') + 1);
-}
+import { CommandParser } from '../client/parser';
+import { VerbatimStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about the state of a Redis Cluster
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'INFO');
+  },
+  transformReply: undefined as unknown as () => VerbatimStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts
index 3bbc9f9cb2d..188c403abb5 100644
--- a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_KEYSLOT';
+import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER KEYSLOT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['CLUSTER', 'KEYSLOT', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_KEYSLOT, 'key'),
+      ['CLUSTER', 'KEYSLOT', 'key']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.equal(
-            typeof await client.clusterKeySlot('key'),
-            'number'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      typeof await client.clusterKeySlot('key'),
+      'number'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts
index 0af524ff128..1add4cb4f4a 100644
--- a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts
+++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts
@@ -1,5 +1,16 @@
-export function transformArguments(key: string): Array<string> {
-    return ['CLUSTER', 'KEYSLOT', key];
-}
+import { CommandParser } from '../client/parser';
+import { Command, NumberReply, RedisArgument } from '../RESP/types';
 
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the hash slot number for a given key
+   * @param parser - The Redis command parser
+   * @param key - The key to get the hash slot for
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('CLUSTER', 'KEYSLOT', key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts
index 982973e8ea5..609ecfd3da9 100644
--- a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_LINKS';
+import CLUSTER_LINKS from './CLUSTER_LINKS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER LINKS', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'LINKS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_LINKS),
+      ['CLUSTER', 'LINKS']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterLinks', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]),
-            links = await client.clusterLinks();
-        assert.ok(Array.isArray(links));
-        for (const link of links) {
-            assert.equal(typeof link.direction, 'string');
-            assert.equal(typeof link.node, 'string');
-            assert.equal(typeof link.createTime, 'number');
-            assert.equal(typeof link.events, 'string');
-            assert.equal(typeof link.sendBufferAllocated, 'number');
-            assert.equal(typeof link.sendBufferUsed, 'number');
-        }
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterLinks', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]),
+      links = await client.clusterLinks();
+    assert.ok(Array.isArray(links));
+    for (const link of links) {
+      assert.equal(typeof link.direction, 'string');
+      assert.equal(typeof link.node, 'string');
+      assert.equal(typeof link['create-time'], 'number');
+      assert.equal(typeof link.events, 'string');
+      assert.equal(typeof link['send-buffer-allocated'], 'number');
+      assert.equal(typeof link['send-buffer-used'], 'number');
+    }
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_LINKS.ts b/packages/client/lib/commands/CLUSTER_LINKS.ts
index 9a5608c102f..d35f4712650 100644
--- a/packages/client/lib/commands/CLUSTER_LINKS.ts
+++ b/packages/client/lib/commands/CLUSTER_LINKS.ts
@@ -1,38 +1,37 @@
-export function transformArguments(): Array<string> {
-    return ['CLUSTER', 'LINKS'];
-}
+import { CommandParser } from '../client/parser';
+import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-type ClusterLinksRawReply = Array<[
-    'direction',
-    string,
-    'node',
-    string,
-    'createTime',
-    number,
-    'events',
-    string,
-    'send-buffer-allocated',
-    number,
-    'send-buffer-used',
-    number
-]>;
+type ClusterLinksReply = ArrayReply<TuplesToMapReply<[
+  [BlobStringReply<'direction'>, BlobStringReply],
+  [BlobStringReply<'node'>, BlobStringReply],
+  [BlobStringReply<'create-time'>, NumberReply],
+  [BlobStringReply<'events'>, BlobStringReply],
+  [BlobStringReply<'send-buffer-allocated'>, NumberReply],
+  [BlobStringReply<'send-buffer-used'>, NumberReply],
+]>>;
 
-type ClusterLinksReply = Array<{
-    direction: string;
-    node: string;
-    createTime: number;
-    events: string;
-    sendBufferAllocated: number;
-    sendBufferUsed: number;
-}>;
-
-export function transformReply(reply: ClusterLinksRawReply): ClusterLinksReply {
-    return reply.map(peerLink => ({
-        direction: peerLink[1],
-        node: peerLink[3],
-        createTime: Number(peerLink[5]),
-        events: peerLink[7],
-        sendBufferAllocated: Number(peerLink[9]),
-        sendBufferUsed: Number(peerLink[11])
-    }));
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about all cluster links (lower level connections to other nodes)
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'LINKS');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<ClusterLinksReply>>) => reply.map(link => {
+      const unwrapped = link as unknown as UnwrapReply<typeof link>;
+      return {
+        direction: unwrapped[1],
+        node: unwrapped[3],
+        'create-time': unwrapped[5],
+        events: unwrapped[7],
+        'send-buffer-allocated': unwrapped[9],
+        'send-buffer-used': unwrapped[11]
+      };
+    }),
+    3: undefined as unknown as () => ClusterLinksReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_MEET.spec.ts b/packages/client/lib/commands/CLUSTER_MEET.spec.ts
index 50a5393efa2..6c063f34e45 100644
--- a/packages/client/lib/commands/CLUSTER_MEET.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_MEET.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_MEET';
+import { strict as assert } from 'node:assert';
+import CLUSTER_MEET from './CLUSTER_MEET';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER MEET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('127.0.0.1', 6379),
-            ['CLUSTER', 'MEET', '127.0.0.1', '6379']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_MEET, '127.0.0.1', 6379),
+      ['CLUSTER', 'MEET', '127.0.0.1', '6379']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_MEET.ts b/packages/client/lib/commands/CLUSTER_MEET.ts
index e6ce1c1fce4..e555250942c 100644
--- a/packages/client/lib/commands/CLUSTER_MEET.ts
+++ b/packages/client/lib/commands/CLUSTER_MEET.ts
@@ -1,5 +1,17 @@
-export function transformArguments(ip: string, port: number): Array<string> {
-    return ['CLUSTER', 'MEET', ip, port.toString()];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Initiates a handshake with another node in the cluster
+   * @param parser - The Redis command parser
+   * @param host - Host name or IP address of the node
+   * @param port - TCP port of the node
+   */
+  parseCommand(parser: CommandParser, host: string, port: number) {
+    parser.push('CLUSTER', 'MEET', host, port.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_MYID.spec.ts b/packages/client/lib/commands/CLUSTER_MYID.spec.ts
index f427d7058e2..78bb4495e3c 100644
--- a/packages/client/lib/commands/CLUSTER_MYID.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_MYID.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_MYID';
+import CLUSTER_MYID from './CLUSTER_MYID';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER MYID', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'MYID']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_MYID),
+      ['CLUSTER', 'MYID']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterMyId', async cluster => {
-        const [master] = cluster.masters,
-            client = await cluster.nodeClient(master);
-        assert.equal(
-            await client.clusterMyId(),
-            master.id
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterMyId', async cluster => {
+    const [master] = cluster.masters,
+      client = await cluster.nodeClient(master);
+    assert.equal(
+      await client.clusterMyId(),
+      master.id
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_MYID.ts b/packages/client/lib/commands/CLUSTER_MYID.ts
index 2b61684634d..ce4ad9c5ff2 100644
--- a/packages/client/lib/commands/CLUSTER_MYID.ts
+++ b/packages/client/lib/commands/CLUSTER_MYID.ts
@@ -1,5 +1,15 @@
-export function transformArguments(): Array<string> {
-    return  ['CLUSTER', 'MYID'];
-}
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the node ID of the current Redis Cluster node
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'MYID');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts
index 180289870ca..6c2a61801bc 100644
--- a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts
@@ -1,22 +1,23 @@
 import { strict as assert } from 'assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_MYSHARDID';
+import CLUSTER_MYSHARDID from './CLUSTER_MYSHARDID';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER MYSHARDID', () => {
-    testUtils.isVersionGreaterThanHook([7, 2]);
+  testUtils.isVersionGreaterThanHook([7, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'MYSHARDID']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_MYSHARDID),
+      ['CLUSTER', 'MYSHARDID']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterMyShardId', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.equal(
-            typeof await client.clusterMyShardId(),
-            'string'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterMyShardId', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      typeof await client.clusterMyShardId(),
+      'string'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts
index 1c4f8b82f56..72e38e4a901 100644
--- a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts
+++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts
@@ -1,7 +1,16 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments() {
-    return ['CLUSTER', 'MYSHARDID'];
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the shard ID of the current Redis Cluster node
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'MYSHARDID');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
 
-export declare function transformReply(): string | Buffer;
diff --git a/packages/client/lib/commands/CLUSTER_NODES.spec.ts b/packages/client/lib/commands/CLUSTER_NODES.spec.ts
index 5c6cb74d6cb..a49996586b7 100644
--- a/packages/client/lib/commands/CLUSTER_NODES.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_NODES.spec.ts
@@ -1,145 +1,21 @@
-import { strict as assert } from 'assert';
-import { RedisClusterNodeLinkStates, transformArguments, transformReply } from './CLUSTER_NODES';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CLUSTER_NODES from './CLUSTER_NODES';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER NODES', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'NODES']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_NODES),
+      ['CLUSTER', 'NODES']
+    );
+  });
 
-    describe('transformReply', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformReply([
-                    'master 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-16384',
-                    'slave 127.0.0.1:30002@31002 slave master 0 0 1 connected',
-                    ''
-                ].join('\n')),
-                [{
-                    id: 'master',
-                    address: '127.0.0.1:30001@31001',
-                    host: '127.0.0.1',
-                    port: 30001,
-                    cport: 31001,
-                    flags: ['myself', 'master'],
-                    pingSent: 0,
-                    pongRecv: 0,
-                    configEpoch: 1,
-                    linkState: RedisClusterNodeLinkStates.CONNECTED,
-                    slots: [{
-                        from: 0,
-                        to: 16384
-                    }],
-                    replicas: [{
-                        id: 'slave',
-                        address: '127.0.0.1:30002@31002',
-                        host: '127.0.0.1',
-                        port: 30002,
-                        cport: 31002,
-                        flags: ['slave'],
-                        pingSent: 0,
-                        pongRecv: 0,
-                        configEpoch: 1,
-                        linkState: RedisClusterNodeLinkStates.CONNECTED
-                    }]
-                }]
-            );
-        });
-
-        it('should support addresses without cport', () => {
-            assert.deepEqual(
-                transformReply(
-                    'id 127.0.0.1:30001 master - 0 0 0 connected 0-16384\n'
-                ),
-                [{
-                    id: 'id',
-                    address: '127.0.0.1:30001',
-                    host: '127.0.0.1',
-                    port: 30001,
-                    cport: null,
-                    flags: ['master'],
-                    pingSent: 0,
-                    pongRecv: 0,
-                    configEpoch: 0,
-                    linkState: RedisClusterNodeLinkStates.CONNECTED,
-                    slots: [{
-                        from: 0,
-                        to: 16384
-                    }],
-                    replicas: []
-                }]
-            );
-        });
-
-        it('should support ipv6 addresses', () => {
-            assert.deepEqual(
-                transformReply(
-                    'id 2a02:6b8:c21:330d:0:1589:ebbe:b1a0:6379@16379 master - 0 0 0 connected 0-549\n'
-                ),
-                [{
-                    id: 'id',
-                    address: '2a02:6b8:c21:330d:0:1589:ebbe:b1a0:6379@16379',
-                    host: '2a02:6b8:c21:330d:0:1589:ebbe:b1a0',
-                    port: 6379,
-                    cport: 16379,
-                    flags: ['master'],
-                    pingSent: 0,
-                    pongRecv: 0,
-                    configEpoch: 0,
-                    linkState: RedisClusterNodeLinkStates.CONNECTED,
-                    slots: [{
-                        from: 0,
-                        to: 549
-                    }],
-                    replicas: []
-                }]
-            );
-        });
-
-        it.skip('with importing slots', () => {
-            assert.deepEqual(
-                transformReply(
-                    'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0-<-16384\n'
-                ),
-                [{
-                    id: 'id',
-                    address: '127.0.0.1:30001@31001',
-                    host: '127.0.0.1',
-                    port: 30001,
-                    cport: 31001,
-                    flags: ['master'],
-                    pingSent: 0,
-                    pongRecv: 0,
-                    configEpoch: 0,
-                    linkState: RedisClusterNodeLinkStates.CONNECTED,
-                    slots: [], // TODO
-                    replicas: []
-                }]
-            );
-        });
-
-        it.skip('with migrating slots', () => {
-            assert.deepEqual(
-                transformReply(
-                    'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0->-16384\n'
-                ),
-                [{
-                    id: 'id',
-                    address: '127.0.0.1:30001@31001',
-                    host: '127.0.0.1',
-                    port: 30001,
-                    cport: 31001,
-                    flags: ['master'],
-                    pingSent: 0,
-                    pongRecv: 0,
-                    configEpoch: 0,
-                    linkState: RedisClusterNodeLinkStates.CONNECTED,
-                    slots: [], // TODO
-                    replicas: []
-                }]
-            );
-        });
-    });
+  testUtils.testWithCluster('clusterNode.clusterNodes', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      typeof await client.clusterNodes(),
+      'string'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_NODES.ts b/packages/client/lib/commands/CLUSTER_NODES.ts
index 7c433da5f12..5ce8388e9c6 100644
--- a/packages/client/lib/commands/CLUSTER_NODES.ts
+++ b/packages/client/lib/commands/CLUSTER_NODES.ts
@@ -1,105 +1,15 @@
-export function transformArguments(): Array<string> {
-    return ['CLUSTER', 'NODES'];
-}
-
-export enum RedisClusterNodeLinkStates {
-    CONNECTED = 'connected',
-    DISCONNECTED = 'disconnected'
-}
-
-interface RedisClusterNodeAddress {
-    host: string;
-    port: number;
-    cport: number | null;
-}
-
-export interface RedisClusterReplicaNode extends RedisClusterNodeAddress {
-    id: string;
-    address: string;
-    flags: Array<string>;
-    pingSent: number;
-    pongRecv: number;
-    configEpoch: number;
-    linkState: RedisClusterNodeLinkStates;
-}
-
-export interface RedisClusterMasterNode extends RedisClusterReplicaNode {
-    slots: Array<{
-        from: number;
-        to: number;
-    }>;
-    replicas: Array<RedisClusterReplicaNode>;
-}
-
-export function transformReply(reply: string): Array<RedisClusterMasterNode> {
-    const lines = reply.split('\n');
-    lines.pop(); // last line is empty
-
-    const mastersMap = new Map<string, RedisClusterMasterNode>(),
-        replicasMap = new Map<string, Array<RedisClusterReplicaNode>>();
-
-    for (const line of lines) {
-        const [id, address, flags, masterId, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '),
-            node = {
-                id,
-                address,
-                ...transformNodeAddress(address),
-                flags: flags.split(','),
-                pingSent: Number(pingSent),
-                pongRecv: Number(pongRecv),
-                configEpoch: Number(configEpoch),
-                linkState: (linkState as RedisClusterNodeLinkStates)
-            };
-
-        if (masterId === '-') {
-            let replicas = replicasMap.get(id);
-            if (!replicas) {
-                replicas = [];
-                replicasMap.set(id, replicas);
-            }
-
-            mastersMap.set(id, {
-                ...node,
-                slots: slots.map(slot => {
-                    // TODO: importing & exporting (https://redis.io/commands/cluster-nodes#special-slot-entries)
-                    const [fromString, toString] = slot.split('-', 2),
-                        from = Number(fromString);
-                    return {
-                        from,
-                        to: toString ? Number(toString) : from
-                    };
-                }),
-                replicas
-            });
-        } else {
-            const replicas = replicasMap.get(masterId);
-            if (!replicas) {
-                replicasMap.set(masterId, [node]);
-            } else {
-                replicas.push(node);
-            }
-        }
-    }
-
-    return [...mastersMap.values()];
-}
-
-function transformNodeAddress(address: string): RedisClusterNodeAddress {
-    const indexOfColon = address.lastIndexOf(':'),
-        indexOfAt = address.indexOf('@', indexOfColon),
-        host = address.substring(0, indexOfColon);
-
-    if (indexOfAt === -1) {
-        return {
-            host,
-            port: Number(address.substring(indexOfColon + 1)),
-            cport: null
-        };
-    }
-
-    return {
-        host: address.substring(0, indexOfColon),
-        port: Number(address.substring(indexOfColon + 1, indexOfAt)),
-        cport: Number(address.substring(indexOfAt + 1))
-    };
-}
+import { CommandParser } from '../client/parser';
+import { VerbatimStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns serialized information about the nodes in a Redis Cluster
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'NODES');
+  },
+  transformReply: undefined as unknown as () => VerbatimStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts
index 6c902dc0d82..11bf086bb66 100644
--- a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts
@@ -1,11 +1,22 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_REPLICAS';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CLUSTER_REPLICAS from './CLUSTER_REPLICAS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER REPLICAS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('0'),
-            ['CLUSTER', 'REPLICAS', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_REPLICAS, '0'),
+      ['CLUSTER', 'REPLICAS', '0']
+    );
+  });
+
+  testUtils.testWithCluster('clusterNode.clusterReplicas', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]),
+      reply = await client.clusterReplicas(cluster.masters[0].id);
+    assert.ok(Array.isArray(reply));
+    for (const replica of reply) {
+      assert.equal(typeof replica, 'string');  
+    }
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.ts
index a4130125fbf..6c205eb73c6 100644
--- a/packages/client/lib/commands/CLUSTER_REPLICAS.ts
+++ b/packages/client/lib/commands/CLUSTER_REPLICAS.ts
@@ -1,5 +1,16 @@
-export function transformArguments(nodeId: string): Array<string> {
-    return ['CLUSTER', 'REPLICAS', nodeId];
-}
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export { transformReply } from './CLUSTER_NODES';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the replica nodes replicating from the specified primary node
+   * @param parser - The Redis command parser
+   * @param nodeId - Node ID of the primary node
+   */
+  parseCommand(parser: CommandParser, nodeId: RedisArgument) {
+    parser.push('CLUSTER', 'REPLICAS', nodeId);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts
index 926b7dd0a77..3f130d360bf 100644
--- a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_REPLICATE';
+import { strict as assert } from 'node:assert';
+import CLUSTER_REPLICATE from './CLUSTER_REPLICATE';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER REPLICATE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('0'),
-            ['CLUSTER', 'REPLICATE', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_REPLICATE, '0'),
+      ['CLUSTER', 'REPLICATE', '0']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.ts
index c74e1ec5960..54fb19f460f 100644
--- a/packages/client/lib/commands/CLUSTER_REPLICATE.ts
+++ b/packages/client/lib/commands/CLUSTER_REPLICATE.ts
@@ -1,5 +1,16 @@
-export function transformArguments(nodeId: string): Array<string> {
-    return ['CLUSTER', 'REPLICATE', nodeId];
-}
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Reconfigures a node as a replica of the specified primary node
+   * @param parser - The Redis command parser
+   * @param nodeId - Node ID of the primary node to replicate
+   */
+  parseCommand(parser: CommandParser, nodeId: RedisArgument) {
+    parser.push('CLUSTER', 'REPLICATE', nodeId);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_RESET.spec.ts b/packages/client/lib/commands/CLUSTER_RESET.spec.ts
index 340da7457c1..1ef55e3f572 100644
--- a/packages/client/lib/commands/CLUSTER_RESET.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_RESET.spec.ts
@@ -1,20 +1,23 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_RESET';
+import { strict as assert } from 'node:assert';
+import CLUSTER_RESET from './CLUSTER_RESET';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER RESET', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['CLUSTER', 'RESET']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_RESET),
+        ['CLUSTER', 'RESET']
+      );
+    });
 
-        it('with mode', () => {
-            assert.deepEqual(
-                transformArguments('HARD'),
-                ['CLUSTER', 'RESET', 'HARD']
-            );
-        });
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_RESET, {
+          mode: 'HARD'
+        }),
+        ['CLUSTER', 'RESET', 'HARD']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_RESET.ts b/packages/client/lib/commands/CLUSTER_RESET.ts
index c6901e045dc..baa29e9e8b2 100644
--- a/packages/client/lib/commands/CLUSTER_RESET.ts
+++ b/packages/client/lib/commands/CLUSTER_RESET.ts
@@ -1,11 +1,24 @@
-export function transformArguments(mode?: 'HARD' | 'SOFT'): Array<string> {
-    const args = ['CLUSTER', 'RESET'];
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-    if (mode) {
-        args.push(mode);
-    }
-
-    return args;
+export interface ClusterResetOptions {
+  mode?: 'HARD' | 'SOFT';
 }
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Resets a Redis Cluster node, clearing all information and returning it to a brand new state
+   * @param parser - The Redis command parser
+   * @param options - Options for the reset operation
+   */
+  parseCommand(parser: CommandParser, options?: ClusterResetOptions) {
+    parser.push('CLUSTER', 'RESET');
+
+    if (options?.mode) {
+      parser.push(options.mode);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts
index 81ba4aa2509..a0d317ffae4 100644
--- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CLUSTER_SAVECONFIG';
+import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER SAVECONFIG', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'SAVECONFIG']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_SAVECONFIG),
+      ['CLUSTER', 'SAVECONFIG']
+    );
+  });
 
-    testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => {
-        const client = await cluster.nodeClient(cluster.masters[0]);
-        assert.equal(
-            await client.clusterSaveConfig(),
-            'OK'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]);
+    assert.equal(
+      await client.clusterSaveConfig(),
+      'OK'
+    );
+  }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts
index 7e7fb181cc6..8a0529b2c6b 100644
--- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts
+++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts
@@ -1,5 +1,16 @@
-export function transformArguments(): Array<string> {
-    return  ['CLUSTER', 'SAVECONFIG'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Forces a Redis Cluster node to save the cluster configuration to disk
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'SAVECONFIG');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-export declare function transformReply(): 'OK';
diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts
index dd241574168..fb02ee2fe65 100644
--- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CLUSTER_SET-CONFIG-EPOCH';
+import { strict as assert } from 'node:assert';
+import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER SET-CONFIG-EPOCH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(0),
-            ['CLUSTER', 'SET-CONFIG-EPOCH', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_SET_CONFIG_EPOCH, 0),
+      ['CLUSTER', 'SET-CONFIG-EPOCH', '0']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts
index c50a6b9d3a5..bcd89f52f8c 100644
--- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts
+++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts
@@ -1,5 +1,16 @@
-export function transformArguments(configEpoch: number): Array<string> {
-    return  ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString()];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Sets the configuration epoch for a Redis Cluster node
+   * @param parser - The Redis command parser
+   * @param configEpoch - The configuration epoch to set
+   */
+  parseCommand(parser: CommandParser, configEpoch: number) {
+    parser.push('CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts
index 0f46aafd13e..fac496c3afb 100644
--- a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
-import { ClusterSlotStates, transformArguments } from './CLUSTER_SETSLOT';
+import { strict as assert } from 'node:assert';
+import CLUSTER_SETSLOT, { CLUSTER_SLOT_STATES } from './CLUSTER_SETSLOT';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER SETSLOT', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(0, ClusterSlotStates.IMPORTING),
-                ['CLUSTER', 'SETSLOT', '0', 'IMPORTING']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_SETSLOT, 0, CLUSTER_SLOT_STATES.IMPORTING),
+        ['CLUSTER', 'SETSLOT', '0', 'IMPORTING']
+      );
+    });
 
-        it('with nodeId', () => {
-            assert.deepEqual(
-                transformArguments(0, ClusterSlotStates.IMPORTING, 'nodeId'),
-                ['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId']
-            );
-        });
+    it('with nodeId', () => {
+      assert.deepEqual(
+        parseArgs(CLUSTER_SETSLOT, 0, CLUSTER_SLOT_STATES.IMPORTING, 'nodeId'),
+        ['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.ts
index c01505c71a3..d50982e2d11 100644
--- a/packages/client/lib/commands/CLUSTER_SETSLOT.ts
+++ b/packages/client/lib/commands/CLUSTER_SETSLOT.ts
@@ -1,22 +1,31 @@
-export enum ClusterSlotStates {
-    IMPORTING = 'IMPORTING',
-    MIGRATING = 'MIGRATING',
-    STABLE = 'STABLE',
-    NODE = 'NODE'
-}
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(
-    slot: number,
-    state: ClusterSlotStates,
-    nodeId?: string
-): Array<string> {
-    const args = ['CLUSTER', 'SETSLOT', slot.toString(), state];
+export const CLUSTER_SLOT_STATES = {
+  IMPORTING: 'IMPORTING',
+  MIGRATING: 'MIGRATING',
+  STABLE: 'STABLE',
+  NODE: 'NODE'
+} as const;
 
-    if (nodeId) {
-        args.push(nodeId);
-    }
+export type ClusterSlotState = typeof CLUSTER_SLOT_STATES[keyof typeof CLUSTER_SLOT_STATES];
 
-    return args;
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Assigns a hash slot to a specific Redis Cluster node
+   * @param parser - The Redis command parser
+   * @param slot - The slot number to assign
+   * @param state - The state to set for the slot (IMPORTING, MIGRATING, STABLE, NODE)
+   * @param nodeId - Node ID (required for IMPORTING, MIGRATING, and NODE states)
+   */
+  parseCommand(parser: CommandParser, slot: number, state: ClusterSlotState, nodeId?: RedisArgument) {
+    parser.push('CLUSTER', 'SETSLOT', slot.toString(), state);
 
-export declare function transformReply(): 'OK';
+    if (nodeId) {
+      parser.push(nodeId);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts
index 6efbfe13ce1..28879b036ae 100644
--- a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts
+++ b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts
@@ -1,76 +1,31 @@
-import { strict as assert } from 'assert';
-import { transformArguments, transformReply } from './CLUSTER_SLOTS';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CLUSTER_SLOTS from './CLUSTER_SLOTS';
+import { parseArgs } from './generic-transformers';
 
 describe('CLUSTER SLOTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CLUSTER', 'SLOTS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CLUSTER_SLOTS),
+      ['CLUSTER', 'SLOTS']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([
-                [
-                    0,
-                    5460,
-                    ['127.0.0.1', 30001, '09dbe9720cda62f7865eabc5fd8857c5d2678366'],
-                    ['127.0.0.1', 30004, '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf']
-                ],
-                [
-                    5461,
-                    10922,
-                    ['127.0.0.1', 30002, 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'],
-                    ['127.0.0.1', 30005, 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f']
-                ],
-                [
-                    10923,
-                    16383,
-                    ['127.0.0.1', 30003, '044ec91f325b7595e76dbcb18cc688b6a5b434a1'],
-                    ['127.0.0.1', 30006, '58e6e48d41228013e5d9c1c37c5060693925e97e']
-                ]
-            ]),
-            [{
-                from: 0,
-                to: 5460,
-                master: {
-                    ip: '127.0.0.1',
-                    port: 30001,
-                    id: '09dbe9720cda62f7865eabc5fd8857c5d2678366'
-                },
-                replicas: [{
-                    ip: '127.0.0.1',
-                    port: 30004,
-                    id: '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf'
-                }]
-            }, {
-                from: 5461,
-                to: 10922,
-                master: {
-                    ip: '127.0.0.1',
-                    port: 30002,
-                    id: 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'
-                },
-                replicas: [{
-                    ip: '127.0.0.1',
-                    port: 30005,
-                    id: 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f'
-                }]
-            }, {
-                from: 10923,
-                to: 16383,
-                master: {
-                    ip: '127.0.0.1',
-                    port: 30003,
-                    id: '044ec91f325b7595e76dbcb18cc688b6a5b434a1'
-                },
-                replicas: [{
-                    ip: '127.0.0.1',
-                    port: 30006,
-                    id: '58e6e48d41228013e5d9c1c37c5060693925e97e'
-                }]
-            }]
-        );
-    });
+  testUtils.testWithCluster('clusterNode.clusterSlots', async cluster => {
+    const client = await cluster.nodeClient(cluster.masters[0]),
+      slots = await client.clusterSlots();
+    assert.ok(Array.isArray(slots));
+    for (const { from, to, master, replicas } of slots) {
+      assert.equal(typeof from, 'number');
+      assert.equal(typeof to, 'number');
+      assert.equal(typeof master.host, 'string');
+      assert.equal(typeof master.port, 'number');
+      assert.equal(typeof master.id, 'string');
+      for (const replica of replicas) {
+        assert.equal(typeof replica.host, 'string');
+        assert.equal(typeof replica.port, 'number');
+        assert.equal(typeof replica.id, 'string');
+      }
+    }
+  }, GLOBAL.CLUSTERS.WITH_REPLICAS);
 });
diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.ts b/packages/client/lib/commands/CLUSTER_SLOTS.ts
index 20d9782dd9e..ca6ad9cb9e2 100644
--- a/packages/client/lib/commands/CLUSTER_SLOTS.ts
+++ b/packages/client/lib/commands/CLUSTER_SLOTS.ts
@@ -1,46 +1,46 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { TuplesReply, BlobStringReply, NumberReply, ArrayReply, UnwrapReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['CLUSTER', 'SLOTS'];
-}
-
-type ClusterSlotsRawNode = [ip: string, port: number, id: string];
-
-type ClusterSlotsRawReply = Array<[
-    from: number,
-    to: number,
-    master: ClusterSlotsRawNode,
-    ...replicas: Array<ClusterSlotsRawNode>
+type RawNode = TuplesReply<[
+  host: BlobStringReply,
+  port: NumberReply,
+  id: BlobStringReply
 ]>;
 
-export interface ClusterSlotsNode {
-    ip: string;
-    port: number;
-    id: string;
-};
+type ClusterSlotsRawReply = ArrayReply<[
+  from: NumberReply,
+  to: NumberReply,
+  master: RawNode,
+  ...replicas: Array<RawNode>
+]>;
 
-export type ClusterSlotsReply = Array<{
-    from: number;
-    to: number;
-    master: ClusterSlotsNode;
-    replicas: Array<ClusterSlotsNode>;
-}>;
+export type ClusterSlotsNode = ReturnType<typeof transformNode>;
 
-export function transformReply(reply: ClusterSlotsRawReply): ClusterSlotsReply {
-    return reply.map(([from, to, master, ...replicas]) => {
-        return {
-            from,
-            to,
-            master: transformNode(master),
-            replicas: replicas.map(transformNode)
-        };
-    });
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about which Redis Cluster node handles which hash slots
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CLUSTER', 'SLOTS');
+  },
+  transformReply(reply: UnwrapReply<ClusterSlotsRawReply>) {
+    return reply.map(([from, to, master, ...replicas]) => ({
+      from,
+      to,
+      master: transformNode(master),
+      replicas: replicas.map(transformNode)
+    }));
+  }
+} as const satisfies Command;
 
-function transformNode([ip, port, id]: ClusterSlotsRawNode): ClusterSlotsNode {
-    return {
-        ip,
-        port,
-        id
-    };
+function transformNode(node: RawNode) {
+  const [host, port, id] = node as unknown as UnwrapReply<typeof node>;
+  return {
+    host,
+    port,
+    id
+  };
 }
diff --git a/packages/client/lib/commands/COMMAND.spec.ts b/packages/client/lib/commands/COMMAND.spec.ts
index baad79845ab..860ffc30685 100644
--- a/packages/client/lib/commands/COMMAND.spec.ts
+++ b/packages/client/lib/commands/COMMAND.spec.ts
@@ -1,17 +1,17 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './COMMAND';
-import { assertPingCommand } from './COMMAND_INFO.spec';
+// import { strict as assert } from 'node:assert';
+// import testUtils, { GLOBAL } from '../test-utils';
+// import { transformArguments } from './COMMAND';
+// import { assertPingCommand } from './COMMAND_INFO.spec';
 
-describe('COMMAND', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['COMMAND']
-        );
-    });
+// describe('COMMAND', () => {
+//     it('transformArguments', () => {
+//         assert.deepEqual(
+//             transformArguments(),
+//             ['COMMAND']
+//         );
+//     });
 
-    testUtils.testWithClient('client.command', async client => {
-        assertPingCommand((await client.command()).find(command => command.name === 'ping'));
-    }, GLOBAL.SERVERS.OPEN);
-});
+//     testUtils.testWithClient('client.command', async client => {
+//         assertPingCommand((await client.command()).find(command => command.name === 'ping'));
+//     }, GLOBAL.SERVERS.OPEN);
+// });
diff --git a/packages/client/lib/commands/COMMAND.ts b/packages/client/lib/commands/COMMAND.ts
index b6ee50b2f4c..3d24b716a0d 100644
--- a/packages/client/lib/commands/COMMAND.ts
+++ b/packages/client/lib/commands/COMMAND.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, UnwrapReply } from '../RESP/types';
 import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(): RedisCommandArguments {
-    return ['COMMAND'];
-}
-
-export function transformReply(reply: Array<CommandRawReply>): Array<CommandReply> {
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns an array with details about all Redis commands
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('COMMAND');
+  },
+  // TODO: This works, as we don't currently handle any of the items returned as a map
+  transformReply(reply: UnwrapReply<ArrayReply<CommandRawReply>>): Array<CommandReply> {
     return reply.map(transformCommandReply);
-}
+  }
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/COMMAND_COUNT.spec.ts b/packages/client/lib/commands/COMMAND_COUNT.spec.ts
index 71482382f67..a36091df482 100644
--- a/packages/client/lib/commands/COMMAND_COUNT.spec.ts
+++ b/packages/client/lib/commands/COMMAND_COUNT.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './COMMAND_COUNT';
+import COMMAND_COUNT from './COMMAND_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('COMMAND COUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['COMMAND', 'COUNT']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(COMMAND_COUNT),
+      ['COMMAND', 'COUNT']
+    );
+  });
 
-    testUtils.testWithClient('client.commandCount', async client => {
-        assert.equal(
-            typeof await client.commandCount(),
-            'number'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.commandCount', async client => {
+    assert.equal(
+      typeof await client.commandCount(),
+      'number'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/COMMAND_COUNT.ts b/packages/client/lib/commands/COMMAND_COUNT.ts
index 34c6a088da6..36b35a58d7b 100644
--- a/packages/client/lib/commands/COMMAND_COUNT.ts
+++ b/packages/client/lib/commands/COMMAND_COUNT.ts
@@ -1,9 +1,15 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(): RedisCommandArguments {
-    return ['COMMAND', 'COUNT'];
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the total number of commands available in the Redis server
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('COMMAND', 'COUNT');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts
index a92d032c5d6..332e2d51fbd 100644
--- a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts
+++ b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './COMMAND_GETKEYS';
+import COMMAND_GETKEYS from './COMMAND_GETKEYS';
+import { parseArgs } from './generic-transformers';
 
 describe('COMMAND GETKEYS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(['GET', 'key']),
-            ['COMMAND', 'GETKEYS', 'GET', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(COMMAND_GETKEYS, ['GET', 'key']),
+      ['COMMAND', 'GETKEYS', 'GET', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.commandGetKeys', async client => {
-        assert.deepEqual(
-            await client.commandGetKeys(['GET', 'key']),
-            ['key']
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.commandGetKeys', async client => {
+    assert.deepEqual(
+      await client.commandGetKeys(['GET', 'key']),
+      ['key']
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.ts b/packages/client/lib/commands/COMMAND_GETKEYS.ts
index 6762fe4b58a..6f447c4d4d6 100644
--- a/packages/client/lib/commands/COMMAND_GETKEYS.ts
+++ b/packages/client/lib/commands/COMMAND_GETKEYS.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(args: Array<RedisCommandArgument>): RedisCommandArguments {
-    return ['COMMAND', 'GETKEYS', ...args];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Extracts the key names from a Redis command
+   * @param parser - The Redis command parser
+   * @param args - Command arguments to analyze
+   */
+  parseCommand(parser: CommandParser, args: Array<RedisArgument>) {
+    parser.push('COMMAND', 'GETKEYS');
+    parser.push(...args);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts
index d568ed0e508..49652762d65 100644
--- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts
+++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts
@@ -1,24 +1,24 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './COMMAND_GETKEYSANDFLAGS';
+// import { strict as assert } from 'node:assert';
+// import testUtils, { GLOBAL } from '../test-utils';
+// import { transformArguments } from './COMMAND_GETKEYSANDFLAGS';
 
-describe('COMMAND GETKEYSANDFLAGS', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+// describe('COMMAND GETKEYSANDFLAGS', () => {
+//     testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(['GET', 'key']),
-            ['COMMAND', 'GETKEYSANDFLAGS', 'GET', 'key']
-        );
-    });
+//     it('transformArguments', () => {
+//         assert.deepEqual(
+//             transformArguments(['GET', 'key']),
+//             ['COMMAND', 'GETKEYSANDFLAGS', 'GET', 'key']
+//         );
+//     });
 
-    testUtils.testWithClient('client.commandGetKeysAndFlags', async client => {
-        assert.deepEqual(
-            await client.commandGetKeysAndFlags(['GET', 'key']),
-            [{
-                key: 'key',
-                flags: ['RO', 'access']
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
+//     testUtils.testWithClient('client.commandGetKeysAndFlags', async client => {
+//         assert.deepEqual(
+//             await client.commandGetKeysAndFlags(['GET', 'key']),
+//             [{
+//                 key: 'key',
+//                 flags: ['RO', 'access']
+//             }]
+//         );
+//     }, GLOBAL.SERVERS.OPEN);
+// });
diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts
index 96b28186ccd..1677adb43ec 100644
--- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts
+++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts
@@ -1,24 +1,30 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, SetReply, UnwrapReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
+export type CommandGetKeysAndFlagsRawReply = ArrayReply<TuplesReply<[
+  key: BlobStringReply,
+  flags: SetReply<BlobStringReply>
+]>>;
 
-export function transformArguments(args: Array<RedisCommandArgument>): RedisCommandArguments {
-    return ['COMMAND', 'GETKEYSANDFLAGS', ...args];
-}
-
-type KeysAndFlagsRawReply = Array<[
-    RedisCommandArgument,
-    RedisCommandArguments
-]>;
-
-type KeysAndFlagsReply = Array<{
-    key: RedisCommandArgument;
-    flags: RedisCommandArguments;
-}>;
-
-export function transformReply(reply: KeysAndFlagsRawReply): KeysAndFlagsReply {
-    return reply.map(([key, flags]) => ({
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Extracts the key names and access flags from a Redis command
+   * @param parser - The Redis command parser
+   * @param args - Command arguments to analyze
+   */
+  parseCommand(parser: CommandParser, args: Array<RedisArgument>) {
+    parser.push('COMMAND', 'GETKEYSANDFLAGS');
+    parser.push(...args);
+  },
+  transformReply(reply: UnwrapReply<CommandGetKeysAndFlagsRawReply>) {
+    return reply.map(entry => {
+      const [key, flags] = entry as unknown as UnwrapReply<typeof entry>;
+      return {
         key,
         flags
-    }));
-}
+      };
+    });
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/COMMAND_INFO.spec.ts b/packages/client/lib/commands/COMMAND_INFO.spec.ts
index c54a5d0aeb3..fd8c22ae803 100644
--- a/packages/client/lib/commands/COMMAND_INFO.spec.ts
+++ b/packages/client/lib/commands/COMMAND_INFO.spec.ts
@@ -1,49 +1,49 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './COMMAND_INFO';
-import { CommandCategories, CommandFlags, CommandReply } from './generic-transformers';
+// import { strict as assert } from 'node:assert';
+// import testUtils, { GLOBAL } from '../test-utils';
+// import { transformArguments } from './COMMAND_INFO';
+// import { CommandCategories, CommandFlags, CommandReply } from './generic-transformers';
 
-export function assertPingCommand(commandInfo: CommandReply | null | undefined): void {
-    assert.deepEqual(
-        commandInfo,
-        {
-            name: 'ping',
-            arity: -1,
-            flags: new Set(
-                testUtils.isVersionGreaterThan([7]) ?
-                    [CommandFlags.FAST] :
-                    [CommandFlags.STALE, CommandFlags.FAST]
-            ),
-            firstKeyIndex: 0,
-            lastKeyIndex: 0,
-            step: 0,
-            categories: new Set(
-                testUtils.isVersionGreaterThan([6]) ?
-                    [CommandCategories.FAST, CommandCategories.CONNECTION] :
-                    []
-            )
-        }
-    );
-}
+// export function assertPingCommand(commandInfo: CommandReply | null | undefined): void {
+//     assert.deepEqual(
+//         commandInfo,
+//         {
+//             name: 'ping',
+//             arity: -1,
+//             flags: new Set(
+//                 testUtils.isVersionGreaterThan([7]) ?
+//                     [CommandFlags.FAST] :
+//                     [CommandFlags.STALE, CommandFlags.FAST]
+//             ),
+//             firstKeyIndex: 0,
+//             lastKeyIndex: 0,
+//             step: 0,
+//             categories: new Set(
+//                 testUtils.isVersionGreaterThan([6]) ?
+//                     [CommandCategories.FAST, CommandCategories.CONNECTION] :
+//                     []
+//             )
+//         }
+//     );
+// }
 
-describe('COMMAND INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(['PING']),
-            ['COMMAND', 'INFO', 'PING']
-        );
-    });
+// describe('COMMAND INFO', () => {
+//     it('transformArguments', () => {
+//         assert.deepEqual(
+//             transformArguments(['PING']),
+//             ['COMMAND', 'INFO', 'PING']
+//         );
+//     });
 
-    describe('client.commandInfo', () => {
-        testUtils.testWithClient('PING', async client => {
-            assertPingCommand((await client.commandInfo(['PING']))[0]);
-        }, GLOBAL.SERVERS.OPEN);
+//     describe('client.commandInfo', () => {
+//         testUtils.testWithClient('PING', async client => {
+//             assertPingCommand((await client.commandInfo(['PING']))[0]);
+//         }, GLOBAL.SERVERS.OPEN);
 
-        testUtils.testWithClient('DOSE_NOT_EXISTS', async client => {
-            assert.deepEqual(
-                await client.commandInfo(['DOSE_NOT_EXISTS']),
-                [null]
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
-});
+//         testUtils.testWithClient('DOSE_NOT_EXISTS', async client => {
+//             assert.deepEqual(
+//                 await client.commandInfo(['DOSE_NOT_EXISTS']),
+//                 [null]
+//             );
+//         }, GLOBAL.SERVERS.OPEN);
+//     });
+// });
diff --git a/packages/client/lib/commands/COMMAND_INFO.ts b/packages/client/lib/commands/COMMAND_INFO.ts
index 6f84d0edaf9..19629f0ccae 100644
--- a/packages/client/lib/commands/COMMAND_INFO.ts
+++ b/packages/client/lib/commands/COMMAND_INFO.ts
@@ -1,12 +1,20 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, UnwrapReply } from '../RESP/types';
 import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(commands: Array<string>): RedisCommandArguments {
-    return ['COMMAND', 'INFO', ...commands];
-}
-
-export function transformReply(reply: Array<CommandRawReply | null>): Array<CommandReply | null> {
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns details about specific Redis commands
+   * @param parser - The Redis command parser
+   * @param commands - Array of command names to get information about
+   */
+  parseCommand(parser: CommandParser, commands: Array<string>) {
+    parser.push('COMMAND', 'INFO', ...commands);
+  },
+  // TODO: This works, as we don't currently handle any of the items returned as a map
+  transformReply(reply: UnwrapReply<ArrayReply<CommandRawReply>>): Array<CommandReply | null> {
     return reply.map(command => command ? transformCommandReply(command) : null);
-}
+  }
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/COMMAND_LIST.spec.ts b/packages/client/lib/commands/COMMAND_LIST.spec.ts
index eef747d9378..d2ee9e66161 100644
--- a/packages/client/lib/commands/COMMAND_LIST.spec.ts
+++ b/packages/client/lib/commands/COMMAND_LIST.spec.ts
@@ -1,56 +1,63 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, FilterBy } from './COMMAND_LIST';
+import COMMAND_LIST from './COMMAND_LIST';
+import { parseArgs } from './generic-transformers';
 
 describe('COMMAND LIST', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['COMMAND', 'LIST']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(COMMAND_LIST),
+        ['COMMAND', 'LIST']
+      );
+    });
 
-        describe('with FILTERBY', () => {
-            it('MODULE', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filterBy: FilterBy.MODULE,
-                        value: 'json'
-                    }),
-                    ['COMMAND', 'LIST', 'FILTERBY', 'MODULE', 'json']
-                );
-            });
+    describe('with FILTERBY', () => {
+      it('MODULE', () => {
+        assert.deepEqual(
+          parseArgs(COMMAND_LIST, {
+            FILTERBY: {
+              type: 'MODULE',
+              value: 'JSON'
+            }
+          }),
+          ['COMMAND', 'LIST', 'FILTERBY', 'MODULE', 'JSON']
+        );
+      });
 
-            it('ACLCAT', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filterBy: FilterBy.ACLCAT,
-                        value: 'admin'
-                    }),
-                    ['COMMAND', 'LIST', 'FILTERBY', 'ACLCAT', 'admin']
-                );
-            });
+      it('ACLCAT', () => {
+        assert.deepEqual(
+          parseArgs(COMMAND_LIST, {
+            FILTERBY: {
+              type: 'ACLCAT',
+              value: 'admin'
+            }
+          }),
+          ['COMMAND', 'LIST', 'FILTERBY', 'ACLCAT', 'admin']
+        );
+      });
 
-            it('PATTERN', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        filterBy: FilterBy.PATTERN,
-                        value: 'a*'
-                    }),
-                    ['COMMAND', 'LIST', 'FILTERBY', 'PATTERN', 'a*']
-                );
-            });
-        });
+      it('PATTERN', () => {
+        assert.deepEqual(
+          parseArgs(COMMAND_LIST, {
+            FILTERBY: {
+              type: 'PATTERN',
+              value: 'a*'
+            }
+          }),
+          ['COMMAND', 'LIST', 'FILTERBY', 'PATTERN', 'a*']
+        );
+      });
     });
+  });
 
-    testUtils.testWithClient('client.commandList', async client => {
-        const commandList = await client.commandList();
-        assert.ok(Array.isArray(commandList));
-        for (const command of commandList) {
-            assert.ok(typeof command === 'string');
-        }
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.commandList', async client => {
+    const commandList = await client.commandList();
+    assert.ok(Array.isArray(commandList));
+    for (const command of commandList) {
+      assert.ok(typeof command === 'string');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/COMMAND_LIST.ts b/packages/client/lib/commands/COMMAND_LIST.ts
index a197bd1a4c6..fa218d86aa7 100644
--- a/packages/client/lib/commands/COMMAND_LIST.ts
+++ b/packages/client/lib/commands/COMMAND_LIST.ts
@@ -1,31 +1,39 @@
-import { RedisCommandArguments } from '.';
-
-export const IS_READ_ONLY = true;
-
-export enum FilterBy {
-    MODULE = 'MODULE',
-    ACLCAT = 'ACLCAT',
-    PATTERN = 'PATTERN'
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+
+export const COMMAND_LIST_FILTER_BY = {
+  MODULE: 'MODULE',
+  ACLCAT: 'ACLCAT',
+  PATTERN: 'PATTERN'
+} as const;
+
+export type CommandListFilterBy = typeof COMMAND_LIST_FILTER_BY[keyof typeof COMMAND_LIST_FILTER_BY];
+
+export interface CommandListOptions {
+  FILTERBY?: {
+    type: CommandListFilterBy;
+    value: RedisArgument;
+  };
 }
 
-interface Filter {
-    filterBy: FilterBy;
-    value: string;
-}
-
-
-export function transformArguments(filter?: Filter): RedisCommandArguments {
-    const args = ['COMMAND', 'LIST'];
-
-    if (filter) {
-        args.push(
-            'FILTERBY',
-            filter.filterBy,
-            filter.value
-        );
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns a list of all commands supported by the Redis server
+   * @param parser - The Redis command parser
+   * @param options - Options for filtering the command list
+   */
+  parseCommand(parser: CommandParser, options?: CommandListOptions) {
+    parser.push('COMMAND', 'LIST');
+
+    if (options?.FILTERBY) {
+      parser.push(
+        'FILTERBY',
+        options.FILTERBY.type,
+        options.FILTERBY.value
+      );
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<string>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CONFIG_GET.spec.ts b/packages/client/lib/commands/CONFIG_GET.spec.ts
index 83b5c410cfb..c3f0eac76dd 100644
--- a/packages/client/lib/commands/CONFIG_GET.spec.ts
+++ b/packages/client/lib/commands/CONFIG_GET.spec.ts
@@ -1,11 +1,60 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CONFIG_GET';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CONFIG_GET from './CONFIG_GET';
+import { parseArgs } from './generic-transformers';
 
 describe('CONFIG GET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('*'),
-            ['CONFIG', 'GET', '*']
-        );
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(CONFIG_GET, '*'),
+        ['CONFIG', 'GET', '*']
+      );
     });
+
+    it('Array', () => {
+      assert.deepEqual(
+        parseArgs(CONFIG_GET, ['1', '2']),
+        ['CONFIG', 'GET', '1', '2']
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.configGet', async client => {
+    const config = await client.configGet('*');
+    assert.equal(typeof config, 'object');
+    for (const [key, value] of Object.entries(config)) {
+      assert.equal(typeof key, 'string');
+      assert.equal(typeof value, 'string');
+    }
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('client.configSet.getSearchConfigSettingTest | Redis >= 8', async client => {
+    assert.ok(
+      await client.configGet('search-timeout'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('client.configSet.getTSConfigSettingTest | Redis >= 8', async client => {
+    assert.ok(
+      await client.configGet('ts-retention-policy'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('client.configSet.getBFConfigSettingTest | Redis >= 8', async client => {
+    assert.ok(
+      await client.configGet('bf-error-rate'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('client.configSet.getCFConfigSettingTest | Redis >= 8', async client => {
+    assert.ok(
+      await client.configGet('cf-initial-size'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
 });
diff --git a/packages/client/lib/commands/CONFIG_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts
index 3afc0eddfd0..d0c80297fc4 100644
--- a/packages/client/lib/commands/CONFIG_GET.ts
+++ b/packages/client/lib/commands/CONFIG_GET.ts
@@ -1,5 +1,21 @@
-export function transformArguments(parameter: string): Array<string> {
-    return ['CONFIG', 'GET', parameter];
-}
+import { CommandParser } from '../client/parser';
+import { MapReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument, transformTuplesReply } from './generic-transformers';
 
-export { transformTuplesReply as transformReply } from './generic-transformers';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets the values of configuration parameters
+   * @param parser - The Redis command parser
+   * @param parameters - Pattern or specific configuration parameter names
+   */
+  parseCommand(parser: CommandParser, parameters: RedisVariadicArgument) {
+    parser.push('CONFIG', 'GET');
+    parser.pushVariadic(parameters);
+  },
+  transformReply: {
+    2: transformTuplesReply<BlobStringReply>,
+    3: undefined as unknown as () => MapReply<BlobStringReply, BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts
index d3f3048b944..f2f573df0dc 100644
--- a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts
+++ b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CONFIG_RESETSTAT';
+import { strict as assert } from 'node:assert';
+import CONFIG_RESETSTAT from './CONFIG_RESETSTAT';
+import { parseArgs } from './generic-transformers';
 
 describe('CONFIG RESETSTAT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CONFIG', 'RESETSTAT']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CONFIG_RESETSTAT),
+      ['CONFIG', 'RESETSTAT']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.ts
index aba54bc3c7b..356a9b29a79 100644
--- a/packages/client/lib/commands/CONFIG_RESETSTAT.ts
+++ b/packages/client/lib/commands/CONFIG_RESETSTAT.ts
@@ -1,5 +1,15 @@
-export function transformArguments(): Array<string> {
-    return ['CONFIG', 'RESETSTAT'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Resets the statistics reported by Redis using the INFO command
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CONFIG', 'RESETSTAT');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts
index cbc3e5b59d8..bc006e84c80 100644
--- a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts
+++ b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CONFIG_REWRITE';
+import { strict as assert } from 'node:assert';
+import CONFIG_REWRITE from './CONFIG_REWRITE';
+import { parseArgs } from './generic-transformers';
 
 describe('CONFIG REWRITE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['CONFIG', 'REWRITE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CONFIG_REWRITE),
+      ['CONFIG', 'REWRITE']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/CONFIG_REWRITE.ts b/packages/client/lib/commands/CONFIG_REWRITE.ts
index 67984adf300..a9f2e0a41ba 100644
--- a/packages/client/lib/commands/CONFIG_REWRITE.ts
+++ b/packages/client/lib/commands/CONFIG_REWRITE.ts
@@ -1,5 +1,15 @@
-export function transformArguments(): Array<string> {
-    return ['CONFIG', 'REWRITE'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Rewrites the Redis configuration file with the current configuration
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('CONFIG', 'REWRITE');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/CONFIG_SET.spec.ts b/packages/client/lib/commands/CONFIG_SET.spec.ts
index 93a7a6ff25e..f9f34dec937 100644
--- a/packages/client/lib/commands/CONFIG_SET.spec.ts
+++ b/packages/client/lib/commands/CONFIG_SET.spec.ts
@@ -1,24 +1,42 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './CONFIG_SET';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CONFIG_SET from './CONFIG_SET';
+import { parseArgs } from './generic-transformers';
 
 describe('CONFIG SET', () => {
-    describe('transformArguments', () => {
-        it('set one parameter (old version)', () => {
-            assert.deepEqual(
-                transformArguments('parameter', 'value'),
-                ['CONFIG', 'SET', 'parameter', 'value']
-            );
-        });
+  describe('transformArguments', () => {
+    it('set one parameter (old version)', () => {
+      assert.deepEqual(
+        parseArgs(CONFIG_SET, 'parameter', 'value'),
+        ['CONFIG', 'SET', 'parameter', 'value']
+      );
+    });
 
-        it('set muiltiple parameters', () => {
-            assert.deepEqual(
-                transformArguments({
-                    1: 'a',
-                    2: 'b',
-                    3: 'c'
-                }),
-                ['CONFIG', 'SET', '1', 'a', '2', 'b', '3', 'c']
-            );
-        });
+    it('set muiltiple parameters', () => {
+      assert.deepEqual(
+        parseArgs(CONFIG_SET, {
+          1: 'a',
+          2: 'b',
+          3: 'c'
+        }),
+        ['CONFIG', 'SET', '1', 'a', '2', 'b', '3', 'c']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.configSet', async client => {
+    assert.equal(
+      await client.configSet('maxmemory', '0'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClient('client.configSet.setReadOnlySearchConfigTest | Redis >= 8',
+    async client => {
+      assert.rejects(
+        client.configSet('search-max-doctablesize', '0'),
+        new Error('ERR CONFIG SET failed (possibly related to argument \'search-max-doctablesize\') - can\'t set immutable config')
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
 });
diff --git a/packages/client/lib/commands/CONFIG_SET.ts b/packages/client/lib/commands/CONFIG_SET.ts
index 41f40d035d2..81b4c65c1d9 100644
--- a/packages/client/lib/commands/CONFIG_SET.ts
+++ b/packages/client/lib/commands/CONFIG_SET.ts
@@ -1,23 +1,32 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '../RESP/types';
 
-type SingleParameter = [parameter: RedisCommandArgument, value: RedisCommandArgument];
+type SingleParameter = [parameter: RedisArgument, value: RedisArgument];
 
-type MultipleParameters = [config: Record<string, RedisCommandArgument>];
+type MultipleParameters = [config: Record<string, RedisArgument>];
 
-export function transformArguments(
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Sets configuration parameters to the specified values
+   * @param parser - The Redis command parser
+   * @param parameterOrConfig - Either a single parameter name or a configuration object
+   * @param value - Value for the parameter (when using single parameter format)
+   */
+  parseCommand(
+    parser: CommandParser,
     ...[parameterOrConfig, value]: SingleParameter | MultipleParameters
-): RedisCommandArguments {
-    const args: RedisCommandArguments = ['CONFIG', 'SET'];
-
-    if (typeof parameterOrConfig === 'string') {
-        args.push(parameterOrConfig, value!);
+  ) {
+    parser.push('CONFIG', 'SET');
+  
+    if (typeof parameterOrConfig === 'string' || parameterOrConfig instanceof Buffer) {
+      parser.push(parameterOrConfig, value!);
     } else {
-        for (const [key, value] of Object.entries(parameterOrConfig)) {
-            args.push(key, value);
-        }
+      for (const [key, value] of Object.entries(parameterOrConfig)) {
+        parser.push(key, value);
+      }
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/COPY.spec.ts b/packages/client/lib/commands/COPY.spec.ts
index 0d68e969cdb..cd0c6ec9fbe 100644
--- a/packages/client/lib/commands/COPY.spec.ts
+++ b/packages/client/lib/commands/COPY.spec.ts
@@ -1,67 +1,55 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './COPY';
+import COPY from './COPY';
+import { parseArgs } from './generic-transformers';
 
 describe('COPY', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
-
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination'),
-                ['COPY', 'source', 'destination']
-            );
-        });
-
-        it('with destination DB flag', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    destinationDb: 1
-                }),
-                ['COPY', 'source', 'destination', 'DB', '1']
-            );
-        });
-
-        it('with replace flag', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    replace: true
-                }),
-                ['COPY', 'source', 'destination', 'REPLACE']
-            );
-        });
-
-        it('with both flags', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    destinationDb: 1,
-                    replace: true
-                }),
-                ['COPY', 'source', 'destination', 'DB', '1', 'REPLACE']
-            );
-        });
+  testUtils.isVersionGreaterThanHook([6, 2]);
+
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(COPY, 'source', 'destination'),
+        ['COPY', 'source', 'destination']
+      );
     });
 
-    describe('transformReply', () => {
-        it('0', () => {
-            assert.equal(
-                transformReply(0),
-                false
-            );
-        });
+    it('with destination DB flag', () => {
+      assert.deepEqual(
+        parseArgs(COPY, 'source', 'destination', {
+          DB: 1
+        }),
+        ['COPY', 'source', 'destination', 'DB', '1']
+      );
+    });
 
-        it('1', () => {
-            assert.equal(
-                transformReply(1),
-                true
-            );
-        });
+    it('with replace flag', () => {
+      assert.deepEqual(
+        parseArgs(COPY, 'source', 'destination', {
+          REPLACE: true
+        }),
+        ['COPY', 'source', 'destination', 'REPLACE']
+      );
     });
 
-    testUtils.testWithClient('client.copy', async client => {
-        assert.equal(
-            await client.copy('source', 'destination'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with both flags', () => {
+      assert.deepEqual(
+        parseArgs(COPY, 'source', 'destination', {
+          DB: 1,
+          REPLACE: true
+        }),
+        ['COPY', 'source', 'destination', 'DB', '1', 'REPLACE']
+      );
+    });
+  });
+
+  testUtils.testAll('copy', async client => {
+    assert.equal(
+      await client.copy('{tag}source', '{tag}destination'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/COPY.ts b/packages/client/lib/commands/COPY.ts
index b1e212a9956..0d8af5636df 100644
--- a/packages/client/lib/commands/COPY.ts
+++ b/packages/client/lib/commands/COPY.ts
@@ -1,28 +1,31 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-interface CopyCommandOptions {
-    destinationDb?: number;
-    replace?: boolean;
+export interface CopyCommandOptions {
+  DB?: number;
+  REPLACE?: boolean;
 }
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Copies the value stored at the source key to the destination key
+   * @param parser - The Redis command parser
+   * @param source - Source key
+   * @param destination - Destination key
+   * @param options - Options for the copy operation
+   */
+  parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) {
+    parser.push('COPY');
+    parser.pushKeys([source, destination]);
 
-export function transformArguments(
-    source: RedisCommandArgument,
-    destination: RedisCommandArgument,
-    options?: CopyCommandOptions
-): RedisCommandArguments {
-    const args = ['COPY', source, destination];
-
-    if (options?.destinationDb) {
-        args.push('DB', options.destinationDb.toString());
+    if (options?.DB) {
+      parser.push('DB', options.DB.toString());
     }
 
-    if (options?.replace) {
-        args.push('REPLACE');
+    if (options?.REPLACE) {
+      parser.push('REPLACE');
     }
-
-    return args;
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/DBSIZE.spec.ts b/packages/client/lib/commands/DBSIZE.spec.ts
index a014a46e6e2..5778e30de3e 100644
--- a/packages/client/lib/commands/DBSIZE.spec.ts
+++ b/packages/client/lib/commands/DBSIZE.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DBSIZE';
+import DBSIZE from './DBSIZE';
+import { parseArgs } from './generic-transformers';
 
 describe('DBSIZE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['DBSIZE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DBSIZE),
+      ['DBSIZE']
+    );
+  });
 
-    testUtils.testWithClient('client.dbSize', async client => {
-        assert.equal(
-            await client.dbSize(),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.dbSize', async client => {
+    assert.equal(
+      await client.dbSize(),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/DBSIZE.ts b/packages/client/lib/commands/DBSIZE.ts
index 6b442ec33a2..b5777b63f7d 100644
--- a/packages/client/lib/commands/DBSIZE.ts
+++ b/packages/client/lib/commands/DBSIZE.ts
@@ -1,7 +1,15 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['DBSIZE'];
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of keys in the current database
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('DBSIZE');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/DECR.spec.ts b/packages/client/lib/commands/DECR.spec.ts
index 75e1205feda..69ff5a5391f 100644
--- a/packages/client/lib/commands/DECR.spec.ts
+++ b/packages/client/lib/commands/DECR.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DECR';
+import DECR from './DECR';
+import { parseArgs } from './generic-transformers';
 
 describe('DECR', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['DECR', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DECR, 'key'),
+      ['DECR', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.decr', async client => {
-        assert.equal(
-            await client.decr('key'),
-            -1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('decr', async client => {
+    assert.equal(
+      await client.decr('key'),
+      -1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/DECR.ts b/packages/client/lib/commands/DECR.ts
index 2b5f2c4bb5c..5155fba81f5 100644
--- a/packages/client/lib/commands/DECR.ts
+++ b/packages/client/lib/commands/DECR.ts
@@ -1,9 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['DECR', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Decrements the integer value of a key by one
+   * @param parser - The Redis command parser
+   * @param key - Key to decrement
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('DECR');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/DECRBY.spec.ts b/packages/client/lib/commands/DECRBY.spec.ts
index d2c23e94728..ae80fd714e0 100644
--- a/packages/client/lib/commands/DECRBY.spec.ts
+++ b/packages/client/lib/commands/DECRBY.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DECRBY';
+import DECRBY from './DECRBY';
+import { parseArgs } from './generic-transformers';
 
 describe('DECRBY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 2),
-            ['DECRBY', 'key', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DECRBY, 'key', 2),
+      ['DECRBY', 'key', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.decrBy', async client => {
-        assert.equal(
-            await client.decrBy('key', 2),
-            -2
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('decrBy', async client => {
+    assert.equal(
+      await client.decrBy('key', 2),
+      -2
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/DECRBY.ts b/packages/client/lib/commands/DECRBY.ts
index afe4d79f0a1..9f35ee15a26 100644
--- a/packages/client/lib/commands/DECRBY.ts
+++ b/packages/client/lib/commands/DECRBY.ts
@@ -1,12 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    decrement: number
-): RedisCommandArguments {
-    return ['DECRBY', key, decrement.toString()];
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Decrements the integer value of a key by the given number
+   * @param parser - The Redis command parser
+   * @param key - Key to decrement
+   * @param decrement - Decrement amount
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, decrement: number) {
+    parser.push('DECRBY');
+    parser.pushKey(key);
+    parser.push(decrement.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/DEL.spec.ts b/packages/client/lib/commands/DEL.spec.ts
index 75a29a8f641..3d0364e7523 100644
--- a/packages/client/lib/commands/DEL.spec.ts
+++ b/packages/client/lib/commands/DEL.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DEL';
+import DEL from './DEL';
+import { parseArgs } from './generic-transformers';
 
 describe('DEL', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['DEL', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(DEL, 'key'),
+        ['DEL', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['key1', 'key2']),
-                ['DEL', 'key1', 'key2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(DEL, ['key1', 'key2']),
+        ['DEL', 'key1', 'key2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.del', async client => {
-        assert.equal(
-            await client.del('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('del', async client => {
+    assert.equal(
+      await client.del('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/DEL.ts b/packages/client/lib/commands/DEL.ts
index d60abe0f28e..7ad1b1160e7 100644
--- a/packages/client/lib/commands/DEL.ts
+++ b/packages/client/lib/commands/DEL.ts
@@ -1,12 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['DEL'], keys);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes the specified keys. A key is ignored if it does not exist
+   * @param parser - The Redis command parser
+   * @param keys - One or more keys to delete
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('DEL');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/DISCARD.spec.ts b/packages/client/lib/commands/DISCARD.spec.ts
index b01f9d650d9..7aa769fc2ee 100644
--- a/packages/client/lib/commands/DISCARD.spec.ts
+++ b/packages/client/lib/commands/DISCARD.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './DISCARD';
+import { strict as assert } from 'node:assert';
+import DISCARD from './DISCARD';
+import { parseArgs } from './generic-transformers';
 
 describe('DISCARD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['DISCARD']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DISCARD),
+      ['DISCARD']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/DISCARD.ts b/packages/client/lib/commands/DISCARD.ts
index acad8a722e1..d8c8c83791e 100644
--- a/packages/client/lib/commands/DISCARD.ts
+++ b/packages/client/lib/commands/DISCARD.ts
@@ -1,7 +1,13 @@
-import { RedisCommandArgument } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['DISCARD'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  /**
+   * Discards a transaction, forgetting all queued commands
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('DISCARD');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/DUMP.spec.ts b/packages/client/lib/commands/DUMP.spec.ts
index aebbf4f3f7c..76fb2ec7c18 100644
--- a/packages/client/lib/commands/DUMP.spec.ts
+++ b/packages/client/lib/commands/DUMP.spec.ts
@@ -1,11 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
+import DUMP from './DUMP';
+import { parseArgs } from './generic-transformers';
 
 describe('DUMP', () => {
-    testUtils.testWithClient('client.dump', async client => {
-        assert.equal(
-            await client.dump('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DUMP, 'key'),
+      ['DUMP', 'key']
+    );
+  });
+
+  testUtils.testAll('client.dump', async client => {
+    assert.equal(
+      await client.dump('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/DUMP.ts b/packages/client/lib/commands/DUMP.ts
index fd4354db45c..c4905cc71c4 100644
--- a/packages/client/lib/commands/DUMP.ts
+++ b/packages/client/lib/commands/DUMP.ts
@@ -1,9 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['DUMP', key];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns a serialized version of the value stored at the key
+   * @param parser - The Redis command parser
+   * @param key - Key to dump
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('DUMP');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ECHO.spec.ts b/packages/client/lib/commands/ECHO.spec.ts
index 27f6b2a17d3..38fd1d4270e 100644
--- a/packages/client/lib/commands/ECHO.spec.ts
+++ b/packages/client/lib/commands/ECHO.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ECHO';
+import ECHO from './ECHO';
+import { parseArgs } from './generic-transformers';
 
 describe('ECHO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('message'),
-            ['ECHO', 'message']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ECHO, 'message'),
+      ['ECHO', 'message']
+    );
+  });
 
-    testUtils.testWithClient('client.echo', async client => {
-        assert.equal(
-            await client.echo('message'),
-            'message'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.echo', async client => {
+    assert.equal(
+      await client.echo('message'),
+      'message'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ECHO.ts b/packages/client/lib/commands/ECHO.ts
index 7a837307e2b..b346ade50b1 100644
--- a/packages/client/lib/commands/ECHO.ts
+++ b/packages/client/lib/commands/ECHO.ts
@@ -1,9 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(message: RedisCommandArgument): RedisCommandArguments {
-    return ['ECHO', message];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the given string
+   * @param parser - The Redis command parser
+   * @param message - Message to echo back
+   */
+  parseCommand(parser: CommandParser, message: RedisArgument) {
+    parser.push('ECHO', message);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EVAL.spec.ts b/packages/client/lib/commands/EVAL.spec.ts
index 7aa029362fd..8ef16eed835 100644
--- a/packages/client/lib/commands/EVAL.spec.ts
+++ b/packages/client/lib/commands/EVAL.spec.ts
@@ -1,29 +1,26 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EVAL';
+import EVAL from './EVAL';
+import { parseArgs } from './generic-transformers';
 
 describe('EVAL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('return KEYS[1] + ARGV[1]', {
-                keys: ['key'],
-                arguments: ['argument']
-            }),
-            ['EVAL', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EVAL, 'return KEYS[1] + ARGV[1]', {
+        keys: ['key'],
+        arguments: ['argument']
+      }),
+      ['EVAL', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument']
+    );
+  });
 
-    testUtils.testWithClient('client.eval', async client => {
-        assert.equal(
-            await client.eval('return 1'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.eval', async cluster => {
-        assert.equal(
-            await cluster.eval('return 1'),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('eval', async client => {
+    assert.equal(
+      await client.eval('return 1'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/EVAL.ts b/packages/client/lib/commands/EVAL.ts
index a82f8bf0aad..ff244c82aaf 100644
--- a/packages/client/lib/commands/EVAL.ts
+++ b/packages/client/lib/commands/EVAL.ts
@@ -1,7 +1,39 @@
-import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ReplyUnion, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = evalFirstKeyIndex;
+export interface EvalOptions {
+  keys?: Array<RedisArgument>;
+  arguments?: Array<RedisArgument>;
+}
+
+export function parseEvalArguments(
+  parser: CommandParser,
+  script: RedisArgument,
+  options?: EvalOptions
+) {
+  parser.push(script);
+  if (options?.keys) {
+    parser.pushKeysLength(options.keys);
+  } else {
+    parser.push('0');
+  }
 
-export function transformArguments(script: string, options?: EvalOptions): Array<string> {
-    return pushEvalArguments(['EVAL', script], options);
+  if (options?.arguments) {
+    parser.push(...options.arguments)
+  }
 }
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Executes a Lua script server side
+   * @param parser - The Redis command parser
+   * @param script - Lua script to execute
+   * @param options - Script execution options including keys and arguments
+   */
+  parseCommand(...args: Parameters<typeof parseEvalArguments>) {
+    args[0].push('EVAL');
+    parseEvalArguments(...args);
+  },
+  transformReply: undefined as unknown as () => ReplyUnion
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EVALSHA.spec.ts b/packages/client/lib/commands/EVALSHA.spec.ts
index 08b330ac4f5..c491d6e2308 100644
--- a/packages/client/lib/commands/EVALSHA.spec.ts
+++ b/packages/client/lib/commands/EVALSHA.spec.ts
@@ -1,14 +1,15 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './EVALSHA';
+import { strict as assert } from 'node:assert';
+import EVALSHA from './EVALSHA';
+import { parseArgs } from './generic-transformers';
 
 describe('EVALSHA', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('sha1', {
-                keys: ['key'],
-                arguments: ['argument']
-            }),
-            ['EVALSHA', 'sha1', '1', 'key', 'argument']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EVALSHA, 'sha1', {
+        keys: ['key'],
+        arguments: ['argument']
+      }),
+      ['EVALSHA', 'sha1', '1', 'key', 'argument']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/EVALSHA.ts b/packages/client/lib/commands/EVALSHA.ts
index 24f7060a052..29bb6ffdfcb 100644
--- a/packages/client/lib/commands/EVALSHA.ts
+++ b/packages/client/lib/commands/EVALSHA.ts
@@ -1,7 +1,17 @@
-import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
+import { Command } from '../RESP/types';
+import EVAL, { parseEvalArguments } from './EVAL';
 
-export const FIRST_KEY_INDEX = evalFirstKeyIndex;
-
-export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
-    return pushEvalArguments(['EVALSHA', sha1], options);
-}
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Executes a Lua script server side using the script's SHA1 digest
+   * @param parser - The Redis command parser
+   * @param sha1 - SHA1 digest of the script
+   * @param options - Script execution options including keys and arguments
+   */
+  parseCommand(...args: Parameters<typeof parseEvalArguments>) {
+    args[0].push('EVALSHA');
+    parseEvalArguments(...args);
+  },
+  transformReply: EVAL.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EVALSHA_RO.spec.ts b/packages/client/lib/commands/EVALSHA_RO.spec.ts
index 939a4a209cb..d3debe933fe 100644
--- a/packages/client/lib/commands/EVALSHA_RO.spec.ts
+++ b/packages/client/lib/commands/EVALSHA_RO.spec.ts
@@ -1,17 +1,18 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './EVALSHA_RO';
+import EVALSHA_RO from './EVALSHA_RO';
+import { parseArgs } from './generic-transformers';
 
 describe('EVALSHA_RO', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('sha1', {
-                keys: ['key'],
-                arguments: ['argument']
-            }),
-            ['EVALSHA_RO', 'sha1', '1', 'key', 'argument']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EVALSHA_RO, 'sha1', {
+        keys: ['key'],
+        arguments: ['argument']
+      }),
+      ['EVALSHA_RO', 'sha1', '1', 'key', 'argument']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/EVALSHA_RO.ts b/packages/client/lib/commands/EVALSHA_RO.ts
index c3fcc3dc9cf..628ca3dee53 100644
--- a/packages/client/lib/commands/EVALSHA_RO.ts
+++ b/packages/client/lib/commands/EVALSHA_RO.ts
@@ -1,9 +1,17 @@
-import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
+import { Command } from '../RESP/types';
+import EVAL, { parseEvalArguments } from './EVAL';
 
-export const FIRST_KEY_INDEX = evalFirstKeyIndex;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
-    return pushEvalArguments(['EVALSHA_RO', sha1], options);
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Executes a read-only Lua script server side using the script's SHA1 digest
+   * @param parser - The Redis command parser
+   * @param sha1 - SHA1 digest of the script
+   * @param options - Script execution options including keys and arguments
+   */
+  parseCommand(...args: Parameters<typeof parseEvalArguments>) {
+    args[0].push('EVALSHA_RO');
+    parseEvalArguments(...args);
+  },
+  transformReply: EVAL.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EVAL_RO.spec.ts b/packages/client/lib/commands/EVAL_RO.spec.ts
index f71d0b2b24a..b5cf1e4e926 100644
--- a/packages/client/lib/commands/EVAL_RO.spec.ts
+++ b/packages/client/lib/commands/EVAL_RO.spec.ts
@@ -1,31 +1,28 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EVAL_RO';
+import EVAL_RO from './EVAL_RO';
+import { parseArgs } from './generic-transformers';
 
 describe('EVAL_RO', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('return KEYS[1] + ARGV[1]', {
-                keys: ['key'],
-                arguments: ['argument']
-            }),
-            ['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EVAL_RO, 'return KEYS[1] + ARGV[1]', {
+        keys: ['key'],
+        arguments: ['argument']
+      }),
+      ['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument']
+    );
+  });
 
-    testUtils.testWithClient('client.evalRo', async client => {
-        assert.equal(
-            await client.evalRo('return 1'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.evalRo', async cluster => {
-        assert.equal(
-            await cluster.evalRo('return 1'),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('evalRo', async cluster => {
+    assert.equal(
+      await cluster.evalRo('return 1'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/EVAL_RO.ts b/packages/client/lib/commands/EVAL_RO.ts
index 590c3af04f3..803c4f840c5 100644
--- a/packages/client/lib/commands/EVAL_RO.ts
+++ b/packages/client/lib/commands/EVAL_RO.ts
@@ -1,9 +1,17 @@
-import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
+import { Command } from '../RESP/types';
+import EVAL, { parseEvalArguments } from './EVAL';
 
-export const FIRST_KEY_INDEX = evalFirstKeyIndex;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(script: string, options?: EvalOptions): Array<string> {
-    return pushEvalArguments(['EVAL_RO', script], options);
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Executes a read-only Lua script server side
+   * @param parser - The Redis command parser
+   * @param script - Lua script to execute
+   * @param options - Script execution options including keys and arguments
+   */
+  parseCommand(...args: Parameters<typeof parseEvalArguments>) {
+    args[0].push('EVAL_RO');
+    parseEvalArguments(...args);
+  },
+  transformReply: EVAL.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EXISTS.spec.ts b/packages/client/lib/commands/EXISTS.spec.ts
index be1a808225e..d2802dd49b3 100644
--- a/packages/client/lib/commands/EXISTS.spec.ts
+++ b/packages/client/lib/commands/EXISTS.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EXISTS';
+import EXISTS from './EXISTS';
+import { parseArgs } from './generic-transformers';
 
 describe('EXISTS', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['EXISTS', 'key']
-            );
-        });
+  describe('parseCommand', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(EXISTS, 'key'),
+        ['EXISTS', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['EXISTS', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(EXISTS, ['1', '2']),
+        ['EXISTS', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.exists', async client => {
-        assert.equal(
-            await client.exists('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('exists', async client => {
+    assert.equal(
+      await client.exists('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/EXISTS.ts b/packages/client/lib/commands/EXISTS.ts
index 3bbc72ada46..ea6ea8cb0cd 100644
--- a/packages/client/lib/commands/EXISTS.ts
+++ b/packages/client/lib/commands/EXISTS.ts
@@ -1,14 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['EXISTS'], keys);
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Determines if the specified keys exist
+   * @param parser - The Redis command parser
+   * @param keys - One or more keys to check
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('EXISTS');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EXPIRE.spec.ts b/packages/client/lib/commands/EXPIRE.spec.ts
index 39f9d70bd93..f3d197b5c69 100644
--- a/packages/client/lib/commands/EXPIRE.spec.ts
+++ b/packages/client/lib/commands/EXPIRE.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EXPIRE';
+import EXPIRE from './EXPIRE';
+import { parseArgs } from './generic-transformers';
 
 describe('EXPIRE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['EXPIRE', 'key', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(EXPIRE, 'key', 1),
+        ['EXPIRE', 'key', '1']
+      );
+    });
 
-        it('with set option', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 'NX'),
-                ['EXPIRE', 'key', '1', 'NX']
-            );
-        });
+    it('with set option', () => {
+      assert.deepEqual(
+        parseArgs(EXPIRE, 'key', 1, 'NX'),
+        ['EXPIRE', 'key', '1', 'NX']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.expire', async client => {
-        assert.equal(
-            await client.expire('key', 0),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('expire', async client => {
+    assert.equal(
+      await client.expire('key', 0),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/EXPIRE.ts b/packages/client/lib/commands/EXPIRE.ts
index d9e85595c3b..15855832c32 100644
--- a/packages/client/lib/commands/EXPIRE.ts
+++ b/packages/client/lib/commands/EXPIRE.ts
@@ -1,19 +1,27 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Sets a timeout on key. After the timeout has expired, the key will be automatically deleted
+   * @param parser - The Redis command parser
+   * @param key - Key to set expiration on
+   * @param seconds - Number of seconds until key expiration
+   * @param mode - Expiration mode: NX (only if key has no expiry), XX (only if key has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current)
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     seconds: number,
     mode?: 'NX' | 'XX' | 'GT' | 'LT'
-): RedisCommandArguments {
-    const args = ['EXPIRE', key, seconds.toString()];
-
+  ) {
+    parser.push('EXPIRE');
+    parser.pushKey(key);
+    parser.push(seconds.toString());
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EXPIREAT.spec.ts b/packages/client/lib/commands/EXPIREAT.spec.ts
index 0335b36f5f5..1949fb051bb 100644
--- a/packages/client/lib/commands/EXPIREAT.spec.ts
+++ b/packages/client/lib/commands/EXPIREAT.spec.ts
@@ -1,36 +1,40 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EXPIREAT';
+import EXPIREAT from './EXPIREAT';
+import { parseArgs } from './generic-transformers';
 
 describe('EXPIREAT', () => {
-    describe('transformArguments', () => {
-        it('number', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['EXPIREAT', 'key', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('number', () => {
+      assert.deepEqual(
+        parseArgs(EXPIREAT, 'key', 1),
+        ['EXPIREAT', 'key', '1']
+      );
+    });
+
+    it('date', () => {
+      const d = new Date();
+      assert.deepEqual(
+        parseArgs(EXPIREAT, 'key', d),
+        ['EXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString()]
+      );
+    });
 
-        it('date', () => {
-            const d = new Date();
-            assert.deepEqual(
-                transformArguments('key', d),
-                ['EXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString()]
-            );
-        });
-        
-        it('with set option', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 'GT'),
-                ['EXPIREAT', 'key', '1', 'GT']
-            );
-        });
+    it('with set option', () => {
+      assert.deepEqual(
+        parseArgs(EXPIREAT, 'key', 1, 'GT'),
+        ['EXPIREAT', 'key', '1', 'GT']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.expireAt', async client => {
-        assert.equal(
-            await client.expireAt('key', 1),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('expireAt', async client => {
+    assert.equal(
+      await client.expireAt('key', 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/EXPIREAT.ts b/packages/client/lib/commands/EXPIREAT.ts
index 84e7760fe7f..4956b8aa23b 100644
--- a/packages/client/lib/commands/EXPIREAT.ts
+++ b/packages/client/lib/commands/EXPIREAT.ts
@@ -1,24 +1,28 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 import { transformEXAT } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Sets the expiration for a key at a specific Unix timestamp
+   * @param parser - The Redis command parser
+   * @param key - Key to set expiration on
+   * @param timestamp - Unix timestamp (seconds since January 1, 1970) or Date object
+   * @param mode - Expiration mode: NX (only if key has no expiry), XX (only if key has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current)
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     timestamp: number | Date,
     mode?: 'NX' | 'XX' | 'GT' | 'LT'
-): RedisCommandArguments {
-    const args = [
-        'EXPIREAT',
-        key,
-        transformEXAT(timestamp)
-    ];
-
+  ) {
+    parser.push('EXPIREAT');
+    parser.pushKey(key);
+    parser.push(transformEXAT(timestamp));
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/EXPIRETIME.spec.ts b/packages/client/lib/commands/EXPIRETIME.spec.ts
index 1d63e759a5d..f2c8d3d4521 100644
--- a/packages/client/lib/commands/EXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/EXPIRETIME.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EXPIRETIME';
+import EXPIRETIME from './EXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('EXPIRETIME', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['EXPIRETIME', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EXPIRETIME, 'key'),
+      ['EXPIRETIME', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.expireTime', async client => {
-        assert.equal(
-            await client.expireTime('key'),
-            -2
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('expireTime', async client => {
+    assert.equal(
+      await client.expireTime('key'),
+      -2
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/EXPIRETIME.ts b/packages/client/lib/commands/EXPIRETIME.ts
index 8c1bb075995..faa8571eca3 100644
--- a/packages/client/lib/commands/EXPIRETIME.ts
+++ b/packages/client/lib/commands/EXPIRETIME.ts
@@ -1,9 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['EXPIRETIME', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key will expire
+   * @param parser - The Redis command parser
+   * @param key - Key to check expiration time
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('EXPIRETIME');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FAILOVER.spec.ts b/packages/client/lib/commands/FAILOVER.spec.ts
index 16094a0dbc3..b23c3516f03 100644
--- a/packages/client/lib/commands/FAILOVER.spec.ts
+++ b/packages/client/lib/commands/FAILOVER.spec.ts
@@ -1,72 +1,73 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './FAILOVER';
+import { strict as assert } from 'node:assert';
+import FAILOVER from './FAILOVER';
+import { parseArgs } from './generic-transformers';
 
 describe('FAILOVER', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['FAILOVER']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(FAILOVER),
+        ['FAILOVER']
+      );
+    });
 
-        describe('with TO', () => {
-            it('simple', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        TO: {
-                            host: 'host',
-                            port: 6379
-                        }
-                    }),
-                    ['FAILOVER', 'TO', 'host', '6379']
-                );
-            });
+    describe('with TO', () => {
+      it('simple', () => {
+        assert.deepEqual(
+          parseArgs(FAILOVER, {
+            TO: {
+              host: 'host',
+              port: 6379
+            }
+          }),
+          ['FAILOVER', 'TO', 'host', '6379']
+        );
+      });
 
-            it('with FORCE', () => {
-                assert.deepEqual(
-                    transformArguments({
-                        TO: {
-                            host: 'host',
-                            port: 6379,
-                            FORCE: true
-                        }
-                    }),
-                    ['FAILOVER', 'TO', 'host', '6379', 'FORCE']
-                );
-            });
-        });
+      it('with FORCE', () => {
+        assert.deepEqual(
+          parseArgs(FAILOVER, {
+            TO: {
+              host: 'host',
+              port: 6379,
+              FORCE: true
+            }
+          }),
+          ['FAILOVER', 'TO', 'host', '6379', 'FORCE']
+        );
+      });
+    });
 
-        it('with ABORT', () => {
-            assert.deepEqual(
-                transformArguments({
-                    ABORT: true
-                }),
-                ['FAILOVER', 'ABORT']
-            );
-        });
+    it('with ABORT', () => {
+      assert.deepEqual(
+        parseArgs(FAILOVER, {
+          ABORT: true
+        }),
+        ['FAILOVER', 'ABORT']
+      );
+    });
 
-        it('with TIMEOUT', () => {
-            assert.deepEqual(
-                transformArguments({
-                    TIMEOUT: 1
-                }),
-                ['FAILOVER', 'TIMEOUT', '1']
-            );
-        });
+    it('with TIMEOUT', () => {
+      assert.deepEqual(
+        parseArgs(FAILOVER, {
+          TIMEOUT: 1
+        }),
+        ['FAILOVER', 'TIMEOUT', '1']
+      );
+    });
 
-        it('with TO, ABORT, TIMEOUT', () => {
-            assert.deepEqual(
-                transformArguments({
-                    TO: {
-                        host: 'host',
-                        port: 6379
-                    },
-                    ABORT: true,
-                    TIMEOUT: 1
-                }),
-                ['FAILOVER', 'TO', 'host', '6379', 'ABORT', 'TIMEOUT', '1']
-            );
-        });
+    it('with TO, ABORT, TIMEOUT', () => {
+      assert.deepEqual(
+        parseArgs(FAILOVER, {
+          TO: {
+            host: 'host',
+            port: 6379
+          },
+          ABORT: true,
+          TIMEOUT: 1
+        }),
+        ['FAILOVER', 'TO', 'host', '6379', 'ABORT', 'TIMEOUT', '1']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/FAILOVER.ts b/packages/client/lib/commands/FAILOVER.ts
index c31dbc063de..24fa7a0347b 100644
--- a/packages/client/lib/commands/FAILOVER.ts
+++ b/packages/client/lib/commands/FAILOVER.ts
@@ -1,33 +1,40 @@
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+
 interface FailoverOptions {
-    TO?: {
-        host: string;
-        port: number;
-        FORCE?: true;
-    };
-    ABORT?: true;
-    TIMEOUT?: number;
+  TO?: {
+    host: string;
+    port: number;
+    FORCE?: true;
+  };
+  ABORT?: true;
+  TIMEOUT?: number;
 }
 
-export function transformArguments(options?: FailoverOptions): Array<string> {
-    const args = ['FAILOVER'];
+export default {
+  /**
+   * Starts a coordinated failover between the primary and a replica
+   * @param parser - The Redis command parser
+   * @param options - Failover options including target host, abort flag, and timeout
+   */
+  parseCommand(parser: CommandParser, options?: FailoverOptions) {
+    parser.push('FAILOVER');
 
     if (options?.TO) {
-        args.push('TO', options.TO.host, options.TO.port.toString());
+      parser.push('TO', options.TO.host, options.TO.port.toString());
 
-        if (options.TO.FORCE) {
-            args.push('FORCE');
-        }
+      if (options.TO.FORCE) {
+        parser.push('FORCE');
+      }
     }
 
     if (options?.ABORT) {
-        args.push('ABORT');
+      parser.push('ABORT');
     }
 
     if (options?.TIMEOUT) {
-        args.push('TIMEOUT', options.TIMEOUT.toString());
+      parser.push('TIMEOUT', options.TIMEOUT.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FCALL.spec.ts b/packages/client/lib/commands/FCALL.spec.ts
index fd29f07527d..6c3a65c1448 100644
--- a/packages/client/lib/commands/FCALL.spec.ts
+++ b/packages/client/lib/commands/FCALL.spec.ts
@@ -1,29 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
-import { transformArguments } from './FCALL';
+import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec';
+import FCALL from './FCALL';
+import { parseArgs } from './generic-transformers';
 
 describe('FCALL', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('function', {
-                keys: ['key'],
-                arguments: ['argument']
-            }),
-            ['FCALL', 'function', '1', 'key', 'argument']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(FCALL, 'function', {
+        keys: ['key'],
+        arguments: ['argument']
+      }),
+      ['FCALL', 'function', '1', 'key', 'argument']
+    );
+  });
 
-    testUtils.testWithClient('client.fCall', async client => {
-        await loadMathFunction(client);
+  testUtils.testWithClient('client.fCall', async client => {
+    const [,, reply] = await Promise.all([
+      loadMathFunction(client),
+      client.set('key', '2'),
+      client.fCall(MATH_FUNCTION.library.square.NAME, {
+        keys: ['key']
+      })
+    ]);
 
-        assert.equal(
-            await client.fCall(MATH_FUNCTION.library.square.NAME, {
-                arguments: ['2']
-            }),
-            4
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 4);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FCALL.ts b/packages/client/lib/commands/FCALL.ts
index a4cadedb6f9..8fa56d4258e 100644
--- a/packages/client/lib/commands/FCALL.ts
+++ b/packages/client/lib/commands/FCALL.ts
@@ -1,7 +1,17 @@
-import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
+import { Command } from '../RESP/types';
+import EVAL, { parseEvalArguments } from './EVAL';
 
-export const FIRST_KEY_INDEX = evalFirstKeyIndex;
-
-export function transformArguments(fn: string, options?: EvalOptions): Array<string> {
-    return pushEvalArguments(['FCALL', fn], options);
-}
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Invokes a Redis function
+   * @param parser - The Redis command parser
+   * @param functionName - Name of the function to call
+   * @param options - Function execution options including keys and arguments
+   */
+  parseCommand(...args: Parameters<typeof parseEvalArguments>) {
+    args[0].push('FCALL');
+    parseEvalArguments(...args);
+  },
+  transformReply: EVAL.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FCALL_RO.spec.ts b/packages/client/lib/commands/FCALL_RO.spec.ts
index 18665f92aa6..447e00072be 100644
--- a/packages/client/lib/commands/FCALL_RO.spec.ts
+++ b/packages/client/lib/commands/FCALL_RO.spec.ts
@@ -1,29 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
-import { transformArguments } from './FCALL_RO';
+import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec';
+import FCALL_RO from './FCALL_RO';
+import { parseArgs } from './generic-transformers';
 
 describe('FCALL_RO', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('function', {
-                keys: ['key'],
-                arguments: ['argument']
-            }),
-            ['FCALL_RO', 'function', '1', 'key', 'argument']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(FCALL_RO, 'function', {
+        keys: ['key'],
+        arguments: ['argument']
+      }),
+      ['FCALL_RO', 'function', '1', 'key', 'argument']
+    );
+  });
 
-    testUtils.testWithClient('client.fCallRo', async client => {
-        await loadMathFunction(client);
+  testUtils.testWithClient('client.fCallRo', async client => {
+    const [,, reply] = await Promise.all([
+      loadMathFunction(client),
+      client.set('key', '2'),
+      client.fCallRo(MATH_FUNCTION.library.square.NAME, {
+        keys: ['key']
+      })
+    ]);
 
-        assert.equal(
-            await client.fCallRo(MATH_FUNCTION.library.square.NAME, {
-                arguments: ['2']
-            }),
-            4
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 4);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FCALL_RO.ts b/packages/client/lib/commands/FCALL_RO.ts
index 66b79aa8833..5aac38aed0b 100644
--- a/packages/client/lib/commands/FCALL_RO.ts
+++ b/packages/client/lib/commands/FCALL_RO.ts
@@ -1,9 +1,17 @@
-import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
+import { Command } from '../RESP/types';
+import EVAL, { parseEvalArguments } from './EVAL';
 
-export const FIRST_KEY_INDEX = evalFirstKeyIndex;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(fn: string, options?: EvalOptions): Array<string> {
-    return pushEvalArguments(['FCALL_RO', fn], options);
-}
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Invokes a read-only Redis function
+   * @param parser - The Redis command parser
+   * @param functionName - Name of the function to call
+   * @param options - Function execution options including keys and arguments
+   */
+  parseCommand(...args: Parameters<typeof parseEvalArguments>) {
+    args[0].push('FCALL_RO');
+    parseEvalArguments(...args);
+  },
+  transformReply: EVAL.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FLUSHALL.spec.ts b/packages/client/lib/commands/FLUSHALL.spec.ts
index db5bb72e9cb..86daff1973a 100644
--- a/packages/client/lib/commands/FLUSHALL.spec.ts
+++ b/packages/client/lib/commands/FLUSHALL.spec.ts
@@ -1,35 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisFlushModes, transformArguments } from './FLUSHALL';
+import FLUSHALL, { REDIS_FLUSH_MODES } from './FLUSHALL';
+import { parseArgs } from './generic-transformers';
 
 describe('FLUSHALL', () => {
-    describe('transformArguments', () => {
-        it('default', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['FLUSHALL']
-            );
-        });
+  describe('transformArguments', () => {
+    it('default', () => {
+      assert.deepEqual(
+        parseArgs(FLUSHALL),
+        ['FLUSHALL']
+      );
+    });
 
-        it('ASYNC', () => {
-            assert.deepEqual(
-                transformArguments(RedisFlushModes.ASYNC),
-                ['FLUSHALL', 'ASYNC']
-            );
-        });
+    it('ASYNC', () => {
+      assert.deepEqual(
+        parseArgs(FLUSHALL,REDIS_FLUSH_MODES.ASYNC),
+        ['FLUSHALL', 'ASYNC']
+      );
+    });
 
-        it('SYNC', () => {
-            assert.deepEqual(
-                transformArguments(RedisFlushModes.SYNC),
-                ['FLUSHALL', 'SYNC']
-            );
-        });
+    it('SYNC', () => {
+      assert.deepEqual(
+        parseArgs(FLUSHALL, REDIS_FLUSH_MODES.SYNC),
+        ['FLUSHALL', 'SYNC']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.flushAll', async client => {
-        assert.equal(
-            await client.flushAll(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.flushAll', async client => {
+    assert.equal(
+      await client.flushAll(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FLUSHALL.ts b/packages/client/lib/commands/FLUSHALL.ts
index 967096bb9bd..de6852d57e0 100644
--- a/packages/client/lib/commands/FLUSHALL.ts
+++ b/packages/client/lib/commands/FLUSHALL.ts
@@ -1,16 +1,26 @@
-export enum RedisFlushModes {
-    ASYNC = 'ASYNC',
-    SYNC = 'SYNC'
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(mode?: RedisFlushModes): Array<string> {
-    const args = ['FLUSHALL'];
+export const REDIS_FLUSH_MODES = {
+  ASYNC: 'ASYNC',
+  SYNC: 'SYNC'
+} as const;
 
+export type RedisFlushMode = typeof REDIS_FLUSH_MODES[keyof typeof REDIS_FLUSH_MODES];
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Removes all keys from all databases
+   * @param parser - The Redis command parser
+   * @param mode - Optional flush mode (ASYNC or SYNC)
+   */
+  parseCommand(parser: CommandParser, mode?: RedisFlushMode) {
+    parser.push('FLUSHALL');
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FLUSHDB.spec.ts b/packages/client/lib/commands/FLUSHDB.spec.ts
index bf460e9e7a8..795df637cb4 100644
--- a/packages/client/lib/commands/FLUSHDB.spec.ts
+++ b/packages/client/lib/commands/FLUSHDB.spec.ts
@@ -1,36 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisFlushModes } from './FLUSHALL';
-import { transformArguments } from './FLUSHDB';
+import FLUSHDB from './FLUSHDB';
+import { REDIS_FLUSH_MODES } from './FLUSHALL';
+import { parseArgs } from './generic-transformers';
 
 describe('FLUSHDB', () => {
-    describe('transformArguments', () => {
-        it('default', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['FLUSHDB']
-            );
-        });
+  describe('transformArguments', () => {
+    it('default', () => {
+      assert.deepEqual(
+        parseArgs(FLUSHDB),
+        ['FLUSHDB']
+      );
+    });
 
-        it('ASYNC', () => {
-            assert.deepEqual(
-                transformArguments(RedisFlushModes.ASYNC),
-                ['FLUSHDB', 'ASYNC']
-            );
-        });
+    it('ASYNC', () => {
+      assert.deepEqual(
+        parseArgs(FLUSHDB, REDIS_FLUSH_MODES.ASYNC),
+        ['FLUSHDB', 'ASYNC']
+      );
+    });
 
-        it('SYNC', () => {
-            assert.deepEqual(
-                transformArguments(RedisFlushModes.SYNC),
-                ['FLUSHDB', 'SYNC']
-            );
-        });
+    it('SYNC', () => {
+      assert.deepEqual(
+        parseArgs(FLUSHDB, REDIS_FLUSH_MODES.SYNC),
+        ['FLUSHDB', 'SYNC']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.flushDb', async client => {
-        assert.equal(
-            await client.flushDb(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.flushDb', async client => {
+    assert.equal(
+      await client.flushDb(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FLUSHDB.ts b/packages/client/lib/commands/FLUSHDB.ts
index 5b8060df9eb..cd1ac201fce 100644
--- a/packages/client/lib/commands/FLUSHDB.ts
+++ b/packages/client/lib/commands/FLUSHDB.ts
@@ -1,13 +1,20 @@
-import { RedisFlushModes } from './FLUSHALL';
-
-export function transformArguments(mode?: RedisFlushModes): Array<string> {
-    const args = ['FLUSHDB'];
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import { RedisFlushMode } from './FLUSHALL';
 
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Removes all keys from the current database
+   * @param parser - The Redis command parser
+   * @param mode - Optional flush mode (ASYNC or SYNC)
+   */
+  parseCommand(parser: CommandParser, mode?: RedisFlushMode) {
+    parser.push('FLUSHDB');
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts
index 563b9aa0a58..b33ea25916b 100644
--- a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts
@@ -1,24 +1,25 @@
-import { strict as assert } from 'assert';
-import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './FUNCTION_DELETE';
+import FUNCTION_DELETE from './FUNCTION_DELETE';
+import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION DELETE', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('library'),
-            ['FUNCTION', 'DELETE', 'library']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(FUNCTION_DELETE, 'library'),
+      ['FUNCTION', 'DELETE', 'library']
+    );
+  });
 
-    testUtils.testWithClient('client.functionDelete', async client => {
-        await loadMathFunction(client);
+  testUtils.testWithClient('client.functionDelete', async client => {
+    await loadMathFunction(client);
 
-        assert.equal(
-            await client.functionDelete(MATH_FUNCTION.name),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(
+      await client.functionDelete(MATH_FUNCTION.name),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_DELETE.ts b/packages/client/lib/commands/FUNCTION_DELETE.ts
index 4aa6be40e12..e7b59ecb0cc 100644
--- a/packages/client/lib/commands/FUNCTION_DELETE.ts
+++ b/packages/client/lib/commands/FUNCTION_DELETE.ts
@@ -1,7 +1,16 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(library: string): RedisCommandArguments {
-    return ['FUNCTION', 'DELETE', library];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Deletes a library and all its functions
+   * @param parser - The Redis command parser
+   * @param library - Name of the library to delete
+   */
+  parseCommand(parser: CommandParser, library: RedisArgument) {
+    parser.push('FUNCTION', 'DELETE', library);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts
index 360ec6b7453..bbd6302bb6a 100644
--- a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './FUNCTION_DUMP';
+import FUNCTION_DUMP from './FUNCTION_DUMP';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION DUMP', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['FUNCTION', 'DUMP']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(FUNCTION_DUMP),
+      ['FUNCTION', 'DUMP']
+    );
+  });
 
-    testUtils.testWithClient('client.functionDump', async client => {
-        assert.equal(
-            typeof await client.functionDump(),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.functionDump', async client => {
+    assert.equal(
+      typeof await client.functionDump(),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_DUMP.ts b/packages/client/lib/commands/FUNCTION_DUMP.ts
index f608e078c27..73d6986b707 100644
--- a/packages/client/lib/commands/FUNCTION_DUMP.ts
+++ b/packages/client/lib/commands/FUNCTION_DUMP.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['FUNCTION', 'DUMP'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns a serialized payload representing the current functions loaded in the server
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('FUNCTION', 'DUMP')
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts
index 12009d03363..4fe90bdb607 100644
--- a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './FUNCTION_FLUSH';
+import FUNCTION_FLUSH from './FUNCTION_FLUSH';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION FLUSH', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['FUNCTION', 'FLUSH']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_FLUSH),
+        ['FUNCTION', 'FLUSH']
+      );
+    });
 
-        it('with mode', () => {
-            assert.deepEqual(
-                transformArguments('SYNC'),
-                ['FUNCTION', 'FLUSH', 'SYNC']
-            );
-        });
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_FLUSH, 'SYNC'),
+        ['FUNCTION', 'FLUSH', 'SYNC']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.functionFlush', async client => {
-        assert.equal(
-            await client.functionFlush(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.functionFlush', async client => {
+    assert.equal(
+      await client.functionFlush(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.ts b/packages/client/lib/commands/FUNCTION_FLUSH.ts
index 143282de97f..8019fc0c215 100644
--- a/packages/client/lib/commands/FUNCTION_FLUSH.ts
+++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts
@@ -1,13 +1,21 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import { RedisFlushMode } from './FLUSHALL';
 
-export function transformArguments(mode?: 'ASYNC' | 'SYNC'): RedisCommandArguments {
-    const args = ['FUNCTION', 'FLUSH'];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Deletes all the libraries and functions from a Redis server
+   * @param parser - The Redis command parser
+   * @param mode - Optional flush mode (ASYNC or SYNC)
+   */
+  parseCommand(parser: CommandParser, mode?: RedisFlushMode) {
+    parser.push('FUNCTION', 'FLUSH');
 
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_KILL.spec.ts b/packages/client/lib/commands/FUNCTION_KILL.spec.ts
index df4848fc82e..c4dbd124d30 100644
--- a/packages/client/lib/commands/FUNCTION_KILL.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_KILL.spec.ts
@@ -1,14 +1,15 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils from '../test-utils';
-import { transformArguments } from './FUNCTION_KILL';
+import FUNCTION_KILL from './FUNCTION_KILL';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION KILL', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['FUNCTION', 'KILL']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(FUNCTION_KILL),
+      ['FUNCTION', 'KILL']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts
index 517272e8376..b1626684b62 100644
--- a/packages/client/lib/commands/FUNCTION_KILL.ts
+++ b/packages/client/lib/commands/FUNCTION_KILL.ts
@@ -1,7 +1,15 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['FUNCTION', 'KILL'];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Kills a function that is currently executing
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('FUNCTION', 'KILL');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_LIST.spec.ts b/packages/client/lib/commands/FUNCTION_LIST.spec.ts
index 80723d070de..6d9b28acf90 100644
--- a/packages/client/lib/commands/FUNCTION_LIST.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_LIST.spec.ts
@@ -1,41 +1,46 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
-import { transformArguments } from './FUNCTION_LIST';
+import FUNCTION_LIST from './FUNCTION_LIST';
+import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION LIST', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['FUNCTION', 'LIST']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_LIST),
+        ['FUNCTION', 'LIST']
+      );
+    });
 
-        it('with pattern', () => {
-            assert.deepEqual(
-                transformArguments('patter*'),
-                ['FUNCTION', 'LIST', 'patter*']
-            );
-        });
+    it('with LIBRARYNAME', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_LIST, {
+          LIBRARYNAME: 'patter*'
+        }),
+        ['FUNCTION', 'LIST', 'LIBRARYNAME', 'patter*']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.functionList', async client => {
+    const [, reply] = await Promise.all([
+      loadMathFunction(client),
+      client.functionList()
+    ]);
 
-    testUtils.testWithClient('client.functionList', async client => {
-        await loadMathFunction(client);
+    reply[0].library_name;
 
-        assert.deepEqual(
-            await client.functionList(),
-            [{
-                libraryName: MATH_FUNCTION.name,
-                engine: MATH_FUNCTION.engine,
-                functions: [{
-                    name: MATH_FUNCTION.library.square.NAME,
-                    description: null,
-                    flags: ['no-writes']
-                }]
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [{
+      library_name: MATH_FUNCTION.name,
+      engine: MATH_FUNCTION.engine,
+      functions: [{
+        name: MATH_FUNCTION.library.square.NAME,
+        description: null,
+        flags: ['no-writes']
+      }]
+    }]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_LIST.ts b/packages/client/lib/commands/FUNCTION_LIST.ts
index d6a39dc726d..64ebaea8f85 100644
--- a/packages/client/lib/commands/FUNCTION_LIST.ts
+++ b/packages/client/lib/commands/FUNCTION_LIST.ts
@@ -1,16 +1,55 @@
-import { RedisCommandArguments } from '.';
-import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NullReply, SetReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-export function transformArguments(pattern?: string): RedisCommandArguments {
-    const args = ['FUNCTION', 'LIST'];
+export interface FunctionListOptions {
+  LIBRARYNAME?: RedisArgument;
+}
 
-    if (pattern) {
-        args.push(pattern);
-    }
+export type FunctionListReplyItem = [
+  [BlobStringReply<'library_name'>, BlobStringReply | NullReply],
+  [BlobStringReply<'engine'>, BlobStringReply],
+  [BlobStringReply<'functions'>, ArrayReply<TuplesToMapReply<[
+    [BlobStringReply<'name'>, BlobStringReply],
+    [BlobStringReply<'description'>, BlobStringReply | NullReply],
+    [BlobStringReply<'flags'>, SetReply<BlobStringReply>],
+  ]>>]
+];
 
-    return args;
-}
+export type FunctionListReply = ArrayReply<TuplesToMapReply<FunctionListReplyItem>>;
 
-export function transformReply(reply: Array<FunctionListRawItemReply>): Array<FunctionListItemReply> {
-    return reply.map(transformFunctionListItemReply);
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Returns all libraries and functions
+   * @param parser - The Redis command parser
+   * @param options - Options for listing functions
+   */
+  parseCommand(parser: CommandParser, options?: FunctionListOptions) {
+    parser.push('FUNCTION', 'LIST');
+
+    if (options?.LIBRARYNAME) {
+      parser.push('LIBRARYNAME', options.LIBRARYNAME);
+    }
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<FunctionListReply>>) => {
+      return reply.map(library => {
+        const unwrapped = library as unknown as UnwrapReply<typeof library>;
+        return {
+          library_name: unwrapped[1],
+          engine: unwrapped[3],
+          functions: (unwrapped[5] as unknown as UnwrapReply<typeof unwrapped[5]>).map(fn => {
+            const unwrapped = fn as unknown as UnwrapReply<typeof fn>;
+            return {
+              name: unwrapped[1],
+              description: unwrapped[3],
+              flags: unwrapped[5]
+            };
+          })
+        };
+      });
+    },
+    3: undefined as unknown as () => FunctionListReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts
index 56e6102a4b4..f44db9ba037 100644
--- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts
@@ -1,42 +1,49 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
-import { transformArguments } from './FUNCTION_LIST_WITHCODE';
+import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE';
+import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION LIST WITHCODE', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['FUNCTION', 'LIST', 'WITHCODE']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_LIST_WITHCODE),
+        ['FUNCTION', 'LIST', 'WITHCODE']
+      );
+    });
 
-        it('with pattern', () => {
-            assert.deepEqual(
-                transformArguments('patter*'),
-                ['FUNCTION', 'LIST', 'patter*', 'WITHCODE']
-            );
-        });
+    it('with LIBRARYNAME', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_LIST_WITHCODE, {
+          LIBRARYNAME: 'patter*'
+        }),
+        ['FUNCTION', 'LIST', 'LIBRARYNAME', 'patter*', 'WITHCODE']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.functionListWithCode', async client => {
+    const [, reply] = await Promise.all([
+      loadMathFunction(client),
+      client.functionListWithCode()
+    ]);
 
-    testUtils.testWithClient('client.functionListWithCode', async client => {
-        await loadMathFunction(client);
+    const a = reply[0];
 
-        assert.deepEqual(
-            await client.functionListWithCode(),
-            [{
-                libraryName: MATH_FUNCTION.name,
-                engine: MATH_FUNCTION.engine,
-                functions: [{
-                    name: MATH_FUNCTION.library.square.NAME,
-                    description: null,
-                    flags: ['no-writes']
-                }],
-                libraryCode: MATH_FUNCTION.code
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    const b = a.functions[0].description;
+    
+    assert.deepEqual(reply, [{
+      library_name: MATH_FUNCTION.name,
+      engine: MATH_FUNCTION.engine,
+      functions: [{
+        name: MATH_FUNCTION.library.square.NAME,
+        description: null,
+        flags: ['no-writes']
+      }],
+      library_code: MATH_FUNCTION.code
+    }]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts
index 0d763301e87..d5fe312d3c2 100644
--- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts
+++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts
@@ -1,26 +1,42 @@
-import { RedisCommandArguments } from '.';
-import { transformArguments as transformFunctionListArguments } from './FUNCTION_LIST';
-import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers';
+import { TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
+import FUNCTION_LIST, { FunctionListReplyItem } from './FUNCTION_LIST';
 
-export function transformArguments(pattern?: string): RedisCommandArguments {
-    const args = transformFunctionListArguments(pattern);
-    args.push('WITHCODE');
-    return args;
-}
+export type FunctionListWithCodeReply = ArrayReply<TuplesToMapReply<[
+  ...FunctionListReplyItem,
+  [BlobStringReply<'library_code'>, BlobStringReply],
+]>>;
 
-type FunctionListWithCodeRawItemReply = [
-    ...FunctionListRawItemReply,
-    'library_code',
-    string
-];
-
-interface FunctionListWithCodeItemReply extends FunctionListItemReply {
-    libraryCode: string;
-}
-
-export function transformReply(reply: Array<FunctionListWithCodeRawItemReply>): Array<FunctionListWithCodeItemReply> {
-    return reply.map(library => ({
-        ...transformFunctionListItemReply(library as unknown as FunctionListRawItemReply),
-        libraryCode: library[7]
-    }));
-}
+export default {
+  NOT_KEYED_COMMAND: FUNCTION_LIST.NOT_KEYED_COMMAND,
+  IS_READ_ONLY: FUNCTION_LIST.IS_READ_ONLY,
+  /**
+   * Returns all libraries and functions including their source code
+   * @param parser - The Redis command parser
+   * @param options - Options for listing functions
+   */
+  parseCommand(...args: Parameters<typeof FUNCTION_LIST.parseCommand>) {
+    FUNCTION_LIST.parseCommand(...args);
+    args[0].push('WITHCODE');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<FunctionListWithCodeReply>>) => {
+      return reply.map(library => {
+        const unwrapped = library as unknown as UnwrapReply<typeof library>;
+        return {
+          library_name: unwrapped[1],
+          engine: unwrapped[3],
+          functions: (unwrapped[5] as unknown as UnwrapReply<typeof unwrapped[5]>).map(fn => {
+            const unwrapped = fn as unknown as UnwrapReply<typeof fn>;
+            return {
+              name: unwrapped[1],
+              description: unwrapped[3],
+              flags: unwrapped[5]
+            };
+          }),
+          library_code: unwrapped[7]
+        };
+      });
+    },
+    3: undefined as unknown as () => FunctionListWithCodeReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts
index 7be371c6b9c..c0a511bffc9 100644
--- a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts
@@ -1,36 +1,79 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { MATH_FUNCTION } from '../client/index.spec';
-import { transformArguments } from './FUNCTION_LOAD';
+import FUNCTION_LOAD from './FUNCTION_LOAD';
+import { RedisClientType } from '../client';
+import { NumberReply, RedisFunctions, RedisModules, RedisScripts, RespVersions } from '../RESP/types';
+import { parseArgs } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+
+
+
+export const MATH_FUNCTION = {
+  name: 'math',
+  engine: 'LUA',
+  code:
+    `#!LUA name=math
+    redis.register_function {
+      function_name = "square",
+      callback = function(keys, args)
+        local number = redis.call('GET', keys[1])
+        return number * number
+      end,
+      flags = { "no-writes" }
+    }`,
+  library: {
+    square: {
+      NAME: 'square',
+      IS_READ_ONLY: true,
+      NUMBER_OF_KEYS: 1,
+      FIRST_KEY_INDEX: 0,
+      parseCommand(parser: CommandParser, key: string) {
+        parser.pushKey(key);
+      },
+      transformReply: undefined as unknown as () => NumberReply
+    }
+  }
+};
+
+export function loadMathFunction<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions
+>(
+  client: RedisClientType<M, F, S, RESP>
+) {
+  return client.functionLoad(
+    MATH_FUNCTION.code,
+    { REPLACE: true }
+  );
+}
 
 describe('FUNCTION LOAD', () => {
-    testUtils.isVersionGreaterThanHook([7]);
-
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments( 'code'),
-                ['FUNCTION', 'LOAD', 'code']
-            );
-        });
-
-        it('with REPLACE', () => {
-            assert.deepEqual(
-                transformArguments('code', {
-                    REPLACE: true
-                }),
-                ['FUNCTION', 'LOAD', 'REPLACE', 'code']
-            );
-        });
+  testUtils.isVersionGreaterThanHook([7]);
+
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_LOAD, 'code'),
+        ['FUNCTION', 'LOAD', 'code']
+      );
+    });
+
+    it('with REPLACE', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_LOAD, 'code', {
+          REPLACE: true
+        }),
+        ['FUNCTION', 'LOAD', 'REPLACE', 'code']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.functionLoad', async client => {
-        assert.equal(
-            await client.functionLoad(
-                MATH_FUNCTION.code,
-                { REPLACE: true }
-            ),
-            MATH_FUNCTION.name
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.functionLoad', async client => {
+    assert.equal(
+      await loadMathFunction(client),
+      MATH_FUNCTION.name
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_LOAD.ts b/packages/client/lib/commands/FUNCTION_LOAD.ts
index 7ab58d58598..0766a124afb 100644
--- a/packages/client/lib/commands/FUNCTION_LOAD.ts
+++ b/packages/client/lib/commands/FUNCTION_LOAD.ts
@@ -1,22 +1,27 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
 
-interface FunctionLoadOptions {
-    REPLACE?: boolean;
+export interface FunctionLoadOptions {
+  REPLACE?: boolean;
 }
 
-export function transformArguments(
-    code: string,
-    options?: FunctionLoadOptions
-): RedisCommandArguments {
-    const args = ['FUNCTION', 'LOAD'];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Loads a library to Redis
+   * @param parser - The Redis command parser
+   * @param code - Library code to load
+   * @param options - Function load options
+   */
+  parseCommand(parser: CommandParser, code: RedisArgument, options?: FunctionLoadOptions) {
+    parser.push('FUNCTION', 'LOAD');
 
     if (options?.REPLACE) {
-        args.push('REPLACE');
+      parser.push('REPLACE');
     }
 
-    args.push(code);
-
-    return args;
-}
-
-export declare function transformReply(): string;
+    parser.push(code);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts
index a5c2e2dcc72..72d7d1d6204 100644
--- a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts
@@ -1,37 +1,41 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './FUNCTION_RESTORE';
+import FUNCTION_RESTORE from './FUNCTION_RESTORE';
+import { RESP_TYPES } from '../RESP/decoder';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION RESTORE', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('dump'),
-                ['FUNCTION', 'RESTORE', 'dump']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_RESTORE, 'dump'),
+        ['FUNCTION', 'RESTORE', 'dump']
+      );
+    });
 
-        it('with mode', () => {
-            assert.deepEqual(
-                transformArguments('dump', 'APPEND'),
-                ['FUNCTION', 'RESTORE', 'dump', 'APPEND']
-            );
-        });
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(FUNCTION_RESTORE, 'dump', {
+          mode: 'APPEND'
+        }),
+        ['FUNCTION', 'RESTORE', 'dump', 'APPEND']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.functionRestore', async client => {
-        assert.equal(
-            await client.functionRestore(
-                await client.functionDump(
-                    client.commandOptions({
-                        returnBuffers: true
-                    })
-                ),
-                'FLUSH'
-            ),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.functionRestore', async client => {
+    assert.equal(
+      await client.functionRestore(
+        await client.withTypeMapping({
+          [RESP_TYPES.BLOB_STRING]: Buffer
+        }).functionDump(),
+        {
+          mode: 'REPLACE'
+        }
+      ),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.ts b/packages/client/lib/commands/FUNCTION_RESTORE.ts
index bc9c41e262d..f18541a614a 100644
--- a/packages/client/lib/commands/FUNCTION_RESTORE.ts
+++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts
@@ -1,16 +1,25 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '../RESP/types';
 
-export function transformArguments(
-    dump: RedisCommandArgument,
-    mode?: 'FLUSH' | 'APPEND' | 'REPLACE'
-): RedisCommandArguments {
-    const args = ['FUNCTION', 'RESTORE', dump];
-
-    if (mode) {
-        args.push(mode);
-    }
-
-    return args;
+export interface FunctionRestoreOptions {
+  mode?: 'FLUSH' | 'APPEND' | 'REPLACE';
 }
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Restores libraries from the dump payload
+   * @param parser - The Redis command parser
+   * @param dump - Serialized payload of functions to restore
+   * @param options - Options for the restore operation
+   */
+  parseCommand(parser: CommandParser, dump: RedisArgument, options?: FunctionRestoreOptions) {
+    parser.push('FUNCTION', 'RESTORE', dump);
+
+    if (options?.mode) {
+      parser.push(options.mode);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/FUNCTION_STATS.spec.ts b/packages/client/lib/commands/FUNCTION_STATS.spec.ts
index a5e26b5fecc..a3c5e00fe72 100644
--- a/packages/client/lib/commands/FUNCTION_STATS.spec.ts
+++ b/packages/client/lib/commands/FUNCTION_STATS.spec.ts
@@ -1,25 +1,26 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './FUNCTION_STATS';
+import FUNCTION_STATS from './FUNCTION_STATS';
+import { parseArgs } from './generic-transformers';
 
 describe('FUNCTION STATS', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['FUNCTION', 'STATS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(FUNCTION_STATS),
+      ['FUNCTION', 'STATS']
+    );
+  });
 
-    testUtils.testWithClient('client.functionStats', async client => {
-        const stats = await client.functionStats();
-        assert.equal(stats.runningScript, null);
-        assert.equal(typeof stats.engines, 'object');
-        for (const [engine, { librariesCount, functionsCount }] of Object.entries(stats.engines)) {
-            assert.equal(typeof engine, 'string');
-            assert.equal(typeof librariesCount, 'number');
-            assert.equal(typeof functionsCount, 'number');
-        }
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.functionStats', async client => {
+    const stats = await client.functionStats();
+    assert.equal(stats.running_script, null);
+    assert.equal(typeof stats.engines, 'object');
+    for (const [engine, { libraries_count, functions_count }] of Object.entries(stats.engines)) {
+      assert.equal(typeof engine, 'string');
+      assert.equal(typeof libraries_count, 'number');
+      assert.equal(typeof functions_count, 'number');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/FUNCTION_STATS.ts b/packages/client/lib/commands/FUNCTION_STATS.ts
index bb5c957e78b..77eccf916bd 100644
--- a/packages/client/lib/commands/FUNCTION_STATS.ts
+++ b/packages/client/lib/commands/FUNCTION_STATS.ts
@@ -1,56 +1,75 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { Command, TuplesToMapReply, BlobStringReply, NullReply, NumberReply, MapReply, Resp2Reply, UnwrapReply } from '../RESP/types';
+import { isNullReply } from './generic-transformers';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['FUNCTION', 'STATS'];
-}
+type RunningScript = NullReply | TuplesToMapReply<[
+  [BlobStringReply<'name'>, BlobStringReply],
+  [BlobStringReply<'command'>, BlobStringReply],
+  [BlobStringReply<'duration_ms'>, NumberReply]
+]>;
 
-type FunctionStatsRawReply = [
-    'running_script',
-    null | [
-        'name',
-        string,
-        'command',
-        string,
-        'duration_ms',
-        number
-    ],
-    'engines',
-    Array<any> // "flat tuples" (there is no way to type that)
-    // ...[string, [
-    //     'libraries_count',
-    //     number,
-    //     'functions_count',
-    //     number
-    // ]]
-];
-
-interface FunctionStatsReply {
-    runningScript: null | {
-        name: string;
-        command: string;
-        durationMs: number;
-    };
-    engines: Record<string, {
-        librariesCount: number;
-        functionsCount: number;
-    }>;
+type Engine = TuplesToMapReply<[
+  [BlobStringReply<'libraries_count'>, NumberReply],
+  [BlobStringReply<'functions_count'>, NumberReply]
+]>;
+
+type Engines = MapReply<BlobStringReply, Engine>;
+
+type FunctionStatsReply = TuplesToMapReply<[
+  [BlobStringReply<'running_script'>, RunningScript],
+  [BlobStringReply<'engines'>, Engines]
+]>;
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information about the function that is currently running and information about the available execution engines
+   * @param parser - The Redis command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('FUNCTION', 'STATS');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<FunctionStatsReply>>) => {
+      return {
+        running_script: transformRunningScript(reply[1]),
+        engines: transformEngines(reply[3])
+      };
+    },
+    3: undefined as unknown as () => FunctionStatsReply
+  }
+} as const satisfies Command;
+
+function transformRunningScript(reply: Resp2Reply<RunningScript>) {
+  if (isNullReply(reply)) {
+    return null;
+  }
+
+  const unwraped = reply as unknown as UnwrapReply<typeof reply>;
+  return {
+    name: unwraped[1],
+    command: unwraped[3],
+    duration_ms: unwraped[5]
+  };
 }
 
-export function transformReply(reply: FunctionStatsRawReply): FunctionStatsReply {
-    const engines = Object.create(null);
-    for (let i = 0; i < reply[3].length; i++) {
-        engines[reply[3][i]] = {
-            librariesCount: reply[3][++i][1],
-            functionsCount: reply[3][i][3]
-        };
-    }
-
-    return {
-        runningScript: reply[1] === null ? null : {
-            name: reply[1][1],
-            command: reply[1][3],
-            durationMs: reply[1][5]
-        },
-        engines
+function transformEngines(reply: Resp2Reply<Engines>) {
+  const unwraped = reply as unknown as UnwrapReply<typeof reply>;
+
+  const engines: Record<string, {
+    libraries_count: NumberReply;
+    functions_count: NumberReply;
+  }> = Object.create(null);
+  for (let i = 0; i < unwraped.length; i++) {
+    const name = unwraped[i] as BlobStringReply,
+      stats = unwraped[++i] as Resp2Reply<Engine>,
+      unwrapedStats = stats as unknown as UnwrapReply<typeof stats>;
+    engines[name.toString()] = {
+      libraries_count: unwrapedStats[1],
+      functions_count: unwrapedStats[3]
     };
+  }
+
+  return engines;
 }
diff --git a/packages/client/lib/commands/GEOADD.spec.ts b/packages/client/lib/commands/GEOADD.spec.ts
index 6425c881c9d..d947141a318 100644
--- a/packages/client/lib/commands/GEOADD.spec.ts
+++ b/packages/client/lib/commands/GEOADD.spec.ts
@@ -1,95 +1,101 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEOADD';
+import GEOADD from './GEOADD';
+import { parseArgs } from './generic-transformers';
 
 describe('GEOADD', () => {
-    describe('transformArguments', () => {
-        it('one member', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    member: 'member',
-                    longitude: 1,
-                    latitude: 2
-                }),
-                ['GEOADD', 'key', '1', '2', 'member']
-            );
-        });
+  describe('transformArguments', () => {
+    it('one member', () => {
+      assert.deepEqual(
+        parseArgs(GEOADD, 'key', {
+          member: 'member',
+          longitude: 1,
+          latitude: 2
+        }),
+        ['GEOADD', 'key', '1', '2', 'member']
+      );
+    });
 
-        it('multiple members', () => {
-            assert.deepEqual(
-                transformArguments('key', [{
-                    longitude: 1,
-                    latitude: 2,
-                    member: '3',
-                }, {
-                    longitude: 4,
-                    latitude: 5,
-                    member: '6',
-                }]),
-                ['GEOADD', 'key', '1', '2', '3', '4', '5', '6']
-            );
-        });
+    it('multiple members', () => {
+      assert.deepEqual(
+        parseArgs(GEOADD, 'key', [{
+          longitude: 1,
+          latitude: 2,
+          member: '3',
+        }, {
+          longitude: 4,
+          latitude: 5,
+          member: '6',
+        }]),
+        ['GEOADD', 'key', '1', '2', '3', '4', '5', '6']
+      );
+    });
 
-        it('with NX', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    longitude: 1,
-                    latitude: 2,
-                    member: 'member'
-                }, {
-                    NX: true
-                }),
-                ['GEOADD', 'key', 'NX', '1', '2', 'member']
-            );
-        });
+    it('with condition', () => {
+      assert.deepEqual(
+        parseArgs(GEOADD, 'key', {
+          longitude: 1,
+          latitude: 2,
+          member: 'member'
+        }, {
+          condition: 'NX'
+        }),
+        ['GEOADD', 'key', 'NX', '1', '2', 'member']
+      );
+    });
 
-        it('with CH', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    longitude: 1,
-                    latitude: 2,
-                    member: 'member'
-                }, {
-                    CH: true
-                }),
-                ['GEOADD', 'key', 'CH', '1', '2', 'member']
-            );
-        });
+    it('with NX (backwards compatibility)', () => {
+      assert.deepEqual(
+        parseArgs(GEOADD, 'key', {
+          longitude: 1,
+          latitude: 2,
+          member: 'member'
+        }, {
+          NX: true
+        }),
+        ['GEOADD', 'key', 'NX', '1', '2', 'member']
+      );
+    });
 
-        it('with XX, CH', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    longitude: 1,
-                    latitude: 2,
-                    member: 'member'
-                }, {
-                    XX: true,
-                    CH: true
-                }),
-                ['GEOADD', 'key', 'XX', 'CH', '1', '2', 'member']
-            );
-        });
+    it('with CH', () => {
+      assert.deepEqual(
+        parseArgs(GEOADD, 'key', {
+          longitude: 1,
+          latitude: 2,
+          member: 'member'
+        }, {
+          CH: true
+        }),
+        ['GEOADD', 'key', 'CH', '1', '2', 'member']
+      );
     });
 
-    testUtils.testWithClient('client.geoAdd', async client => {
-        assert.equal(
-            await client.geoAdd('key', {
-                member: 'member',
-                longitude: 1,
-                latitude: 2
-            }),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with condition, CH', () => {
+      assert.deepEqual(
+        parseArgs(GEOADD, 'key', {
+          longitude: 1,
+          latitude: 2,
+          member: 'member'
+        }, {
+          condition: 'XX',
+          CH: true
+        }),
+        ['GEOADD', 'key', 'XX', 'CH', '1', '2', 'member']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.geoAdd', async cluster => {
-        assert.equal(
-            await cluster.geoAdd('key', {
-                member: 'member',
-                longitude: 1,
-                latitude: 2
-            }),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('geoAdd', async client => {
+    assert.equal(
+      await client.geoAdd('key', {
+        member: 'member',
+        longitude: 1,
+        latitude: 2
+      }),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEOADD.ts b/packages/client/lib/commands/GEOADD.ts
index daccb0842e0..3da7b0e74b6 100644
--- a/packages/client/lib/commands/GEOADD.ts
+++ b/packages/client/lib/commands/GEOADD.ts
@@ -1,53 +1,73 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoCoordinates } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { GeoCoordinates } from './GEOSEARCH';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface GeoMember extends GeoCoordinates {
-    member: RedisCommandArgument;
-}
-
-interface NX {
-    NX?: true;
+export interface GeoMember extends GeoCoordinates {
+  member: RedisArgument;
 }
 
-interface XX {
-    XX?: true;
+export interface GeoAddOptions {
+  condition?: 'NX' | 'XX';
+  /**
+   * @deprecated Use `{ condition: 'NX' }` instead.
+   */
+  NX?: boolean;
+  /**
+   * @deprecated Use `{ condition: 'XX' }` instead.
+   */
+  XX?: boolean;
+  CH?: boolean;
 }
 
-type SetGuards = NX | XX;
-
-interface GeoAddCommonOptions {
-    CH?: true;
-}
-
-type GeoAddOptions = SetGuards & GeoAddCommonOptions;
-
-export function transformArguments(
-    key: RedisCommandArgument, toAdd: GeoMember | Array<GeoMember>,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds geospatial items to the specified key
+   * @param parser - The Redis command parser
+   * @param key - Key to add the geospatial items to
+   * @param toAdd - Geospatial member(s) to add
+   * @param options - Options for the GEOADD command
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    toAdd: GeoMember | Array<GeoMember>,
     options?: GeoAddOptions
-): RedisCommandArguments {
-    const args = ['GEOADD', key];
+  ) {
+    parser.push('GEOADD')
+    parser.pushKey(key);
 
-    if ((options as NX)?.NX) {
-        args.push('NX');
-    } else if ((options as XX)?.XX) {
-        args.push('XX');
+    if (options?.condition) {
+      parser.push(options.condition);
+    } else if (options?.NX) {
+      parser.push('NX');
+    } else if (options?.XX) {
+      parser.push('XX');
     }
 
     if (options?.CH) {
-        args.push('CH');
+      parser.push('CH');
     }
 
-    for (const { longitude, latitude, member } of (Array.isArray(toAdd) ? toAdd : [toAdd])) {
-        args.push(
-            longitude.toString(),
-            latitude.toString(),
-            member
-        );
+    if (Array.isArray(toAdd)) {
+      for (const member of toAdd) {
+        pushMember(parser, member);
+      }
+    } else {
+      pushMember(parser, toAdd);
     }
 
-    return args;
-}
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
 
-export declare function transformReply(): number;
+function pushMember(
+  parser: CommandParser,
+  { longitude, latitude, member }: GeoMember
+) {
+  parser.push(
+    longitude.toString(),
+    latitude.toString(),
+    member
+  );
+}
diff --git a/packages/client/lib/commands/GEODIST.spec.ts b/packages/client/lib/commands/GEODIST.spec.ts
index bbc62480ee1..a23df405d1d 100644
--- a/packages/client/lib/commands/GEODIST.spec.ts
+++ b/packages/client/lib/commands/GEODIST.spec.ts
@@ -1,57 +1,55 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEODIST';
+import GEODIST from './GEODIST';
+import { parseArgs } from './generic-transformers';
 
 describe('GEODIST', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', '1', '2'),
-                ['GEODIST', 'key', '1', '2']
-            );
-        });
-
-        it('with unit', () => {
-            assert.deepEqual(
-                transformArguments('key', '1', '2', 'm'),
-                ['GEODIST', 'key', '1', '2', 'm']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(GEODIST, 'key', '1', '2'),
+        ['GEODIST', 'key', '1', '2']
+      );
     });
 
-    describe('client.geoDist', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.geoDist('key', '1', '2'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with unit', () => {
+      assert.deepEqual(
+        parseArgs(GEODIST, 'key', '1', '2', 'm'),
+        ['GEODIST', 'key', '1', '2', 'm']
+      );
+    });
+  });
 
-        testUtils.testWithClient('with value', async client => {
-            const [, dist] = await Promise.all([
-                client.geoAdd('key', [{
-                    member: '1',
-                    longitude: 1,
-                    latitude: 1
-                }, {
-                    member: '2',
-                    longitude: 2,
-                    latitude: 2
-                }]),
-                client.geoDist('key', '1', '2')
-            ]);
+  testUtils.testAll('geoDist null', async client => {
+    assert.equal(
+      await client.geoDist('key', '1', '2'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 
-            assert.equal(
-                dist,
-                157270.0561
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
+  testUtils.testAll('geoDist with member', async client => {
+    const [, dist] = await Promise.all([
+      client.geoAdd('key', [{
+        member: '1',
+        longitude: 1,
+        latitude: 1
+      }, {
+        member: '2',
+        longitude: 2,
+        latitude: 2
+      }]),
+      client.geoDist('key', '1', '2')
+    ]);
 
-    testUtils.testWithCluster('cluster.geoDist', async cluster => {
-        assert.equal(
-            await cluster.geoDist('key', '1', '2'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(
+      dist,
+      157270.0561
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEODIST.ts b/packages/client/lib/commands/GEODIST.ts
index 5dbf8ece9cc..f86d8156ebf 100644
--- a/packages/client/lib/commands/GEODIST.ts
+++ b/packages/client/lib/commands/GEODIST.ts
@@ -1,25 +1,33 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoUnits } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
+import { GeoUnits } from './GEOSEARCH';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member1: RedisCommandArgument,
-    member2: RedisCommandArgument,
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the distance between two members in a geospatial index
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param member1 - First member in the geospatial index
+   * @param member2 - Second member in the geospatial index
+   * @param unit - Unit of distance (m, km, ft, mi)
+   */
+  parseCommand(parser: CommandParser,
+    key: RedisArgument,
+    member1: RedisArgument,
+    member2: RedisArgument,
     unit?: GeoUnits
-): RedisCommandArguments {
-    const args = ['GEODIST', key, member1, member2];
+  ) {
+    parser.push('GEODIST');
+    parser.pushKey(key);
+    parser.push(member1, member2);
 
     if (unit) {
-        args.push(unit);
+      parser.push(unit);
     }
-
-    return args;
-}
-
-export function transformReply(reply: RedisCommandArgument | null): number | null {
+  },
+  transformReply(reply: BlobStringReply | NullReply) {
     return reply === null ? null : Number(reply);
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEOHASH.spec.ts b/packages/client/lib/commands/GEOHASH.spec.ts
index c421c148f43..ad26dff8434 100644
--- a/packages/client/lib/commands/GEOHASH.spec.ts
+++ b/packages/client/lib/commands/GEOHASH.spec.ts
@@ -1,35 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEOHASH';
+import GEOHASH from './GEOHASH';
+import { parseArgs } from './generic-transformers';
 
 describe('GEOHASH', () => {
-    describe('transformArguments', () => {
-        it('single member', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member'),
-                ['GEOHASH', 'key', 'member']
-            );
-        });
-
-        it('multiple members', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['GEOHASH', 'key', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single member', () => {
+      assert.deepEqual(
+        parseArgs(GEOHASH, 'key', 'member'),
+        ['GEOHASH', 'key', 'member']
+      );
     });
 
-    testUtils.testWithClient('client.geoHash', async client => {
-        assert.deepEqual(
-            await client.geoHash('key', 'member'),
-            [null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('multiple members', () => {
+      assert.deepEqual(
+        parseArgs(GEOHASH, 'key', ['1', '2']),
+        ['GEOHASH', 'key', '1', '2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.geoHash', async cluster => {
-        assert.deepEqual(
-            await cluster.geoHash('key', 'member'),
-            [null]
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('geoHash', async client => {
+    assert.deepEqual(
+      await client.geoHash('key', 'member'),
+      [null]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEOHASH.ts b/packages/client/lib/commands/GEOHASH.ts
index 55e22c497e2..bddc7a1fc0d 100644
--- a/packages/client/lib/commands/GEOHASH.ts
+++ b/packages/client/lib/commands/GEOHASH.ts
@@ -1,15 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['GEOHASH', key], member);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the Geohash string representation of one or more position members
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param member - One or more members in the geospatial index
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) {
+    parser.push('GEOHASH');
+    parser.pushKey(key);
+    parser.pushVariadic(member);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEOPOS.spec.ts b/packages/client/lib/commands/GEOPOS.spec.ts
index 9c08ccd08f5..002d16d0256 100644
--- a/packages/client/lib/commands/GEOPOS.spec.ts
+++ b/packages/client/lib/commands/GEOPOS.spec.ts
@@ -1,73 +1,166 @@
-import { strict as assert } from 'assert';
+import { strict as assert, fail } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './GEOPOS';
+import GEOPOS from './GEOPOS';
+import { parseArgs } from './generic-transformers';
 
 describe('GEOPOS', () => {
-    describe('transformArguments', () => {
-        it('single member', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member'),
-                ['GEOPOS', 'key', 'member']
-            );
-        });
-
-        it('multiple members', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['GEOPOS', 'key', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single member', () => {
+      assert.deepEqual(
+        parseArgs(GEOPOS, 'key', 'member'),
+        ['GEOPOS', 'key', 'member']
+      );
     });
 
-    describe('transformReply', () => {
-        it('null', () => {
-            assert.deepEqual(
-                transformReply([null]),
-                [null]
-            );
-        });
-
-        it('with member', () => {
-            assert.deepEqual(
-                transformReply([['1', '2']]),
-                [{
-                    longitude: '1',
-                    latitude: '2'
-                }]
-            );
-        });
+    it('multiple members', () => {
+      assert.deepEqual(
+        parseArgs(GEOPOS, 'key', ['1', '2']),
+        ['GEOPOS', 'key', '1', '2']
+      );
     });
+  });
 
-    describe('client.geoPos', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.deepEqual(
-                await client.geoPos('key', 'member'),
-                [null]
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('with member', async client => {
-            const coordinates = {
-                longitude: '-122.06429868936538696',
-                latitude: '37.37749628831998194'
-            };
-
-            await client.geoAdd('key', {
-                member: 'member',
-                ...coordinates
-            });
-
-            assert.deepEqual(
-                await client.geoPos('key', 'member'),
-                [coordinates]
-            );
-        }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('geoPos null', async client => {
+    assert.deepEqual(
+      await client.geoPos('key', 'member'),
+      [null]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('geoPos with member', async client => {
+    const coordinates = {
+      longitude: '-122.06429868936538696',
+      latitude: '37.37749628831998194'
+    };
+
+    await client.geoAdd('key', {
+      member: 'member',
+      ...coordinates
     });
 
-    testUtils.testWithCluster('cluster.geoPos', async cluster => {
-        assert.deepEqual(
-            await cluster.geoPos('key', 'member'),
-            [null]
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    const result = await client.geoPos('key', 'member');
+
+    /**
+     *  - Redis < 8: Returns coordinates with 14 decimal places (e.g., "-122.06429868936539")
+     *  - Redis 8+:  Returns coordinates with 17 decimal places (e.g., "-122.06429868936538696")
+     *
+     */
+    const PRECISION = 13; // Number of decimal places to compare
+
+    if (result && result.length === 1 && result[0] != null) {
+      const { longitude, latitude } = result[0];
+
+      assert.ok(
+        compareWithPrecision(longitude, coordinates.longitude, PRECISION),
+        `Longitude mismatch: ${longitude} vs ${coordinates.longitude}`
+      );
+      assert.ok(
+        compareWithPrecision(latitude, coordinates.latitude, PRECISION),
+        `Latitude mismatch: ${latitude} vs ${coordinates.latitude}`
+      );
+
+    } else {
+      assert.fail('Expected a valid result');
+    }
+
+
+
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
+
+describe('compareWithPrecision', () => {
+  it('should match exact same numbers', () => {
+    assert.strictEqual(
+      compareWithPrecision('123.456789', '123.456789', 6),
+      true
+    );
+  });
+
+  it('should match when actual has more precision than needed', () => {
+    assert.strictEqual(
+      compareWithPrecision('123.456789123456', '123.456789', 6),
+      true
+    );
+  });
+
+  it('should match when expected has more precision than needed', () => {
+    assert.strictEqual(
+      compareWithPrecision('123.456789', '123.456789123456', 6),
+      true
+    );
+  });
+
+  it('should fail when decimals differ within precision', () => {
+    assert.strictEqual(
+      compareWithPrecision('123.456689', '123.456789', 6),
+      false
+    );
+  });
+
+  it('should handle negative numbers', () => {
+    assert.strictEqual(
+      compareWithPrecision('-122.06429868936538', '-122.06429868936539', 13),
+      true
+    );
+  });
+
+  it('should fail when integer parts differ', () => {
+    assert.strictEqual(
+      compareWithPrecision('124.456789', '123.456789', 6),
+      false
+    );
+  });
+
+  it('should handle zero decimal places', () => {
+    assert.strictEqual(
+      compareWithPrecision('123.456789', '123.456789', 0),
+      true
+    );
+  });
+
+  it('should handle numbers without decimal points', () => {
+    assert.strictEqual(
+      compareWithPrecision('123', '123', 6),
+      true
+    );
+  });
+
+  it('should handle one number without decimal point', () => {
+    assert.strictEqual(
+      compareWithPrecision('123', '123.000', 3),
+      true
+    );
+  });
+
+  it('should match Redis coordinates with different precision', () => {
+    assert.strictEqual(
+      compareWithPrecision(
+        '-122.06429868936538696',
+        '-122.06429868936539',
+        13
+      ),
+      true
+    );
+  });
+
+  it('should match Redis latitude with different precision', () => {
+    assert.strictEqual(
+      compareWithPrecision(
+        '37.37749628831998194',
+        '37.37749628831998',
+        14
+      ),
+      true
+    );
+  });
 });
+
+export const compareWithPrecision = (actual: string, expected: string, decimals: number): boolean => {
+  return Math.abs(Number(actual) - Number(expected)) < Math.pow(10, -decimals);
+};
diff --git a/packages/client/lib/commands/GEOPOS.ts b/packages/client/lib/commands/GEOPOS.ts
index 0a5f079deeb..6fed2a8abc8 100644
--- a/packages/client/lib/commands/GEOPOS.ts
+++ b/packages/client/lib/commands/GEOPOS.ts
@@ -1,27 +1,28 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['GEOPOS', key], member);
-}
-
-type GeoCoordinatesRawReply = Array<[RedisCommandArgument, RedisCommandArgument] | null>;
-
-interface GeoCoordinates {
-    longitude: RedisCommandArgument;
-    latitude: RedisCommandArgument;
-}
-
-export function transformReply(reply: GeoCoordinatesRawReply): Array<GeoCoordinates | null> {
-    return reply.map(coordinates => coordinates === null ? null : {
-        longitude: coordinates[0],
-        latitude: coordinates[1]
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the longitude and latitude of one or more members in a geospatial index
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param member - One or more members in the geospatial index
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) {
+    parser.push('GEOPOS');
+    parser.pushKey(key);
+    parser.pushVariadic(member);
+  },
+  transformReply(reply: UnwrapReply<ArrayReply<TuplesReply<[BlobStringReply, BlobStringReply]> | NullReply>>) {
+    return reply.map(item => {
+      const unwrapped = item as unknown as UnwrapReply<typeof item>;
+      return unwrapped === null ? null : {
+        longitude: unwrapped[0],
+        latitude: unwrapped[1]
+      };
     });
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUS.spec.ts b/packages/client/lib/commands/GEORADIUS.spec.ts
index 786b2665029..3c33395c5f6 100644
--- a/packages/client/lib/commands/GEORADIUS.spec.ts
+++ b/packages/client/lib/commands/GEORADIUS.spec.ts
@@ -1,35 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEORADIUS';
+import GEORADIUS from './GEORADIUS';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm'),
-            ['GEORADIUS', 'key', '1', '2', '3', 'm']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GEORADIUS, 'key', {
+        longitude: 1,
+        latitude: 2
+      }, 3, 'm'),
+      ['GEORADIUS', 'key', '1', '2', '3', 'm']
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadius', async client => {
-        assert.deepEqual(
-            await client.geoRadius('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoRadius', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadius('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm'),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('geoRadius', async client => {
+    assert.deepEqual(
+      await client.geoRadius('key', {
+        longitude: 1,
+        latitude: 2
+      }, 3, 'm'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUS.ts b/packages/client/lib/commands/GEORADIUS.ts
index f47cf508848..2f622415064 100644
--- a/packages/client/lib/commands/GEORADIUS.ts
+++ b/packages/client/lib/commands/GEORADIUS.ts
@@ -1,25 +1,36 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchOptions, GeoCoordinates, pushGeoRadiusArguments, GeoUnits } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { GeoCoordinates, GeoUnits, GeoSearchOptions, parseGeoSearchOptions } from './GEOSEARCH';
 
-export const FIRST_KEY_INDEX = 1;
+export function parseGeoRadiusArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  from: GeoCoordinates,
+  radius: number,
+  unit: GeoUnits,
+  options?: GeoSearchOptions
+) {
+  parser.pushKey(key);
+  parser.push(from.longitude.toString(), from.latitude.toString(), radius.toString(), unit);
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    coordinates: GeoCoordinates,
-    radius: number,
-    unit: GeoUnits,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    return pushGeoRadiusArguments(
-        ['GEORADIUS'],
-        key,
-        coordinates,
-        radius,
-        unit,
-        options
-    );
+  parseGeoSearchOptions(parser, options)
 }
 
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Queries members in a geospatial index based on a radius from a center point
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center coordinates for the search
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param options - Additional search options
+   */
+  parseCommand(...args: Parameters<typeof parseGeoRadiusArguments>) {
+    args[0].push('GEORADIUS');
+    return parseGeoRadiusArguments(...args);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
+
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts
index 8cc4212c839..c81c3d75815 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEORADIUSBYMEMBER';
+import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUSBYMEMBER', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member', 3 , 'm'),
-            ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GEORADIUSBYMEMBER, 'key', 'member', 3, 'm'),
+      ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm']
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusByMember', async client => {
-        assert.deepEqual(
-            await client.geoRadiusByMember('key',  'member', 3 , 'm'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoRadiusByMember', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusByMember('key', 'member', 3 , 'm'),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('geoRadiusByMember', async client => {
+    assert.deepEqual(
+      await client.geoRadiusByMember('key', 'member', 3, 'm'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts
index 96bb622fb85..ee29ab84115 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts
@@ -1,25 +1,42 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchOptions, pushGeoRadiusArguments, GeoUnits } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { GeoUnits, GeoSearchOptions, parseGeoSearchOptions } from './GEOSEARCH';
 
-export const FIRST_KEY_INDEX = 1;
+export function parseGeoRadiusByMemberArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  from: RedisArgument,
+  radius: number,
+  unit: GeoUnits,
+  options?: GeoSearchOptions
+) {
+  parser.pushKey(key);
+  parser.push(from, radius.toString(), unit);
 
-export const IS_READ_ONLY = true;
+  parseGeoSearchOptions(parser, options);
+}
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: string,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Queries members in a geospatial index based on a radius from a member
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Member name to use as center point
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param options - Additional search options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    from: RedisArgument,
     radius: number,
     unit: GeoUnits,
     options?: GeoSearchOptions
-): RedisCommandArguments {
-    return pushGeoRadiusArguments(
-        ['GEORADIUSBYMEMBER'],
-        key,
-        member,
-        radius,
-        unit,
-        options
-    );
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+  ) {
+    parser.push('GEORADIUSBYMEMBER');
+    parseGeoRadiusByMemberArguments(parser, key, from, radius, unit, options);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts
deleted file mode 100644
index 100ecc03368..00000000000
--- a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './GEORADIUSBYMEMBERSTORE';
-
-describe('GEORADIUSBYMEMBERSTORE', () => {
-    describe('transformArguments', () => {
-        it('STORE', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member', 3 , 'm', 'dest', {
-                    SORT: 'ASC',
-                    COUNT: {
-                        value: 1,
-                        ANY: true
-                    }
-                }),
-                ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'ASC', 'COUNT', '1', 'ANY', 'STORE', 'dest']
-            );
-        });
-
-        it('STOREDIST', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member', 3 , 'm', 'dest', { STOREDIST: true }),
-                ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STOREDIST', 'dest']
-            );
-        });
-    });
-
-    testUtils.testWithClient('client.geoRadiusByMemberStore', async client => {
-        await client.geoAdd('source', {
-            longitude: 1,
-            latitude: 1,
-            member: 'member'
-        });
-
-        assert.equal(
-            await client.geoRadiusByMemberStore('source', 'member', 3 , 'm', 'dest'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoRadiusByMemberStore', async cluster => {
-        await cluster.geoAdd('{tag}source', {
-            longitude: 1,
-            latitude: 1,
-            member: 'member'
-        });
-
-        assert.equal(
-            await cluster.geoRadiusByMemberStore('{tag}source', 'member', 3 , 'm','{tag}destination'),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
-});
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts
deleted file mode 100644
index 28f3c25fac9..00000000000
--- a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoUnits, GeoRadiusStoreOptions, pushGeoRadiusStoreArguments } from './generic-transformers';
-
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: string,
-    radius: number,
-    unit: GeoUnits,
-    destination: RedisCommandArgument,
-    options?: GeoRadiusStoreOptions,
-): RedisCommandArguments {
-    return pushGeoRadiusStoreArguments(
-        ['GEORADIUSBYMEMBER'],
-        key,
-        member,
-        radius,
-        unit,
-        destination,
-        options
-    );
-}
-
-export declare function transformReply(): number
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts
index f3a47856e86..bd4aa86dec1 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEORADIUSBYMEMBER_RO';
+import GEORADIUSBYMEMBER_RO from './GEORADIUSBYMEMBER_RO';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUSBYMEMBER_RO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member', 3 , 'm'),
-            ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GEORADIUSBYMEMBER_RO, 'key', 'member', 3, 'm'),
+      ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm']
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusByMemberRo', async client => {
-        assert.deepEqual(
-            await client.geoRadiusByMemberRo('key',  'member', 3 , 'm'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoRadiusByMemberRo', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusByMemberRo('key', 'member', 3 , 'm'),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('geoRadiusByMemberRo', async client => {
+    assert.deepEqual(
+      await client.geoRadiusByMemberRo('key', 'member', 3, 'm'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts
index 63f29ae65b5..8629694588a 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts
@@ -1,25 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchOptions, pushGeoRadiusArguments, GeoUnits } from './generic-transformers';
+import { Command } from '../RESP/types';
+import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: string,
-    radius: number,
-    unit: GeoUnits,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    return pushGeoRadiusArguments(
-        ['GEORADIUSBYMEMBER_RO'],
-        key,
-        member,
-        radius,
-        unit,
-        options
-    );
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Read-only variant that queries members in a geospatial index based on a radius from a member
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Member name to use as center point
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param options - Additional search options
+   */
+  parseCommand(...args: Parameters<typeof parseGeoRadiusByMemberArguments>) {
+    const parser = args[0];
+    parser.push('GEORADIUSBYMEMBER_RO');
+    parseGeoRadiusByMemberArguments(...args);
+  },
+  transformReply: GEORADIUSBYMEMBER.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts
index 7904a763998..52b31b03594 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts
@@ -1,31 +1,45 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisCommandArguments } from '.';
-import { GeoReplyWith } from './generic-transformers';
-import { transformArguments } from './GEORADIUSBYMEMBER_RO_WITH';
+import GEORADIUSBYMEMBER_RO_WITH from './GEORADIUSBYMEMBER_RO_WITH';
+import { CommandArguments } from '../RESP/types';
+import { GEO_REPLY_WITH } from './GEOSEARCH_WITH';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUSBYMEMBER_RO WITH', () => {
-    it('transformArguments', () => {
-        const expectedReply: RedisCommandArguments = ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm', 'WITHDIST'];
-        expectedReply.preserve = ['WITHDIST'];
+  it('transformArguments', () => {
+    const expectedReply: CommandArguments = ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm', 'WITHDIST'];
+    expectedReply.preserve = ['WITHDIST'];
 
-        assert.deepEqual(
-            transformArguments('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]),
-            expectedReply
-        );
-    });
+    assert.deepEqual(
+      parseArgs(GEORADIUSBYMEMBER_RO_WITH, 'key', 'member', 3, 'm', [
+        GEO_REPLY_WITH.DISTANCE
+      ]),
+      expectedReply
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusByMemberRoWith', async client => {
-        assert.deepEqual(
-            await client.geoRadiusByMemberRoWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('geoRadiusByMemberRoWith', async client => {
+    const [, reply] = await Promise.all([
+      client.geoAdd('key', {
+        member: 'member',
+        longitude: 1,
+        latitude: 2
+      }),
+      client.geoRadiusByMemberRoWith('key', 'member', 1, 'm', [
+        GEO_REPLY_WITH.HASH,
+        GEO_REPLY_WITH.DISTANCE,
+        GEO_REPLY_WITH.COORDINATES
+      ])
+    ]);
 
-    testUtils.testWithCluster('cluster.geoRadiusByMemberRoWith', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusByMemberRoWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(reply.length, 1);
+    assert.equal(reply[0].member, 'member');
+    assert.equal(typeof reply[0].distance, 'string');
+    assert.equal(typeof reply[0].hash, 'number');
+    assert.equal(typeof reply[0].coordinates?.longitude, 'string');
+    assert.equal(typeof reply[0].coordinates?.latitude, 'string');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts
index 6061be734b5..239c6cf2444 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts
@@ -1,30 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoReplyWith, GeoSearchOptions, GeoUnits } from './generic-transformers';
-import { transformArguments as geoRadiusTransformArguments } from './GEORADIUSBYMEMBER_RO';
+import { Command } from '../RESP/types';
+import GEORADIUSBYMEMBER_WITH, { parseGeoRadiusByMemberWithArguments } from './GEORADIUSBYMEMBER_WITH';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER_RO';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: string,
-    radius: number,
-    unit: GeoUnits,
-    replyWith: Array<GeoReplyWith>,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    const args: RedisCommandArguments = geoRadiusTransformArguments(
-        key,
-        member,
-        radius,
-        unit,
-        options
-    );
-
-    args.push(...replyWith);
-
-    args.preserve = replyWith;
-
-    return args;
-}
-
-export { transformGeoMembersWithReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Read-only variant that queries members in a geospatial index based on a radius from a member with additional information
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Member name to use as center point
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param withValues - Information to include with each returned member
+   */
+  parseCommand(...args: Parameters<typeof parseGeoRadiusByMemberWithArguments>) {
+    const parser = args[0];
+    parser.push('GEORADIUSBYMEMBER_RO');
+    parseGeoRadiusByMemberWithArguments(...args);
+  },
+  transformReply: GEORADIUSBYMEMBER_WITH.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts
new file mode 100644
index 00000000000..9edb08d1eae
--- /dev/null
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts
@@ -0,0 +1,40 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import GEORADIUSBYMEMBER_STORE from './GEORADIUSBYMEMBER_STORE';
+import { parseArgs } from './generic-transformers';
+
+describe('GEORADIUSBYMEMBER STORE', () => {
+  describe('transformArguments', () => {
+    it('STORE', () => {
+      assert.deepEqual(
+        parseArgs(GEORADIUSBYMEMBER_STORE, 'key', 'member', 3, 'm', 'destination'),
+        ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STORE', 'destination']
+      );
+    });
+
+    it('STOREDIST', () => {
+      assert.deepEqual(
+        parseArgs(GEORADIUSBYMEMBER_STORE, 'key', 'member', 3, 'm', 'destination', {
+          STOREDIST: true
+        }),
+        ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STOREDIST', 'destination']
+      );
+    });
+  });
+
+  testUtils.testAll('geoRadiusByMemberStore', async client => {
+    const [, reply] = await Promise.all([
+      client.geoAdd('{tag}source', {
+        longitude: 1,
+        latitude: 2,
+        member: 'member'
+      }),
+      client.geoRadiusByMemberStore('{tag}source', 'member', 3, 'm', '{tag}destination')
+    ]);
+
+    assert.equal(reply, 1);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts
new file mode 100644
index 00000000000..20a1e0b6699
--- /dev/null
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts
@@ -0,0 +1,43 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER';
+import { GeoSearchOptions, GeoUnits } from './GEOSEARCH';
+
+export interface GeoRadiusStoreOptions extends GeoSearchOptions {
+  STOREDIST?: boolean;
+}
+
+export default {
+  IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY,
+  /**
+   * Queries members in a geospatial index based on a radius from a member and stores the results
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Member name to use as center point
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param destination - Key to store the results
+   * @param options - Additional search and storage options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    from: RedisArgument,
+    radius: number,
+    unit: GeoUnits,
+    destination: RedisArgument,
+    options?: GeoRadiusStoreOptions
+  ) {
+    parser.push('GEORADIUSBYMEMBER')
+    parseGeoRadiusByMemberArguments(parser, key, from, radius, unit, options);
+
+    if (options?.STOREDIST) {
+      parser.push('STOREDIST');
+      parser.pushKey(destination);
+    } else {
+      parser.push('STORE');
+      parser.pushKey(destination);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts
index 24bffd9e89f..9d634d60656 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts
@@ -1,31 +1,45 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisCommandArguments } from '.';
-import { GeoReplyWith } from './generic-transformers';
-import { transformArguments } from './GEORADIUSBYMEMBER_WITH';
+import GEORADIUSBYMEMBER_WITH from './GEORADIUSBYMEMBER_WITH';
+import { CommandArguments } from '../RESP/types';
+import { GEO_REPLY_WITH } from './GEOSEARCH_WITH';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUSBYMEMBER WITH', () => {
-    it('transformArguments', () => {
-        const expectedReply: RedisCommandArguments = ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'WITHDIST'];
-        expectedReply.preserve = ['WITHDIST'];
+  it('transformArguments', () => {
+    const expectedReply: CommandArguments = ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'WITHDIST'];
+    expectedReply.preserve = ['WITHDIST'];
 
-        assert.deepEqual(
-            transformArguments('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]),
-            expectedReply
-        );
-    });
+    assert.deepEqual(
+      parseArgs(GEORADIUSBYMEMBER_WITH, 'key', 'member', 3, 'm', [
+        GEO_REPLY_WITH.DISTANCE
+      ]),
+      expectedReply
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusByMemberWith', async client => {
-        assert.deepEqual(
-            await client.geoRadiusByMemberWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('geoRadiusByMemberWith', async client => {
+    const [, reply] = await Promise.all([
+      client.geoAdd('key', {
+        member: 'member',
+        longitude: 1,
+        latitude: 2
+      }),
+      client.geoRadiusByMemberWith('key', 'member', 1, 'm', [
+        GEO_REPLY_WITH.HASH,
+        GEO_REPLY_WITH.DISTANCE,
+        GEO_REPLY_WITH.COORDINATES
+      ])
+    ]);
 
-    testUtils.testWithCluster('cluster.geoRadiusByMemberWith', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusByMemberWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(reply.length, 1);
+    assert.equal(reply[0].member, 'member');
+    assert.equal(typeof reply[0].distance, 'string');
+    assert.equal(typeof reply[0].hash, 'number');
+    assert.equal(typeof reply[0].coordinates!.longitude, 'string');
+    assert.equal(typeof reply[0].coordinates!.latitude, 'string');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts
index 7d7dbe06a54..9f7a01bb525 100644
--- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts
+++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts
@@ -1,30 +1,49 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoReplyWith, GeoSearchOptions, GeoUnits } from './generic-transformers';
-import { transformArguments as transformGeoRadiusArguments } from './GEORADIUSBYMEMBER';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER';
+import { GeoSearchOptions, GeoUnits, parseGeoSearchOptions } from './GEOSEARCH';
+import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER';
+export function parseGeoRadiusByMemberWithArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  from: RedisArgument,
+  radius: number,
+  unit: GeoUnits,
+  replyWith: Array<GeoReplyWith>,
+  options?: GeoSearchOptions
+) {
+  parser.pushKey(key);
+  parser.push(from, radius.toString(), unit);
+  parseGeoSearchOptions(parser, options);
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: string,
+  parser.push(...replyWith);
+  parser.preserve = replyWith;
+}
+
+export default {
+  IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY,
+  /**
+   * Queries members in a geospatial index based on a radius from a member with additional information
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Member name to use as center point
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param replyWith - Information to include with each returned member
+   * @param options - Additional search options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    from: RedisArgument,
     radius: number,
     unit: GeoUnits,
     replyWith: Array<GeoReplyWith>,
     options?: GeoSearchOptions
-): RedisCommandArguments {
-    const args: RedisCommandArguments = transformGeoRadiusArguments(
-        key,
-        member,
-        radius,
-        unit,
-        options
-    );
-
-    args.push(...replyWith);
-
-    args.preserve = replyWith;
-
-    return args;
-}
-
-export { transformGeoMembersWithReply as transformReply } from './generic-transformers';
+  ) {
+    parser.push('GEORADIUSBYMEMBER');
+    parseGeoRadiusByMemberWithArguments(parser, key, from, radius, unit, replyWith, options);
+  },
+  transformReply: GEOSEARCH_WITH.transformReply
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/GEORADIUSSTORE.spec.ts b/packages/client/lib/commands/GEORADIUSSTORE.spec.ts
deleted file mode 100644
index 4c6372732e5..00000000000
--- a/packages/client/lib/commands/GEORADIUSSTORE.spec.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './GEORADIUSSTORE';
-
-describe('GEORADIUSSTORE', () => {
-    describe('transformArguments', () => {
-        it('STORE', () => {
-            assert.deepEqual(
-                transformArguments('key', {longitude: 1, latitude: 2}, 3 , 'm', 'dest', {
-                    SORT: 'ASC',
-                    COUNT: {
-                        value: 1,
-                        ANY: true
-                    }
-                }),
-                ['GEORADIUS', 'key', '1', '2', '3', 'm', 'ASC', 'COUNT', '1', 'ANY', 'STORE', 'dest']
-            );
-        });
-
-        it('STOREDIST', () => {
-            assert.deepEqual(
-                transformArguments('key', {longitude: 1, latitude: 2}, 3 , 'm', 'dest', { STOREDIST: true }),
-                ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STOREDIST', 'dest']
-            );
-        });
-    });
-
-    testUtils.testWithClient('client.geoRadiusStore', async client => {
-        await client.geoAdd('source', {
-            longitude: 1,
-            latitude: 1,
-            member: 'member'
-        });
-
-        assert.equal(
-            await client.geoRadiusStore('source', {longitude: 1, latitude: 1}, 3 , 'm', 'dest'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoRadiusStore', async cluster => {
-        await cluster.geoAdd('{tag}source', {
-            longitude: 1,
-            latitude: 1,
-            member: 'member'
-        });
-
-        assert.equal(
-            await cluster.geoRadiusStore('{tag}source', {longitude: 1, latitude: 1}, 3 , 'm', '{tag}destination'),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
-});
diff --git a/packages/client/lib/commands/GEORADIUSSTORE.ts b/packages/client/lib/commands/GEORADIUSSTORE.ts
deleted file mode 100644
index ad2317aa3af..00000000000
--- a/packages/client/lib/commands/GEORADIUSSTORE.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoCoordinates, GeoUnits, GeoRadiusStoreOptions, pushGeoRadiusStoreArguments } from './generic-transformers';
-
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    coordinates: GeoCoordinates,
-    radius: number,
-    unit: GeoUnits,
-    destination: RedisCommandArgument,
-    options?: GeoRadiusStoreOptions,
-): RedisCommandArguments {
-    return pushGeoRadiusStoreArguments(
-        ['GEORADIUS'],
-        key,
-        coordinates,
-        radius,
-        unit,
-        destination,
-        options
-    );
-}
-
-export declare function transformReply(): number;
diff --git a/packages/client/lib/commands/GEORADIUS_RO.spec.ts b/packages/client/lib/commands/GEORADIUS_RO.spec.ts
index b3cdca18d3f..917eba3ab8e 100644
--- a/packages/client/lib/commands/GEORADIUS_RO.spec.ts
+++ b/packages/client/lib/commands/GEORADIUS_RO.spec.ts
@@ -1,35 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEORADIUS_RO';
+import GEORADIUS_RO from './GEORADIUS_RO';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUS_RO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm'),
-            ['GEORADIUS_RO', 'key', '1', '2', '3', 'm']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GEORADIUS_RO, 'key', {
+        longitude: 1,
+        latitude: 2
+      }, 3, 'm'),
+      ['GEORADIUS_RO', 'key', '1', '2', '3', 'm']
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusRo', async client => {
-        assert.deepEqual(
-            await client.geoRadiusRo('key',  {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoRadiusRo', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusRo('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm'),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('geoRadiusRo', async client => {
+    assert.deepEqual(
+      await client.geoRadiusRo('key', {
+        longitude: 1,
+        latitude: 2
+      }, 3, 'm'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUS_RO.ts b/packages/client/lib/commands/GEORADIUS_RO.ts
index ac378a5150b..29cf6f8ccd7 100644
--- a/packages/client/lib/commands/GEORADIUS_RO.ts
+++ b/packages/client/lib/commands/GEORADIUS_RO.ts
@@ -1,25 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchOptions, GeoCoordinates, pushGeoRadiusArguments, GeoUnits } from './generic-transformers';
+import { Command } from '../RESP/types';
+import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    coordinates: GeoCoordinates,
-    radius: number,
-    unit: GeoUnits,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    return pushGeoRadiusArguments(
-        ['GEORADIUS_RO'],
-        key,
-        coordinates,
-        radius,
-        unit,
-        options
-    );
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Read-only variant that queries members in a geospatial index based on a radius from a center point
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center coordinates for the search
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param options - Additional search options
+   */
+  parseCommand(...args: Parameters<typeof parseGeoRadiusArguments>) {
+    args[0].push('GEORADIUS_RO');
+    parseGeoRadiusArguments(...args);
+  },
+  transformReply: GEORADIUS.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts
index 21b00ff90b8..01d79954b64 100644
--- a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts
+++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts
@@ -1,40 +1,49 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisCommandArguments } from '.';
-import { GeoReplyWith } from './generic-transformers';
-import { transformArguments } from './GEORADIUS_RO_WITH';
+import GEORADIUS_RO_WITH from './GEORADIUS_RO_WITH';
+import { GEO_REPLY_WITH } from './GEOSEARCH_WITH';
+import { CommandArguments } from '../RESP/types';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUS_RO WITH', () => {
-    it('transformArguments', () => {
-        const expectedReply: RedisCommandArguments = ['GEORADIUS_RO', 'key', '1', '2', '3', 'm', 'WITHDIST'];
-        expectedReply.preserve = ['WITHDIST'];
+  it('transformArguments', () => {
+    const expectedReply: CommandArguments = ['GEORADIUS_RO', 'key', '1', '2', '3', 'm', 'WITHDIST'];
+    expectedReply.preserve = ['WITHDIST'];
 
-        assert.deepEqual(
-            transformArguments('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm', [GeoReplyWith.DISTANCE]),
-            expectedReply
-        );
-    });
+    assert.deepEqual(
+      parseArgs(GEORADIUS_RO_WITH, 'key', {
+        longitude: 1,
+        latitude: 2
+      }, 3, 'm', [GEO_REPLY_WITH.DISTANCE]),
+      expectedReply
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusRoWith', async client => {
-        assert.deepEqual(
-            await client.geoRadiusRoWith('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('geoRadiusRoWith', async client => {
+    const [, reply] = await Promise.all([
+      client.geoAdd('key', {
+        member: 'member',
+        longitude: 1,
+        latitude: 2
+      }),
+      client.geoRadiusRoWith('key', {
+        longitude: 1,
+        latitude: 2
+      }, 1, 'm', [
+        GEO_REPLY_WITH.HASH,
+        GEO_REPLY_WITH.DISTANCE,
+        GEO_REPLY_WITH.COORDINATES
+      ])
+    ]);
 
-    testUtils.testWithCluster('cluster.geoRadiusReadOnlyWith', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusRoWith('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(reply.length, 1);
+    assert.equal(reply[0].member, 'member');
+    assert.equal(typeof reply[0].distance, 'string');
+    assert.equal(typeof reply[0].hash, 'number');
+    assert.equal(typeof reply[0].coordinates!.longitude, 'string');
+    assert.equal(typeof reply[0].coordinates!.latitude, 'string');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts
index 424e5fcd998..aaaef482f05 100644
--- a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts
+++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts
@@ -1,30 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoReplyWith, GeoSearchOptions, GeoCoordinates, GeoUnits } from './generic-transformers';
-import { transformArguments as transformGeoRadiusRoArguments } from './GEORADIUS_RO';
+import { Command } from '../RESP/types';
+import { parseGeoRadiusWithArguments } from './GEORADIUS_WITH';
+import GEORADIUS_WITH from './GEORADIUS_WITH';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS_RO';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    coordinates: GeoCoordinates,
-    radius: number,
-    unit: GeoUnits,
-    replyWith: Array<GeoReplyWith>,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    const args: RedisCommandArguments = transformGeoRadiusRoArguments(
-        key,
-        coordinates,
-        radius,
-        unit,
-        options
-    );
-
-    args.push(...replyWith);
-
-    args.preserve = replyWith;
-
-    return args;
-}
-
-export { transformGeoMembersWithReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Read-only variant that queries members in a geospatial index based on a radius from a center point with additional information
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center coordinates for the search
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param replyWith - Information to include with each returned member
+   * @param options - Additional search options
+   */
+  parseCommand(...args: Parameters<typeof parseGeoRadiusWithArguments>) {
+    args[0].push('GEORADIUS_RO');
+    parseGeoRadiusWithArguments(...args);
+  },
+  transformReply: GEORADIUS_WITH.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUS_STORE.spec.ts b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts
new file mode 100644
index 00000000000..9a9bcf37bcf
--- /dev/null
+++ b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts
@@ -0,0 +1,49 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import GEORADIUS_STORE from './GEORADIUS_STORE';
+import { parseArgs } from './generic-transformers';
+
+describe('GEORADIUS STORE', () => {
+  describe('transformArguments', () => {
+    it('STORE', () => {
+      assert.deepEqual(
+        parseArgs(GEORADIUS_STORE, 'key', {
+          longitude: 1,
+          latitude: 2
+        }, 3, 'm', 'destination'),
+        ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STORE', 'destination']
+      );
+    });
+
+    it('STOREDIST', () => {
+      assert.deepEqual(
+        parseArgs(GEORADIUS_STORE, 'key', {
+          longitude: 1,
+          latitude: 2 
+        }, 3, 'm', 'destination', {
+          STOREDIST: true
+        }),
+        ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STOREDIST', 'destination']
+      );
+    });
+  });
+
+  testUtils.testAll('geoRadiusStore', async client => {
+    const [, reply] = await Promise.all([
+      client.geoAdd('{tag}source', {
+        longitude: 1,
+        latitude: 2,
+        member: 'member'
+      }),
+      client.geoRadiusStore('{tag}source', {
+        longitude: 1,
+        latitude: 2
+      }, 1, 'm', '{tag}destination')
+    ]);
+
+    assert.equal(reply, 1);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/GEORADIUS_STORE.ts b/packages/client/lib/commands/GEORADIUS_STORE.ts
new file mode 100644
index 00000000000..b2db8ca9882
--- /dev/null
+++ b/packages/client/lib/commands/GEORADIUS_STORE.ts
@@ -0,0 +1,42 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS';
+import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH';
+
+export interface GeoRadiusStoreOptions extends GeoSearchOptions {
+  STOREDIST?: boolean;
+}
+
+export default {
+  IS_READ_ONLY: GEORADIUS.IS_READ_ONLY,
+  /**
+   * Queries members in a geospatial index based on a radius from a center point and stores the results
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center coordinates for the search
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param destination - Key to store the results
+   * @param options - Additional search and storage options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    from: GeoCoordinates,
+    radius: number,
+    unit: GeoUnits,
+    destination: RedisArgument,
+    options?: GeoRadiusStoreOptions
+  ) {
+    parser.push('GEORADIUS');
+    parseGeoRadiusArguments(parser, key, from, radius, unit, options);
+    if (options?.STOREDIST) {
+      parser.push('STOREDIST');
+      parser.pushKey(destination);
+    } else {
+      parser.push('STORE');
+      parser.pushKey(destination);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts
index 44366198beb..f514c9be96f 100644
--- a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts
+++ b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts
@@ -1,40 +1,49 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisCommandArguments } from '.';
-import { GeoReplyWith } from './generic-transformers';
-import { transformArguments } from './GEORADIUS_WITH';
+import GEORADIUS_WITH from './GEORADIUS_WITH';
+import { GEO_REPLY_WITH } from './GEOSEARCH_WITH';
+import { CommandArguments } from '../RESP/types';
+import { parseArgs } from './generic-transformers';
 
 describe('GEORADIUS WITH', () => {
-    it('transformArguments', () => {
-        const expectedReply: RedisCommandArguments = ['GEORADIUS', 'key', '1', '2', '3', 'm', 'WITHDIST'];
-        expectedReply.preserve = ['WITHDIST'];
+  it('transformArguments', () => {
+    const expectedReply: CommandArguments = ['GEORADIUS', 'key', '1', '2', '3', 'm', 'WITHDIST'];
+    expectedReply.preserve = ['WITHDIST'];
 
-        assert.deepEqual(
-            transformArguments('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm', [GeoReplyWith.DISTANCE]),
-            expectedReply
-        );
-    });
+    assert.deepEqual(
+      parseArgs(GEORADIUS_WITH, 'key', {
+        longitude: 1,
+        latitude: 2
+      }, 3, 'm', [GEO_REPLY_WITH.DISTANCE]),
+      expectedReply
+    );
+  });
 
-    testUtils.testWithClient('client.geoRadiusWith', async client => {
-        assert.deepEqual(
-            await client.geoRadiusWith('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('geoRadiusWith', async client => {
+    const [, reply] = await Promise.all([
+      client.geoAdd('key', {
+        member: 'member',
+        longitude: 1,
+        latitude: 2
+      }),
+      client.geoRadiusWith('key', {
+        longitude: 1,
+        latitude: 2
+      }, 1, 'm', [
+        GEO_REPLY_WITH.HASH,
+        GEO_REPLY_WITH.DISTANCE,
+        GEO_REPLY_WITH.COORDINATES
+      ])
+    ]);
 
-    testUtils.testWithCluster('cluster.geoRadiusWith', async cluster => {
-        assert.deepEqual(
-            await cluster.geoRadiusWith('key', {
-                longitude: 1,
-                latitude: 2
-            }, 3 , 'm', [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(reply.length, 1);
+    assert.equal(reply[0].member, 'member');
+    assert.equal(typeof reply[0].distance, 'string');
+    assert.equal(typeof reply[0].hash, 'number');
+    assert.equal(typeof reply[0].coordinates?.longitude, 'string');
+    assert.equal(typeof reply[0].coordinates?.latitude, 'string');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEORADIUS_WITH.ts b/packages/client/lib/commands/GEORADIUS_WITH.ts
index dc3f4288f01..5028a926145 100644
--- a/packages/client/lib/commands/GEORADIUS_WITH.ts
+++ b/packages/client/lib/commands/GEORADIUS_WITH.ts
@@ -1,30 +1,46 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoReplyWith, GeoSearchOptions, GeoCoordinates, GeoUnits } from './generic-transformers';
-import { transformArguments as transformGeoRadiusArguments } from './GEORADIUS';
+import { CommandParser } from '../client/parser';
+import { Command, RedisArgument } from '../RESP/types';
+import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS';
+import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH';
+import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS';
+export function parseGeoRadiusWithArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  from: GeoCoordinates,
+  radius: number,
+  unit: GeoUnits,
+  replyWith: Array<GeoReplyWith>,
+  options?: GeoSearchOptions,
+) {
+  parseGeoRadiusArguments(parser, key, from, radius, unit, options)
+  parser.pushVariadic(replyWith);
+  parser.preserve = replyWith;
+}
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    coordinates: GeoCoordinates,
+export default {
+  IS_READ_ONLY: GEORADIUS.IS_READ_ONLY,
+  /**
+   * Queries members in a geospatial index based on a radius from a center point with additional information
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center coordinates for the search
+   * @param radius - Radius of the search area
+   * @param unit - Unit of distance (m, km, ft, mi)
+   * @param replyWith - Information to include with each returned member
+   * @param options - Additional search options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    from: GeoCoordinates,
     radius: number,
     unit: GeoUnits,
     replyWith: Array<GeoReplyWith>,
     options?: GeoSearchOptions
-): RedisCommandArguments {
-    const args: RedisCommandArguments = transformGeoRadiusArguments(
-        key,
-        coordinates,
-        radius,
-        unit,
-        options
-    );
-
-    args.push(...replyWith);
-
-    args.preserve = replyWith;
-
-    return args;
-}
-
-export { transformGeoMembersWithReply as transformReply } from './generic-transformers';
+  ) {
+    parser.push('GEORADIUS');
+    parseGeoRadiusWithArguments(parser, key, from, radius, unit, replyWith, options);
+  },
+  transformReply: GEOSEARCH_WITH.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEOSEARCH.spec.ts b/packages/client/lib/commands/GEOSEARCH.spec.ts
index ec0d4bcc4f8..4cd7e61a0ac 100644
--- a/packages/client/lib/commands/GEOSEARCH.spec.ts
+++ b/packages/client/lib/commands/GEOSEARCH.spec.ts
@@ -1,37 +1,88 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GEOSEARCH';
+import GEOSEARCH from './GEOSEARCH';
+import { parseArgs } from './generic-transformers';
 
 describe('GEOSEARCH', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member', {
-                radius: 1,
-                unit: 'm'
-            }),
-            ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
-        );
+  describe('transformArguments', () => {
+    it('FROMMEMBER, BYRADIUS, without options', () => {
+      assert.deepEqual(
+        parseArgs(GEOSEARCH, 'key', 'member', {
+          radius: 1,
+          unit: 'm'
+        }),
+        ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
+      );
+    });
+
+    it('FROMLONLAT, BYBOX, without options', () => {
+      assert.deepEqual(
+        parseArgs(GEOSEARCH, 'key', {
+          longitude: 1,
+          latitude: 2
+        }, {
+          width: 1,
+          height: 2,
+          unit: 'm'
+        }),
+        ['GEOSEARCH', 'key', 'FROMLONLAT', '1', '2', 'BYBOX', '1', '2', 'm']
+      );
     });
 
-    testUtils.testWithClient('client.geoSearch', async client => {
+    it('with SORT', () => {
+      assert.deepEqual(
+        parseArgs(GEOSEARCH, 'key', 'member', {
+          radius: 1,
+          unit: 'm'
+        }, {
+          SORT: 'ASC'
+        }),
+        ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC']
+      );
+    });
+
+    describe('with COUNT', () => {
+      it('number', () => {
         assert.deepEqual(
-            await client.geoSearch('key', 'member', {
-                radius: 1,
-                unit: 'm'
-            }),
-            []
+          parseArgs(GEOSEARCH, 'key', 'member', {
+            radius: 1,
+            unit: 'm'
+          }, {
+            COUNT: 1
+          }),
+          ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'COUNT', '1']
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
 
-    testUtils.testWithCluster('cluster.geoSearch', async cluster => {
+      it('with ANY', () => {
         assert.deepEqual(
-            await cluster.geoSearch('key', 'member', {
-                radius: 1,
-                unit: 'm'
-            }),
-            []
+          parseArgs(GEOSEARCH, 'key', 'member', {
+            radius: 1,
+            unit: 'm'
+          }, {
+            COUNT: {
+              value: 1,
+              ANY: true
+            }
+          }),
+          ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'COUNT', '1', 'ANY']
         );
-    }, GLOBAL.CLUSTERS.OPEN);
+      });
+    });
+  });
+
+  testUtils.testAll('geoSearch', async client => {
+    assert.deepEqual(
+      await client.geoSearch('key', 'member', {
+        radius: 1,
+        unit: 'm'
+      }),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEOSEARCH.ts b/packages/client/lib/commands/GEOSEARCH.ts
index a02a21391f6..a26ccec23eb 100644
--- a/packages/client/lib/commands/GEOSEARCH.ts
+++ b/packages/client/lib/commands/GEOSEARCH.ts
@@ -1,17 +1,102 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export type GeoUnits = 'm' | 'km' | 'mi' | 'ft';
 
-export const IS_READ_ONLY = true;
+export interface GeoCoordinates {
+  longitude: RedisArgument | number;
+  latitude: RedisArgument | number;
+}
+
+export type GeoSearchFrom = RedisArgument | GeoCoordinates;
+
+export interface GeoSearchByRadius {
+  radius: number;
+  unit: GeoUnits;
+}
+
+export interface GeoSearchByBox {
+  width: number;
+  height: number;
+  unit: GeoUnits;
+}
+
+export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox;
+
+export function parseGeoSearchArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  from: GeoSearchFrom,
+  by: GeoSearchBy,
+  options?: GeoSearchOptions,
+) {
+  parser.pushKey(key);
+
+  if (typeof from === 'string' || from instanceof Buffer) {
+    parser.push('FROMMEMBER', from);
+  } else {
+    parser.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString());
+  }
+
+  if ('radius' in by) {
+    parser.push('BYRADIUS', by.radius.toString(), by.unit);
+  } else {
+    parser.push('BYBOX', by.width.toString(), by.height.toString(), by.unit);
+  }
 
-export function transformArguments(
-    key: RedisCommandArgument,
+  parseGeoSearchOptions(parser, options);
+}
+
+export type GeoCountArgument = number | {
+  value: number;
+  ANY?: boolean;
+};
+
+export interface GeoSearchOptions {
+  SORT?: 'ASC' | 'DESC';
+  COUNT?: GeoCountArgument;
+}
+
+export function parseGeoSearchOptions(
+  parser: CommandParser,
+  options?: GeoSearchOptions
+) {
+  if (options?.SORT) {
+    parser.push(options.SORT);
+  }
+
+  if (options?.COUNT) {
+    if (typeof options.COUNT === 'number') {
+      parser.push('COUNT', options.COUNT.toString());
+    } else {
+      parser.push('COUNT', options.COUNT.value.toString());
+  
+      if (options.COUNT.ANY) {
+        parser.push('ANY');
+      }
+    }
+  }
+}
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Queries members inside an area of a geospatial index
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center point of the search (member name or coordinates)
+   * @param by - Search area specification (radius or box dimensions)
+   * @param options - Additional search options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     from: GeoSearchFrom,
     by: GeoSearchBy,
     options?: GeoSearchOptions
-): RedisCommandArguments {
-    return pushGeoSearchArguments(['GEOSEARCH'], key, from, by, options);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+  ) {
+    parser.push('GEOSEARCH');
+    parseGeoSearchArguments(parser, key, from, by, options);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts
index eb32fa134e4..b8427ae0412 100644
--- a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts
+++ b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts
@@ -1,81 +1,45 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './GEOSEARCHSTORE';
+import GEOSEARCHSTORE from './GEOSEARCHSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('GEOSEARCHSTORE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
-
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'source', 'member', {
-                    radius: 1,
-                    unit: 'm'
-                }, {
-                    SORT: 'ASC',
-                    COUNT: {
-                        value: 1,
-                        ANY: true
-                    }
-                }),
-                ['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC', 'COUNT', '1', 'ANY']
-            );
-        });
-
-        it('with STOREDIST', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'source', 'member', {
-                    radius: 1,
-                    unit: 'm'
-                }, {
-                    SORT: 'ASC',
-                    COUNT: {
-                        value: 1,
-                        ANY: true
-                    },
-                    STOREDIST: true
-                }),
-                ['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC', 'COUNT', '1', 'ANY', 'STOREDIST']
-            );
-        });
+  testUtils.isVersionGreaterThanHook([6, 2]);
+
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(GEOSEARCHSTORE, 'source', 'destination', 'member', {
+          radius: 1,
+          unit: 'm'
+        }),
+        ['GEOSEARCHSTORE', 'source', 'destination', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
+      );
     });
 
-    it('transformReply with empty array (https://github.com/redis/redis/issues/9261)', () => {
-        assert.throws(
-            () => (transformReply as any)([]),
-            TypeError
-        );
+    it('with STOREDIST', () => {
+      assert.deepEqual(
+        parseArgs(GEOSEARCHSTORE, 'destination', 'source', 'member', {
+          radius: 1,
+          unit: 'm'
+        }, {
+          STOREDIST: true
+        }),
+        ['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'STOREDIST']
+      );
     });
-
-    testUtils.testWithClient('client.geoSearchStore', async client => {
-        await client.geoAdd('source', {
-            longitude: 1,
-            latitude: 1,
-            member: 'member'
-        });
-
-        assert.equal(
-            await client.geoSearchStore('destination', 'source', 'member', {
-                radius: 1,
-                unit: 'm'
-            }),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.geoSearchStore', async cluster => {
-        await cluster.geoAdd('{tag}source', {
-            longitude: 1,
-            latitude: 1,
-            member: 'member'
-        });
-
-        assert.equal(
-            await cluster.geoSearchStore('{tag}destination', '{tag}source', 'member', {
-                radius: 1,
-                unit: 'm'
-            }),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  });
+
+  testUtils.testAll('geoSearchStore', async client => {
+    assert.equal(
+      await client.geoSearchStore('{tag}destination', '{tag}source', 'member', {
+        radius: 1,
+        unit: 'm'
+      }),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.ts b/packages/client/lib/commands/GEOSEARCHSTORE.ts
index 7a91450cd9e..194eafda818 100644
--- a/packages/client/lib/commands/GEOSEARCHSTORE.ts
+++ b/packages/client/lib/commands/GEOSEARCHSTORE.ts
@@ -1,38 +1,41 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, parseGeoSearchArguments } from './GEOSEARCH';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEOSEARCH';
-
-interface GeoSearchStoreOptions extends GeoSearchOptions {
-    STOREDIST?: true;
+export interface GeoSearchStoreOptions extends GeoSearchOptions {
+  STOREDIST?: boolean;
 }
 
-export function transformArguments(
-    destination: RedisCommandArgument,
-    source: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Searches a geospatial index and stores the results in a new sorted set
+   * @param parser - The Redis command parser
+   * @param destination - Key to store the results
+   * @param source - Key of the geospatial index to search
+   * @param from - Center point of the search (member name or coordinates)
+   * @param by - Search area specification (radius or box dimensions)
+   * @param options - Additional search and storage options
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    source: RedisArgument,
     from: GeoSearchFrom,
     by: GeoSearchBy,
     options?: GeoSearchStoreOptions
-): RedisCommandArguments {
-    const args = pushGeoSearchArguments(
-        ['GEOSEARCHSTORE', destination],
-        source,
-        from,
-        by,
-        options
-    );
+  ) {
+    parser.push('GEOSEARCHSTORE');
 
-    if (options?.STOREDIST) {
-        args.push('STOREDIST');
+    if (destination !== undefined) {
+      parser.pushKey(destination);
     }
+  
+    parseGeoSearchArguments(parser, source, from, by, options);
 
-    return args;
-}
-
-export function transformReply(reply: number): number {
-    if (typeof reply !== 'number') {
-        throw new TypeError(`https://github.com/redis/redis/issues/9261`);
+    if (options?.STOREDIST) {
+      parser.push('STOREDIST');
     }
-
-    return reply;
-}
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts
index c1f5213775a..973e5d5827f 100644
--- a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts
+++ b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts
@@ -1,42 +1,50 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { RedisCommandArguments } from '.';
-import { GeoReplyWith } from './generic-transformers';
-import { transformArguments } from './GEOSEARCH_WITH';
+import GEOSEARCH_WITH, { GEO_REPLY_WITH } from './GEOSEARCH_WITH';
+import { CommandArguments } from '../RESP/types';
+import { parseArgs } from './generic-transformers';
 
 describe('GEOSEARCH WITH', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        const expectedReply: RedisCommandArguments = ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'WITHDIST'];
-        expectedReply.preserve = ['WITHDIST'];
+  it('transformArguments', () => {
+    const expectedReply: CommandArguments = ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'WITHDIST'];
+    expectedReply.preserve = ['WITHDIST'];
 
-        assert.deepEqual(
-            transformArguments('key', 'member', {
-                radius: 1,
-                unit: 'm'
-            }, [GeoReplyWith.DISTANCE]),
-            expectedReply
-        );
-    });
+    assert.deepEqual(
+      parseArgs(GEOSEARCH_WITH, 'key', 'member', {
+        radius: 1,
+        unit: 'm'
+      }, [GEO_REPLY_WITH.DISTANCE]),
+      expectedReply
+    );
+  });
 
-    testUtils.testWithClient('client.geoSearchWith', async client => {
-        assert.deepEqual(
-            await client.geoSearchWith('key', 'member', {
-                radius: 1,
-                unit: 'm'
-            }, [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('.geoSearchWith', async client => {
+    const [ , reply ] = await Promise.all([
+      client.geoAdd('key', {
+        member: 'member',
+        longitude: 1,
+        latitude: 2
+      }),
+      client.geoSearchWith('key', 'member', {
+        radius: 1,
+        unit: 'm'
+      }, [
+        GEO_REPLY_WITH.HASH,
+        GEO_REPLY_WITH.DISTANCE,
+        GEO_REPLY_WITH.COORDINATES
+      ])
+    ]);
 
-    testUtils.testWithCluster('cluster.geoSearchWith', async cluster => {
-        assert.deepEqual(
-            await cluster.geoSearchWith('key', 'member', {
-                radius: 1,
-                unit: 'm'
-            }, [GeoReplyWith.DISTANCE]),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+    assert.equal(reply.length, 1);
+    assert.equal(reply[0].member, 'member');
+    assert.equal(typeof reply[0].distance, 'string');
+    assert.equal(typeof reply[0].hash, 'number');
+    assert.equal(typeof reply[0].coordinates!.longitude, 'string');
+    assert.equal(typeof reply[0].coordinates!.latitude, 'string');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.ts b/packages/client/lib/commands/GEOSEARCH_WITH.ts
index d7a5f456a94..dca125a816e 100644
--- a/packages/client/lib/commands/GEOSEARCH_WITH.ts
+++ b/packages/client/lib/commands/GEOSEARCH_WITH.ts
@@ -1,23 +1,82 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { GeoSearchFrom, GeoSearchBy, GeoReplyWith, GeoSearchOptions } from './generic-transformers';
-import { transformArguments as geoSearchTransformArguments } from './GEOSEARCH';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Command } from '../RESP/types';
+import GEOSEARCH, { GeoSearchBy, GeoSearchFrom, GeoSearchOptions } from './GEOSEARCH';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEOSEARCH';
+export const GEO_REPLY_WITH = {
+  DISTANCE: 'WITHDIST',
+  HASH: 'WITHHASH',
+  COORDINATES: 'WITHCOORD'
+} as const;
 
-export function transformArguments(
-    key: RedisCommandArgument,
+export type GeoReplyWith = typeof GEO_REPLY_WITH[keyof typeof GEO_REPLY_WITH];
+
+export interface GeoReplyWithMember {
+  member: BlobStringReply;
+  distance?: BlobStringReply;
+  hash?: NumberReply;
+  coordinates?: {
+    longitude: DoubleReply;
+    latitude: DoubleReply;
+  };
+}
+
+export default {
+  IS_READ_ONLY: GEOSEARCH.IS_READ_ONLY,
+  /**
+   * Queries members inside an area of a geospatial index with additional information
+   * @param parser - The Redis command parser
+   * @param key - Key of the geospatial index
+   * @param from - Center point of the search (member name or coordinates)
+   * @param by - Search area specification (radius or box dimensions)
+   * @param replyWith - Information to include with each returned member
+   * @param options - Additional search options
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     from: GeoSearchFrom,
     by: GeoSearchBy,
     replyWith: Array<GeoReplyWith>,
     options?: GeoSearchOptions
-): RedisCommandArguments {
-    const args: RedisCommandArguments = geoSearchTransformArguments(key, from, by, options);
+  ) {
+    GEOSEARCH.parseCommand(parser, key, from, by, options);
+    parser.push(...replyWith);
+    parser.preserve = replyWith;
+  },
+  transformReply(
+    reply: UnwrapReply<ArrayReply<TuplesReply<[BlobStringReply, ...Array<any>]>>>,
+    replyWith: Array<GeoReplyWith>
+  ) {
+    const replyWithSet = new Set(replyWith);
+    let index = 0;
+    const distanceIndex = replyWithSet.has(GEO_REPLY_WITH.DISTANCE) && ++index,
+      hashIndex = replyWithSet.has(GEO_REPLY_WITH.HASH) && ++index,
+      coordinatesIndex = replyWithSet.has(GEO_REPLY_WITH.COORDINATES) && ++index;
+    
+    return reply.map(raw => {
+      const unwrapped = raw as unknown as UnwrapReply<typeof raw>;
 
-    args.push(...replyWith);
+      const item: GeoReplyWithMember = {
+        member: unwrapped[0]
+      };
 
-    args.preserve = replyWith;
-
-    return args;
-}
+      if (distanceIndex) {
+        item.distance = unwrapped[distanceIndex];
+      }
+  
+      if (hashIndex) {
+        item.hash = unwrapped[hashIndex];
+      }
+  
+      if (coordinatesIndex) {
+        const [longitude, latitude] = unwrapped[coordinatesIndex];
+        item.coordinates = {
+          longitude,
+          latitude
+        };
+      }
 
-export { transformGeoMembersWithReply as transformReply } from './generic-transformers';
+      return item;
+    });
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GET.spec.ts b/packages/client/lib/commands/GET.spec.ts
index 2946ea19b60..3e630d03e0b 100644
--- a/packages/client/lib/commands/GET.spec.ts
+++ b/packages/client/lib/commands/GET.spec.ts
@@ -1,33 +1,23 @@
-import { strict as assert } from 'assert';
-import RedisClient from '../client';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GET';
+import { parseArgs } from './generic-transformers';
+import GET from './GET';
 
 describe('GET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['GET', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GET, 'key'),
+      ['GET', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.get', async client => {
-        const a = await client.get(
-            'key'
-        );
-
-
-
-        assert.equal(
-            await client.get('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.get', async cluster => {
-        assert.equal(
-            await cluster.get('key'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('get', async client => {
+    assert.equal(
+      await client.get('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GET.ts b/packages/client/lib/commands/GET.ts
index 127b0a56349..e55c900eea2 100644
--- a/packages/client/lib/commands/GET.ts
+++ b/packages/client/lib/commands/GET.ts
@@ -1,11 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['GET', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets the value of a key
+   * @param parser - The Redis command parser
+   * @param key - Key to get the value of
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('GET');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GETBIT.spec.ts b/packages/client/lib/commands/GETBIT.spec.ts
index 4206084eced..66d2798313c 100644
--- a/packages/client/lib/commands/GETBIT.spec.ts
+++ b/packages/client/lib/commands/GETBIT.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GETBIT';
+import GETBIT from './GETBIT';
+import { parseArgs } from './generic-transformers';
 
 describe('GETBIT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0),
-            ['GETBIT', 'key', '0']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(GETBIT, 'key', 0),
+      ['GETBIT', 'key', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.getBit', async client => {
-        assert.equal(
-            await client.getBit('key', 0),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.getBit', async cluster => {
-        assert.equal(
-            await cluster.getBit('key', 0),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('getBit', async client => {
+    assert.equal(
+      await client.getBit('key', 0),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GETBIT.ts b/packages/client/lib/commands/GETBIT.ts
index 67f67f39b19..7d4a240473f 100644
--- a/packages/client/lib/commands/GETBIT.ts
+++ b/packages/client/lib/commands/GETBIT.ts
@@ -1,15 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
 import { BitValue } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    offset: number
-): RedisCommandArguments {
-    return ['GETBIT', key, offset.toString()];
-}
-
-export declare function transformReply(): BitValue;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the bit value at a given offset in a string value
+   * @param parser - The Redis command parser
+   * @param key - Key to retrieve the bit from
+   * @param offset - Bit offset
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, offset: number) {
+    parser.push('GETBIT');
+    parser.pushKey(key);
+    parser.push(offset.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply<BitValue>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GETDEL.spec.ts b/packages/client/lib/commands/GETDEL.spec.ts
index db3a486696a..15ad5918008 100644
--- a/packages/client/lib/commands/GETDEL.spec.ts
+++ b/packages/client/lib/commands/GETDEL.spec.ts
@@ -1,28 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GETDEL';
+import GETDEL from './GETDEL';
+import { parseArgs } from './generic-transformers';
 
 describe('GETDEL', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['GETDEL', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GETDEL, 'key'),
+      ['GETDEL', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.getDel', async client => {
-        assert.equal(
-            await client.getDel('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.getDel', async cluster => {
-        assert.equal(
-            await cluster.getDel('key'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('getDel', async client => {
+    assert.equal(
+      await client.getDel('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GETDEL.ts b/packages/client/lib/commands/GETDEL.ts
index 2d91e6cc025..7dbdc7d2535 100644
--- a/packages/client/lib/commands/GETDEL.ts
+++ b/packages/client/lib/commands/GETDEL.ts
@@ -1,9 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['GETDEL', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets the value of a key and deletes the key
+   * @param parser - The Redis command parser
+   * @param key - Key to get and delete
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('GETDEL');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GETEX.spec.ts b/packages/client/lib/commands/GETEX.spec.ts
index 1bf86089da1..5965d8f196f 100644
--- a/packages/client/lib/commands/GETEX.spec.ts
+++ b/packages/client/lib/commands/GETEX.spec.ts
@@ -1,96 +1,123 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GETEX';
+import GETEX from './GETEX';
+import { parseArgs } from './generic-transformers';
 
 describe('GETEX', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('EX', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    EX: 1
-                }),
-                ['GETEX', 'key', 'EX', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('EX | PX', () => {
+      assert.deepEqual(
+        parseArgs(GETEX, 'key', {
+          type: 'EX',
+          value: 1
+        }),
+        ['GETEX', 'key', 'EX', '1']
+      );
+    });
 
-        it('PX', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    PX: 1
-                }),
-                ['GETEX', 'key', 'PX', '1']
-            );
-        });
+    it('EX (backwards compatibility)', () => {
+      assert.deepEqual(
+        parseArgs(GETEX, 'key', {
+          EX: 1
+        }),
+        ['GETEX', 'key', 'EX', '1']
+      );
+    });
 
-        describe('EXAT', () => {
-            it('number', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        EXAT: 1
-                    }),
-                    ['GETEX', 'key', 'EXAT', '1']
-                );
-            });
+    it('PX (backwards compatibility)', () => {
+      assert.deepEqual(
+        parseArgs(GETEX, 'key', {
+          PX: 1
+        }),
+        ['GETEX', 'key', 'PX', '1']
+      );
+    });
 
-            it('date', () => {
-                const d = new Date();
-                assert.deepEqual(
-                    transformArguments('key', {
-                        EXAT: d
-                    }),
-                    ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()]
-                );
-            });
-        });
+    describe('EXAT | PXAT', () => {
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(GETEX, 'key', {
+            type: 'EXAT',
+            value: 1
+          }),
+          ['GETEX', 'key', 'EXAT', '1']
+        );
+      });
 
-        describe('PXAT', () => {
-            it('number', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        PXAT: 1
-                    }),
-                    ['GETEX', 'key', 'PXAT', '1']
-                );
-            });
+      it('date', () => {
+        const d = new Date();
+        assert.deepEqual(
+          parseArgs(GETEX, 'key', {
+            EXAT: d
+          }),
+          ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()]
+        );
+      });
+    });
 
-            it('date', () => {
-                const d = new Date();
-                assert.deepEqual(
-                    transformArguments('key', {
-                        PXAT: d
-                    }),
-                    ['GETEX', 'key', 'PXAT', d.getTime().toString()]
-                );
-            });
-        });
+    describe('EXAT (backwards compatibility)', () => {
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(GETEX, 'key', {
+            EXAT: 1
+          }),
+          ['GETEX', 'key', 'EXAT', '1']
+        );
+      });
 
-        it('PERSIST', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    PERSIST: true
-                }),
-                ['GETEX', 'key', 'PERSIST']
-            );
-        });
+      it('date', () => {
+        const d = new Date();
+        assert.deepEqual(
+          parseArgs(GETEX, 'key', {
+            EXAT: d
+          }),
+          ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()]
+        );
+      });
     });
 
-    testUtils.testWithClient('client.getEx', async client => {
-        assert.equal(
-            await client.getEx('key', {
-                PERSIST: true
-            }),
-            null
+    describe('PXAT (backwards compatibility)', () => {
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(GETEX, 'key', {
+            PXAT: 1
+          }),
+          ['GETEX', 'key', 'PXAT', '1']
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
 
-    testUtils.testWithCluster('cluster.getEx', async cluster => {
-        assert.equal(
-            await cluster.getEx('key', {
-                PERSIST: true
-            }),
-            null
+      it('date', () => {
+        const d = new Date();
+        assert.deepEqual(
+          parseArgs(GETEX, 'key', {
+            PXAT: d
+          }),
+          ['GETEX', 'key', 'PXAT', d.getTime().toString()]
         );
-    }, GLOBAL.CLUSTERS.OPEN);
+      });
+    });
+
+    it('PERSIST (backwards compatibility)', () => {
+      assert.deepEqual(
+        parseArgs(GETEX, 'key', {
+          PERSIST: true
+        }),
+        ['GETEX', 'key', 'PERSIST']
+      );
+    });
+  });
+
+  testUtils.testAll('getEx', async client => {
+    assert.equal(
+      await client.getEx('key', {
+        type: 'PERSIST'
+      }),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GETEX.ts b/packages/client/lib/commands/GETEX.ts
index 5b3cec6d887..836c2b5effe 100644
--- a/packages/client/lib/commands/GETEX.ts
+++ b/packages/client/lib/commands/GETEX.ts
@@ -1,39 +1,83 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 import { transformEXAT, transformPXAT } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-type GetExModes = {
-    EX: number;
+export type GetExOptions = {
+  type: 'EX' | 'PX';
+  value: number;
+} | {
+  type: 'EXAT' | 'PXAT';
+  value: number | Date;
+} | {
+  type: 'PERSIST';
+} | {
+  /**
+   * @deprecated Use `{ type: 'EX', value: number }` instead.
+   */
+  EX: number;
 } | {
-    PX: number;
+  /**
+   * @deprecated Use `{ type: 'PX', value: number }` instead.
+   */
+  PX: number;
 } | {
-    EXAT: number | Date;
+  /**
+   * @deprecated Use `{ type: 'EXAT', value: number | Date }` instead.
+   */
+  EXAT: number | Date;
 } | {
-    PXAT: number | Date;
+  /**
+   * @deprecated Use `{ type: 'PXAT', value: number | Date }` instead.
+   */
+  PXAT: number | Date;
 } | {
-    PERSIST: true;
+  /**
+   * @deprecated Use `{ type: 'PERSIST' }` instead.
+   */
+  PERSIST: true;
 };
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    mode: GetExModes
-): RedisCommandArguments {
-    const args = ['GETEX', key];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets the value of a key and optionally sets its expiration
+   * @param parser - The Redis command parser
+   * @param key - Key to get value from
+   * @param options - Options for setting expiration
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options: GetExOptions) {
+    parser.push('GETEX');
+    parser.pushKey(key);
 
-    if ('EX' in mode) {
-        args.push('EX', mode.EX.toString());
-    } else if ('PX' in mode) {
-        args.push('PX', mode.PX.toString());
-    } else if ('EXAT' in mode) {
-        args.push('EXAT', transformEXAT(mode.EXAT));
-    } else if ('PXAT' in mode) {
-        args.push('PXAT', transformPXAT(mode.PXAT));
-    } else { // PERSIST
-        args.push('PERSIST');
-    }
-
-    return args;
-}
+    if ('type' in options) {
+      switch (options.type) {
+        case 'EX':
+        case 'PX':
+          parser.push(options.type, options.value.toString());
+          break;
+        
+        case 'EXAT':
+        case 'PXAT':
+          parser.push(options.type, transformEXAT(options.value));
+          break;
 
-export declare function transformReply(): RedisCommandArgument | null;
+        case 'PERSIST':
+          parser.push('PERSIST');
+          break;
+      }
+    } else {
+      if ('EX' in options) {
+        parser.push('EX', options.EX.toString());
+      } else if ('PX' in options) {
+        parser.push('PX', options.PX.toString());
+      } else if ('EXAT' in options) {
+        parser.push('EXAT', transformEXAT(options.EXAT));
+      } else if ('PXAT' in options) {
+        parser.push('PXAT', transformPXAT(options.PXAT));
+      } else { // PERSIST
+        parser.push('PERSIST');
+      }
+    }
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GETRANGE.spec.ts b/packages/client/lib/commands/GETRANGE.spec.ts
index 0c9dbc2c70f..8a8e7dde038 100644
--- a/packages/client/lib/commands/GETRANGE.spec.ts
+++ b/packages/client/lib/commands/GETRANGE.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GETRANGE';
+import GETRANGE from './GETRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('GETRANGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, -1),
-            ['GETRANGE', 'key', '0', '-1']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(GETRANGE, 'key', 0, -1),
+      ['GETRANGE', 'key', '0', '-1']
+    );
+  });
 
-    testUtils.testWithClient('client.getRange', async client => {
-        assert.equal(
-            await client.getRange('key', 0, -1),
-            ''
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lTrim', async cluster => {
-        assert.equal(
-            await cluster.getRange('key', 0, -1),
-            ''
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('getRange', async client => {
+    assert.equal(
+      await client.getRange('key', 0, -1),
+      ''
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GETRANGE.ts b/packages/client/lib/commands/GETRANGE.ts
index 2d12d937cc6..f5f1586f0a9 100644
--- a/packages/client/lib/commands/GETRANGE.ts
+++ b/packages/client/lib/commands/GETRANGE.ts
@@ -1,15 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    start: number,
-    end: number
-): RedisCommandArguments {
-    return ['GETRANGE', key, start.toString(), end.toString()];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns a substring of the string stored at a key
+   * @param parser - The Redis command parser
+   * @param key - Key to get substring from
+   * @param start - Start position of the substring
+   * @param end - End position of the substring
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, start: number, end: number) {
+    parser.push('GETRANGE');
+    parser.pushKey(key);
+    parser.push(start.toString(), end.toString());
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/GETSET.spec.ts b/packages/client/lib/commands/GETSET.spec.ts
index 73fbcec57ea..5b162c16cc4 100644
--- a/packages/client/lib/commands/GETSET.spec.ts
+++ b/packages/client/lib/commands/GETSET.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GETSET';
+import GETSET from './GETSET';
+import { parseArgs } from './generic-transformers';
 
 describe('GETSET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'value'),
-            ['GETSET', 'key', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(GETSET, 'key', 'value'),
+      ['GETSET', 'key', 'value']
+    );
+  });
 
-    testUtils.testWithClient('client.getSet', async client => {
-        assert.equal(
-            await client.getSet('key', 'value'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.getSet', async cluster => {
-        assert.equal(
-            await cluster.getSet('key', 'value'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('getSet', async client => {
+    assert.equal(
+      await client.getSet('key', 'value'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/GETSET.ts b/packages/client/lib/commands/GETSET.ts
index 87d111792c6..1b9d98f90d7 100644
--- a/packages/client/lib/commands/GETSET.ts
+++ b/packages/client/lib/commands/GETSET.ts
@@ -1,12 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return ['GETSET', key, value];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Sets a key to a new value and returns its old value
+   * @param parser - The Redis command parser
+   * @param key - Key to set
+   * @param value - Value to set
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) {
+    parser.push('GETSET');
+    parser.pushKey(key);
+    parser.push(value);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HDEL.spec.ts b/packages/client/lib/commands/HDEL.spec.ts
index eb24bcfacbd..767d916e147 100644
--- a/packages/client/lib/commands/HDEL.spec.ts
+++ b/packages/client/lib/commands/HDEL.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HDEL';
+import HDEL from './HDEL';
+import { parseArgs } from './generic-transformers';
 
 describe('HDEL', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'field'),
-                ['HDEL', 'key', 'field']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(HDEL, 'key', 'field'),
+        ['HDEL', 'key', 'field']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['HDEL', 'key', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(HDEL, 'key', ['1', '2']),
+        ['HDEL', 'key', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.hDel', async client => {
-        assert.equal(
-            await client.hDel('key', 'field'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hDel', async client => {
+    assert.equal(
+      await client.hDel('key', 'field'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HDEL.ts b/packages/client/lib/commands/HDEL.ts
index 1a994e109d6..cc5c4dab1b9 100644
--- a/packages/client/lib/commands/HDEL.ts
+++ b/packages/client/lib/commands/HDEL.ts
@@ -1,13 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['HDEL', key], field);
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Removes one or more fields from a hash
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param field - Field(s) to remove
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, field: RedisVariadicArgument) {
+    parser.push('HDEL');
+    parser.pushKey(key);
+    parser.pushVariadic(field);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HELLO.spec.ts b/packages/client/lib/commands/HELLO.spec.ts
index 12d6d98c7c9..5d11be344c1 100644
--- a/packages/client/lib/commands/HELLO.spec.ts
+++ b/packages/client/lib/commands/HELLO.spec.ts
@@ -1,76 +1,72 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HELLO';
+import HELLO from './HELLO';
+import { parseArgs } from './generic-transformers';
 
 describe('HELLO', () => {
-    testUtils.isVersionGreaterThanHook([6]);
+  testUtils.isVersionGreaterThanHook([6]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['HELLO']
-            );
-        });
-
-        it('with protover', () => {
-            assert.deepEqual(
-                transformArguments({
-                    protover: 3
-                }),
-                ['HELLO', '3']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(HELLO),
+        ['HELLO']
+      );
+    });
 
-        it('with protover, auth', () => {
-            assert.deepEqual(
-                transformArguments({
-                    protover: 3,
-                    auth: {
-                        username: 'username',
-                        password: 'password'
-                    }
-                }),
-                ['HELLO', '3', 'AUTH', 'username', 'password']
-            );
-        });
+    it('with protover', () => {
+      assert.deepEqual(
+        parseArgs(HELLO, 3),
+        ['HELLO', '3']
+      );
+    });
 
-        it('with protover, clientName', () => {
-            assert.deepEqual(
-                transformArguments({
-                    protover: 3,
-                    clientName: 'clientName'
-                }),
-                ['HELLO', '3', 'SETNAME', 'clientName']
-            );
-        });
+    it('with protover, AUTH', () => {
+      assert.deepEqual(
+        parseArgs(HELLO, 3, {
+          AUTH: {
+            username: 'username',
+            password: 'password'
+          }
+        }),
+        ['HELLO', '3', 'AUTH', 'username', 'password']
+      );
+    });
 
-        it('with protover, auth, clientName', () => {
-            assert.deepEqual(
-                transformArguments({
-                    protover: 3,
-                    auth: {
-                        username: 'username',
-                        password: 'password'
-                    },
-                    clientName: 'clientName'
-                }),
-                ['HELLO', '3', 'AUTH', 'username', 'password', 'SETNAME', 'clientName']
-            );
-        });
+    it('with protover, SETNAME', () => {
+      assert.deepEqual(
+        parseArgs(HELLO, 3, {
+          SETNAME: 'name'
+        }),
+        ['HELLO', '3', 'SETNAME', 'name']
+      );
     });
 
-    testUtils.testWithClient('client.hello', async client => {
-        const reply = await client.hello();
-        assert.equal(reply.server, 'redis');
-        assert.equal(typeof reply.version, 'string');
-        assert.equal(reply.proto, 2);
-        assert.equal(typeof reply.id, 'number');
-        assert.equal(reply.mode, 'standalone');
-        assert.equal(reply.role, 'master');
-        assert.deepEqual(reply.modules, []);
-    }, {
-        ...GLOBAL.SERVERS.OPEN,
-        minimumDockerVersion: [6, 2]
+    it('with protover, AUTH, SETNAME', () => {
+      assert.deepEqual(
+        parseArgs(HELLO, 3, {
+          AUTH: {
+            username: 'username',
+            password: 'password'
+          },
+          SETNAME: 'name'
+        }),
+        ['HELLO', '3', 'AUTH', 'username', 'password', 'SETNAME', 'name']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.hello', async client => {
+    const reply = await client.hello();
+    assert.equal(reply.server, 'redis');
+    assert.equal(typeof reply.version, 'string');
+    assert.equal(reply.proto, 2);
+    assert.equal(typeof reply.id, 'number');
+    assert.equal(reply.mode, 'standalone');
+    assert.equal(reply.role, 'master');
+    assert.ok(reply.modules instanceof Array);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    minimumDockerVersion: [6, 2]
+  });
 });
diff --git a/packages/client/lib/commands/HELLO.ts b/packages/client/lib/commands/HELLO.ts
index d943f2e4c35..23feaad554a 100644
--- a/packages/client/lib/commands/HELLO.ts
+++ b/packages/client/lib/commands/HELLO.ts
@@ -1,65 +1,64 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { AuthOptions } from './AUTH';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, RespVersions, TuplesToMapReply, BlobStringReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-interface HelloOptions {
-    protover: number;
-    auth?: Required<AuthOptions>;
-    clientName?: string;
+export interface HelloOptions {
+  protover?: RespVersions;
+  AUTH?: {
+    username: RedisArgument;
+    password: RedisArgument;
+  };
+  SETNAME?: string;
 }
 
-export function transformArguments(options?: HelloOptions): RedisCommandArguments {
-    const args: RedisCommandArguments = ['HELLO'];
+export type HelloReply = TuplesToMapReply<[
+  [BlobStringReply<'server'>, BlobStringReply],
+  [BlobStringReply<'version'>, BlobStringReply],
+  [BlobStringReply<'proto'>, NumberReply<RespVersions>],
+  [BlobStringReply<'id'>, NumberReply],
+  [BlobStringReply<'mode'>, BlobStringReply],
+  [BlobStringReply<'role'>, BlobStringReply],
+  [BlobStringReply<'modules'>, ArrayReply<BlobStringReply>]
+]>;
 
-    if (options) {
-        args.push(options.protover.toString());
+export default {
+  /**
+   * Handshakes with the Redis server and switches to the specified protocol version
+   * @param parser - The Redis command parser
+   * @param protover - Protocol version to use
+   * @param options - Additional options for authentication and connection naming
+   */
+  parseCommand(parser: CommandParser, protover?: RespVersions, options?: HelloOptions) {
+    parser.push('HELLO');
 
-        if (options.auth) {
-            args.push('AUTH', options.auth.username, options.auth.password);
-        }
+    if (protover) {
+      parser.push(protover.toString());
 
-        if (options.clientName) {
-            args.push('SETNAME', options.clientName);
-        }
+      if (options?.AUTH) {
+        parser.push(
+          'AUTH',
+          options.AUTH.username,
+          options.AUTH.password
+        );
+      }
+  
+      if (options?.SETNAME) {
+        parser.push(
+          'SETNAME',
+          options.SETNAME
+        );
+      }
     }
-
-    return args;
-}
-
-type HelloRawReply = [
-    _: never,
-    server: RedisCommandArgument,
-    _: never,
-    version: RedisCommandArgument,
-    _: never,
-    proto: number,
-    _: never,
-    id: number,
-    _: never,
-    mode: RedisCommandArgument,
-    _: never,
-    role: RedisCommandArgument,
-    _: never,
-    modules: Array<RedisCommandArgument>
-];
-
-interface HelloTransformedReply {
-    server: RedisCommandArgument;
-    version: RedisCommandArgument;
-    proto: number;
-    id: number;
-    mode: RedisCommandArgument;
-    role: RedisCommandArgument;
-    modules: Array<RedisCommandArgument>;
-}
-
-export function transformReply(reply: HelloRawReply): HelloTransformedReply {
-    return {
-        server: reply[1],
-        version: reply[3],
-        proto: reply[5],
-        id: reply[7],
-        mode: reply[9],
-        role: reply[11],
-        modules: reply[13]
-    };
-}
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<HelloReply>>) => ({
+      server: reply[1],
+      version: reply[3],
+      proto: reply[5],
+      id: reply[7],
+      mode: reply[9],
+      role: reply[11],
+      modules: reply[13]
+    }),
+    3: undefined as unknown as () => HelloReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXISTS.spec.ts b/packages/client/lib/commands/HEXISTS.spec.ts
index 3764319c123..acd462ab7e2 100644
--- a/packages/client/lib/commands/HEXISTS.spec.ts
+++ b/packages/client/lib/commands/HEXISTS.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HEXISTS';
+import HEXISTS from './HEXISTS';
+import { parseArgs } from './generic-transformers';
 
 describe('HEXISTS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'field'),
-            ['HEXISTS', 'key', 'field']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(HEXISTS, 'key', 'field'),
+      ['HEXISTS', 'key', 'field']
+    );
+  });
 
-    testUtils.testWithClient('client.hExists', async client => {
-        assert.equal(
-            await client.hExists('key', 'field'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hExists', async client => {
+    assert.equal(
+      await client.hExists('key', 'field'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HEXISTS.ts b/packages/client/lib/commands/HEXISTS.ts
index 289be20aa83..50b8f1ae8c2 100644
--- a/packages/client/lib/commands/HEXISTS.ts
+++ b/packages/client/lib/commands/HEXISTS.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument
-): RedisCommandArguments {
-    return ['HEXISTS', key, field];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Determines whether a field exists in a hash
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param field - Field to check
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) {
+    parser.push('HEXISTS');
+    parser.pushKey(key);
+    parser.push(field);
+  },
+  transformReply: undefined as unknown as () => NumberReply<0 | 1>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts
index 3714f617f58..d28cc065ec9 100644
--- a/packages/client/lib/commands/HEXPIRE.spec.ts
+++ b/packages/client/lib/commands/HEXPIRE.spec.ts
@@ -1,6 +1,7 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HEXPIRE';
+import HEXPIRE from './HEXPIRE';
+import { parseArgs } from './generic-transformers';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
 
 describe('HEXPIRE', () => {
@@ -9,21 +10,21 @@ describe('HEXPIRE', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field', 1),
+        parseArgs(HEXPIRE, 'key', 'field', 1),
         ['HEXPIRE', 'key', '1', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2'], 1),
+        parseArgs(HEXPIRE, 'key', ['field1', 'field2'], 1),
         ['HEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
       );
     });
 
     it('with set option', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1'], 1, 'NX'),
+        parseArgs(HEXPIRE, 'key', ['field1'], 1, 'NX'),
         ['HEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1']
       );
     });
diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts
index 938f9039939..95ee58eac1d 100644
--- a/packages/client/lib/commands/HEXPIRE.ts
+++ b/packages/client/lib/commands/HEXPIRE.ts
@@ -1,44 +1,47 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-/**
- * @readonly
- * @enum {number}
- */
 export const HASH_EXPIRATION = {
-  /** @property {number} */
   /** The field does not exist */
   FIELD_NOT_EXISTS: -2,
-  /** @property {number} */
   /** Specified NX | XX | GT | LT condition not met */
   CONDITION_NOT_MET: 0,
-  /** @property {number} */
   /** Expiration time was set or updated */
   UPDATED: 1,
-  /** @property {number} */
   /** Field deleted because the specified expiration time is in the past */
   DELETED: 2
 } as const;
-  
-export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION];
-
-export const FIRST_KEY_INDEX = 1;
 
-export function transformArguments(
-  key: RedisCommandArgument, 
-  fields: RedisCommandArgument| Array<RedisCommandArgument>,
-  seconds: number,
-  mode?: 'NX' | 'XX' | 'GT' | 'LT',
-) {
-  const args = ['HEXPIRE', key, seconds.toString()];
-
-  if (mode) {
-    args.push(mode);
-  }
+export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION];
 
-  args.push('FIELDS');
+export default {
+  /**
+   * Sets a timeout on hash fields. After the timeout has expired, the fields will be automatically deleted
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param fields - Fields to set expiration on
+   * @param seconds - Number of seconds until field expiration
+   * @param mode - Expiration mode: NX (only if field has no expiry), XX (only if field has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current)
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+    seconds: number,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT'
+  ) {
+    parser.push('HEXPIRE');
+    parser.pushKey(key);
+    parser.push(seconds.toString());
+    
+    if (mode) {
+      parser.push(mode);
+    }
 
-  return pushVerdictArgument(args, fields);
-}
+    parser.push('FIELDS');
 
-export declare function transformReply(): Array<HashExpiration>;
\ No newline at end of file
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<HashExpiration>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXPIREAT.spec.ts b/packages/client/lib/commands/HEXPIREAT.spec.ts
index 1c65fb61773..c7cc9fe749b 100644
--- a/packages/client/lib/commands/HEXPIREAT.spec.ts
+++ b/packages/client/lib/commands/HEXPIREAT.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HEXPIREAT';
+import HEXPIREAT from './HEXPIREAT';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HEXPIREAT', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,14 +10,14 @@ describe('HEXPIREAT', () => {
   describe('transformArguments', () => {
     it('string + number', () => {
       assert.deepEqual(
-        transformArguments('key', 'field', 1),
+        parseArgs(HEXPIREAT, 'key', 'field', 1),
         ['HEXPIREAT', 'key', '1', 'FIELDS', '1', 'field']
       );
     });
 
     it('array + number', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2'], 1),
+        parseArgs(HEXPIREAT, 'key', ['field1', 'field2'], 1),
         ['HEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
       );
     });
@@ -25,14 +26,14 @@ describe('HEXPIREAT', () => {
       const d = new Date();
 
       assert.deepEqual(
-        transformArguments('key', ['field1'], d),
+        parseArgs(HEXPIREAT, 'key', ['field1'], d),
         ['HEXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString(), 'FIELDS', '1', 'field1']
       );
     });
 
     it('with set option', () => {
       assert.deepEqual(
-        transformArguments('key', 'field1', 1, 'GT'),
+        parseArgs(HEXPIREAT, 'key', 'field1', 1, 'GT'),
         ['HEXPIREAT', 'key', '1', 'GT', 'FIELDS', '1', 'field1']
       );
     });
diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts
index 58c52d3a1f6..c09efd4aa34 100644
--- a/packages/client/lib/commands/HEXPIREAT.ts
+++ b/packages/client/lib/commands/HEXPIREAT.ts
@@ -1,28 +1,34 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument, transformEXAT } from './generic-transformers';
-import { HashExpiration } from './HEXPIRE';
+import { CommandParser } from '../client/parser';
+import { RedisVariadicArgument, transformEXAT } from './generic-transformers';
+import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  /**
+   * Sets the expiration for hash fields at a specific Unix timestamp
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param fields - Fields to set expiration on
+   * @param timestamp - Unix timestamp (seconds since January 1, 1970) or Date object
+   * @param mode - Expiration mode: NX (only if field has no expiry), XX (only if field has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current)
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+    timestamp: number | Date,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT'
+  ) {
+    parser.push('HEXPIREAT');
+    parser.pushKey(key);
+    parser.push(transformEXAT(timestamp));
 
-export function transformArguments(
-  key: RedisCommandArgument,
-  fields: RedisCommandArgument | Array<RedisCommandArgument>,
-  timestamp: number | Date,
-  mode?: 'NX' | 'XX' | 'GT' | 'LT'
-) {
-  const args = [
-    'HEXPIREAT',
-    key,
-    transformEXAT(timestamp)
-  ];
+    if (mode) {
+      parser.push(mode);
+    }
 
-  if (mode) {
-    args.push(mode);
-  }
+    parser.push('FIELDS')
 
-  args.push('FIELDS')
-
-  return pushVerdictArgument(args, fields);
-}
-
-export declare function transformReply(): Array<HashExpiration>;
\ No newline at end of file
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts
index 9c3eb024bed..32a8730e8a9 100644
--- a/packages/client/lib/commands/HEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts
@@ -1,6 +1,7 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { HASH_EXPIRATION_TIME, transformArguments } from './HEXPIRETIME';
+import HEXPIRETIME, { HASH_EXPIRATION_TIME }  from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HEXPIRETIME', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -8,14 +9,14 @@ describe('HEXPIRETIME', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field'),
+        parseArgs(HEXPIRETIME, 'key', 'field'),
         ['HEXPIRETIME', 'key', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2']),
+        parseArgs(HEXPIRETIME, 'key', ['field1', 'field2']),
         ['HEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2']
       );
     });
diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts
index 01764b1032d..94504935090 100644
--- a/packages/client/lib/commands/HEXPIRETIME.ts
+++ b/packages/client/lib/commands/HEXPIRETIME.ts
@@ -1,21 +1,31 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
 export const HASH_EXPIRATION_TIME = {
-  /** @property {number} */
   /** The field does not exist */
   FIELD_NOT_EXISTS: -2,
-  /** @property {number} */
   /** The field exists but has no associated expire */
   NO_EXPIRATION: -1,
 } as const;
 
-export const FIRST_KEY_INDEX = 1
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
-  return pushVerdictArgument(['HEXPIRETIME', key, 'FIELDS'], fields);
-}
-
-export declare function transformReply(): Array<number>;
\ No newline at end of file
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the absolute Unix timestamp (since January 1, 1970) at which the given hash fields will expire
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param fields - Fields to check expiration time
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument
+  ) {
+    parser.push('HEXPIRETIME');
+    parser.pushKey(key);
+    parser.push('FIELDS');
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HGET.spec.ts b/packages/client/lib/commands/HGET.spec.ts
index 6b6d0a3ee22..47061876aea 100644
--- a/packages/client/lib/commands/HGET.spec.ts
+++ b/packages/client/lib/commands/HGET.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HGET';
+import HGET from './HGET';
+import { parseArgs } from './generic-transformers';
 
 describe('HGET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'field'),
-            ['HGET', 'key', 'field']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HGET, 'key', 'field'),
+      ['HGET', 'key', 'field']
+    );
+  });
 
-    testUtils.testWithClient('client.hGet', async client => {
-        assert.equal(
-            await client.hGet('key', 'field'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hGet', async client => {
+    assert.equal(
+      await client.hGet('key', 'field'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HGET.ts b/packages/client/lib/commands/HGET.ts
index fcfd31e6172..8c4d690992b 100644
--- a/packages/client/lib/commands/HGET.ts
+++ b/packages/client/lib/commands/HGET.ts
@@ -1,14 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument
-): RedisCommandArguments {
-    return ['HGET', key, field];
-}
-
-export declare function transformReply(): RedisCommandArgument | undefined;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets the value of a field in a hash
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param field - Field to get the value of
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) {
+    parser.push('HGET');
+    parser.pushKey(key);
+    parser.push(field);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HGETALL.spec.ts b/packages/client/lib/commands/HGETALL.spec.ts
index fcd1a30457c..93d122bae07 100644
--- a/packages/client/lib/commands/HGETALL.spec.ts
+++ b/packages/client/lib/commands/HGETALL.spec.ts
@@ -1,41 +1,34 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformReply } from './HGETALL';
 
 describe('HGETALL', () => {
-    describe('transformReply', () => {
-        it('empty', () => {
-            assert.deepEqual(
-                transformReply([]),
-                Object.create(null)
-            );
-        });
 
-        it('with values', () => {
-            assert.deepEqual(
-                transformReply(['key1', 'value1', 'key2', 'value2']),
-                Object.create(null, {
-                    key1: {
-                        value: 'value1',
-                        configurable: true,
-                        enumerable: true,
-                        writable: true
-                    },
-                    key2: {
-                        value: 'value2',
-                        configurable: true,
-                        enumerable: true,
-                        writable: true
-                    }
-                })
-            );
-        });
-    });
+  testUtils.testAll('hGetAll empty', async client => {
+    assert.deepEqual(
+      await client.hGetAll('key'),
+      Object.create(null)
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 
-    testUtils.testWithClient('client.hGetAll', async client => {
-        assert.deepEqual(
-            await client.hGetAll('key'),
-            Object.create(null)
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hGetAll with value', async client => {
+    const [, reply] = await Promise.all([
+      client.hSet('key', 'field', 'value'),
+      client.hGetAll('key')
+    ]);
+    assert.deepEqual(
+      reply,
+      Object.create(null, {
+        field: {
+          value: 'value',
+          enumerable: true
+        }
+      })
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HGETALL.ts b/packages/client/lib/commands/HGETALL.ts
index bf51760ff0e..13238ab6ea3 100644
--- a/packages/client/lib/commands/HGETALL.ts
+++ b/packages/client/lib/commands/HGETALL.ts
@@ -1,13 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, MapReply, BlobStringReply, Command } from '../RESP/types';
+import { transformTuplesReply } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export const TRANSFORM_LEGACY_REPLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['HGETALL', key];
-}
-
-export { transformTuplesReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets all fields and values in a hash
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('HGETALL');
+    parser.pushKey(key);
+  },
+  TRANSFORM_LEGACY_REPLY: true,
+  transformReply: {
+    2: transformTuplesReply<BlobStringReply>,
+    3: undefined as unknown as () => MapReply<BlobStringReply, BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HGETDEL.spec.ts b/packages/client/lib/commands/HGETDEL.spec.ts
new file mode 100644
index 00000000000..b2e19967f1d
--- /dev/null
+++ b/packages/client/lib/commands/HGETDEL.spec.ts
@@ -0,0 +1,48 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import { BasicCommandParser } from '../client/parser';
+import HGETDEL from './HGETDEL';
+
+describe('HGETDEL parseCommand', () => {
+    it('hGetDel parseCommand base', () => {
+        const parser = new BasicCommandParser;
+        HGETDEL.parseCommand(parser, 'key', 'field');
+        assert.deepEqual(parser.redisArgs, ['HGETDEL', 'key', 'FIELDS', '1', 'field']);
+    });
+
+    it('hGetDel parseCommand variadic', () => {
+        const parser = new BasicCommandParser;
+        HGETDEL.parseCommand(parser, 'key', ['field1', 'field2']);
+        assert.deepEqual(parser.redisArgs, ['HGETDEL', 'key', 'FIELDS', '2', 'field1', 'field2']);
+    });
+});
+
+
+describe('HGETDEL call', () => {
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty single field', async client => {
+    assert.deepEqual(
+        await client.hGetDel('key', 'filed1'),
+        [null]
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty multiple fields', async client => {
+    assert.deepEqual(
+        await client.hGetDel('key', ['filed1', 'field2']),
+        [null, null]
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel partially populated multiple fields', async client => {
+    await client.hSet('key', 'field1', 'value1')
+    assert.deepEqual(
+        await client.hGetDel('key', ['field1', 'field2']),
+        ['value1', null]
+    );
+    
+    assert.deepEqual(
+        await client.hGetDel('key', 'field1'),
+        [null]
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/client/lib/commands/HGETDEL.ts b/packages/client/lib/commands/HGETDEL.ts
new file mode 100644
index 00000000000..8b55cae3ed5
--- /dev/null
+++ b/packages/client/lib/commands/HGETDEL.ts
@@ -0,0 +1,19 @@
+import { CommandParser } from '../client/parser';
+import { RedisVariadicArgument } from './generic-transformers';
+import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types';
+
+export default {
+  /**
+   * Gets and deletes the specified fields from a hash
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param fields - Fields to get and delete
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) {
+    parser.push('HGETDEL');
+    parser.pushKey(key);
+    parser.push('FIELDS')
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HGETEX.spec.ts b/packages/client/lib/commands/HGETEX.spec.ts
new file mode 100644
index 00000000000..2625a0ac023
--- /dev/null
+++ b/packages/client/lib/commands/HGETEX.spec.ts
@@ -0,0 +1,78 @@
+import { strict as assert } from 'node:assert';
+import testUtils,{ GLOBAL } from '../test-utils';
+import { BasicCommandParser } from '../client/parser';
+import HGETEX from './HGETEX';
+import { setTimeout } from 'timers/promises';
+
+describe('HGETEX parseCommand', () => {
+  it('hGetEx parseCommand base', () => {
+    const parser = new BasicCommandParser;
+    HGETEX.parseCommand(parser, 'key', 'field');
+    assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'FIELDS', '1', 'field']);
+  });
+
+  it('hGetEx parseCommand expiration PERSIST string', () => {
+    const parser = new BasicCommandParser;
+    HGETEX.parseCommand(parser, 'key', 'field', {expiration: 'PERSIST'});
+    assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'PERSIST', 'FIELDS', '1', 'field']);
+  });
+
+  it('hGetEx parseCommand expiration PERSIST obj', () => {
+    const parser = new BasicCommandParser;
+    HGETEX.parseCommand(parser, 'key', 'field', {expiration: {type: 'PERSIST'}});
+    assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'PERSIST', 'FIELDS', '1', 'field']);
+  });
+
+  it('hGetEx parseCommand expiration EX obj', () => {
+    const parser = new BasicCommandParser;
+    HGETEX.parseCommand(parser, 'key', 'field', {expiration: {type: 'EX', value: 1000}});
+    assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'EX', '1000', 'FIELDS', '1', 'field']);
+  });
+
+  it('hGetEx parseCommand expiration EXAT obj variadic', () => {
+    const parser = new BasicCommandParser;
+    HGETEX.parseCommand(parser, 'key', ['field1', 'field2'], {expiration: {type: 'EXAT', value: 1000}});
+    assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'EXAT', '1000', 'FIELDS', '2', 'field1', 'field2']);
+  });
+});
+
+
+describe('HGETEX call', () => {
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty single field', async client => {
+      assert.deepEqual(
+        await client.hGetEx('key', 'field1', {expiration: 'PERSIST'}),
+        [null]
+      );
+    }, GLOBAL.SERVERS.OPEN);
+  
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty multiple fields', async client => {
+      assert.deepEqual(
+        await client.hGetEx('key', ['field1', 'field2'], {expiration: 'PERSIST'}),
+        [null, null]
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx set expiry', async client => {
+        await client.hSet('key', 'field', 'value')
+        assert.deepEqual(
+            await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 50}}),
+            ['value']
+        );
+        await setTimeout(100)
+        assert.deepEqual(
+            await client.hGet('key', 'field'),
+            null
+        );  
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'gGetEx set expiry PERSIST', async client => {
+        await client.hSet('key', 'field', 'value')
+        await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 50}})
+        await client.hGetEx('key', 'field', {expiration: 'PERSIST'})
+        await setTimeout(100)
+        assert.deepEqual(
+            await client.hGet('key', 'field'),
+            'value'
+        )
+    }, GLOBAL.SERVERS.OPEN);
+});
\ No newline at end of file
diff --git a/packages/client/lib/commands/HGETEX.ts b/packages/client/lib/commands/HGETEX.ts
new file mode 100644
index 00000000000..6b039575a27
--- /dev/null
+++ b/packages/client/lib/commands/HGETEX.ts
@@ -0,0 +1,49 @@
+import { CommandParser } from '../client/parser';
+import { RedisVariadicArgument } from './generic-transformers';
+import { ArrayReply, Command, BlobStringReply, NullReply, RedisArgument } from '../RESP/types';
+
+export interface HGetExOptions {
+  expiration?: {
+    type: 'EX' | 'PX' | 'EXAT' | 'PXAT';
+    value: number;
+  } | {
+    type: 'PERSIST';
+  } | 'PERSIST';
+}
+
+export default {
+  /**
+   * Gets the values of the specified fields in a hash and optionally sets their expiration
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param fields - Fields to get values from
+   * @param options - Options for setting expiration
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+    options?: HGetExOptions
+  ) {
+    parser.push('HGETEX');
+    parser.pushKey(key);
+
+    if (options?.expiration) {
+        if (typeof options.expiration === 'string') {
+          parser.push(options.expiration);
+        } else if (options.expiration.type === 'PERSIST') {
+          parser.push('PERSIST');
+        } else {
+          parser.push(
+            options.expiration.type,
+            options.expiration.value.toString()
+          );
+        }
+    }
+
+    parser.push('FIELDS')
+
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HINCRBY.spec.ts b/packages/client/lib/commands/HINCRBY.spec.ts
index de406217921..ad382d97a99 100644
--- a/packages/client/lib/commands/HINCRBY.spec.ts
+++ b/packages/client/lib/commands/HINCRBY.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HINCRBY';
+import HINCRBY from './HINCRBY';
+import { parseArgs } from './generic-transformers';
 
 describe('HINCRBY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'field', 1),
-            ['HINCRBY', 'key', 'field', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HINCRBY, 'key', 'field', 1),
+      ['HINCRBY', 'key', 'field', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.hIncrBy', async client => {
-        assert.equal(
-            await client.hIncrBy('key', 'field', 1),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hIncrBy', async client => {
+    assert.equal(
+      await client.hIncrBy('key', 'field', 1),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HINCRBY.ts b/packages/client/lib/commands/HINCRBY.ts
index b2cf6eefe89..cb028315f4c 100644
--- a/packages/client/lib/commands/HINCRBY.ts
+++ b/packages/client/lib/commands/HINCRBY.ts
@@ -1,13 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument,
+export default {
+  /**
+   * Increments the integer value of a field in a hash by the given number
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param field - Field to increment
+   * @param increment - Increment amount
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    field: RedisArgument,
     increment: number
-): RedisCommandArguments {
-    return ['HINCRBY', key, field, increment.toString()];
-}
-
-export declare function transformReply(): number;
+  ) {
+    parser.push('HINCRBY');
+    parser.pushKey(key);
+    parser.push(field, increment.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts
index bd0147a3481..2edbd6f9477 100644
--- a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts
+++ b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HINCRBYFLOAT';
+import HINCRBYFLOAT from './HINCRBYFLOAT';
+import { parseArgs } from './generic-transformers';
 
 describe('HINCRBYFLOAT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'field', 1.5),
-            ['HINCRBYFLOAT', 'key', 'field', '1.5']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HINCRBYFLOAT, 'key', 'field', 1.5),
+      ['HINCRBYFLOAT', 'key', 'field', '1.5']
+    );
+  });
 
-    testUtils.testWithClient('client.hIncrByFloat', async client => {
-        assert.equal(
-            await client.hIncrByFloat('key', 'field', 1.5),
-            '1.5'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hIncrByFloat', async client => {
+    assert.equal(
+      await client.hIncrByFloat('key', 'field', 1.5),
+      '1.5'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HINCRBYFLOAT.ts b/packages/client/lib/commands/HINCRBYFLOAT.ts
index 0e2de6e9b29..6d85fa50432 100644
--- a/packages/client/lib/commands/HINCRBYFLOAT.ts
+++ b/packages/client/lib/commands/HINCRBYFLOAT.ts
@@ -1,13 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument,
+export default {
+  /**
+   * Increments the float value of a field in a hash by the given amount
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   * @param field - Field to increment
+   * @param increment - Increment amount (float)
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    field: RedisArgument,
     increment: number
-): RedisCommandArguments {
-    return ['HINCRBYFLOAT', key, field, increment.toString()];
-}
-
-export declare function transformReply(): number;
+  ) {
+    parser.push('HINCRBYFLOAT');
+    parser.pushKey(key);
+    parser.push(field, increment.toString());
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HKEYS.spec.ts b/packages/client/lib/commands/HKEYS.spec.ts
index f94538f67d3..58445696d20 100644
--- a/packages/client/lib/commands/HKEYS.spec.ts
+++ b/packages/client/lib/commands/HKEYS.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HKEYS';
+import HKEYS from './HKEYS';
+import { parseArgs } from './generic-transformers';
 
 describe('HKEYS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['HKEYS', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HKEYS, 'key'),
+      ['HKEYS', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.hKeys', async client => {
-        assert.deepEqual(
-            await client.hKeys('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hKeys', async client => {
+    assert.deepEqual(
+      await client.hKeys('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HKEYS.ts b/packages/client/lib/commands/HKEYS.ts
index 3d629733d0e..bf2783eb2dc 100644
--- a/packages/client/lib/commands/HKEYS.ts
+++ b/packages/client/lib/commands/HKEYS.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['HKEYS', key];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets all field names in a hash
+   * @param parser - The Redis command parser
+   * @param key - Key of the hash
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('HKEYS')
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HLEN.spec.ts b/packages/client/lib/commands/HLEN.spec.ts
index be9d4b13a7d..640e461ad07 100644
--- a/packages/client/lib/commands/HLEN.spec.ts
+++ b/packages/client/lib/commands/HLEN.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HLEN';
+import HLEN from './HLEN';
+import { parseArgs } from './generic-transformers';
 
 describe('HLEN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['HLEN', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HLEN, 'key'),
+      ['HLEN', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.hLen', async client => {
-        assert.equal(
-            await client.hLen('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hLen', async client => {
+    assert.equal(
+      await client.hLen('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HLEN.ts b/packages/client/lib/commands/HLEN.ts
index 15a93d408d7..7ffbdeee9d6 100644
--- a/packages/client/lib/commands/HLEN.ts
+++ b/packages/client/lib/commands/HLEN.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['HLEN', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets the number of fields in a hash.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the hash.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('HLEN');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HMGET.spec.ts b/packages/client/lib/commands/HMGET.spec.ts
index a7c934b760d..8cc90e4abd5 100644
--- a/packages/client/lib/commands/HMGET.spec.ts
+++ b/packages/client/lib/commands/HMGET.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HMGET';
+import HMGET from './HMGET';
+import { parseArgs } from './generic-transformers';
 
 describe('HMGET', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'field'),
-                ['HMGET', 'key', 'field']
-            );
-        });
+  describe('parseCommand', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(HMGET, 'key', 'field'),
+        ['HMGET', 'key', 'field']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['field1', 'field2']),
-                ['HMGET', 'key', 'field1', 'field2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(HMGET, 'key', ['field1', 'field2']),
+        ['HMGET', 'key', 'field1', 'field2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.hmGet', async client => {
-        assert.deepEqual(
-            await client.hmGet('key', 'field'),
-            [null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hmGet', async client => {
+    assert.deepEqual(
+      await client.hmGet('key', 'field'),
+      [null]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HMGET.ts b/packages/client/lib/commands/HMGET.ts
index 64b4014abeb..18a7baa219e 100644
--- a/packages/client/lib/commands/HMGET.ts
+++ b/packages/client/lib/commands/HMGET.ts
@@ -1,15 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    fields: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['HMGET', key], fields);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets the values of all the specified fields in a hash.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the hash.
+   * @param fields - Fields to get from the hash.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) {
+    parser.push('HMGET');
+    parser.pushKey(key);
+    parser.pushVariadic(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts
index 8cf3f1fe221..0b317977cbf 100644
--- a/packages/client/lib/commands/HPERSIST.spec.ts
+++ b/packages/client/lib/commands/HPERSIST.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HPERSIST';
+import HPERSIST from './HPERSIST';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HPERSIST', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,14 +10,14 @@ describe('HPERSIST', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field'),
+        parseArgs(HPERSIST, 'key', 'field'),
         ['HPERSIST', 'key', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2']),
+        parseArgs(HPERSIST, 'key', ['field1', 'field2']),
         ['HPERSIST', 'key', 'FIELDS', '2', 'field1', 'field2']
       );
     });
diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts
index 862a7548ac1..00ab1f4b4b5 100644
--- a/packages/client/lib/commands/HPERSIST.ts
+++ b/packages/client/lib/commands/HPERSIST.ts
@@ -1,10 +1,23 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
-  return pushVerdictArgument(['HPERSIST', key, 'FIELDS'], fields);
-}
-
-export declare function transformReply(): Array<number> | null;
\ No newline at end of file
+export default {
+  /**
+   * Removes the expiration from the specified fields in a hash.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the hash.
+   * @param fields - Fields to remove expiration from.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument
+  ) {
+    parser.push('HPERSIST');
+    parser.pushKey(key);
+    parser.push('FIELDS');
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts
index 852d9f5bd21..2f68fb9b7f3 100644
--- a/packages/client/lib/commands/HPEXPIRE.spec.ts
+++ b/packages/client/lib/commands/HPEXPIRE.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HPEXPIRE';
+import HPEXPIRE from './HPEXPIRE';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HEXPIRE', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,21 +10,21 @@ describe('HEXPIRE', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field', 1),
+        parseArgs(HPEXPIRE, 'key', 'field', 1),
         ['HPEXPIRE', 'key', '1', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2'], 1),
+        parseArgs(HPEXPIRE, 'key', ['field1', 'field2'], 1),
         ['HPEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
       );
     });
 
     it('with set option', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1'], 1, 'NX'),
+        parseArgs(HPEXPIRE, 'key', ['field1'], 1, 'NX'),
         ['HPEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1']
       );
     });
diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts
index afbb056ed4e..2e20c96bb13 100644
--- a/packages/client/lib/commands/HPEXPIRE.ts
+++ b/packages/client/lib/commands/HPEXPIRE.ts
@@ -1,24 +1,36 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
-import { HashExpiration } from "./HEXPIRE";
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
+import { HashExpiration } from './HEXPIRE';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  /**
+   * Parses the arguments for the `HPEXPIRE` command.
+   *
+   * @param parser - The command parser instance.
+   * @param key - The key of the hash.
+   * @param fields - The fields to set the expiration for.
+   * @param ms - The expiration time in milliseconds.
+   * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT').
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument, 
+    fields: RedisVariadicArgument,
+    ms: number,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT',
+  ) {
+    parser.push('HPEXPIRE');
+    parser.pushKey(key);
+    parser.push(ms.toString());
 
-export function transformArguments(
-  key: RedisCommandArgument, 
-  fields: RedisCommandArgument | Array<RedisCommandArgument>,
-  ms: number,
-  mode?: 'NX' | 'XX' | 'GT' | 'LT',
-) {
-  const args = ['HPEXPIRE', key, ms.toString()];
+    if (mode) {
+      parser.push(mode);
+    }
 
-  if (mode) {
-    args.push(mode);
-  }
+    parser.push('FIELDS')
 
-  args.push('FIELDS')
-
-  return pushVerdictArgument(args, fields);
-}
-
-export declare function transformReply(): Array<HashExpiration> | null;
\ No newline at end of file
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<HashExpiration> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIREAT.spec.ts b/packages/client/lib/commands/HPEXPIREAT.spec.ts
index 9747cca1a2d..7c369980bf4 100644
--- a/packages/client/lib/commands/HPEXPIREAT.spec.ts
+++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HPEXPIREAT';
+import HPEXPIREAT from './HPEXPIREAT';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HPEXPIREAT', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,14 +10,14 @@ describe('HPEXPIREAT', () => {
   describe('transformArguments', () => {
     it('string + number', () => {
       assert.deepEqual(
-        transformArguments('key', 'field', 1),
+        parseArgs(HPEXPIREAT, 'key', 'field', 1),
         ['HPEXPIREAT', 'key', '1', 'FIELDS', '1', 'field']
       );
     });
 
     it('array + number', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2'], 1),
+        parseArgs(HPEXPIREAT, 'key', ['field1', 'field2'], 1),
         ['HPEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
       );
     });
@@ -24,14 +25,14 @@ describe('HPEXPIREAT', () => {
     it('date', () => {
       const d = new Date();
       assert.deepEqual(
-        transformArguments('key', ['field1'], d),
+        parseArgs(HPEXPIREAT, 'key', ['field1'], d),
         ['HPEXPIREAT', 'key', d.getTime().toString(), 'FIELDS', '1', 'field1']
       );
     });
 
     it('with set option', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1'], 1, 'XX'),
+        parseArgs(HPEXPIREAT, 'key', ['field1'], 1, 'XX'),
         ['HPEXPIREAT', 'key', '1', 'XX', 'FIELDS', '1', 'field1']
       );
     });
diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts
index b6e01d8ee5c..58fedc84765 100644
--- a/packages/client/lib/commands/HPEXPIREAT.ts
+++ b/packages/client/lib/commands/HPEXPIREAT.ts
@@ -1,25 +1,37 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument, transformEXAT, transformPXAT } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument, transformPXAT } from './generic-transformers';
 import { HashExpiration } from './HEXPIRE';
 
-export const FIRST_KEY_INDEX = 1;
-export const IS_READ_ONLY = true;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Parses the arguments for the `HPEXPIREAT` command.
+   *
+   * @param parser - The command parser instance.
+   * @param key - The key of the hash.
+   * @param fields - The fields to set the expiration for.
+   * @param timestamp - The expiration timestamp (Unix timestamp or Date object).
+   * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT').
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+    timestamp: number | Date,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT'
+  ) {
+    parser.push('HPEXPIREAT');
+    parser.pushKey(key);
+    parser.push(transformPXAT(timestamp));
 
-export function transformArguments(
-  key: RedisCommandArgument,
-  fields: RedisCommandArgument | Array<RedisCommandArgument>,
-  timestamp: number | Date,
-  mode?: 'NX' | 'XX' | 'GT' | 'LT'
-) {
-  const args = ['HPEXPIREAT', key, transformPXAT(timestamp)];
+    if (mode) {
+      parser.push(mode);
+    }
 
-  if (mode) {
-    args.push(mode);
-  }
+    parser.push('FIELDS')
 
-  args.push('FIELDS')
-
-  return pushVerdictArgument(args, fields);
-}
-
-export declare function transformReply(): Array<HashExpiration> | null;
\ No newline at end of file
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<HashExpiration> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
index ff03b73c71d..5673a725afc 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HPEXPIRETIME';
+import HPEXPIRETIME from './HPEXPIRETIME';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HPEXPIRETIME', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,14 +10,14 @@ describe('HPEXPIRETIME', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field'),
+        parseArgs(HPEXPIRETIME, 'key', 'field'),
         ['HPEXPIRETIME', 'key', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2']),
+        parseArgs(HPEXPIRETIME, 'key', ['field1', 'field2']),
         ['HPEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2']
       );
     });
diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts
index 22a794ccefa..d27a15749ae 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.ts
@@ -1,11 +1,26 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
-  return pushVerdictArgument(['HPEXPIRETIME', key, 'FIELDS'], fields);
-}
-
-export declare function transformReply(): Array<number> | null;
\ No newline at end of file
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HPEXPIRETIME command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to retrieve expiration time for
+   * @param fields - The fields to retrieve expiration time for
+   * @see https://redis.io/commands/hpexpiretime/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+  ) {
+    parser.push('HPEXPIRETIME');
+    parser.pushKey(key);
+    parser.push('FIELDS');
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts
index ddca26ea85b..baaa11b19c8 100644
--- a/packages/client/lib/commands/HPTTL.spec.ts
+++ b/packages/client/lib/commands/HPTTL.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HPTTL';
+import HPTTL from './HPTTL';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HPTTL', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,14 +10,14 @@ describe('HPTTL', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field'),
+        parseArgs(HPTTL, 'key', 'field'),
         ['HPTTL', 'key', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2']),
+        parseArgs(HPTTL, 'key', ['field1', 'field2']),
         ['HPTTL', 'key', 'FIELDS', '2', 'field1', 'field2']
       );
     });
diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts
index 988b805c0c9..f177e6b5a02 100644
--- a/packages/client/lib/commands/HPTTL.ts
+++ b/packages/client/lib/commands/HPTTL.ts
@@ -1,11 +1,26 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-export const IS_READ_ONLY =  true;
-
-export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
-  return pushVerdictArgument(['HPTTL', key, 'FIELDS'], fields);
-}
-
-export declare function transformReply(): Array<number> | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HPTTL command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to check time-to-live for
+   * @param fields - The fields to check time-to-live for
+   * @see https://redis.io/commands/hpttl/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument
+  ) {
+    parser.push('HPTTL');
+    parser.pushKey(key);
+    parser.push('FIELDS');
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HRANDFIELD.spec.ts b/packages/client/lib/commands/HRANDFIELD.spec.ts
index df0a4fc7a1d..151636057a0 100644
--- a/packages/client/lib/commands/HRANDFIELD.spec.ts
+++ b/packages/client/lib/commands/HRANDFIELD.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HRANDFIELD';
+import HRANDFIELD from './HRANDFIELD';
+import { parseArgs } from './generic-transformers';
 
 describe('HRANDFIELD', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['HRANDFIELD', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HRANDFIELD, 'key'),
+      ['HRANDFIELD', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.hRandField', async client => {
-        assert.equal(
-            await client.hRandField('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hRandField', async client => {
+    assert.equal(
+      await client.hRandField('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HRANDFIELD.ts b/packages/client/lib/commands/HRANDFIELD.ts
index a2c70aabd52..38cc34dcee5 100644
--- a/packages/client/lib/commands/HRANDFIELD.ts
+++ b/packages/client/lib/commands/HRANDFIELD.ts
@@ -1,11 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['HRANDFIELD', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HRANDFIELD command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash to get a random field from
+   * @see https://redis.io/commands/hrandfield/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('HRANDFIELD');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts
index 4bfce0f7e23..ee3fc984d55 100644
--- a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts
+++ b/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HRANDFIELD_COUNT';
+import HRANDFIELD_COUNT from './HRANDFIELD_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('HRANDFIELD COUNT', () => {
-    testUtils.isVersionGreaterThanHook([6, 2, 5]);
+  testUtils.isVersionGreaterThanHook([6, 2, 5]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['HRANDFIELD', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HRANDFIELD_COUNT, 'key', 1),
+      ['HRANDFIELD', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.hRandFieldCount', async client => {
-        assert.deepEqual(
-            await client.hRandFieldCount('key', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hRandFieldCount', async client => {
+    assert.deepEqual(
+      await client.hRandFieldCount('key', 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.ts
index 01b8df63273..99c5cb0dada 100644
--- a/packages/client/lib/commands/HRANDFIELD_COUNT.ts
+++ b/packages/client/lib/commands/HRANDFIELD_COUNT.ts
@@ -1,16 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformHRandFieldArguments } from './HRANDFIELD';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HRANDFIELD';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return [
-        ...transformHRandFieldArguments(key),
-        count.toString()
-    ];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HRANDFIELD command with count parameter
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash to get random fields from
+   * @param count - The number of fields to return (positive: unique fields, negative: may repeat fields)
+   * @see https://redis.io/commands/hrandfield/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    parser.push('HRANDFIELD');
+    parser.pushKey(key);
+    parser.push(count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts
index c4e6409a726..e69de29bb2d 100644
--- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts
+++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts
@@ -1,21 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HRANDFIELD_COUNT_WITHVALUES';
-
-describe('HRANDFIELD COUNT WITHVALUES', () => {
-    testUtils.isVersionGreaterThanHook([6, 2, 5]);
-
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['HRANDFIELD', 'key', '1', 'WITHVALUES']
-        );
-    });
-
-    testUtils.testWithClient('client.hRandFieldCountWithValues', async client => {
-        assert.deepEqual(
-            await client.hRandFieldCountWithValues('key', 1),
-            Object.create(null)
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts
index 3e09dbb9a14..e247006c6a7 100644
--- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts
+++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts
@@ -1,16 +1,49 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformHRandFieldCountArguments } from './HRANDFIELD_COUNT';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '../RESP/types';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HRANDFIELD_COUNT';
+export type HRandFieldCountWithValuesReply = Array<{
+  field: BlobStringReply;
+  value: BlobStringReply;
+}>;
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return [
-        ...transformHRandFieldCountArguments(key, count),
-        'WITHVALUES'
-    ];
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HRANDFIELD command with count parameter and WITHVALUES option
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash to get random fields from
+   * @param count - The number of fields to return (positive: unique fields, negative: may repeat fields)
+   * @see https://redis.io/commands/hrandfield/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    parser.push('HRANDFIELD');
+    parser.pushKey(key);
+    parser.push(count.toString(), 'WITHVALUES');
+  },
+  transformReply: {
+    2: (rawReply: UnwrapReply<ArrayReply<BlobStringReply>>) => {
+      const reply: HRandFieldCountWithValuesReply = [];
 
-export { transformTuplesReply as transformReply } from './generic-transformers';
+      let i = 0;
+      while (i < rawReply.length) {
+        reply.push({
+          field: rawReply[i++],
+          value: rawReply[i++]
+        });
+      }
+
+      return reply;
+    },
+    3: (reply: UnwrapReply<ArrayReply<TuplesReply<[BlobStringReply, BlobStringReply]>>>) => {
+      return reply.map(entry => {
+        const [field, value] = entry as unknown as UnwrapReply<typeof entry>;
+        return {
+          field,
+          value
+        };
+      }) satisfies HRandFieldCountWithValuesReply;
+    }
+  }
+} as const satisfies Command;
+ 
\ No newline at end of file
diff --git a/packages/client/lib/commands/HSCAN.spec.ts b/packages/client/lib/commands/HSCAN.spec.ts
index 6757888a875..9e489f6190d 100644
--- a/packages/client/lib/commands/HSCAN.spec.ts
+++ b/packages/client/lib/commands/HSCAN.spec.ts
@@ -1,90 +1,83 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './HSCAN';
+import { parseArgs } from './generic-transformers';
+import HSCAN from './HSCAN';
 
 describe('HSCAN', () => {
-    describe('transformArguments', () => {
-        it('cusror only', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['HSCAN', 'key', '0']
-            );
-        });
-
-        it('with MATCH', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern'
-                }),
-                ['HSCAN', 'key', '0', 'MATCH', 'pattern']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    COUNT: 1
-                }),
-                ['HSCAN', 'key', '0', 'COUNT', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('cusror only', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN, 'key', '0'),
+        ['HSCAN', 'key', '0']
+      );
+    });
 
-        it('with MATCH & COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern',
-                    COUNT: 1
-                }),
-                ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
-            );
-        });
+    it('with MATCH', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN, 'key', '0', {
+          MATCH: 'pattern'
+        }),
+        ['HSCAN', 'key', '0', 'MATCH', 'pattern']
+      );
     });
 
-    describe('transformReply', () => {
-        it('without tuples', () => {
-            assert.deepEqual(
-                transformReply(['0', []]),
-                {
-                    cursor: 0,
-                    tuples: []
-                }
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN, 'key', '0', {
+          COUNT: 1
+        }),
+        ['HSCAN', 'key', '0', 'COUNT', '1']
+      );
+    });
 
-        it('with tuples', () => {
-            assert.deepEqual(
-                transformReply(['0', ['field', 'value']]),
-                {
-                    cursor: 0,
-                    tuples: [{
-                        field: 'field',
-                        value: 'value'
-                    }]
-                }
-            );
-        });
+    it('with MATCH & COUNT', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN, 'key', '0', {
+          MATCH: 'pattern',
+          COUNT: 1
+        }),
+        ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.hScan', async client => {
-        assert.deepEqual(
-            await client.hScan('key', 0),
-            {
-                cursor: 0,
-                tuples: []
-            }
-        );
+  describe('transformReply', () => {
+    it('without tuples', () => {
+      assert.deepEqual(
+        HSCAN.transformReply(['0' as any, []]),
+        {
+          cursor: '0',
+          entries: []
+        }
+      );
+    });
+    
+    it('with tuples', () => {
+      assert.deepEqual(
+        HSCAN.transformReply(['0' as any, ['field', 'value'] as any]),
+        {
+          cursor: '0',
+          entries: [{
+            field: 'field',
+            value: 'value'
+          }]
+        }
+      );
+    });
+  });
 
-        await Promise.all([
-            client.hSet('key', 'a', '1'),
-            client.hSet('key', 'b', '2')
-        ]);
+  testUtils.testWithClient('client.hScan', async client => {
+    const [, reply] = await Promise.all([
+      client.hSet('key', 'field', 'value'),
+      client.hScan('key', '0')
+    ]);
 
-        assert.deepEqual(
-            await client.hScan('key', 0),
-            {
-                cursor: 0,
-                tuples: [{field: 'a', value: '1'}, {field: 'b', value: '2'}]
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {
+      cursor: '0',
+      entries: [{
+        field: 'field',
+        value: 'value'
+      }]
+    });
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/HSCAN.ts b/packages/client/lib/commands/HSCAN.ts
index 5167693b604..78141814ff1 100644
--- a/packages/client/lib/commands/HSCAN.ts
+++ b/packages/client/lib/commands/HSCAN.ts
@@ -1,44 +1,46 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { ScanOptions, pushScanArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
+import { ScanCommonOptions, parseScanArguments } from './SCAN';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    cursor: number,
-    options?: ScanOptions
-): RedisCommandArguments {
-    return pushScanArguments([
-        'HSCAN',
-        key
-    ], cursor, options);
-}
-
-export type HScanRawReply = [RedisCommandArgument, Array<RedisCommandArgument>];
-
-export interface HScanTuple {
-    field: RedisCommandArgument;
-    value: RedisCommandArgument;
-}
-
-interface HScanReply {
-    cursor: number;
-    tuples: Array<HScanTuple>;
+export interface HScanEntry {
+  field: BlobStringReply;
+  value: BlobStringReply;
 }
 
-export function transformReply([cursor, rawTuples]: HScanRawReply): HScanReply {
-    const parsedTuples = [];
-    for (let i = 0; i < rawTuples.length; i += 2) {
-        parsedTuples.push({
-            field: rawTuples[i],
-            value: rawTuples[i + 1]
-        });
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HSCAN command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash to scan
+   * @param cursor - The cursor position to start scanning from
+   * @param options - Options for the scan (COUNT, MATCH, TYPE)
+   * @see https://redis.io/commands/hscan/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    cursor: RedisArgument,
+    options?: ScanCommonOptions
+  ) {
+    parser.push('HSCAN');
+    parser.pushKey(key);
+    parseScanArguments(parser, cursor, options);
+  },
+  transformReply([cursor, rawEntries]: [BlobStringReply, Array<BlobStringReply>]) {
+    const entries = [];
+    let i = 0;
+    while (i < rawEntries.length) {
+      entries.push({
+        field: rawEntries[i++],
+        value: rawEntries[i++]
+      } satisfies HScanEntry);
     }
 
     return {
-        cursor: Number(cursor),
-        tuples: parsedTuples
+      cursor,
+      entries
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts
index 7e05b841e43..83a452a6897 100644
--- a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts
+++ b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts
@@ -1,79 +1,82 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './HSCAN_NOVALUES';
+import HSCAN_NOVALUES from './HSCAN_NOVALUES';
+import { parseArgs } from './generic-transformers';
 
 describe('HSCAN_NOVALUES', () => {
-    testUtils.isVersionGreaterThanHook([7, 4]);
-    
-    describe('transformArguments', () => {
-        it('cusror only', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['HSCAN', 'key', '0', 'NOVALUES']
-            );
-        });
+  testUtils.isVersionGreaterThanHook([7,4]);
+  
+  describe('transformArguments', () => {
+    it('cusror only', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN_NOVALUES, 'key', '0'),
+        ['HSCAN', 'key', '0', 'NOVALUES']
+      );
+    });
+
+    it('with MATCH', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN_NOVALUES, 'key', '0', {
+          MATCH: 'pattern'
+        }),
+        ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES']
+      );
+    });
 
-        it('with MATCH', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern'
-                }),
-                ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES']
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN_NOVALUES, 'key', '0', {
+          COUNT: 1
+        }),
+        ['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    COUNT: 1
-                }),
-                ['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES']
-            );
-        });
+    it('with MATCH & COUNT', () => {
+      assert.deepEqual(
+        parseArgs(HSCAN_NOVALUES, 'key', '0', {
+          MATCH: 'pattern',
+          COUNT: 1
+        }),
+        ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1', 'NOVALUES']
+      );
     });
+  });
 
-    describe('transformReply', () => {
-        it('without keys', () => {
-            assert.deepEqual(
-                transformReply(['0', []]),
-                {
-                    cursor: 0,
-                    keys: []
-                }
-            );
-        });
+  describe('transformReply', () => {
+    it('without keys', () => {
+      assert.deepEqual(
+        HSCAN_NOVALUES.transformReply(['0' as any, []]),
+        {
+          cursor: '0',
+          fields: []
+        }
+      );
+    });
 
-        it('with keys', () => {
-            assert.deepEqual(
-                transformReply(['0', ['key1', 'key2']]),
-                {
-                    cursor: 0,
-                    keys: ['key1', 'key2']
-                }
-            );
-        });
+    it('with keys', () => {
+      assert.deepEqual(
+        HSCAN_NOVALUES.transformReply(['0' as any, ['key1', 'key2'] as any]),
+        {
+           cursor: '0',
+           fields: ['key1', 'key2']
+        }
+      );
     });
+  });
 
-    testUtils.testWithClient('client.hScanNoValues', async client => {
-        assert.deepEqual(
-            await client.hScanNoValues('key', 0),
-            {
-                cursor: 0,
-                keys: []
-            }
-        );
 
-        await Promise.all([
-            client.hSet('key', 'a', '1'),
-            client.hSet('key', 'b', '2')
-        ]);
+  testUtils.testWithClient('client.hScanNoValues', async client => {
+    const [, reply] = await Promise.all([
+      client.hSet('key', 'field', 'value'),
+      client.hScanNoValues('key', '0')
+    ]);
 
-        assert.deepEqual(
-            await client.hScanNoValues('key', 0),
-            {
-                cursor: 0,
-                keys: ['a', 'b']
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {
+      cursor: '0',
+      fields: [
+        'field',
+      ]
+    });
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.ts b/packages/client/lib/commands/HSCAN_NOVALUES.ts
index 37a929754c6..8f7afe52b8e 100644
--- a/packages/client/lib/commands/HSCAN_NOVALUES.ts
+++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts
@@ -1,27 +1,24 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { ScanOptions } from './generic-transformers';
-import { HScanRawReply, transformArguments as transformHScanArguments } from './HSCAN';
+import { BlobStringReply, Command } from '../RESP/types';
+import HSCAN from './HSCAN';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HSCAN';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HSCAN command with NOVALUES option
+   * 
+   * @param args - The same parameters as HSCAN command
+   * @see https://redis.io/commands/hscan/
+   */
+  parseCommand(...args: Parameters<typeof HSCAN.parseCommand>) {
+    const parser = args[0];
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    cursor: number,
-    options?: ScanOptions
-): RedisCommandArguments {
-    const args = transformHScanArguments(key, cursor, options);
-    args.push('NOVALUES');
-    return args;
-}
-
-interface HScanNoValuesReply {
-    cursor: number;
-    keys: Array<RedisCommandArgument>;
-}
-
-export function transformReply([cursor, rawData]: HScanRawReply): HScanNoValuesReply {
+    HSCAN.parseCommand(...args);
+    parser.push('NOVALUES');
+  },
+  transformReply([cursor, fields]: [BlobStringReply, Array<BlobStringReply>]) {
     return {
-        cursor: Number(cursor),
-        keys: rawData
+      cursor,
+      fields
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HSET.spec.ts b/packages/client/lib/commands/HSET.spec.ts
index 73bc966f87a..2cb53e6485a 100644
--- a/packages/client/lib/commands/HSET.spec.ts
+++ b/packages/client/lib/commands/HSET.spec.ts
@@ -1,74 +1,71 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './HSET';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
+import HSET from './HSET';
+import { parseArgs } from './generic-transformers';
 
 describe('HSET', () => {
-    describe('transformArguments', () => {
-        describe('field, value', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'field', 'value'),
-                    ['HSET', 'key', 'field', 'value']
-                );
-            });
-
-            it('number', () => {
-                assert.deepEqual(
-                    transformArguments('key', 1, 2),
-                    ['HSET', 'key', '1', '2']
-                );
-            });
+  describe('transformArguments', () => {
+    describe('field, value', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(HSET, 'key', 'field', 'value'),
+          ['HSET', 'key', 'field', 'value']
+        );
+      });
 
-            it('Buffer', () => {
-                assert.deepEqual(
-                    transformArguments(Buffer.from('key'), Buffer.from('field'), Buffer.from('value')),
-                    ['HSET', Buffer.from('key'), Buffer.from('field'), Buffer.from('value')]
-                );
-            });
-        });
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(HSET, 'key', 1, 2),
+          ['HSET', 'key', '1', '2']
+        );
+      });
 
-        it('Map', () => {
-            assert.deepEqual(
-                transformArguments('key', new Map([['field', 'value']])),
-                ['HSET', 'key', 'field', 'value']
-            );
-        });
+      it('Buffer', () => {
+        assert.deepEqual(
+          parseArgs(HSET, Buffer.from('key'), Buffer.from('field'), Buffer.from('value')),
+          ['HSET', Buffer.from('key'), Buffer.from('field'), Buffer.from('value')]
+        );
+      });
+    });
 
-        it('Array', () => {
-            assert.deepEqual(
-                transformArguments('key', [['field', 'value']]),
-                ['HSET', 'key', 'field', 'value']
-            );
-        });
+    it('Map', () => {
+      assert.deepEqual(
+        parseArgs(HSET, 'key', new Map([['field', 'value']])),
+        ['HSET', 'key', 'field', 'value']
+      );
+    });
 
-        describe('Object', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('key', { field: 'value' }),
-                    ['HSET', 'key', 'field', 'value']
-                );
-            });
-            
-            it('Buffer', () => {
-                assert.deepEqual(
-                    transformArguments('key', { field: Buffer.from('value') }),
-                    ['HSET', 'key', 'field', Buffer.from('value')]
-                );
-            });
-        });
+    it('Array', () => {
+      assert.deepEqual(
+        parseArgs(HSET, 'key', [['field', 'value']]),
+        ['HSET', 'key', 'field', 'value']
+      );
     });
 
-    testUtils.testWithClient('client.hSet', async client => {
-        assert.equal(
-            await client.hSet('key', 'field', 'value'),
-            1
+    describe('Object', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(HSET, 'key', { field: 'value' }),
+          ['HSET', 'key', 'field', 'value']
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
 
-    testUtils.testWithCluster('cluster.hSet', async cluster => {
-        assert.equal(
-            await cluster.hSet('key', { field: 'value' }),
-            1
+      it('Buffer', () => {
+        assert.deepEqual(
+          parseArgs(HSET, 'key', { field: Buffer.from('value') }),
+          ['HSET', 'key', 'field', Buffer.from('value')]
         );
-    }, GLOBAL.CLUSTERS.OPEN);
+      });
+    });
+  });
+
+  testUtils.testAll('hSet', async client => {
+    assert.equal(
+      await client.hSet('key', 'field', 'value'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HSET.ts b/packages/client/lib/commands/HSET.ts
index 261ef98c779..7dc4da8d3cf 100644
--- a/packages/client/lib/commands/HSET.ts
+++ b/packages/client/lib/commands/HSET.ts
@@ -1,73 +1,83 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export type HashTypes = RedisArgument | number;
 
-type Types = RedisCommandArgument | number;
+type HSETObject = Record<string | number, HashTypes>;
 
-type HSETObject = Record<string | number, Types>;
+type HSETMap = Map<HashTypes, HashTypes>;
 
-type HSETMap = Map<Types, Types>;
+type HSETTuples = Array<[HashTypes, HashTypes]> | Array<HashTypes>;
 
-type HSETTuples = Array<[Types, Types]> | Array<Types>;
+type GenericArguments = [key: RedisArgument];
 
-type GenericArguments = [key: RedisCommandArgument];
-
-type SingleFieldArguments = [...generic: GenericArguments, field: Types, value: Types];
+type SingleFieldArguments = [...generic: GenericArguments, field: HashTypes, value: HashTypes];
 
 type MultipleFieldsArguments = [...generic: GenericArguments, value: HSETObject | HSETMap | HSETTuples];
 
-export function transformArguments(...[ key, value, fieldValue ]: SingleFieldArguments | MultipleFieldsArguments): RedisCommandArguments {
-    const args: RedisCommandArguments = ['HSET', key];
-
-    if (typeof value === 'string' || typeof value === 'number' || Buffer.isBuffer(value)) {
-        args.push(
-            convertValue(value),
-            convertValue(fieldValue!)
-        );
+export type HSETArguments = SingleFieldArguments | MultipleFieldsArguments;
+
+export default {
+  /**
+   * Constructs the HSET command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash
+   * @param value - Either the field name (when using single field) or an object/map/array of field-value pairs
+   * @param fieldValue - The value to set (only used with single field variant)
+   * @see https://redis.io/commands/hset/
+   */
+  parseCommand(parser: CommandParser, ...[key, value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments) {
+    parser.push('HSET');
+    parser.pushKey(key);
+
+    if (typeof value === 'string' || typeof value === 'number' || value instanceof Buffer) {
+      parser.push(
+        convertValue(value),
+        convertValue(fieldValue!)
+      );
     } else if (value instanceof Map) {
-        pushMap(args, value);
+      pushMap(parser, value);
     } else if (Array.isArray(value)) {
-        pushTuples(args, value);
+      pushTuples(parser, value);
     } else {
-        pushObject(args, value);
+      pushObject(parser, value);
     }
-
-    return args;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
+
+function pushMap(parser: CommandParser, map: HSETMap): void {
+  for (const [key, value] of map.entries()) {
+    parser.push(
+      convertValue(key),
+      convertValue(value)
+    );
+  }
 }
 
-function pushMap(args: RedisCommandArguments, map: HSETMap): void {
-    for (const [key, value] of map.entries()) {
-        args.push(
-            convertValue(key),
-            convertValue(value)
-        );
+function pushTuples(parser: CommandParser, tuples: HSETTuples): void {
+  for (const tuple of tuples) {
+    if (Array.isArray(tuple)) {
+      pushTuples(parser, tuple);
+      continue;
     }
-}
 
-function pushTuples(args: RedisCommandArguments, tuples: HSETTuples): void {
-    for (const tuple of tuples) {
-        if (Array.isArray(tuple)) {
-            pushTuples(args, tuple);
-            continue;
-        }
-
-        args.push(convertValue(tuple));
-    }
+    parser.push(convertValue(tuple));
+  }
 }
 
-function pushObject(args: RedisCommandArguments, object: HSETObject): void {
-    for (const key of Object.keys(object)) {
-        args.push(
-            convertValue(key),
-            convertValue(object[key])
-        );
-    }
+function pushObject(parser: CommandParser, object: HSETObject): void {
+  for (const key of Object.keys(object)) {
+    parser.push(
+      convertValue(key),
+      convertValue(object[key])
+    );
+  }
 }
 
-function convertValue(value: Types): RedisCommandArgument {
-    return typeof value === 'number' ?
-        value.toString() :
-        value;
+function convertValue(value: HashTypes): RedisArgument {
+  return typeof value === 'number' ?
+    value.toString() :
+    value;
 }
-
-export declare function transformReply(): number;
diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts
new file mode 100644
index 00000000000..fc38e0f0f45
--- /dev/null
+++ b/packages/client/lib/commands/HSETEX.spec.ts
@@ -0,0 +1,98 @@
+import { strict as assert } from 'node:assert';
+import testUtils,{ GLOBAL } from '../test-utils';
+import { BasicCommandParser } from '../client/parser';
+import HSETEX from './HSETEX';
+
+describe('HSETEX parseCommand', () => {
+  it('hSetEx parseCommand base', () => {
+    const parser = new BasicCommandParser;
+    HSETEX.parseCommand(parser, 'key', ['field', 'value']);
+    assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'field', 'value']);
+  });
+
+  it('hSetEx parseCommand base empty obj', () => {
+    const parser = new BasicCommandParser;
+    assert.throws(() => {HSETEX.parseCommand(parser, 'key', {})});
+  });
+
+  it('hSetEx parseCommand base one key obj', () => {
+    const parser = new BasicCommandParser;
+    HSETEX.parseCommand(parser, 'key', {'k': 'v'});
+    assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'k', 'v']);
+  });
+
+  it('hSetEx parseCommand array', () => {
+    const parser = new BasicCommandParser;
+    HSETEX.parseCommand(parser, 'key', ['field1', 'value1', 'field2', 'value2']);
+    assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']);
+  });
+
+  it('hSetEx parseCommand array invalid args, throws an error', () => {
+    const parser = new BasicCommandParser;
+    assert.throws(() => {HSETEX.parseCommand(parser, 'key', ['field1', 'value1', 'field2'])});
+  });
+
+  it('hSetEx parseCommand array in array', () => {
+    const parser1 = new BasicCommandParser;
+    HSETEX.parseCommand(parser1, 'key', [['field1', 'value1'], ['field2', 'value2']]);
+    assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']);
+
+    const parser2 = new BasicCommandParser;
+    HSETEX.parseCommand(parser2, 'key', [['field1', 'value1'], ['field2', 'value2'], ['field3', 'value3']]);
+    assert.deepEqual(parser2.redisArgs, ['HSETEX', 'key', 'FIELDS', '3', 'field1', 'value1', 'field2', 'value2', 'field3', 'value3']);
+  });
+  
+  it('hSetEx parseCommand map', () => {
+    const parser1 = new BasicCommandParser;
+    HSETEX.parseCommand(parser1, 'key', new Map([['field1', 'value1'], ['field2', 'value2']]));
+    assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']);
+  });
+
+  it('hSetEx parseCommand obj', () => {
+    const parser1 = new BasicCommandParser;
+    HSETEX.parseCommand(parser1, 'key', {field1: "value1", field2: "value2"});
+    assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']);
+  });
+
+  it('hSetEx parseCommand options FNX KEEPTTL', () => {
+    const parser = new BasicCommandParser;
+    HSETEX.parseCommand(parser, 'key', ['field', 'value'], {mode: 'FNX', expiration: 'KEEPTTL'});
+    assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FNX', 'KEEPTTL', 'FIELDS', '1', 'field', 'value']);
+  });
+
+  it('hSetEx parseCommand options FXX EX 500', () => {
+    const parser = new BasicCommandParser;
+    HSETEX.parseCommand(parser, 'key', ['field', 'value'], {mode: 'FXX', expiration: {type: 'EX', value: 500}});
+    assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FXX', 'EX', '500', 'FIELDS', '1', 'field', 'value']);
+  });
+});
+
+
+describe('HSETEX call', () => {
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx calls', async client => {
+      assert.deepEqual(
+        await client.hSetEx('key_hsetex_call',  ['field1', 'value1'], {expiration: {type: "EX", value: 500}, mode: "FNX"}),
+        1
+      );
+
+      assert.deepEqual(
+        await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}),
+        0
+      );
+
+      assert.deepEqual(
+        await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}),
+        0
+      );
+
+      assert.deepEqual(
+        await client.hSetEx('key_hsetex_call', ['field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}),
+        1
+      );
+
+      assert.deepEqual(
+        await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}),
+        1
+      );
+    }, GLOBAL.SERVERS.OPEN);
+});
\ No newline at end of file
diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts
new file mode 100644
index 00000000000..316b95a91c3
--- /dev/null
+++ b/packages/client/lib/commands/HSETEX.ts
@@ -0,0 +1,119 @@
+import { BasicCommandParser, CommandParser } from '../client/parser';
+import {  Command, NumberReply, RedisArgument } from '../RESP/types';
+
+export interface HSetExOptions {
+    expiration?: {
+      type: 'EX' | 'PX' | 'EXAT' | 'PXAT';
+      value: number;
+    } | {
+      type: 'KEEPTTL';
+    } | 'KEEPTTL';
+    mode?: 'FNX' | 'FXX'
+  }
+
+export type HashTypes = RedisArgument | number;
+
+type HSETEXObject = Record<string | number, HashTypes>;
+
+type HSETEXMap = Map<HashTypes, HashTypes>;
+
+type HSETEXTuples = Array<[HashTypes, HashTypes]> | Array<HashTypes>;
+
+export default {
+  /**
+   * Constructs the HSETEX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash
+   * @param fields - Object, Map, or Array of field-value pairs to set
+   * @param options - Optional configuration for expiration and mode settings
+   * @see https://redis.io/commands/hsetex/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: HSETEXObject | HSETEXMap | HSETEXTuples,
+    options?: HSetExOptions
+  ) {
+    parser.push('HSETEX');
+    parser.pushKey(key);
+
+    if (options?.mode) {
+        parser.push(options.mode)
+    }
+    if (options?.expiration) {
+        if (typeof options.expiration === 'string') {
+          parser.push(options.expiration);
+        } else if (options.expiration.type === 'KEEPTTL') {
+          parser.push('KEEPTTL');
+        } else {
+          parser.push(
+            options.expiration.type,
+            options.expiration.value.toString()
+          );
+        }
+    }
+
+    parser.push('FIELDS')
+    if (fields instanceof Map) {
+        pushMap(parser, fields);
+    } else if (Array.isArray(fields)) {
+        pushTuples(parser, fields);
+    } else {
+        pushObject(parser, fields);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply<0 | 1>
+} as const satisfies Command;
+
+
+function pushMap(parser: CommandParser, map: HSETEXMap): void {
+    parser.push(map.size.toString())
+    for (const [key, value] of map.entries()) {
+        parser.push(
+            convertValue(key),
+            convertValue(value)
+        );
+    }
+}
+
+function pushTuples(parser: CommandParser, tuples: HSETEXTuples): void {
+    const tmpParser = new BasicCommandParser
+    _pushTuples(tmpParser, tuples)
+
+    if (tmpParser.redisArgs.length%2 != 0) {
+        throw Error('invalid number of arguments, expected key value ....[key value] pairs, got key without value')
+    }
+
+    parser.push((tmpParser.redisArgs.length/2).toString())
+    parser.push(...tmpParser.redisArgs)
+}
+
+function _pushTuples(parser: CommandParser, tuples: HSETEXTuples): void {
+    for (const tuple of tuples) {
+        if (Array.isArray(tuple)) {
+            _pushTuples(parser, tuple);
+            continue;
+        }
+        parser.push(convertValue(tuple));
+  }
+}
+
+function pushObject(parser: CommandParser, object: HSETEXObject): void {
+    const len = Object.keys(object).length
+    if (len == 0) {
+        throw Error('object without keys')
+    }
+
+    parser.push(len.toString())
+    for (const key of Object.keys(object)) {
+        parser.push(
+            convertValue(key),
+            convertValue(object[key])
+        );
+    }
+}
+
+function convertValue(value: HashTypes): RedisArgument {
+    return typeof value === 'number' ? value.toString() : value;
+}
\ No newline at end of file
diff --git a/packages/client/lib/commands/HSETNX.spec.ts b/packages/client/lib/commands/HSETNX.spec.ts
index 190fa50ae97..e65f9fb219c 100644
--- a/packages/client/lib/commands/HSETNX.spec.ts
+++ b/packages/client/lib/commands/HSETNX.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HSETNX';
+import HSETNX from './HSETNX';
+import { parseArgs } from './generic-transformers';
 
 describe('HSETNX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'field', 'value'),
-            ['HSETNX', 'key', 'field', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HSETNX, 'key', 'field', 'value'),
+      ['HSETNX', 'key', 'field', 'value']
+    );
+  });
 
-    testUtils.testWithClient('client.hSetNX', async client => {
-        assert.equal(
-            await client.hSetNX('key', 'field', 'value'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hSetNX', async client => {
+    assert.equal(
+      await client.hSetNX('key', 'field', 'value'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HSETNX.ts b/packages/client/lib/commands/HSETNX.ts
index 9ac6ef0edd8..dc10b6c5e00 100644
--- a/packages/client/lib/commands/HSETNX.ts
+++ b/packages/client/lib/commands/HSETNX.ts
@@ -1,13 +1,26 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command, NumberReply } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return ['HSETNX', key, field, value];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HSETNX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash
+   * @param field - The field to set if it does not exist
+   * @param value - The value to set
+   * @see https://redis.io/commands/hsetnx/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    field: RedisArgument,
+    value: RedisArgument
+  ) {
+    parser.push('HSETNX');
+    parser.pushKey(key);
+    parser.push(field, value);
+  },
+  transformReply: undefined as unknown as () => NumberReply<0 | 1>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HSTRLEN.spec.ts b/packages/client/lib/commands/HSTRLEN.spec.ts
index 79c3150211e..47dd0eaf795 100644
--- a/packages/client/lib/commands/HSTRLEN.spec.ts
+++ b/packages/client/lib/commands/HSTRLEN.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HSTRLEN';
+import HSTRLEN from './HSTRLEN';
+import { parseArgs } from './generic-transformers';
 
 describe('HSTRLEN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'field'),
-            ['HSTRLEN', 'key', 'field']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(HSTRLEN, 'key', 'field'),
+      ['HSTRLEN', 'key', 'field']
+    );
+  });
 
-    testUtils.testWithClient('client.hStrLen', async client => {
-        assert.equal(
-            await client.hStrLen('key', 'field'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hStrLen', async client => {
+    assert.equal(
+      await client.hStrLen('key', 'field'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HSTRLEN.ts b/packages/client/lib/commands/HSTRLEN.ts
index a820e6c5643..016c14e27a8 100644
--- a/packages/client/lib/commands/HSTRLEN.ts
+++ b/packages/client/lib/commands/HSTRLEN.ts
@@ -1,12 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    field: RedisCommandArgument
-): RedisCommandArguments {
-    return ['HSTRLEN', key, field];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the HSTRLEN command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the hash
+   * @param field - The field to get the string length of
+   * @see https://redis.io/commands/hstrlen/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) {
+    parser.push('HSTRLEN');
+    parser.pushKey(key);
+    parser.push(field);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts
index 21b8b329a5d..a79500e4d06 100644
--- a/packages/client/lib/commands/HTTL.spec.ts
+++ b/packages/client/lib/commands/HTTL.spec.ts
@@ -1,7 +1,8 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HTTL';
+import HTTL from './HTTL';
 import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('HTTL', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
@@ -9,18 +10,17 @@ describe('HTTL', () => {
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
-        transformArguments('key', 'field'),
+        parseArgs(HTTL, 'key', 'field'),
         ['HTTL', 'key', 'FIELDS', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
-        transformArguments('key', ['field1', 'field2']),
+        parseArgs(HTTL, 'key', ['field1', 'field2']),
         ['HTTL', 'key', 'FIELDS', '2', 'field1', 'field2']
       );
     });
-  
   });
 
   testUtils.testWithClient('hTTL', async client => {
diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts
index d3eedd0db0e..710b4c7c1ff 100644
--- a/packages/client/lib/commands/HTTL.ts
+++ b/packages/client/lib/commands/HTTL.ts
@@ -1,11 +1,24 @@
-import { RedisCommandArgument } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
-  return pushVerdictArgument(['HTTL', key, 'FIELDS'], fields);
-}
-
-export declare function transformReply(): Array<number> | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the remaining time to live of field(s) in a hash.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the hash.
+   * @param fields - Fields to check time to live.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    fields: RedisVariadicArgument
+  ) {
+    parser.push('HTTL');
+    parser.pushKey(key);
+    parser.push('FIELDS');
+    parser.pushVariadicWithLength(fields);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HVALS.spec.ts b/packages/client/lib/commands/HVALS.spec.ts
index d0a6c39ce5f..89cbb52861c 100644
--- a/packages/client/lib/commands/HVALS.spec.ts
+++ b/packages/client/lib/commands/HVALS.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './HVALS';
+import HVALS from './HVALS';
+import { parseArgs } from './generic-transformers';
 
 describe('HVALS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['HVALS', 'key']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(HVALS, 'key'),
+      ['HVALS', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.hVals', async client => {
-        assert.deepEqual(
-            await client.hVals('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('hVals', async client => {
+    assert.deepEqual(
+      await client.hVals('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/HVALS.ts b/packages/client/lib/commands/HVALS.ts
index ef63fdc7f8a..faa5fe43442 100644
--- a/packages/client/lib/commands/HVALS.ts
+++ b/packages/client/lib/commands/HVALS.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['HVALS', key];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets all values in a hash.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the hash.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('HVALS');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/INCR.spec.ts b/packages/client/lib/commands/INCR.spec.ts
index 321d83edc54..0fe7ed7f8e6 100644
--- a/packages/client/lib/commands/INCR.spec.ts
+++ b/packages/client/lib/commands/INCR.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './INCR';
+import INCR from './INCR';
+import { parseArgs } from './generic-transformers';
 
 describe('INCR', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['INCR', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INCR, 'key'),
+      ['INCR', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.incr', async client => {
-        assert.equal(
-            await client.incr('key'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('incr', async client => {
+    assert.equal(
+      await client.incr('key'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/INCR.ts b/packages/client/lib/commands/INCR.ts
index 2f9a9adfe2b..0a294ccdc5a 100644
--- a/packages/client/lib/commands/INCR.ts
+++ b/packages/client/lib/commands/INCR.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['INCR', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the INCR command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to increment
+   * @see https://redis.io/commands/incr/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('INCR');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/INCRBY.spec.ts b/packages/client/lib/commands/INCRBY.spec.ts
index a671d0ec259..e2a5842f20a 100644
--- a/packages/client/lib/commands/INCRBY.spec.ts
+++ b/packages/client/lib/commands/INCRBY.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './INCRBY';
+import INCRBY from './INCRBY';
+import { parseArgs } from './generic-transformers';
 
-describe('INCR', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['INCRBY', 'key', '1']
-        );
-    });
+describe('INCRBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INCRBY, 'key', 1),
+      ['INCRBY', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.incrBy', async client => {
-        assert.equal(
-            await client.incrBy('key', 1),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('incrBy', async client => {
+    assert.equal(
+      await client.incrBy('key', 1),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/INCRBY.ts b/packages/client/lib/commands/INCRBY.ts
index 75c61156d6b..f23ec1a74a4 100644
--- a/packages/client/lib/commands/INCRBY.ts
+++ b/packages/client/lib/commands/INCRBY.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    increment: number
-): RedisCommandArguments {
-    return ['INCRBY', key, increment.toString()];
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the INCRBY command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to increment
+   * @param increment - The amount to increment by
+   * @see https://redis.io/commands/incrby/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, increment: number) {
+    parser.push('INCRBY');
+    parser.pushKey(key);
+    parser.push(increment.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/INCRBYFLOAT.spec.ts b/packages/client/lib/commands/INCRBYFLOAT.spec.ts
index b2dd5aa5da9..57596970708 100644
--- a/packages/client/lib/commands/INCRBYFLOAT.spec.ts
+++ b/packages/client/lib/commands/INCRBYFLOAT.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './INCRBYFLOAT';
+import INCRBYFLOAT from './INCRBYFLOAT';
+import { parseArgs } from './generic-transformers';
 
 describe('INCRBYFLOAT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1.5),
-            ['INCRBYFLOAT', 'key', '1.5']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INCRBYFLOAT, 'key', 1.5),
+      ['INCRBYFLOAT', 'key', '1.5']
+    );
+  });
 
-    testUtils.testWithClient('client.incrByFloat', async client => {
-        assert.equal(
-            await client.incrByFloat('key', 1.5),
-            '1.5'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('incrByFloat', async client => {
+    assert.equal(
+      await client.incrByFloat('key', 1.5),
+      '1.5'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/INCRBYFLOAT.ts b/packages/client/lib/commands/INCRBYFLOAT.ts
index ace3702339e..9effa756db5 100644
--- a/packages/client/lib/commands/INCRBYFLOAT.ts
+++ b/packages/client/lib/commands/INCRBYFLOAT.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    increment: number
-): RedisCommandArguments {
-    return ['INCRBYFLOAT', key, increment.toString()];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  /**
+   * Constructs the INCRBYFLOAT command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to increment
+   * @param increment - The floating-point value to increment by
+   * @see https://redis.io/commands/incrbyfloat/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, increment: number) {
+    parser.push('INCRBYFLOAT');
+    parser.pushKey(key);
+    parser.push(increment.toString());
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/INFO.spec.ts b/packages/client/lib/commands/INFO.spec.ts
index 118682c7da1..7ee8a95c137 100644
--- a/packages/client/lib/commands/INFO.spec.ts
+++ b/packages/client/lib/commands/INFO.spec.ts
@@ -1,20 +1,29 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './INFO';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import INFO from './INFO';
+import { parseArgs } from './generic-transformers';
 
 describe('INFO', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['INFO']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(INFO),
+        ['INFO']
+      );
+    });
 
-        it('server section', () => {
-            assert.deepEqual(
-                transformArguments('server'),
-                ['INFO', 'server']
-            );
-        });
+    it('server section', () => {
+      assert.deepEqual(
+        parseArgs(INFO, 'server'),
+        ['INFO', 'server']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.info', async client => {
+    assert.equal(
+      typeof await client.info(),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/INFO.ts b/packages/client/lib/commands/INFO.ts
index 8ab24221b26..799fcb1825a 100644
--- a/packages/client/lib/commands/INFO.ts
+++ b/packages/client/lib/commands/INFO.ts
@@ -1,13 +1,22 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types';
 
-export function transformArguments(section?: string): Array<string> {
-    const args = ['INFO'];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the INFO command
+   * 
+   * @param parser - The command parser
+   * @param section - Optional specific section of information to retrieve
+   * @see https://redis.io/commands/info/
+   */
+  parseCommand(parser: CommandParser, section?: RedisArgument) {
+    parser.push('INFO');
 
     if (section) {
-        args.push(section);
+      parser.push(section);
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => VerbatimStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/KEYS.spec.ts b/packages/client/lib/commands/KEYS.spec.ts
index c066331ea7c..8100559a7e9 100644
--- a/packages/client/lib/commands/KEYS.spec.ts
+++ b/packages/client/lib/commands/KEYS.spec.ts
@@ -1,11 +1,11 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
 
 describe('KEYS', () => {
-    testUtils.testWithClient('client.keys', async client => {
-        assert.deepEqual(
-            await client.keys('pattern'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('keys', async client => {
+    assert.deepEqual(
+      await client.keys('pattern'),
+      []
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/KEYS.ts b/packages/client/lib/commands/KEYS.ts
index c96ee001436..eb240c26ceb 100644
--- a/packages/client/lib/commands/KEYS.ts
+++ b/packages/client/lib/commands/KEYS.ts
@@ -1,7 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(pattern: RedisCommandArgument): RedisCommandArguments {
-    return ['KEYS', pattern];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the KEYS command
+   * 
+   * @param parser - The command parser
+   * @param pattern - The pattern to match keys against
+   * @see https://redis.io/commands/keys/
+   */
+  parseCommand(parser: CommandParser, pattern: RedisArgument) {
+    parser.push('KEYS', pattern);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LASTSAVE.spec.ts b/packages/client/lib/commands/LASTSAVE.spec.ts
index a6b4863f39e..fba26811170 100644
--- a/packages/client/lib/commands/LASTSAVE.spec.ts
+++ b/packages/client/lib/commands/LASTSAVE.spec.ts
@@ -1,16 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LASTSAVE';
+import LASTSAVE from './LASTSAVE';
+import { parseArgs } from './generic-transformers';
 
 describe('LASTSAVE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['LASTSAVE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LASTSAVE),
+      ['LASTSAVE']
+    );
+  });
 
-    testUtils.testWithClient('client.lastSave', async client => {
-        assert.ok((await client.lastSave()) instanceof Date);
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.lastSave', async client => {
+    assert.equal(
+      typeof await client.lastSave(),
+      'number'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LASTSAVE.ts b/packages/client/lib/commands/LASTSAVE.ts
index 76944d3548b..fbbc6a0046a 100644
--- a/packages/client/lib/commands/LASTSAVE.ts
+++ b/packages/client/lib/commands/LASTSAVE.ts
@@ -1,9 +1,17 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['LASTSAVE'];
-}
-
-export function transformReply(reply: number): Date {
-    return new Date(reply);
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LASTSAVE command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/lastsave/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('LASTSAVE');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts
index 3888ff8bd36..654751b5b57 100644
--- a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts
+++ b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts
@@ -1,19 +1,20 @@
-import {strict as assert} from 'assert';
-import testUtils, {GLOBAL} from '../test-utils';
-import { transformArguments } from './LATENCY_DOCTOR';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import LATENCY_DOCTOR from './LATENCY_DOCTOR';
+import { parseArgs } from './generic-transformers';
 
 describe('LATENCY DOCTOR', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['LATENCY', 'DOCTOR']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LATENCY_DOCTOR),
+      ['LATENCY', 'DOCTOR']
+    );
+  });
 
-    testUtils.testWithClient('client.latencyDoctor', async client => {
-        assert.equal(
-            typeof (await client.latencyDoctor()),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.latencyDoctor', async client => {
+    assert.equal(
+      typeof await client.latencyDoctor(),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.ts b/packages/client/lib/commands/LATENCY_DOCTOR.ts
index d2106c06114..5ba7ee6a7bf 100644
--- a/packages/client/lib/commands/LATENCY_DOCTOR.ts
+++ b/packages/client/lib/commands/LATENCY_DOCTOR.ts
@@ -1,5 +1,17 @@
-export function transformArguments(): Array<string> {
-    return ['LATENCY', 'DOCTOR'];
-}
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LATENCY DOCTOR command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/latency-doctor/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('LATENCY', 'DOCTOR');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts
index 21755a253b3..7135dc1c420 100644
--- a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts
+++ b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts
@@ -1,28 +1,27 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LATENCY_GRAPH';
+import LATENCY_GRAPH from './LATENCY_GRAPH';
+import { parseArgs } from './generic-transformers';
 
 describe('LATENCY GRAPH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('command'),
-            [
-                'LATENCY',
-                'GRAPH',
-                'command'
-            ]
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LATENCY_GRAPH, 'command'),
+      [
+        'LATENCY',
+        'GRAPH',
+        'command'
+      ]
+    );
+  });
 
-    testUtils.testWithClient('client.latencyGraph', async client => {
-        await Promise.all([
-            client.configSet('latency-monitor-threshold', '1'),
-            client.sendCommand(['DEBUG', 'SLEEP', '0.001'])
-        ]);
+  testUtils.testWithClient('client.latencyGraph', async client => {
+    const [,, reply] = await Promise.all([
+      client.configSet('latency-monitor-threshold', '1'),
+      client.sendCommand(['DEBUG', 'SLEEP', '0.001']),
+      client.latencyGraph('command')
+    ]);
 
-        assert.equal(
-            typeof await client.latencyGraph('command'),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(typeof reply, 'string');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LATENCY_GRAPH.ts b/packages/client/lib/commands/LATENCY_GRAPH.ts
index e4e078b90f2..8c53624c741 100644
--- a/packages/client/lib/commands/LATENCY_GRAPH.ts
+++ b/packages/client/lib/commands/LATENCY_GRAPH.ts
@@ -1,25 +1,39 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export type EventType =
-    'active-defrag-cycle'
-    | 'aof-fsync-always'
-    | 'aof-stat'
-    | 'aof-rewrite-diff-write'
-    | 'aof-rename'
-    | 'aof-write'
-    | 'aof-write-active-child'
-    | 'aof-write-alone'
-    | 'aof-write-pending-fsync'
-    | 'command'
-    | 'expire-cycle'
-    | 'eviction-cycle'
-    | 'eviction-del'
-    | 'fast-command'
-    | 'fork'
-    | 'rdb-unlink-temp-file';
+export const LATENCY_EVENTS = {
+  ACTIVE_DEFRAG_CYCLE: 'active-defrag-cycle',
+  AOF_FSYNC_ALWAYS: 'aof-fsync-always',
+  AOF_STAT: 'aof-stat',
+  AOF_REWRITE_DIFF_WRITE: 'aof-rewrite-diff-write',
+  AOF_RENAME: 'aof-rename',
+  AOF_WRITE: 'aof-write',
+  AOF_WRITE_ACTIVE_CHILD: 'aof-write-active-child',
+  AOF_WRITE_ALONE: 'aof-write-alone',
+  AOF_WRITE_PENDING_FSYNC: 'aof-write-pending-fsync',
+  COMMAND: 'command',
+  EXPIRE_CYCLE: 'expire-cycle',
+  EVICTION_CYCLE: 'eviction-cycle',
+  EVICTION_DEL: 'eviction-del',
+  FAST_COMMAND: 'fast-command',
+  FORK: 'fork',
+  RDB_UNLINK_TEMP_FILE: 'rdb-unlink-temp-file'
+} as const;
 
-export function transformArguments(event: EventType): RedisCommandArguments {
-    return ['LATENCY', 'GRAPH', event];
-}
+export type LatencyEvent = typeof LATENCY_EVENTS[keyof typeof LATENCY_EVENTS];
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LATENCY GRAPH command
+   * 
+   * @param parser - The command parser
+   * @param event - The latency event to get the graph for
+   * @see https://redis.io/commands/latency-graph/
+   */
+  parseCommand(parser: CommandParser, event: LatencyEvent) {
+    parser.push('LATENCY', 'GRAPH', event);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts
index e79e969b261..64f94d0d1a3 100644
--- a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts
+++ b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts
@@ -1,26 +1,27 @@
-import {strict as assert} from 'assert';
-import testUtils, {GLOBAL} from '../test-utils';
-import { transformArguments } from './LATENCY_HISTORY';
+import { strict as assert } from 'assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import LATENCY_HISTORY from './LATENCY_HISTORY';
+import { parseArgs } from './generic-transformers';
 
 describe('LATENCY HISTORY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('command'),
-            ['LATENCY', 'HISTORY', 'command']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LATENCY_HISTORY, 'command'),
+      ['LATENCY', 'HISTORY', 'command']
+    );
+  });
 
-    testUtils.testWithClient('client.latencyHistory', async client => {
-        await Promise.all([
-            client.configSet('latency-monitor-threshold', '100'),
-            client.sendCommand(['DEBUG', 'SLEEP', '1'])
-        ]);
-        
-        const latencyHisRes = await client.latencyHistory('command');
-        assert.ok(Array.isArray(latencyHisRes));
-        for (const [timestamp, latency] of latencyHisRes) {
-            assert.equal(typeof timestamp, 'number');
-            assert.equal(typeof latency, 'number');
-        }
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.latencyHistory', async client => {
+    const [,, reply] = await Promise.all([
+      client.configSet('latency-monitor-threshold', '100'),
+      client.sendCommand(['DEBUG', 'SLEEP', '1']),
+      client.latencyHistory('command')
+    ]);
+
+    assert.ok(Array.isArray(reply));
+    for (const [timestamp, latency] of reply) {
+      assert.equal(typeof timestamp, 'number');
+      assert.equal(typeof latency, 'number');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LATENCY_HISTORY.ts b/packages/client/lib/commands/LATENCY_HISTORY.ts
index c0b1964553e..dec7129befa 100644
--- a/packages/client/lib/commands/LATENCY_HISTORY.ts
+++ b/packages/client/lib/commands/LATENCY_HISTORY.ts
@@ -1,27 +1,41 @@
-export type EventType = (
-    'active-defrag-cycle' |
-    'aof-fsync-always' |
-    'aof-stat' |
-    'aof-rewrite-diff-write' |
-    'aof-rename' |
-    'aof-write' |
-    'aof-write-active-child' |
-    'aof-write-alone' |
-    'aof-write-pending-fsync' |
-    'command' |
-    'expire-cycle' |
-    'eviction-cycle' |
-    'eviction-del' |
-    'fast-command' |
-    'fork' |
-    'rdb-unlink-temp-file'
+import { CommandParser } from '../client/parser';
+import { ArrayReply, TuplesReply, NumberReply, Command } from '../RESP/types';
+
+export type LatencyEventType = (
+  'active-defrag-cycle' |
+  'aof-fsync-always' |
+  'aof-stat' |
+  'aof-rewrite-diff-write' |
+  'aof-rename' |
+  'aof-write' |
+  'aof-write-active-child' |
+  'aof-write-alone' |
+  'aof-write-pending-fsync' |
+  'command' |
+  'expire-cycle' |
+  'eviction-cycle' |
+  'eviction-del' |
+  'fast-command' |
+  'fork' |
+  'rdb-unlink-temp-file'
 );
 
-export function transformArguments(event: EventType) {
-    return ['LATENCY', 'HISTORY', event];
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LATENCY HISTORY command
+   * 
+   * @param parser - The command parser
+   * @param event - The latency event to get the history for
+   * @see https://redis.io/commands/latency-history/
+   */
+  parseCommand(parser: CommandParser, event: LatencyEventType) {
+    parser.push('LATENCY', 'HISTORY', event);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<TuplesReply<[
+    timestamp: NumberReply,
+    latency: NumberReply
+  ]>>
+} as const satisfies Command;
 
-export declare function transformReply(): Array<[
-    timestamp: number,
-    latency: number,
-]>;
diff --git a/packages/client/lib/commands/LATENCY_LATEST.spec.ts b/packages/client/lib/commands/LATENCY_LATEST.spec.ts
index 4087f212139..2cd2d9a5e06 100644
--- a/packages/client/lib/commands/LATENCY_LATEST.spec.ts
+++ b/packages/client/lib/commands/LATENCY_LATEST.spec.ts
@@ -1,27 +1,28 @@
-import {strict as assert} from 'assert';
-import testUtils, {GLOBAL} from '../test-utils';
-import { transformArguments } from './LATENCY_LATEST';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import LATENCY_LATEST from './LATENCY_LATEST';
+import { parseArgs } from './generic-transformers';
 
 describe('LATENCY LATEST', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['LATENCY', 'LATEST']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LATENCY_LATEST),
+      ['LATENCY', 'LATEST']
+    );
+  });
 
-    testUtils.testWithClient('client.latencyLatest', async client => {
-        await Promise.all([
-            client.configSet('latency-monitor-threshold', '100'),
-            client.sendCommand(['DEBUG', 'SLEEP', '1'])
-        ]);
-        const latency = await client.latencyLatest();
-        assert.ok(Array.isArray(latency));
-        for (const [name, timestamp, latestLatency, allTimeLatency] of latency) {
-            assert.equal(typeof name, 'string');
-            assert.equal(typeof timestamp, 'number');
-            assert.equal(typeof latestLatency, 'number');
-            assert.equal(typeof allTimeLatency, 'number');
-        }
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.latencyLatest', async client => {
+    const [,, reply] = await Promise.all([
+      client.configSet('latency-monitor-threshold', '100'),
+      client.sendCommand(['DEBUG', 'SLEEP', '1']),
+      client.latencyLatest()
+    ]);
+    assert.ok(Array.isArray(reply));
+    for (const [name, timestamp, latestLatency, allTimeLatency] of reply) {
+      assert.equal(typeof name, 'string');
+      assert.equal(typeof timestamp, 'number');
+      assert.equal(typeof latestLatency, 'number');
+      assert.equal(typeof allTimeLatency, 'number');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LATENCY_LATEST.ts b/packages/client/lib/commands/LATENCY_LATEST.ts
index 3e4dd6236c6..8fbdd46a13a 100644
--- a/packages/client/lib/commands/LATENCY_LATEST.ts
+++ b/packages/client/lib/commands/LATENCY_LATEST.ts
@@ -1,12 +1,23 @@
-import { RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(): RedisCommandArguments {
-    return ['LATENCY', 'LATEST'];
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LATENCY LATEST command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/latency-latest/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('LATENCY', 'LATEST');
+  },
+  transformReply: undefined as unknown as () => ArrayReply<[
+    name: BlobStringReply,
+    timestamp: NumberReply,
+    latestLatency: NumberReply,
+    allTimeLatency: NumberReply
+  ]>
+} as const satisfies Command;
 
-export declare function transformReply(): Array<[
-    name: string,
-    timestamp: number,
-    latestLatency: number,
-    allTimeLatency: number
-]>;
diff --git a/packages/client/lib/commands/LCS.spec.ts b/packages/client/lib/commands/LCS.spec.ts
index a4d9035571e..aedbb1b34e3 100644
--- a/packages/client/lib/commands/LCS.spec.ts
+++ b/packages/client/lib/commands/LCS.spec.ts
@@ -1,28 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LCS';
+import LCS from './LCS';
+import { parseArgs } from './generic-transformers';
 
 describe('LCS', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('1', '2'),
-            ['LCS', '1', '2']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LCS, '1', '2'),
+      ['LCS', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.lcs', async client => {
-        assert.equal(
-            await client.lcs('1', '2'),
-            ''
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lcs', async cluster => {
-        assert.equal(
-            await cluster.lcs('{tag}1', '{tag}2'),
-            ''
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lcs', async client => {
+    assert.equal(
+      await client.lcs('{tag}1', '{tag}2'),
+      ''
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LCS.ts b/packages/client/lib/commands/LCS.ts
index b075b73e8a8..9b2317e147f 100644
--- a/packages/client/lib/commands/LCS.ts
+++ b/packages/client/lib/commands/LCS.ts
@@ -1,18 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key1: RedisCommandArgument,
-    key2: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'LCS',
-        key1,
-        key2
-    ];
-}
-
-export declare function transformReply(): string | Buffer;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LCS command (Longest Common Substring)
+   * 
+   * @param parser - The command parser
+   * @param key1 - First key containing the first string
+   * @param key2 - Second key containing the second string
+   * @see https://redis.io/commands/lcs/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key1: RedisArgument,
+    key2: RedisArgument
+  ) {
+    parser.push('LCS');
+    parser.pushKeys([key1, key2]);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LCS_IDX.spec.ts b/packages/client/lib/commands/LCS_IDX.spec.ts
index fc3ee54f7c0..c4cc6681d85 100644
--- a/packages/client/lib/commands/LCS_IDX.spec.ts
+++ b/packages/client/lib/commands/LCS_IDX.spec.ts
@@ -1,41 +1,35 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LCS_IDX';
+import LCS_IDX from './LCS_IDX';
+import { parseArgs } from './generic-transformers';
 
-describe('LCS_IDX', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+describe('LCS IDX', () => {
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('1', '2'),
-            ['LCS', '1', '2', 'IDX']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LCS_IDX, '1', '2'),
+      ['LCS', '1', '2', 'IDX']
+    );
+  });
 
-    testUtils.testWithClient('client.lcsIdx', async client => {
-        const [, reply] = await Promise.all([
-            client.mSet({
-                '1': 'abc',
-                '2': 'bc'
-            }),
-            client.lcsIdx('1', '2')
-        ]);
+  testUtils.testWithClient('client.lcsIdx', async client => {
+    const [, reply] = await Promise.all([
+      client.mSet({
+        '1': 'abc',
+        '2': 'bc'
+      }),
+      client.lcsIdx('1', '2')
+    ]);
 
-        assert.deepEqual(
-            reply,
-            {
-                matches: [{
-                    key1: {
-                        start: 1,
-                        end: 2
-                    },
-                    key2: {
-                        start: 0,
-                        end: 1
-                    }
-                }],
-                length: 2
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(
+      reply,
+      {
+        matches: [
+          [[1, 2], [0, 1]]
+        ],
+        len: 2
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LCS_IDX.ts b/packages/client/lib/commands/LCS_IDX.ts
index 262a02ba4c6..684aa99efb0 100644
--- a/packages/client/lib/commands/LCS_IDX.ts
+++ b/packages/client/lib/commands/LCS_IDX.ts
@@ -1,42 +1,58 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { RangeReply, RawRangeReply, transformRangeReply } from './generic-transformers';
-import { transformArguments as transformLcsArguments } from './LCS';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NumberReply, UnwrapReply, Resp2Reply, Command, TuplesReply } from '../RESP/types';
+import LCS from './LCS';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS';
-
-export function transformArguments(
-    key1: RedisCommandArgument,
-    key2: RedisCommandArgument
-): RedisCommandArguments {
-    const args = transformLcsArguments(key1, key2);
-    args.push('IDX');
-    return args;
+export interface LcsIdxOptions {
+  MINMATCHLEN?: number;
 }
 
-type RawReply = [
-    'matches',
-    Array<[
-        key1: RawRangeReply,
-        key2: RawRangeReply
-    ]>,
-    'len',
-    number
-];
+export type LcsIdxRange = TuplesReply<[
+  start: NumberReply,
+  end: NumberReply
+]>;
 
-interface Reply {
-    matches: Array<{
-        key1: RangeReply;
-        key2: RangeReply;
-    }>;
-    length: number;
-}
+export type LcsIdxMatches = ArrayReply<
+  TuplesReply<[
+    key1: LcsIdxRange,
+    key2: LcsIdxRange
+  ]>
+>;
 
-export function transformReply(reply: RawReply): Reply {
-    return {
-        matches: reply[1].map(([key1, key2]) => ({
-            key1: transformRangeReply(key1),
-            key2: transformRangeReply(key2)
-        })),
-        length: reply[3]
-    };
-}
+export type LcsIdxReply = TuplesToMapReply<[
+  [BlobStringReply<'matches'>, LcsIdxMatches],
+  [BlobStringReply<'len'>, NumberReply]
+]>;
+
+export default {
+  IS_READ_ONLY: LCS.IS_READ_ONLY,
+  /**
+   * Constructs the LCS command with IDX option
+   * 
+   * @param parser - The command parser
+   * @param key1 - First key containing the first string
+   * @param key2 - Second key containing the second string
+   * @param options - Additional options for the LCS IDX command
+   * @see https://redis.io/commands/lcs/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key1: RedisArgument,
+    key2: RedisArgument,
+    options?: LcsIdxOptions
+  ) {
+    LCS.parseCommand(parser, key1, key2);
+
+    parser.push('IDX');
+
+    if (options?.MINMATCHLEN) {
+      parser.push('MINMATCHLEN', options.MINMATCHLEN.toString());
+    }
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<LcsIdxReply>>) => ({
+      matches: reply[1],
+      len: reply[3]
+    }),
+    3: undefined as unknown as () => LcsIdxReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts
index 8be9b993135..92ecad4761c 100644
--- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts
+++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts
@@ -1,42 +1,35 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LCS_IDX_WITHMATCHLEN';
+import LCS_IDX_WITHMATCHLEN from './LCS_IDX_WITHMATCHLEN';
+import { parseArgs } from './generic-transformers';
 
-describe('LCS_IDX_WITHMATCHLEN', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+describe('LCS IDX WITHMATCHLEN', () => {
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('1', '2'),
-            ['LCS', '1', '2', 'IDX', 'WITHMATCHLEN']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LCS_IDX_WITHMATCHLEN, '1', '2'),
+      ['LCS', '1', '2', 'IDX', 'WITHMATCHLEN']
+    );
+  });
 
-    testUtils.testWithClient('client.lcsIdxWithMatchLen', async client => {
-        const [, reply] = await Promise.all([
-            client.mSet({
-                '1': 'abc',
-                '2': 'bc'
-            }),
-            client.lcsIdxWithMatchLen('1', '2')
-        ]);
+  testUtils.testWithClient('client.lcsIdxWithMatchLen', async client => {
+    const [, reply] = await Promise.all([
+      client.mSet({
+        '1': 'abc',
+        '2': 'bc'
+      }),
+      client.lcsIdxWithMatchLen('1', '2')
+    ]);
 
-        assert.deepEqual(
-            reply,
-            {
-                matches: [{
-                    key1: {
-                        start: 1,
-                        end: 2
-                    },
-                    key2: {
-                        start: 0,
-                        end: 1
-                    },
-                    length: 2
-                }],
-                length: 2
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(
+      reply,
+      {
+        matches: [
+          [[1, 2], [0, 1], 2]
+        ],
+        len: 2
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts
index 989870d6ca2..f3578b789fc 100644
--- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts
+++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts
@@ -1,45 +1,37 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { RangeReply, RawRangeReply, transformRangeReply } from './generic-transformers';
-import { transformArguments as transformLcsArguments } from './LCS';
+import { TuplesToMapReply, BlobStringReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
+import LCS_IDX, { LcsIdxRange } from './LCS_IDX';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS';
+export type LcsIdxWithMatchLenMatches = ArrayReply<
+  TuplesReply<[
+    key1: LcsIdxRange,
+    key2: LcsIdxRange,
+    len: NumberReply
+  ]>
+>;
 
-export function transformArguments(
-    key1: RedisCommandArgument,
-    key2: RedisCommandArgument
-): RedisCommandArguments {
-    const args = transformLcsArguments(key1, key2);
-    args.push('IDX', 'WITHMATCHLEN');
-    return args;
-}
+export type LcsIdxWithMatchLenReply = TuplesToMapReply<[
+  [BlobStringReply<'matches'>, LcsIdxWithMatchLenMatches],
+  [BlobStringReply<'len'>, NumberReply]
+]>;
 
-type RawReply = [
-    'matches',
-    Array<[
-        key1: RawRangeReply,
-        key2: RawRangeReply,
-        length: number
-    ]>,
-    'len',
-    number
-];
-
-interface Reply {
-    matches: Array<{
-        key1: RangeReply;
-        key2: RangeReply;
-        length: number;
-    }>;
-    length: number;
-}
-
-export function transformReply(reply: RawReply): Reply {
-    return {
-        matches: reply[1].map(([key1, key2, length]) => ({
-            key1: transformRangeReply(key1),
-            key2: transformRangeReply(key2),
-            length
-        })),
-        length: reply[3]
-    };
-}
+export default {
+  IS_READ_ONLY: LCS_IDX.IS_READ_ONLY,
+  /**
+   * Constructs the LCS command with IDX and WITHMATCHLEN options
+   * 
+   * @param args - The same parameters as LCS_IDX command
+   * @see https://redis.io/commands/lcs/
+   */
+  parseCommand(...args: Parameters<typeof LCS_IDX.parseCommand>) {
+    const parser = args[0];
+    LCS_IDX.parseCommand(...args);
+    parser.push('WITHMATCHLEN');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<LcsIdxWithMatchLenReply>>) => ({
+      matches: reply[1],
+      len: reply[3]
+    }),
+    3: undefined as unknown as () => LcsIdxWithMatchLenReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LCS_LEN.spec.ts b/packages/client/lib/commands/LCS_LEN.spec.ts
index bf4eefd3301..53a2e83c326 100644
--- a/packages/client/lib/commands/LCS_LEN.spec.ts
+++ b/packages/client/lib/commands/LCS_LEN.spec.ts
@@ -1,28 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LCS_LEN';
+import LCS_LEN from './LCS_LEN';
+import { parseArgs } from './generic-transformers';
 
 describe('LCS_LEN', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('1', '2'),
-            ['LCS', '1', '2', 'LEN']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LCS_LEN, '1', '2'),
+      ['LCS', '1', '2', 'LEN']
+    );
+  });
 
-    testUtils.testWithClient('client.lcsLen', async client => {
-        assert.equal(
-            await client.lcsLen('1', '2'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lcsLen', async cluster => {
-        assert.equal(
-            await cluster.lcsLen('{tag}1', '{tag}2'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lcsLen', async client => {
+    assert.equal(
+      await client.lcsLen('{tag}1', '{tag}2'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LCS_LEN.ts b/packages/client/lib/commands/LCS_LEN.ts
index a5121e4c13f..bb35c3d9209 100644
--- a/packages/client/lib/commands/LCS_LEN.ts
+++ b/packages/client/lib/commands/LCS_LEN.ts
@@ -1,15 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformLcsArguments } from './LCS';
+import { NumberReply, Command } from '../RESP/types';
+import LCS from './LCS';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS';
+export default {
+  IS_READ_ONLY: LCS.IS_READ_ONLY,
+  /**
+   * Constructs the LCS command with LEN option
+   * 
+   * @param args - The same parameters as LCS command
+   * @see https://redis.io/commands/lcs/
+   */
+  parseCommand(...args: Parameters<typeof LCS.parseCommand>) {
+    const parser = args[0];
 
-export function transformArguments(
-    key1: RedisCommandArgument,
-    key2: RedisCommandArgument
-): RedisCommandArguments {
-    const args = transformLcsArguments(key1, key2);
-    args.push('LEN');
-    return args;
-}
-
-export declare function transformReply(): number;
+    LCS.parseCommand(...args);
+    parser.push('LEN');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LINDEX.spec.ts b/packages/client/lib/commands/LINDEX.spec.ts
index aa3aafa789b..41eff474a1a 100644
--- a/packages/client/lib/commands/LINDEX.spec.ts
+++ b/packages/client/lib/commands/LINDEX.spec.ts
@@ -1,36 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LINDEX';
-describe('LINDEX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0),
-            ['LINDEX', 'key', '0']
-        );
-    });
-
-    describe('client.lIndex', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.lIndex('key', 0),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+import LINDEX from './LINDEX';
+import { parseArgs } from './generic-transformers';
 
-        testUtils.testWithClient('with value', async client => {
-            const [, lIndexReply] = await Promise.all([
-                client.lPush('key', 'element'),
-                client.lIndex('key', 0)
-            ]);
-
-            assert.equal(lIndexReply, 'element');
-        }, GLOBAL.SERVERS.OPEN);
-    });
+describe('LINDEX', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LINDEX, 'key', 0),
+      ['LINDEX', 'key', '0']
+    );
+  });
 
-    testUtils.testWithCluster('cluster.lIndex', async cluster => {
-        assert.equal(
-            await cluster.lIndex('key', 0),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lIndex', async client => {
+    assert.equal(
+      await client.lIndex('key', 0),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
\ No newline at end of file
diff --git a/packages/client/lib/commands/LINDEX.ts b/packages/client/lib/commands/LINDEX.ts
index 8e74ad8aae6..dd7671a41c6 100644
--- a/packages/client/lib/commands/LINDEX.ts
+++ b/packages/client/lib/commands/LINDEX.ts
@@ -1,14 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    index: number
-): RedisCommandArguments {
-    return ['LINDEX', key, index.toString()];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LINDEX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param index - The index of the element to retrieve
+   * @see https://redis.io/commands/lindex/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, index: number) {
+    parser.push('LINDEX');
+    parser.pushKey(key);
+    parser.push(index.toString());
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LINSERT.spec.ts b/packages/client/lib/commands/LINSERT.spec.ts
index 6454cc48536..c3c89d56c12 100644
--- a/packages/client/lib/commands/LINSERT.spec.ts
+++ b/packages/client/lib/commands/LINSERT.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LINSERT';
+import LINSERT from './LINSERT';
+import { parseArgs } from './generic-transformers';
 
 describe('LINSERT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'BEFORE', 'pivot', 'element'),
-            ['LINSERT', 'key', 'BEFORE', 'pivot', 'element']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LINSERT, 'key', 'BEFORE', 'pivot', 'element'),
+      ['LINSERT', 'key', 'BEFORE', 'pivot', 'element']
+    );
+  });
 
-    testUtils.testWithClient('client.lInsert', async client => {
-        assert.equal(
-            await client.lInsert('key', 'BEFORE', 'pivot', 'element'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lInsert', async cluster => {
-        assert.equal(
-            await cluster.lInsert('key', 'BEFORE', 'pivot', 'element'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lInsert', async client => {
+    assert.equal(
+      await client.lInsert('key', 'BEFORE', 'pivot', 'element'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LINSERT.ts b/packages/client/lib/commands/LINSERT.ts
index 0a8e1f32ba4..ede230191ba 100644
--- a/packages/client/lib/commands/LINSERT.ts
+++ b/packages/client/lib/commands/LINSERT.ts
@@ -1,22 +1,30 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
 type LInsertPosition = 'BEFORE' | 'AFTER';
 
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LINSERT command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param position - The position where to insert (BEFORE or AFTER)
+   * @param pivot - The element to find in the list
+   * @param element - The element to insert
+   * @see https://redis.io/commands/linsert/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     position: LInsertPosition,
-    pivot: RedisCommandArgument,
-    element: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'LINSERT',
-        key,
-        position,
-        pivot,
-        element
-    ];
-}
-
-export declare function transformReply(): number;
+    pivot: RedisArgument,
+    element: RedisArgument
+  ) {
+    parser.push('LINSERT');
+    parser.pushKey(key);
+    parser.push(position, pivot, element);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LLEN.spec.ts b/packages/client/lib/commands/LLEN.spec.ts
index fb126ddad55..d86078d0b48 100644
--- a/packages/client/lib/commands/LLEN.spec.ts
+++ b/packages/client/lib/commands/LLEN.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LLEN';
+import LLEN from './LLEN';
+import { parseArgs } from './generic-transformers';
 
 describe('LLEN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['LLEN', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LLEN, 'key'),
+      ['LLEN', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.lLen', async client => {
-        assert.equal(
-            await client.lLen('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lLen', async cluster => {
-        assert.equal(
-            await cluster.lLen('key'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lLen', async client => {
+    assert.equal(
+      await client.lLen('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LLEN.ts b/packages/client/lib/commands/LLEN.ts
index 3410e57d424..7ece6823bb5 100644
--- a/packages/client/lib/commands/LLEN.ts
+++ b/packages/client/lib/commands/LLEN.ts
@@ -1,11 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['LLEN', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LLEN command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list to get the length of
+   * @see https://redis.io/commands/llen/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('LLEN');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LMOVE.spec.ts b/packages/client/lib/commands/LMOVE.spec.ts
index f1d418c394e..bed3ff8eab0 100644
--- a/packages/client/lib/commands/LMOVE.spec.ts
+++ b/packages/client/lib/commands/LMOVE.spec.ts
@@ -1,28 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LMOVE';
+import LMOVE from './LMOVE';
+import { parseArgs } from './generic-transformers';
 
 describe('LMOVE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('source', 'destination', 'LEFT', 'RIGHT'),
-            ['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LMOVE, 'source', 'destination', 'LEFT', 'RIGHT'),
+      ['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT']
+    );
+  });
 
-    testUtils.testWithClient('client.lMove', async client => {
-        assert.equal(
-            await client.lMove('source', 'destination', 'LEFT', 'RIGHT'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lMove', async cluster => {
-        assert.equal(
-            await cluster.lMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lMove', async client => {
+    assert.equal(
+      await client.lMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LMOVE.ts b/packages/client/lib/commands/LMOVE.ts
index 849c6385f5a..9ed0003b23d 100644
--- a/packages/client/lib/commands/LMOVE.ts
+++ b/packages/client/lib/commands/LMOVE.ts
@@ -1,21 +1,29 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 import { ListSide } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    source: RedisCommandArgument,
-    destination: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the LMOVE command
+   * 
+   * @param parser - The command parser
+   * @param source - The source list key
+   * @param destination - The destination list key
+   * @param sourceSide - The side to pop from (LEFT or RIGHT)
+   * @param destinationSide - The side to push to (LEFT or RIGHT)
+   * @see https://redis.io/commands/lmove/
+   */
+  parseCommand(
+    parser: CommandParser,
+    source: RedisArgument,
+    destination: RedisArgument,
     sourceSide: ListSide,
     destinationSide: ListSide
-): RedisCommandArguments {
-    return [
-        'LMOVE',
-        source,
-        destination,
-        sourceSide,
-        destinationSide,
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+  ) {
+    parser.push('LMOVE');
+    parser.pushKeys([source, destination]);
+    parser.push(sourceSide, destinationSide);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LMPOP.spec.ts b/packages/client/lib/commands/LMPOP.spec.ts
index 5675ee9a285..bd2cf869e74 100644
--- a/packages/client/lib/commands/LMPOP.spec.ts
+++ b/packages/client/lib/commands/LMPOP.spec.ts
@@ -1,32 +1,51 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LMPOP';
+import LMPOP from './LMPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('LMPOP', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'LEFT'),
-                ['LMPOP', '1', 'key', 'LEFT']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(LMPOP, 'key', 'LEFT'),
+        ['LMPOP', '1', 'key', 'LEFT']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 'LEFT', {
-                    COUNT: 2
-                }),
-                ['LMPOP', '1', 'key', 'LEFT', 'COUNT', '2']
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(LMPOP, 'key', 'LEFT', {
+          COUNT: 2
+        }),
+        ['LMPOP', '1', 'key', 'LEFT', 'COUNT', '2']
+      );
     });
+  });
+
+  testUtils.testAll('lmPop - null', async client => {
+    assert.equal(
+      await client.lmPop('key', 'RIGHT'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
+
+  testUtils.testAll('lmPop - with member', async client => {
+    const [, reply] = await Promise.all([
+      client.lPush('key', 'element'),
+      client.lmPop('key', 'RIGHT')
+    ]);
 
-    testUtils.testWithClient('client.lmPop', async client => {
-        assert.deepEqual(
-            await client.lmPop('key', 'RIGHT'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [
+      'key',
+      ['element']
+    ]);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LMPOP.ts b/packages/client/lib/commands/LMPOP.ts
index 29d868b982f..54dc40c1c3d 100644
--- a/packages/client/lib/commands/LMPOP.ts
+++ b/packages/client/lib/commands/LMPOP.ts
@@ -1,22 +1,42 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types';
+import { ListSide, RedisVariadicArgument, Tail } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
+export interface LMPopOptions {
+  COUNT?: number;
+}
+
+export function parseLMPopArguments(
+  parser: CommandParser,
+  keys: RedisVariadicArgument,
+  side: ListSide,
+  options?: LMPopOptions
+) {
+  parser.pushKeysLength(keys);
+  parser.push(side);
 
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    side: ListSide,
-    options?: LMPopOptions
-): RedisCommandArguments {
-    return transformLMPopArguments(
-        ['LMPOP'],
-        keys,
-        side,
-        options
-    );
+  if (options?.COUNT !== undefined) {
+    parser.push('COUNT', options.COUNT.toString());
+  }
 }
 
-export declare function transformReply(): null | [
-    key: string,
-    elements: Array<string>
-];
+export type LMPopArguments = Tail<Parameters<typeof parseLMPopArguments>>;
+
+export default {
+  IS_READ_ONLY: false,  
+  /**
+   * Constructs the LMPOP command
+   * 
+   * @param parser - The command parser
+   * @param args - Arguments including keys, side (LEFT or RIGHT), and options
+   * @see https://redis.io/commands/lmpop/
+   */
+  parseCommand(parser: CommandParser, ...args: LMPopArguments) {
+    parser.push('LMPOP');
+    parseLMPopArguments(parser, ...args);
+  },
+  transformReply: undefined as unknown as () => NullReply | TuplesReply<[
+    key: BlobStringReply,
+    elements: Array<BlobStringReply>
+  ]>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LOLWUT.spec.ts b/packages/client/lib/commands/LOLWUT.spec.ts
index db335893302..b06030b0d0e 100644
--- a/packages/client/lib/commands/LOLWUT.spec.ts
+++ b/packages/client/lib/commands/LOLWUT.spec.ts
@@ -1,35 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LOLWUT';
+import LOLWUT from './LOLWUT';
+import { parseArgs } from './generic-transformers';
 
 describe('LOLWUT', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['LOLWUT']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(LOLWUT),
+        ['LOLWUT']
+      );
+    });
 
-        it('with version', () => {
-            assert.deepEqual(
-                transformArguments(5),
-                ['LOLWUT', 'VERSION', '5']
-            );
-        });
+    it('with version', () => {
+      assert.deepEqual(
+        parseArgs(LOLWUT, 5),
+        ['LOLWUT', 'VERSION', '5']
+      );
+    });
 
-        it('with version and optional arguments', () => {
-            assert.deepEqual(
-                transformArguments(5, 1, 2, 3),
-                ['LOLWUT', 'VERSION', '5', '1', '2', '3']
-            );
-        });
+    it('with version and optional arguments', () => {
+      assert.deepEqual(
+        parseArgs(LOLWUT, 5, 1, 2, 3),
+        ['LOLWUT', 'VERSION', '5', '1', '2', '3']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.LOLWUT', async client => {
-        assert.equal(
-            typeof (await client.LOLWUT()),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.LOLWUT', async client => {
+    assert.equal(
+      typeof (await client.LOLWUT()),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/LOLWUT.ts b/packages/client/lib/commands/LOLWUT.ts
index 5d5fc726065..5e07a103720 100644
--- a/packages/client/lib/commands/LOLWUT.ts
+++ b/packages/client/lib/commands/LOLWUT.ts
@@ -1,19 +1,26 @@
-import { RedisCommandArgument } from '.';
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(version?: number, ...optionalArguments: Array<number>): Array<string> {
-    const args = ['LOLWUT'];
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LOLWUT command
+   * 
+   * @param parser - The command parser
+   * @param version - Optional version parameter
+   * @param optionalArguments - Additional optional numeric arguments
+   * @see https://redis.io/commands/lolwut/
+   */
+  parseCommand(parser: CommandParser, version?: number, ...optionalArguments: Array<number>) {
+    parser.push('LOLWUT');
     if (version) {
-        args.push(
-            'VERSION',
-            version.toString(),
-            ...optionalArguments.map(String),
-        );
+      parser.push(
+        'VERSION',
+        version.toString()
+      );
+      parser.pushVariadic(optionalArguments.map(String));
     }
-
-    return args;
-}
-
-export declare function transformReply(): RedisCommandArgument;
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LPOP.spec.ts b/packages/client/lib/commands/LPOP.spec.ts
index d694fb10588..93449bdbf5f 100644
--- a/packages/client/lib/commands/LPOP.spec.ts
+++ b/packages/client/lib/commands/LPOP.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LPOP';
+import LPOP from './LPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('LPOP', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['LPOP', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LPOP, 'key'),
+      ['LPOP', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.lPop', async client => {
-        assert.equal(
-            await client.lPop('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lPop', async cluster => {
-        assert.equal(
-            await cluster.lPop('key'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lPop', async client => {
+    assert.equal(
+      await client.lPop('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LPOP.ts b/packages/client/lib/commands/LPOP.ts
index 5dd1bea5196..aaa83be465d 100644
--- a/packages/client/lib/commands/LPOP.ts
+++ b/packages/client/lib/commands/LPOP.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['LPOP', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  /**
+   * Constructs the LPOP command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list to pop from
+   * @see https://redis.io/commands/lpop/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('LPOP');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LPOP_COUNT.spec.ts b/packages/client/lib/commands/LPOP_COUNT.spec.ts
index 9d87fad3862..04bb3648d0a 100644
--- a/packages/client/lib/commands/LPOP_COUNT.spec.ts
+++ b/packages/client/lib/commands/LPOP_COUNT.spec.ts
@@ -1,28 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LPOP_COUNT';
+import LPOP_COUNT from './LPOP_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('LPOP COUNT', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['LPOP', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LPOP_COUNT, 'key', 1),
+      ['LPOP', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.lPopCount', async client => {
-        assert.equal(
-            await client.lPopCount('key', 1),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lPopCount', async cluster => {
-        assert.equal(
-            await cluster.lPopCount('key', 1),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lPopCount', async client => {
+    assert.equal(
+      await client.lPopCount('key', 1),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LPOP_COUNT.ts b/packages/client/lib/commands/LPOP_COUNT.ts
index 021517b018a..cdc0dc41a22 100644
--- a/packages/client/lib/commands/LPOP_COUNT.ts
+++ b/packages/client/lib/commands/LPOP_COUNT.ts
@@ -1,12 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NullReply, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import LPOP from './LPOP';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return ['LPOP', key, count.toString()];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument> | null;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the LPOP command with count parameter
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list to pop from
+   * @param count - The number of elements to pop
+   * @see https://redis.io/commands/lpop/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    LPOP.parseCommand(parser, key);
+    parser.push(count.toString())
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LPOS.spec.ts b/packages/client/lib/commands/LPOS.spec.ts
index 6b6050f2c3b..f26af3f540f 100644
--- a/packages/client/lib/commands/LPOS.spec.ts
+++ b/packages/client/lib/commands/LPOS.spec.ts
@@ -1,58 +1,55 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LPOS';
+import LPOS from './LPOS';
+import { parseArgs } from './generic-transformers';
 
 describe('LPOS', () => {
-    testUtils.isVersionGreaterThanHook([6, 0, 6]);
+  testUtils.isVersionGreaterThanHook([6, 0, 6]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element'),
-                ['LPOS', 'key', 'element']
-            );
-        });
-
-        it('with RANK', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', {
-                    RANK: 0
-                }),
-                ['LPOS', 'key', 'element', 'RANK', '0']
-            );
-        });
+  describe('processCommand', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(LPOS, 'key', 'element'),
+        ['LPOS', 'key', 'element']
+      );
+    });
 
-        it('with MAXLEN', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', {
-                    MAXLEN: 10
-                }),
-                ['LPOS', 'key', 'element', 'MAXLEN', '10']
-            );
-        });
+    it('with RANK', () => {
+      assert.deepEqual(
+        parseArgs(LPOS, 'key', 'element', {
+          RANK: 0
+        }),
+        ['LPOS', 'key', 'element', 'RANK', '0']
+      );
+    });
 
-        it('with RANK, MAXLEN', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', {
-                    RANK: 0,
-                    MAXLEN: 10
-                }),
-                ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10']
-            );
-        });
+    it('with MAXLEN', () => {
+      assert.deepEqual(
+        parseArgs(LPOS, 'key', 'element', {
+          MAXLEN: 10
+        }),
+        ['LPOS', 'key', 'element', 'MAXLEN', '10']
+      );
     });
 
-    testUtils.testWithClient('client.lPos', async client => {
-        assert.equal(
-            await client.lPos('key', 'element'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with RANK, MAXLEN', () => {
+      assert.deepEqual(
+        parseArgs(LPOS, 'key', 'element', {
+          RANK: 0,
+          MAXLEN: 10
+        }),
+        ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.lPos', async cluster => {
-        assert.equal(
-            await cluster.lPos('key', 'element'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lPos', async client => {
+    assert.equal(
+      await client.lPos('key', 'element'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LPOS.ts b/packages/client/lib/commands/LPOS.ts
index 1f2e34ab88e..54078b8185f 100644
--- a/packages/client/lib/commands/LPOS.ts
+++ b/packages/client/lib/commands/LPOS.ts
@@ -1,30 +1,40 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types';
 
 export interface LPosOptions {
-    RANK?: number;
-    MAXLEN?: number;
+  RANK?: number;
+  MAXLEN?: number;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    element: RedisCommandArgument,
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LPOS command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param element - The element to search for
+   * @param options - Optional parameters for RANK and MAXLEN
+   * @see https://redis.io/commands/lpos/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    element: RedisArgument,
     options?: LPosOptions
-): RedisCommandArguments {
-    const args = ['LPOS', key, element];
+  ) {
+    parser.push('LPOS');
+    parser.pushKey(key);
+    parser.push(element);
 
-    if (typeof options?.RANK === 'number') {
-        args.push('RANK', options.RANK.toString());
+    if (options?.RANK !== undefined) {
+      parser.push('RANK', options.RANK.toString());
     }
 
-    if (typeof options?.MAXLEN === 'number') {
-        args.push('MAXLEN', options.MAXLEN.toString());
+    if (options?.MAXLEN !== undefined) {
+      parser.push('MAXLEN', options.MAXLEN.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): number | null;
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LPOS_COUNT.spec.ts b/packages/client/lib/commands/LPOS_COUNT.spec.ts
index 4b01f2f59b9..702ef5a746b 100644
--- a/packages/client/lib/commands/LPOS_COUNT.spec.ts
+++ b/packages/client/lib/commands/LPOS_COUNT.spec.ts
@@ -1,58 +1,55 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LPOS_COUNT';
+import LPOS_COUNT from './LPOS_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('LPOS COUNT', () => {
-    testUtils.isVersionGreaterThanHook([6, 0, 6]);
+  testUtils.isVersionGreaterThanHook([6, 0, 6]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', 0),
-                ['LPOS', 'key', 'element', 'COUNT', '0']
-            );
-        });
-
-        it('with RANK', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', 0, {
-                    RANK: 0
-                }),
-                ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0']
-            );
-        });
+  describe('processCommand', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(LPOS_COUNT, 'key', 'element', 0),
+        ['LPOS', 'key', 'element', 'COUNT', '0']
+      );
+    });
 
-        it('with MAXLEN', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', 0, {
-                    MAXLEN: 10
-                }),
-                ['LPOS', 'key', 'element', 'COUNT', '0', 'MAXLEN', '10']
-            );
-        });
+    it('with RANK', () => {
+      assert.deepEqual(
+        parseArgs(LPOS_COUNT, 'key', 'element', 0, {
+          RANK: 0
+        }),
+        ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0']
+      );
+    });
 
-        it('with RANK, MAXLEN', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element', 0, {
-                    RANK: 0,
-                    MAXLEN: 10
-                }),
-                ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0', 'MAXLEN', '10']
-            );
-        });
+    it('with MAXLEN', () => {
+      assert.deepEqual(
+        parseArgs(LPOS_COUNT, 'key', 'element', 0, {
+          MAXLEN: 10
+        }),
+        ['LPOS', 'key', 'element', 'MAXLEN', '10', 'COUNT', '0']
+      );
     });
 
-    testUtils.testWithClient('client.lPosCount', async client => {
-        assert.deepEqual(
-            await client.lPosCount('key', 'element', 0),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with RANK, MAXLEN', () => {
+      assert.deepEqual(
+        parseArgs(LPOS_COUNT, 'key', 'element', 0, {
+          RANK: 0,
+          MAXLEN: 10
+        }),
+        ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10', 'COUNT', '0']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.lPosCount', async cluster => {
-        assert.deepEqual(
-            await cluster.lPosCount('key', 'element', 0),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lPosCount', async client => {
+    assert.deepEqual(
+      await client.lPosCount('key', 'element', 0),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LPOS_COUNT.ts b/packages/client/lib/commands/LPOS_COUNT.ts
index 0549df82db5..ace6e49c1e5 100644
--- a/packages/client/lib/commands/LPOS_COUNT.ts
+++ b/packages/client/lib/commands/LPOS_COUNT.ts
@@ -1,27 +1,30 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { LPosOptions } from './LPOS';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
+import LPOS, { LPosOptions } from './LPOS';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LPOS';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    element: RedisCommandArgument,
+export default {
+  CACHEABLE: LPOS.CACHEABLE,
+  IS_READ_ONLY: LPOS.IS_READ_ONLY,
+  /**
+   * Constructs the LPOS command with COUNT option
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param element - The element to search for
+   * @param count - The number of positions to return
+   * @param options - Optional parameters for RANK and MAXLEN
+   * @see https://redis.io/commands/lpos/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    element: RedisArgument,
     count: number,
     options?: LPosOptions
-): RedisCommandArguments {
-    const args = ['LPOS', key, element];
-
-    if (typeof options?.RANK === 'number') {
-        args.push('RANK', options.RANK.toString());
-    }
-
-    args.push('COUNT', count.toString());
-
-    if (typeof options?.MAXLEN === 'number') {
-        args.push('MAXLEN', options.MAXLEN.toString());
-    }
-
-    return args;
-}
+  ) {
+    LPOS.parseCommand(parser, key, element, options);
 
-export declare function transformReply(): Array<number>;
+    parser.push('COUNT', count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LPUSH.spec.ts b/packages/client/lib/commands/LPUSH.spec.ts
index b5b1f5084eb..09c7d9da772 100644
--- a/packages/client/lib/commands/LPUSH.spec.ts
+++ b/packages/client/lib/commands/LPUSH.spec.ts
@@ -1,35 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LPUSH';
+import LPUSH from './LPUSH';
+import { parseArgs } from './generic-transformers';
 
 describe('LPUSH', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'field'),
-                ['LPUSH', 'key', 'field']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['LPUSH', 'key', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(LPUSH, 'key', 'field'),
+        ['LPUSH', 'key', 'field']
+      );
     });
 
-    testUtils.testWithClient('client.lPush', async client => {
-        assert.equal(
-            await client.lPush('key', 'field'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(LPUSH, 'key', ['1', '2']),
+        ['LPUSH', 'key', '1', '2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.lPush', async cluster => {
-        assert.equal(
-            await cluster.lPush('key', 'field'),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lPush', async client => {
+    assert.equal(
+      await client.lPush('key', 'field'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LPUSH.ts b/packages/client/lib/commands/LPUSH.ts
index 7144b146e27..89e1e094870 100644
--- a/packages/client/lib/commands/LPUSH.ts
+++ b/packages/client/lib/commands/LPUSH.ts
@@ -1,12 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    elements: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['LPUSH', key], elements);}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the LPUSH command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param elements - One or more elements to push to the list
+   * @see https://redis.io/commands/lpush/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, elements: RedisVariadicArgument) {
+    parser.push('LPUSH');
+    parser.pushKey(key);
+    parser.pushVariadic(elements);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LPUSHX.spec.ts b/packages/client/lib/commands/LPUSHX.spec.ts
index d978e5a588f..179a0ddb29e 100644
--- a/packages/client/lib/commands/LPUSHX.spec.ts
+++ b/packages/client/lib/commands/LPUSHX.spec.ts
@@ -1,35 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LPUSHX';
+import LPUSHX from './LPUSHX';
+import { parseArgs } from './generic-transformers';
 
 describe('LPUSHX', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element'),
-                ['LPUSHX', 'key', 'element']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['LPUSHX', 'key', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(LPUSHX, 'key', 'element'),
+        ['LPUSHX', 'key', 'element']
+      );
     });
 
-    testUtils.testWithClient('client.lPushX', async client => {
-        assert.equal(
-            await client.lPushX('key', 'element'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(LPUSHX, 'key', ['1', '2']),
+        ['LPUSHX', 'key', '1', '2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.lPushX', async cluster => {
-        assert.equal(
-            await cluster.lPushX('key', 'element'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lPushX', async client => {
+    assert.equal(
+      await client.lPushX('key', 'element'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LPUSHX.ts b/packages/client/lib/commands/LPUSHX.ts
index 0b518add6da..e87bd4ff0d5 100644
--- a/packages/client/lib/commands/LPUSHX.ts
+++ b/packages/client/lib/commands/LPUSHX.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    element: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['LPUSHX', key], element);
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the LPUSHX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param elements - One or more elements to push to the list if it exists
+   * @see https://redis.io/commands/lpushx/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, elements: RedisVariadicArgument) {
+    parser.push('LPUSHX');
+    parser.pushKey(key);
+    parser.pushVariadic(elements);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LRANGE.spec.ts b/packages/client/lib/commands/LRANGE.spec.ts
index dffe6087b80..c0bb046d898 100644
--- a/packages/client/lib/commands/LRANGE.spec.ts
+++ b/packages/client/lib/commands/LRANGE.spec.ts
@@ -1,27 +1,23 @@
-
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LRANGE';
+import LRANGE from './LRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('LRANGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, -1),
-            ['LRANGE', 'key', '0', '-1']
-        );
-    });
-
-    testUtils.testWithClient('client.lRange', async client => {
-        assert.deepEqual(
-            await client.lRange('key', 0, -1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(LRANGE, 'key', 0, -1),
+      ['LRANGE', 'key', '0', '-1']
+    );
+  });
 
-    testUtils.testWithCluster('cluster.lRange', async cluster => {
-        assert.deepEqual(
-            await cluster.lRange('key', 0, -1),
-            []
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lRange', async client => {
+    assert.deepEqual(
+      await client.lRange('key', 0, -1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LRANGE.ts b/packages/client/lib/commands/LRANGE.ts
index df12c57d804..040bb6b4498 100644
--- a/packages/client/lib/commands/LRANGE.ts
+++ b/packages/client/lib/commands/LRANGE.ts
@@ -1,20 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    start: number,
-    stop: number
-): RedisCommandArguments {
-    return [
-        'LRANGE',
-        key,
-        start.toString(),
-        stop.toString()
-    ];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LRANGE command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param start - The starting index
+   * @param stop - The ending index
+   * @see https://redis.io/commands/lrange/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) {
+    parser.push('LRANGE');
+    parser.pushKey(key);
+    parser.push(start.toString(), stop.toString())
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LREM.spec.ts b/packages/client/lib/commands/LREM.spec.ts
index 3405f4beb07..2a36d8ee2f1 100644
--- a/packages/client/lib/commands/LREM.spec.ts
+++ b/packages/client/lib/commands/LREM.spec.ts
@@ -1,27 +1,23 @@
-
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LREM';
+import LREM from './LREM';
+import { parseArgs } from './generic-transformers';
 
 describe('LREM', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 'element'),
-            ['LREM', 'key', '0', 'element']
-        );
-    });
-
-    testUtils.testWithClient('client.lRem', async client => {
-        assert.equal(
-            await client.lRem('key', 0, 'element'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LREM, 'key', 0, 'element'),
+      ['LREM', 'key', '0', 'element']
+    );
+  });
 
-    testUtils.testWithCluster('cluster.lRem', async cluster => {
-        assert.equal(
-            await cluster.lRem('key', 0, 'element'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lRem', async client => {
+    assert.equal(
+      await client.lRem('key', 0, 'element'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LREM.ts b/packages/client/lib/commands/LREM.ts
index b4951334888..4e5de0fa78c 100644
--- a/packages/client/lib/commands/LREM.ts
+++ b/packages/client/lib/commands/LREM.ts
@@ -1,18 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number,
-    element: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'LREM',
-        key,
-        count.toString(),
-        element
-    ];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LREM command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param count - The count of elements to remove (negative: from tail to head, 0: all occurrences, positive: from head to tail)
+   * @param element - The element to remove
+   * @see https://redis.io/commands/lrem/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number, element: RedisArgument) {
+    parser.push('LREM');
+    parser.pushKey(key);
+    parser.push(count.toString());
+    parser.push(element);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LSET.spec.ts b/packages/client/lib/commands/LSET.spec.ts
index d7241032cc6..c7522942402 100644
--- a/packages/client/lib/commands/LSET.spec.ts
+++ b/packages/client/lib/commands/LSET.spec.ts
@@ -1,28 +1,24 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LSET';
+import LSET from './LSET';
+import { parseArgs } from './generic-transformers';
 
 describe('LSET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 'element'),
-            ['LSET', 'key', '0', 'element']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LSET, 'key', 0, 'element'),
+      ['LSET', 'key', '0', 'element']
+    );
+  });
 
-    testUtils.testWithClient('client.lSet', async client => {
-        await client.lPush('key', 'element');
-        assert.equal(
-            await client.lSet('key', 0, 'element'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lSet', async cluster => {
-        await cluster.lPush('key', 'element');
-        assert.equal(
-            await cluster.lSet('key', 0, 'element'),
-            'OK'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lSet', async client => {
+    await client.lPush('key', 'element');
+    assert.equal(
+      await client.lSet('key', 0, 'element'),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LSET.ts b/packages/client/lib/commands/LSET.ts
index 33c7b4cc060..052961a316e 100644
--- a/packages/client/lib/commands/LSET.ts
+++ b/packages/client/lib/commands/LSET.ts
@@ -1,18 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    index: number,
-    element: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'LSET',
-        key,
-        index.toString(),
-        element
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the LSET command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param index - The index of the element to replace
+   * @param element - The new value to set
+   * @see https://redis.io/commands/lset/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, index: number, element: RedisArgument) {
+    parser.push('LSET');
+    parser.pushKey(key);
+    parser.push(index.toString(), element);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/LTRIM.spec.ts b/packages/client/lib/commands/LTRIM.spec.ts
index 5b6ac5d3660..5b6d77c91de 100644
--- a/packages/client/lib/commands/LTRIM.spec.ts
+++ b/packages/client/lib/commands/LTRIM.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LTRIM';
+import LTRIM from './LTRIM';
+import { parseArgs } from './generic-transformers';
 
 describe('LTRIM', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, -1),
-            ['LTRIM', 'key', '0', '-1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(LTRIM, 'key', 0, -1),
+      ['LTRIM', 'key', '0', '-1']
+    );
+  });
 
-    testUtils.testWithClient('client.lTrim', async client => {
-        assert.equal(
-            await client.lTrim('key', 0, -1),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.lTrim', async cluster => {
-        assert.equal(
-            await cluster.lTrim('key', 0, -1),
-            'OK'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('lTrim', async client => {
+    assert.equal(
+      await client.lTrim('key', 0, -1),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/LTRIM.ts b/packages/client/lib/commands/LTRIM.ts
index 668497cdde6..31c2b66b5a9 100644
--- a/packages/client/lib/commands/LTRIM.ts
+++ b/packages/client/lib/commands/LTRIM.ts
@@ -1,18 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    start: number,
-    stop: number
-): RedisCommandArguments {
-    return [
-        'LTRIM',
-        key,
-        start.toString(),
-        stop.toString()
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  /**
+   * Constructs the LTRIM command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the list
+   * @param start - The starting index
+   * @param stop - The ending index
+   * @see https://redis.io/commands/ltrim/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) {
+    parser.push('LTRIM');
+    parser.pushKey(key);
+    parser.push(start.toString(), stop.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts
index ad97047606c..9d822f8e07e 100644
--- a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts
+++ b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MEMORY_DOCTOR';
+import MEMORY_DOCTOR from './MEMORY_DOCTOR';
+import { parseArgs } from './generic-transformers';
 
 describe('MEMORY DOCTOR', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['MEMORY', 'DOCTOR']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MEMORY_DOCTOR),
+      ['MEMORY', 'DOCTOR']
+    );
+  });
 
-    testUtils.testWithClient('client.memoryDoctor', async client => {
-        assert.equal(
-            typeof (await client.memoryDoctor()),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.memoryDoctor', async client => {
+    assert.equal(
+      typeof (await client.memoryDoctor()),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.ts b/packages/client/lib/commands/MEMORY_DOCTOR.ts
index 95a37246ffa..21e42ccc7ea 100644
--- a/packages/client/lib/commands/MEMORY_DOCTOR.ts
+++ b/packages/client/lib/commands/MEMORY_DOCTOR.ts
@@ -1,5 +1,17 @@
-export function transformArguments(): Array<string> {
-    return ['MEMORY', 'DOCTOR'];
-}
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MEMORY DOCTOR command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/memory-doctor/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('MEMORY', 'DOCTOR');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts
index ce866f1e116..a4a85f5b994 100644
--- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts
+++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MEMORY_MALLOC-STATS';
+import MEMORY_MALLOC_STATS from './MEMORY_MALLOC-STATS';
+import { parseArgs } from './generic-transformers';
 
 describe('MEMORY MALLOC-STATS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['MEMORY', 'MALLOC-STATS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MEMORY_MALLOC_STATS),
+      ['MEMORY', 'MALLOC-STATS']
+    );
+  });
 
-    testUtils.testWithClient('client.memoryMallocStats', async client => {
-        assert.equal(
-            typeof (await client.memoryDoctor()),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.memoryMallocStats', async client => {
+    assert.equal(
+      typeof (await client.memoryMallocStats()),
+      'string'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts
index 3977e3a1de4..69ad8c37a85 100644
--- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts
+++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts
@@ -1,5 +1,18 @@
-export function transformArguments(): Array<string> {
-    return ['MEMORY', 'MALLOC-STATS'];
-}
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MEMORY MALLOC-STATS command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/memory-malloc-stats/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('MEMORY', 'MALLOC-STATS');
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
 
-export declare function transformReply(): string;
diff --git a/packages/client/lib/commands/MEMORY_PURGE.spec.ts b/packages/client/lib/commands/MEMORY_PURGE.spec.ts
index 5d34331feb6..be5fb738b0a 100644
--- a/packages/client/lib/commands/MEMORY_PURGE.spec.ts
+++ b/packages/client/lib/commands/MEMORY_PURGE.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MEMORY_PURGE';
+import MEMORY_PURGE from './MEMORY_PURGE';
+import { parseArgs } from './generic-transformers';
 
 describe('MEMORY PURGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['MEMORY', 'PURGE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MEMORY_PURGE),
+      ['MEMORY', 'PURGE']
+    );
+  });
 
-    testUtils.testWithClient('client.memoryPurge', async client => {
-        assert.equal(
-            await client.memoryPurge(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.memoryPurge', async client => {
+    assert.equal(
+      await client.memoryPurge(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/MEMORY_PURGE.ts b/packages/client/lib/commands/MEMORY_PURGE.ts
index cfa38179273..39f837016ad 100644
--- a/packages/client/lib/commands/MEMORY_PURGE.ts
+++ b/packages/client/lib/commands/MEMORY_PURGE.ts
@@ -1,5 +1,18 @@
-export function transformArguments(): Array<string> {
-    return ['MEMORY', 'PURGE'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the MEMORY PURGE command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/memory-purge/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('MEMORY', 'PURGE');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-export declare function transformReply(): string;
diff --git a/packages/client/lib/commands/MEMORY_STATS.spec.ts b/packages/client/lib/commands/MEMORY_STATS.spec.ts
index 12aa21181e6..6aad05116af 100644
--- a/packages/client/lib/commands/MEMORY_STATS.spec.ts
+++ b/packages/client/lib/commands/MEMORY_STATS.spec.ts
@@ -1,108 +1,47 @@
-import { strict as assert } from 'assert';
-import { transformArguments, transformReply } from './MEMORY_STATS';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MEMORY_STATS from './MEMORY_STATS';
+import { parseArgs } from './generic-transformers';
 
 describe('MEMORY STATS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['MEMORY', 'STATS']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MEMORY_STATS),
+      ['MEMORY', 'STATS']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([
-                'peak.allocated',
-                952728,
-                'total.allocated',
-                892904,
-                'startup.allocated',
-                809952,
-                'replication.backlog',
-                0,
-                'clients.slaves',
-                0,
-                'clients.normal',
-                41000,
-                'aof.buffer',
-                0,
-                'lua.caches',
-                0,
-                'db.0',
-                [
-                    'overhead.hashtable.main',
-                    72,
-                    'overhead.hashtable.expires',
-                    0
-                ],
-                'overhead.total',
-                850952,
-                'keys.count',
-                0,
-                'keys.bytes-per-key',
-                0,
-                'dataset.bytes',
-                41952,
-                'dataset.percentage',
-                '50.573825836181641',
-                'peak.percentage',
-                '93.720771789550781',
-                'allocator.allocated',
-                937632,
-                'allocator.active',
-                1191936,
-                'allocator.resident',
-                4005888,
-                'allocator-fragmentation.ratio',
-                '1.2712193727493286',
-                'allocator-fragmentation.bytes',
-                254304,
-                'allocator-rss.ratio',
-                '3.3608248233795166',
-                'allocator-rss.bytes',
-                2813952,
-                'rss-overhead.ratio',
-                '2.4488751888275146',
-                'rss-overhead.bytes',
-                5804032,
-                'fragmentation',
-                '11.515504837036133',
-                'fragmentation.bytes',
-                8958032
-            ]),
-            {
-                peakAllocated: 952728,
-                totalAllocated: 892904,
-                startupAllocated: 809952,
-                replicationBacklog: 0,
-                clientsReplicas: 0,
-                clientsNormal: 41000,
-                aofBuffer: 0,
-                luaCaches: 0,
-                overheadTotal: 850952,
-                keysCount: 0,
-                keysBytesPerKey: 0,
-                datasetBytes: 41952,
-                datasetPercentage: 50.573825836181641,
-                peakPercentage: 93.720771789550781,
-                allocatorAllocated: 937632,
-                allocatorActive: 1191936,
-                allocatorResident: 4005888,
-                allocatorFragmentationRatio: 1.2712193727493286,
-                allocatorFragmentationBytes: 254304,
-                allocatorRssRatio: 3.3608248233795166,
-                allocatorRssBytes: 2813952,
-                rssOverheadRatio: 2.4488751888275146,
-                rssOverheadBytes: 5804032,
-                fragmentation: 11.515504837036133,
-                fragmentationBytes: 8958032,
-                db: {
-                    0: {
-                        overheadHashtableMain: 72,
-                        overheadHashtableExpires: 0
-                    }
-                }
-            }
-        );
-    });
+  testUtils.testWithClient('client.memoryStats', async client => {
+    const memoryStats = await client.memoryStats();
+    assert.equal(typeof memoryStats['peak.allocated'], 'number');
+    assert.equal(typeof memoryStats['total.allocated'], 'number');
+    assert.equal(typeof memoryStats['startup.allocated'], 'number');
+    assert.equal(typeof memoryStats['replication.backlog'], 'number');
+    assert.equal(typeof memoryStats['clients.slaves'], 'number');
+    assert.equal(typeof memoryStats['clients.normal'], 'number');
+    assert.equal(typeof memoryStats['aof.buffer'], 'number');
+    assert.equal(typeof memoryStats['lua.caches'], 'number');
+    assert.equal(typeof memoryStats['overhead.total'], 'number');
+    assert.equal(typeof memoryStats['keys.count'], 'number');
+    assert.equal(typeof memoryStats['keys.bytes-per-key'], 'number');
+    assert.equal(typeof memoryStats['dataset.bytes'], 'number');
+    assert.equal(typeof memoryStats['dataset.percentage'], 'number');
+    assert.equal(typeof memoryStats['peak.percentage'], 'number');
+    assert.equal(typeof memoryStats['allocator.allocated'], 'number');
+    assert.equal(typeof memoryStats['allocator.active'], 'number');
+    assert.equal(typeof memoryStats['allocator.resident'], 'number');
+    assert.equal(typeof memoryStats['allocator-fragmentation.ratio'], 'number', 'allocator-fragmentation.ratio');
+    assert.equal(typeof memoryStats['allocator-fragmentation.bytes'], 'number');
+    assert.equal(typeof memoryStats['allocator-rss.ratio'], 'number', 'allocator-rss.ratio');
+    assert.equal(typeof memoryStats['allocator-rss.bytes'], 'number');
+    assert.equal(typeof memoryStats['rss-overhead.ratio'], 'number', 'rss-overhead.ratio');
+    assert.equal(typeof memoryStats['rss-overhead.bytes'], 'number');
+    assert.equal(typeof memoryStats['fragmentation'], 'number', 'fragmentation');
+    assert.equal(typeof memoryStats['fragmentation.bytes'], 'number');
+    
+    if (testUtils.isVersionGreaterThan([7])) {
+      assert.equal(typeof memoryStats['cluster.links'], 'number');
+      assert.equal(typeof memoryStats['functions.caches'], 'number');
+    }
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/MEMORY_STATS.ts b/packages/client/lib/commands/MEMORY_STATS.ts
index 8ae83d2239a..9391a91613c 100644
--- a/packages/client/lib/commands/MEMORY_STATS.ts
+++ b/packages/client/lib/commands/MEMORY_STATS.ts
@@ -1,93 +1,75 @@
-export function transformArguments(): Array<string> {
-    return ['MEMORY', 'STATS'];
-}
+import { CommandParser } from '../client/parser';
+import { TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; 
+import { transformDoubleReply } from './generic-transformers';
 
-interface MemoryStatsReply {
-    peakAllocated: number;
-    totalAllocated: number;
-    startupAllocated: number;
-    replicationBacklog: number;
-    clientsReplicas: number;
-    clientsNormal: number;
-    aofBuffer: number;
-    luaCaches: number;
-    overheadTotal: number;
-    keysCount: number;
-    keysBytesPerKey: number;
-    datasetBytes: number;
-    datasetPercentage: number;
-    peakPercentage: number;
-    allocatorAllocated?: number,
-    allocatorActive?: number;
-    allocatorResident?: number;
-    allocatorFragmentationRatio?: number;
-    allocatorFragmentationBytes?: number;
-    allocatorRssRatio?: number;
-    allocatorRssBytes?: number;
-    rssOverheadRatio?: number;
-    rssOverheadBytes?: number;
-    fragmentation?: number;
-    fragmentationBytes: number;
-    db: {
-        [key: number]: {
-            overheadHashtableMain: number;
-            overheadHashtableExpires: number;
-        };
-    };
-}
+export type MemoryStatsReply = TuplesToMapReply<[
+  [BlobStringReply<'peak.allocated'>, NumberReply],
+  [BlobStringReply<'total.allocated'>, NumberReply],
+  [BlobStringReply<'startup.allocated'>, NumberReply],
+  [BlobStringReply<'replication.backlog'>, NumberReply],
+  [BlobStringReply<'clients.slaves'>, NumberReply],
+  [BlobStringReply<'clients.normal'>, NumberReply],
+  /** added in 7.0 */
+  [BlobStringReply<'cluster.links'>, NumberReply],
+  [BlobStringReply<'aof.buffer'>, NumberReply],
+  [BlobStringReply<'lua.caches'>, NumberReply],
+  /** added in 7.0 */
+  [BlobStringReply<'functions.caches'>, NumberReply],
+  // FIXME: 'db.0', and perhaps others' is here and is a map that should be handled?
+  [BlobStringReply<'overhead.total'>, NumberReply],
+  [BlobStringReply<'keys.count'>, NumberReply],
+  [BlobStringReply<'keys.bytes-per-key'>, NumberReply],
+  [BlobStringReply<'dataset.bytes'>, NumberReply],
+  [BlobStringReply<'dataset.percentage'>, DoubleReply],
+  [BlobStringReply<'peak.percentage'>, DoubleReply],
+  [BlobStringReply<'allocator.allocated'>, NumberReply],
+  [BlobStringReply<'allocator.active'>, NumberReply],
+  [BlobStringReply<'allocator.resident'>, NumberReply],
+  [BlobStringReply<'allocator-fragmentation.ratio'>, DoubleReply],
+  [BlobStringReply<'allocator-fragmentation.bytes'>, NumberReply],
+  [BlobStringReply<'allocator-rss.ratio'>, DoubleReply],
+  [BlobStringReply<'allocator-rss.bytes'>, NumberReply],
+  [BlobStringReply<'rss-overhead.ratio'>, DoubleReply],
+  [BlobStringReply<'rss-overhead.bytes'>, NumberReply],
+  [BlobStringReply<'fragmentation'>, DoubleReply],
+  [BlobStringReply<'fragmentation.bytes'>, NumberReply]
+]>;
 
-const FIELDS_MAPPING = {
-        'peak.allocated': 'peakAllocated',
-        'total.allocated': 'totalAllocated',
-        'startup.allocated': 'startupAllocated',
-        'replication.backlog': 'replicationBacklog',
-        'clients.slaves': 'clientsReplicas',
-        'clients.normal': 'clientsNormal',
-        'aof.buffer': 'aofBuffer',
-        'lua.caches': 'luaCaches',
-        'overhead.total': 'overheadTotal',
-        'keys.count': 'keysCount',
-        'keys.bytes-per-key': 'keysBytesPerKey',
-        'dataset.bytes': 'datasetBytes',
-        'dataset.percentage': 'datasetPercentage',
-        'peak.percentage': 'peakPercentage',
-        'allocator.allocated': 'allocatorAllocated',
-        'allocator.active': 'allocatorActive',
-        'allocator.resident': 'allocatorResident',
-        'allocator-fragmentation.ratio': 'allocatorFragmentationRatio',
-        'allocator-fragmentation.bytes': 'allocatorFragmentationBytes',
-        'allocator-rss.ratio': 'allocatorRssRatio',
-        'allocator-rss.bytes': 'allocatorRssBytes',
-        'rss-overhead.ratio': 'rssOverheadRatio',
-        'rss-overhead.bytes': 'rssOverheadBytes',
-        'fragmentation': 'fragmentation',
-        'fragmentation.bytes': 'fragmentationBytes'
-    },
-    DB_FIELDS_MAPPING = {
-        'overhead.hashtable.main': 'overheadHashtableMain',
-        'overhead.hashtable.expires': 'overheadHashtableExpires'
-    };
-
-export function transformReply(rawReply: Array<string | number | Array<string | number>>): MemoryStatsReply {
-    const reply: any = {
-        db: {}
-    };
-
-    for (let i = 0; i < rawReply.length; i += 2) {
-        const key = rawReply[i] as string;
-        if (key.startsWith('db.')) {
-            const dbTuples = rawReply[i + 1] as Array<string | number>,
-                db: any = {};
-            for (let j = 0; j < dbTuples.length; j += 2) {
-                db[DB_FIELDS_MAPPING[dbTuples[j] as keyof typeof DB_FIELDS_MAPPING]] = dbTuples[j + 1];
-            }
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MEMORY STATS command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/memory-stats/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('MEMORY', 'STATS');
+  },
+  transformReply: {
+    2: (rawReply: UnwrapReply<ArrayReply<BlobStringReply | NumberReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      const reply: any = {};
 
-            reply.db[key.substring(3)] = db;
-            continue;
+      let i = 0;
+      while (i < rawReply.length) {
+        switch(rawReply[i].toString()) {
+          case 'dataset.percentage':
+          case 'peak.percentage':
+          case 'allocator-fragmentation.ratio':
+          case 'allocator-rss.ratio':
+          case 'rss-overhead.ratio':
+          case 'fragmentation':
+            reply[rawReply[i++] as any] = transformDoubleReply[2](rawReply[i++] as unknown as BlobStringReply, preserve, typeMapping);
+            break;
+          default:
+            reply[rawReply[i++] as any] = rawReply[i++];
         }
+        
+      }
 
-        reply[FIELDS_MAPPING[key as keyof typeof FIELDS_MAPPING]] = Number(rawReply[i + 1]);
-    }
-
-    return reply as MemoryStatsReply;
-}
+      return reply as MemoryStatsReply;
+    },
+    3: undefined as unknown as () => MemoryStatsReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MEMORY_USAGE.spec.ts b/packages/client/lib/commands/MEMORY_USAGE.spec.ts
index fe5ff404d93..edf673564ee 100644
--- a/packages/client/lib/commands/MEMORY_USAGE.spec.ts
+++ b/packages/client/lib/commands/MEMORY_USAGE.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MEMORY_USAGE';
+import MEMORY_USAGE from './MEMORY_USAGE';
+import { parseArgs } from './generic-transformers';
 
 describe('MEMORY USAGE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['MEMORY', 'USAGE', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(MEMORY_USAGE, 'key'),
+        ['MEMORY', 'USAGE', 'key']
+      );
+    });
 
-        it('with SAMPLES', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    SAMPLES: 1
-                }),
-                ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1']
-            );
-        });
+    it('with SAMPLES', () => {
+      assert.deepEqual(
+        parseArgs(MEMORY_USAGE, 'key', {
+          SAMPLES: 1
+        }),
+        ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.memoryUsage', async client => {
-        assert.equal(
-            await client.memoryUsage('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.memoryUsage', async client => {
+    assert.equal(
+      await client.memoryUsage('key'),
+      null
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/MEMORY_USAGE.ts b/packages/client/lib/commands/MEMORY_USAGE.ts
index 959cdb0a0c4..a1fa79f6210 100644
--- a/packages/client/lib/commands/MEMORY_USAGE.ts
+++ b/packages/client/lib/commands/MEMORY_USAGE.ts
@@ -1,19 +1,27 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-interface MemoryUsageOptions {
-    SAMPLES?: number;
+export interface MemoryUsageOptions {
+  SAMPLES?: number;
 }
 
-export function transformArguments(key: string, options?: MemoryUsageOptions): Array<string> {
-    const args = ['MEMORY', 'USAGE', key];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MEMORY USAGE command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get memory usage for
+   * @param options - Optional parameters including SAMPLES
+   * @see https://redis.io/commands/memory-usage/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: MemoryUsageOptions) {
+    parser.push('MEMORY', 'USAGE');
+    parser.pushKey(key);
 
     if (options?.SAMPLES) {
-        args.push('SAMPLES', options.SAMPLES.toString());
+      parser.push('SAMPLES', options.SAMPLES.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): number | null;
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MGET.spec.ts b/packages/client/lib/commands/MGET.spec.ts
index 9ff47895f4e..048fa6f0a58 100644
--- a/packages/client/lib/commands/MGET.spec.ts
+++ b/packages/client/lib/commands/MGET.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MGET';
+import MGET from './MGET';
+import { parseArgs } from './generic-transformers';
 
 describe('MGET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(['1', '2']),
-            ['MGET', '1', '2']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(MGET, ['1', '2']),
+      ['MGET', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.mGet', async client => {
-        assert.deepEqual(
-            await client.mGet(['key']),
-            [null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.mGet', async cluster => {
-        assert.deepEqual(
-            await cluster.mGet(['key']),
-            [null]
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('mGet', async client => {
+    assert.deepEqual(
+      await client.mGet(['key']),
+      [null]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/MGET.ts b/packages/client/lib/commands/MGET.ts
index 6635a2ca20c..22145dd3485 100644
--- a/packages/client/lib/commands/MGET.ts
+++ b/packages/client/lib/commands/MGET.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    keys: Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return ['MGET', ...keys];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument | null>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MGET command
+   * 
+   * @param parser - The command parser
+   * @param keys - Array of keys to get
+   * @see https://redis.io/commands/mget/
+   */
+  parseCommand(parser: CommandParser, keys: Array<RedisArgument>) {
+    parser.push('MGET');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => Array<BlobStringReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MIGRATE.spec.ts b/packages/client/lib/commands/MIGRATE.spec.ts
index ca7ceb48b39..dd2fbdc82ff 100644
--- a/packages/client/lib/commands/MIGRATE.spec.ts
+++ b/packages/client/lib/commands/MIGRATE.spec.ts
@@ -1,76 +1,77 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './MIGRATE';
+import { strict as assert } from 'node:assert';
+import MIGRATE from './MIGRATE';
+import { parseArgs } from './generic-transformers';
 
 describe('MIGRATE', () => {
-    describe('transformArguments', () => {
-        it('single key', () => {
-            assert.deepEqual(
-                transformArguments('127.0.0.1', 6379, 'key', 0, 10),
-                ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single key', () => {
+      assert.deepEqual(
+        parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10),
+        ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10']
+      );
+    });
+
+    it('multiple keys', () => {
+      assert.deepEqual(
+        parseArgs(MIGRATE, '127.0.0.1', 6379, ['1', '2'], 0, 10),
+        ['MIGRATE', '127.0.0.1', '6379', '', '0', '10', 'KEYS', '1', '2']
+      );
+    });
 
-        it('multiple keys', () => {
-            assert.deepEqual(
-                transformArguments('127.0.0.1', 6379, ['1', '2'], 0, 10),
-                ['MIGRATE', '127.0.0.1', '6379', '', '0', '10', 'KEYS', '1', '2']
-            );
-        });
+    it('with COPY', () => {
+      assert.deepEqual(
+        parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, {
+          COPY: true
+        }),
+        ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY']
+      );
+    });
 
-        it('with COPY', () => {
-            assert.deepEqual(
-                transformArguments('127.0.0.1', 6379, 'key', 0, 10, {
-                    COPY: true
-                }),
-                ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY']
-            );
-        });
+    it('with REPLACE', () => {
+      assert.deepEqual(
+        parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, {
+          REPLACE: true
+        }),
+        ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'REPLACE']
+      );
+    });
 
-        it('with REPLACE', () => {
-            assert.deepEqual(
-                transformArguments('127.0.0.1', 6379, 'key', 0, 10, {
-                    REPLACE: true
-                }),
-                ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'REPLACE']
-            );
-        });
-        
-        describe('with AUTH', () => {
-            it('password only', () => {
-                assert.deepEqual(
-                    transformArguments('127.0.0.1', 6379, 'key', 0, 10, {
-                        AUTH: {
-                            password: 'password'
-                        }
-                    }),
-                    ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'AUTH', 'password']
-                );
-            });
+    describe('with AUTH', () => {
+      it('password only', () => {
+        assert.deepEqual(
+          parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, {
+            AUTH: {
+              password: 'password'
+            }
+          }),
+          ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'AUTH', 'password']
+        );
+      });
 
-            it('username & password', () => {
-                assert.deepEqual(
-                    transformArguments('127.0.0.1', 6379, 'key', 0, 10, {
-                        AUTH: {
-                            username: 'username',
-                            password: 'password'
-                        }
-                    }),
-                    ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'AUTH2', 'username', 'password']
-                );
-            });
-        });
+      it('username & password', () => {
+        assert.deepEqual(
+          parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, {
+            AUTH: {
+              username: 'username',
+              password: 'password'
+            }
+          }),
+          ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'AUTH2', 'username', 'password']
+        );
+      });
+    });
 
-        it('with COPY, REPLACE, AUTH', () => {
-            assert.deepEqual(
-                transformArguments('127.0.0.1', 6379, 'key', 0, 10, {
-                    COPY: true,
-                    REPLACE: true,
-                    AUTH: {
-                        password: 'password'
-                    }
-                }),
-                ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY', 'REPLACE', 'AUTH', 'password']
-            );
-        });
+    it('with COPY, REPLACE, AUTH', () => {
+      assert.deepEqual(
+        parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, {
+          COPY: true,
+          REPLACE: true,
+          AUTH: {
+            password: 'password'
+          }
+        }),
+        ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY', 'REPLACE', 'AUTH', 'password']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/MIGRATE.ts b/packages/client/lib/commands/MIGRATE.ts
index aaff3164081..ba798e331ab 100644
--- a/packages/client/lib/commands/MIGRATE.ts
+++ b/packages/client/lib/commands/MIGRATE.ts
@@ -1,65 +1,77 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 import { AuthOptions } from './AUTH';
 
-interface MigrateOptions {
-    COPY?: true;
-    REPLACE?: true;
-    AUTH?: AuthOptions;
+export interface MigrateOptions {
+  COPY?: true;
+  REPLACE?: true;
+  AUTH?: AuthOptions;
 }
 
-export function transformArguments(
-    host: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the MIGRATE command
+   * 
+   * @param parser - The command parser
+   * @param host - Target Redis instance host
+   * @param port - Target Redis instance port
+   * @param key - Key or keys to migrate
+   * @param destinationDb - Target database index
+   * @param timeout - Timeout in milliseconds
+   * @param options - Optional parameters including COPY, REPLACE, and AUTH
+   * @see https://redis.io/commands/migrate/
+   */
+  parseCommand(
+    parser: CommandParser,
+    host: RedisArgument,
     port: number,
-    key: RedisCommandArgument | Array<RedisCommandArgument>,
+    key: RedisArgument | Array<RedisArgument>,
     destinationDb: number,
     timeout: number,
     options?: MigrateOptions
-): RedisCommandArguments {
-    const args = ['MIGRATE', host, port.toString()],
-        isKeyArray = Array.isArray(key);
-
+  ) {
+    parser.push('MIGRATE', host, port.toString());
+    const isKeyArray = Array.isArray(key);
+  
     if (isKeyArray) {
-        args.push('');
+      parser.push('');
     } else {
-        args.push(key);
+      parser.push(key);
     }
-
-    args.push(
-        destinationDb.toString(),
-        timeout.toString()
+  
+    parser.push(
+      destinationDb.toString(),
+      timeout.toString()
     );
-
+  
     if (options?.COPY) {
-        args.push('COPY');
+      parser.push('COPY');
     }
-
+  
     if (options?.REPLACE) {
-        args.push('REPLACE');
+      parser.push('REPLACE');
     }
-
+  
     if (options?.AUTH) {
-        if (options.AUTH.username) {
-            args.push(
-                'AUTH2',
-                options.AUTH.username,
-                options.AUTH.password
-            );
-        } else {
-            args.push(
-                'AUTH',
-                options.AUTH.password
-            );
-        }
+      if (options.AUTH.username) {
+        parser.push(
+          'AUTH2',
+          options.AUTH.username,
+          options.AUTH.password
+        );
+      } else {
+        parser.push(
+          'AUTH',
+          options.AUTH.password
+        );
+      }
     }
-
+  
     if (isKeyArray) {
-        args.push(
-            'KEYS',
-            ...key
-        );
+      parser.push('KEYS');
+      parser.pushVariadic(key);
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MODULE_LIST.spec.ts b/packages/client/lib/commands/MODULE_LIST.spec.ts
index eeeb774ebff..0aab973cf21 100644
--- a/packages/client/lib/commands/MODULE_LIST.spec.ts
+++ b/packages/client/lib/commands/MODULE_LIST.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './MODULE_LIST';
+import { strict as assert } from 'node:assert';
+import MODULE_LIST from './MODULE_LIST';
+import { parseArgs } from './generic-transformers';
 
 describe('MODULE LIST', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['MODULE', 'LIST']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MODULE_LIST),
+      ['MODULE', 'LIST']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/MODULE_LIST.ts b/packages/client/lib/commands/MODULE_LIST.ts
index d75b2428308..8183c419a66 100644
--- a/packages/client/lib/commands/MODULE_LIST.ts
+++ b/packages/client/lib/commands/MODULE_LIST.ts
@@ -1,5 +1,33 @@
-export function transformArguments(): Array<string> {
-    return ['MODULE', 'LIST'];
-}
+import { CommandParser } from '../client/parser';
+import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export type ModuleListReply = ArrayReply<TuplesToMapReply<[
+  [BlobStringReply<'name'>, BlobStringReply],
+  [BlobStringReply<'ver'>, NumberReply],
+]>>;
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MODULE LIST command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/module-list/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('MODULE', 'LIST');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<ModuleListReply>>) => {
+      return reply.map(module => {
+        const unwrapped = module as unknown as UnwrapReply<typeof module>;
+        return {
+          name: unwrapped[1],
+          ver: unwrapped[3]
+        };
+      });
+    },
+    3: undefined as unknown as () => ModuleListReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MODULE_LOAD.spec.ts b/packages/client/lib/commands/MODULE_LOAD.spec.ts
index 5a99a232ca4..418dd9b5daf 100644
--- a/packages/client/lib/commands/MODULE_LOAD.spec.ts
+++ b/packages/client/lib/commands/MODULE_LOAD.spec.ts
@@ -1,20 +1,21 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './MODULE_LOAD';
+import { strict as assert } from 'node:assert';
+import MODULE_LOAD from './MODULE_LOAD';
+import { parseArgs } from './generic-transformers';
 
 describe('MODULE LOAD', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('path'),
-                ['MODULE', 'LOAD', 'path']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(MODULE_LOAD, 'path'),
+        ['MODULE', 'LOAD', 'path']
+      );
+    });
 
-        it('with module args', () => {
-            assert.deepEqual(
-                transformArguments('path', ['1', '2']),
-                ['MODULE', 'LOAD', 'path', '1', '2']
-            );
-        });
+    it('with module args', () => {
+      assert.deepEqual(
+        parseArgs(MODULE_LOAD, 'path', ['1', '2']),
+        ['MODULE', 'LOAD', 'path', '1', '2']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/MODULE_LOAD.ts b/packages/client/lib/commands/MODULE_LOAD.ts
index b44b4b57ce6..9dd6176ebaa 100644
--- a/packages/client/lib/commands/MODULE_LOAD.ts
+++ b/packages/client/lib/commands/MODULE_LOAD.ts
@@ -1,11 +1,23 @@
-export function transformArguments(path: string, moduleArgs?: Array<string>): Array<string> {
-    const args = ['MODULE', 'LOAD', path];
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-    if (moduleArgs) {
-        args.push(...moduleArgs);
-    }
-
-    return args;
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MODULE LOAD command
+   * 
+   * @param parser - The command parser
+   * @param path - Path to the module file
+   * @param moduleArguments - Optional arguments to pass to the module
+   * @see https://redis.io/commands/module-load/
+   */
+  parseCommand(parser: CommandParser, path: RedisArgument, moduleArguments?: Array<RedisArgument>) {
+    parser.push('MODULE', 'LOAD', path);
 
-export declare function transformReply(): string;
+    if (moduleArguments) {
+      parser.push(...moduleArguments);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts
index d8af96c54f1..581f41e03c8 100644
--- a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts
+++ b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './MODULE_UNLOAD';
+import { strict as assert } from 'node:assert';
+import MODULE_UNLOAD from './MODULE_UNLOAD';
+import { parseArgs } from './generic-transformers';
 
 describe('MODULE UNLOAD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('name'),
-            ['MODULE', 'UNLOAD', 'name']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MODULE_UNLOAD, 'name'),
+      ['MODULE', 'UNLOAD', 'name']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/MODULE_UNLOAD.ts b/packages/client/lib/commands/MODULE_UNLOAD.ts
index d5927778fe6..6d19b2b2a73 100644
--- a/packages/client/lib/commands/MODULE_UNLOAD.ts
+++ b/packages/client/lib/commands/MODULE_UNLOAD.ts
@@ -1,5 +1,18 @@
-export function transformArguments(name: string): Array<string> {
-    return ['MODULE', 'UNLOAD', name];
-}
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MODULE UNLOAD command
+   * 
+   * @param parser - The command parser
+   * @param name - The name of the module to unload
+   * @see https://redis.io/commands/module-unload/
+   */
+  parseCommand(parser: CommandParser, name: RedisArgument) {
+    parser.push('MODULE', 'UNLOAD', name);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MOVE.spec.ts b/packages/client/lib/commands/MOVE.spec.ts
index f7fdc481cbf..91a01378b22 100644
--- a/packages/client/lib/commands/MOVE.spec.ts
+++ b/packages/client/lib/commands/MOVE.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MOVE';
+import MOVE from './MOVE';
+import { parseArgs } from './generic-transformers';
 
 describe('MOVE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['MOVE', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MOVE, 'key', 1),
+      ['MOVE', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.move', async client => {
-        assert.equal(
-            await client.move('key', 1),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.move', async client => {
+    assert.equal(
+      await client.move('key', 1),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/MOVE.ts b/packages/client/lib/commands/MOVE.ts
index 17cc6742c5f..0c08a6fa100 100644
--- a/packages/client/lib/commands/MOVE.ts
+++ b/packages/client/lib/commands/MOVE.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(key: string, db: number): Array<string> {
-    return ['MOVE', key, db.toString()];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  /**
+   * Constructs the MOVE command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to move
+   * @param db - The destination database index
+   * @see https://redis.io/commands/move/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, db: number) {
+    parser.push('MOVE');
+    parser.pushKey(key);
+    parser.push(db.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MSET.spec.ts b/packages/client/lib/commands/MSET.spec.ts
index 0568f38487e..cfb14eceb05 100644
--- a/packages/client/lib/commands/MSET.spec.ts
+++ b/packages/client/lib/commands/MSET.spec.ts
@@ -1,42 +1,39 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MSET';
+import MSET from './MSET';
+import { parseArgs } from './generic-transformers';
 
 describe('MSET', () => {
-    describe('transformArguments', () => {
-        it("['key1', 'value1', 'key2', 'value2']", () => {
-            assert.deepEqual(
-                transformArguments(['key1', 'value1', 'key2', 'value2']),
-                ['MSET', 'key1', 'value1', 'key2', 'value2']
-            );
-        });
-
-        it("[['key1', 'value1'], ['key2', 'value2']]", () => {
-            assert.deepEqual(
-                transformArguments([['key1', 'value1'], ['key2', 'value2']]),
-                ['MSET', 'key1', 'value1', 'key2', 'value2']
-            );
-        });
+  describe('transformArguments', () => {
+    it("['key1', 'value1', 'key2', 'value2']", () => {
+      assert.deepEqual(
+        parseArgs(MSET, ['key1', 'value1', 'key2', 'value2']),
+        ['MSET', 'key1', 'value1', 'key2', 'value2']
+      );
+    });
 
-        it("{key1: 'value1'. key2: 'value2'}", () => {
-            assert.deepEqual(
-                transformArguments({ key1: 'value1', key2: 'value2' }),
-                ['MSET', 'key1', 'value1', 'key2', 'value2']
-            );
-        });
+    it("[['key1', 'value1'], ['key2', 'value2']]", () => {
+      assert.deepEqual(
+        parseArgs(MSET, [['key1', 'value1'], ['key2', 'value2']]),
+        ['MSET', 'key1', 'value1', 'key2', 'value2']
+      );
     });
 
-    testUtils.testWithClient('client.mSet', async client => {
-        assert.equal(
-            await client.mSet(['key1', 'value1', 'key2', 'value2']),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it("{key1: 'value1'. key2: 'value2'}", () => {
+      assert.deepEqual(
+        parseArgs(MSET, { key1: 'value1', key2: 'value2' }),
+        ['MSET', 'key1', 'value1', 'key2', 'value2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.mSet', async cluster => {
-        assert.equal(
-            await cluster.mSet(['{key}1', 'value1', '{key}2', 'value2']),
-            'OK'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('mSet', async client => {
+    assert.equal(
+      await client.mSet(['{tag}key1', 'value1', '{tag}key2', 'value2']),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/MSET.ts b/packages/client/lib/commands/MSET.ts
index bd7111659d1..ab734bae5c7 100644
--- a/packages/client/lib/commands/MSET.ts
+++ b/packages/client/lib/commands/MSET.ts
@@ -1,24 +1,48 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
 export type MSetArguments =
-    Array<[RedisCommandArgument, RedisCommandArgument]> |
-    Array<RedisCommandArgument> |
-    Record<string, RedisCommandArgument>;
-
-export function transformArguments(toSet: MSetArguments): RedisCommandArguments {
-    const args: RedisCommandArguments = ['MSET'];
+  Array<[RedisArgument, RedisArgument]> |
+  Array<RedisArgument> |
+  Record<string, RedisArgument>;
 
-    if (Array.isArray(toSet)) {
-        args.push(...toSet.flat());
+export function parseMSetArguments(parser: CommandParser, toSet: MSetArguments) {
+  if (Array.isArray(toSet)) {
+    if (toSet.length == 0) {
+      throw new Error("empty toSet Argument")
+    }
+    if (Array.isArray(toSet[0])) {
+      for (const tuple of (toSet as Array<[RedisArgument, RedisArgument]>)) {
+        parser.pushKey(tuple[0]);
+        parser.push(tuple[1]);
+      }
     } else {
-        for (const key of Object.keys(toSet)) {
-            args.push(key, toSet[key]);
-        }
+      const arr = toSet as Array<RedisArgument>;
+      for (let i=0; i < arr.length; i += 2) {
+        parser.pushKey(arr[i]);
+        parser.push(arr[i+1]);
+      }
     }
-
-    return args;
+  } else {
+    for (const tuple of Object.entries(toSet)) {
+      parser.pushKey(tuple[0]);
+      parser.push(tuple[1]);
+    }
+  }
 }
 
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MSET command
+   * 
+   * @param parser - The command parser
+   * @param toSet - Key-value pairs to set (array of tuples, flat array, or object)
+   * @see https://redis.io/commands/mset/
+   */
+  parseCommand(parser: CommandParser, toSet: MSetArguments) {
+    parser.push('MSET');
+    return parseMSetArguments(parser, toSet);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/MSETNX.spec.ts b/packages/client/lib/commands/MSETNX.spec.ts
index 854a9affd8a..0a9f636abc7 100644
--- a/packages/client/lib/commands/MSETNX.spec.ts
+++ b/packages/client/lib/commands/MSETNX.spec.ts
@@ -1,42 +1,39 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MSETNX';
+import MSETNX from './MSETNX';
+import { parseArgs } from './generic-transformers';
 
 describe('MSETNX', () => {
-    describe('transformArguments', () => {
-        it("['key1', 'value1', 'key2', 'value2']", () => {
-            assert.deepEqual(
-                transformArguments(['key1', 'value1', 'key2', 'value2']),
-                ['MSETNX', 'key1', 'value1', 'key2', 'value2']
-            );
-        });
-
-        it("[['key1', 'value1'], ['key2', 'value2']]", () => {
-            assert.deepEqual(
-                transformArguments([['key1', 'value1'], ['key2', 'value2']]),
-                ['MSETNX', 'key1', 'value1', 'key2', 'value2']
-            );
-        });
+  describe('transformArguments', () => {
+    it("['key1', 'value1', 'key2', 'value2']", () => {
+      assert.deepEqual(
+        parseArgs(MSETNX, ['key1', 'value1', 'key2', 'value2']),
+        ['MSETNX', 'key1', 'value1', 'key2', 'value2']
+      );
+    });
 
-        it("{key1: 'value1'. key2: 'value2'}", () => {
-            assert.deepEqual(
-                transformArguments({ key1: 'value1', key2: 'value2' }),
-                ['MSETNX', 'key1', 'value1', 'key2', 'value2']
-            );
-        });
+    it("[['key1', 'value1'], ['key2', 'value2']]", () => {
+      assert.deepEqual(
+        parseArgs(MSETNX, [['key1', 'value1'], ['key2', 'value2']]),
+        ['MSETNX', 'key1', 'value1', 'key2', 'value2']
+      );
     });
 
-    testUtils.testWithClient('client.mSetNX', async client => {
-        assert.equal(
-            await client.mSetNX(['key1', 'value1', 'key2', 'value2']),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it("{key1: 'value1'. key2: 'value2'}", () => {
+      assert.deepEqual(
+        parseArgs(MSETNX, { key1: 'value1', key2: 'value2' }),
+        ['MSETNX', 'key1', 'value1', 'key2', 'value2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.mSetNX', async cluster => {
-        assert.equal(
-            await cluster.mSetNX(['{key}1', 'value1', '{key}2', 'value2']),
-            true
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('mSetNX', async client => {
+    assert.equal(
+      await client.mSetNX(['{key}1', 'value1', '{key}2', 'value2']),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/MSETNX.ts b/packages/client/lib/commands/MSETNX.ts
index 0ef33936114..9a2186023f6 100644
--- a/packages/client/lib/commands/MSETNX.ts
+++ b/packages/client/lib/commands/MSETNX.ts
@@ -1,20 +1,19 @@
-import { RedisCommandArguments } from '.';
-import { MSetArguments } from './MSET';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+import { MSetArguments, parseMSetArguments } from './MSET';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(toSet: MSetArguments): RedisCommandArguments {
-    const args: RedisCommandArguments = ['MSETNX'];
-
-    if (Array.isArray(toSet)) {
-        args.push(...toSet.flat());
-    } else {
-        for (const key of Object.keys(toSet)) {
-            args.push(key, toSet[key]);
-        }
-    }
-
-    return args;
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the MSETNX command
+   * 
+   * @param parser - The command parser
+   * @param toSet - Key-value pairs to set if none of the keys exist (array of tuples, flat array, or object)
+   * @see https://redis.io/commands/msetnx/
+   */
+  parseCommand(parser: CommandParser, toSet: MSetArguments) {
+    parser.push('MSETNX');
+    return parseMSetArguments(parser, toSet);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts
index 6f42969d547..34f82be9b8d 100644
--- a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts
+++ b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './OBJECT_ENCODING';
+import OBJECT_ENCODING from './OBJECT_ENCODING';
+import { parseArgs } from './generic-transformers';
 
 describe('OBJECT ENCODING', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['OBJECT', 'ENCODING', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(OBJECT_ENCODING, 'key'),
+      ['OBJECT', 'ENCODING', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.objectEncoding', async client => {
-        assert.equal(
-            await client.objectEncoding('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('objectEncoding', async client => {
+    assert.equal(
+      await client.objectEncoding('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/OBJECT_ENCODING.ts b/packages/client/lib/commands/OBJECT_ENCODING.ts
index ac219ae89ed..2c0f6b41bbc 100644
--- a/packages/client/lib/commands/OBJECT_ENCODING.ts
+++ b/packages/client/lib/commands/OBJECT_ENCODING.ts
@@ -1,11 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['OBJECT', 'ENCODING', key];
-}
-
-export declare function transformReply(): string | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the OBJECT ENCODING command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get the internal encoding for
+   * @see https://redis.io/commands/object-encoding/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('OBJECT', 'ENCODING');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/OBJECT_FREQ.spec.ts b/packages/client/lib/commands/OBJECT_FREQ.spec.ts
index 6d2513cf18c..081501b12e6 100644
--- a/packages/client/lib/commands/OBJECT_FREQ.spec.ts
+++ b/packages/client/lib/commands/OBJECT_FREQ.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './OBJECT_FREQ';
+import OBJECT_FREQ from './OBJECT_FREQ';
+import { parseArgs } from './generic-transformers';
 
 describe('OBJECT FREQ', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['OBJECT', 'FREQ', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(OBJECT_FREQ, 'key'),
+      ['OBJECT', 'FREQ', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.objectFreq', async client => {
-        assert.equal(
-            await client.objectFreq('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('client.objectFreq', async client => {
+    assert.equal(
+      await client.objectFreq('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/OBJECT_FREQ.ts b/packages/client/lib/commands/OBJECT_FREQ.ts
index 071d16f2748..42a310a97c5 100644
--- a/packages/client/lib/commands/OBJECT_FREQ.ts
+++ b/packages/client/lib/commands/OBJECT_FREQ.ts
@@ -1,11 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['OBJECT', 'FREQ', key];
-}
-
-export declare function transformReply(): number | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the OBJECT FREQ command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get the access frequency for
+   * @see https://redis.io/commands/object-freq/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('OBJECT', 'FREQ');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts
index 61529e1366b..30d47b8133f 100644
--- a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts
+++ b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './OBJECT_IDLETIME';
+import OBJECT_IDLETIME from './OBJECT_IDLETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('OBJECT IDLETIME', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['OBJECT', 'IDLETIME', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(OBJECT_IDLETIME, 'key'),
+      ['OBJECT', 'IDLETIME', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.objectIdleTime', async client => {
-        assert.equal(
-            await client.objectIdleTime('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('client.objectIdleTime', async client => {
+    assert.equal(
+      await client.objectIdleTime('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.ts b/packages/client/lib/commands/OBJECT_IDLETIME.ts
index 38847d6f4cf..2d4afeda65a 100644
--- a/packages/client/lib/commands/OBJECT_IDLETIME.ts
+++ b/packages/client/lib/commands/OBJECT_IDLETIME.ts
@@ -1,11 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['OBJECT', 'IDLETIME', key];
-}
-
-export declare function transformReply(): number | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the OBJECT IDLETIME command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get the idle time for
+   * @see https://redis.io/commands/object-idletime/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('OBJECT', 'IDLETIME');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts
index 199dca3fe82..8bac08a2e5b 100644
--- a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts
+++ b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './OBJECT_REFCOUNT';
+import OBJECT_REFCOUNT from './OBJECT_REFCOUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('OBJECT REFCOUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['OBJECT', 'REFCOUNT', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(OBJECT_REFCOUNT, 'key'),
+      ['OBJECT', 'REFCOUNT', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.objectRefCount', async client => {
-        assert.equal(
-            await client.objectRefCount('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('client.objectRefCount', async client => {
+    assert.equal(
+      await client.objectRefCount('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.ts
index 9fd259b5b90..7948a4941de 100644
--- a/packages/client/lib/commands/OBJECT_REFCOUNT.ts
+++ b/packages/client/lib/commands/OBJECT_REFCOUNT.ts
@@ -1,11 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['OBJECT', 'REFCOUNT', key];
-}
-
-export declare function transformReply(): number | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the OBJECT REFCOUNT command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get the reference count for
+   * @see https://redis.io/commands/object-refcount/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('OBJECT', 'REFCOUNT');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PERSIST.spec.ts b/packages/client/lib/commands/PERSIST.spec.ts
index 4e53bd85a6c..fff6d7b3a76 100644
--- a/packages/client/lib/commands/PERSIST.spec.ts
+++ b/packages/client/lib/commands/PERSIST.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PERSIST';
+import PERSIST from './PERSIST';
+import { parseArgs } from './generic-transformers';
 
 describe('PERSIST', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['PERSIST', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(PERSIST, 'key'),
+      ['PERSIST', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.persist', async client => {
-        assert.equal(
-            await client.persist('key'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('persist', async client => {
+    assert.equal(
+      await client.persist('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PERSIST.ts b/packages/client/lib/commands/PERSIST.ts
index d7c9f8623e4..3b1f4a7062c 100644
--- a/packages/client/lib/commands/PERSIST.ts
+++ b/packages/client/lib/commands/PERSIST.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['PERSIST', key];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  /**
+   * Constructs the PERSIST command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to remove the expiration from
+   * @see https://redis.io/commands/persist/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('PERSIST');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PEXPIRE.spec.ts b/packages/client/lib/commands/PEXPIRE.spec.ts
index 03bde656103..368bc9b4907 100644
--- a/packages/client/lib/commands/PEXPIRE.spec.ts
+++ b/packages/client/lib/commands/PEXPIRE.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PEXPIRE';
+import PEXPIRE from './PEXPIRE';
+import { parseArgs } from './generic-transformers';
 
 describe('PEXPIRE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['PEXPIRE', 'key', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(PEXPIRE, 'key', 1),
+        ['PEXPIRE', 'key', '1']
+      );
+    });
 
-        it('with set option', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 'GT'),
-                ['PEXPIRE', 'key', '1', 'GT']
-            );
-        });
+    it('with set option', () => {
+      assert.deepEqual(
+        parseArgs(PEXPIRE, 'key', 1, 'GT'),
+        ['PEXPIRE', 'key', '1', 'GT']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pExpire', async client => {
-        assert.equal(
-            await client.pExpire('key', 1),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pExpire', async client => {
+    assert.equal(
+      await client.pExpire('key', 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PEXPIRE.ts b/packages/client/lib/commands/PEXPIRE.ts
index cbb5666a514..f1d96076885 100644
--- a/packages/client/lib/commands/PEXPIRE.ts
+++ b/packages/client/lib/commands/PEXPIRE.ts
@@ -1,19 +1,30 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    milliseconds: number,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PEXPIRE command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set the expiration for
+   * @param ms - The expiration time in milliseconds
+   * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT')
+   * @see https://redis.io/commands/pexpire/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    ms: number,
     mode?: 'NX' | 'XX' | 'GT' | 'LT'
-): RedisCommandArguments {
-    const args = ['PEXPIRE', key, milliseconds.toString()];
+  ) {
+    parser.push('PEXPIRE');
+    parser.pushKey(key);
+    parser.push(ms.toString());
 
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PEXPIREAT.spec.ts b/packages/client/lib/commands/PEXPIREAT.spec.ts
index fec03c8fb75..f1053920403 100644
--- a/packages/client/lib/commands/PEXPIREAT.spec.ts
+++ b/packages/client/lib/commands/PEXPIREAT.spec.ts
@@ -1,36 +1,40 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PEXPIREAT';
+import PEXPIREAT from './PEXPIREAT';
+import { parseArgs } from './generic-transformers';
 
 describe('PEXPIREAT', () => {
-    describe('transformArguments', () => {
-        it('number', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['PEXPIREAT', 'key', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('number', () => {
+      assert.deepEqual(
+        parseArgs(PEXPIREAT, 'key', 1),
+        ['PEXPIREAT', 'key', '1']
+      );
+    });
 
-        it('date', () => {
-            const d = new Date();
-            assert.deepEqual(
-                transformArguments('key', d),
-                ['PEXPIREAT', 'key', d.getTime().toString()]
-            );
-        });
+    it('date', () => {
+      const d = new Date();
+      assert.deepEqual(
+        parseArgs(PEXPIREAT, 'key', d),
+        ['PEXPIREAT', 'key', d.getTime().toString()]
+      );
+    });
 
-        it('with set option', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, 'XX'),
-                ['PEXPIREAT', 'key', '1', 'XX']
-            );
-        });
+    it('with set option', () => {
+      assert.deepEqual(
+        parseArgs(PEXPIREAT, 'key', 1, 'XX'),
+        ['PEXPIREAT', 'key', '1', 'XX']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pExpireAt', async client => {
-        assert.equal(
-            await client.pExpireAt('key', 1),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pExpireAt', async client => {
+    assert.equal(
+      await client.pExpireAt('key', 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PEXPIREAT.ts b/packages/client/lib/commands/PEXPIREAT.ts
index da912ec4fcb..072cc33bbb4 100644
--- a/packages/client/lib/commands/PEXPIREAT.ts
+++ b/packages/client/lib/commands/PEXPIREAT.ts
@@ -1,24 +1,31 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 import { transformPXAT } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    millisecondsTimestamp: number | Date,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PEXPIREAT command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set the expiration for
+   * @param msTimestamp - The expiration timestamp in milliseconds (Unix timestamp or Date object)
+   * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT')
+   * @see https://redis.io/commands/pexpireat/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    msTimestamp: number | Date,
     mode?: 'NX' | 'XX' | 'GT' | 'LT'
-): RedisCommandArguments {
-    const args = [
-        'PEXPIREAT',
-        key,
-        transformPXAT(millisecondsTimestamp)
-    ];
+  ) {
+    parser.push('PEXPIREAT');
+    parser.pushKey(key);
+    parser.push(transformPXAT(msTimestamp));
 
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PEXPIRETIME.spec.ts b/packages/client/lib/commands/PEXPIRETIME.spec.ts
index a2fd7f03f82..dbfc69e80dc 100644
--- a/packages/client/lib/commands/PEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/PEXPIRETIME.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PEXPIRETIME';
+import PEXPIRETIME from './PEXPIRETIME';
+import { parseArgs } from './generic-transformers';
 
 describe('PEXPIRETIME', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['PEXPIRETIME', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(PEXPIRETIME, 'key'),
+      ['PEXPIRETIME', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.pExpireTime', async client => {
-        assert.equal(
-            await client.pExpireTime('key'),
-            -2
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pExpireTime', async client => {
+    assert.equal(
+      await client.pExpireTime('key'),
+      -2
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PEXPIRETIME.ts b/packages/client/lib/commands/PEXPIRETIME.ts
index 4c1acba8f04..6b3488662c3 100644
--- a/packages/client/lib/commands/PEXPIRETIME.ts
+++ b/packages/client/lib/commands/PEXPIRETIME.ts
@@ -1,9 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['PEXPIRETIME', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PEXPIRETIME command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get the expiration time for in milliseconds
+   * @see https://redis.io/commands/pexpiretime/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('PEXPIRETIME');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PFADD.spec.ts b/packages/client/lib/commands/PFADD.spec.ts
index 8c0e752fd50..55c4311e638 100644
--- a/packages/client/lib/commands/PFADD.spec.ts
+++ b/packages/client/lib/commands/PFADD.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PFADD';
+import PFADD from './PFADD';
+import { parseArgs } from './generic-transformers';
 
 describe('PFADD', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element'),
-                ['PFADD', 'key', 'element']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(PFADD, 'key', 'element'),
+        ['PFADD', 'key', 'element']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['PFADD', 'key', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(PFADD, 'key', ['1', '2']),
+        ['PFADD', 'key', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pfAdd', async client => {
-        assert.equal(
-            await client.pfAdd('key', '1'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pfAdd', async client => {
+    assert.equal(
+      await client.pfAdd('key', '1'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PFADD.ts b/packages/client/lib/commands/PFADD.ts
index 8c8985de890..f5d2a280ca0 100644
--- a/packages/client/lib/commands/PFADD.ts
+++ b/packages/client/lib/commands/PFADD.ts
@@ -1,13 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    element: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['PFADD', key], element);
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PFADD command
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the HyperLogLog
+   * @param element - Optional elements to add
+   * @see https://redis.io/commands/pfadd/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element?: RedisVariadicArgument) {
+    parser.push('PFADD')
+    parser.pushKey(key);
+    if (element) {
+      parser.pushVariadic(element);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PFCOUNT.spec.ts b/packages/client/lib/commands/PFCOUNT.spec.ts
index a1ea06c4494..aec2ebecf0b 100644
--- a/packages/client/lib/commands/PFCOUNT.spec.ts
+++ b/packages/client/lib/commands/PFCOUNT.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PFCOUNT';
+import PFCOUNT from './PFCOUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('PFCOUNT', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['PFCOUNT', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(PFCOUNT, 'key'),
+        ['PFCOUNT', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['PFCOUNT', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(PFCOUNT, ['1', '2']),
+        ['PFCOUNT', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pfCount', async client => {
-        assert.equal(
-            await client.pfCount('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pfCount', async client => {
+    assert.equal(
+      await client.pfCount('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PFCOUNT.ts b/packages/client/lib/commands/PFCOUNT.ts
index a4cf2dbcb26..1358fed7d67 100644
--- a/packages/client/lib/commands/PFCOUNT.ts
+++ b/packages/client/lib/commands/PFCOUNT.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['PFCOUNT'], key);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PFCOUNT command
+   * 
+   * @param parser - The command parser
+   * @param keys - One or more keys of HyperLogLog structures to count
+   * @see https://redis.io/commands/pfcount/
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('PFCOUNT');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PFMERGE.spec.ts b/packages/client/lib/commands/PFMERGE.spec.ts
index 881fc5f5439..a286e932913 100644
--- a/packages/client/lib/commands/PFMERGE.spec.ts
+++ b/packages/client/lib/commands/PFMERGE.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PFMERGE';
+import PFMERGE from './PFMERGE';
+import { parseArgs } from './generic-transformers';
 
 describe('PFMERGE', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'source'),
-                ['PFMERGE', 'destination', 'source']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(PFMERGE, 'destination', 'source'),
+        ['PFMERGE', 'destination', 'source']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['PFMERGE', 'destination', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(PFMERGE, 'destination', ['1', '2']),
+        ['PFMERGE', 'destination', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pfMerge', async client => {
-        assert.equal(
-            await client.pfMerge('destination', 'source'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pfMerge', async client => {
+    assert.equal(
+      await client.pfMerge('{tag}destination', '{tag}source'),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PFMERGE.ts b/packages/client/lib/commands/PFMERGE.ts
index e934062b3fe..834a5dfbf55 100644
--- a/packages/client/lib/commands/PFMERGE.ts
+++ b/packages/client/lib/commands/PFMERGE.ts
@@ -1,10 +1,26 @@
-import { RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(destination: string, source: string | Array<string>): RedisCommandArguments {
-    return pushVerdictArguments(['PFMERGE', destination], source);
-}
-
-export declare function transformReply(): string;
+export default {
+  /**
+   * Constructs the PFMERGE command
+   * 
+   * @param parser - The command parser
+   * @param destination - The destination key to merge to
+   * @param sources - One or more source keys to merge from
+   * @see https://redis.io/commands/pfmerge/
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    sources?: RedisVariadicArgument
+  ) {
+    parser.push('PFMERGE');
+    parser.pushKey(destination);
+    if (sources) {
+      parser.pushKeys(sources);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PING.spec.ts b/packages/client/lib/commands/PING.spec.ts
index 06cbae43a13..56f513685f4 100644
--- a/packages/client/lib/commands/PING.spec.ts
+++ b/packages/client/lib/commands/PING.spec.ts
@@ -1,37 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PING';
+import PING from './PING';
+import { parseArgs } from './generic-transformers';
 
 describe('PING', () => {
-    describe('transformArguments', () => {
-        it('default', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['PING']
-            );
-        });
-
-        it('with message', () => {
-            assert.deepEqual(
-                transformArguments('message'),
-                ['PING', 'message']
-            );
-        });
+  describe('transformArguments', () => {
+    it('default', () => {
+      assert.deepEqual(
+        parseArgs(PING),
+        ['PING']
+      );
     });
 
-    describe('client.ping', () => {
-        testUtils.testWithClient('string', async client => {
-            assert.equal(
-                await client.ping(),
-                'PONG'
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('buffer', async client => {
-            assert.deepEqual(
-                await client.ping(client.commandOptions({ returnBuffers: true })),
-                Buffer.from('PONG')
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with message', () => {
+      assert.deepEqual(
+        parseArgs(PING, 'message'),
+        ['PING', 'message']
+      );
     });
+  });
+
+  testUtils.testAll('ping', async client => {
+    assert.equal(
+      await client.ping(),
+      'PONG'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PING.ts b/packages/client/lib/commands/PING.ts
index 95fa006122d..1e8d21e1584 100644
--- a/packages/client/lib/commands/PING.ts
+++ b/packages/client/lib/commands/PING.ts
@@ -1,12 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(message?: RedisCommandArgument): RedisCommandArguments {
-    const args: RedisCommandArguments = ['PING'];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PING command
+   * 
+   * @param parser - The command parser
+   * @param message - Optional message to be returned instead of PONG
+   * @see https://redis.io/commands/ping/
+   */
+  parseCommand(parser: CommandParser, message?: RedisArgument) {
+    parser.push('PING');
     if (message) {
-        args.push(message);
+      parser.push(message);
     }
-
-    return args;
-}
-
-export declare function transformReply(): RedisCommandArgument;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply | BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PSETEX.spec.ts b/packages/client/lib/commands/PSETEX.spec.ts
index f6262ed8709..8580e2f8e9d 100644
--- a/packages/client/lib/commands/PSETEX.spec.ts
+++ b/packages/client/lib/commands/PSETEX.spec.ts
@@ -1,27 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PSETEX';
+import PSETEX from './PSETEX';
+import { parseArgs } from './generic-transformers';
 
 describe('PSETEX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1, 'value'),
-            ['PSETEX', 'key', '1', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(PSETEX, 'key', 1, 'value'),
+      ['PSETEX', 'key', '1', 'value']
+    );
+  });
 
-    testUtils.testWithClient('client.pSetEx', async client => {
-        const a = await client.pSetEx('key', 1, 'value');
-        assert.equal(
-            await client.pSetEx('key', 1, 'value'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.pSetEx', async cluster => {
-        assert.equal(
-            await cluster.pSetEx('key', 1, 'value'),
-            'OK'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('pSetEx', async client => {
+    assert.equal(
+      await client.pSetEx('key', 1, 'value'),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PSETEX.ts b/packages/client/lib/commands/PSETEX.ts
index f2739b6e274..5b6d83bd694 100644
--- a/packages/client/lib/commands/PSETEX.ts
+++ b/packages/client/lib/commands/PSETEX.ts
@@ -1,18 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    milliseconds: number,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'PSETEX',
-        key,
-        milliseconds.toString(),
-        value
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  /**
+   * Constructs the PSETEX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set
+   * @param ms - The expiration time in milliseconds
+   * @param value - The value to set
+   * @see https://redis.io/commands/psetex/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, ms: number, value: RedisArgument) {
+    parser.push('PSETEX');
+    parser.pushKey(key);
+    parser.push(ms.toString(), value);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PTTL.spec.ts b/packages/client/lib/commands/PTTL.spec.ts
index e65421de590..deb04bad97e 100644
--- a/packages/client/lib/commands/PTTL.spec.ts
+++ b/packages/client/lib/commands/PTTL.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PTTL';
+import PTTL from './PTTL';
+import { parseArgs } from './generic-transformers';
 
 describe('PTTL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['PTTL', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(PTTL, 'key'),
+      ['PTTL', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.pTTL', async client => {
-        assert.equal(
-            await client.pTTL('key'),
-            -2
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('pTTL', async client => {
+    assert.equal(
+      await client.pTTL('key'),
+      -2
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/PTTL.ts b/packages/client/lib/commands/PTTL.ts
index a2975623f7a..9d408aeee17 100644
--- a/packages/client/lib/commands/PTTL.ts
+++ b/packages/client/lib/commands/PTTL.ts
@@ -1,11 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['PTTL', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PTTL command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to get the time to live in milliseconds
+   * @see https://redis.io/commands/pttl/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('PTTL');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PUBLISH.spec.ts b/packages/client/lib/commands/PUBLISH.spec.ts
index b2084e668ba..930adc8c4d7 100644
--- a/packages/client/lib/commands/PUBLISH.spec.ts
+++ b/packages/client/lib/commands/PUBLISH.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PUBLISH';
+import PUBLISH from './PUBLISH';
+import { parseArgs } from './generic-transformers';
 
 describe('PUBLISH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('channel', 'message'),
-            ['PUBLISH', 'channel', 'message']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(PUBLISH, 'channel', 'message'),
+      ['PUBLISH', 'channel', 'message']
+    );
+  });
 
-    testUtils.testWithClient('client.publish', async client => {
-        assert.equal(
-            await client.publish('channel', 'message'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.publish', async client => {
+    assert.equal(
+      await client.publish('channel', 'message'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts
index 7862a0936cb..197a2b069eb 100644
--- a/packages/client/lib/commands/PUBLISH.ts
+++ b/packages/client/lib/commands/PUBLISH.ts
@@ -1,12 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    channel: RedisCommandArgument,
-    message: RedisCommandArgument
-): RedisCommandArguments {
-    return ['PUBLISH', channel, message];
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  IS_FORWARD_COMMAND: true,
+  /**
+   * Constructs the PUBLISH command
+   * 
+   * @param parser - The command parser
+   * @param channel - The channel to publish to
+   * @param message - The message to publish
+   * @see https://redis.io/commands/publish/
+   */
+  parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) {
+    parser.push('PUBLISH', channel, message);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts
index c427eab4850..369e339a497 100644
--- a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts
+++ b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PUBSUB_CHANNELS';
+import PUBSUB_CHANNELS from './PUBSUB_CHANNELS';
+import { parseArgs } from './generic-transformers';
 
 describe('PUBSUB CHANNELS', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['PUBSUB', 'CHANNELS']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_CHANNELS),
+        ['PUBSUB', 'CHANNELS']
+      );
+    });
 
-        it('with pattern', () => {
-            assert.deepEqual(
-                transformArguments('patter*'),
-                ['PUBSUB', 'CHANNELS', 'patter*']
-            );
-        });
+    it('with pattern', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_CHANNELS, 'patter*'),
+        ['PUBSUB', 'CHANNELS', 'patter*']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pubSubChannels', async client => {
-        assert.deepEqual(
-            await client.pubSubChannels(),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.pubSubChannels', async client => {
+    assert.deepEqual(
+      await client.pubSubChannels(),
+      []
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.ts
index 86a144ede8e..c9eb9bf7b4e 100644
--- a/packages/client/lib/commands/PUBSUB_CHANNELS.ts
+++ b/packages/client/lib/commands/PUBSUB_CHANNELS.ts
@@ -1,13 +1,23 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export function transformArguments(pattern?: string): Array<string> {
-    const args = ['PUBSUB', 'CHANNELS'];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PUBSUB CHANNELS command
+   * 
+   * @param parser - The command parser
+   * @param pattern - Optional pattern to filter channels
+   * @see https://redis.io/commands/pubsub-channels/
+   */
+  parseCommand(parser: CommandParser, pattern?: RedisArgument) {
+    parser.push('PUBSUB', 'CHANNELS');
 
     if (pattern) {
-        args.push(pattern);
+      parser.push(pattern);
     }
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
 
-    return args;
-}
-
-export declare function transformReply(): Array<string>;
diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts
index d738b916c60..d75256bb43c 100644
--- a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts
+++ b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PUBSUB_NUMPAT';
+import PUBSUB_NUMPAT from './PUBSUB_NUMPAT';
+import { parseArgs } from './generic-transformers';
 
 describe('PUBSUB NUMPAT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['PUBSUB', 'NUMPAT']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(PUBSUB_NUMPAT),
+      ['PUBSUB', 'NUMPAT']
+    );
+  });
 
-    testUtils.testWithClient('client.pubSubNumPat', async client => {
-        assert.equal(
-            await client.pubSubNumPat(),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.pubSubNumPat', async client => {
+    assert.equal(
+      await client.pubSubNumPat(),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.ts
index 15be6aa1b18..4b876db88f1 100644
--- a/packages/client/lib/commands/PUBSUB_NUMPAT.ts
+++ b/packages/client/lib/commands/PUBSUB_NUMPAT.ts
@@ -1,7 +1,17 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['PUBSUB', 'NUMPAT'];
-}
-
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PUBSUB NUMPAT command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/pubsub-numpat/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('PUBSUB', 'NUMPAT');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts
index e35558ef865..11339ae2bb5 100644
--- a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts
+++ b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts
@@ -1,35 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PUBSUB_NUMSUB';
+import PUBSUB_NUMSUB from './PUBSUB_NUMSUB';
+import { parseArgs } from './generic-transformers';
 
 describe('PUBSUB NUMSUB', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['PUBSUB', 'NUMSUB']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_NUMSUB),
+        ['PUBSUB', 'NUMSUB']
+      );
+    });
 
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('channel'),
-                ['PUBSUB', 'NUMSUB', 'channel']
-            );
-        });
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_NUMSUB, 'channel'),
+        ['PUBSUB', 'NUMSUB', 'channel']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['PUBSUB', 'NUMSUB', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_NUMSUB, ['1', '2']),
+        ['PUBSUB', 'NUMSUB', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pubSubNumSub', async client => {
-        assert.deepEqual(
-            await client.pubSubNumSub(),
-            Object.create(null)
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.pubSubNumSub', async client => {
+    assert.deepEqual(
+      await client.pubSubNumSub(),
+      Object.create(null)
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.ts
index f47238f8882..da6647dc553 100644
--- a/packages/client/lib/commands/PUBSUB_NUMSUB.ts
+++ b/packages/client/lib/commands/PUBSUB_NUMSUB.ts
@@ -1,24 +1,37 @@
-import { pushVerdictArguments } from './generic-transformers';
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const IS_READ_ONLY = true;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PUBSUB NUMSUB command
+   * 
+   * @param parser - The command parser
+   * @param channels - Optional channel names to get subscription count for
+   * @see https://redis.io/commands/pubsub-numsub/
+   */
+  parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) {
+    parser.push('PUBSUB', 'NUMSUB');
 
-export function transformArguments(
-    channels?: Array<RedisCommandArgument> | RedisCommandArgument
-): RedisCommandArguments {
-    const args = ['PUBSUB', 'NUMSUB'];
-
-    if (channels) return pushVerdictArguments(args, channels);
-
-    return args;
-}
-
-export function transformReply(rawReply: Array<string | number>): Record<string, number> {
-    const transformedReply = Object.create(null);
-
-    for (let i = 0; i < rawReply.length; i +=2) {
-        transformedReply[rawReply[i]] = rawReply[i + 1];
+    if (channels) {
+      parser.pushVariadic(channels);
+    }
+  },
+  /**
+   * Transforms the PUBSUB NUMSUB reply into a record of channel name to subscriber count
+   * 
+   * @param rawReply - The raw reply from Redis
+   * @returns Record mapping channel names to their subscriber counts
+   */
+  transformReply(rawReply: UnwrapReply<ArrayReply<BlobStringReply | NumberReply>>) {
+    const reply = Object.create(null);
+    let i = 0;
+    while (i < rawReply.length) {
+      reply[rawReply[i++].toString()] = rawReply[i++].toString();
     }
 
-    return transformedReply;
-}
+    return reply as Record<string, NumberReply>;
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts
index 1e5f2292b39..36597a9cfd8 100644
--- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts
+++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PUBSUB_SHARDCHANNELS';
+import PUBSUB_SHARDCHANNELS from './PUBSUB_SHARDCHANNELS';
+import { parseArgs } from './generic-transformers';
 
 describe('PUBSUB SHARDCHANNELS', () => {
-    testUtils.isVersionGreaterThanHook([7]);
-    
-    describe('transformArguments', () => {
-        it('without pattern', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['PUBSUB', 'SHARDCHANNELS']
-            );
-        });
+  testUtils.isVersionGreaterThanHook([7]);
 
-        it('with pattern', () => {
-            assert.deepEqual(
-                transformArguments('patter*'),
-                ['PUBSUB', 'SHARDCHANNELS', 'patter*']
-            );
-        });
+  describe('transformArguments', () => {
+    it('without pattern', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_SHARDCHANNELS),
+        ['PUBSUB', 'SHARDCHANNELS']
+      );
     });
 
-    testUtils.testWithClient('client.pubSubShardChannels', async client => {
-        assert.deepEqual(
-            await client.pubSubShardChannels(),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with pattern', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_SHARDCHANNELS, 'patter*'),
+        ['PUBSUB', 'SHARDCHANNELS', 'patter*']
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.pubSubShardChannels', async client => {
+    assert.deepEqual(
+      await client.pubSubShardChannels(),
+      []
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts
index e998677848a..30601de55df 100644
--- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts
+++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts
@@ -1,13 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PUBSUB SHARDCHANNELS command
+   * 
+   * @param parser - The command parser
+   * @param pattern - Optional pattern to filter shard channels
+   * @see https://redis.io/commands/pubsub-shardchannels/
+   */
+  parseCommand(parser: CommandParser, pattern?: RedisArgument) {
+    parser.push('PUBSUB', 'SHARDCHANNELS');
 
-export function transformArguments(
-    pattern?: RedisCommandArgument
-): RedisCommandArguments {
-    const args: RedisCommandArguments = ['PUBSUB', 'SHARDCHANNELS'];
-    if (pattern) args.push(pattern);
-    return args;
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+    if (pattern) {
+      parser.push(pattern);
+    }
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts
index fea1373b55d..e335941897d 100644
--- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts
+++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts
@@ -1,48 +1,49 @@
 import { strict as assert } from 'assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PUBSUB_SHARDNUMSUB';
+import PUBSUB_SHARDNUMSUB from './PUBSUB_SHARDNUMSUB';
+import { parseArgs } from './generic-transformers';
 
 describe('PUBSUB SHARDNUMSUB', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['PUBSUB', 'SHARDNUMSUB']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_SHARDNUMSUB),
+        ['PUBSUB', 'SHARDNUMSUB']
+      );
+    });
 
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('channel'),
-                ['PUBSUB', 'SHARDNUMSUB', 'channel']
-            );
-        });
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_SHARDNUMSUB, 'channel'),
+        ['PUBSUB', 'SHARDNUMSUB', 'channel']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['PUBSUB', 'SHARDNUMSUB', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(PUBSUB_SHARDNUMSUB, ['1', '2']),
+        ['PUBSUB', 'SHARDNUMSUB', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.pubSubShardNumSub', async client => {
-        assert.deepEqual(
-            await client.pubSubShardNumSub(['foo', 'bar']),
-            Object.create(null, {
-                foo: {
-                    value: 0,
-                    configurable: true,
-                    enumerable: true
-                },
-                bar: {
-                    value: 0,
-                    configurable: true,
-                    enumerable: true
-                }
-            })
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.pubSubShardNumSub', async client => {
+    assert.deepEqual(
+      await client.pubSubShardNumSub(['foo', 'bar']),
+      Object.create(null, {
+        foo: {
+          value: 0,
+          configurable: true,
+          enumerable: true
+        },
+        bar: {
+          value: 0,
+          configurable: true,
+          enumerable: true
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts
index 4d7f4d8a71e..9d54a113d78 100644
--- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts
+++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts
@@ -1,24 +1,37 @@
-import { pushVerdictArguments } from './generic-transformers';
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const IS_READ_ONLY = true;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the PUBSUB SHARDNUMSUB command
+   * 
+   * @param parser - The command parser
+   * @param channels - Optional shard channel names to get subscription count for
+   * @see https://redis.io/commands/pubsub-shardnumsub/
+   */
+  parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) {
+    parser.push('PUBSUB', 'SHARDNUMSUB');
 
-export function transformArguments(
-    channels?: Array<RedisCommandArgument> | RedisCommandArgument
-): RedisCommandArguments {
-    const args = ['PUBSUB', 'SHARDNUMSUB'];
-
-    if (channels) return pushVerdictArguments(args, channels);
-
-    return args;
-}
-
-export function transformReply(rawReply: Array<string | number>): Record<string, number> {
-    const transformedReply = Object.create(null);
-
-    for (let i = 0; i < rawReply.length; i += 2) {
-        transformedReply[rawReply[i]] = rawReply[i + 1];
+    if (channels) {
+      parser.pushVariadic(channels);
     }
+  },
+  /**
+   * Transforms the PUBSUB SHARDNUMSUB reply into a record of shard channel name to subscriber count
+   * 
+   * @param reply - The raw reply from Redis
+   * @returns Record mapping shard channel names to their subscriber counts
+   */
+  transformReply(reply: UnwrapReply<ArrayReply<BlobStringReply | NumberReply>>) {
+    const transformedReply: Record<string, NumberReply> = Object.create(null);
 
+    for (let i = 0; i < reply.length; i += 2) {
+      transformedReply[(reply[i] as BlobStringReply).toString()] = reply[i + 1] as NumberReply;
+    }
+  
     return transformedReply;
-}
+  }
+} as const satisfies Command;
+
diff --git a/packages/client/lib/commands/RANDOMKEY.spec.ts b/packages/client/lib/commands/RANDOMKEY.spec.ts
index 81c42b2fd83..f86617a3b75 100644
--- a/packages/client/lib/commands/RANDOMKEY.spec.ts
+++ b/packages/client/lib/commands/RANDOMKEY.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RANDOMKEY';
+import RANDOMKEY from './RANDOMKEY';
+import { parseArgs } from './generic-transformers';
 
 describe('RANDOMKEY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['RANDOMKEY']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RANDOMKEY),
+      ['RANDOMKEY']
+    );
+  });
 
-    testUtils.testWithClient('client.randomKey', async client => {
-        assert.equal(
-            await client.randomKey(),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('randomKey', async client => {
+    assert.equal(
+      await client.randomKey(),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RANDOMKEY.ts b/packages/client/lib/commands/RANDOMKEY.ts
index f2d511d4dec..263f539113b 100644
--- a/packages/client/lib/commands/RANDOMKEY.ts
+++ b/packages/client/lib/commands/RANDOMKEY.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(): RedisCommandArguments {
-    return ['RANDOMKEY'];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the RANDOMKEY command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/randomkey/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('RANDOMKEY');
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/READONLY.spec.ts b/packages/client/lib/commands/READONLY.spec.ts
index aa4db47f81a..ac303322330 100644
--- a/packages/client/lib/commands/READONLY.spec.ts
+++ b/packages/client/lib/commands/READONLY.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './READONLY';
+import { strict as assert } from 'node:assert';
+import READONLY from './READONLY';
+import { parseArgs } from './generic-transformers';
 
 describe('READONLY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['READONLY']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(READONLY),
+      ['READONLY']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/READONLY.ts b/packages/client/lib/commands/READONLY.ts
index db7db881628..16eef975818 100644
--- a/packages/client/lib/commands/READONLY.ts
+++ b/packages/client/lib/commands/READONLY.ts
@@ -1,5 +1,17 @@
-export function transformArguments(): Array<string> {
-    return ['READONLY'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the READONLY command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/readonly/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('READONLY');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/READWRITE.spec.ts b/packages/client/lib/commands/READWRITE.spec.ts
index 6ce4a3ee56a..cc3f99a5d16 100644
--- a/packages/client/lib/commands/READWRITE.spec.ts
+++ b/packages/client/lib/commands/READWRITE.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './READWRITE';
+import { strict as assert } from 'node:assert';
+import READWRITE from './READWRITE';
+import { parseArgs } from './generic-transformers';
 
 describe('READWRITE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['READWRITE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(READWRITE),
+      ['READWRITE']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/READWRITE.ts b/packages/client/lib/commands/READWRITE.ts
index 60dc865e89e..f747366448c 100644
--- a/packages/client/lib/commands/READWRITE.ts
+++ b/packages/client/lib/commands/READWRITE.ts
@@ -1,5 +1,17 @@
-export function transformArguments(): Array<string> {
-    return ['READWRITE'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the READWRITE command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/readwrite/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('READWRITE');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RENAME.spec.ts b/packages/client/lib/commands/RENAME.spec.ts
index 49e0af600f6..05dd9417b96 100644
--- a/packages/client/lib/commands/RENAME.spec.ts
+++ b/packages/client/lib/commands/RENAME.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RENAME';
+import RENAME from './RENAME';
+import { parseArgs } from './generic-transformers';
 
 describe('RENAME', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('from', 'to'),
-            ['RENAME', 'from', 'to']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RENAME, 'source', 'destination'),
+      ['RENAME', 'source', 'destination']
+    );
+  });
 
-    testUtils.testWithClient('client.rename', async client => {
-        await client.set('from', 'value');
-
-        assert.equal(
-            await client.rename('from', 'to'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('rename', async client => {
+    const [, reply] = await Promise.all([
+      client.set('{tag}source', 'value'),
+      client.rename('{tag}source', '{tag}destination')
+    ]);
+    
+    assert.equal(reply, 'OK');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RENAME.ts b/packages/client/lib/commands/RENAME.ts
index 2d1134084fb..0033758d128 100644
--- a/packages/client/lib/commands/RENAME.ts
+++ b/packages/client/lib/commands/RENAME.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    newKey: RedisCommandArgument
-): RedisCommandArguments {
-    return ['RENAME', key, newKey];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the RENAME command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to rename
+   * @param newKey - The new key name
+   * @see https://redis.io/commands/rename/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) {
+    parser.push('RENAME');
+    parser.pushKeys([key, newKey]);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RENAMENX.spec.ts b/packages/client/lib/commands/RENAMENX.spec.ts
index 6345eb5bd09..2367b453322 100644
--- a/packages/client/lib/commands/RENAMENX.spec.ts
+++ b/packages/client/lib/commands/RENAMENX.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RENAMENX';
+import RENAMENX from './RENAMENX';
+import { parseArgs } from './generic-transformers';
 
 describe('RENAMENX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('from', 'to'),
-            ['RENAMENX', 'from', 'to']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RENAMENX, 'source', 'destination'),
+      ['RENAMENX', 'source', 'destination']
+    );
+  });
 
-    testUtils.testWithClient('client.renameNX', async client => {
-        await client.set('from', 'value');
+  testUtils.testAll('renameNX', async client => {
+    const [, reply] = await Promise.all([
+      client.set('{tag}source', 'value'),
+      client.renameNX('{tag}source', '{tag}destination')
+    ]);
 
-        assert.equal(
-            await client.renameNX('from', 'to'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 1);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RENAMENX.ts b/packages/client/lib/commands/RENAMENX.ts
index 322ff0a88cc..38c12dee727 100644
--- a/packages/client/lib/commands/RENAMENX.ts
+++ b/packages/client/lib/commands/RENAMENX.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    newKey: RedisCommandArgument
-): RedisCommandArguments {
-    return ['RENAMENX', key, newKey];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the RENAMENX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to rename
+   * @param newKey - The new key name, if it doesn't exist
+   * @see https://redis.io/commands/renamenx/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) {
+    parser.push('RENAMENX');
+    parser.pushKeys([key, newKey]);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/REPLICAOF.spec.ts b/packages/client/lib/commands/REPLICAOF.spec.ts
index ab1906944cf..13668639494 100644
--- a/packages/client/lib/commands/REPLICAOF.spec.ts
+++ b/packages/client/lib/commands/REPLICAOF.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './REPLICAOF';
+import { strict as assert } from 'node:assert';
+import REPLICAOF from './REPLICAOF';
+import { parseArgs } from './generic-transformers';
 
 describe('REPLICAOF', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('host', 1),
-            ['REPLICAOF', 'host', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(REPLICAOF, 'host', 1),
+      ['REPLICAOF', 'host', '1']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/REPLICAOF.ts b/packages/client/lib/commands/REPLICAOF.ts
index bd452e0f371..08d4167fff4 100644
--- a/packages/client/lib/commands/REPLICAOF.ts
+++ b/packages/client/lib/commands/REPLICAOF.ts
@@ -1,5 +1,19 @@
-export function transformArguments(host: string, port: number): Array<string> {
-    return ['REPLICAOF', host, port.toString()];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the REPLICAOF command
+   * 
+   * @param parser - The command parser
+   * @param host - The host of the master to replicate from
+   * @param port - The port of the master to replicate from
+   * @see https://redis.io/commands/replicaof/
+   */
+  parseCommand(parser: CommandParser, host: string, port: number) {
+    parser.push('REPLICAOF', host, port.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RESTORE-ASKING.spec.ts b/packages/client/lib/commands/RESTORE-ASKING.spec.ts
index de9fce5c628..1258cf68e2d 100644
--- a/packages/client/lib/commands/RESTORE-ASKING.spec.ts
+++ b/packages/client/lib/commands/RESTORE-ASKING.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './RESTORE-ASKING';
+import { strict as assert } from 'node:assert';
+import RESTORE_ASKING from './RESTORE-ASKING';
+import { parseArgs } from './generic-transformers';
 
 describe('RESTORE-ASKING', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['RESTORE-ASKING']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RESTORE_ASKING),
+      ['RESTORE-ASKING']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/RESTORE-ASKING.ts b/packages/client/lib/commands/RESTORE-ASKING.ts
index d53d8541cd7..947ee9544d9 100644
--- a/packages/client/lib/commands/RESTORE-ASKING.ts
+++ b/packages/client/lib/commands/RESTORE-ASKING.ts
@@ -1,5 +1,17 @@
-export function transformArguments(): Array<string> {
-    return ['RESTORE-ASKING'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the RESTORE-ASKING command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/restore-asking/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('RESTORE-ASKING');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RESTORE.spec.ts b/packages/client/lib/commands/RESTORE.spec.ts
index 89d42f3d4de..6083b2eb1a5 100644
--- a/packages/client/lib/commands/RESTORE.spec.ts
+++ b/packages/client/lib/commands/RESTORE.spec.ts
@@ -1,74 +1,85 @@
 import { strict as assert } from 'assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RESTORE';
+import RESTORE from './RESTORE';
+import { RESP_TYPES } from '../RESP/decoder';
+import { parseArgs } from './generic-transformers';
 
 describe('RESTORE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, 'value'),
-                ['RESTORE', 'key', '0', 'value']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(RESTORE, 'key', 0, 'value'),
+        ['RESTORE', 'key', '0', 'value']
+      );
+    });
 
-        it('with REPLACE', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, 'value', {
-                    REPLACE: true
-                }),
-                ['RESTORE', 'key', '0', 'value', 'REPLACE']
-            );
-        });
+    it('with REPLACE', () => {
+      assert.deepEqual(
+        parseArgs(RESTORE, 'key', 0, 'value', {
+          REPLACE: true
+        }),
+        ['RESTORE', 'key', '0', 'value', 'REPLACE']
+      );
+    });
 
-        it('with ABSTTL', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, 'value', {
-                    ABSTTL: true
-                }),
-                ['RESTORE', 'key', '0', 'value', 'ABSTTL']
-            );
-        });
+    it('with ABSTTL', () => {
+      assert.deepEqual(
+        parseArgs(RESTORE, 'key', 0, 'value', {
+          ABSTTL: true
+        }),
+        ['RESTORE', 'key', '0', 'value', 'ABSTTL']
+      );
+    });
 
-        it('with IDLETIME', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, 'value', {
-                    IDLETIME: 1
-                }),
-                ['RESTORE', 'key', '0', 'value', 'IDLETIME', '1']
-            );
-        });
+    it('with IDLETIME', () => {
+      assert.deepEqual(
+        parseArgs(RESTORE, 'key', 0, 'value', {
+          IDLETIME: 1
+        }),
+        ['RESTORE', 'key', '0', 'value', 'IDLETIME', '1']
+      );
+    });
 
-        it('with FREQ', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, 'value', {
-                    FREQ: 1
-                }),
-                ['RESTORE', 'key', '0', 'value', 'FREQ', '1']
-            );
-        });
+    it('with FREQ', () => {
+      assert.deepEqual(
+        parseArgs(RESTORE, 'key', 0, 'value', {
+          FREQ: 1
+        }),
+        ['RESTORE', 'key', '0', 'value', 'FREQ', '1']
+      );
+    });
 
-        it('with REPLACE, ABSTTL, IDLETIME and FREQ', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, 'value', {
-                    REPLACE: true,
-                    ABSTTL: true,
-                    IDLETIME: 1,
-                    FREQ: 2
-                }),
-                ['RESTORE', 'key', '0', 'value', 'REPLACE', 'ABSTTL', 'IDLETIME', '1', 'FREQ', '2']
-            );
-        });
+    it('with REPLACE, ABSTTL, IDLETIME and FREQ', () => {
+      assert.deepEqual(
+        parseArgs(RESTORE, 'key', 0, 'value', {
+          REPLACE: true,
+          ABSTTL: true,
+          IDLETIME: 1,
+          FREQ: 2
+        }),
+        ['RESTORE', 'key', '0', 'value', 'REPLACE', 'ABSTTL', 'IDLETIME', '1', 'FREQ', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.restore', async client => {
-        const [, dump] = await Promise.all([
-            client.set('source', 'value'),
-            client.dump(client.commandOptions({ returnBuffers: true }), 'source')
-        ]);
+  testUtils.testWithClient('client.restore', async client => {
+    const [, dump] = await Promise.all([
+      client.set('source', 'value'),
+      client.dump('source')
+    ]);
 
-        assert.equal(
-            await client.restore('destination', 0, dump),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(
+      await client.restore('destination', 0, dump),
+      'OK'
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      commandOptions: {
+        typeMapping: {
+          [RESP_TYPES.BLOB_STRING]: Buffer
+        }
+      }
+    }
+  });
 });
diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts
index d9ac11c424b..5b07a773cc4 100644
--- a/packages/client/lib/commands/RESTORE.ts
+++ b/packages/client/lib/commands/RESTORE.ts
@@ -1,39 +1,59 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface RestoreOptions {
-    REPLACE?: true;
-    ABSTTL?: true;
-    IDLETIME?: number;
-    FREQ?: number;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
+
+/**
+ * Options for the RESTORE command
+ * 
+ * @property REPLACE - Replace existing key
+ * @property ABSTTL - Use the TTL value as absolute timestamp
+ * @property IDLETIME - Set the idle time (seconds) for the key
+ * @property FREQ - Set the frequency counter for LFU policy
+ */
+export interface RestoreOptions {
+  REPLACE?: boolean;
+  ABSTTL?: boolean;
+  IDLETIME?: number;
+  FREQ?: number;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the RESTORE command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to restore
+   * @param ttl - Time to live in milliseconds, 0 for no expiry
+   * @param serializedValue - The serialized value from DUMP command
+   * @param options - Options for the RESTORE command
+   * @see https://redis.io/commands/restore/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     ttl: number,
-    serializedValue: RedisCommandArgument,
+    serializedValue: RedisArgument,
     options?: RestoreOptions
-): RedisCommandArguments {
-    const args =  ['RESTORE', key, ttl.toString(), serializedValue];
+  ) {
+    parser.push('RESTORE');
+    parser.pushKey(key);
+    parser.push(ttl.toString(), serializedValue);
 
     if (options?.REPLACE) {
-        args.push('REPLACE');
+      parser.push('REPLACE');
     }
 
     if (options?.ABSTTL) {
-        args.push('ABSTTL');
+      parser.push('ABSTTL');
     }
 
     if (options?.IDLETIME) {
-        args.push('IDLETIME', options.IDLETIME.toString());
+      parser.push('IDLETIME', options.IDLETIME.toString());
     }
 
     if (options?.FREQ) {
-        args.push('FREQ', options.FREQ.toString());
+      parser.push('FREQ', options.FREQ.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ROLE.spec.ts b/packages/client/lib/commands/ROLE.spec.ts
index 2e6d9b163ae..09ce6ed3427 100644
--- a/packages/client/lib/commands/ROLE.spec.ts
+++ b/packages/client/lib/commands/ROLE.spec.ts
@@ -1,69 +1,70 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './ROLE';
+import ROLE from './ROLE';
+import { parseArgs } from './generic-transformers';
 
 describe('ROLE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['ROLE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ROLE),
+      ['ROLE']
+    );
+  });
 
-    describe('transformReply', () => {
-        it('master', () => {
-            assert.deepEqual(
-                transformReply(['master', 3129659, [['127.0.0.1', '9001', '3129242'], ['127.0.0.1', '9002', '3129543']]]),
-                {
-                    role: 'master',
-                    replicationOffest: 3129659,
-                    replicas: [{
-                        ip: '127.0.0.1',
-                        port: 9001,
-                        replicationOffest: 3129242
-                    }, {
-                        ip: '127.0.0.1',
-                        port: 9002,
-                        replicationOffest: 3129543
-                    }]
-                }
-            );
-        });
+  describe('transformReply', () => {
+    it('master', () => {
+      assert.deepEqual(
+        ROLE.transformReply(['master', 3129659, [['127.0.0.1', '9001', '3129242'], ['127.0.0.1', '9002', '3129543']]] as any),
+        {
+          role: 'master',
+          replicationOffest: 3129659,
+          replicas: [{
+            host: '127.0.0.1',
+            port: 9001,
+            replicationOffest: 3129242
+          }, {
+            host: '127.0.0.1',
+            port: 9002,
+            replicationOffest: 3129543
+          }]
+        }
+      );
+    });
 
-        it('replica', () => {
-            assert.deepEqual(
-                transformReply(['slave', '127.0.0.1', 9000, 'connected', 3167038]),
-                {
-                    role: 'slave',
-                    master: {
-                        ip: '127.0.0.1',
-                        port: 9000
-                    },
-                    state: 'connected',
-                    dataReceived: 3167038
-                }
-            );
-        });
+    it('replica', () => {
+      assert.deepEqual(
+        ROLE.transformReply(['slave', '127.0.0.1', 9000, 'connected', 3167038] as any),
+        {
+          role: 'slave',
+          master: {
+            host: '127.0.0.1',
+            port: 9000
+          },
+          state: 'connected',
+          dataReceived: 3167038
+        }
+      );
+    });
 
-        it('sentinel', () => {
-            assert.deepEqual(
-                transformReply(['sentinel', ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master']]),
-                {
-                    role: 'sentinel',
-                    masterNames: ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master']
-                }
-            );
-        });
+    it('sentinel', () => {
+      assert.deepEqual(
+        ROLE.transformReply(['sentinel', ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master']] as any),
+        {
+          role: 'sentinel',
+          masterNames: ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master']
+        }
+      );
     });
+  });
 
-    testUtils.testWithClient('client.role', async client => {
-        assert.deepEqual(
-            await client.role(),
-            {
-                role: 'master',
-                replicationOffest: 0,
-                replicas: []
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.role', async client => {
+    assert.deepEqual(
+      await client.role(),
+      {
+        role: 'master',
+        replicationOffest: 0,
+        replicas: []
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ROLE.ts b/packages/client/lib/commands/ROLE.ts
index b1d6041fdfa..749ac4935fa 100644
--- a/packages/client/lib/commands/ROLE.ts
+++ b/packages/client/lib/commands/ROLE.ts
@@ -1,75 +1,95 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, NumberReply, ArrayReply, TuplesReply, UnwrapReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['ROLE'];
-}
+/**
+ * Role information returned for a Redis master
+ */
+type MasterRole = [
+  role: BlobStringReply<'master'>,
+  replicationOffest: NumberReply,
+  replicas: ArrayReply<TuplesReply<[host: BlobStringReply, port: BlobStringReply, replicationOffest: BlobStringReply]>>
+];
 
-interface RoleReplyInterface<T extends string> {
-    role: T;
-}
+/**
+ * Role information returned for a Redis slave
+ */
+type SlaveRole = [
+  role: BlobStringReply<'slave'>,
+  masterHost: BlobStringReply,
+  masterPort: NumberReply,
+  state: BlobStringReply<'connect' | 'connecting' | 'sync' | 'connected'>,
+  dataReceived: NumberReply
+];
 
-type RoleMasterRawReply = ['master', number, Array<[string, string, string]>];
+/**
+ * Role information returned for a Redis sentinel
+ */
+type SentinelRole = [
+  role: BlobStringReply<'sentinel'>,
+  masterNames: ArrayReply<BlobStringReply>
+];
 
-interface RoleMasterReply extends RoleReplyInterface<'master'> {
-    replicationOffest: number;
-    replicas: Array<{
-        ip: string;
-        port: number;
-        replicationOffest: number;
-    }>;
-}
+/**
+ * Combined role type for Redis instance role information
+ */
+type Role = TuplesReply<MasterRole | SlaveRole | SentinelRole>;
 
-type RoleReplicaState = 'connect' | 'connecting' | 'sync' | 'connected';
-
-type RoleReplicaRawReply = ['slave', string, number, RoleReplicaState, number];
-
-interface RoleReplicaReply extends RoleReplyInterface<'slave'>  {
-    master: {
-        ip: string;
-        port: number;
-    };
-    state: RoleReplicaState;
-    dataReceived: number;
-}
-
-type RoleSentinelRawReply = ['sentinel', Array<string>];
-
-interface RoleSentinelReply extends RoleReplyInterface<'sentinel'>  {
-    masterNames: Array<string>;
-}
-
-type RoleRawReply = RoleMasterRawReply | RoleReplicaRawReply | RoleSentinelRawReply;
-
-type RoleReply = RoleMasterReply | RoleReplicaReply | RoleSentinelReply;
-
-export function transformReply(reply: RoleRawReply): RoleReply {
-    switch (reply[0]) {
-        case 'master':
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the ROLE command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/role/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('ROLE');
+  },
+  /**
+   * Transforms the ROLE reply into a structured object
+   * 
+   * @param reply - The raw reply from Redis
+   * @returns Structured object representing role information
+   */
+  transformReply(reply: UnwrapReply<Role>) {
+    switch (reply[0] as unknown as UnwrapReply<typeof reply[0]>) {
+      case 'master': {
+        const [role, replicationOffest, replicas] = reply as MasterRole;
+        return {
+          role,
+          replicationOffest,
+          replicas: (replicas as unknown as UnwrapReply<typeof replicas>).map(replica => {
+            const [host, port, replicationOffest] = replica as unknown as UnwrapReply<typeof replica>;
             return {
-                role: 'master',
-                replicationOffest: reply[1],
-                replicas: reply[2].map(([ip, port, replicationOffest]) => ({
-                    ip,
-                    port: Number(port),
-                    replicationOffest: Number(replicationOffest)
-                }))
+              host,
+              port: Number(port),
+              replicationOffest: Number(replicationOffest)
             };
+          })
+        };
+      }
 
-        case 'slave':
-            return {
-                role: 'slave',
-                master: {
-                    ip: reply[1],
-                    port: reply[2]
-                },
-                state: reply[3],
-                dataReceived: reply[4]
-            };
+      case 'slave': {
+        const [role, masterHost, masterPort, state, dataReceived] = reply as SlaveRole;
+        return {
+          role,
+          master: {
+            host: masterHost,
+            port: masterPort
+          },
+          state,
+          dataReceived,
+        };
+      }
 
-        case 'sentinel':
-            return {
-                role: 'sentinel',
-                masterNames: reply[1]
-            };
+      case 'sentinel': {
+        const [role, masterNames] = reply as SentinelRole;
+        return {
+          role,
+          masterNames
+        };
+      }
     }
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RPOP.spec.ts b/packages/client/lib/commands/RPOP.spec.ts
index 6e57afa3216..844965eae1a 100644
--- a/packages/client/lib/commands/RPOP.spec.ts
+++ b/packages/client/lib/commands/RPOP.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RPOP';
+import RPOP from './RPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('RPOP', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['RPOP', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RPOP, 'key'),
+      ['RPOP', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.rPop', async client => {
-        assert.equal(
-            await client.rPop('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.rPop', async cluster => {
-        assert.equal(
-            await cluster.rPop('key'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('rPop', async client => {
+    assert.equal(
+      await client.rPop('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RPOP.ts b/packages/client/lib/commands/RPOP.ts
index ed696b6d522..4e284496579 100644
--- a/packages/client/lib/commands/RPOP.ts
+++ b/packages/client/lib/commands/RPOP.ts
@@ -1,9 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['RPOP', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  /**
+   * Constructs the RPOP command
+   * 
+   * @param parser - The command parser
+   * @param key - The list key to pop from
+   * @see https://redis.io/commands/rpop/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('RPOP');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RPOPLPUSH.spec.ts b/packages/client/lib/commands/RPOPLPUSH.spec.ts
index cef3049bd91..728d600bc9d 100644
--- a/packages/client/lib/commands/RPOPLPUSH.spec.ts
+++ b/packages/client/lib/commands/RPOPLPUSH.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RPOPLPUSH';
+import RPOPLPUSH from './RPOPLPUSH';
+import { parseArgs } from './generic-transformers';
 
 describe('RPOPLPUSH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('source', 'destination'),
-            ['RPOPLPUSH', 'source', 'destination']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RPOPLPUSH, 'source', 'destination'),
+      ['RPOPLPUSH', 'source', 'destination']
+    );
+  });
 
-    testUtils.testWithClient('client.rPopLPush', async client => {
-        assert.equal(
-            await client.rPopLPush('source', 'destination'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.rPopLPush', async cluster => {
-        assert.equal(
-            await cluster.rPopLPush('{tag}source', '{tag}destination'),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('rPopLPush', async client => {
+    assert.equal(
+      await client.rPopLPush('{tag}source', '{tag}destination'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RPOPLPUSH.ts b/packages/client/lib/commands/RPOPLPUSH.ts
index da45f6f6024..936aeb01c8f 100644
--- a/packages/client/lib/commands/RPOPLPUSH.ts
+++ b/packages/client/lib/commands/RPOPLPUSH.ts
@@ -1,12 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    source: RedisCommandArgument,
-    destination: RedisCommandArgument
-): RedisCommandArguments {
-    return ['RPOPLPUSH', source, destination];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  /**
+   * Constructs the RPOPLPUSH command
+   * 
+   * @param parser - The command parser
+   * @param source - The source list key
+   * @param destination - The destination list key
+   * @see https://redis.io/commands/rpoplpush/
+   */
+  parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument) {
+    parser.push('RPOPLPUSH');
+    parser.pushKeys([source, destination]);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RPOP_COUNT.spec.ts b/packages/client/lib/commands/RPOP_COUNT.spec.ts
index 3657a608039..e055d8655b5 100644
--- a/packages/client/lib/commands/RPOP_COUNT.spec.ts
+++ b/packages/client/lib/commands/RPOP_COUNT.spec.ts
@@ -1,28 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RPOP_COUNT';
+import RPOP_COUNT from './RPOP_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('RPOP COUNT', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['RPOP', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RPOP_COUNT, 'key', 1),
+      ['RPOP', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.rPopCount', async client => {
-        assert.equal(
-            await client.rPopCount('key', 1),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.rPopCount', async cluster => {
-        assert.equal(
-            await cluster.rPopCount('key', 1),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('rPopCount', async client => {
+    assert.equal(
+      await client.rPopCount('key', 1),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RPOP_COUNT.ts b/packages/client/lib/commands/RPOP_COUNT.ts
index b3bc778ee5c..2a60335da94 100644
--- a/packages/client/lib/commands/RPOP_COUNT.ts
+++ b/packages/client/lib/commands/RPOP_COUNT.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return ['RPOP', key, count.toString()];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument> | null;
+export default {
+  /**
+   * Constructs the RPOP command with count parameter
+   * 
+   * @param parser - The command parser
+   * @param key - The list key to pop from
+   * @param count - The number of elements to pop
+   * @see https://redis.io/commands/rpop/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    parser.push('RPOP');
+    parser.pushKey(key);
+    parser.push(count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RPUSH.spec.ts b/packages/client/lib/commands/RPUSH.spec.ts
index afa5c1c6400..559fb7a2746 100644
--- a/packages/client/lib/commands/RPUSH.spec.ts
+++ b/packages/client/lib/commands/RPUSH.spec.ts
@@ -1,35 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RPUSH';
+import RPUSH from './RPUSH';
+import { parseArgs } from './generic-transformers';
 
 describe('RPUSH', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element'),
-                ['RPUSH', 'key', 'element']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['RPUSH', 'key', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(RPUSH, 'key', 'element'),
+        ['RPUSH', 'key', 'element']
+      );
     });
 
-    testUtils.testWithClient('client.rPush', async client => {
-        assert.equal(
-            await client.rPush('key', 'element'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(RPUSH, 'key', ['1', '2']),
+        ['RPUSH', 'key', '1', '2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.rPush', async cluster => {
-        assert.equal(
-            await cluster.rPush('key', 'element'),
-            1
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('rPush', async client => {
+    assert.equal(
+      await client.rPush('key', 'element'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RPUSH.ts b/packages/client/lib/commands/RPUSH.ts
index 15e282f0892..452623e7f0d 100644
--- a/packages/client/lib/commands/RPUSH.ts
+++ b/packages/client/lib/commands/RPUSH.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    element: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['RPUSH', key], element);
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the RPUSH command
+   * 
+   * @param parser - The command parser
+   * @param key - The list key to push to
+   * @param element - One or more elements to push
+   * @see https://redis.io/commands/rpush/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element: RedisVariadicArgument) {
+    parser.push('RPUSH');
+    parser.pushKey(key);
+    parser.pushVariadic(element);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/RPUSHX.spec.ts b/packages/client/lib/commands/RPUSHX.spec.ts
index ee2041de6f2..b9fb660c5bc 100644
--- a/packages/client/lib/commands/RPUSHX.spec.ts
+++ b/packages/client/lib/commands/RPUSHX.spec.ts
@@ -1,35 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RPUSHX';
+import RPUSHX from './RPUSHX';
+import { parseArgs } from './generic-transformers';
 
 describe('RPUSHX', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'element'),
-                ['RPUSHX', 'key', 'element']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['RPUSHX', 'key', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(RPUSHX, 'key', 'element'),
+        ['RPUSHX', 'key', 'element']
+      );
     });
 
-    testUtils.testWithClient('client.rPushX', async client => {
-        assert.equal(
-            await client.rPushX('key', 'element'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(RPUSHX, 'key', ['1', '2']),
+        ['RPUSHX', 'key', '1', '2']
+      );
+    });
+  });
 
-    testUtils.testWithCluster('cluster.rPushX', async cluster => {
-        assert.equal(
-            await cluster.rPushX('key', 'element'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('rPushX', async client => {
+    assert.equal(
+      await client.rPushX('key', 'element'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/RPUSHX.ts b/packages/client/lib/commands/RPUSHX.ts
index 29253cd6edb..a9ec4bd1ef6 100644
--- a/packages/client/lib/commands/RPUSHX.ts
+++ b/packages/client/lib/commands/RPUSHX.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    element: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['RPUSHX', key], element);
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the RPUSHX command
+   * 
+   * @param parser - The command parser
+   * @param key - The list key to push to (only if it exists)
+   * @param element - One or more elements to push
+   * @see https://redis.io/commands/rpushx/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element: RedisVariadicArgument) {
+    parser.push('RPUSHX');
+    parser.pushKey(key);
+    parser.pushVariadic(element);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SADD.spec.ts b/packages/client/lib/commands/SADD.spec.ts
index 4533f6f9ad5..179e8602efc 100644
--- a/packages/client/lib/commands/SADD.spec.ts
+++ b/packages/client/lib/commands/SADD.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SADD';
+import SADD from './SADD';
+import { parseArgs } from './generic-transformers';
 
 describe('SADD', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member'),
-                ['SADD', 'key', 'member']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SADD, 'key', 'member'),
+        ['SADD', 'key', 'member']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['SADD', 'key', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SADD, 'key', ['1', '2']),
+        ['SADD', 'key', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sAdd', async client => {
-        assert.equal(
-            await client.sAdd('key', 'member'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sAdd', async client => {
+    assert.equal(
+      await client.sAdd('key', 'member'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SADD.ts b/packages/client/lib/commands/SADD.ts
index 7d7121e5391..3ee55706b95 100644
--- a/packages/client/lib/commands/SADD.ts
+++ b/packages/client/lib/commands/SADD.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    members: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SADD', key], members);
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the SADD command
+   * 
+   * @param parser - The command parser
+   * @param key - The set key to add members to
+   * @param members - One or more members to add to the set
+   * @see https://redis.io/commands/sadd/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) {
+    parser.push('SADD');
+    parser.pushKey(key);
+    parser.pushVariadic(members);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SAVE.spec.ts b/packages/client/lib/commands/SAVE.spec.ts
index 1e1987b5ab8..5f0074f7492 100644
--- a/packages/client/lib/commands/SAVE.spec.ts
+++ b/packages/client/lib/commands/SAVE.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './SAVE';
+import { strict as assert } from 'node:assert';
+import SAVE from './SAVE';
+import { parseArgs } from './generic-transformers';
 
 describe('SAVE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['SAVE']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SAVE),
+      ['SAVE']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/SAVE.ts b/packages/client/lib/commands/SAVE.ts
index 3d75c29df90..078b14da7a3 100644
--- a/packages/client/lib/commands/SAVE.ts
+++ b/packages/client/lib/commands/SAVE.ts
@@ -1,7 +1,17 @@
-import { RedisCommandArgument } from '.';
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export function transformArguments(): Array<string> {
-    return ['SAVE'];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SAVE command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/save/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('SAVE');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCAN.spec.ts b/packages/client/lib/commands/SCAN.spec.ts
index 7657b744e02..2a32cbebf4f 100644
--- a/packages/client/lib/commands/SCAN.spec.ts
+++ b/packages/client/lib/commands/SCAN.spec.ts
@@ -1,84 +1,63 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './SCAN';
+import { parseArgs } from './generic-transformers';
+import SCAN from './SCAN';
 
 describe('SCAN', () => {
-    describe('transformArguments', () => {
-        it('cusror only', () => {
-            assert.deepEqual(
-                transformArguments(0),
-                ['SCAN', '0']
-            );
-        });
-
-        it('with MATCH', () => {
-            assert.deepEqual(
-                transformArguments(0, {
-                    MATCH: 'pattern'
-                }),
-                ['SCAN', '0', 'MATCH', 'pattern']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments(0, {
-                    COUNT: 1
-                }),
-                ['SCAN', '0', 'COUNT', '1']
-            );
-        });
-
-        it('with TYPE', () => {
-            assert.deepEqual(
-                transformArguments(0, {
-                    TYPE: 'stream'
-                }),
-                ['SCAN', '0', 'TYPE', 'stream']
-            );
-        });
+  describe('transformArguments', () => {
+    it('cusror only', () => {
+      assert.deepEqual(
+        parseArgs(SCAN, '0'),
+        ['SCAN', '0']
+      );
+    });
 
-        it('with MATCH & COUNT & TYPE', () => {
-            assert.deepEqual(
-                transformArguments(0, {
-                    MATCH: 'pattern',
-                    COUNT: 1,
-                    TYPE: 'stream'
-                }),
-                ['SCAN', '0', 'MATCH', 'pattern', 'COUNT', '1', 'TYPE', 'stream']
-            );
-        });
+    it('with MATCH', () => {
+      assert.deepEqual(
+        parseArgs(SCAN, '0', {
+          MATCH: 'pattern'
+        }),
+        ['SCAN', '0', 'MATCH', 'pattern']
+      );
     });
 
-    describe('transformReply', () => {
-        it('without keys', () => {
-            assert.deepEqual(
-                transformReply(['0', []]),
-                {
-                    cursor: 0,
-                    keys: []
-                }
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(SCAN, '0', {
+          COUNT: 1
+        }),
+        ['SCAN', '0', 'COUNT', '1']
+      );
+    });
 
-        it('with keys', () => {
-            assert.deepEqual(
-                transformReply(['0', ['key']]),
-                {
-                    cursor: 0,
-                    keys: ['key']
-                }
-            );
-        });
+    it('with TYPE', () => {
+      assert.deepEqual(
+        parseArgs(SCAN, '0', {
+          TYPE: 'stream'
+        }),
+        ['SCAN', '0', 'TYPE', 'stream']
+      );
     });
 
-    testUtils.testWithClient('client.scan', async client => {
-        assert.deepEqual(
-            await client.scan(0),
-            {
-                cursor: 0,
-                keys: []
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with MATCH & COUNT & TYPE', () => {
+      assert.deepEqual(
+        parseArgs(SCAN, '0', {
+          MATCH: 'pattern',
+          COUNT: 1,
+          TYPE: 'stream'
+        }),
+        ['SCAN', '0', 'MATCH', 'pattern', 'COUNT', '1', 'TYPE', 'stream']
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.scan', async client => {
+    assert.deepEqual(
+      await client.scan('0'),
+      {
+        cursor: '0',
+        keys: []
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts
index ee5908eb9bd..d3153b786f1 100644
--- a/packages/client/lib/commands/SCAN.ts
+++ b/packages/client/lib/commands/SCAN.ts
@@ -1,34 +1,103 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { ScanOptions, pushScanArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, CommandArguments, BlobStringReply, ArrayReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-export interface ScanCommandOptions extends ScanOptions {
-    TYPE?: RedisCommandArgument;
+/**
+ * Common options for SCAN-type commands
+ *
+ * @property MATCH - Pattern to filter returned keys
+ * @property COUNT - Hint for how many elements to return per iteration
+ */
+export interface ScanCommonOptions {
+  MATCH?: string;
+  COUNT?: number;
 }
 
-export function transformArguments(
-    cursor: number,
-    options?: ScanCommandOptions
-): RedisCommandArguments {
-    const args = pushScanArguments(['SCAN'], cursor, options);
+/**
+ * Parses scan arguments for SCAN-type commands
+ *
+ * @param parser - The command parser
+ * @param cursor - The cursor position for iteration
+ * @param options - Scan options
+ */
+export function parseScanArguments(
+  parser: CommandParser,
+  cursor: RedisArgument,
+  options?: ScanOptions
+) {
+  parser.push(cursor);
+  if (options?.MATCH) {
+    parser.push('MATCH', options.MATCH);
+  }
 
-    if (options?.TYPE) {
-        args.push('TYPE', options.TYPE);
-    }
-
-    return args;
+  if (options?.COUNT) {
+    parser.push('COUNT', options.COUNT.toString());
+  }
 }
 
-type ScanRawReply = [string, Array<string>];
+/**
+ * Pushes scan arguments to the command arguments array
+ *
+ * @param args - The command arguments array
+ * @param cursor - The cursor position for iteration
+ * @param options - Scan options
+ * @returns The updated command arguments array
+ */
+export function pushScanArguments(
+  args: CommandArguments,
+  cursor: RedisArgument,
+  options?: ScanOptions
+): CommandArguments {
+  args.push(cursor.toString());
 
-export interface ScanReply {
-    cursor: number;
-    keys: Array<RedisCommandArgument>;
+  if (options?.MATCH) {
+    args.push('MATCH', options.MATCH);
+  }
+
+  if (options?.COUNT) {
+    args.push('COUNT', options.COUNT.toString());
+  }
+
+  return args;
 }
 
-export function transformReply([cursor, keys]: ScanRawReply): ScanReply {
+/**
+ * Options for the SCAN command
+ *
+ * @property TYPE - Filter by value type
+ */
+export interface ScanOptions extends ScanCommonOptions {
+  TYPE?: RedisArgument;
+}
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCAN command
+   *
+   * @param parser - The command parser
+   * @param cursor - The cursor position to start scanning from
+   * @param options - Scan options
+   * @see https://redis.io/commands/scan/
+   */
+  parseCommand(parser: CommandParser, cursor: RedisArgument, options?: ScanOptions) {
+    parser.push('SCAN');
+    parseScanArguments(parser, cursor, options);
+
+    if (options?.TYPE) {
+      parser.push('TYPE', options.TYPE);
+    }
+  },
+  /**
+   * Transforms the SCAN reply into a structured object
+   *
+   * @param reply - The raw reply containing cursor and keys
+   * @returns Object with cursor and keys properties
+   */
+  transformReply([cursor, keys]: [BlobStringReply, ArrayReply<BlobStringReply>]) {
     return {
-        cursor: Number(cursor),
-        keys
+      cursor,
+      keys
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCARD.spec.ts b/packages/client/lib/commands/SCARD.spec.ts
index afc21c6b00c..53434583832 100644
--- a/packages/client/lib/commands/SCARD.spec.ts
+++ b/packages/client/lib/commands/SCARD.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SCARD';
+import { parseArgs } from './generic-transformers';
+import SCARD from './SCARD';
 
 describe('SCARD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['SCARD', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SCARD, 'key'),
+      ['SCARD', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.sCard', async client => {
-        assert.equal(
-            await client.sCard('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sCard', async client => {
+    assert.equal(
+      await client.sCard('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SCARD.ts b/packages/client/lib/commands/SCARD.ts
index 0d3ce49b6b2..20a2aefae00 100644
--- a/packages/client/lib/commands/SCARD.ts
+++ b/packages/client/lib/commands/SCARD.ts
@@ -1,7 +1,19 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(key: string): Array<string> {
-    return ['SCARD', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCARD command
+   * 
+   * @param parser - The command parser
+   * @param key - The set key to get the cardinality of
+   * @see https://redis.io/commands/scard/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('SCARD');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts
index 192f90f75a5..c98143a3415 100644
--- a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts
+++ b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SCRIPT_DEBUG';
+import SCRIPT_DEBUG from './SCRIPT_DEBUG';
+import { parseArgs } from './generic-transformers';
 
 describe('SCRIPT DEBUG', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('NO'),
-            ['SCRIPT', 'DEBUG', 'NO']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SCRIPT_DEBUG, 'NO'),
+      ['SCRIPT', 'DEBUG', 'NO']
+    );
+  });
 
-    testUtils.testWithClient('client.scriptDebug', async client => {
-        assert.equal(
-            await client.scriptDebug('NO'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.scriptDebug', async client => {
+    assert.equal(
+      await client.scriptDebug('NO'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.ts b/packages/client/lib/commands/SCRIPT_DEBUG.ts
index e9e1e909d59..3f09c550449 100644
--- a/packages/client/lib/commands/SCRIPT_DEBUG.ts
+++ b/packages/client/lib/commands/SCRIPT_DEBUG.ts
@@ -1,5 +1,18 @@
-export function transformArguments(mode: 'YES' | 'SYNC' | 'NO'): Array<string> {
-    return ['SCRIPT', 'DEBUG', mode];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCRIPT DEBUG command
+   * 
+   * @param parser - The command parser
+   * @param mode - Debug mode: YES, SYNC, or NO
+   * @see https://redis.io/commands/script-debug/
+   */
+  parseCommand(parser: CommandParser, mode: 'YES' | 'SYNC' | 'NO') {
+    parser.push('SCRIPT', 'DEBUG', mode);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts
index e0fbbcc5537..cf65156c72d 100644
--- a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts
+++ b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SCRIPT_EXISTS';
+import SCRIPT_EXISTS from './SCRIPT_EXISTS';
+import { parseArgs } from './generic-transformers';
 
 describe('SCRIPT EXISTS', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('sha1'),
-                ['SCRIPT', 'EXISTS', 'sha1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SCRIPT_EXISTS, 'sha1'),
+        ['SCRIPT', 'EXISTS', 'sha1']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['SCRIPT', 'EXISTS', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SCRIPT_EXISTS, ['1', '2']),
+        ['SCRIPT', 'EXISTS', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.scriptExists', async client => {
-        assert.deepEqual(
-            await client.scriptExists('sha1'),
-            [false]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.scriptExists', async client => {
+    assert.deepEqual(
+      await client.scriptExists('sha1'),
+      [0]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.ts b/packages/client/lib/commands/SCRIPT_EXISTS.ts
index cee889215d3..66479654a0d 100644
--- a/packages/client/lib/commands/SCRIPT_EXISTS.ts
+++ b/packages/client/lib/commands/SCRIPT_EXISTS.ts
@@ -1,8 +1,20 @@
-import { RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export function transformArguments(sha1: string | Array<string>): RedisCommandArguments {
-    return pushVerdictArguments(['SCRIPT', 'EXISTS'], sha1);
-}
-
-export { transformBooleanArrayReply as transformReply } from './generic-transformers';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCRIPT EXISTS command
+   * 
+   * @param parser - The command parser
+   * @param sha1 - One or more SHA1 digests of scripts
+   * @see https://redis.io/commands/script-exists/
+   */
+  parseCommand(parser: CommandParser, sha1: RedisVariadicArgument) {
+    parser.push('SCRIPT', 'EXISTS');
+    parser.pushVariadic(sha1);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts
index ae156e937d1..c51efd1a36c 100644
--- a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts
+++ b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SCRIPT_FLUSH';
+import SCRIPT_FLUSH from './SCRIPT_FLUSH';
+import { parseArgs } from './generic-transformers';
 
 describe('SCRIPT FLUSH', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['SCRIPT', 'FLUSH']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(SCRIPT_FLUSH),
+        ['SCRIPT', 'FLUSH']
+      );
+    });
 
-        it('with mode', () => {
-            assert.deepEqual(
-                transformArguments('SYNC'),
-                ['SCRIPT', 'FLUSH', 'SYNC']
-            );
-        });
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(SCRIPT_FLUSH, 'SYNC'),
+        ['SCRIPT', 'FLUSH', 'SYNC']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.scriptFlush', async client => {
-        assert.equal(
-            await client.scriptFlush(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.scriptFlush', async client => {
+    assert.equal(
+      await client.scriptFlush(),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.ts b/packages/client/lib/commands/SCRIPT_FLUSH.ts
index 2c220e9e3d1..91b61a4e59a 100644
--- a/packages/client/lib/commands/SCRIPT_FLUSH.ts
+++ b/packages/client/lib/commands/SCRIPT_FLUSH.ts
@@ -1,11 +1,22 @@
-export function transformArguments(mode?: 'ASYNC' | 'SYNC'): Array<string> {
-    const args = ['SCRIPT', 'FLUSH'];
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCRIPT FLUSH command
+   * 
+   * @param parser - The command parser
+   * @param mode - Optional flush mode: ASYNC or SYNC
+   * @see https://redis.io/commands/script-flush/
+   */
+  parseCommand(parser: CommandParser, mode?: 'ASYNC' | 'SYNC') {
+    parser.push('SCRIPT', 'FLUSH');
 
     if (mode) {
-        args.push(mode);
+      parser.push(mode);
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCRIPT_KILL.spec.ts b/packages/client/lib/commands/SCRIPT_KILL.spec.ts
index e57265aa61a..7186efd54cf 100644
--- a/packages/client/lib/commands/SCRIPT_KILL.spec.ts
+++ b/packages/client/lib/commands/SCRIPT_KILL.spec.ts
@@ -1,11 +1,12 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './SCRIPT_KILL';
+import { strict as assert } from 'node:assert';
+import SCRIPT_KILL from './SCRIPT_KILL';
+import { parseArgs } from './generic-transformers';
 
 describe('SCRIPT KILL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['SCRIPT', 'KILL']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SCRIPT_KILL),
+      ['SCRIPT', 'KILL']
+    );
+  });
 });
diff --git a/packages/client/lib/commands/SCRIPT_KILL.ts b/packages/client/lib/commands/SCRIPT_KILL.ts
index c0a53da8681..ee2b2835cc1 100644
--- a/packages/client/lib/commands/SCRIPT_KILL.ts
+++ b/packages/client/lib/commands/SCRIPT_KILL.ts
@@ -1,5 +1,17 @@
-export function transformArguments(): Array<string> {
-    return ['SCRIPT', 'KILL'];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCRIPT KILL command
+   * 
+   * @param parser - The command parser
+   * @see https://redis.io/commands/script-kill/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('SCRIPT', 'KILL');
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts
index 062f3c201e1..b0df9887e11 100644
--- a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts
+++ b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts
@@ -1,23 +1,24 @@
-import { strict as assert } from 'assert';
-import { scriptSha1 } from '../lua-script';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SCRIPT_LOAD';
+import SCRIPT_LOAD from './SCRIPT_LOAD';
+import { scriptSha1 } from '../lua-script';
+import { parseArgs } from './generic-transformers';
 
 describe('SCRIPT LOAD', () => {
-    const SCRIPT = 'return 1;',
-        SCRIPT_SHA1 = scriptSha1(SCRIPT);
+  const SCRIPT = 'return 1;',
+    SCRIPT_SHA1 = scriptSha1(SCRIPT);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(SCRIPT),
-            ['SCRIPT', 'LOAD', SCRIPT]
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SCRIPT_LOAD, SCRIPT),
+      ['SCRIPT', 'LOAD', SCRIPT]
+    );
+  });
 
-    testUtils.testWithClient('client.scriptLoad', async client => {
-        assert.equal(
-            await client.scriptLoad(SCRIPT),
-            SCRIPT_SHA1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.scriptLoad', async client => {
+    assert.equal(
+      await client.scriptLoad(SCRIPT),
+      SCRIPT_SHA1
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/SCRIPT_LOAD.ts b/packages/client/lib/commands/SCRIPT_LOAD.ts
index 7cb28c1ec7f..6e9acb388fc 100644
--- a/packages/client/lib/commands/SCRIPT_LOAD.ts
+++ b/packages/client/lib/commands/SCRIPT_LOAD.ts
@@ -1,5 +1,18 @@
-export function transformArguments(script: string): Array<string> {
-    return ['SCRIPT', 'LOAD', script];
-}
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command, RedisArgument } from '../RESP/types';
 
-export declare function transformReply(): string;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SCRIPT LOAD command
+   * 
+   * @param parser - The command parser
+   * @param script - The Lua script to load
+   * @see https://redis.io/commands/script-load/
+   */
+  parseCommand(parser: CommandParser, script: RedisArgument) {
+    parser.push('SCRIPT', 'LOAD', script);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SDIFF.spec.ts b/packages/client/lib/commands/SDIFF.spec.ts
index 340906e9350..a943a80688d 100644
--- a/packages/client/lib/commands/SDIFF.spec.ts
+++ b/packages/client/lib/commands/SDIFF.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SDIFF';
+import SDIFF from './SDIFF';
+import { parseArgs } from './generic-transformers';
 
 describe('SDIFF', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['SDIFF', 'key']
-            );
-        });
+  describe('processCommand', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SDIFF, 'key'),
+        ['SDIFF', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['SDIFF', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SDIFF, ['1', '2']),
+        ['SDIFF', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sDiff', async client => {
-        assert.deepEqual(
-            await client.sDiff('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sDiff', async client => {
+    assert.deepEqual(
+      await client.sDiff('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SDIFF.ts b/packages/client/lib/commands/SDIFF.ts
index 9c4f3b4820b..07d700adac6 100644
--- a/packages/client/lib/commands/SDIFF.ts
+++ b/packages/client/lib/commands/SDIFF.ts
@@ -1,14 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SDIFF'], keys);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SDIFF command
+   * 
+   * @param parser - The command parser
+   * @param keys - One or more set keys to compute the difference from
+   * @see https://redis.io/commands/sdiff/
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('SDIFF');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SDIFFSTORE.spec.ts b/packages/client/lib/commands/SDIFFSTORE.spec.ts
index 263b4f43f64..43213adfbb0 100644
--- a/packages/client/lib/commands/SDIFFSTORE.spec.ts
+++ b/packages/client/lib/commands/SDIFFSTORE.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SDIFFSTORE';
+import SDIFFSTORE from './SDIFFSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('SDIFFSTORE', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key'),
-                ['SDIFFSTORE', 'destination', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SDIFFSTORE, 'destination', 'key'),
+        ['SDIFFSTORE', 'destination', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['SDIFFSTORE', 'destination', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SDIFFSTORE, 'destination', ['1', '2']),
+        ['SDIFFSTORE', 'destination', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sDiffStore', async client => {
-        assert.equal(
-            await client.sDiffStore('destination', 'key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sDiffStore', async client => {
+    assert.equal(
+      await client.sDiffStore('{tag}destination', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SDIFFSTORE.ts b/packages/client/lib/commands/SDIFFSTORE.ts
index a927e12ef0e..478d015d8c0 100644
--- a/packages/client/lib/commands/SDIFFSTORE.ts
+++ b/packages/client/lib/commands/SDIFFSTORE.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    destination: RedisCommandArgument,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SDIFFSTORE', destination], keys);
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the SDIFFSTORE command
+   * 
+   * @param parser - The command parser
+   * @param destination - The destination key to store the result
+   * @param keys - One or more set keys to compute the difference from
+   * @see https://redis.io/commands/sdiffstore/
+   */
+  parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) {
+    parser.push('SDIFFSTORE');
+    parser.pushKey(destination);
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SET.spec.ts b/packages/client/lib/commands/SET.spec.ts
index 0b3331fd3a4..b8aa57fe77b 100644
--- a/packages/client/lib/commands/SET.spec.ts
+++ b/packages/client/lib/commands/SET.spec.ts
@@ -1,129 +1,165 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SET';
+import SET from './SET';
+import { parseArgs } from './generic-transformers';
 
 describe('SET', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'value'),
-                ['SET', 'key', 'value']
-            );
-        });
+  describe('transformArguments', () => {
+    describe('value', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value'),
+          ['SET', 'key', 'value']
+        );
+      });
+  
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 0),
+          ['SET', 'key', '0']
+        );
+      });
+    });
+
+    describe('expiration', () => {
+      it('\'KEEPTTL\'', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            expiration: 'KEEPTTL'
+          }),
+          ['SET', 'key', 'value', 'KEEPTTL']
+        );
+      });
 
-        it('number', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['SET', 'key', '0']
-            );
-        });
+      it('{ type: \'KEEPTTL\' }', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            expiration: {
+              type: 'KEEPTTL'
+            }
+          }),
+          ['SET', 'key', 'value', 'KEEPTTL']
+        );
+      });
 
-        describe('TTL', () => {
-            it('with EX', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        EX: 0
-                    }),
-                    ['SET', 'key', 'value', 'EX', '0']
-                );
-            });
+      it('{ type: \'EX\' }', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            expiration: {
+              type: 'EX',
+              value: 0
+            }
+          }),
+          ['SET', 'key', 'value', 'EX', '0']
+        );
+      });
 
-            it('with PX', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        PX: 0
-                    }),
-                    ['SET', 'key', 'value', 'PX', '0']
-                );
-            });
+      it('with EX (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            EX: 0
+          }),
+          ['SET', 'key', 'value', 'EX', '0']
+        );
+      });
 
-            it('with EXAT', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        EXAT: 0
-                    }),
-                    ['SET', 'key', 'value', 'EXAT', '0']
-                );
-            });
+      it('with PX (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            PX: 0
+          }),
+          ['SET', 'key', 'value', 'PX', '0']
+        );
+      });
 
-            it('with PXAT', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        PXAT: 0
-                    }),
-                    ['SET', 'key', 'value', 'PXAT', '0']
-                );
-            });
+      it('with EXAT (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            EXAT: 0
+          }),
+          ['SET', 'key', 'value', 'EXAT', '0']
+        );
+      });
 
-            it('with KEEPTTL', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        KEEPTTL: true
-                    }),
-                    ['SET', 'key', 'value', 'KEEPTTL']
-                );
-            });
-        });
+      it('with PXAT (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            PXAT: 0
+          }),
+          ['SET', 'key', 'value', 'PXAT', '0']
+        );
+      });
 
-        describe('Guards', () => {
-            it('with NX', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        NX: true
-                    }),
-                    ['SET', 'key', 'value', 'NX']
-                );
-            });
+      it('with KEEPTTL (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            KEEPTTL: true
+          }),
+          ['SET', 'key', 'value', 'KEEPTTL']
+        );
+      });
+    });
 
-            it('with XX', () => {
-                assert.deepEqual(
-                    transformArguments('key', 'value', {
-                        XX: true
-                    }),
-                    ['SET', 'key', 'value', 'XX']
-                );
-            });
-        });
+    describe('condition', () => {
+      it('with condition', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            condition: 'NX'
+          }),
+          ['SET', 'key', 'value', 'NX']
+        );
+      });
 
-        it('with GET', () => {
-            assert.deepEqual(
-                transformArguments('key', 'value', {
-                    GET: true
-                }),
-                ['SET', 'key', 'value', 'GET']
-            );
-        });
+      it('with NX (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            NX: true
+          }),
+          ['SET', 'key', 'value', 'NX']
+        );
+      });
 
-        it('with EX, NX, GET', () => {
-            assert.deepEqual(
-                transformArguments('key', 'value', {
-                    EX: 1,
-                    NX: true,
-                    GET: true
-                }),
-                ['SET', 'key', 'value', 'EX', '1', 'NX', 'GET']
-            );
-        });
+      it('with XX (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(SET, 'key', 'value', {
+            XX: true
+          }),
+          ['SET', 'key', 'value', 'XX']
+        );
+      });
     });
 
-    describe('client.set', () => {
-        testUtils.testWithClient('simple', async client => {
-            assert.equal(
-                await client.set('key', 'value'),
-                'OK'
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with GET', () => {
+      assert.deepEqual(
+        parseArgs(SET, 'key', 'value', {
+          GET: true
+        }),
+        ['SET', 'key', 'value', 'GET']
+      );
+    });
 
-        testUtils.testWithClient('with GET on empty key', async client => {
-            assert.equal(
-                await client.set('key', 'value', {
-                    GET: true
-                }),
-                null
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            minimumDockerVersion: [6, 2]
-        });
+    it('with expiration, condition, GET', () => {
+      assert.deepEqual(
+        parseArgs(SET, 'key', 'value', {
+          expiration: {
+            type: 'EX',
+            value: 0 
+          },
+          condition: 'NX',
+          GET: true
+        }),
+        ['SET', 'key', 'value', 'EX', '0', 'NX', 'GET']
+      );
     });
+  });
+
+  testUtils.testAll('set', async client => {
+    assert.equal(
+      await client.set('key', 'value'),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SET.ts b/packages/client/lib/commands/SET.ts
index 08ae56552b9..d1384255679 100644
--- a/packages/client/lib/commands/SET.ts
+++ b/packages/client/lib/commands/SET.ts
@@ -1,63 +1,96 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export interface SetOptions {
+  expiration?: {
+    type: 'EX' | 'PX' | 'EXAT' | 'PXAT';
+    value: number;
+  } | {
+    type: 'KEEPTTL';
+  } | 'KEEPTTL';
+  /**
+   * @deprecated Use `expiration` { type: 'EX', value: number } instead
+   */
+  EX?: number;
+  /**
+   * @deprecated Use `expiration` { type: 'PX', value: number } instead
+   */
+  PX?: number;
+  /**
+   * @deprecated Use `expiration` { type: 'EXAT', value: number } instead
+   */
+  EXAT?: number;
+  /**
+   * @deprecated Use `expiration` { type: 'PXAT', value: number } instead
+   */
+  PXAT?: number;
+  /**
+   * @deprecated Use `expiration` 'KEEPTTL' instead
+   */
+  KEEPTTL?: boolean;
 
-type MaximumOneOf<T, K extends keyof T = keyof T> =
-    K extends keyof T ? { [P in K]?: T[K] } & Partial<Record<Exclude<keyof T, K>, never>> : never;
-
-type SetTTL = MaximumOneOf<{
-    EX: number;
-    PX: number;
-    EXAT: number;
-    PXAT: number;
-    KEEPTTL: true;
-}>;
-
-type SetGuards = MaximumOneOf<{
-    NX: true;
-    XX: true;
-}>;
-
-interface SetCommonOptions {
-    GET?: true;
+  condition?: 'NX' | 'XX';
+  /**
+   * @deprecated Use `{ condition: 'NX' }` instead.
+   */
+  NX?: boolean;
+  /**
+   * @deprecated Use `{ condition: 'XX' }` instead.
+   */
+  XX?: boolean;
+  
+  GET?: boolean;
 }
 
-export type SetOptions = SetTTL & SetGuards & SetCommonOptions;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    value: RedisCommandArgument | number,
-    options?: SetOptions
-): RedisCommandArguments {
-    const args = [
-        'SET',
-        key,
-        typeof value === 'number' ? value.toString() : value
-    ];
+export default {
+  /**
+   * Constructs the SET command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set
+   * @param value - The value to set
+   * @param options - Additional options for the SET command
+   * @see https://redis.io/commands/set/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument | number, options?: SetOptions) {
+    parser.push('SET');
+    parser.pushKey(key);
+    parser.push(typeof value === 'number' ? value.toString() : value);
 
-    if (options?.EX !== undefined) {
-        args.push('EX', options.EX.toString());
+    if (options?.expiration) {
+      if (typeof options.expiration === 'string') {
+        parser.push(options.expiration);
+      } else if (options.expiration.type === 'KEEPTTL') {
+        parser.push('KEEPTTL');
+      } else {
+        parser.push(
+          options.expiration.type,
+          options.expiration.value.toString()
+        );
+      }
+    } else if (options?.EX !== undefined) {
+      parser.push('EX', options.EX.toString());
     } else if (options?.PX !== undefined) {
-        args.push('PX', options.PX.toString());
+      parser.push('PX', options.PX.toString());
     } else if (options?.EXAT !== undefined) {
-        args.push('EXAT', options.EXAT.toString());
+      parser.push('EXAT', options.EXAT.toString());
     } else if (options?.PXAT !== undefined) {
-        args.push('PXAT', options.PXAT.toString());
+      parser.push('PXAT', options.PXAT.toString());
     } else if (options?.KEEPTTL) {
-        args.push('KEEPTTL');
+      parser.push('KEEPTTL');
     }
 
-    if (options?.NX) {
-        args.push('NX');
+    if (options?.condition) {
+      parser.push(options.condition);
+    } else if (options?.NX) {
+      parser.push('NX');
     } else if (options?.XX) {
-        args.push('XX');
+      parser.push('XX');
     }
 
     if (options?.GET) {
-        args.push('GET');
+      parser.push('GET');
     }
-
-    return args;
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SETBIT.spec.ts b/packages/client/lib/commands/SETBIT.spec.ts
index 43fbff7c2d9..1eedcc69959 100644
--- a/packages/client/lib/commands/SETBIT.spec.ts
+++ b/packages/client/lib/commands/SETBIT.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SETBIT';
+import SETBIT from './SETBIT';
+import { parseArgs } from './generic-transformers';
 
 describe('SETBIT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 1),
-            ['SETBIT', 'key', '0', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SETBIT, 'key', 0, 1),
+      ['SETBIT', 'key', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.setBit', async client => {
-        assert.equal(
-            await client.setBit('key', 0, 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.setBit', async cluster => {
-        assert.equal(
-            await cluster.setBit('key', 0, 1),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('setBit', async client => {
+    assert.equal(
+      await client.setBit('key', 0, 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SETBIT.ts b/packages/client/lib/commands/SETBIT.ts
index 94f463330a8..b9c29796db9 100644
--- a/packages/client/lib/commands/SETBIT.ts
+++ b/packages/client/lib/commands/SETBIT.ts
@@ -1,14 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 import { BitValue } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    offset: number,
-    value: BitValue
-): RedisCommandArguments {
-    return ['SETBIT', key, offset.toString(), value.toString()];
-}
-
-export declare function transformReply(): BitValue;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SETBIT command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set the bit on
+   * @param offset - The bit offset (zero-based)
+   * @param value - The bit value (0 or 1)
+   * @see https://redis.io/commands/setbit/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: BitValue) {
+    parser.push('SETBIT');
+    parser.pushKey(key);
+    parser.push(offset.toString(), value.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply<BitValue>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SETEX.spec.ts b/packages/client/lib/commands/SETEX.spec.ts
index bca298c6c04..7bc934ccd68 100644
--- a/packages/client/lib/commands/SETEX.spec.ts
+++ b/packages/client/lib/commands/SETEX.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SETEX';
+import SETEX from './SETEX';
+import { parseArgs } from './generic-transformers';
 
 describe('SETEX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1, 'value'),
-            ['SETEX', 'key', '1', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SETEX, 'key', 1, 'value'),
+      ['SETEX', 'key', '1', 'value']
+    );
+  });
 
-    testUtils.testWithClient('client.setEx', async client => {
-        assert.equal(
-            await client.setEx('key', 1, 'value'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.setEx', async cluster => {
-        assert.equal(
-            await cluster.setEx('key', 1, 'value'),
-            'OK'
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('setEx', async client => {
+    assert.equal(
+      await client.setEx('key', 1, 'value'),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SETEX.ts b/packages/client/lib/commands/SETEX.ts
index bb3068501f0..39c7c60f53b 100644
--- a/packages/client/lib/commands/SETEX.ts
+++ b/packages/client/lib/commands/SETEX.ts
@@ -1,18 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    seconds: number,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'SETEX',
-        key,
-        seconds.toString(),
-        value
-    ];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  /**
+   * Constructs the SETEX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set
+   * @param seconds - The expiration time in seconds
+   * @param value - The value to set
+   * @see https://redis.io/commands/setex/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, seconds: number, value: RedisArgument) {
+    parser.push('SETEX');
+    parser.pushKey(key);
+    parser.push(seconds.toString(), value);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SETNX .spec.ts b/packages/client/lib/commands/SETNX .spec.ts
index c5bdfcffa2c..81a5af3d411 100644
--- a/packages/client/lib/commands/SETNX .spec.ts	
+++ b/packages/client/lib/commands/SETNX .spec.ts	
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SETNX';
+import SETNX from './SETNX';
+import { parseArgs } from './generic-transformers';
 
 describe('SETNX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'value'),
-            ['SETNX', 'key', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SETNX, 'key', 'value'),
+      ['SETNX', 'key', 'value']
+    );
+  });
 
-    testUtils.testWithClient('client.setNX', async client => {
-        assert.equal(
-            await client.setNX('key', 'value'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.setNX', async cluster => {
-        assert.equal(
-            await cluster.setNX('key', 'value'),
-            true
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('setNX', async client => {
+    assert.equal(
+      await client.setNX('key', 'value'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SETNX.ts b/packages/client/lib/commands/SETNX.ts
index b01d45dc32f..b32b6c5ef34 100644
--- a/packages/client/lib/commands/SETNX.ts
+++ b/packages/client/lib/commands/SETNX.ts
@@ -1,12 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return ['SETNX', key, value];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  /**
+   * Constructs the SETNX command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to set if it doesn't exist
+   * @param value - The value to set
+   * @see https://redis.io/commands/setnx/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) {
+    parser.push('SETNX');
+    parser.pushKey(key);
+    parser.push(value);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SETRANGE.spec.ts b/packages/client/lib/commands/SETRANGE.spec.ts
index 398b7730404..acdab5bcd3b 100644
--- a/packages/client/lib/commands/SETRANGE.spec.ts
+++ b/packages/client/lib/commands/SETRANGE.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SETRANGE';
+import SETRANGE from './SETRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('SETRANGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 'value'),
-            ['SETRANGE', 'key', '0', 'value']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SETRANGE, 'key', 0, 'value'),
+      ['SETRANGE', 'key', '0', 'value']
+    );
+  });
 
-    testUtils.testWithClient('client.setRange', async client => {
-        assert.equal(
-            await client.setRange('key', 0, 'value'),
-            5
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.setRange', async cluster => {
-        assert.equal(
-            await cluster.setRange('key', 0, 'value'),
-            5
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('setRange', async client => {
+    assert.equal(
+      await client.setRange('key', 0, 'value'),
+      5
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SETRANGE.ts b/packages/client/lib/commands/SETRANGE.ts
index 038a8a5dd7f..366e6c28a7d 100644
--- a/packages/client/lib/commands/SETRANGE.ts
+++ b/packages/client/lib/commands/SETRANGE.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    offset: number,
-    value: RedisCommandArgument
-): RedisCommandArguments {
-    return ['SETRANGE', key, offset.toString(), value];
-}
-
-export declare function transformReply(): number;
+export default {
+  /**
+   * Constructs the SETRANGE command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to modify
+   * @param offset - The offset at which to start writing
+   * @param value - The value to write at the offset
+   * @see https://redis.io/commands/setrange/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: RedisArgument) {
+    parser.push('SETRANGE');
+    parser.pushKey(key);
+    parser.push(offset.toString(), value);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SHUTDOWN.spec.ts b/packages/client/lib/commands/SHUTDOWN.spec.ts
index d58cf4443c7..9c4ca852ad3 100644
--- a/packages/client/lib/commands/SHUTDOWN.spec.ts
+++ b/packages/client/lib/commands/SHUTDOWN.spec.ts
@@ -1,27 +1,50 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './SHUTDOWN';
+import { strict as assert } from 'node:assert';
+import SHUTDOWN from './SHUTDOWN';
+import { parseArgs } from './generic-transformers';
 
 describe('SHUTDOWN', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(),
-                ['SHUTDOWN']
-            );
-        }); 
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(SHUTDOWN),
+        ['SHUTDOWN']
+      );
+    });
+
+    it('with mode', () => {
+      assert.deepEqual(
+        parseArgs(SHUTDOWN, {
+          mode: 'NOSAVE'
+        }),
+        ['SHUTDOWN', 'NOSAVE']
+      );
+    });
 
-        it('NOSAVE', () => {
-            assert.deepEqual(
-                transformArguments('NOSAVE'),
-                ['SHUTDOWN', 'NOSAVE']
-            );
-        });
+    it('with NOW', () => {
+      assert.deepEqual(
+        parseArgs(SHUTDOWN, {
+          NOW: true
+        }),
+        ['SHUTDOWN', 'NOW']
+      );
+    });
+
+    it('with FORCE', () => {
+      assert.deepEqual(
+        parseArgs(SHUTDOWN, {
+          FORCE: true
+        }),
+        ['SHUTDOWN', 'FORCE']
+      );
+    });
 
-        it('SAVE', () => {
-            assert.deepEqual(
-                transformArguments('SAVE'),
-                ['SHUTDOWN', 'SAVE']
-            );
-        });
+    it('with ABORT', () => {
+      assert.deepEqual(
+        parseArgs(SHUTDOWN, {
+          ABORT: true
+        }),
+        ['SHUTDOWN', 'ABORT']
+      );
     });
+  });
 });
diff --git a/packages/client/lib/commands/SHUTDOWN.ts b/packages/client/lib/commands/SHUTDOWN.ts
index 1990d05a2ed..6a5416d430c 100644
--- a/packages/client/lib/commands/SHUTDOWN.ts
+++ b/packages/client/lib/commands/SHUTDOWN.ts
@@ -1,11 +1,49 @@
-export function transformArguments(mode?: 'NOSAVE' | 'SAVE'): Array<string> {
-    const args = ['SHUTDOWN'];
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
 
-    if (mode) {
-        args.push(mode);
+/**
+ * Options for the SHUTDOWN command
+ * 
+ * @property mode - NOSAVE will not save DB, SAVE will force save DB
+ * @property NOW - Immediately terminate all clients
+ * @property FORCE - Force shutdown even in case of errors
+ * @property ABORT - Abort a shutdown in progress
+ */
+export interface ShutdownOptions {
+  mode?: 'NOSAVE' | 'SAVE';
+  NOW?: boolean;
+  FORCE?: boolean;
+  ABORT?: boolean;
+}
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SHUTDOWN command
+   * 
+   * @param parser - The command parser
+   * @param options - Options for the shutdown process
+   * @see https://redis.io/commands/shutdown/
+   */
+  parseCommand(parser: CommandParser, options?: ShutdownOptions) {
+    parser.push('SHUTDOWN');
+
+    if (options?.mode) {
+      parser.push(options.mode);
     }
 
-    return args;
-}
+    if (options?.NOW) {
+      parser.push('NOW');
+    }
+
+    if (options?.FORCE) {
+      parser.push('FORCE');
+    }
 
-export declare function transformReply(): void;
+    if (options?.ABORT) {
+      parser.push('ABORT');
+    }
+  },
+  transformReply: undefined as unknown as () => void | SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SINTER.spec.ts b/packages/client/lib/commands/SINTER.spec.ts
index 2324eac3ee8..6ca7b959ca7 100644
--- a/packages/client/lib/commands/SINTER.spec.ts
+++ b/packages/client/lib/commands/SINTER.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SINTER';
+import SINTER from './SINTER';
+import { parseArgs } from './generic-transformers';
 
 describe('SINTER', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['SINTER', 'key']
-            );
-        });
+  describe('processCommand', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SINTER, 'key'),
+        ['SINTER', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['SINTER', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SINTER, ['1', '2']),
+        ['SINTER', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sInter', async client => {
-        assert.deepEqual(
-            await client.sInter('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sInter', async client => {
+    assert.deepEqual(
+      await client.sInter('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SINTER.ts b/packages/client/lib/commands/SINTER.ts
index fe1feee7ade..a129d71fd7a 100644
--- a/packages/client/lib/commands/SINTER.ts
+++ b/packages/client/lib/commands/SINTER.ts
@@ -1,14 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SINTER'], keys);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SINTER command
+   * 
+   * @param parser - The command parser
+   * @param keys - One or more set keys to compute the intersection from
+   * @see https://redis.io/commands/sinter/
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('SINTER');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SINTERCARD.spec.ts b/packages/client/lib/commands/SINTERCARD.spec.ts
index a93699f6a13..51aed13415d 100644
--- a/packages/client/lib/commands/SINTERCARD.spec.ts
+++ b/packages/client/lib/commands/SINTERCARD.spec.ts
@@ -1,30 +1,43 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SINTERCARD';
+import SINTERCARD from './SINTERCARD';
+import { parseArgs } from './generic-transformers';
 
 describe('SINTERCARD', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['SINTERCARD', '2', '1', '2']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(SINTERCARD, ['1', '2']),
+        ['SINTERCARD', '2', '1', '2']
+      );
+    });
+
+    it('with limit (backwards compatibility)', () => {
+      assert.deepEqual(
+        parseArgs(SINTERCARD, ['1', '2'], 1),
+        ['SINTERCARD', '2', '1', '2', 'LIMIT', '1']
+      );
+    });
 
-        it('with limit', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2'], 1),
-                ['SINTERCARD', '2', '1', '2', 'LIMIT', '1']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(SINTERCARD, ['1', '2'], {
+          LIMIT: 1
+        }),
+        ['SINTERCARD', '2', '1', '2', 'LIMIT', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sInterCard', async client => {
-        assert.deepEqual(
-            await client.sInterCard('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sInterCard', async client => {
+    assert.deepEqual(
+      await client.sInterCard('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SINTERCARD.ts b/packages/client/lib/commands/SINTERCARD.ts
index ddb7e5b00ef..191c0881a8d 100644
--- a/packages/client/lib/commands/SINTERCARD.ts
+++ b/packages/client/lib/commands/SINTERCARD.ts
@@ -1,21 +1,35 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
+/**
+ * Options for the SINTERCARD command
+ * 
+ * @property LIMIT - Maximum number of elements to return
+ */
+export interface SInterCardOptions {
+  LIMIT?: number;
+}
 
-export function transformArguments(
-    keys: Array<RedisCommandArgument> | RedisCommandArgument,
-    limit?: number
-): RedisCommandArguments {
-    const args = pushVerdictArgument(['SINTERCARD'], keys);
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SINTERCARD command
+   * 
+   * @param parser - The command parser
+   * @param keys - One or more set keys to compute the intersection cardinality from
+   * @param options - Options for the SINTERCARD command or a number for LIMIT (backwards compatibility)
+   * @see https://redis.io/commands/sintercard/
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SInterCardOptions | number) {
+    parser.push('SINTERCARD');
+    parser.pushKeysLength(keys);
 
-    if (limit) {
-        args.push('LIMIT', limit.toString());
+    if (typeof options === 'number') { // backwards compatibility
+      parser.push('LIMIT', options.toString());
+    } else if (options?.LIMIT !== undefined) {
+      parser.push('LIMIT', options.LIMIT.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SINTERSTORE.spec.ts b/packages/client/lib/commands/SINTERSTORE.spec.ts
index c4a6a095e7d..83302a5c829 100644
--- a/packages/client/lib/commands/SINTERSTORE.spec.ts
+++ b/packages/client/lib/commands/SINTERSTORE.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SINTERSTORE';
+import SINTERSTORE from './SINTERSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('SINTERSTORE', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key'),
-                ['SINTERSTORE', 'destination', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SINTERSTORE, 'destination', 'key'),
+        ['SINTERSTORE', 'destination', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['SINTERSTORE', 'destination', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SINTERSTORE, 'destination', ['1', '2']),
+        ['SINTERSTORE', 'destination', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sInterStore', async client => {
-        assert.equal(
-            await client.sInterStore('destination', 'key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sInterStore', async client => {
+    assert.equal(
+      await client.sInterStore('{tag}destination', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SINTERSTORE.ts b/packages/client/lib/commands/SINTERSTORE.ts
index 02bf9d061a0..377b63fbddc 100644
--- a/packages/client/lib/commands/SINTERSTORE.ts
+++ b/packages/client/lib/commands/SINTERSTORE.ts
@@ -1,13 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    destination: RedisCommandArgument,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SINTERSTORE', destination], keys);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SINTERSTORE command
+   * 
+   * @param parser - The command parser
+   * @param destination - The destination key to store the result
+   * @param keys - One or more set keys to compute the intersection from
+   * @see https://redis.io/commands/sinterstore/
+   */
+  parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) {
+    parser.push('SINTERSTORE');
+    parser.pushKey(destination)
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SISMEMBER.spec.ts b/packages/client/lib/commands/SISMEMBER.spec.ts
index 8d18c83697a..4796475c52c 100644
--- a/packages/client/lib/commands/SISMEMBER.spec.ts
+++ b/packages/client/lib/commands/SISMEMBER.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SISMEMBER';
+import SISMEMBER from './SISMEMBER';
+import { parseArgs } from './generic-transformers';
 
 describe('SISMEMBER', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member'),
-            ['SISMEMBER', 'key', 'member']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(SISMEMBER, 'key', 'member'),
+      ['SISMEMBER', 'key', 'member']
+    );
+  });
 
-    testUtils.testWithClient('client.sIsMember', async client => {
-        assert.equal(
-            await client.sIsMember('key', 'member'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sIsMember', async client => {
+    assert.equal(
+      await client.sIsMember('key', 'member'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SISMEMBER.ts b/packages/client/lib/commands/SISMEMBER.ts
index 4d40c63250e..3310d43d97b 100644
--- a/packages/client/lib/commands/SISMEMBER.ts
+++ b/packages/client/lib/commands/SISMEMBER.ts
@@ -1,12 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument
-): RedisCommandArguments {
-    return ['SISMEMBER', key, member];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SISMEMBER command
+   * 
+   * @param parser - The command parser
+   * @param key - The set key to check membership in
+   * @param member - The member to check for existence
+   * @see https://redis.io/commands/sismember/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) {
+    parser.push('SISMEMBER');
+    parser.pushKey(key);
+    parser.push(member);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SMEMBERS.spec.ts b/packages/client/lib/commands/SMEMBERS.spec.ts
index b9c58c9eebb..6e2582e5abc 100644
--- a/packages/client/lib/commands/SMEMBERS.spec.ts
+++ b/packages/client/lib/commands/SMEMBERS.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SMEMBERS';
+import SMEMBERS from './SMEMBERS';
+import { parseArgs } from './generic-transformers';
 
 describe('SMEMBERS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['SMEMBERS', 'key']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(SMEMBERS, 'key'),
+      ['SMEMBERS', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.sMembers', async client => {
-        assert.deepEqual(
-            await client.sMembers('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sMembers', async client => {
+    assert.deepEqual(
+      await client.sMembers('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SMEMBERS.ts b/packages/client/lib/commands/SMEMBERS.ts
index 7950a4c073a..399ffd86147 100644
--- a/packages/client/lib/commands/SMEMBERS.ts
+++ b/packages/client/lib/commands/SMEMBERS.ts
@@ -1,9 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, SetReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['SMEMBERS', key];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SMEMBERS command
+   * 
+   * @param parser - The command parser
+   * @param key - The set key to get all members from
+   * @see https://redis.io/commands/smembers/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('SMEMBERS');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: undefined as unknown as () => ArrayReply<BlobStringReply>,
+    3: undefined as unknown as () => SetReply<BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SMISMEMBER.spec.ts b/packages/client/lib/commands/SMISMEMBER.spec.ts
index e3728134029..deff6912360 100644
--- a/packages/client/lib/commands/SMISMEMBER.spec.ts
+++ b/packages/client/lib/commands/SMISMEMBER.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SMISMEMBER';
+import SMISMEMBER from './SMISMEMBER';
+import { parseArgs } from './generic-transformers';
 
 describe('SMISMEMBER', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', ['1', '2']),
-            ['SMISMEMBER', 'key', '1', '2']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(SMISMEMBER, 'key', ['1', '2']),
+      ['SMISMEMBER', 'key', '1', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.smIsMember', async client => {
-        assert.deepEqual(
-            await client.smIsMember('key', ['1', '2']),
-            [false, false]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('smIsMember', async client => {
+    assert.deepEqual(
+      await client.smIsMember('key', ['1', '2']),
+      [0, 0]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SMISMEMBER.ts b/packages/client/lib/commands/SMISMEMBER.ts
index 175120bdfb9..b5950dcfd7f 100644
--- a/packages/client/lib/commands/SMISMEMBER.ts
+++ b/packages/client/lib/commands/SMISMEMBER.ts
@@ -1,12 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    members: Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return ['SMISMEMBER', key, ...members];
-}
-
-export { transformBooleanArrayReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SMISMEMBER command
+   * 
+   * @param parser - The command parser
+   * @param key - The set key to check membership in
+   * @param members - The members to check for existence
+   * @see https://redis.io/commands/smismember/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, members: Array<RedisArgument>) {
+    parser.push('SMISMEMBER');
+    parser.pushKey(key);
+    parser.pushVariadic(members);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SMOVE.spec.ts b/packages/client/lib/commands/SMOVE.spec.ts
index e3308ee8143..c68a6e41914 100644
--- a/packages/client/lib/commands/SMOVE.spec.ts
+++ b/packages/client/lib/commands/SMOVE.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SMOVE';
+import SMOVE from './SMOVE';
+import { parseArgs } from './generic-transformers';
 
 describe('SMOVE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('source', 'destination', 'member'),
-            ['SMOVE', 'source', 'destination', 'member']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SMOVE, 'source', 'destination', 'member'),
+      ['SMOVE', 'source', 'destination', 'member']
+    );
+  });
 
-    testUtils.testWithClient('client.sMove', async client => {
-        assert.equal(
-            await client.sMove('source', 'destination', 'member'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sMove', async client => {
+    assert.equal(
+      await client.sMove('{tag}source', '{tag}destination', 'member'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SMOVE.ts b/packages/client/lib/commands/SMOVE.ts
index 83c4027dbd5..d5f150b99f2 100644
--- a/packages/client/lib/commands/SMOVE.ts
+++ b/packages/client/lib/commands/SMOVE.ts
@@ -1,13 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    source: RedisCommandArgument,
-    destination: RedisCommandArgument,
-    member: RedisCommandArgument
-): RedisCommandArguments {
-    return ['SMOVE', source, destination, member];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SMOVE command
+   * 
+   * @param parser - The command parser
+   * @param source - The source set key
+   * @param destination - The destination set key
+   * @param member - The member to move
+   * @see https://redis.io/commands/smove/
+   */
+  parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, member: RedisArgument) {
+    parser.push('SMOVE');
+    parser.pushKeys([source, destination]);
+    parser.push(member);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SORT.spec.ts b/packages/client/lib/commands/SORT.spec.ts
index 4967b020ad5..330b321a1b8 100644
--- a/packages/client/lib/commands/SORT.spec.ts
+++ b/packages/client/lib/commands/SORT.spec.ts
@@ -1,96 +1,100 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SORT';
+import SORT from './SORT';
+import { parseArgs } from './generic-transformers';
 
 describe('SORT', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['SORT', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(SORT, 'key'),
+        ['SORT', 'key']
+      );
+    });
 
-        it('with BY', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    BY: 'pattern'
-                }),
-                ['SORT', 'key', 'BY', 'pattern']
-            );
-        });
+    it('with BY', () => {
+      assert.deepEqual(
+        parseArgs(SORT, 'key', {
+          BY: 'pattern'
+        }),
+        ['SORT', 'key', 'BY', 'pattern']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['SORT', 'key', 'LIMIT', '0', '1']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(SORT, 'key', {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['SORT', 'key', 'LIMIT', '0', '1']
+      );
+    });
 
-        describe('with GET', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        GET: 'pattern'
-                    }),
-                    ['SORT', 'key', 'GET', 'pattern']
-                );
-            });
+    describe('with GET', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(SORT, 'key', {
+            GET: 'pattern'
+          }),
+          ['SORT', 'key', 'GET', 'pattern']
+        );
+      });
 
-            it('array', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        GET: ['1', '2']
-                    }),
-                    ['SORT', 'key', 'GET', '1', 'GET', '2']
-                );
-            });
-        });
+      it('array', () => {
+        assert.deepEqual(
+          parseArgs(SORT, 'key', {
+            GET: ['1', '2']
+          }),
+          ['SORT', 'key', 'GET', '1', 'GET', '2']
+        );
+      });
+    });
 
-        it('with DIRECTION', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    DIRECTION: 'ASC'
-                }),
-                ['SORT', 'key', 'ASC']
-            );
-        });
+    it('with DIRECTION', () => {
+      assert.deepEqual(
+        parseArgs(SORT, 'key', {
+          DIRECTION: 'ASC'
+        }),
+        ['SORT', 'key', 'ASC']
+      );
+    });
 
-        it('with ALPHA', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    ALPHA: true
-                }),
-                ['SORT', 'key', 'ALPHA']
-            );
-        });
+    it('with ALPHA', () => {
+      assert.deepEqual(
+        parseArgs(SORT, 'key', {
+          ALPHA: true
+        }),
+        ['SORT', 'key', 'ALPHA']
+      );
+    });
 
-        it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    BY: 'pattern',
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    },
-                    GET: 'pattern',
-                    DIRECTION: 'ASC',
-                    ALPHA: true
-                }),
-                ['SORT', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA']
-            );
-        });
+    it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => {
+      assert.deepEqual(
+        parseArgs(SORT, 'key', {
+          BY: 'pattern',
+          LIMIT: {
+            offset: 0,
+            count: 1
+          },
+          GET: 'pattern',
+          DIRECTION: 'ASC',
+          ALPHA: true
+        }),
+        ['SORT', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sort', async client => {
-        assert.deepEqual(
-            await client.sort('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sort', async client => {
+    assert.deepEqual(
+      await client.sort('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SORT.ts b/packages/client/lib/commands/SORT.ts
index 15e95bde677..5ec889f3063 100644
--- a/packages/client/lib/commands/SORT.ts
+++ b/packages/client/lib/commands/SORT.ts
@@ -1,13 +1,84 @@
-import { RedisCommandArguments } from '.';
-import { pushSortArguments, SortOptions } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+/**
+ * Options for the SORT command
+ * 
+ * @property BY - Pattern for external key to sort by
+ * @property LIMIT - Offset and count for results pagination
+ * @property GET - Pattern(s) for retrieving external keys
+ * @property DIRECTION - Sort direction: ASC (ascending) or DESC (descending)
+ * @property ALPHA - Sort lexicographically instead of numerically
+ */
+export interface SortOptions {
+  BY?: RedisArgument;
+  LIMIT?: {
+    offset: number;
+    count: number;
+  };
+  GET?: RedisArgument | Array<RedisArgument>;
+  DIRECTION?: 'ASC' | 'DESC';
+  ALPHA?: boolean;
+}
+
+/**
+ * Parses sort arguments for the SORT command
+ * 
+ * @param parser - The command parser
+ * @param key - The key to sort
+ * @param options - Sort options
+ */
+export function parseSortArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  options?: SortOptions
+) {
+  parser.pushKey(key);
+
+  if (options?.BY) {
+    parser.push('BY', options.BY);
+  }
+
+  if (options?.LIMIT) {
+    parser.push(
+      'LIMIT',
+      options.LIMIT.offset.toString(),
+      options.LIMIT.count.toString()
+    );
+  }
+
+  if (options?.GET) {
+    if (Array.isArray(options.GET)) {
+      for (const pattern of options.GET) {
+        parser.push('GET', pattern);
+      }
+    } else {
+      parser.push('GET', options.GET);
+    }
+  }
+
+  if (options?.DIRECTION) {
+    parser.push(options.DIRECTION);
+  }
 
-export function transformArguments(
-    key: string,
-    options?: SortOptions
-): RedisCommandArguments {
-    return pushSortArguments(['SORT', key], options);
+  if (options?.ALPHA) {
+    parser.push('ALPHA');
+  }
 }
 
-export declare function transformReply(): Array<string>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SORT command
+   * 
+   * @param parser - The command parser
+   * @param key - The key to sort (list, set, or sorted set)
+   * @param options - Sort options
+   * @see https://redis.io/commands/sort/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: SortOptions) {
+    parser.push('SORT');
+    parseSortArguments(parser, key, options);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SORT_RO.spec.ts b/packages/client/lib/commands/SORT_RO.spec.ts
index fe3ca1240d7..86f8e507033 100644
--- a/packages/client/lib/commands/SORT_RO.spec.ts
+++ b/packages/client/lib/commands/SORT_RO.spec.ts
@@ -1,98 +1,102 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SORT_RO';
+import SORT_RO from './SORT_RO';
+import { parseArgs } from './generic-transformers';
 
 describe('SORT_RO', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['SORT_RO', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(SORT_RO, 'key'),
+        ['SORT_RO', 'key']
+      );
+    });
 
-        it('with BY', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    BY: 'pattern'
-                }),
-                ['SORT_RO', 'key', 'BY', 'pattern']
-            );
-        });
+    it('with BY', () => {
+      assert.deepEqual(
+        parseArgs(SORT_RO, 'key', {
+          BY: 'pattern'
+        }),
+        ['SORT_RO', 'key', 'BY', 'pattern']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['SORT_RO', 'key', 'LIMIT', '0', '1']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(SORT_RO, 'key', {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['SORT_RO', 'key', 'LIMIT', '0', '1']
+      );
+    });
 
-        describe('with GET', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        GET: 'pattern'
-                    }),
-                    ['SORT_RO', 'key', 'GET', 'pattern']
-                );
-            });
+    describe('with GET', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(SORT_RO, 'key', {
+            GET: 'pattern'
+          }),
+          ['SORT_RO', 'key', 'GET', 'pattern']
+        );
+      });
 
-            it('array', () => {
-                assert.deepEqual(
-                    transformArguments('key', {
-                        GET: ['1', '2']
-                    }),
-                    ['SORT_RO', 'key', 'GET', '1', 'GET', '2']
-                );
-            });
-        });
+      it('array', () => {
+        assert.deepEqual(
+          parseArgs(SORT_RO, 'key', {
+            GET: ['1', '2']
+          }),
+          ['SORT_RO', 'key', 'GET', '1', 'GET', '2']
+        );
+      });
+    });
 
-        it('with DIRECTION', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    DIRECTION: 'ASC'
-                }),
-                ['SORT_RO', 'key', 'ASC']
-            );
-        });
+    it('with DIRECTION', () => {
+      assert.deepEqual(
+        parseArgs(SORT_RO, 'key', {
+          DIRECTION: 'ASC'
+        }),
+        ['SORT_RO', 'key', 'ASC']
+      );
+    });
 
-        it('with ALPHA', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    ALPHA: true
-                }),
-                ['SORT_RO', 'key', 'ALPHA']
-            );
-        });
+    it('with ALPHA', () => {
+      assert.deepEqual(
+        parseArgs(SORT_RO, 'key', {
+          ALPHA: true
+        }),
+        ['SORT_RO', 'key', 'ALPHA']
+      );
+    });
 
-        it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    BY: 'pattern',
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    },
-                    GET: 'pattern',
-                    DIRECTION: 'ASC',
-                    ALPHA: true,
-                }),
-                ['SORT_RO', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA']
-            );
-        });
+    it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => {
+      assert.deepEqual(
+        parseArgs(SORT_RO, 'key', {
+          BY: 'pattern',
+          LIMIT: {
+            offset: 0,
+            count: 1
+          },
+          GET: 'pattern',
+          DIRECTION: 'ASC',
+          ALPHA: true,
+        }),
+        ['SORT_RO', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sortRo', async client => {
-        assert.deepEqual(
-            await client.sortRo('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sortRo', async client => {
+    assert.deepEqual(
+      await client.sortRo('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SORT_RO.ts b/packages/client/lib/commands/SORT_RO.ts
index 4af7acd80d7..5531f927d52 100644
--- a/packages/client/lib/commands/SORT_RO.ts
+++ b/packages/client/lib/commands/SORT_RO.ts
@@ -1,15 +1,17 @@
-import { RedisCommandArguments } from '.';
-import { pushSortArguments, SortOptions } from "./generic-transformers";
+import { Command } from '../RESP/types';
+import SORT, { parseSortArguments } from './SORT';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Read-only variant of SORT that sorts the elements in a list, set or sorted set.
+   * @param args - Same parameters as the SORT command.
+   */
+  parseCommand(...args: Parameters<typeof parseSortArguments>) {
+    const parser = args[0];
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: string,
-    options?: SortOptions
-): RedisCommandArguments {
-    return pushSortArguments(['SORT_RO', key], options);
-}
-
-export declare function transformReply(): Array<string>;
+    parser.push('SORT_RO');
+    parseSortArguments(...args);
+  },
+  transformReply: SORT.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SORT_STORE.spec.ts b/packages/client/lib/commands/SORT_STORE.spec.ts
index d078135255d..a812cec52c5 100644
--- a/packages/client/lib/commands/SORT_STORE.spec.ts
+++ b/packages/client/lib/commands/SORT_STORE.spec.ts
@@ -1,96 +1,100 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SORT_STORE';
+import SORT_STORE from './SORT_STORE';
+import { parseArgs } from './generic-transformers';
 
 describe('SORT STORE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination'),
-                ['SORT', 'source', 'STORE', 'destination']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(SORT_STORE, 'source', 'destination'),
+        ['SORT', 'source', 'STORE', 'destination']
+      );
+    });
 
-        it('with BY', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    BY: 'pattern'
-                }),
-                ['SORT', 'source', 'BY', 'pattern', 'STORE', 'destination']
-            );
-        });
+    it('with BY', () => {
+      assert.deepEqual(
+        parseArgs(SORT_STORE, 'source', 'destination', {
+          BY: 'pattern'
+        }),
+        ['SORT', 'source', 'BY', 'pattern', 'STORE', 'destination']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['SORT', 'source', 'LIMIT', '0', '1', 'STORE', 'destination']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(SORT_STORE, 'source', 'destination', {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['SORT', 'source', 'LIMIT', '0', '1', 'STORE', 'destination']
+      );
+    });
 
-        describe('with GET', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('source', 'destination', {
-                        GET: 'pattern'
-                    }),
-                    ['SORT', 'source', 'GET', 'pattern', 'STORE', 'destination']
-                );
-            });
+    describe('with GET', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(SORT_STORE, 'source', 'destination', {
+            GET: 'pattern'
+          }),
+          ['SORT', 'source', 'GET', 'pattern', 'STORE', 'destination']
+        );
+      });
 
-            it('array', () => {
-                assert.deepEqual(
-                    transformArguments('source', 'destination', {
-                        GET: ['1', '2']
-                    }),
-                    ['SORT', 'source', 'GET', '1', 'GET', '2', 'STORE', 'destination']
-                );
-            });
-        });
+      it('array', () => {
+        assert.deepEqual(
+          parseArgs(SORT_STORE, 'source', 'destination', {
+            GET: ['1', '2']
+          }),
+          ['SORT', 'source', 'GET', '1', 'GET', '2', 'STORE', 'destination']
+        );
+      });
+    });
 
-        it('with DIRECTION', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    DIRECTION: 'ASC'
-                }),
-                ['SORT', 'source', 'ASC', 'STORE', 'destination']
-            );
-        });
+    it('with DIRECTION', () => {
+      assert.deepEqual(
+        parseArgs(SORT_STORE, 'source', 'destination', {
+          DIRECTION: 'ASC'
+        }),
+        ['SORT', 'source', 'ASC', 'STORE', 'destination']
+      );
+    });
 
-        it('with ALPHA', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    ALPHA: true
-                }),
-                ['SORT', 'source', 'ALPHA', 'STORE', 'destination']
-            );
-        });
+    it('with ALPHA', () => {
+      assert.deepEqual(
+        parseArgs(SORT_STORE, 'source', 'destination', {
+          ALPHA: true
+        }),
+        ['SORT', 'source', 'ALPHA', 'STORE', 'destination']
+      );
+    });
 
-        it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', {
-                    BY: 'pattern',
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    },
-                    GET: 'pattern',
-                    DIRECTION: 'ASC',
-                    ALPHA: true
-                }),
-                ['SORT', 'source', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA', 'STORE', 'destination']
-            );
-        });
+    it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => {
+      assert.deepEqual(
+        parseArgs(SORT_STORE, 'source', 'destination', {
+          BY: 'pattern',
+          LIMIT: {
+            offset: 0,
+            count: 1
+          },
+          GET: 'pattern',
+          DIRECTION: 'ASC',
+          ALPHA: true
+        }),
+        ['SORT', 'source', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA', 'STORE', 'destination']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sortStore', async client => {
-        assert.equal(
-            await client.sortStore('source', 'destination'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sortStore', async client => {
+    assert.equal(
+      await client.sortStore('{tag}source', '{tag}destination'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SORT_STORE.ts b/packages/client/lib/commands/SORT_STORE.ts
index 9acaf023175..5fd52e076df 100644
--- a/packages/client/lib/commands/SORT_STORE.ts
+++ b/packages/client/lib/commands/SORT_STORE.ts
@@ -1,17 +1,19 @@
-import { RedisCommandArguments } from '.';
-import { SortOptions } from './generic-transformers';
-import { transformArguments as transformSortArguments } from './SORT';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import SORT, { SortOptions } from './SORT';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    source: string,
-    destination: string,
-    options?: SortOptions
-): RedisCommandArguments {
-    const args = transformSortArguments(source, options);
-    args.push('STORE', destination);
-    return args;
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Sorts the elements in a list, set or sorted set and stores the result in a new list.
+   * @param parser - The Redis command parser.
+   * @param source - Key of the source list, set or sorted set.
+   * @param destination - Destination key where the result will be stored.
+   * @param options - Optional sorting parameters.
+   */
+  parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: SortOptions) {
+    SORT.parseCommand(parser, source, options);
+    parser.push('STORE', destination);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SPOP.spec.ts b/packages/client/lib/commands/SPOP.spec.ts
index 6a384d181fc..542e1ba3fcb 100644
--- a/packages/client/lib/commands/SPOP.spec.ts
+++ b/packages/client/lib/commands/SPOP.spec.ts
@@ -1,28 +1,38 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SPOP';
+import SPOP from './SPOP';
+import { BasicCommandParser } from '../client/parser';
 
 describe('SPOP', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['SPOP', 'key']
-            );
-        });
+  it('transformArguments', () => {
+    const parser = new BasicCommandParser();
+    SPOP.parseCommand(parser, 'key');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['SPOP', 'key']
+    );
+  });
 
-        it('with count', () => {
-            assert.deepEqual(
-                transformArguments('key', 2),
-                ['SPOP', 'key', '2']
-            );
-        });
-    });
+  testUtils.testAll('sPop', async client => {
+    assert.equal(
+      await client.sPop('key'),
+      null
+    );
 
-    testUtils.testWithClient('client.sPop', async client => {
-        assert.equal(
-            await client.sPop('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    await client.sAdd('key', 'member');
+
+    assert.equal(
+      await client.sPop('key'),
+      'member'
+    );
+
+    assert.equal(
+      await client.sPop('key'),
+      null
+    );
+
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SPOP.ts b/packages/client/lib/commands/SPOP.ts
index 38ce8573f3f..8e9450b2b01 100644
--- a/packages/client/lib/commands/SPOP.ts
+++ b/packages/client/lib/commands/SPOP.ts
@@ -1,18 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count?: number
-): RedisCommandArguments {
-    const args = ['SPOP', key];
-
-    if (typeof count === 'number') {
-        args.push(count.toString());
-    }
-
-    return args;
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SPOP command to remove and return a random member from a set
+   *
+   * @param parser - The command parser
+   * @param key - The key of the set to pop from
+   * @see https://redis.io/commands/spop/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('SPOP');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SPOP_COUNT.spec.ts b/packages/client/lib/commands/SPOP_COUNT.spec.ts
new file mode 100644
index 00000000000..9720101f31f
--- /dev/null
+++ b/packages/client/lib/commands/SPOP_COUNT.spec.ts
@@ -0,0 +1,38 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import SPOP_COUNT from './SPOP_COUNT';
+import { BasicCommandParser } from '../client/parser';
+
+describe('SPOP_COUNT', () => {
+  it('transformArguments', () => {
+    const parser = new BasicCommandParser();
+    SPOP_COUNT.parseCommand(parser, 'key', 1);
+    assert.deepEqual(
+      parser.redisArgs,
+      ['SPOP', 'key', '1']
+    );
+  });
+
+  testUtils.testAll('sPopCount', async client => {
+
+    assert.deepEqual(
+      await client.sPopCount('key', 1),
+      []
+    );
+
+    await Promise.all([
+      client.sAdd('key', 'member'),
+      client.sAdd('key', 'member2'),
+      client.sAdd('key', 'member3')
+    ])
+
+    assert.deepEqual(
+      (await client.sPopCount('key', 3)).length,
+      3
+    );
+
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/SPOP_COUNT.ts b/packages/client/lib/commands/SPOP_COUNT.ts
new file mode 100644
index 00000000000..a285e6f5c48
--- /dev/null
+++ b/packages/client/lib/commands/SPOP_COUNT.ts
@@ -0,0 +1,20 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command, ArrayReply } from '../RESP/types';
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SPOP command to remove and return multiple random members from a set
+   *
+   * @param parser - The command parser
+   * @param key - The key of the set to pop from
+   * @param count - The number of members to pop
+   * @see https://redis.io/commands/spop/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    parser.push('SPOP');
+    parser.pushKey(key);
+    parser.push(count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<string>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SPUBLISH.spec.ts b/packages/client/lib/commands/SPUBLISH.spec.ts
index 60b6ce2dad0..5a53bc40b7d 100644
--- a/packages/client/lib/commands/SPUBLISH.spec.ts
+++ b/packages/client/lib/commands/SPUBLISH.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SPUBLISH';
+import SPUBLISH from './SPUBLISH';
+import { parseArgs } from './generic-transformers';
 
 describe('SPUBLISH', () => {
-    testUtils.isVersionGreaterThanHook([7]);
-    
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('channel', 'message'),
-            ['SPUBLISH', 'channel', 'message']
-        );
-    });
+  testUtils.isVersionGreaterThanHook([7]);
 
-    testUtils.testWithClient('client.sPublish', async client => {
-        assert.equal(
-            await client.sPublish('channel', 'message'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SPUBLISH, 'channel', 'message'),
+      ['SPUBLISH', 'channel', 'message']
+    );
+  });
+
+  testUtils.testAll('sPublish', async client => {
+    assert.equal(
+      await client.sPublish('channel', 'message'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SPUBLISH.ts b/packages/client/lib/commands/SPUBLISH.ts
index 42a7ab49072..6dd9f37e66b 100644
--- a/packages/client/lib/commands/SPUBLISH.ts
+++ b/packages/client/lib/commands/SPUBLISH.ts
@@ -1,14 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    channel: RedisCommandArgument,
-    message: RedisCommandArgument
-): RedisCommandArguments {
-    return ['SPUBLISH', channel, message];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SPUBLISH command to post a message to a Sharded Pub/Sub channel
+   *
+   * @param parser - The command parser
+   * @param channel - The channel to publish to
+   * @param message - The message to publish
+   * @see https://redis.io/commands/spublish/
+   */
+  parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) {
+    parser.push('SPUBLISH');
+    parser.pushKey(channel);
+    parser.push(message);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SRANDMEMBER.spec.ts b/packages/client/lib/commands/SRANDMEMBER.spec.ts
index 291271540be..637aac27b29 100644
--- a/packages/client/lib/commands/SRANDMEMBER.spec.ts
+++ b/packages/client/lib/commands/SRANDMEMBER.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SRANDMEMBER';
+import SRANDMEMBER from './SRANDMEMBER';
+import { parseArgs } from './generic-transformers';
 
 describe('SRANDMEMBER', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['SRANDMEMBER', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SRANDMEMBER, 'key'),
+      ['SRANDMEMBER', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.sRandMember', async client => {
-        assert.equal(
-            await client.sRandMember('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sRandMember', async client => {
+    assert.equal(
+      await client.sRandMember('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SRANDMEMBER.ts b/packages/client/lib/commands/SRANDMEMBER.ts
index d84e61993e5..9e04e45b52a 100644
--- a/packages/client/lib/commands/SRANDMEMBER.ts
+++ b/packages/client/lib/commands/SRANDMEMBER.ts
@@ -1,9 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['SRANDMEMBER', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SRANDMEMBER command to get a random member from a set
+   *
+   * @param parser - The command parser
+   * @param key - The key of the set to get random member from
+   * @see https://redis.io/commands/srandmember/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('SRANDMEMBER')
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts
index d3d787b3e63..13bb0d52d96 100644
--- a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts
+++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SRANDMEMBER_COUNT';
+import SRANDMEMBER_COUNT from './SRANDMEMBER_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('SRANDMEMBER COUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['SRANDMEMBER', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SRANDMEMBER_COUNT, 'key', 1),
+      ['SRANDMEMBER', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.sRandMemberCount', async client => {
-        assert.deepEqual(
-            await client.sRandMemberCount('key', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sRandMemberCount', async client => {
+    assert.deepEqual(
+      await client.sRandMemberCount('key', 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts
index d265d89e9a6..c7dd434b710 100644
--- a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts
+++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts
@@ -1,16 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformSRandMemberArguments } from './SRANDMEMBER';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import SRANDMEMBER from './SRANDMEMBER';
 
-export { FIRST_KEY_INDEX } from './SRANDMEMBER';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return [
-        ...transformSRandMemberArguments(key),
-        count.toString()
-    ];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: SRANDMEMBER.IS_READ_ONLY,
+  /**
+   * Constructs the SRANDMEMBER command to get multiple random members from a set
+   *
+   * @param parser - The command parser
+   * @param key - The key of the set to get random members from
+   * @param count - The number of members to return. If negative, may return the same member multiple times
+   * @see https://redis.io/commands/srandmember/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    SRANDMEMBER.parseCommand(parser, key);
+    parser.push(count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SREM.spec.ts b/packages/client/lib/commands/SREM.spec.ts
index d53d7b0334d..6def4178fc8 100644
--- a/packages/client/lib/commands/SREM.spec.ts
+++ b/packages/client/lib/commands/SREM.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SREM';
+import SREM from './SREM';
+import { parseArgs } from './generic-transformers';
 
 describe('SREM', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member'),
-                ['SREM', 'key', 'member']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SREM, 'key', 'member'),
+        ['SREM', 'key', 'member']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['SREM', 'key', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SREM, 'key', ['1', '2']),
+        ['SREM', 'key', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sRem', async client => {
-        assert.equal(
-            await client.sRem('key', 'member'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sRem', async client => {
+    assert.equal(
+      await client.sRem('key', 'member'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SREM.ts b/packages/client/lib/commands/SREM.ts
index 34aebdf02e3..d97ed7774d8 100644
--- a/packages/client/lib/commands/SREM.ts
+++ b/packages/client/lib/commands/SREM.ts
@@ -1,13 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    members: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SREM', key], members);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SREM command to remove one or more members from a set
+   *
+   * @param parser - The command parser
+   * @param key - The key of the set to remove members from
+   * @param members - One or more members to remove from the set
+   * @returns The number of members that were removed from the set
+   * @see https://redis.io/commands/srem/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) {
+    parser.push('SREM');
+    parser.pushKey(key);
+    parser.pushVariadic(members);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SSCAN.spec.ts b/packages/client/lib/commands/SSCAN.spec.ts
index 71a90bf81d8..e5d689c6e98 100644
--- a/packages/client/lib/commands/SSCAN.spec.ts
+++ b/packages/client/lib/commands/SSCAN.spec.ts
@@ -1,74 +1,56 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './SSCAN';
+import SSCAN from './SSCAN';
+import { parseArgs } from './generic-transformers';
 
 describe('SSCAN', () => {
-    describe('transformArguments', () => {
-        it('cusror only', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['SSCAN', 'key', '0']
-            );
-        });
-
-        it('with MATCH', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern'
-                }),
-                ['SSCAN', 'key', '0', 'MATCH', 'pattern']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    COUNT: 1
-                }),
-                ['SSCAN', 'key', '0', 'COUNT', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('cusror only', () => {
+      assert.deepEqual(
+        parseArgs(SSCAN, 'key', '0'),
+        ['SSCAN', 'key', '0']
+      );
+    });
 
-        it('with MATCH & COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern',
-                    COUNT: 1
-                }),
-                ['SSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
-            );
-        });
+    it('with MATCH', () => {
+      assert.deepEqual(
+        parseArgs(SSCAN, 'key', '0', {
+          MATCH: 'pattern'
+        }),
+        ['SSCAN', 'key', '0', 'MATCH', 'pattern']
+      );
     });
 
-    describe('transformReply', () => {
-        it('without members', () => {
-            assert.deepEqual(
-                transformReply(['0', []]),
-                {
-                    cursor: 0,
-                    members: []
-                }
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(SSCAN, 'key', '0', {
+          COUNT: 1
+        }),
+        ['SSCAN', 'key', '0', 'COUNT', '1']
+      );
+    });
 
-        it('with members', () => {
-            assert.deepEqual(
-                transformReply(['0', ['member']]),
-                {
-                    cursor: 0,
-                    members: ['member']
-                }
-            );
-        });
+    it('with MATCH & COUNT', () => {
+      assert.deepEqual(
+        parseArgs(SSCAN, 'key', '0', {
+          MATCH: 'pattern',
+          COUNT: 1
+        }),
+        ['SSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sScan', async client => {
-        assert.deepEqual(
-            await client.sScan('key', 0),
-            {
-                cursor: 0,
-                members: []
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sScan', async client => {
+    assert.deepEqual(
+      await client.sScan('key', '0'),
+      {
+        cursor: '0',
+        members: []
+      }
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SSCAN.ts b/packages/client/lib/commands/SSCAN.ts
index 9b3938f159b..14e2c079ff0 100644
--- a/packages/client/lib/commands/SSCAN.ts
+++ b/packages/client/lib/commands/SSCAN.ts
@@ -1,31 +1,40 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { ScanOptions, pushScanArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
+import { ScanCommonOptions, parseScanArguments} from './SCAN';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    cursor: number,
-    options?: ScanOptions
-): RedisCommandArguments {
-    return pushScanArguments([
-        'SSCAN',
-        key,
-    ], cursor, options);
-}
-
-type SScanRawReply = [string, Array<RedisCommandArgument>];
-
-interface SScanReply {
-    cursor: number;
-    members: Array<RedisCommandArgument>;
-}
-
-export function transformReply([cursor, members]: SScanRawReply): SScanReply {
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SSCAN command to incrementally iterate over elements in a set
+   *
+   * @param parser - The command parser
+   * @param key - The key of the set to scan
+   * @param cursor - The cursor position to start scanning from
+   * @param options - Optional scanning parameters (COUNT and MATCH)
+   * @returns Iterator containing cursor position and matching members
+   * @see https://redis.io/commands/sscan/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    cursor: RedisArgument,
+    options?: ScanCommonOptions
+  ) {
+    parser.push('SSCAN');
+    parser.pushKey(key);
+    parseScanArguments(parser, cursor, options);
+  },
+  /**
+   * Transforms the SSCAN reply into a cursor result object
+   * 
+   * @param cursor - The next cursor position
+   * @param members - Array of matching set members
+   * @returns Object containing cursor and members array
+   */
+  transformReply([cursor, members]: [BlobStringReply, Array<BlobStringReply>]) {
     return {
-        cursor: Number(cursor),
-        members
+      cursor,
+      members
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/STRLEN.spec.ts b/packages/client/lib/commands/STRLEN.spec.ts
index 519c68d3e5d..dbb7a08541b 100644
--- a/packages/client/lib/commands/STRLEN.spec.ts
+++ b/packages/client/lib/commands/STRLEN.spec.ts
@@ -1,26 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './STRLEN';
+import STRLEN from './STRLEN';
+import { parseArgs } from './generic-transformers';
 
 describe('STRLEN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['STRLEN', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(STRLEN, 'key'),
+      ['STRLEN', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.strLen', async client => {
-        assert.equal(
-            await client.strLen('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithCluster('cluster.strLen', async cluster => {
-        assert.equal(
-            await cluster.strLen('key'),
-            0
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
+  testUtils.testAll('strLen', async client => {
+    assert.equal(
+      await client.strLen('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/STRLEN.ts b/packages/client/lib/commands/STRLEN.ts
index de88340d8b6..0f0e612422a 100644
--- a/packages/client/lib/commands/STRLEN.ts
+++ b/packages/client/lib/commands/STRLEN.ts
@@ -1,11 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['STRLEN', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the STRLEN command to get the length of a string value
+   *
+   * @param parser - The command parser
+   * @param key - The key holding the string value
+   * @returns The length of the string value, or 0 when key does not exist
+   * @see https://redis.io/commands/strlen/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('STRLEN');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SUNION.spec.ts b/packages/client/lib/commands/SUNION.spec.ts
index 2918607c1d6..a4389d4236e 100644
--- a/packages/client/lib/commands/SUNION.spec.ts
+++ b/packages/client/lib/commands/SUNION.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUNION';
+import SUNION from './SUNION';
+import { parseArgs } from './generic-transformers';
 
 describe('SUNION', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['SUNION', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SUNION, 'key'),
+        ['SUNION', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['SUNION', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SUNION, ['1', '2']),
+        ['SUNION', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sUnion', async client => {
-        assert.deepEqual(
-            await client.sUnion('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sUnion', async client => {
+    assert.deepEqual(
+      await client.sUnion('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SUNION.ts b/packages/client/lib/commands/SUNION.ts
index 52c112e6610..7acecd1d12a 100644
--- a/packages/client/lib/commands/SUNION.ts
+++ b/packages/client/lib/commands/SUNION.ts
@@ -1,14 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SUNION'], keys);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the SUNION command to return the members of the set resulting from the union of all the given sets
+   *
+   * @param parser - The command parser
+   * @param keys - One or more set keys to compute the union from
+   * @returns Array of all elements that are members of at least one of the given sets
+   * @see https://redis.io/commands/sunion/
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('SUNION');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SUNIONSTORE.spec.ts b/packages/client/lib/commands/SUNIONSTORE.spec.ts
index 142533eea2b..8f3db2cacd7 100644
--- a/packages/client/lib/commands/SUNIONSTORE.spec.ts
+++ b/packages/client/lib/commands/SUNIONSTORE.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUNIONSTORE';
+import SUNIONSTORE from './SUNIONSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('SUNIONSTORE', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key'),
-                ['SUNIONSTORE', 'destination', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(SUNIONSTORE, 'destination', 'key'),
+        ['SUNIONSTORE', 'destination', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['SUNIONSTORE', 'destination', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(SUNIONSTORE, 'destination', ['1', '2']),
+        ['SUNIONSTORE', 'destination', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.sUnionStore', async client => {
-        assert.equal(
-            await client.sUnionStore('destination', 'key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('sUnionStore', async client => {
+    assert.equal(
+      await client.sUnionStore('{tag}destination', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/SUNIONSTORE.ts b/packages/client/lib/commands/SUNIONSTORE.ts
index 94df6771a04..0a877c9cb8d 100644
--- a/packages/client/lib/commands/SUNIONSTORE.ts
+++ b/packages/client/lib/commands/SUNIONSTORE.ts
@@ -1,13 +1,22 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    destination: RedisCommandArgument,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['SUNIONSTORE', destination], keys);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the SUNIONSTORE command to store the union of multiple sets into a destination set
+   *
+   * @param parser - The command parser
+   * @param destination - The destination key to store the resulting set
+   * @param keys - One or more source set keys to compute the union from
+   * @returns The number of elements in the resulting set
+   * @see https://redis.io/commands/sunionstore/
+   */
+  parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) {
+    parser.push('SUNIONSTORE');
+    parser.pushKey(destination);
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/SWAPDB.spec.ts b/packages/client/lib/commands/SWAPDB.spec.ts
index add87512a64..a3b53b27218 100644
--- a/packages/client/lib/commands/SWAPDB.spec.ts
+++ b/packages/client/lib/commands/SWAPDB.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SWAPDB';
+import SWAPDB from './SWAPDB';
+import { parseArgs } from './generic-transformers';
 
 describe('SWAPDB', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(0, 1),
-            ['SWAPDB', '0', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SWAPDB, 0, 1),
+      ['SWAPDB', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.swapDb', async client => {
-        assert.equal(
-            await client.swapDb(0, 1),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.swapDb', async client => {
+    assert.equal(
+      await client.swapDb(0, 1),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/SWAPDB.ts b/packages/client/lib/commands/SWAPDB.ts
index 7f13d6b008e..66b19409a2b 100644
--- a/packages/client/lib/commands/SWAPDB.ts
+++ b/packages/client/lib/commands/SWAPDB.ts
@@ -1,5 +1,18 @@
-export function transformArguments(index1: number, index2: number): Array<string> {
-    return ['SWAPDB', index1.toString(), index2.toString()];
-}
+import { CommandParser } from '../client/parser';
+import { SimpleStringReply, Command } from '../RESP/types';
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Swaps the data of two Redis databases.
+   * @param parser - The Redis command parser.
+   * @param index1 - First database index.
+   * @param index2 - Second database index.
+   */
+  parseCommand(parser: CommandParser, index1: number, index2: number) {
+    parser.push('SWAPDB', index1.toString(), index2.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-export declare function transformReply(): string;
diff --git a/packages/client/lib/commands/TIME.spec.ts b/packages/client/lib/commands/TIME.spec.ts
index bbaa7942db0..4ee704f0dd0 100644
--- a/packages/client/lib/commands/TIME.spec.ts
+++ b/packages/client/lib/commands/TIME.spec.ts
@@ -1,18 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './TIME';
+import TIME from './TIME';
+import { parseArgs } from './generic-transformers';
 
 describe('TIME', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['TIME']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(TIME),
+      ['TIME']
+    );
+  });
 
-    testUtils.testWithClient('client.time', async client => {
-        const reply = await client.time();
-        assert.ok(reply instanceof Date);
-        assert.ok(typeof reply.microseconds === 'number');
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.time', async client => {
+    const reply = await client.time();
+    assert.ok(Array.isArray(reply));
+    assert.equal(typeof reply[0], 'string');
+    assert.equal(typeof reply[1], 'string');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/TIME.ts b/packages/client/lib/commands/TIME.ts
index 1a364d6d8be..dc248d82069 100644
--- a/packages/client/lib/commands/TIME.ts
+++ b/packages/client/lib/commands/TIME.ts
@@ -1,15 +1,21 @@
-export function transformArguments(): Array<string> {
-    return ['TIME'];
-}
+import { CommandParser } from '../client/parser';
+import { BlobStringReply, Command } from '../RESP/types';
 
-interface TimeReply extends Date {
-    microseconds: number;
-}
-
-export function transformReply(reply: [string, string]): TimeReply {
-    const seconds = Number(reply[0]),
-        microseconds = Number(reply[1]),
-        d: Partial<TimeReply> = new Date(seconds * 1000 + microseconds / 1000);
-    d.microseconds = microseconds;
-    return d as TimeReply;
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the TIME command to return the server's current time
+   *
+   * @param parser - The command parser
+   * @returns Array containing the Unix timestamp in seconds and microseconds
+   * @see https://redis.io/commands/time/
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('TIME');
+  },
+  transformReply: undefined as unknown as () => [
+    unixTimestamp: BlobStringReply<`${number}`>,
+    microseconds: BlobStringReply<`${number}`>
+  ]
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/TOUCH.spec.ts b/packages/client/lib/commands/TOUCH.spec.ts
index 578c49587d7..69a3498346b 100644
--- a/packages/client/lib/commands/TOUCH.spec.ts
+++ b/packages/client/lib/commands/TOUCH.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './TOUCH';
+import TOUCH from './TOUCH';
+import { parseArgs } from './generic-transformers';
 
 describe('TOUCH', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['TOUCH', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(TOUCH, 'key'),
+        ['TOUCH', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['TOUCH', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(TOUCH, ['1', '2']),
+        ['TOUCH', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.touch', async client => {
-        assert.equal(
-            await client.touch('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('touch', async client => {
+    assert.equal(
+      await client.touch('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/TOUCH.ts b/packages/client/lib/commands/TOUCH.ts
index e67dff8e932..953a696111c 100644
--- a/packages/client/lib/commands/TOUCH.ts
+++ b/packages/client/lib/commands/TOUCH.ts
@@ -1,12 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['TOUCH'], key);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the TOUCH command to alter the last access time of keys
+   *
+   * @param parser - The command parser
+   * @param key - One or more keys to touch
+   * @returns The number of keys that were touched
+   * @see https://redis.io/commands/touch/
+   */
+  parseCommand(parser: CommandParser, key: RedisVariadicArgument) {
+    parser.push('TOUCH');
+    parser.pushKeys(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/TTL.spec.ts b/packages/client/lib/commands/TTL.spec.ts
index e37a6ab714b..4d36053c02e 100644
--- a/packages/client/lib/commands/TTL.spec.ts
+++ b/packages/client/lib/commands/TTL.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './TTL';
+import TTL from './TTL';
+import { parseArgs } from './generic-transformers';
 
 describe('TTL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TTL', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(TTL, 'key'),
+      ['TTL', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.ttl', async client => {
-        assert.equal(
-            await client.ttl('key'),
-            -2
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('ttl', async client => {
+    assert.equal(
+      await client.ttl('key'),
+      -2
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/TTL.ts b/packages/client/lib/commands/TTL.ts
index 29586f31fa8..c3340eda32e 100644
--- a/packages/client/lib/commands/TTL.ts
+++ b/packages/client/lib/commands/TTL.ts
@@ -1,11 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['TTL', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the TTL command to get the remaining time to live of a key
+   *
+   * @param parser - The command parser
+   * @param key - Key to check
+   * @returns Time to live in seconds, -2 if key does not exist, -1 if has no timeout
+   * @see https://redis.io/commands/ttl/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TTL');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/TYPE.spec.ts b/packages/client/lib/commands/TYPE.spec.ts
index 1040bf979b3..ae7392cdce9 100644
--- a/packages/client/lib/commands/TYPE.spec.ts
+++ b/packages/client/lib/commands/TYPE.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './TYPE';
+import TYPE from './TYPE';
+import { parseArgs } from './generic-transformers';
 
 describe('TYPE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['TYPE', 'key']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(TYPE, 'key'),
+      ['TYPE', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.type', async client => {
-        assert.equal(
-            await client.type('key'),
-            'none'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('type', async client => {
+    assert.equal(
+      await client.type('key'),
+      'none'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/TYPE.ts b/packages/client/lib/commands/TYPE.ts
index 10cd3f99b0e..740aa08e94a 100644
--- a/packages/client/lib/commands/TYPE.ts
+++ b/packages/client/lib/commands/TYPE.ts
@@ -1,11 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['TYPE', key];
-}
-
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the TYPE command to determine the data type stored at key
+   *
+   * @param parser - The command parser
+   * @param key - Key to check
+   * @returns String reply: "none", "string", "list", "set", "zset", "hash", "stream"
+   * @see https://redis.io/commands/type/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('TYPE');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/UNLINK.spec.ts b/packages/client/lib/commands/UNLINK.spec.ts
index e8355407d8f..2c32bee8e33 100644
--- a/packages/client/lib/commands/UNLINK.spec.ts
+++ b/packages/client/lib/commands/UNLINK.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './UNLINK';
+import UNLINK from './UNLINK';
+import { parseArgs } from './generic-transformers';
 
 describe('UNLINK', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['UNLINK', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(UNLINK, 'key'),
+        ['UNLINK', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['UNLINK', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(UNLINK, ['1', '2']),
+        ['UNLINK', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.unlink', async client => {
-        assert.equal(
-            await client.unlink('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('unlink', async client => {
+    assert.equal(
+      await client.unlink('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/UNLINK.ts b/packages/client/lib/commands/UNLINK.ts
index 53b0360e2df..4aa9cc315ab 100644
--- a/packages/client/lib/commands/UNLINK.ts
+++ b/packages/client/lib/commands/UNLINK.ts
@@ -1,12 +1,20 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['UNLINK'], key);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the UNLINK command to asynchronously delete one or more keys
+   *
+   * @param parser - The command parser
+   * @param keys - One or more keys to unlink
+   * @returns The number of keys that were unlinked
+   * @see https://redis.io/commands/unlink/
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('UNLINK');
+    parser.pushKeys(keys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/UNWATCH.spec.ts b/packages/client/lib/commands/UNWATCH.spec.ts
deleted file mode 100644
index 109ed0fa7c0..00000000000
--- a/packages/client/lib/commands/UNWATCH.spec.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './UNWATCH';
-
-describe('UNWATCH', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['UNWATCH']
-        );
-    });
-
-    testUtils.testWithClient('client.unwatch', async client => {
-        assert.equal(
-            await client.unwatch(),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/client/lib/commands/UNWATCH.ts b/packages/client/lib/commands/UNWATCH.ts
deleted file mode 100644
index ce42e7697bf..00000000000
--- a/packages/client/lib/commands/UNWATCH.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export function transformArguments(): Array<string> {
-    return ['UNWATCH'];
-}
-
-export declare function transformReply(): string;
diff --git a/packages/client/lib/commands/VADD.spec.ts b/packages/client/lib/commands/VADD.spec.ts
new file mode 100644
index 00000000000..e064beab498
--- /dev/null
+++ b/packages/client/lib/commands/VADD.spec.ts
@@ -0,0 +1,121 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VADD from './VADD';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VADD', () => {
+  describe('parseCommand', () => {
+    it('basic usage', () => {
+      const parser = new BasicCommandParser();
+      VADD.parseCommand(parser, 'key', [1.0, 2.0, 3.0], 'element');
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VADD', 'key', 'VALUES', '3', '1', '2', '3', 'element']
+      );
+    });
+
+    it('with REDUCE option', () => {
+      const parser = new BasicCommandParser();
+      VADD.parseCommand(parser, 'key', [1.0, 2], 'element', { REDUCE: 50 });
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element']
+      );
+    });
+
+    it('with quantization options', () => {
+      let parser = new BasicCommandParser();
+      VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'Q8' });
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'Q8']
+      );
+
+      parser = new BasicCommandParser();
+      VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'BIN' });
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'BIN']
+      );
+
+      parser = new BasicCommandParser();
+      VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'NOQUANT' });
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'NOQUANT']
+      );
+    });
+
+    it('with all options', () => {
+      const parser = new BasicCommandParser();
+      VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', {
+        REDUCE: 50,
+        CAS: true,
+        QUANT: 'Q8',
+        EF: 200,
+        SETATTR: { name: 'test', value: 42 },
+        M: 16
+      });
+      assert.deepEqual(
+        parser.redisArgs,
+        [
+          'VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element',
+          'CAS', 'Q8', 'EF', '200', 'SETATTR', '{"name":"test","value":42}', 'M', '16'
+        ]
+      );
+    });
+  });
+
+  testUtils.testAll('vAdd', async client => {
+    assert.equal(
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element'),
+      true
+    );
+
+    // same element should not be added again
+    assert.equal(
+      await client.vAdd('key', [1, 2 , 3], 'element'),
+      false
+    );
+
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] },
+  });
+
+  testUtils.testWithClient('vAdd with RESP3', async client => {
+    // Test basic functionality with RESP3
+    assert.equal(
+      await client.vAdd('resp3-key', [1.5, 2.5, 3.5], 'resp3-element'),
+      true
+    );
+
+    // same element should not be added again
+    assert.equal(
+      await client.vAdd('resp3-key', [1, 2 , 3], 'resp3-element'),
+      false
+    );
+
+    // Test with options to ensure complex parameters work with RESP3
+    assert.equal(
+      await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'resp3-element2', {
+        QUANT: 'Q8',
+        CAS: true,
+        SETATTR: { type: 'test', value: 123 }
+      }),
+      true
+    );
+
+    // Verify the vector set was created correctly
+    assert.equal(
+      await client.vCard('resp3-key'),
+      2
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VADD.ts b/packages/client/lib/commands/VADD.ts
new file mode 100644
index 00000000000..0406bd58d03
--- /dev/null
+++ b/packages/client/lib/commands/VADD.ts
@@ -0,0 +1,65 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformBooleanReply, transformDoubleArgument } from './generic-transformers';
+
+export interface VAddOptions {
+  REDUCE?: number;
+  CAS?: boolean;
+  QUANT?: 'NOQUANT' | 'BIN' | 'Q8',
+  EF?: number;
+  SETATTR?: Record<string, any>;
+  M?: number;
+}
+
+export default {
+  /**
+   * Add a new element into the vector set specified by key
+   * 
+   * @param parser - The command parser
+   * @param key - The name of the key that will hold the vector set data
+   * @param vector - The vector data as array of numbers
+   * @param element - The name of the element being added to the vector set
+   * @param options - Optional parameters for vector addition
+   * @see https://redis.io/commands/vadd/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    vector: Array<number>,
+    element: RedisArgument,
+    options?: VAddOptions
+  ) {
+    parser.push('VADD');
+    parser.pushKey(key);
+
+    if (options?.REDUCE !== undefined) {
+      parser.push('REDUCE', options.REDUCE.toString());
+    }
+
+    parser.push('VALUES', vector.length.toString());
+    for (const value of vector) {
+      parser.push(transformDoubleArgument(value));
+    }
+
+    parser.push(element);
+
+    if (options?.CAS) {
+      parser.push('CAS');
+    }
+
+    options?.QUANT && parser.push(options.QUANT);
+
+    if (options?.EF !== undefined) {
+      parser.push('EF', options.EF.toString());
+    }
+
+    if (options?.SETATTR) {
+      parser.push('SETATTR', JSON.stringify(options.SETATTR));
+    }
+
+    if (options?.M !== undefined) {
+      parser.push('M', options.M.toString());
+    }
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VCARD.spec.ts b/packages/client/lib/commands/VCARD.spec.ts
new file mode 100644
index 00000000000..feb9040fcb7
--- /dev/null
+++ b/packages/client/lib/commands/VCARD.spec.ts
@@ -0,0 +1,60 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VCARD from './VCARD';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VCARD', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VCARD.parseCommand(parser, 'key')
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VCARD', 'key']
+    );
+  });
+
+  testUtils.testAll('vCard', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+    await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
+
+    assert.equal(
+      await client.vCard('key'),
+      2
+    );
+
+    assert.equal(await client.vCard('unknown'), 0);
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vCard with RESP3', async client => {
+    // Test empty vector set
+    assert.equal(
+      await client.vCard('resp3-empty-key'),
+      0
+    );
+
+    // Add elements and test cardinality
+    await client.vAdd('resp3-key', [1.0, 2.0], 'elem1');
+    assert.equal(
+      await client.vCard('resp3-key'),
+      1
+    );
+
+    await client.vAdd('resp3-key', [3.0, 4.0], 'elem2');
+    await client.vAdd('resp3-key', [5.0, 6.0], 'elem3');
+    assert.equal(
+      await client.vCard('resp3-key'),
+      3
+    );
+
+    assert.equal(await client.vCard('unknown'), 0);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VCARD.ts b/packages/client/lib/commands/VCARD.ts
new file mode 100644
index 00000000000..575abf9b710
--- /dev/null
+++ b/packages/client/lib/commands/VCARD.ts
@@ -0,0 +1,18 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve the number of elements in a vector set
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @see https://redis.io/commands/vcard/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('VCARD');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VDIM.spec.ts b/packages/client/lib/commands/VDIM.spec.ts
new file mode 100644
index 00000000000..db3f5f3bd8f
--- /dev/null
+++ b/packages/client/lib/commands/VDIM.spec.ts
@@ -0,0 +1,43 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VDIM from './VDIM';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VDIM', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VDIM.parseCommand(parser, 'key');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VDIM', 'key']
+    );
+  });
+
+  testUtils.testAll('vDim', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
+
+    assert.equal(
+      await client.vDim('key'),
+      3
+    );
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vDim with RESP3', async client => {
+    await client.vAdd('resp3-5d', [1.0, 2.0, 3.0, 4.0, 5.0], 'elem5d');
+
+    assert.equal(
+      await client.vDim('resp3-5d'),
+      5
+    );
+
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VDIM.ts b/packages/client/lib/commands/VDIM.ts
new file mode 100644
index 00000000000..f7933e77eac
--- /dev/null
+++ b/packages/client/lib/commands/VDIM.ts
@@ -0,0 +1,18 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve the dimension of the vectors in a vector set
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @see https://redis.io/commands/vdim/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('VDIM');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VEMB.spec.ts b/packages/client/lib/commands/VEMB.spec.ts
new file mode 100644
index 00000000000..ed9515ebddf
--- /dev/null
+++ b/packages/client/lib/commands/VEMB.spec.ts
@@ -0,0 +1,42 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VEMB from './VEMB';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VEMB', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VEMB.parseCommand(parser, 'key', 'element');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VEMB', 'key', 'element']
+    );
+  });
+
+  testUtils.testAll('vEmb', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
+
+    const result = await client.vEmb('key', 'element');
+    assert.ok(Array.isArray(result));
+    assert.equal(result.length, 3);
+    assert.equal(typeof result[0], 'number');
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vEmb with RESP3', async client => {
+    await client.vAdd('resp3-key', [1.5, 2.5, 3.5, 4.5], 'resp3-element');
+
+    const result = await client.vEmb('resp3-key', 'resp3-element');
+    assert.ok(Array.isArray(result));
+    assert.equal(result.length, 4);
+    assert.equal(typeof result[0], 'number');
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VEMB.ts b/packages/client/lib/commands/VEMB.ts
new file mode 100644
index 00000000000..d534c27d65d
--- /dev/null
+++ b/packages/client/lib/commands/VEMB.ts
@@ -0,0 +1,21 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformDoubleArrayReply } from './generic-transformers';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve the approximate vector associated with a vector set element
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param element - The name of the element to retrieve the vector for
+   * @see https://redis.io/commands/vemb/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
+    parser.push('VEMB');
+    parser.pushKey(key);
+    parser.push(element);
+  },
+  transformReply: transformDoubleArrayReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VEMB_RAW.spec.ts b/packages/client/lib/commands/VEMB_RAW.spec.ts
new file mode 100644
index 00000000000..33d3af8540d
--- /dev/null
+++ b/packages/client/lib/commands/VEMB_RAW.spec.ts
@@ -0,0 +1,68 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VEMB_RAW from './VEMB_RAW';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VEMB_RAW', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VEMB_RAW.parseCommand(parser, 'key', 'element');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VEMB', 'key', 'element', 'RAW']
+    );
+  });
+
+  testUtils.testAll('vEmbRaw', async client => {
+    await client.vAdd('key1', [1.0, 2.0, 3.0], 'element');
+    const result1 = await client.vEmbRaw('key1', 'element');
+    assert.equal(result1.quantization, 'int8');
+    assert.ok(result1.quantizationRange !== undefined);
+
+    await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' });
+    const result2 = await client.vEmbRaw('key2', 'element');
+    assert.equal(result2.quantization, 'int8');
+    assert.ok(result2.quantizationRange !== undefined);
+
+    await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' });
+    const result3 = await client.vEmbRaw('key3', 'element');
+    assert.equal(result3.quantization, 'f32');
+    assert.equal(result3.quantizationRange, undefined);
+
+    await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' });
+    const result4 = await client.vEmbRaw('key4', 'element');
+    assert.equal(result4.quantization, 'bin');
+    assert.equal(result4.quantizationRange, undefined);
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vEmbRaw with RESP3', async client => {
+    await client.vAdd('key1', [1.0, 2.0, 3.0], 'element');
+    const result1 = await client.vEmbRaw('key1', 'element');
+    assert.equal(result1.quantization, 'int8');
+    assert.ok(result1.quantizationRange !== undefined);
+
+    await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' });
+    const result2 = await client.vEmbRaw('key2', 'element');
+    assert.equal(result2.quantization, 'int8');
+    assert.ok(result2.quantizationRange !== undefined);
+
+    await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' });
+    const result3 = await client.vEmbRaw('key3', 'element');
+    assert.equal(result3.quantization, 'f32');
+    assert.equal(result3.quantizationRange, undefined);
+
+    await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' });
+    const result4 = await client.vEmbRaw('key4', 'element');
+    assert.equal(result4.quantization, 'bin');
+    assert.equal(result4.quantizationRange, undefined);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VEMB_RAW.ts b/packages/client/lib/commands/VEMB_RAW.ts
new file mode 100644
index 00000000000..b6881d321c9
--- /dev/null
+++ b/packages/client/lib/commands/VEMB_RAW.ts
@@ -0,0 +1,57 @@
+import { CommandParser } from '../client/parser';
+import {
+  RedisArgument,
+  Command,
+  BlobStringReply,
+  SimpleStringReply,
+  DoubleReply
+} from '../RESP/types';
+import { transformDoubleReply } from './generic-transformers';
+import VEMB from './VEMB';
+
+type RawVembReply = {
+  quantization: SimpleStringReply;
+  raw: BlobStringReply;
+  l2Norm: DoubleReply;
+  quantizationRange?: DoubleReply;
+};
+
+const transformRawVembReply = {
+  2: (reply: any[]): RawVembReply => {
+    return {
+      quantization: reply[0],
+      raw: reply[1],
+      l2Norm: transformDoubleReply[2](reply[2]),
+      ...(reply[3] !== undefined && { quantizationRange: transformDoubleReply[2](reply[3]) })
+    };
+  },
+  3: (reply: any[]): RawVembReply => {
+    return {
+      quantization: reply[0],
+      raw: reply[1],
+      l2Norm: reply[2],
+      quantizationRange: reply[3]
+    };
+  },
+};
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve the RAW approximate vector associated with a vector set element
+   *
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param element - The name of the element to retrieve the vector for
+   * @see https://redis.io/commands/vemb/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    element: RedisArgument
+  ) {
+    VEMB.parseCommand(parser, key, element);
+    parser.push('RAW');
+  },
+  transformReply: transformRawVembReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VGETATTR.spec.ts b/packages/client/lib/commands/VGETATTR.spec.ts
new file mode 100644
index 00000000000..d904146c670
--- /dev/null
+++ b/packages/client/lib/commands/VGETATTR.spec.ts
@@ -0,0 +1,77 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VGETATTR from './VGETATTR';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VGETATTR', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VGETATTR.parseCommand(parser, 'key', 'element');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VGETATTR', 'key', 'element']
+    );
+  });
+
+  testUtils.testAll('vGetAttr', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
+
+    const nullResult = await client.vGetAttr('key', 'element');
+    assert.equal(nullResult, null);
+
+    await client.vSetAttr('key', 'element', { name: 'test' });
+
+    const result = await client.vGetAttr('key', 'element');
+
+    assert.ok(result !== null);
+    assert.equal(typeof result, 'object')
+
+    assert.deepEqual(result, {
+      name: 'test'
+    })
+
+
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vGetAttr with RESP3', async client => {
+    await client.vAdd('resp3-key', [1.0, 2.0], 'resp3-element');
+
+    // Test null case (no attributes set)
+    const nullResult = await client.vGetAttr('resp3-key', 'resp3-element');
+
+    assert.equal(nullResult, null);
+
+    // Set complex attributes and retrieve them
+    const complexAttrs = {
+      name: 'test-item',
+      category: 'electronics',
+      price: 99.99,
+      inStock: true,
+      tags: ['new', 'featured']
+    };
+    await client.vSetAttr('resp3-key', 'resp3-element', complexAttrs);
+
+    const result = await client.vGetAttr('resp3-key', 'resp3-element');
+
+    assert.ok(result !== null);
+    assert.equal(typeof result, 'object')
+
+    assert.deepEqual(result, {
+      name: 'test-item',
+      category: 'electronics',
+      price: 99.99,
+      inStock: true,
+      tags: ['new', 'featured']
+    })
+
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VGETATTR.ts b/packages/client/lib/commands/VGETATTR.ts
new file mode 100644
index 00000000000..05ec8706fb1
--- /dev/null
+++ b/packages/client/lib/commands/VGETATTR.ts
@@ -0,0 +1,21 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformRedisJsonNullReply } from './generic-transformers';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve the attributes of a vector set element
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param element - The name of the element to retrieve attributes for
+   * @see https://redis.io/commands/vgetattr/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
+    parser.push('VGETATTR');
+    parser.pushKey(key);
+    parser.push(element);
+  },
+  transformReply: transformRedisJsonNullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VINFO.spec.ts b/packages/client/lib/commands/VINFO.spec.ts
new file mode 100644
index 00000000000..074598644ff
--- /dev/null
+++ b/packages/client/lib/commands/VINFO.spec.ts
@@ -0,0 +1,58 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VINFO from './VINFO';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VINFO', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VINFO.parseCommand(parser, 'key');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VINFO', 'key']
+    );
+  });
+
+  testUtils.testAll('vInfo', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
+
+    const result = await client.vInfo('key');
+    assert.ok(typeof result === 'object' && result !== null);
+
+    assert.equal(result['vector-dim'], 3);
+    assert.equal(result['size'], 1);
+    assert.ok('quant-type' in result);
+    assert.ok('hnsw-m' in result);
+    assert.ok('projection-input-dim' in result);
+    assert.ok('max-level' in result);
+    assert.ok('attributes-count' in result);
+    assert.ok('vset-uid' in result);
+    assert.ok('hnsw-max-node-uid' in result);
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vInfo with RESP3', async client => {
+    await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element');
+
+    const result = await client.vInfo('resp3-key');
+    assert.ok(typeof result === 'object' && result !== null);
+
+    assert.equal(result['vector-dim'], 3);
+    assert.equal(result['size'], 1);
+    assert.ok('quant-type' in result);
+    assert.ok('hnsw-m' in result);
+    assert.ok('projection-input-dim' in result);
+    assert.ok('max-level' in result);
+    assert.ok('attributes-count' in result);
+    assert.ok('vset-uid' in result);
+    assert.ok('hnsw-max-node-uid' in result);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VINFO.ts b/packages/client/lib/commands/VINFO.ts
new file mode 100644
index 00000000000..4e0d68d7cb0
--- /dev/null
+++ b/packages/client/lib/commands/VINFO.ts
@@ -0,0 +1,38 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command, UnwrapReply, Resp2Reply, TuplesToMapReply, SimpleStringReply, NumberReply } from '../RESP/types';
+
+export type VInfoReplyMap = TuplesToMapReply<[
+  [SimpleStringReply<'quant-type'>, SimpleStringReply],
+  [SimpleStringReply<'vector-dim'>, NumberReply],
+  [SimpleStringReply<'size'>, NumberReply],
+  [SimpleStringReply<'max-level'>, NumberReply],
+  [SimpleStringReply<'vset-uid'>, NumberReply],
+  [SimpleStringReply<'hnsw-max-node-uid'>, NumberReply],
+]>;
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve metadata and internal details about a vector set, including size, dimensions, quantization type, and graph structure
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @see https://redis.io/commands/vinfo/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('VINFO');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<Resp2Reply<VInfoReplyMap>>): VInfoReplyMap => {
+      const ret = Object.create(null);
+
+      for (let i = 0; i < reply.length; i += 2) {
+        ret[reply[i].toString()] = reply[i + 1];
+      }
+
+      return ret as unknown as VInfoReplyMap;
+    },
+    3: undefined as unknown as () => VInfoReplyMap
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VLINKS.spec.ts b/packages/client/lib/commands/VLINKS.spec.ts
new file mode 100644
index 00000000000..e788f9f9a98
--- /dev/null
+++ b/packages/client/lib/commands/VLINKS.spec.ts
@@ -0,0 +1,42 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VLINKS from './VLINKS';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VLINKS', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VLINKS.parseCommand(parser, 'key', 'element');
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VLINKS', 'key', 'element']
+    );
+  });
+
+  testUtils.testAll('vLinks', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+    await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
+
+    const result = await client.vLinks('key', 'element1');
+    assert.ok(Array.isArray(result));
+    assert.ok(result.length)
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vLinks with RESP3', async client => {
+    await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+    await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
+
+    const result = await client.vLinks('resp3-key', 'element1');
+    assert.ok(Array.isArray(result));
+    assert.ok(result.length)
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VLINKS.ts b/packages/client/lib/commands/VLINKS.ts
new file mode 100644
index 00000000000..9e97fc7de9b
--- /dev/null
+++ b/packages/client/lib/commands/VLINKS.ts
@@ -0,0 +1,20 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve the neighbors of a specified element in a vector set; the connections for each layer of the HNSW graph
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param element - The name of the element to retrieve neighbors for
+   * @see https://redis.io/commands/vlinks/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
+    parser.push('VLINKS');
+    parser.pushKey(key);
+    parser.push(element);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<ArrayReply<BlobStringReply>>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts
new file mode 100644
index 00000000000..db96bd1a8af
--- /dev/null
+++ b/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts
@@ -0,0 +1,75 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VLINKS_WITHSCORES from './VLINKS_WITHSCORES';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VLINKS WITHSCORES', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VLINKS_WITHSCORES.parseCommand(parser, 'key', 'element');
+    assert.deepEqual(parser.redisArgs, [
+      'VLINKS',
+      'key',
+      'element',
+      'WITHSCORES'
+    ]);
+  });
+
+  testUtils.testAll(
+    'vLinksWithScores',
+    async client => {
+      // Create a vector set with multiple elements to build HNSW graph layers
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
+      await client.vAdd('key', [1.2, 2.2, 3.2], 'element3');
+      await client.vAdd('key', [2.0, 3.0, 4.0], 'element4');
+
+      const result = await client.vLinksWithScores('key', 'element1');
+
+      assert.ok(Array.isArray(result));
+
+      for (const layer of result) {
+        assert.equal(
+          typeof layer,
+          'object'
+        );
+      }
+
+      assert.ok(result.length >= 1, 'Should have at least layer 0');
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    }
+  );
+
+  testUtils.testWithClient(
+    'vLinksWithScores with RESP3',
+    async client => {
+      await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
+      await client.vAdd('resp3-key', [1.2, 2.2, 3.2], 'element3');
+      await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element4');
+
+      const result = await client.vLinksWithScores('resp3-key', 'element1');
+
+      assert.ok(Array.isArray(result));
+
+      for (const layer of result) {
+        assert.equal(
+          typeof layer,
+          'object'
+        );
+      }
+
+      assert.ok(result.length >= 1, 'Should have at least layer 0');
+    },
+    {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    }
+  );
+});
diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.ts
new file mode 100644
index 00000000000..10ebe160fcd
--- /dev/null
+++ b/packages/client/lib/commands/VLINKS_WITHSCORES.ts
@@ -0,0 +1,42 @@
+import { BlobStringReply, Command, DoubleReply, MapReply } from '../RESP/types';
+import { transformDoubleReply } from './generic-transformers';
+import VLINKS from './VLINKS';
+
+
+function transformVLinksWithScoresReply(reply: any): Array<Record<string, DoubleReply>> {
+  const layers: Array<Record<string, DoubleReply>> = [];
+
+  for (const layer of reply) {
+    const obj: Record<string, DoubleReply> = Object.create(null);
+
+    // Each layer contains alternating element names and scores
+    for (let i = 0; i < layer.length; i += 2) {
+      const element = layer[i];
+      const score = transformDoubleReply[2](layer[i + 1]);
+      obj[element.toString()] = score;
+    }
+
+    layers.push(obj);
+  }
+
+  return layers;
+}
+
+export default {
+  IS_READ_ONLY: VLINKS.IS_READ_ONLY,
+  /**
+   * Get the connections for each layer of the HNSW graph with similarity scores
+   * @param args - Same parameters as the VLINKS command
+   * @see https://redis.io/commands/vlinks/
+   */
+  parseCommand(...args: Parameters<typeof VLINKS.parseCommand>) {
+    const parser = args[0];
+
+    VLINKS.parseCommand(...args);
+    parser.push('WITHSCORES');
+  },
+  transformReply: {
+    2: transformVLinksWithScoresReply,
+    3: undefined as unknown as () => Array<MapReply<BlobStringReply, DoubleReply>>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VRANDMEMBER.spec.ts b/packages/client/lib/commands/VRANDMEMBER.spec.ts
new file mode 100644
index 00000000000..28c020e3563
--- /dev/null
+++ b/packages/client/lib/commands/VRANDMEMBER.spec.ts
@@ -0,0 +1,201 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VRANDMEMBER from './VRANDMEMBER';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VRANDMEMBER', () => {
+  describe('parseCommand', () => {
+    it('without count', () => {
+      const parser = new BasicCommandParser();
+      VRANDMEMBER.parseCommand(parser, 'key');
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VRANDMEMBER', 'key']
+      );
+    });
+
+    it('with count', () => {
+      const parser = new BasicCommandParser();
+      VRANDMEMBER.parseCommand(parser, 'key', 2);
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VRANDMEMBER', 'key', '2']
+      );
+    });
+  });
+
+  describe('RESP2 tests', () => {
+    testUtils.testAll('vRandMember without count - returns single element as string', async client => {
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
+      await client.vAdd('key', [7.0, 8.0, 9.0], 'element3');
+
+      const result = await client.vRandMember('key');
+      assert.equal(typeof result, 'string');
+      assert.ok(['element1', 'element2', 'element3'].includes(result as string));
+    }, {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    });
+
+    testUtils.testAll('vRandMember with positive count - returns distinct elements', async client => {
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
+      await client.vAdd('key', [7.0, 8.0, 9.0], 'element3');
+
+      const result = await client.vRandMember('key', 2);
+      assert.ok(Array.isArray(result));
+      assert.equal(result.length, 2);
+
+    }, {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    });
+
+    testUtils.testAll('vRandMember with negative count - allows duplicates', async client => {
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
+
+      const result = await client.vRandMember('key', -5);
+      assert.ok(Array.isArray(result));
+      assert.equal(result.length, 5);
+
+      // All elements should be from our set (duplicates allowed)
+      result.forEach(element => {
+        assert.ok(['element1', 'element2'].includes(element));
+      });
+    }, {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    });
+
+    testUtils.testAll('vRandMember count exceeds set size - returns entire set', async client => {
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
+
+      const result = await client.vRandMember('key', 10);
+      assert.ok(Array.isArray(result));
+      assert.equal(result.length, 2); // Only 2 elements exist
+
+      // Should contain both elements
+      assert.ok(result.includes('element1'));
+      assert.ok(result.includes('element2'));
+    }, {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    });
+
+    testUtils.testAll('vRandMember on non-existent key', async client => {
+      // Without count - should return null
+      const resultNoCount = await client.vRandMember('nonexistent');
+      assert.equal(resultNoCount, null);
+
+      // With count - should return empty array
+      const resultWithCount = await client.vRandMember('nonexistent', 5);
+      assert.ok(Array.isArray(resultWithCount));
+      assert.equal(resultWithCount.length, 0);
+    }, {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    });
+  });
+
+  describe('RESP3 tests', () => {
+    testUtils.testWithClient('vRandMember without count - returns single element as string', async client => {
+      await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
+      await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3');
+
+      const result = await client.vRandMember('resp3-key');
+      assert.equal(typeof result, 'string');
+      assert.ok(['element1', 'element2', 'element3'].includes(result as string));
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    });
+
+    testUtils.testWithClient('vRandMember with positive count - returns distinct elements', async client => {
+      await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
+      await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3');
+
+      const result = await client.vRandMember('resp3-key', 2);
+      assert.ok(Array.isArray(result));
+      assert.equal(result.length, 2);
+
+      // Should be distinct elements (no duplicates)
+      const uniqueElements = new Set(result);
+      assert.equal(uniqueElements.size, 2);
+
+      // All elements should be from our set
+      result.forEach(element => {
+        assert.ok(['element1', 'element2', 'element3'].includes(element));
+      });
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    });
+
+    testUtils.testWithClient('vRandMember with negative count - allows duplicates', async client => {
+      await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
+
+      const result = await client.vRandMember('resp3-key', -5);
+      assert.ok(Array.isArray(result));
+      assert.equal(result.length, 5);
+
+      // All elements should be from our set (duplicates allowed)
+      result.forEach(element => {
+        assert.ok(['element1', 'element2'].includes(element));
+      });
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    });
+
+    testUtils.testWithClient('vRandMember count exceeds set size - returns entire set', async client => {
+      await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
+
+      const result = await client.vRandMember('resp3-key', 10);
+      assert.ok(Array.isArray(result));
+      assert.equal(result.length, 2); // Only 2 elements exist
+
+      // Should contain both elements
+      assert.ok(result.includes('element1'));
+      assert.ok(result.includes('element2'));
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    });
+
+    testUtils.testWithClient('vRandMember on non-existent key', async client => {
+      // Without count - should return null
+      const resultNoCount = await client.vRandMember('resp3-nonexistent');
+      assert.equal(resultNoCount, null);
+
+      // With count - should return empty array
+      const resultWithCount = await client.vRandMember('resp3-nonexistent', 5);
+      assert.ok(Array.isArray(resultWithCount));
+      assert.equal(resultWithCount.length, 0);
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    });
+  });
+});
diff --git a/packages/client/lib/commands/VRANDMEMBER.ts b/packages/client/lib/commands/VRANDMEMBER.ts
new file mode 100644
index 00000000000..299af33b9fa
--- /dev/null
+++ b/packages/client/lib/commands/VRANDMEMBER.ts
@@ -0,0 +1,23 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, ArrayReply, Command, NullReply } from '../RESP/types';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve random elements of a vector set
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param count - Optional number of elements to return
+   * @see https://redis.io/commands/vrandmember/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count?: number) {
+    parser.push('VRANDMEMBER');
+    parser.pushKey(key);
+    
+    if (count !== undefined) {
+      parser.push(count.toString());
+    }
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | ArrayReply<BlobStringReply> | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VREM.spec.ts b/packages/client/lib/commands/VREM.spec.ts
new file mode 100644
index 00000000000..9e558c991c1
--- /dev/null
+++ b/packages/client/lib/commands/VREM.spec.ts
@@ -0,0 +1,63 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VREM from './VREM';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VREM', () => {
+  const parser = new BasicCommandParser();
+  VREM.parseCommand(parser, 'key', 'element');
+  it('parseCommand', () => {
+    assert.deepEqual(
+      parser.redisArgs,
+      ['VREM', 'key', 'element']
+    );
+  });
+
+  testUtils.testAll('vRem', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
+
+    assert.equal(
+      await client.vRem('key', 'element'),
+      true
+    );
+
+    assert.equal(
+      await client.vRem('key', 'element'),
+      false
+    );
+
+    assert.equal(
+      await client.vCard('key'),
+      0
+    );
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vRem with RESP3', async client => {
+    await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element');
+
+    assert.equal(
+      await client.vRem('resp3-key', 'resp3-element'),
+      true
+    );
+
+    assert.equal(
+      await client.vRem('resp3-key', 'resp3-element'),
+      false
+    );
+
+
+    assert.equal(
+      await client.vCard('resp3-key'),
+      0
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VREM.ts b/packages/client/lib/commands/VREM.ts
new file mode 100644
index 00000000000..7eb22b2e2ec
--- /dev/null
+++ b/packages/client/lib/commands/VREM.ts
@@ -0,0 +1,20 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformBooleanReply } from './generic-transformers';
+
+export default {
+  /**
+   * Remove an element from a vector set
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param element - The name of the element to remove from the vector set
+   * @see https://redis.io/commands/vrem/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
+    parser.push('VREM');
+    parser.pushKey(key);
+    parser.push(element);
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VSETATTR.spec.ts b/packages/client/lib/commands/VSETATTR.spec.ts
new file mode 100644
index 00000000000..303006d4081
--- /dev/null
+++ b/packages/client/lib/commands/VSETATTR.spec.ts
@@ -0,0 +1,58 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VSETATTR from './VSETATTR';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VSETATTR', () => {
+  describe('parseCommand', () => {
+    it('with object', () => {
+      const parser = new BasicCommandParser();
+      VSETATTR.parseCommand(parser, 'key', 'element', { name: 'test', value: 42 }),
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VSETATTR', 'key', 'element', '{"name":"test","value":42}']
+      );
+    });
+
+    it('with string', () => {
+      const parser = new BasicCommandParser();
+      VSETATTR.parseCommand(parser, 'key', 'element', '{"name":"test"}'),
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VSETATTR', 'key', 'element', '{"name":"test"}']
+      );
+    });
+  });
+
+  testUtils.testAll('vSetAttr', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
+
+    assert.equal(
+      await client.vSetAttr('key', 'element', { name: 'test', value: 42 }),
+      true
+    );
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vSetAttr with RESP3 - returns boolean', async client => {
+    await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element');
+
+    const result = await client.vSetAttr('resp3-key', 'resp3-element', {
+      name: 'test-item',
+      category: 'electronics',
+      price: 99.99
+    });
+
+    // RESP3 returns boolean instead of number
+    assert.equal(typeof result, 'boolean');
+    assert.equal(result, true);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VSETATTR.ts b/packages/client/lib/commands/VSETATTR.ts
new file mode 100644
index 00000000000..084b8f8008e
--- /dev/null
+++ b/packages/client/lib/commands/VSETATTR.ts
@@ -0,0 +1,32 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformBooleanReply } from './generic-transformers';
+
+export default {
+  /**
+   * Set or replace attributes on a vector set element
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param element - The name of the element to set attributes for
+   * @param attributes - The attributes to set (as JSON string or object)
+   * @see https://redis.io/commands/vsetattr/
+   */
+  parseCommand(
+    parser: CommandParser, 
+    key: RedisArgument, 
+    element: RedisArgument, 
+    attributes: RedisArgument | Record<string, any>
+  ) {
+    parser.push('VSETATTR');
+    parser.pushKey(key);
+    parser.push(element);
+    
+    if (typeof attributes === 'object' && attributes !== null) {
+      parser.push(JSON.stringify(attributes));
+    } else {
+      parser.push(attributes);
+    }
+  },
+  transformReply: transformBooleanReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VSIM.spec.ts b/packages/client/lib/commands/VSIM.spec.ts
new file mode 100644
index 00000000000..dbfad76fd59
--- /dev/null
+++ b/packages/client/lib/commands/VSIM.spec.ts
@@ -0,0 +1,107 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VSIM from './VSIM';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VSIM', () => {
+  describe('parseCommand', () => {
+    it('with vector', () => {
+      const parser = new BasicCommandParser();
+      VSIM.parseCommand(parser, 'key', [1.0, 2.0, 3.0]),
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VSIM', 'key', 'VALUES', '3', '1', '2', '3']
+      );
+    });
+
+    it('with element', () => {
+      const parser = new BasicCommandParser();
+      VSIM.parseCommand(parser, 'key', 'element');
+      assert.deepEqual(
+        parser.redisArgs,
+        ['VSIM', 'key', 'ELE', 'element']
+      );
+    });
+
+    it('with options', () => {
+      const parser = new BasicCommandParser();
+      VSIM.parseCommand(parser, 'key', 'element', {
+        COUNT: 5,
+        EF: 100,
+        FILTER: '.price > 20',
+        'FILTER-EF': 50,
+        TRUTH: true,
+        NOTHREAD: true,
+        EPSILON: 0.1
+      });
+      assert.deepEqual(
+        parser.redisArgs,
+        [
+          'VSIM', 'key', 'ELE', 'element', 'COUNT', '5',
+          'EPSILON', '0.1', 'EF', '100', 'FILTER', '.price > 20',
+          'FILTER-EF', '50', 'TRUTH', 'NOTHREAD',
+        ]
+      );
+    });
+  });
+
+  testUtils.testAll('vSim', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+    await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
+
+    const result = await client.vSim('key', 'element1');
+    assert.ok(Array.isArray(result));
+    assert.ok(result.includes('element1'));
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+
+  testUtils.testAll('vSim with options', async client => {
+    await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+    await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
+
+    const result = await client.vSim('key', 'element1', { 
+      EPSILON: 0.1,
+      COUNT: 1,
+      EF: 100,
+      FILTER: '.year == 8',
+      'FILTER-EF': 50,
+      TRUTH: true,
+      NOTHREAD: true
+    });
+
+    assert.ok(Array.isArray(result));
+  }, {
+    client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+    cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+  });
+
+  testUtils.testWithClient('vSim with RESP3', async client => {
+    await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+    await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
+    await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3');
+
+    // Test similarity search with vector
+    const resultWithVector = await client.vSim('resp3-key', [1.05, 2.05, 3.05]);
+    assert.ok(Array.isArray(resultWithVector));
+    assert.ok(resultWithVector.length > 0);
+
+    // Test similarity search with element
+    const resultWithElement = await client.vSim('resp3-key', 'element1');
+    assert.ok(Array.isArray(resultWithElement));
+    assert.ok(resultWithElement.includes('element1'));
+
+    // Test with options
+    const resultWithOptions = await client.vSim('resp3-key', 'element1', { COUNT: 2 });
+    assert.ok(Array.isArray(resultWithOptions));
+    assert.ok(resultWithOptions.length <= 2);
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    },
+    minimumDockerVersion: [8, 0]
+  });
+});
diff --git a/packages/client/lib/commands/VSIM.ts b/packages/client/lib/commands/VSIM.ts
new file mode 100644
index 00000000000..7c94cd7c79d
--- /dev/null
+++ b/packages/client/lib/commands/VSIM.ts
@@ -0,0 +1,73 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { transformDoubleArgument } from './generic-transformers';
+
+export interface VSimOptions {
+  COUNT?: number;
+  EPSILON?: number;
+  EF?: number;
+  FILTER?: string;
+  'FILTER-EF'?: number;
+  TRUTH?: boolean;
+  NOTHREAD?: boolean;
+}
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Retrieve elements similar to a given vector or element with optional filtering
+   * 
+   * @param parser - The command parser
+   * @param key - The key of the vector set
+   * @param query - The query vector (array of numbers) or element name (string)
+   * @param options - Optional parameters for similarity search
+   * @see https://redis.io/commands/vsim/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    query: RedisArgument | Array<number>,
+    options?: VSimOptions
+  ) {
+    parser.push('VSIM');
+    parser.pushKey(key);
+
+    if (Array.isArray(query)) {
+      parser.push('VALUES', query.length.toString());
+      for (const value of query) {
+        parser.push(transformDoubleArgument(value));
+      }
+    } else {
+      parser.push('ELE', query);
+    }
+
+    if (options?.COUNT !== undefined) {
+      parser.push('COUNT', options.COUNT.toString());
+    }
+
+    if (options?.EPSILON !== undefined) {
+      parser.push('EPSILON', options.EPSILON.toString());
+    }
+
+    if (options?.EF !== undefined) {
+      parser.push('EF', options.EF.toString());
+    }
+
+    if (options?.FILTER) {
+      parser.push('FILTER', options.FILTER);
+    }
+
+    if (options?.['FILTER-EF'] !== undefined) {
+      parser.push('FILTER-EF', options['FILTER-EF'].toString());
+    }
+
+    if (options?.TRUTH) {
+      parser.push('TRUTH');
+    }
+
+    if (options?.NOTHREAD) {
+      parser.push('NOTHREAD');
+    }
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts b/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts
new file mode 100644
index 00000000000..ff9bc41376f
--- /dev/null
+++ b/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts
@@ -0,0 +1,62 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import VSIM_WITHSCORES from './VSIM_WITHSCORES';
+import { BasicCommandParser } from '../client/parser';
+
+describe('VSIM WITHSCORES', () => {
+  it('parseCommand', () => {
+    const parser = new BasicCommandParser();
+    VSIM_WITHSCORES.parseCommand(parser, 'key', 'element')
+    assert.deepEqual(parser.redisArgs, [
+      'VSIM',
+      'key',
+      'ELE',
+      'element',
+      'WITHSCORES'
+    ]);
+  });
+
+  testUtils.testAll(
+    'vSimWithScores',
+    async client => {
+      await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
+
+      const result = await client.vSimWithScores('key', 'element1');
+
+      assert.ok(typeof result === 'object');
+      assert.ok('element1' in result);
+      assert.ok('element2' in result);
+      assert.equal(typeof result['element1'], 'number');
+      assert.equal(typeof result['element2'], 'number');
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
+    }
+  );
+
+  testUtils.testWithClient(
+    'vSimWithScores with RESP3 - returns Map with scores',
+    async client => {
+      await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
+      await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
+      await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3');
+
+      const result = await client.vSimWithScores('resp3-key', 'element1');
+
+      assert.ok(typeof result === 'object');
+      assert.ok('element1' in result);
+      assert.ok('element2' in result);
+      assert.equal(typeof result['element1'], 'number');
+      assert.equal(typeof result['element2'], 'number');
+    },
+    {
+      ...GLOBAL.SERVERS.OPEN,
+      clientOptions: {
+        RESP: 3
+      },
+      minimumDockerVersion: [8, 0]
+    }
+  );
+});
diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.ts b/packages/client/lib/commands/VSIM_WITHSCORES.ts
new file mode 100644
index 00000000000..fda05be6642
--- /dev/null
+++ b/packages/client/lib/commands/VSIM_WITHSCORES.ts
@@ -0,0 +1,36 @@
+import {
+  ArrayReply,
+  BlobStringReply,
+  Command,
+  DoubleReply,
+  MapReply,
+  UnwrapReply
+} from '../RESP/types';
+import { transformDoubleReply } from './generic-transformers';
+import VSIM from './VSIM';
+
+export default {
+  IS_READ_ONLY: VSIM.IS_READ_ONLY,
+  /**
+   * Retrieve elements similar to a given vector or element with similarity scores
+   * @param args - Same parameters as the VSIM command
+   * @see https://redis.io/commands/vsim/
+   */
+  parseCommand(...args: Parameters<typeof VSIM.parseCommand>) {
+    const parser = args[0];
+
+    VSIM.parseCommand(...args);
+    parser.push('WITHSCORES');
+  },
+  transformReply: {
+    2: (reply: ArrayReply<BlobStringReply>) => {
+      const inferred = reply as unknown as UnwrapReply<typeof reply>;
+      const members: Record<string, DoubleReply> = {};
+      for (let i = 0; i < inferred.length; i += 2) {
+        members[inferred[i].toString()] = transformDoubleReply[2](inferred[i + 1]);
+      }
+      return members;
+    },
+    3: undefined as unknown as () => MapReply<BlobStringReply, DoubleReply>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/WAIT.spec.ts b/packages/client/lib/commands/WAIT.spec.ts
index c85ef598612..d2778e7967b 100644
--- a/packages/client/lib/commands/WAIT.spec.ts
+++ b/packages/client/lib/commands/WAIT.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './WAIT';
+import WAIT from './WAIT';
+import { parseArgs } from './generic-transformers';
 
 describe('WAIT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(0, 1),
-            ['WAIT', '0', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(WAIT, 0, 1),
+      ['WAIT', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.wait', async client => {
-        assert.equal(
-            await client.wait(0, 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.wait', async client => {
+    assert.equal(
+      await client.wait(0, 1),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/WAIT.ts b/packages/client/lib/commands/WAIT.ts
index dff51ed9680..7ccebbc4ec9 100644
--- a/packages/client/lib/commands/WAIT.ts
+++ b/packages/client/lib/commands/WAIT.ts
@@ -1,7 +1,20 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
 
-export function transformArguments(numberOfReplicas: number, timeout: number): Array<string> {
-    return ['WAIT', numberOfReplicas.toString(), timeout.toString()];
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the WAIT command to synchronize with replicas
+   *
+   * @param parser - The command parser
+   * @param numberOfReplicas - Number of replicas that must acknowledge the write
+   * @param timeout - Maximum time to wait in milliseconds
+   * @returns The number of replicas that acknowledged the write
+   * @see https://redis.io/commands/wait/
+   */
+  parseCommand(parser: CommandParser, numberOfReplicas: number, timeout: number) {
+    parser.push('WAIT', numberOfReplicas.toString(), timeout.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/WATCH.spec.ts b/packages/client/lib/commands/WATCH.spec.ts
deleted file mode 100644
index acaa062874f..00000000000
--- a/packages/client/lib/commands/WATCH.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './WATCH';
-
-describe('WATCH', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['WATCH', 'key']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['WATCH', '1', '2']
-            );
-        });
-    });
-});
diff --git a/packages/client/lib/commands/WATCH.ts b/packages/client/lib/commands/WATCH.ts
deleted file mode 100644
index 58c6dfd1dad..00000000000
--- a/packages/client/lib/commands/WATCH.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: string | Array<string>): RedisCommandArguments {
-    return pushVerdictArguments(['WATCH'], key);
-}
-
-export declare function transformReply(): string;
diff --git a/packages/client/lib/commands/XACK.spec.ts b/packages/client/lib/commands/XACK.spec.ts
index 0586a5921fd..4ad60b256d0 100644
--- a/packages/client/lib/commands/XACK.spec.ts
+++ b/packages/client/lib/commands/XACK.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XACK';
+import XACK from './XACK';
+import { parseArgs } from './generic-transformers';
 
 describe('XACK', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '1-0'),
-                ['XACK', 'key', 'group', '1-0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(XACK, 'key', 'group', '0-0'),
+        ['XACK', 'key', 'group', '0-0']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', ['1-0', '2-0']),
-                ['XACK', 'key', 'group', '1-0', '2-0']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(XACK, 'key', 'group', ['0-0', '1-0']),
+        ['XACK', 'key', 'group', '0-0', '1-0']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.xAck', async client => {
-        assert.equal(
-            await client.xAck('key', 'group', '1-0'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xAck', async client => {
+    assert.equal(
+      await client.xAck('key', 'group', '0-0'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XACK.ts b/packages/client/lib/commands/XACK.ts
index 670d810fc00..26e1c962baa 100644
--- a/packages/client/lib/commands/XACK.ts
+++ b/packages/client/lib/commands/XACK.ts
@@ -1,14 +1,25 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    id: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['XACK', key, group], id);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XACK command to acknowledge the processing of stream messages in a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - The consumer group name
+   * @param id - One or more message IDs to acknowledge
+   * @returns The number of messages successfully acknowledged
+   * @see https://redis.io/commands/xack/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument, id: RedisVariadicArgument) {
+    parser.push('XACK');
+    parser.pushKey(key);
+    parser.push(group)
+    parser.pushVariadic(id);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
+ 
\ No newline at end of file
diff --git a/packages/client/lib/commands/XACKDEL.spec.ts b/packages/client/lib/commands/XACKDEL.spec.ts
new file mode 100644
index 00000000000..9d7bad15a24
--- /dev/null
+++ b/packages/client/lib/commands/XACKDEL.spec.ts
@@ -0,0 +1,196 @@
+import { strict as assert } from "node:assert";
+import XACKDEL from "./XACKDEL";
+import { parseArgs } from "./generic-transformers";
+import testUtils, { GLOBAL } from "../test-utils";
+import {
+  STREAM_DELETION_POLICY,
+  STREAM_DELETION_REPLY_CODES,
+} from "./common-stream.types";
+
+describe("XACKDEL", () => {
+  describe("transformArguments", () => {
+    it("string - without policy", () => {
+      assert.deepEqual(parseArgs(XACKDEL, "key", "group", "0-0"), [
+        "XACKDEL",
+        "key",
+        "group",
+        "IDS",
+        "1",
+        "0-0",
+      ]);
+    });
+
+    it("string - with policy", () => {
+      assert.deepEqual(
+        parseArgs(
+          XACKDEL,
+          "key",
+          "group",
+          "0-0",
+          STREAM_DELETION_POLICY.KEEPREF
+        ),
+        ["XACKDEL", "key", "group", "KEEPREF", "IDS", "1", "0-0"]
+      );
+    });
+
+    it("array - without policy", () => {
+      assert.deepEqual(parseArgs(XACKDEL, "key", "group", ["0-0", "1-0"]), [
+        "XACKDEL",
+        "key",
+        "group",
+        "IDS",
+        "2",
+        "0-0",
+        "1-0",
+      ]);
+    });
+
+    it("array - with policy", () => {
+      assert.deepEqual(
+        parseArgs(
+          XACKDEL,
+          "key",
+          "group",
+          ["0-0", "1-0"],
+          STREAM_DELETION_POLICY.DELREF
+        ),
+        ["XACKDEL", "key", "group", "DELREF", "IDS", "2", "0-0", "1-0"]
+      );
+    });
+  });
+
+  testUtils.testAll(
+    `XACKDEL non-existing key - without policy`,
+    async (client) => {
+      const reply = await client.xAckDel("{tag}stream-key", "testgroup", "0-0");
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.NOT_FOUND]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XACKDEL existing key - without policy`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const groupName = "testgroup";
+
+      // create consumer group, stream and message
+      await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true });
+      const messageId = await client.xAdd(streamKey, "*", { field: "value" });
+
+      // read message
+      await client.xReadGroup(groupName, "testconsumer", {
+        key: streamKey,
+        id: ">",
+      });
+
+      const reply = await client.xAckDel(streamKey, groupName, messageId);
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XACKDEL existing key - with policy`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const groupName = "testgroup";
+
+      // create consumer group, stream and message
+      await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true });
+      const messageId = await client.xAdd(streamKey, "*", { field: "value" });
+
+      // read message
+      await client.xReadGroup(groupName, "testconsumer", {
+        key: streamKey,
+        id: ">",
+      });
+
+      const reply = await client.xAckDel(
+        streamKey,
+        groupName,
+        messageId,
+        STREAM_DELETION_POLICY.DELREF
+      );
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XACKDEL acknowledge policy - with consumer group`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const groupName = "testgroup";
+
+      // create consumer groups, stream and message
+      await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true });
+      await client.xGroupCreate(streamKey, "some-other-group", "0");
+      const messageId = await client.xAdd(streamKey, "*", { field: "value" });
+
+      // read message
+      await client.xReadGroup(groupName, "testconsumer", {
+        key: streamKey,
+        id: ">",
+      });
+
+      const reply = await client.xAckDel(
+        streamKey,
+        groupName,
+        messageId,
+        STREAM_DELETION_POLICY.ACKED
+      );
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DANGLING_REFS]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XACKDEL multiple keys`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const groupName = "testgroup";
+
+      // create consumer groups, stream and add messages
+      await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true });
+      const messageIds = await Promise.all([
+        client.xAdd(streamKey, "*", { field: "value1" }),
+        client.xAdd(streamKey, "*", { field: "value2" }),
+      ]);
+
+      // read messages
+      await client.xReadGroup(groupName, "testconsumer", {
+        key: streamKey,
+        id: ">",
+      });
+
+      const reply = await client.xAckDel(
+        streamKey,
+        groupName,
+        [...messageIds, "0-0"],
+        STREAM_DELETION_POLICY.DELREF
+      );
+      assert.deepEqual(reply, [
+        STREAM_DELETION_REPLY_CODES.DELETED,
+        STREAM_DELETION_REPLY_CODES.DELETED,
+        STREAM_DELETION_REPLY_CODES.NOT_FOUND,
+      ]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+});
diff --git a/packages/client/lib/commands/XACKDEL.ts b/packages/client/lib/commands/XACKDEL.ts
new file mode 100644
index 00000000000..6e209879e49
--- /dev/null
+++ b/packages/client/lib/commands/XACKDEL.ts
@@ -0,0 +1,45 @@
+import { CommandParser } from "../client/parser";
+import { RedisArgument, ArrayReply, Command } from "../RESP/types";
+import {
+  StreamDeletionReplyCode,
+  StreamDeletionPolicy,
+} from "./common-stream.types";
+import { RedisVariadicArgument } from "./generic-transformers";
+
+/**
+ * Acknowledges and deletes one or multiple messages for a stream consumer group
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XACKDEL command to acknowledge and delete one or multiple messages for a stream consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - The consumer group name
+   * @param id - One or more message IDs to acknowledge and delete
+   * @param policy - Policy to apply when deleting entries (optional, defaults to KEEPREF)
+   * @returns Array of integers: -1 (not found), 1 (acknowledged and deleted), 2 (acknowledged with dangling refs)
+   * @see https://redis.io/commands/xackdel/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    id: RedisVariadicArgument,
+    policy?: StreamDeletionPolicy
+  ) {
+    parser.push("XACKDEL");
+    parser.pushKey(key);
+    parser.push(group);
+
+    if (policy) {
+      parser.push(policy);
+    }
+
+    parser.push("IDS");
+    parser.pushVariadicWithLength(id);
+  },
+  transformReply:
+    undefined as unknown as () => ArrayReply<StreamDeletionReplyCode>,
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XADD.spec.ts b/packages/client/lib/commands/XADD.spec.ts
index 4b556ecc27c..a41e8682751 100644
--- a/packages/client/lib/commands/XADD.spec.ts
+++ b/packages/client/lib/commands/XADD.spec.ts
@@ -1,118 +1,174 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XADD';
+import XADD from './XADD';
+import { parseArgs } from './generic-transformers';
+import { STREAM_DELETION_POLICY } from './common-stream.types';
 
 describe('XADD', () => {
-    describe('transformArguments', () => {
-        it('single field', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }),
-                ['XADD', 'key', '*', 'field', 'value']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single field', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }),
+        ['XADD', 'key', '*', 'field', 'value']
+      );
+    });
 
-        it('multiple fields', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    '1': 'I',
-                    '2': 'II'
-                }),
-                ['XADD', 'key', '*', '1', 'I', '2', 'II']
-            );
-        });
+    it('multiple fields', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          '1': 'I',
+          '2': 'II'
+        }),
+        ['XADD', 'key', '*', '1', 'I', '2', 'II']
+      );
+    });
 
-        it('with NOMKSTREAM', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }, {
-                    NOMKSTREAM: true
-                }),
-                ['XADD', 'key', 'NOMKSTREAM', '*', 'field', 'value']
-            );
-        });
+    it('with TRIM', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            threshold: 1000
+          }
+        }),
+        ['XADD', 'key', '1000', '*', 'field', 'value']
+      );
+    });
 
-        it('with TRIM', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }, {
-                    TRIM: {
-                        threshold: 1000
-                    }
-                }),
-                ['XADD', 'key', '1000', '*', 'field', 'value']
-            );
-        });
+    it('with TRIM.strategy', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            strategy: 'MAXLEN',
+            threshold: 1000
+          }
+        }),
+        ['XADD', 'key', 'MAXLEN', '1000', '*', 'field', 'value']
+      );
+    });
 
-        it('with TRIM.strategy', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }, {
-                    TRIM: {
-                        strategy: 'MAXLEN',
-                        threshold: 1000
-                    }
-                }),
-                ['XADD', 'key', 'MAXLEN', '1000', '*','field', 'value']
-            );
-        });
+    it('with TRIM.strategyModifier', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            strategyModifier: '=',
+            threshold: 1000
+          }
+        }),
+        ['XADD', 'key', '=', '1000', '*', 'field', 'value']
+      );
+    });
 
-        it('with TRIM.strategyModifier', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }, {
-                    TRIM: {
-                        strategyModifier: '=',
-                        threshold: 1000
-                    }
-                }),
-                ['XADD', 'key', '=', '1000', '*', 'field', 'value']
-            );
-        });
+    it('with TRIM.limit', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            threshold: 1000,
+            limit: 1
+          }
+        }),
+        ['XADD', 'key', '1000', 'LIMIT', '1', '*', 'field', 'value']
+      );
+    });
 
-        it('with TRIM.limit', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }, {
-                    TRIM: {
-                        threshold: 1000,
-                        limit: 1
-                    }
-                }),
-                ['XADD', 'key', '1000', 'LIMIT', '1', '*', 'field', 'value']
-            );
-        });
+    it('with TRIM.policy', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            threshold: 1000,
+            policy: STREAM_DELETION_POLICY.DELREF
+          }
+        }),
+        ['XADD', 'key', '1000', 'DELREF', '*', 'field', 'value']
+      );
+    });
 
-        it('with NOMKSTREAM, TRIM, TRIM.*', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', {
-                    field: 'value'
-                }, {
-                    NOMKSTREAM: true,
-                    TRIM: {
-                        strategy: 'MAXLEN',
-                        strategyModifier: '=',
-                        threshold: 1000,
-                        limit: 1
-                    }
-                }),
-                ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '=', '1000', 'LIMIT', '1', '*', 'field', 'value']
-            );
-        });
+    it('with all TRIM options', () => {
+      assert.deepEqual(
+        parseArgs(XADD, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            strategy: 'MAXLEN',
+            strategyModifier: '~',
+            threshold: 1000,
+            limit: 100,
+            policy: STREAM_DELETION_POLICY.ACKED
+          }
+        }),
+        ['XADD', 'key', 'MAXLEN', '~', '1000', 'LIMIT', '100', 'ACKED', '*', 'field', 'value']
+      );
     });
+  });
+
+  testUtils.testAll('xAdd', async client => {
+    assert.equal(
+      typeof await client.xAdd('key', '*', {
+        field: 'value'
+      }),
+      'string'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll(
+    'xAdd with TRIM policy',
+    async (client) => {
+      assert.equal(
+        typeof await client.xAdd('{tag}key', '*', 
+          { field: 'value' },
+          {
+            TRIM: {
+              strategy: 'MAXLEN',
+              threshold: 1000,
+              policy: STREAM_DELETION_POLICY.KEEPREF
+            }
+          }
+        ),
+        'string'
+      );
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
 
-    testUtils.testWithClient('client.xAdd', async client => {
-        assert.equal(
-            typeof await client.xAdd('key', '*', {
-                field: 'value'
-            }),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll(
+    'xAdd with all TRIM options',
+    async (client) => {
+      assert.equal(
+        typeof await client.xAdd('{tag}key2', '*', 
+          { field: 'value' },
+          {
+            TRIM: {
+              strategy: 'MAXLEN',
+              strategyModifier: '~',
+              threshold: 1000,
+              limit: 10,
+              policy: STREAM_DELETION_POLICY.DELREF
+            }
+          }
+        ),
+        'string'
+      );
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
 });
diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts
index e7a1b6804ff..f2509a9fa7b 100644
--- a/packages/client/lib/commands/XADD.ts
+++ b/packages/client/lib/commands/XADD.ts
@@ -1,52 +1,95 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface XAddOptions {
-    NOMKSTREAM?: true;
-    TRIM?: {
-        strategy?: 'MAXLEN' | 'MINID';
-        strategyModifier?: '=' | '~';
-        threshold: number;
-        limit?: number;
-    };
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, Command } from '../RESP/types';
+import { StreamDeletionPolicy } from './common-stream.types';
+import { Tail } from './generic-transformers';
+
+/**
+ * Options for the XADD command
+ * 
+ * @property TRIM - Optional trimming configuration
+ * @property TRIM.strategy - Trim strategy: MAXLEN (by length) or MINID (by ID)
+ * @property TRIM.strategyModifier - Exact ('=') or approximate ('~') trimming
+ * @property TRIM.threshold - Maximum stream length or minimum ID to retain
+ * @property TRIM.limit - Maximum number of entries to trim in one call
+ * @property TRIM.policy - Policy to apply when trimming entries (optional, defaults to KEEPREF)
+ */
+export interface XAddOptions {
+  TRIM?: {
+    strategy?: 'MAXLEN' | 'MINID';
+    strategyModifier?: '=' | '~';
+    threshold: number;
+    limit?: number;
+    /** added in 8.2 */
+    policy?: StreamDeletionPolicy;
+  };
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    id: RedisCommandArgument,
-    message: Record<string, RedisCommandArgument>,
-    options?: XAddOptions
-): RedisCommandArguments {
-    const args = ['XADD', key];
+/**
+ * Parses arguments for the XADD command
+ *
+ * @param optional - Optional command modifier
+ * @param parser - The command parser
+ * @param key - The stream key
+ * @param id - Message ID (* for auto-generation)
+ * @param message - Key-value pairs representing the message fields
+ * @param options - Additional options for stream trimming
+ */
+export function parseXAddArguments(
+  optional: RedisArgument | undefined,
+  parser: CommandParser,
+  key: RedisArgument,
+  id: RedisArgument,
+  message: Record<string, RedisArgument>,
+  options?: XAddOptions
+) {
+  parser.push('XADD');
+  parser.pushKey(key);
+  if (optional) {
+    parser.push(optional);
+  }
 
-    if (options?.NOMKSTREAM) {
-        args.push('NOMKSTREAM');
+  if (options?.TRIM) {
+    if (options.TRIM.strategy) {
+      parser.push(options.TRIM.strategy);
     }
 
-    if (options?.TRIM) {
-        if (options.TRIM.strategy) {
-            args.push(options.TRIM.strategy);
-        }
-
-        if (options.TRIM.strategyModifier) {
-            args.push(options.TRIM.strategyModifier);
-        }
+    if (options.TRIM.strategyModifier) {
+      parser.push(options.TRIM.strategyModifier);
+    }
 
-        args.push(options.TRIM.threshold.toString());
+    parser.push(options.TRIM.threshold.toString());
 
-        if (options.TRIM.limit) {
-            args.push('LIMIT', options.TRIM.limit.toString());
-        }
+    if (options.TRIM.limit) {
+      parser.push('LIMIT', options.TRIM.limit.toString());
     }
 
-    args.push(id);
-
-    for (const [key, value] of Object.entries(message)) {
-        args.push(key, value);
+    if (options.TRIM.policy) {
+      parser.push(options.TRIM.policy);
     }
+  }
+
+  parser.push(id);
 
-    return args;
+  for (const [key, value] of Object.entries(message)) {
+    parser.push(key, value);
+  }
 }
 
-export declare function transformReply(): string;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XADD command to append a new entry to a stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param id - Message ID (* for auto-generation)
+   * @param message - Key-value pairs representing the message fields
+   * @param options - Additional options for stream trimming
+   * @returns The ID of the added entry
+   * @see https://redis.io/commands/xadd/
+   */
+  parseCommand(...args: Tail<Parameters<typeof parseXAddArguments>>) {
+    return parseXAddArguments(undefined, ...args);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts
new file mode 100644
index 00000000000..a957d0f06c1
--- /dev/null
+++ b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts
@@ -0,0 +1,162 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import XADD_NOMKSTREAM from './XADD_NOMKSTREAM';
+import { parseArgs } from './generic-transformers';
+import { STREAM_DELETION_POLICY } from './common-stream.types';
+
+describe('XADD NOMKSTREAM', () => {
+  testUtils.isVersionGreaterThanHook([6, 2]);
+
+  describe('transformArguments', () => {
+    it('single field', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', '*', 'field', 'value']
+      );
+    });
+
+    it('multiple fields', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          '1': 'I',
+          '2': 'II'
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', '*', '1', 'I', '2', 'II']
+      );
+    });
+
+    it('with TRIM', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            threshold: 1000
+          }
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', '1000', '*', 'field', 'value']
+      );
+    });
+
+    it('with TRIM.strategy', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            strategy: 'MAXLEN',
+            threshold: 1000
+          }
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '1000', '*', 'field', 'value']
+      );
+    });
+
+    it('with TRIM.strategyModifier', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            strategyModifier: '=',
+            threshold: 1000
+          }
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', '=', '1000', '*', 'field', 'value']
+      );
+    });
+
+    it('with TRIM.limit', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            threshold: 1000,
+            limit: 1
+          }
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', '1000', 'LIMIT', '1', '*', 'field', 'value']
+      );
+    });
+
+    it('with TRIM.policy', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            threshold: 1000,
+            policy: STREAM_DELETION_POLICY.DELREF
+          }
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', '1000', 'DELREF', '*', 'field', 'value']
+      );
+    });
+
+    it('with all TRIM options', () => {
+      assert.deepEqual(
+        parseArgs(XADD_NOMKSTREAM, 'key', '*', {
+          field: 'value'
+        }, {
+          TRIM: {
+            strategy: 'MAXLEN',
+            strategyModifier: '~',
+            threshold: 1000,
+            limit: 100,
+            policy: STREAM_DELETION_POLICY.ACKED
+          }
+        }),
+        ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '~', '1000', 'LIMIT', '100', 'ACKED', '*', 'field', 'value']
+      );
+    });
+  });
+
+  testUtils.testAll(
+    'xAddNoMkStream - null when stream does not exist',
+    async (client) => {
+      assert.equal(
+        await client.xAddNoMkStream('{tag}nonexistent-stream', '*', {
+          field: 'value'
+        }),
+        null
+      );
+    },
+    {
+      client: GLOBAL.SERVERS.OPEN,
+      cluster: GLOBAL.CLUSTERS.OPEN,
+    }
+  );
+
+  testUtils.testAll(
+    'xAddNoMkStream - with all TRIM options',
+    async (client) => {
+      const streamKey = '{tag}stream';
+      
+      // Create stream and add some messages
+      await client.xAdd(streamKey, '*', { field: 'value1' });
+      
+      // Use NOMKSTREAM with all TRIM options
+      const messageId = await client.xAddNoMkStream(streamKey, '*', 
+        { field: 'value2' },
+        {
+          TRIM: {
+            strategyModifier: '~',
+            limit: 1,
+            strategy: 'MAXLEN',
+            threshold: 2,
+            policy: STREAM_DELETION_POLICY.DELREF
+          }
+        }
+      );
+      
+      assert.equal(typeof messageId, 'string');
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+});
diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.ts
new file mode 100644
index 00000000000..8b1861a065b
--- /dev/null
+++ b/packages/client/lib/commands/XADD_NOMKSTREAM.ts
@@ -0,0 +1,21 @@
+import { BlobStringReply, NullReply, Command } from '../RESP/types';
+import { Tail } from './generic-transformers';
+import { parseXAddArguments } from './XADD';
+
+/**
+ * Command for adding entries to an existing stream without creating it if it doesn't exist
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XADD command with NOMKSTREAM option to append a new entry to an existing stream
+   *
+   * @param args - Arguments tuple containing parser, key, id, message, and options
+   * @returns The ID of the added entry, or null if the stream doesn't exist
+   * @see https://redis.io/commands/xadd/
+   */
+  parseCommand(...args: Tail<Parameters<typeof parseXAddArguments>>) {
+    return parseXAddArguments('NOMKSTREAM', ...args);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XAUTOCLAIM.spec.ts b/packages/client/lib/commands/XAUTOCLAIM.spec.ts
index bae914bda05..58b09a63e78 100644
--- a/packages/client/lib/commands/XAUTOCLAIM.spec.ts
+++ b/packages/client/lib/commands/XAUTOCLAIM.spec.ts
@@ -1,98 +1,69 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XAUTOCLAIM';
+import XAUTOCLAIM from './XAUTOCLAIM';
+import { parseArgs } from './generic-transformers';
 
 describe('XAUTOCLAIM', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0'),
-                ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    COUNT: 1
-                }),
-                ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'COUNT', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XAUTOCLAIM, 'key', 'group', 'consumer', 1, '0-0'),
+        ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0']
+      );
     });
 
-    testUtils.testWithClient('client.xAutoClaim without messages', async client => {
-        const [,, reply] = await Promise.all([
-            client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }),
-            client.xGroupCreateConsumer('key', 'group', 'consumer'),
-            client.xAutoClaim('key', 'group', 'consumer', 1, '0-0')
-        ]);
-
-        assert.deepEqual(reply, {
-            nextId: '0-0',
-            messages: []
-        });
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('client.xAutoClaim with messages', async client => {
-        const [,, id,, reply] = await Promise.all([
-            client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }),
-            client.xGroupCreateConsumer('key', 'group', 'consumer'),
-            client.xAdd('key', '*', { foo: 'bar' }),
-            client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }),
-            client.xAutoClaim('key', 'group', 'consumer', 0, '0-0')
-        ]);
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(XAUTOCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+          COUNT: 1
+        }),
+        ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'COUNT', '1']
+      );
+    });
+  });
 
-        assert.deepEqual(reply, {
-            nextId: '0-0',
-            messages: [{
-                id,
-                message: Object.create(null, {
-                    foo: {
-                        value: 'bar',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }]
-        });
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xAutoClaim', async client => {
+    const message = Object.create(null, {
+      field: {
+        value: 'value',
+        enumerable: true
+      }
+    });
 
-    testUtils.testWithClient('client.xAutoClaim with trimmed messages', async client => {
-        const [,,,,, id,, reply] = await Promise.all([
-            client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }),
-            client.xGroupCreateConsumer('key', 'group', 'consumer'),
-            client.xAdd('key', '*', { foo: 'bar' }),
-            client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }),
-            client.xTrim('key', 'MAXLEN', 0),
-            client.xAdd('key', '*', { bar: 'baz' }),
-            client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }),
-            client.xAutoClaim('key', 'group', 'consumer', 0, '0-0')
-        ]);
+    const [, id1, id2, , , reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xAdd('key', '*', message),
+      client.xAdd('key', '*', message),
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      }),
+      client.xTrim('key', 'MAXLEN', 1),
+      client.xAutoClaim('key', 'group', 'consumer', 0, '0-0')
+    ]);
 
-        assert.deepEqual(reply, {
-            nextId: '0-0',
-            messages: testUtils.isVersionGreaterThan([7, 0]) ? [{
-                id,
-                message: Object.create(null, {
-                    bar: {
-                        value: 'baz',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }] : [null, {
-                id,
-                message: Object.create(null, {
-                    bar: {
-                        value: 'baz',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }]
-        });
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {
+      nextId: '0-0',
+      ...(testUtils.isVersionGreaterThan([7, 0]) ? {
+        messages: [{
+          id: id2,
+          message
+        }],
+        deletedMessages: [id1]
+      } : {
+        messages: [null, {
+          id: id2,
+          message
+        }],
+        deletedMessages: undefined
+      })
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XAUTOCLAIM.ts b/packages/client/lib/commands/XAUTOCLAIM.ts
index 831563981a6..bd6f7b05346 100644
--- a/packages/client/lib/commands/XAUTOCLAIM.ts
+++ b/packages/client/lib/commands/XAUTOCLAIM.ts
@@ -1,39 +1,74 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { StreamMessagesNullReply, transformStreamMessagesNullReply } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, TuplesReply, BlobStringReply, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
+import { StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers';
 
+/**
+ * Options for the XAUTOCLAIM command
+ * 
+ * @property COUNT - Limit the number of messages to claim
+ */
 export interface XAutoClaimOptions {
-    COUNT?: number;
+  COUNT?: number;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    consumer: RedisCommandArgument,
+/**
+ * Raw reply structure for XAUTOCLAIM command
+ * 
+ * @property nextId - The ID to use for the next XAUTOCLAIM call
+ * @property messages - Array of claimed messages or null entries
+ * @property deletedMessages - Array of message IDs that no longer exist
+ */
+export type XAutoClaimRawReply = TuplesReply<[
+  nextId: BlobStringReply,
+  messages: ArrayReply<StreamMessageRawReply | NullReply>,
+  deletedMessages: ArrayReply<BlobStringReply>
+]>;
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XAUTOCLAIM command to automatically claim pending messages in a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - The consumer group name
+   * @param consumer - The consumer name that will claim the messages
+   * @param minIdleTime - Minimum idle time in milliseconds for a message to be claimed
+   * @param start - Message ID to start scanning from
+   * @param options - Additional options for the claim operation
+   * @returns Object containing nextId, claimed messages, and list of deleted message IDs
+   * @see https://redis.io/commands/xautoclaim/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    consumer: RedisArgument,
     minIdleTime: number,
-    start: string,
+    start: RedisArgument,
     options?: XAutoClaimOptions
-): RedisCommandArguments {
-    const args = ['XAUTOCLAIM', key, group, consumer, minIdleTime.toString(), start];
+  ) {
+    parser.push('XAUTOCLAIM');
+    parser.pushKey(key);
+    parser.push(group, consumer, minIdleTime.toString(), start);
 
     if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
+      parser.push('COUNT', options.COUNT.toString());
     }
-
-    return args;
-}
-
-type XAutoClaimRawReply = [RedisCommandArgument, Array<any>];
-
-interface XAutoClaimReply {
-    nextId: RedisCommandArgument;
-    messages: StreamMessagesNullReply;
-}
-
-export function transformReply(reply: XAutoClaimRawReply): XAutoClaimReply {
+  },
+  /**
+   * Transforms the raw XAUTOCLAIM reply into a structured object
+   * 
+   * @param reply - Raw reply from Redis
+   * @param preserve - Preserve options (unused)
+   * @param typeMapping - Type mapping for message fields
+   * @returns Structured object containing nextId, messages, and deletedMessages
+   */
+  transformReply(reply: UnwrapReply<XAutoClaimRawReply>, preserve?: any, typeMapping?: TypeMapping) {
     return {
-        nextId: reply[0],
-        messages: transformStreamMessagesNullReply(reply[1])
+      nextId: reply[0],
+      messages: (reply[1] as unknown as UnwrapReply<typeof reply[1]>).map(transformStreamMessageNullReply.bind(undefined, typeMapping)),
+      deletedMessages: reply[2]
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts
index 9aa24cd04a4..78911657086 100644
--- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts
+++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts
@@ -1,31 +1,38 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XAUTOCLAIM_JUSTID';
+import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID';
+import { parseArgs } from './generic-transformers';
 
 describe('XAUTOCLAIM JUSTID', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group', 'consumer', 1, '0-0'),
-            ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XAUTOCLAIM_JUSTID, 'key', 'group', 'consumer', 1, '0-0'),
+      ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID']
+    );
+  });
 
-    testUtils.testWithClient('client.xAutoClaimJustId', async client => {
-        await Promise.all([
-            client.xGroupCreate('key', 'group', '$', {
-                MKSTREAM: true
-            }),
-            client.xGroupCreateConsumer('key', 'group', 'consumer'),
-        ]);
+  testUtils.testWithClient('client.xAutoClaimJustId', async client => {
+    const [, , id, , reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xGroupCreateConsumer('key', 'group', 'consumer'),
+      client.xAdd('key', '*', {
+        field: 'value'
+      }),
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      }),
+      client.xAutoClaimJustId('key', 'group', 'consumer', 0, '0-0')
+    ]);
 
-        assert.deepEqual(
-            await client.xAutoClaimJustId('key', 'group', 'consumer', 1, '0-0'),
-            {
-                nextId: '0-0',
-                messages: []
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {
+      nextId: '0-0',
+      messages: [id],
+      deletedMessages: testUtils.isVersionGreaterThan([7, 0]) ? [] : undefined
+    });
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts
index a30ac1579e7..efa299c6f8f 100644
--- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts
+++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts
@@ -1,25 +1,44 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformXAutoClaimArguments } from './XAUTOCLAIM';
+import { TuplesReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '../RESP/types';
+import XAUTOCLAIM from './XAUTOCLAIM';
 
-export { FIRST_KEY_INDEX } from './XAUTOCLAIM';
+/**
+ * Raw reply structure for XAUTOCLAIM JUSTID command
+ * 
+ * @property nextId - The ID to use for the next XAUTOCLAIM call
+ * @property messages - Array of message IDs that were claimed
+ * @property deletedMessages - Array of message IDs that no longer exist
+ */
+type XAutoClaimJustIdRawReply = TuplesReply<[
+  nextId: BlobStringReply,
+  messages: ArrayReply<BlobStringReply>,
+  deletedMessages: ArrayReply<BlobStringReply>
+]>;
 
-export function transformArguments(...args: Parameters<typeof transformXAutoClaimArguments>): RedisCommandArguments {
-    return [
-        ...transformXAutoClaimArguments(...args),
-        'JUSTID'
-    ];
-}
-
-type XAutoClaimJustIdRawReply = [RedisCommandArgument, Array<RedisCommandArgument>];
-
-interface XAutoClaimJustIdReply {
-    nextId: RedisCommandArgument;
-    messages: Array<RedisCommandArgument>;
-}
-
-export function transformReply(reply: XAutoClaimJustIdRawReply): XAutoClaimJustIdReply {
+export default {
+  IS_READ_ONLY: XAUTOCLAIM.IS_READ_ONLY,
+  /**
+   * Constructs the XAUTOCLAIM command with JUSTID option to get only message IDs
+   *
+   * @param args - Same parameters as XAUTOCLAIM command
+   * @returns Object containing nextId and arrays of claimed and deleted message IDs
+   * @see https://redis.io/commands/xautoclaim/
+   */
+  parseCommand(...args: Parameters<typeof XAUTOCLAIM.parseCommand>) {
+    const parser = args[0];
+    XAUTOCLAIM.parseCommand(...args);
+    parser.push('JUSTID');
+  },
+  /**
+   * Transforms the raw XAUTOCLAIM JUSTID reply into a structured object
+   * 
+   * @param reply - Raw reply from Redis
+   * @returns Structured object containing nextId, message IDs, and deleted message IDs
+   */
+  transformReply(reply: UnwrapReply<XAutoClaimJustIdRawReply>) {
     return {
-        nextId: reply[0],
-        messages: reply[1]
+      nextId: reply[0],
+      messages: reply[1],
+      deletedMessages: reply[2]
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XCLAIM.spec.ts b/packages/client/lib/commands/XCLAIM.spec.ts
index 6626e84c731..90768509225 100644
--- a/packages/client/lib/commands/XCLAIM.spec.ts
+++ b/packages/client/lib/commands/XCLAIM.spec.ts
@@ -1,120 +1,126 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XCLAIM';
+import XCLAIM from './XCLAIM';
+import { parseArgs } from './generic-transformers';
 
 describe('XCLAIM', () => {
-    describe('transformArguments', () => {
-        it('single id (string)', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0'),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0']
-            );
-        });
-
-        it('multiple ids (array)', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, ['0-0', '1-0']),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', '1-0']
-            );
-        });
-
-        it('with IDLE', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    IDLE: 1
-                }),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1']
-            );
-        });
-
-        it('with TIME (number)', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    TIME: 1
-                }),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', '1']
-            );
-        });
-
-        it('with TIME (date)', () => {
-            const d = new Date();
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    TIME: d
-                }),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', d.getTime().toString()]
-            );
-        });
+  describe('transformArguments', () => {
+    it('single id (string)', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0'),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0']
+      );
+    });
 
-        it('with RETRYCOUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    RETRYCOUNT: 1
-                }),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'RETRYCOUNT', '1']
-            );
-        });
+    it('multiple ids (array)', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, ['0-0', '1-0']),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', '1-0']
+      );
+    });
 
-        it('with FORCE', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    FORCE: true
-                }),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'FORCE']
-            );
-        });
+    it('with IDLE', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+          IDLE: 1
+        }),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1']
+      );
+    });
+    
+    describe('with TIME', () => {
+      it('number', () => {
+        assert.deepEqual(
+          parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+            TIME: 1
+          }),
+          ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', '1']
+        );
+      });
+  
+      it('Date', () => {
+        const d = new Date();
+        assert.deepEqual(
+          parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+            TIME: d
+          }),
+          ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', d.getTime().toString()]
+        );
+      });
+    });
 
-        it('with IDLE, TIME, RETRYCOUNT, FORCE, JUSTID', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', 'consumer', 1, '0-0', {
-                    IDLE: 1,
-                    TIME: 1,
-                    RETRYCOUNT: 1,
-                    FORCE: true
-                }),
-                ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1', 'TIME', '1', 'RETRYCOUNT', '1', 'FORCE']
-            );
-        });
+    it('with RETRYCOUNT', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+          RETRYCOUNT: 1
+        }),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'RETRYCOUNT', '1']
+      );
     });
 
-    testUtils.testWithClient('client.xClaim', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+    it('with FORCE', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+          FORCE: true
+        }),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'FORCE']
+      );
+    });
 
-        assert.deepEqual(
-            await client.xClaim('key', 'group', 'consumer', 0, '0-0'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('with LASTID', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+          LASTID: '0-0'
+        }),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'LASTID', '0-0']
+      );
+    });
 
-    testUtils.testWithClient('client.xClaim with a message', async client => {
-        await client.xGroupCreate('key', 'group', '$', { MKSTREAM: true });
-        const id = await client.xAdd('key', '*', { foo: 'bar' });
-        await client.xReadGroup('group', 'consumer', { key: 'key', id: '>' });
+    it('with IDLE, TIME, RETRYCOUNT, FORCE, LASTID', () => {
+      assert.deepEqual(
+        parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', {
+          IDLE: 1,
+          TIME: 1,
+          RETRYCOUNT: 1,
+          FORCE: true,
+          LASTID: '0-0'
+        }),
+        ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1', 'TIME', '1', 'RETRYCOUNT', '1', 'FORCE', 'LASTID', '0-0']
+      );
+    });
+  });
 
-        assert.deepEqual(
-            await client.xClaim('key', 'group', 'consumer', 0, id),
-            [{
-                id,
-                message: Object.create(null, { 'foo': {
-                    value: 'bar',
-                    configurable: true,
-                    enumerable: true
-                } })
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xClaim', async client => {
+    const message = Object.create(null, {
+      field: {
+        value: 'value',
+        enumerable: true
+      }
+    });
 
-    testUtils.testWithClient('client.xClaim with a trimmed message', async client => {
-        await client.xGroupCreate('key', 'group', '$', { MKSTREAM: true });
-        const id = await client.xAdd('key', '*', { foo: 'bar' });
-        await client.xReadGroup('group', 'consumer', { key: 'key', id: '>' });
-        await client.xTrim('key', 'MAXLEN', 0);
+    const [, , , , , reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xAdd('key', '1-0', message),
+      client.xAdd('key', '2-0', message),
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      }),
+      client.xTrim('key', 'MAXLEN', 1),
+      client.xClaim('key', 'group', 'consumer', 0, ['1-0', '2-0'])
+    ]);
 
-        assert.deepEqual(
-            await client.xClaim('key', 'group', 'consumer', 0, id),
-            testUtils.isVersionGreaterThan([7, 0]) ? []: [null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [
+      ...(testUtils.isVersionGreaterThan([7, 0]) ? [] : [null]),
+      {
+        id: '2-0',
+        message
+      }
+    ]);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XCLAIM.ts b/packages/client/lib/commands/XCLAIM.ts
index e7b458e2376..2bc771288ac 100644
--- a/packages/client/lib/commands/XCLAIM.ts
+++ b/packages/client/lib/commands/XCLAIM.ts
@@ -1,48 +1,89 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
+import { RedisVariadicArgument, StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers';
 
+/**
+ * Options for the XCLAIM command
+ * 
+ * @property IDLE - Set the idle time (in milliseconds) for the claimed messages
+ * @property TIME - Set the last delivery time (Unix timestamp or Date)
+ * @property RETRYCOUNT - Set the retry counter for the claimed messages
+ * @property FORCE - Create the pending message entry even if the message doesn't exist
+ * @property LASTID - Update the consumer group last ID
+ */
 export interface XClaimOptions {
-    IDLE?: number;
-    TIME?: number | Date;
-    RETRYCOUNT?: number;
-    FORCE?: true;
+  IDLE?: number;
+  TIME?: number | Date;
+  RETRYCOUNT?: number;
+  FORCE?: boolean;
+  LASTID?: RedisArgument;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    consumer: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XCLAIM command to claim pending messages in a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - The consumer group name
+   * @param consumer - The consumer name that will claim the messages
+   * @param minIdleTime - Minimum idle time in milliseconds for a message to be claimed
+   * @param id - One or more message IDs to claim
+   * @param options - Additional options for the claim operation
+   * @returns Array of claimed messages
+   * @see https://redis.io/commands/xclaim/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    consumer: RedisArgument,
     minIdleTime: number,
-    id: RedisCommandArgument | Array<RedisCommandArgument>,
+    id: RedisVariadicArgument,
     options?: XClaimOptions
-): RedisCommandArguments {
-    const args =  pushVerdictArguments(
-        ['XCLAIM', key, group, consumer, minIdleTime.toString()],
-        id
-    );
+  ) {
+    parser.push('XCLAIM');
+    parser.pushKey(key);
+    parser.push(group, consumer, minIdleTime.toString());
+    parser.pushVariadic(id);
 
-    if (options?.IDLE) {
-        args.push('IDLE', options.IDLE.toString());
+    if (options?.IDLE !== undefined) {
+      parser.push('IDLE', options.IDLE.toString());
     }
 
-    if (options?.TIME) {
-        args.push(
-            'TIME',
-            (typeof options.TIME === 'number' ? options.TIME : options.TIME.getTime()).toString()
-        );
+    if (options?.TIME !== undefined) {
+      parser.push(
+        'TIME',
+        (options.TIME instanceof Date ? options.TIME.getTime() : options.TIME).toString()
+      );
     }
 
-    if (options?.RETRYCOUNT) {
-        args.push('RETRYCOUNT', options.RETRYCOUNT.toString());
+    if (options?.RETRYCOUNT !== undefined) {
+      parser.push('RETRYCOUNT', options.RETRYCOUNT.toString());
     }
 
     if (options?.FORCE) {
-        args.push('FORCE');
+      parser.push('FORCE');
     }
 
-    return args;
-}
-
-export { transformStreamMessagesNullReply as transformReply } from './generic-transformers';
+    if (options?.LASTID !== undefined) {
+      parser.push('LASTID', options.LASTID);
+    }
+  },
+  /**
+   * Transforms the raw XCLAIM reply into an array of messages
+   * 
+   * @param reply - Raw reply from Redis
+   * @param preserve - Preserve options (unused)
+   * @param typeMapping - Type mapping for message fields
+   * @returns Array of claimed messages with their fields
+   */
+  transformReply(
+    reply: UnwrapReply<ArrayReply<StreamMessageRawReply | NullReply>>, 
+    preserve?: any,
+    typeMapping?: TypeMapping
+  ) {
+    return reply.map(transformStreamMessageNullReply.bind(undefined, typeMapping));
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts
index 619f876d53d..d7bf9fdc70c 100644
--- a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts
+++ b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts
@@ -1,23 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XCLAIM_JUSTID';
+import XCLAIM_JUSTID from './XCLAIM_JUSTID';
+import { parseArgs } from './generic-transformers';
 
 describe('XCLAIM JUSTID', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group', 'consumer', 1, '0-0'),
-            ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XCLAIM_JUSTID, 'key', 'group', 'consumer', 1, '0-0'),
+      ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID']
+    );
+  });
 
-    testUtils.testWithClient('client.xClaimJustId', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  // TODO: test with messages
+  testUtils.testWithClient('client.xClaimJustId', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xClaimJustId('key', 'group', 'consumer', 1, '0-0')
+    ]);
 
-        assert.deepEqual(
-            await client.xClaimJustId('key', 'group', 'consumer', 1, '0-0'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, []);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.ts b/packages/client/lib/commands/XCLAIM_JUSTID.ts
index 50d0d5a0366..56e1d576158 100644
--- a/packages/client/lib/commands/XCLAIM_JUSTID.ts
+++ b/packages/client/lib/commands/XCLAIM_JUSTID.ts
@@ -1,13 +1,27 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformXClaimArguments } from './XCLAIM';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import XCLAIM from './XCLAIM';
 
-export { FIRST_KEY_INDEX } from './XCLAIM';
-
-export function transformArguments(...args: Parameters<typeof transformXClaimArguments>): RedisCommandArguments {
-    return [
-        ...transformXClaimArguments(...args),
-        'JUSTID'
-    ];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+/**
+ * Command variant for XCLAIM that returns only message IDs
+ */
+export default {
+  IS_READ_ONLY: XCLAIM.IS_READ_ONLY,
+  /**
+   * Constructs the XCLAIM command with JUSTID option to get only message IDs
+   *
+   * @param args - Same parameters as XCLAIM command
+   * @returns Array of successfully claimed message IDs
+   * @see https://redis.io/commands/xclaim/
+   */
+  parseCommand(...args: Parameters<typeof XCLAIM.parseCommand>) {
+    const parser = args[0];
+    XCLAIM.parseCommand(...args);
+    parser.push('JUSTID');
+  },
+  /**
+   * Transforms the XCLAIM JUSTID reply into an array of message IDs
+   * 
+   * @returns Array of claimed message IDs
+   */
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XDEL.spec.ts b/packages/client/lib/commands/XDEL.spec.ts
index 00f9e2f9c67..510168bb765 100644
--- a/packages/client/lib/commands/XDEL.spec.ts
+++ b/packages/client/lib/commands/XDEL.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XDEL';
+import XDEL from './XDEL';
+import { parseArgs } from './generic-transformers';
 
 describe('XDEL', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', '0-0'),
-                ['XDEL', 'key', '0-0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(XDEL, 'key', '0-0'),
+        ['XDEL', 'key', '0-0']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['0-0', '1-0']),
-                ['XDEL', 'key', '0-0', '1-0']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(XDEL, 'key', ['0-0', '1-0']),
+        ['XDEL', 'key', '0-0', '1-0']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.xDel', async client => {
-        assert.equal(
-            await client.xDel('key', '0-0'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xDel', async client => {
+    assert.equal(
+      await client.xDel('key', '0-0'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XDEL.ts b/packages/client/lib/commands/XDEL.ts
index 82b30d21092..db8df7d4fd2 100644
--- a/packages/client/lib/commands/XDEL.ts
+++ b/packages/client/lib/commands/XDEL.ts
@@ -1,13 +1,25 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    id: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['XDEL', key], id);
-}
-
-export declare function transformReply(): number;
+/**
+ * Command for removing messages from a stream
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XDEL command to remove one or more messages from a stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param id - One or more message IDs to delete
+   * @returns The number of messages actually deleted
+   * @see https://redis.io/commands/xdel/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, id: RedisVariadicArgument) {
+    parser.push('XDEL');
+    parser.pushKey(key);
+    parser.pushVariadic(id);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XDELEX.spec.ts b/packages/client/lib/commands/XDELEX.spec.ts
new file mode 100644
index 00000000000..8c421503256
--- /dev/null
+++ b/packages/client/lib/commands/XDELEX.spec.ts
@@ -0,0 +1,156 @@
+import { strict as assert } from "node:assert";
+import XDELEX from "./XDELEX";
+import { parseArgs } from "./generic-transformers";
+import testUtils, { GLOBAL } from "../test-utils";
+import {
+  STREAM_DELETION_POLICY,
+  STREAM_DELETION_REPLY_CODES,
+} from "./common-stream.types";
+
+describe("XDELEX", () => {
+  describe("transformArguments", () => {
+    it("string - without policy", () => {
+      assert.deepEqual(parseArgs(XDELEX, "key", "0-0"), [
+        "XDELEX",
+        "key",
+        "IDS",
+        "1",
+        "0-0",
+      ]);
+    });
+
+    it("string - with policy", () => {
+      assert.deepEqual(
+        parseArgs(XDELEX, "key", "0-0", STREAM_DELETION_POLICY.KEEPREF),
+        ["XDELEX", "key", "KEEPREF", "IDS", "1", "0-0"]
+      );
+    });
+
+    it("array - without policy", () => {
+      assert.deepEqual(parseArgs(XDELEX, "key", ["0-0", "1-0"]), [
+        "XDELEX",
+        "key",
+        "IDS",
+        "2",
+        "0-0",
+        "1-0",
+      ]);
+    });
+
+    it("array - with policy", () => {
+      assert.deepEqual(
+        parseArgs(XDELEX, "key", ["0-0", "1-0"], STREAM_DELETION_POLICY.DELREF),
+        ["XDELEX", "key", "DELREF", "IDS", "2", "0-0", "1-0"]
+      );
+    });
+  });
+
+  testUtils.testAll(
+    `XDELEX non-existing key - without policy`,
+    async (client) => {
+      const reply = await client.xDelEx("{tag}stream-key", "0-0");
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.NOT_FOUND]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XDELEX existing key - without policy`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const messageId = await client.xAdd(streamKey, "*", {
+        field: "value",
+      });
+
+      const reply = await client.xDelEx(
+        streamKey,
+        messageId,
+      );
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XDELEX existing key - with policy`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const messageId = await client.xAdd(streamKey, "*", {
+        field: "value",
+      });
+
+      const reply = await client.xDelEx(
+        streamKey,
+        messageId,
+        STREAM_DELETION_POLICY.DELREF
+      );
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XDELEX acknowledge policy - with consumer group`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+
+      // Add a message to the stream
+      const messageId = await client.xAdd(streamKey, "*", {
+        field: "value",
+      });
+
+      // Create consumer group
+      await client.xGroupCreate(streamKey, "testgroup", "0");
+
+      const reply = await client.xDelEx(
+        streamKey,
+        messageId,
+        STREAM_DELETION_POLICY.ACKED
+      );
+      assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DANGLING_REFS]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    `XDELEX multiple keys`,
+    async (client) => {
+      const streamKey = "{tag}stream-key";
+      const messageIds = await Promise.all([
+        client.xAdd(streamKey, "*", {
+          field: "value1",
+        }),
+        client.xAdd(streamKey, "*", {
+          field: "value2",
+        }),
+      ]);
+
+      const reply = await client.xDelEx(
+        streamKey,
+        [...messageIds, "0-0"],
+        STREAM_DELETION_POLICY.DELREF
+      );
+      assert.deepEqual(reply, [
+        STREAM_DELETION_REPLY_CODES.DELETED,
+        STREAM_DELETION_REPLY_CODES.DELETED,
+        STREAM_DELETION_REPLY_CODES.NOT_FOUND,
+      ]);
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+});
diff --git a/packages/client/lib/commands/XDELEX.ts b/packages/client/lib/commands/XDELEX.ts
new file mode 100644
index 00000000000..021dd0a9e13
--- /dev/null
+++ b/packages/client/lib/commands/XDELEX.ts
@@ -0,0 +1,42 @@
+import { CommandParser } from "../client/parser";
+import { RedisArgument, ArrayReply, Command } from "../RESP/types";
+import {
+  StreamDeletionPolicy,
+  StreamDeletionReplyCode,
+} from "./common-stream.types";
+import { RedisVariadicArgument } from "./generic-transformers";
+
+/**
+ * Deletes one or multiple entries from the stream
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XDELEX command to delete one or multiple entries from the stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param id - One or more message IDs to delete
+   * @param policy - Policy to apply when deleting entries (optional, defaults to KEEPREF)
+   * @returns Array of integers: -1 (not found), 1 (deleted), 2 (dangling refs)
+   * @see https://redis.io/commands/xdelex/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    id: RedisVariadicArgument,
+    policy?: StreamDeletionPolicy
+  ) {
+    parser.push("XDELEX");
+    parser.pushKey(key);
+
+    if (policy) {
+      parser.push(policy);
+    }
+
+    parser.push("IDS");
+    parser.pushVariadicWithLength(id);
+  },
+  transformReply:
+    undefined as unknown as () => ArrayReply<StreamDeletionReplyCode>,
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XGROUP_CREATE.spec.ts b/packages/client/lib/commands/XGROUP_CREATE.spec.ts
index 57516e44cc8..7c9d6298c6b 100644
--- a/packages/client/lib/commands/XGROUP_CREATE.spec.ts
+++ b/packages/client/lib/commands/XGROUP_CREATE.spec.ts
@@ -1,32 +1,45 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XGROUP_CREATE';
+import XGROUP_CREATE from './XGROUP_CREATE';
+import { parseArgs } from './generic-transformers';
 
 describe('XGROUP CREATE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '$'),
-                ['XGROUP', 'CREATE', 'key', 'group', '$']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XGROUP_CREATE, 'key', 'group', '$'),
+        ['XGROUP', 'CREATE', 'key', 'group', '$']
+      );
+    });
+
+    it('with MKSTREAM', () => {
+      assert.deepEqual(
+        parseArgs(XGROUP_CREATE, 'key', 'group', '$', {
+          MKSTREAM: true
+        }),
+        ['XGROUP', 'CREATE', 'key', 'group', '$', 'MKSTREAM']
+      );
+    });
 
-        it('with MKSTREAM', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '$', {
-                    MKSTREAM: true
-                }),
-                ['XGROUP', 'CREATE', 'key', 'group', '$', 'MKSTREAM']
-            );
-        });
+    it('with ENTRIESREAD', () => {
+      assert.deepEqual(
+        parseArgs(XGROUP_CREATE, 'key', 'group', '$', {
+          ENTRIESREAD: 1
+        }),
+        ['XGROUP', 'CREATE', 'key', 'group', '$', 'ENTRIESREAD', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.xGroupCreate', async client => {
-        assert.equal(
-            await client.xGroupCreate('key', 'group', '$', {
-                MKSTREAM: true
-            }),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xGroupCreate', async client => {
+    assert.equal(
+      await client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XGROUP_CREATE.ts b/packages/client/lib/commands/XGROUP_CREATE.ts
index 8cfd4e262e4..db6df04fa0f 100644
--- a/packages/client/lib/commands/XGROUP_CREATE.ts
+++ b/packages/client/lib/commands/XGROUP_CREATE.ts
@@ -1,24 +1,52 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-interface XGroupCreateOptions {
-    MKSTREAM?: true;
+/**
+ * Options for creating a consumer group
+ * 
+ * @property MKSTREAM - Create the stream if it doesn't exist
+ * @property ENTRIESREAD - Set the number of entries that were read in this consumer group (Redis 7.0+)
+ */
+export interface XGroupCreateOptions {
+  MKSTREAM?: boolean;
+  /**
+   * added in 7.0
+   */
+  ENTRIESREAD?: number;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    id: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XGROUP CREATE command to create a consumer group for a stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @param id - ID of the last delivered item in the stream ('$' for last item, '0' for all items)
+   * @param options - Additional options for group creation
+   * @returns 'OK' if successful
+   * @see https://redis.io/commands/xgroup-create/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    id: RedisArgument,
     options?: XGroupCreateOptions
-): RedisCommandArguments {
-    const args = ['XGROUP', 'CREATE', key, group, id];
+  ) {
+    parser.push('XGROUP', 'CREATE');
+    parser.pushKey(key);
+    parser.push(group, id);
 
     if (options?.MKSTREAM) {
-        args.push('MKSTREAM');
+      parser.push('MKSTREAM');
     }
 
-    return args;
-}
+    if (options?.ENTRIESREAD) {
+      parser.push('ENTRIESREAD', options.ENTRIESREAD.toString());
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-export declare function transformReply(): RedisCommandArgument;
diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts
index 62443345188..eb749073d35 100644
--- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts
+++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts
@@ -1,25 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XGROUP_CREATECONSUMER';
+import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER';
+import { parseArgs } from './generic-transformers';
 
 describe('XGROUP CREATECONSUMER', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group', 'consumer'),
-            ['XGROUP', 'CREATECONSUMER', 'key', 'group', 'consumer']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XGROUP_CREATECONSUMER, 'key', 'group', 'consumer'),
+      ['XGROUP', 'CREATECONSUMER', 'key', 'group', 'consumer']
+    );
+  });
 
-    testUtils.testWithClient('client.xGroupCreateConsumer', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  testUtils.testAll('xGroupCreateConsumer', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xGroupCreateConsumer('key', 'group', 'consumer')
+    ]);
 
-        assert.equal(
-            await client.xGroupCreateConsumer('key', 'group', 'consumer'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 1);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts
index 2b816a6b480..0b730c7f96b 100644
--- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts
+++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts
@@ -1,13 +1,30 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command, NumberReply } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    consumer: RedisCommandArgument
-): RedisCommandArguments {
-    return ['XGROUP', 'CREATECONSUMER', key, group, consumer];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+/**
+ * Command for creating a new consumer in a consumer group
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XGROUP CREATECONSUMER command to create a new consumer in a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @param consumer - Name of the consumer to create
+   * @returns 1 if the consumer was created, 0 if it already existed
+   * @see https://redis.io/commands/xgroup-createconsumer/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    consumer: RedisArgument
+  ) {
+    parser.push('XGROUP', 'CREATECONSUMER');
+    parser.pushKey(key);
+    parser.push(group, consumer);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts
index d071aedf64f..fabef789d78 100644
--- a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts
+++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts
@@ -1,23 +1,27 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XGROUP_DELCONSUMER';
+import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER';
+import { parseArgs } from './generic-transformers';
 
 describe('XGROUP DELCONSUMER', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group', 'consumer'),
-            ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XGROUP_DELCONSUMER, 'key', 'group', 'consumer'),
+      ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer']
+    );
+  });
 
-    testUtils.testWithClient('client.xGroupDelConsumer', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  testUtils.testAll('xGroupDelConsumer', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xGroupDelConsumer('key', 'group', 'consumer')
+    ]);
 
-        assert.equal(
-            await client.xGroupDelConsumer('key', 'group', 'consumer'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 0);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts
index 4e4fc096d07..5feffe74042 100644
--- a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts
+++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts
@@ -1,13 +1,30 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    consumer: RedisCommandArgument
-): RedisCommandArguments {
-    return ['XGROUP', 'DELCONSUMER', key, group, consumer];
-}
-
-export declare function transformReply(): number;
+/**
+ * Command for removing a consumer from a consumer group
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XGROUP DELCONSUMER command to remove a consumer from a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @param consumer - Name of the consumer to remove 
+   * @returns The number of pending messages owned by the deleted consumer
+   * @see https://redis.io/commands/xgroup-delconsumer/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    consumer: RedisArgument
+  ) {
+    parser.push('XGROUP', 'DELCONSUMER');
+    parser.pushKey(key);
+    parser.push(group, consumer);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts
index ea8e7b7be98..8277c66d3f6 100644
--- a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts
+++ b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts
@@ -1,23 +1,27 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XGROUP_DESTROY';
+import XGROUP_DESTROY from './XGROUP_DESTROY';
+import { parseArgs } from './generic-transformers';
 
 describe('XGROUP DESTROY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group'),
-            ['XGROUP', 'DESTROY', 'key', 'group']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XGROUP_DESTROY, 'key', 'group'),
+      ['XGROUP', 'DESTROY', 'key', 'group']
+    );
+  });
 
-    testUtils.testWithClient('client.xGroupDestroy', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  testUtils.testAll('xGroupDestroy', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xGroupDestroy('key', 'group')
+    ]);
 
-        assert.equal(
-            await client.xGroupDestroy('key', 'group'),
-            true
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 1);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XGROUP_DESTROY.ts b/packages/client/lib/commands/XGROUP_DESTROY.ts
index 85910c02471..ed454abbb2b 100644
--- a/packages/client/lib/commands/XGROUP_DESTROY.ts
+++ b/packages/client/lib/commands/XGROUP_DESTROY.ts
@@ -1,12 +1,24 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument
-): RedisCommandArguments {
-    return ['XGROUP', 'DESTROY', key, group];
-}
-
-export { transformBooleanReply as transformReply } from './generic-transformers';
+/**
+ * Command for removing a consumer group
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XGROUP DESTROY command to remove a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group to destroy
+   * @returns 1 if the group was destroyed, 0 if it did not exist
+   * @see https://redis.io/commands/xgroup-destroy/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) {
+    parser.push('XGROUP', 'DESTROY');
+    parser.pushKey(key);
+    parser.push(group);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XGROUP_SETID.spec.ts b/packages/client/lib/commands/XGROUP_SETID.spec.ts
index 8df51f5401d..6ea0dd79c37 100644
--- a/packages/client/lib/commands/XGROUP_SETID.spec.ts
+++ b/packages/client/lib/commands/XGROUP_SETID.spec.ts
@@ -1,23 +1,27 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XGROUP_SETID';
+import XGROUP_SETID from './XGROUP_SETID';
+import { parseArgs } from './generic-transformers';
 
 describe('XGROUP SETID', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group', '0'),
-            ['XGROUP', 'SETID', 'key', 'group', '0']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XGROUP_SETID, 'key', 'group', '0'),
+      ['XGROUP', 'SETID', 'key', 'group', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.xGroupSetId', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  testUtils.testAll('xGroupSetId', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xGroupSetId('key', 'group', '0')
+    ]);
 
-        assert.equal(
-            await client.xGroupSetId('key', 'group', '0'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XGROUP_SETID.ts b/packages/client/lib/commands/XGROUP_SETID.ts
index e732fc8d7bf..4f3076b6032 100644
--- a/packages/client/lib/commands/XGROUP_SETID.ts
+++ b/packages/client/lib/commands/XGROUP_SETID.ts
@@ -1,13 +1,43 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    id: RedisCommandArgument
-): RedisCommandArguments {
-    return ['XGROUP', 'SETID', key, group, id];
+/**
+ * Options for setting a consumer group's ID position
+ * 
+ * @property ENTRIESREAD - Set the number of entries that were read in this consumer group (Redis 7.0+)
+ */
+export interface XGroupSetIdOptions {
+  /** added in 7.0 */
+  ENTRIESREAD?: number;
 }
 
-export declare function transformReply(): RedisCommandArgument;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XGROUP SETID command to set the last delivered ID for a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @param id - ID to set as last delivered message ('$' for last item, '0' for all items)
+   * @param options - Additional options for setting the group ID
+   * @returns 'OK' if successful
+   * @see https://redis.io/commands/xgroup-setid/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    id: RedisArgument,
+    options?: XGroupSetIdOptions
+  ) {
+    parser.push('XGROUP', 'SETID');
+    parser.pushKey(key);
+    parser.push(group, id);
+
+    if (options?.ENTRIESREAD) {
+      parser.push('ENTRIESREAD', options.ENTRIESREAD.toString());
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts
index a2c58999773..b1f245dbf18 100644
--- a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts
+++ b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts
@@ -1,43 +1,39 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './XINFO_CONSUMERS';
+import XINFO_CONSUMERS from './XINFO_CONSUMERS';
+import { parseArgs } from './generic-transformers';
 
 describe('XINFO CONSUMERS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'group'),
-            ['XINFO', 'CONSUMERS', 'key', 'group']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XINFO_CONSUMERS, 'key', 'group'),
+      ['XINFO', 'CONSUMERS', 'key', 'group']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([
-                ['name', 'Alice', 'pending', 1, 'idle', 9104628, 'inactive', 9281221],
-                ['name', 'Bob', 'pending', 1, 'idle', 83841983, 'inactive', 7213871]
-            ]),
-            [{
-                name: 'Alice',
-                pending: 1,
-                idle: 9104628,
-                inactive: 9281221,
-            }, {
-                name: 'Bob',
-                pending: 1,
-                idle: 83841983,
-                inactive: 7213871,
-            }]
-        );
-    });
+  testUtils.testAll('xInfoConsumers', async client => {
+    const [, , reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      // using `XREADGROUP` and not `XGROUP CREATECONSUMER` because the latter was introduced in Redis 6.2
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '0-0'
+      }),
+      client.xInfoConsumers('key', 'group')
+    ]);
 
-    testUtils.testWithClient('client.xInfoConsumers', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
-
-        assert.deepEqual(
-            await client.xInfoConsumers('key', 'group'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    for (const consumer of reply) {
+      assert.equal(typeof consumer.name, 'string');
+      assert.equal(typeof consumer.pending, 'number');
+      assert.equal(typeof consumer.idle, 'number');
+      if (testUtils.isVersionGreaterThan([7, 2])) {
+        assert.equal(typeof consumer.inactive, 'number');
+      }
+    }
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.ts b/packages/client/lib/commands/XINFO_CONSUMERS.ts
index 9b3893cc93c..49267f13980 100644
--- a/packages/client/lib/commands/XINFO_CONSUMERS.ts
+++ b/packages/client/lib/commands/XINFO_CONSUMERS.ts
@@ -1,28 +1,56 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
+/**
+ * Reply structure for XINFO CONSUMERS command
+ * 
+ * @property name - Name of the consumer
+ * @property pending - Number of pending messages for this consumer
+ * @property idle - Idle time in milliseconds
+ * @property inactive - Time in milliseconds since last interaction (Redis 7.2+)
+ */
+export type XInfoConsumersReply = ArrayReply<TuplesToMapReply<[
+  [BlobStringReply<'name'>, BlobStringReply],
+  [BlobStringReply<'pending'>, NumberReply],
+  [BlobStringReply<'idle'>, NumberReply],
+  /** added in 7.2 */
+  [BlobStringReply<'inactive'>, NumberReply]
+]>>;
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument
-): RedisCommandArguments {
-    return ['XINFO', 'CONSUMERS', key, group];
-}
-
-type XInfoConsumersReply = Array<{
-    name: RedisCommandArgument;
-    pending: number;
-    idle: number;
-    inactive: number;
-}>;
-
-export function transformReply(rawReply: Array<any>): XInfoConsumersReply {
-    return rawReply.map(consumer => ({
-        name: consumer[1],
-        pending: consumer[3],
-        idle: consumer[5],
-        inactive: consumer[7]
-    }));
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XINFO CONSUMERS command to list the consumers in a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @returns Array of consumer information objects
+   * @see https://redis.io/commands/xinfo-consumers/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) {
+    parser.push('XINFO', 'CONSUMERS');
+    parser.pushKey(key);
+    parser.push(group);
+  },
+  transformReply: {
+    /**
+     * Transforms RESP2 reply into a structured consumer information array
+     * 
+     * @param reply - Raw RESP2 reply from Redis
+     * @returns Array of consumer information objects
+     */
+    2: (reply: UnwrapReply<Resp2Reply<XInfoConsumersReply>>) => {
+      return reply.map(consumer => {
+        const unwrapped = consumer as unknown as UnwrapReply<typeof consumer>;
+        return {
+          name: unwrapped[1],
+          pending: unwrapped[3],
+          idle: unwrapped[5],
+          inactive: unwrapped[7]
+        };
+      });
+    },
+    3: undefined as unknown as () => XInfoConsumersReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XINFO_GROUPS.spec.ts b/packages/client/lib/commands/XINFO_GROUPS.spec.ts
index dea8ac58d9c..a1196f4957a 100644
--- a/packages/client/lib/commands/XINFO_GROUPS.spec.ts
+++ b/packages/client/lib/commands/XINFO_GROUPS.spec.ts
@@ -1,48 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './XINFO_GROUPS';
+import XINFO_GROUPS from './XINFO_GROUPS';
+import { parseArgs } from './generic-transformers';
 
 describe('XINFO GROUPS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['XINFO', 'GROUPS', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XINFO_GROUPS, 'key'),
+      ['XINFO', 'GROUPS', 'key']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([
-                ['name', 'mygroup', 'consumers', 2, 'pending', 2, 'last-delivered-id', '1588152489012-0'],
-                ['name', 'some-other-group', 'consumers', 1, 'pending', 0, 'last-delivered-id', '1588152498034-0']
-            ]),
-            [{
-                name: 'mygroup',
-                consumers: 2,
-                pending: 2,
-                lastDeliveredId: '1588152489012-0'
-            }, {
-                name: 'some-other-group',
-                consumers: 1,
-                pending: 0,
-                lastDeliveredId: '1588152498034-0'
-            }]
-        );
-    });
-
-    testUtils.testWithClient('client.xInfoGroups', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
-
-        assert.deepEqual(
-            await client.xInfoGroups('key'),
-            [{
-                name: 'group',
-                consumers: 0,
-                pending: 0,
-                lastDeliveredId: '0-0'
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xInfoGroups', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xInfoGroups('key')
+    ]);
+    
+    assert.deepEqual(
+      reply,
+      [{
+        name: 'group',
+        consumers: 0,
+        pending: 0,
+        'last-delivered-id': '0-0',
+        'entries-read': testUtils.isVersionGreaterThan([7, 0]) ? null : undefined,
+        lag: testUtils.isVersionGreaterThan([7, 0]) ? 0 : undefined
+      }]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XINFO_GROUPS.ts b/packages/client/lib/commands/XINFO_GROUPS.ts
index dcf504c8ce7..1d8142bfaef 100644
--- a/packages/client/lib/commands/XINFO_GROUPS.ts
+++ b/packages/client/lib/commands/XINFO_GROUPS.ts
@@ -1,25 +1,60 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 2;
+/**
+ * Reply structure for XINFO GROUPS command containing information about consumer groups
+ */
+export type XInfoGroupsReply = ArrayReply<TuplesToMapReply<[
+  [BlobStringReply<'name'>, BlobStringReply],
+  [BlobStringReply<'consumers'>, NumberReply],
+  [BlobStringReply<'pending'>, NumberReply],
+  [BlobStringReply<'last-delivered-id'>, NumberReply],
+  /** added in 7.0 */
+  [BlobStringReply<'entries-read'>, NumberReply | NullReply],
+  /** added in 7.0 */
+  [BlobStringReply<'lag'>, NumberReply],
+]>>;
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['XINFO', 'GROUPS', key];
-}
-
-type XInfoGroupsReply = Array<{
-    name: RedisCommandArgument;
-    consumers: number;
-    pending: number;
-    lastDeliveredId: RedisCommandArgument;
-}>;
-
-export function transformReply(rawReply: Array<any>): XInfoGroupsReply {
-    return rawReply.map(group => ({
-        name: group[1],
-        consumers: group[3],
-        pending: group[5],
-        lastDeliveredId: group[7]
-    }));
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XINFO GROUPS command to list the consumer groups of a stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @returns Array of consumer group information objects
+   * @see https://redis.io/commands/xinfo-groups/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('XINFO', 'GROUPS');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    /**
+     * Transforms RESP2 reply into a structured consumer group information array
+     * 
+     * @param reply - Raw RESP2 reply from Redis
+     * @returns Array of consumer group information objects containing:
+     *          name - Name of the consumer group
+     *          consumers - Number of consumers in the group
+     *          pending - Number of pending messages for the group
+     *          last-delivered-id - ID of the last delivered message
+     *          entries-read - Number of entries read in the group (Redis 7.0+)
+     *          lag - Number of entries not read by the group (Redis 7.0+)
+     */
+    2: (reply: UnwrapReply<Resp2Reply<XInfoGroupsReply>>) => {
+      return reply.map(group => {
+        const unwrapped = group as unknown as UnwrapReply<typeof group>;
+        return {
+          name: unwrapped[1],
+          consumers: unwrapped[3],
+          pending: unwrapped[5],
+          'last-delivered-id': unwrapped[7],
+          'entries-read': unwrapped[9],
+          lag: unwrapped[11]
+        };
+      });
+    },
+    3: undefined as unknown as () => XInfoGroupsReply
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XINFO_STREAM.spec.ts b/packages/client/lib/commands/XINFO_STREAM.spec.ts
index ca8d44f2875..7e1829f3059 100644
--- a/packages/client/lib/commands/XINFO_STREAM.spec.ts
+++ b/packages/client/lib/commands/XINFO_STREAM.spec.ts
@@ -1,72 +1,40 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './XINFO_STREAM';
+import XINFO_STREAM from './XINFO_STREAM';
+import { parseArgs } from './generic-transformers';
 
 describe('XINFO STREAM', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['XINFO', 'STREAM', 'key']
-        );
-    });
-
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply([
-                'length', 2,
-                'radix-tree-keys', 1,
-                'radix-tree-nodes', 2,
-                'last-generated-id', '1538385846314-0',
-                'groups', 2,
-                'first-entry', ['1538385820729-0', ['foo', 'bar']],
-                'last-entry', ['1538385846314-0', ['field', 'value']]
-            ]),
-            {
-                length: 2,
-                radixTreeKeys: 1,
-                radixTreeNodes: 2,
-                groups: 2,
-                lastGeneratedId: '1538385846314-0',
-                firstEntry: {
-                    id: '1538385820729-0',
-                    message: Object.create(null, {
-                        foo: {
-                            value: 'bar',
-                            configurable: true,
-                            enumerable: true
-                        }
-                    })
-                },
-                lastEntry: {
-                    id: '1538385846314-0',
-                    message: Object.create(null, {
-                        field: {
-                            value: 'value',
-                            configurable: true,
-                            enumerable: true
-                        }
-                    })
-                }
-            }
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(XINFO_STREAM, 'key'),
+      ['XINFO', 'STREAM', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.xInfoStream', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  testUtils.testAll('xInfoStream', async client => {
+    const [, reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xInfoStream('key')
+    ]);
 
-        assert.deepEqual(
-            await client.xInfoStream('key'),
-            {
-                length: 0,
-                radixTreeKeys: 0,
-                radixTreeNodes: 1,
-                groups: 1,
-                lastGeneratedId: '0-0',
-                firstEntry: null,
-                lastEntry: null
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {
+      length: 0,
+      'radix-tree-keys': 0,
+      'radix-tree-nodes': 1,
+      'last-generated-id': '0-0',
+      ...testUtils.isVersionGreaterThan([7, 0]) && {
+        'max-deleted-entry-id': '0-0',
+        'entries-added': 0,
+        'recorded-first-entry-id': '0-0',
+      },
+      groups: 1,
+      'first-entry': null,
+      'last-entry': null
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XINFO_STREAM.ts b/packages/client/lib/commands/XINFO_STREAM.ts
index e9de25be8cb..546dd70cab7 100644
--- a/packages/client/lib/commands/XINFO_STREAM.ts
+++ b/packages/client/lib/commands/XINFO_STREAM.ts
@@ -1,64 +1,114 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { StreamMessageReply, transformTuplesReply } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, TuplesReply, ArrayReply, UnwrapReply, Command } from '../RESP/types';
+import { isNullReply, transformTuplesReply } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
+/**
+ * Reply structure for XINFO STREAM command containing detailed information about a stream
+ * 
+ * @property length - Number of entries in the stream
+ * @property radix-tree-keys - Number of radix tree keys
+ * @property radix-tree-nodes - Number of radix tree nodes
+ * @property last-generated-id - Last generated message ID
+ * @property max-deleted-entry-id - Highest message ID deleted (Redis 7.2+)
+ * @property entries-added - Total number of entries added (Redis 7.2+)
+ * @property recorded-first-entry-id - ID of the first recorded entry (Redis 7.2+)
+ * @property groups - Number of consumer groups
+ * @property first-entry - First entry in the stream
+ * @property last-entry - Last entry in the stream
+ */
+export type XInfoStreamReply = TuplesToMapReply<[
+  [BlobStringReply<'length'>, NumberReply],
+  [BlobStringReply<'radix-tree-keys'>, NumberReply],
+  [BlobStringReply<'radix-tree-nodes'>, NumberReply],
+  [BlobStringReply<'last-generated-id'>, BlobStringReply],
+  /** added in 7.2 */
+  [BlobStringReply<'max-deleted-entry-id'>, BlobStringReply],
+  /** added in 7.2 */
+  [BlobStringReply<'entries-added'>, NumberReply],
+  /** added in 7.2 */
+  [BlobStringReply<'recorded-first-entry-id'>, BlobStringReply],
+  [BlobStringReply<'groups'>, NumberReply],
+  [BlobStringReply<'first-entry'>, ReturnType<typeof transformEntry>],
+  [BlobStringReply<'last-entry'>, ReturnType<typeof transformEntry>]
+]>;
 
-export const IS_READ_ONLY = true;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XINFO STREAM command to get detailed information about a stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @returns Detailed information about the stream including its length, structure, and entries
+   * @see https://redis.io/commands/xinfo-stream/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('XINFO', 'STREAM');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    // TODO: is there a "type safe" way to do it?
+    2(reply: any) {
+      const parsedReply: Partial<XInfoStreamReply['DEFAULT']> = {};
 
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['XINFO', 'STREAM', key];
-}
-
-interface XInfoStreamReply {
-    length: number;
-    radixTreeKeys: number;
-    radixTreeNodes: number;
-    groups: number;
-    lastGeneratedId: RedisCommandArgument;
-    firstEntry: StreamMessageReply | null;
-    lastEntry: StreamMessageReply | null;
-}
-
-export function transformReply(rawReply: Array<any>): XInfoStreamReply {
-    const parsedReply: Partial<XInfoStreamReply> = {};
-
-    for (let i = 0; i < rawReply.length; i+= 2) {
-        switch (rawReply[i]) {
-            case 'length':
-                parsedReply.length = rawReply[i + 1];
-                break;
+      for (let i = 0; i < reply.length; i += 2) {
+        switch (reply[i]) {
+          case 'first-entry':
+          case 'last-entry':
+            parsedReply[reply[i] as ('first-entry' | 'last-entry')] = transformEntry(reply[i + 1]) as any;
+            break;
 
-            case 'radix-tree-keys':
-                parsedReply.radixTreeKeys = rawReply[i + 1];
-                break;
-
-            case 'radix-tree-nodes':
-                parsedReply.radixTreeNodes = rawReply[i + 1];
-                break;
+          default:
+            parsedReply[reply[i] as keyof typeof parsedReply] = reply[i + 1];
+            break;
+        }
+      }
 
-            case 'groups':
-                parsedReply.groups = rawReply[i + 1];
-                break;
+      return parsedReply as XInfoStreamReply['DEFAULT'];
+    },
+    3(reply: any) {
+      if (reply instanceof Map) {
+        reply.set(
+          'first-entry',
+          transformEntry(reply.get('first-entry'))
+        );
+        reply.set(
+          'last-entry',
+          transformEntry(reply.get('last-entry'))
+        );
+      } else if (reply instanceof Array) {
+        reply[17] = transformEntry(reply[17]);
+        reply[19] = transformEntry(reply[19]);
+      } else {
+        reply['first-entry'] = transformEntry(reply['first-entry']);
+        reply['last-entry'] = transformEntry(reply['last-entry']);
+      }
 
-            case 'last-generated-id':
-                parsedReply.lastGeneratedId = rawReply[i + 1];
-                break;
+      return reply as XInfoStreamReply;
+    }
+  }
+} as const satisfies Command;
 
-            case 'first-entry':
-                parsedReply.firstEntry = rawReply[i + 1] ? {
-                    id: rawReply[i + 1][0],
-                    message: transformTuplesReply(rawReply[i + 1][1])
-                } : null;
-                break;
+/**
+ * Raw entry structure from Redis stream
+ */
+type RawEntry = TuplesReply<[
+  id: BlobStringReply,
+  message: ArrayReply<BlobStringReply>
+]> | NullReply;
 
-            case 'last-entry':
-                parsedReply.lastEntry = rawReply[i + 1] ? {
-                    id: rawReply[i + 1][0],
-                    message: transformTuplesReply(rawReply[i + 1][1])
-                } : null;
-                break;
-        }
-    }
+/**
+ * Transforms a raw stream entry into a structured object
+ * 
+ * @param entry - Raw entry from Redis
+ * @returns Structured object with id and message, or null if entry is null
+ */
+function transformEntry(entry: RawEntry) {
+  if (isNullReply(entry)) return entry;
 
-    return parsedReply as XInfoStreamReply;
+  const [id, message] = entry as unknown as UnwrapReply<typeof entry>;
+  return {
+    id,
+    message: transformTuplesReply(message)
+  };
 }
diff --git a/packages/client/lib/commands/XLEN.spec.ts b/packages/client/lib/commands/XLEN.spec.ts
index 178024ba89e..3e22b9aebfa 100644
--- a/packages/client/lib/commands/XLEN.spec.ts
+++ b/packages/client/lib/commands/XLEN.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XLEN';
+import XLEN from './XLEN';
+import { parseArgs } from './generic-transformers';
 
 describe('XLEN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['XLEN', 'key']
-        );
-    });
+  it('processCommand', () => {
+    assert.deepEqual(
+      parseArgs(XLEN, 'key'),
+      ['XLEN', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.xLen', async client => {
-        assert.equal(
-            await client.xLen('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('xLen', async client => {
+    assert.equal(
+      await client.xLen('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XLEN.ts b/packages/client/lib/commands/XLEN.ts
index fda4192c8a0..f7718371cf2 100644
--- a/packages/client/lib/commands/XLEN.ts
+++ b/packages/client/lib/commands/XLEN.ts
@@ -1,11 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['XLEN', key];
-}
-
-export declare function transformReply(): number;
+/**
+ * Command for getting the length of a stream
+ */
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XLEN command to get the number of entries in a stream
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @returns The number of entries inside the stream
+   * @see https://redis.io/commands/xlen/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('XLEN');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XPENDING.spec.ts b/packages/client/lib/commands/XPENDING.spec.ts
index b1fef2a217f..55cb957fc62 100644
--- a/packages/client/lib/commands/XPENDING.spec.ts
+++ b/packages/client/lib/commands/XPENDING.spec.ts
@@ -1,62 +1,61 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XPENDING';
+import XPENDING from './XPENDING';
+import { parseArgs } from './generic-transformers';
 
 describe('XPENDING', () => {
-    describe('transformArguments', () => {
-        it('transformArguments', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group'),
-                ['XPENDING', 'key', 'group']
-            );
-        });
+  describe('transformArguments', () => {
+    it('transformArguments', () => {
+      assert.deepEqual(
+        parseArgs(XPENDING, 'key', 'group'),
+        ['XPENDING', 'key', 'group']
+      );
     });
+  });
 
-    describe('client.xPending', () => {
-        testUtils.testWithClient('simple', async client => {
-            await client.xGroupCreate('key', 'group', '$', {
-                MKSTREAM: true
-            });
+  describe('client.xPending', () => {
+    testUtils.testWithClient('simple', async client => {
+      const [, reply] = await Promise.all([
+        client.xGroupCreate('key', 'group', '$', {
+          MKSTREAM: true
+        }),
+        client.xPending('key', 'group')
+      ]);
 
-            assert.deepEqual(
-                await client.xPending('key', 'group'),
-                {
-                    pending: 0,
-                    firstId: null,
-                    lastId: null,
-                    consumers: null
-                }
-            );
-        }, GLOBAL.SERVERS.OPEN);
+      assert.deepEqual(reply, {
+        pending: 0,
+        firstId: null,
+        lastId: null,
+        consumers: null
+      });
+    }, GLOBAL.SERVERS.OPEN);
 
-        testUtils.testWithClient('with consumers', async client => {
-            const [,, id] = await Promise.all([
-                client.xGroupCreate('key', 'group', '$', {
-                    MKSTREAM: true
-                }),
-                client.xGroupCreateConsumer('key', 'group', 'consumer'),
-                client.xAdd('key', '*', { field: 'value' }),
-                client.xReadGroup('group', 'consumer', {
-                    key: 'key',
-                    id: '>'
-                })
-            ]);
+    testUtils.testWithClient('with consumers', async client => {
+      const [, , id, , reply] = await Promise.all([
+        client.xGroupCreate('key', 'group', '$', {
+          MKSTREAM: true
+        }),
+        client.xGroupCreateConsumer('key', 'group', 'consumer'),
+        client.xAdd('key', '*', { field: 'value' }),
+        client.xReadGroup('group', 'consumer', {
+          key: 'key',
+          id: '>'
+        }),
+        client.xPending('key', 'group')
+      ]);
 
-            assert.deepEqual(
-                await client.xPending('key', 'group'),
-                {
-                    pending: 1,
-                    firstId: id,
-                    lastId: id,
-                    consumers: [{
-                        name: 'consumer',
-                        deliveriesCounter: 1
-                    }]
-                }
-            );
-        }, {
-            ...GLOBAL.SERVERS.OPEN,
-            minimumDockerVersion: [6, 2]
-        });
+      assert.deepEqual(reply, {
+        pending: 1,
+        firstId: id,
+        lastId: id,
+        consumers: [{
+          name: 'consumer',
+          deliveriesCounter: 1
+        }]
+      });
+    }, {
+      ...GLOBAL.SERVERS.OPEN,
+      minimumDockerVersion: [6, 2]
     });
+  });
 });
diff --git a/packages/client/lib/commands/XPENDING.ts b/packages/client/lib/commands/XPENDING.ts
index ac56e429410..cff9ef2f51b 100644
--- a/packages/client/lib/commands/XPENDING.ts
+++ b/packages/client/lib/commands/XPENDING.ts
@@ -1,44 +1,60 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+/**
+ * Raw reply structure for XPENDING command
+ * 
+ * @property pending - Number of pending messages in the group
+ * @property firstId - ID of the first pending message
+ * @property lastId - ID of the last pending message
+ * @property consumers - Array of consumer info with delivery counts
+ */
+type XPendingRawReply = TuplesReply<[
+  pending: NumberReply,
+  firstId: BlobStringReply | NullReply,
+  lastId: BlobStringReply | NullReply,
+  consumers: ArrayReply<TuplesReply<[
+    name: BlobStringReply,
+    deliveriesCounter: BlobStringReply
+  ]>> | NullReply
+]>;
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument
-): RedisCommandArguments {
-    return ['XPENDING', key, group];
-}
-
-type XPendingRawReply = [
-    pending: number,
-    firstId: RedisCommandArgument | null,
-    lastId: RedisCommandArgument | null,
-    consumers: Array<[
-        name: RedisCommandArgument,
-        deliveriesCounter: RedisCommandArgument
-    ]> | null
-];
-
-interface XPendingReply {
-    pending: number;
-    firstId: RedisCommandArgument | null;
-    lastId: RedisCommandArgument | null;
-    consumers: Array<{
-        name: RedisCommandArgument;
-        deliveriesCounter: number;
-    }> | null;
-}
-
-export function transformReply(reply: XPendingRawReply): XPendingReply {
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XPENDING command to inspect pending messages of a consumer group
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @returns Summary of pending messages including total count, ID range, and per-consumer stats 
+   * @see https://redis.io/commands/xpending/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) {
+    parser.push('XPENDING');
+    parser.pushKey(key);
+    parser.push(group);
+  },
+  /**
+   * Transforms the raw XPENDING reply into a structured object
+   * 
+   * @param reply - Raw reply from Redis
+   * @returns Object containing pending count, ID range, and consumer statistics
+   */
+  transformReply(reply: UnwrapReply<XPendingRawReply>) {
+    const consumers = reply[3] as unknown as UnwrapReply<typeof reply[3]>;
     return {
-        pending: reply[0],
-        firstId: reply[1],
-        lastId: reply[2],
-        consumers: reply[3] === null ? null : reply[3].map(([name, deliveriesCounter]) => ({
-            name,
-            deliveriesCounter: Number(deliveriesCounter)
-        }))
-    };
-}
+      pending: reply[0],
+      firstId: reply[1],
+      lastId: reply[2],
+      consumers: consumers === null ? null : consumers.map(consumer => {
+        const [name, deliveriesCounter] = consumer as unknown as UnwrapReply<typeof consumer>;
+        return {
+          name,
+          deliveriesCounter: Number(deliveriesCounter)
+        };
+      })
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XPENDING_RANGE.spec.ts b/packages/client/lib/commands/XPENDING_RANGE.spec.ts
index 0b57c704bb0..33cd836f2a9 100644
--- a/packages/client/lib/commands/XPENDING_RANGE.spec.ts
+++ b/packages/client/lib/commands/XPENDING_RANGE.spec.ts
@@ -1,53 +1,67 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XPENDING_RANGE';
+import XPENDING_RANGE from './XPENDING_RANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('XPENDING RANGE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '-', '+', 1),
-                ['XPENDING', 'key', 'group', '-', '+', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1),
+        ['XPENDING', 'key', 'group', '-', '+', '1']
+      );
+    });
 
-        it('with IDLE', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '-', '+', 1, {
-                    IDLE: 1,
-                }),
-                ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1']
-            );
-        });
+    it('with IDLE', () => {
+      assert.deepEqual(
+        parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1, {
+          IDLE: 1,
+        }),
+        ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1']
+      );
+    });
 
-        it('with consumer', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '-', '+', 1, {
-                    consumer: 'consumer'
-                }),
-                ['XPENDING', 'key', 'group', '-', '+', '1', 'consumer']
-            );
-        });
+    it('with consumer', () => {
+      assert.deepEqual(
+        parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1, {
+          consumer: 'consumer'
+        }),
+        ['XPENDING', 'key', 'group', '-', '+', '1', 'consumer']
+      );
+    });
 
-        it('with IDLE, consumer', () => {
-            assert.deepEqual(
-                transformArguments('key', 'group', '-', '+', 1, {
-                    IDLE: 1,
-                    consumer: 'consumer'
-                }),
-                ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1', 'consumer']
-            );
-        });
+    it('with IDLE, consumer', () => {
+      assert.deepEqual(
+        parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1, {
+          IDLE: 1,
+          consumer: 'consumer'
+        }),
+        ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1', 'consumer']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.xPendingRange', async client => {
-        await client.xGroupCreate('key', 'group', '$', {
-            MKSTREAM: true
-        });
+  testUtils.testAll('xPendingRange', async client => {
+    const [, id, , reply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xAdd('key', '*', { field: 'value' }),
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      }),
+      client.xPendingRange('key', 'group', '-', '+', 1)
+    ]);
 
-        assert.deepEqual(
-            await client.xPendingRange('key', 'group', '-', '+', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.ok(Array.isArray(reply));
+    assert.equal(reply.length, 1);
+    assert.equal(reply[0].id, id);
+    assert.equal(reply[0].consumer, 'consumer');
+    assert.equal(typeof reply[0].millisecondsSinceLastDelivery, 'number');
+    assert.equal(reply[0].deliveriesCounter, 1);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XPENDING_RANGE.ts b/packages/client/lib/commands/XPENDING_RANGE.ts
index 87660de545d..e136061fe9e 100644
--- a/packages/client/lib/commands/XPENDING_RANGE.ts
+++ b/packages/client/lib/commands/XPENDING_RANGE.ts
@@ -1,56 +1,86 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface XPendingRangeOptions {
-    IDLE?: number;
-    consumer?: RedisCommandArgument;
+/**
+ * Options for the XPENDING RANGE command
+ * 
+ * @property IDLE - Filter by message idle time in milliseconds
+ * @property consumer - Filter by specific consumer name
+ */
+export interface XPendingRangeOptions {
+  IDLE?: number;
+  consumer?: RedisArgument;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    group: RedisCommandArgument,
-    start: string,
-    end: string,
+/**
+ * Raw reply structure for XPENDING RANGE command
+ * 
+ * @property id - Message ID
+ * @property consumer - Name of the consumer that holds the message
+ * @property millisecondsSinceLastDelivery - Time since last delivery attempt
+ * @property deliveriesCounter - Number of times this message was delivered
+ */
+type XPendingRangeRawReply = ArrayReply<TuplesReply<[
+  id: BlobStringReply,
+  consumer: BlobStringReply,
+  millisecondsSinceLastDelivery: NumberReply,
+  deliveriesCounter: NumberReply
+]>>;
+
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XPENDING command with range parameters to get detailed information about pending messages
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param group - Name of the consumer group
+   * @param start - Start of ID range (use '-' for minimum ID)
+   * @param end - End of ID range (use '+' for maximum ID)
+   * @param count - Maximum number of messages to return
+   * @param options - Additional filtering options
+   * @returns Array of pending message details
+   * @see https://redis.io/commands/xpending/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    group: RedisArgument,
+    start: RedisArgument,
+    end: RedisArgument,
     count: number,
     options?: XPendingRangeOptions
-): RedisCommandArguments {
-    const args = ['XPENDING', key, group];
+  ) { 
+    parser.push('XPENDING');
+    parser.pushKey(key);
+    parser.push(group);
 
-    if (options?.IDLE) {
-        args.push('IDLE', options.IDLE.toString());
+    if (options?.IDLE !== undefined) {
+      parser.push('IDLE', options.IDLE.toString());
     }
 
-    args.push(start, end, count.toString());
+    parser.push(start, end, count.toString());
 
     if (options?.consumer) {
-        args.push(options.consumer);
+      parser.push(options.consumer);
     }
-
-    return args;
-}
-
-type XPendingRangeRawReply = Array<[
-    id: RedisCommandArgument,
-    consumer: RedisCommandArgument,
-    millisecondsSinceLastDelivery: number,
-    deliveriesCounter: number
-]>;
-
-type XPendingRangeReply = Array<{
-    id: RedisCommandArgument;
-    owner: RedisCommandArgument;
-    millisecondsSinceLastDelivery: number;
-    deliveriesCounter: number;
-}>;
-
-export function transformReply(reply: XPendingRangeRawReply): XPendingRangeReply {
-    return reply.map(([id, owner, millisecondsSinceLastDelivery, deliveriesCounter]) => ({
-        id,
-        owner,
-        millisecondsSinceLastDelivery,
-        deliveriesCounter
-    }));
-}
+  },
+  /**
+   * Transforms the raw XPENDING RANGE reply into a structured array of message details
+   * 
+   * @param reply - Raw reply from Redis
+   * @returns Array of objects containing message ID, consumer, idle time, and delivery count
+   */
+  transformReply(reply: UnwrapReply<XPendingRangeRawReply>) {
+    return reply.map(pending => {
+      const unwrapped = pending as unknown as UnwrapReply<typeof pending>;
+      return {
+        id: unwrapped[0],
+        consumer: unwrapped[1],
+        millisecondsSinceLastDelivery: unwrapped[2],
+        deliveriesCounter: unwrapped[3]
+      };
+    });
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XRANGE.spec.ts b/packages/client/lib/commands/XRANGE.spec.ts
index 01c713e9595..b111a97aff1 100644
--- a/packages/client/lib/commands/XRANGE.spec.ts
+++ b/packages/client/lib/commands/XRANGE.spec.ts
@@ -1,30 +1,46 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XRANGE';
+import XRANGE from './XRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('XRANGE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+'),
-                ['XRANGE', 'key', '-', '+']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XRANGE, 'key', '-', '+'),
+        ['XRANGE', 'key', '-', '+']
+      );
+    });
+
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(XRANGE, 'key', '-', '+', {
+          COUNT: 1
+        }),
+        ['XRANGE', 'key', '-', '+', 'COUNT', '1']
+      );
+    });
+  });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    COUNT: 1
-                }),
-                ['XRANGE', 'key', '-', '+', 'COUNT', '1']
-            );
-        });
+  testUtils.testAll('xRange', async client => {
+    const message = Object.create(null, {
+      field: {
+        value: 'value',
+        enumerable: true
+      }
     });
 
-    testUtils.testWithClient('client.xRange', async client => {
-        assert.deepEqual(
-            await client.xRange('key', '+', '-'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    const [id, reply] = await Promise.all([
+      client.xAdd('key', '*', message),
+      client.xRange('key', '-', '+')
+    ]);
+    
+    assert.deepEqual(reply, [{
+      id,
+      message
+    }]);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XRANGE.ts b/packages/client/lib/commands/XRANGE.ts
index ae56639f769..4b83a66e5e6 100644
--- a/packages/client/lib/commands/XRANGE.ts
+++ b/packages/client/lib/commands/XRANGE.ts
@@ -1,26 +1,68 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
+import { StreamMessageRawReply, transformStreamMessageReply } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface XRangeOptions {
-    COUNT?: number;
+/**
+ * Options for the XRANGE command
+ * 
+ * @property COUNT - Limit the number of entries returned
+ */
+export interface XRangeOptions {
+  COUNT?: number;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    start: RedisCommandArgument,
-    end: RedisCommandArgument,
-    options?: XRangeOptions
-): RedisCommandArguments {
-    const args = ['XRANGE', key, start, end];
+/**
+ * Helper function to build XRANGE command arguments
+ * 
+ * @param start - Start of ID range (use '-' for minimum ID)
+ * @param end - End of ID range (use '+' for maximum ID)
+ * @param options - Additional options for the range query
+ * @returns Array of arguments for the XRANGE command
+ */
+export function xRangeArguments(
+  start: RedisArgument,
+  end: RedisArgument,
+  options?: XRangeOptions
+) {
+  const args = [start, end];
 
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
-    }
+  if (options?.COUNT) {
+    args.push('COUNT', options.COUNT.toString());
+  }
 
-    return args;
+  return args;
 }
 
-export { transformStreamMessagesReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XRANGE command to read stream entries in a specific range
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param args - Arguments tuple containing start ID, end ID, and options
+   * @returns Array of messages in the specified range
+   * @see https://redis.io/commands/xrange/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters<typeof xRangeArguments>) {
+    parser.push('XRANGE');
+    parser.pushKey(key);
+    parser.pushVariadic(xRangeArguments(args[0], args[1], args[2]));
+  },
+  /**
+   * Transforms the raw XRANGE reply into structured message objects
+   * 
+   * @param reply - Raw reply from Redis
+   * @param preserve - Preserve options (unused)
+   * @param typeMapping - Type mapping for message fields
+   * @returns Array of structured message objects
+   */
+  transformReply(
+    reply: UnwrapReply<ArrayReply<StreamMessageRawReply>>,
+    preserve?: any,
+    typeMapping?: TypeMapping
+  ) {
+    return reply.map(transformStreamMessageReply.bind(undefined, typeMapping));
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XREAD.spec.ts b/packages/client/lib/commands/XREAD.spec.ts
index b607f53532e..0edcfe43117 100644
--- a/packages/client/lib/commands/XREAD.spec.ts
+++ b/packages/client/lib/commands/XREAD.spec.ts
@@ -1,103 +1,167 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { FIRST_KEY_INDEX, transformArguments } from './XREAD';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, parseFirstKey } from '../test-utils';
+import XREAD from './XREAD';
+import { parseArgs } from './generic-transformers';
 
 describe('XREAD', () => {
-    describe('FIRST_KEY_INDEX', () => {
-        it('single stream', () => {
-            assert.equal(
-                FIRST_KEY_INDEX({ key: 'key', id: '' }),
-                'key'
-            );
-        });
+  describe('FIRST_KEY_INDEX', () => {
+    it('single stream', () => {
+      assert.equal(
+        parseFirstKey(XREAD, {
+          key: 'key',
+          id: ''
+        }),
+        'key'
+      );
+    });
 
-        it('multiple streams', () => {
-            assert.equal(
-                FIRST_KEY_INDEX([{ key: '1', id: '' }, { key: '2', id: '' }]),
-                '1'
-            );
-        });
+    it('multiple streams', () => {
+      assert.equal(
+        parseFirstKey(XREAD, [{
+          key: '1',
+          id: ''
+        }, {
+          key: '2',
+          id: ''
+        }]),
+        '1'
+      );
     });
+  });
 
-    describe('transformArguments', () => {
-        it('single stream', () => {
-            assert.deepEqual(
-                transformArguments({
-                    key: 'key',
-                    id: '0'
-                }),
-                ['XREAD', 'STREAMS', 'key', '0']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single stream', () => {
+      assert.deepEqual(
+        parseArgs(XREAD, {
+          key: 'key',
+          id: '0-0'
+        }),
+        ['XREAD', 'STREAMS', 'key', '0-0']
+      );
+    });
 
-        it('multiple streams', () => {
-            assert.deepEqual(
-                transformArguments([{
-                    key: '1',
-                    id: '0'
-                }, {
-                    key: '2',
-                    id: '0'
-                }]),
-                ['XREAD', 'STREAMS', '1', '2', '0', '0']
-            );
-        });
+    it('multiple streams', () => {
+      assert.deepEqual(
+        parseArgs(XREAD, [{
+          key: '1',
+          id: '0-0'
+        }, {
+          key: '2',
+          id: '0-0'
+        }]),
+        ['XREAD', 'STREAMS', '1', '2', '0-0', '0-0']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments({
-                    key: 'key',
-                    id: '0'
-                }, {
-                    COUNT: 1
-                }),
-                ['XREAD', 'COUNT', '1', 'STREAMS', 'key', '0']
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(XREAD, {
+          key: 'key',
+          id: '0-0'
+        }, {
+          COUNT: 1
+        }),
+        ['XREAD', 'COUNT', '1', 'STREAMS', 'key', '0-0']
+      );
+    });
 
-        it('with BLOCK', () => {
-            assert.deepEqual(
-                transformArguments({
-                    key: 'key',
-                    id: '0'
-                }, {
-                    BLOCK: 0
-                }),
-                ['XREAD', 'BLOCK', '0', 'STREAMS', 'key', '0']
-            );
-        });
+    it('with BLOCK', () => {
+      assert.deepEqual(
+        parseArgs(XREAD, {
+          key: 'key',
+          id: '0-0'
+        }, {
+          BLOCK: 0
+        }),
+        ['XREAD', 'BLOCK', '0', 'STREAMS', 'key', '0-0']
+      );
+    });
 
-        it('with COUNT, BLOCK', () => {
-            assert.deepEqual(
-                transformArguments({
-                    key: 'key',
-                    id: '0'
-                }, {
-                    COUNT: 1,
-                    BLOCK: 0
-                }),
-                ['XREAD', 'COUNT', '1', 'BLOCK', '0', 'STREAMS', 'key', '0']
-            );
-        });
+    it('with COUNT, BLOCK', () => {
+      assert.deepEqual(
+        parseArgs(XREAD, {
+          key: 'key',
+          id: '0-0'
+        }, {
+          COUNT: 1,
+          BLOCK: 0
+        }),
+        ['XREAD', 'COUNT', '1', 'BLOCK', '0', 'STREAMS', 'key', '0-0']
+      );
     });
+  });
+
+  testUtils.testAll('client.xRead', async client => {
+    const message = { field: 'value' }, 
+    [id, reply] = await Promise.all([
+      client.xAdd('key', '*', message),
+      client.xRead({
+        key: 'key',
+        id: '0-0'
+      }),
+    ])
+
+    // FUTURE resp3 compatible
+    const obj = Object.assign(Object.create(null), {
+      'key': [{
+        id: id,
+        message: Object.create(null, {
+          field: {
+            value: 'value',
+            configurable: true,
+            enumerable: true
+          }
+        })
+      }]
+    });
+
+    // v4 compatible
+    const expected = [{
+      name: 'key',
+      messages: [{
+        id: id,
+        message: Object.assign(Object.create(null), {
+          field: 'value'
+        })
+      }]
+    }];
+
+    assert.deepStrictEqual(reply, expected);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testWithClient('client.xRead should throw with resp3 and unstableResp3: false', async client => {
+    assert.throws(
+      () => client.xRead({
+        key: 'key',
+        id: '0-0'
+      }),
+      {
+        message: 'Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance'
+      }
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    }
+  });
 
-    testUtils.testWithClient('client.xRead', async client => {
-        assert.equal(
-            await client.xRead({
-                key: 'key',
-                id: '0'
-            }),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.xRead should not throw with resp3 and unstableResp3: true', async client => {
+    assert.doesNotThrow(
+      () => client.xRead({
+        key: 'key',
+        id: '0-0'
+      })
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3,
+      unstableResp3: true
+    }
+  });
 
-    testUtils.testWithCluster('cluster.xRead', async cluster => {
-        assert.equal(
-            await cluster.xRead({
-                key: 'key',
-                id: '0'
-            }),
-            null
-        );
-    }, GLOBAL.CLUSTERS.OPEN);
 });
diff --git a/packages/client/lib/commands/XREAD.ts b/packages/client/lib/commands/XREAD.ts
index e5f85dbe7fe..110443ad3a5 100644
--- a/packages/client/lib/commands/XREAD.ts
+++ b/packages/client/lib/commands/XREAD.ts
@@ -1,46 +1,83 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { Command, RedisArgument, ReplyUnion } from '../RESP/types';
+import { transformStreamsMessagesReplyResp2 } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = (streams: Array<XReadStream> | XReadStream): RedisCommandArgument => {
-    return Array.isArray(streams) ? streams[0].key : streams.key;
-};
+/**
+ * Structure representing a stream to read from
+ * 
+ * @property key - The stream key
+ * @property id - The message ID to start reading from
+ */
+export interface XReadStream {
+  key: RedisArgument;
+  id: RedisArgument;
+}
+
+export type XReadStreams = Array<XReadStream> | XReadStream;
 
-export const IS_READ_ONLY = true;
+/**
+ * Helper function to push stream keys and IDs to the command parser
+ * 
+ * @param parser - The command parser
+ * @param streams - Single stream or array of streams to read from
+ */
+export function pushXReadStreams(parser: CommandParser, streams: XReadStreams) {
+  parser.push('STREAMS');
 
-interface XReadStream {
-    key: RedisCommandArgument;
-    id: RedisCommandArgument;
+  if (Array.isArray(streams)) {
+    for (let i = 0; i < streams.length; i++) {
+      parser.pushKey(streams[i].key);
+    }
+    for (let i = 0; i < streams.length; i++) {
+      parser.push(streams[i].id);
+    }
+  } else {
+    parser.pushKey(streams.key);
+    parser.push(streams.id);
+  }
 }
 
-interface XReadOptions {
-    COUNT?: number;
-    BLOCK?: number;
+/**
+ * Options for the XREAD command
+ * 
+ * @property COUNT - Limit the number of entries returned per stream
+ * @property BLOCK - Milliseconds to block waiting for new entries (0 for indefinite)
+ */
+export interface XReadOptions {
+  COUNT?: number;
+  BLOCK?: number;
 }
 
-export function transformArguments(
-    streams: Array<XReadStream> | XReadStream,
-    options?: XReadOptions
-): RedisCommandArguments {
-    const args: RedisCommandArguments = ['XREAD'];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XREAD command to read messages from one or more streams
+   *
+   * @param parser - The command parser
+   * @param streams - Single stream or array of streams to read from
+   * @param options - Additional options for reading streams
+   * @returns Array of stream entries, each containing the stream name and its messages
+   * @see https://redis.io/commands/xread/
+   */
+  parseCommand(parser: CommandParser, streams: XReadStreams, options?: XReadOptions) {
+    parser.push('XREAD');
 
     if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
+      parser.push('COUNT', options.COUNT.toString());
     }
 
-    if (typeof options?.BLOCK === 'number') {
-        args.push('BLOCK', options.BLOCK.toString());
+    if (options?.BLOCK !== undefined) {
+      parser.push('BLOCK', options.BLOCK.toString());
     }
 
-    args.push('STREAMS');
-
-    const streamsArray = Array.isArray(streams) ? streams : [streams],
-        argsLength = args.length;
-    for (let i = 0; i < streamsArray.length; i++) {
-        const stream = streamsArray[i];
-        args[argsLength + i] = stream.key;
-        args[argsLength + streamsArray.length + i] = stream.id;
-    }
-
-    return args;
-}
-
-export { transformStreamsMessagesReply as transformReply } from './generic-transformers';
+    pushXReadStreams(parser, streams);
+  },
+  /**
+   * Transform functions for different RESP versions
+   */
+  transformReply: {
+    2: transformStreamsMessagesReplyResp2,
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XREADGROUP.spec.ts b/packages/client/lib/commands/XREADGROUP.spec.ts
index fa196d504ad..acc7cc2dea9 100644
--- a/packages/client/lib/commands/XREADGROUP.spec.ts
+++ b/packages/client/lib/commands/XREADGROUP.spec.ts
@@ -1,153 +1,190 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { FIRST_KEY_INDEX, transformArguments } from './XREADGROUP';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL, parseFirstKey } from '../test-utils';
+import XREADGROUP from './XREADGROUP';
+import { parseArgs } from './generic-transformers';
 
 describe('XREADGROUP', () => {
-    describe('FIRST_KEY_INDEX', () => {
-        it('single stream', () => {
-            assert.equal(
-                FIRST_KEY_INDEX('', '', { key: 'key', id: '' }),
-                'key'
-            );
-        });
-
-        it('multiple streams', () => {
-            assert.equal(
-                FIRST_KEY_INDEX('', '', [{ key: '1', id: '' }, { key: '2', id: '' }]),
-                '1'
-            );
-        });
+  describe('FIRST_KEY_INDEX', () => {
+    it('single stream', () => {
+      assert.equal(
+        parseFirstKey(XREADGROUP, '', '', { key: 'key', id: '' }),
+        'key'
+      );
     });
 
-    describe('transformArguments', () => {
-        it('single stream', () => {
-            assert.deepEqual(
-                transformArguments('group', 'consumer', {
-                    key: 'key',
-                    id: '0'
-                }),
-                ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', 'key', '0']
-            );
-        });
-
-        it('multiple streams', () => {
-            assert.deepEqual(
-                transformArguments('group', 'consumer', [{
-                    key: '1',
-                    id: '0'
-                }, {
-                    key: '2',
-                    id: '0'
-                }]),
-                ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', '1', '2', '0', '0']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('group', 'consumer', {
-                    key: 'key',
-                    id: '0'
-                }, {
-                    COUNT: 1
-                }),
-                ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'STREAMS', 'key', '0']
-            );
-        });
-
-        it('with BLOCK', () => {
-            assert.deepEqual(
-                transformArguments('group', 'consumer', {
-                    key: 'key',
-                    id: '0'
-                }, {
-                    BLOCK: 0
-                }),
-                ['XREADGROUP', 'GROUP', 'group', 'consumer', 'BLOCK', '0', 'STREAMS', 'key', '0']
-            );
-        });
-
-        it('with NOACK', () => {
-            assert.deepEqual(
-                transformArguments('group', 'consumer', {
-                    key: 'key',
-                    id: '0'
-                }, {
-                    NOACK: true
-                }),
-                ['XREADGROUP', 'GROUP', 'group', 'consumer', 'NOACK', 'STREAMS', 'key', '0']
-            );
-        });
-
-        it('with COUNT, BLOCK, NOACK', () => {
-            assert.deepEqual(
-                transformArguments('group', 'consumer', {
-                    key: 'key',
-                    id: '0'
-                }, {
-                    COUNT: 1,
-                    BLOCK: 0,
-                    NOACK: true
-                }),
-                ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'BLOCK', '0', 'NOACK', 'STREAMS', 'key', '0']
-            );
-        });
+    it('multiple streams', () => {
+      assert.equal(
+        parseFirstKey(XREADGROUP, '', '', [{ key: '1', id: '' }, { key: '2', id: '' }]),
+        '1'
+      );
     });
+  });
 
-    describe('client.xReadGroup', () => {
-        testUtils.testWithClient('null', async client => {
-            const [, readGroupReply] = await Promise.all([
-                client.xGroupCreate('key', 'group', '$', {
-                    MKSTREAM: true
-                }),
-                client.xReadGroup('group', 'consumer', {
-                    key: 'key',
-                    id: '>'
-                })
-            ]);
-
-            assert.equal(readGroupReply, null);
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('with a message', async client => {
-            const [, id, readGroupReply] = await Promise.all([
-                client.xGroupCreate('key', 'group', '$', {
-                    MKSTREAM: true
-                }),
-                client.xAdd('key', '*', { field: 'value' }),
-                client.xReadGroup('group', 'consumer', {
-                    key: 'key',
-                    id: '>'
-                })
-            ]);
-
-            assert.deepEqual(readGroupReply, [{
-                name: 'key',
-                messages: [{
-                    id,
-                    message: Object.create(null, {
-                        field: {
-                            value: 'value',
-                            configurable: true,
-                            enumerable: true
-                        }
-                    })
-                }]
-            }]);
-        }, GLOBAL.SERVERS.OPEN);
+  describe('transformArguments', () => {
+    it('single stream', () => {
+      assert.deepEqual(
+        parseArgs(XREADGROUP, 'group', 'consumer', {
+          key: 'key',
+          id: '0-0'
+        }),
+        ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', 'key', '0-0']
+      );
     });
 
-    testUtils.testWithCluster('cluster.xReadGroup', async cluster => {
-        const [, readGroupReply] = await Promise.all([
-            cluster.xGroupCreate('key', 'group', '$', {
-                MKSTREAM: true
-            }),
-            cluster.xReadGroup('group', 'consumer', {
-                key: 'key',
-                id: '>'
-            })
-        ]);
-
-        assert.equal(readGroupReply, null);
-    }, GLOBAL.CLUSTERS.OPEN);
+    it('multiple streams', () => {
+      assert.deepEqual(
+        parseArgs(XREADGROUP, 'group', 'consumer', [{
+          key: '1',
+          id: '0-0'
+        }, {
+          key: '2',
+          id: '0-0'
+        }]),
+        ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', '1', '2', '0-0', '0-0']
+      );
+    });
+
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(XREADGROUP, 'group', 'consumer', {
+          key: 'key',
+          id: '0-0'
+        }, {
+          COUNT: 1
+        }),
+        ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'STREAMS', 'key', '0-0']
+      );
+    });
+
+    it('with BLOCK', () => {
+      assert.deepEqual(
+        parseArgs(XREADGROUP, 'group', 'consumer', {
+          key: 'key',
+          id: '0-0'
+        }, {
+          BLOCK: 0
+        }),
+        ['XREADGROUP', 'GROUP', 'group', 'consumer', 'BLOCK', '0', 'STREAMS', 'key', '0-0']
+      );
+    });
+
+    it('with NOACK', () => {
+      assert.deepEqual(
+        parseArgs(XREADGROUP, 'group', 'consumer', {
+          key: 'key',
+          id: '0-0'
+        }, {
+          NOACK: true
+        }),
+        ['XREADGROUP', 'GROUP', 'group', 'consumer', 'NOACK', 'STREAMS', 'key', '0-0']
+      );
+    });
+
+    it('with COUNT, BLOCK, NOACK', () => {
+      assert.deepEqual(
+        parseArgs(XREADGROUP, 'group', 'consumer', {
+          key: 'key',
+          id: '0-0'
+        }, {
+          COUNT: 1,
+          BLOCK: 0,
+          NOACK: true
+        }),
+        ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'BLOCK', '0', 'NOACK', 'STREAMS', 'key', '0-0']
+      );
+    });
+  });
+
+  testUtils.testAll('xReadGroup - null', async client => {
+    const [, readGroupReply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      })
+    ]);
+
+    assert.equal(readGroupReply, null);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('xReadGroup - with a message', async client => {
+    const [, id, readGroupReply] = await Promise.all([
+      client.xGroupCreate('key', 'group', '$', {
+        MKSTREAM: true
+      }),
+      client.xAdd('key', '*', { field: 'value' }),
+      client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      })
+    ]);
+
+
+    // FUTURE resp3 compatible
+    const obj = Object.assign(Object.create(null), {
+      'key': [{
+        id: id,
+        message: Object.create(null, {
+          field: {
+            value: 'value',
+            configurable: true,
+            enumerable: true
+          }
+        })
+      }]
+    });
+
+    // v4 compatible
+    const expected = [{
+      name: 'key',
+      messages: [{
+        id: id,
+        message: Object.assign(Object.create(null), {
+          field: 'value'
+        })
+      }]
+    }];
+
+    assert.deepStrictEqual(readGroupReply, expected);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testWithClient('client.xReadGroup should throw with resp3 and unstableResp3: false', async client => {
+    assert.throws(
+      () => client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      }),
+      {
+        message: 'Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance'
+      }
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3
+    }
+  });
+
+  testUtils.testWithClient('client.xReadGroup should not throw with resp3 and unstableResp3: true', async client => {
+    assert.doesNotThrow(
+      () => client.xReadGroup('group', 'consumer', {
+        key: 'key',
+        id: '>'
+      })
+    );
+  }, {
+    ...GLOBAL.SERVERS.OPEN,
+    clientOptions: {
+      RESP: 3,
+      unstableResp3: true
+    }
+  });
 });
diff --git a/packages/client/lib/commands/XREADGROUP.ts b/packages/client/lib/commands/XREADGROUP.ts
index e90e698a2ad..b274aab95fe 100644
--- a/packages/client/lib/commands/XREADGROUP.ts
+++ b/packages/client/lib/commands/XREADGROUP.ts
@@ -1,57 +1,63 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export interface XReadGroupStream {
-    key: RedisCommandArgument;
-    id: RedisCommandArgument;
-}
-
+import { CommandParser } from '../client/parser';
+import { Command, RedisArgument, ReplyUnion } from '../RESP/types';
+import { XReadStreams, pushXReadStreams } from './XREAD';
+import { transformStreamsMessagesReplyResp2 } from './generic-transformers';
+
+/**
+ * Options for the XREADGROUP command
+ * 
+ * @property COUNT - Limit the number of entries returned per stream
+ * @property BLOCK - Milliseconds to block waiting for new entries (0 for indefinite)
+ * @property NOACK - Skip adding the message to the PEL (Pending Entries List)
+ */
 export interface XReadGroupOptions {
-    COUNT?: number;
-    BLOCK?: number;
-    NOACK?: true;
+  COUNT?: number;
+  BLOCK?: number;
+  NOACK?: boolean;
 }
 
-export const FIRST_KEY_INDEX = (
-    _group: RedisCommandArgument,
-    _consumer: RedisCommandArgument,
-    streams: Array<XReadGroupStream> | XReadGroupStream
-): RedisCommandArgument => {
-    return Array.isArray(streams) ? streams[0].key : streams.key;
-};
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    group: RedisCommandArgument,
-    consumer: RedisCommandArgument,
-    streams: Array<XReadGroupStream> | XReadGroupStream,
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the XREADGROUP command to read messages from streams as a consumer group member
+   *
+   * @param parser - The command parser
+   * @param group - Name of the consumer group
+   * @param consumer - Name of the consumer in the group
+   * @param streams - Single stream or array of streams to read from
+   * @param options - Additional options for reading streams
+   * @returns Array of stream entries, each containing the stream name and its messages
+   * @see https://redis.io/commands/xreadgroup/
+   */
+  parseCommand(
+    parser: CommandParser,
+    group: RedisArgument,
+    consumer: RedisArgument,
+    streams: XReadStreams,
     options?: XReadGroupOptions
-): RedisCommandArguments {
-    const args = ['XREADGROUP', 'GROUP', group, consumer];
+  ) {
+    parser.push('XREADGROUP', 'GROUP', group, consumer);
 
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
+    if (options?.COUNT !== undefined) {
+      parser.push('COUNT', options.COUNT.toString());
     }
 
-    if (typeof options?.BLOCK === 'number') {
-        args.push('BLOCK', options.BLOCK.toString());
+    if (options?.BLOCK !== undefined) {
+      parser.push('BLOCK', options.BLOCK.toString());
     }
 
     if (options?.NOACK) {
-        args.push('NOACK');
+      parser.push('NOACK');
     }
 
-    args.push('STREAMS');
-
-    const streamsArray = Array.isArray(streams) ? streams : [streams],
-        argsLength = args.length;
-    for (let i = 0; i < streamsArray.length; i++) {
-        const stream = streamsArray[i];
-        args[argsLength + i] = stream.key;
-        args[argsLength + streamsArray.length + i] = stream.id;
-    }
-
-    return args;
-}
-
-export { transformStreamsMessagesReply as transformReply } from './generic-transformers';
+    pushXReadStreams(parser, streams);
+  },
+  /**
+   * Transform functions for different RESP versions
+   */
+  transformReply: {
+    2: transformStreamsMessagesReplyResp2,
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true,
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XREVRANGE.spec.ts b/packages/client/lib/commands/XREVRANGE.spec.ts
index fd6e1a3adfe..9872dc5e9e0 100644
--- a/packages/client/lib/commands/XREVRANGE.spec.ts
+++ b/packages/client/lib/commands/XREVRANGE.spec.ts
@@ -1,30 +1,46 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XREVRANGE';
+import XREVRANGE from './XREVRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('XREVRANGE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+'),
-                ['XREVRANGE', 'key', '-', '+']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XREVRANGE, 'key', '-', '+'),
+        ['XREVRANGE', 'key', '-', '+']
+      );
+    });
+
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(XREVRANGE, 'key', '-', '+', {
+          COUNT: 1
+        }),
+        ['XREVRANGE', 'key', '-', '+', 'COUNT', '1']
+      );
+    });
+  });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    COUNT: 1
-                }),
-                ['XREVRANGE', 'key', '-', '+', 'COUNT', '1']
-            );
-        });
+  testUtils.testAll('xRevRange', async client => {
+    const message = Object.create(null, {
+      field: {
+        value: 'value',
+        enumerable: true
+      }
     });
 
-    testUtils.testWithClient('client.xRevRange', async client => {
-        assert.deepEqual(
-            await client.xRevRange('key', '+', '-'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    const [id, reply] = await Promise.all([
+      client.xAdd('key', '*', message),
+      client.xRange('key', '-', '+')
+    ]);
+    
+    assert.deepEqual(reply, [{
+      id,
+      message
+    }]);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/XREVRANGE.ts b/packages/client/lib/commands/XREVRANGE.ts
index 96bbeba83ce..452c2ab3807 100644
--- a/packages/client/lib/commands/XREVRANGE.ts
+++ b/packages/client/lib/commands/XREVRANGE.ts
@@ -1,26 +1,35 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { Command, RedisArgument } from '../RESP/types';
+import XRANGE, { xRangeArguments } from './XRANGE';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface XRangeRevOptions {
-    COUNT?: number;
-}
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    start: RedisCommandArgument,
-    end: RedisCommandArgument,
-    options?: XRangeRevOptions
-): RedisCommandArguments {
-    const args = ['XREVRANGE', key, start, end];
-
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
-    }
-
-    return args;
+/**
+ * Options for the XREVRANGE command
+ * 
+ * @property COUNT - Limit the number of entries returned
+ */
+export interface XRevRangeOptions {
+  COUNT?: number;
 }
 
-export { transformStreamMessagesReply as transformReply } from './generic-transformers';
+/**
+ * Command for reading stream entries in reverse order
+ */
+export default {
+  CACHEABLE: XRANGE.CACHEABLE,
+  IS_READ_ONLY: XRANGE.IS_READ_ONLY,
+  /**
+   * Constructs the XREVRANGE command to read stream entries in reverse order
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param args - Arguments tuple containing start ID, end ID, and options
+   * @returns Array of messages in the specified range in reverse order
+   * @see https://redis.io/commands/xrevrange/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters<typeof xRangeArguments>) {
+    parser.push('XREVRANGE');
+    parser.pushKey(key);
+    parser.pushVariadic(xRangeArguments(args[0], args[1], args[2]));
+  },
+  transformReply: XRANGE.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/XSETID.spec.ts b/packages/client/lib/commands/XSETID.spec.ts
index e69de29bb2d..b3609695345 100644
--- a/packages/client/lib/commands/XSETID.spec.ts
+++ b/packages/client/lib/commands/XSETID.spec.ts
@@ -0,0 +1,47 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import XSETID from './XSETID';
+import { parseArgs } from './generic-transformers';
+
+describe('XSETID', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XSETID, 'key', '0-0'),
+        ['XSETID', 'key', '0-0']
+      );
+    });
+
+    it('with ENTRIESADDED', () => {
+      assert.deepEqual(
+        parseArgs(XSETID, 'key', '0-0', {
+          ENTRIESADDED: 1
+        }),
+        ['XSETID', 'key', '0-0', 'ENTRIESADDED', '1']
+      );
+    });
+
+    it('with MAXDELETEDID', () => {
+      assert.deepEqual(
+        parseArgs(XSETID, 'key', '0-0', {
+          MAXDELETEDID: '1-1'
+        }),
+        ['XSETID', 'key', '0-0', 'MAXDELETEDID', '1-1']
+      );
+    });
+  });
+
+  testUtils.testAll('xSetId', async client => {
+    const id = await client.xAdd('key', '*', {
+      field: 'value'
+    });
+
+    assert.equal(
+      await client.xSetId('key', id),
+      'OK'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/XSETID.ts b/packages/client/lib/commands/XSETID.ts
index 76acc7ebab4..c76ac0b23a4 100644
--- a/packages/client/lib/commands/XSETID.ts
+++ b/packages/client/lib/commands/XSETID.ts
@@ -1,28 +1,32 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface XSetIdOptions {
-    ENTRIESADDED?: number;
-    MAXDELETEDID?: RedisCommandArgument;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
+export interface XSetIdOptions {
+  /** added in 7.0 */
+  ENTRIESADDED?: number;
+  /** added in 7.0 */
+  MAXDELETEDID?: RedisArgument;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    lastId: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    lastId: RedisArgument,
     options?: XSetIdOptions
-): RedisCommandArguments {
-    const args = ['XSETID', key, lastId];
+  ) {
+    parser.push('XSETID');
+    parser.pushKey(key);
+    parser.push(lastId);
 
     if (options?.ENTRIESADDED) {
-        args.push('ENTRIESADDED', options.ENTRIESADDED.toString());
+      parser.push('ENTRIESADDED', options.ENTRIESADDED.toString());
     }
 
     if (options?.MAXDELETEDID) {
-        args.push('MAXDELETEDID', options.MAXDELETEDID);
+      parser.push('MAXDELETEDID', options.MAXDELETEDID);
     }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
 
-    return args;
-}
-
-export declare function transformReply(): 'OK';
diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts
index a8f8078eb28..b88cf84676e 100644
--- a/packages/client/lib/commands/XTRIM.spec.ts
+++ b/packages/client/lib/commands/XTRIM.spec.ts
@@ -1,49 +1,142 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './XTRIM';
+import XTRIM from './XTRIM';
+import { parseArgs } from './generic-transformers';
+import { STREAM_DELETION_POLICY } from './common-stream.types';
 
 describe('XTRIM', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'MAXLEN', 1),
-                ['XTRIM', 'key', 'MAXLEN', '1']
-            );
-        });
-
-        it('with strategyModifier', () => {
-            assert.deepEqual(
-                transformArguments('key', 'MAXLEN', 1, {
-                    strategyModifier: '='
-                }),
-                ['XTRIM', 'key', 'MAXLEN', '=', '1']
-            );
-        });
-
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('key', 'MAXLEN', 1, {
-                    LIMIT: 1
-                }),
-                ['XTRIM', 'key', 'MAXLEN', '1', 'LIMIT', '1']
-            );
-        });
-
-        it('with strategyModifier, LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('key', 'MAXLEN', 1, {
-                    strategyModifier: '=',
-                    LIMIT: 1
-                }),
-                ['XTRIM', 'key', 'MAXLEN', '=', '1', 'LIMIT', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MAXLEN', 1),
+        ['XTRIM', 'key', 'MAXLEN', '1']
+      );
     });
 
-    testUtils.testWithClient('client.xTrim', async client => {
-        assert.equal(
-            await client.xTrim('key', 'MAXLEN', 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    it('simple - MINID', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MINID', 123),
+        ['XTRIM', 'key', 'MINID', '123']
+      );
+    });
+
+    it('with strategyModifier', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MAXLEN', 1, {
+          strategyModifier: '='
+        }),
+        ['XTRIM', 'key', 'MAXLEN', '=', '1']
+      );
+    });
+
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MAXLEN', 1, {
+          LIMIT: 1
+        }),
+        ['XTRIM', 'key', 'MAXLEN', '1', 'LIMIT', '1']
+      );
+    });
+
+    it('with strategyModifier, LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MAXLEN', 1, {
+          strategyModifier: '=',
+          LIMIT: 1
+        }),
+        ['XTRIM', 'key', 'MAXLEN', '=', '1', 'LIMIT', '1']
+      );
+    });
+
+    it('with policy', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MAXLEN', 1, {
+          policy: STREAM_DELETION_POLICY.DELREF
+        }),
+        ['XTRIM', 'key', 'MAXLEN', '1', 'DELREF']
+      );
+    });
+
+    it('with all options', () => {
+      assert.deepEqual(
+        parseArgs(XTRIM, 'key', 'MAXLEN', 1, {
+          strategyModifier: '~',
+          LIMIT: 100,
+          policy: STREAM_DELETION_POLICY.ACKED
+        }),
+        ['XTRIM', 'key', 'MAXLEN', '~', '1', 'LIMIT', '100', 'ACKED']
+      );
+    });
+  });
+
+  testUtils.testAll('xTrim with MAXLEN', async client => {
+    assert.equal(
+      typeof await client.xTrim('key', 'MAXLEN', 1),
+      'number'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN,
+  });
+
+  testUtils.testAll('xTrim with MINID', async client => {
+    assert.equal(
+      typeof await client.xTrim('key', 'MINID', 1),
+      'number'
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN,
+  });
+
+  testUtils.testAll(
+    'xTrim with LIMIT',
+    async (client) => {
+      assert.equal(
+        typeof await client.xTrim('{tag}key', 'MAXLEN', 1000, {
+          strategyModifier: '~',
+          LIMIT: 10
+        }),
+        'number'
+      );
+    },
+    {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN,
+    }
+  );
+
+  testUtils.testAll(
+    'xTrim with policy',
+    async (client) => {
+      assert.equal(
+        typeof await client.xTrim('{tag}key', 'MAXLEN', 0, {
+          policy: STREAM_DELETION_POLICY.DELREF
+        }),
+        'number'
+      );
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
+
+  testUtils.testAll(
+    'xTrim with all options',
+    async (client) => {
+      assert.equal(
+        typeof await client.xTrim('{tag}key', 'MINID', 0, {
+          strategyModifier: '~',
+          LIMIT: 10,
+          policy: STREAM_DELETION_POLICY.KEEPREF
+        }),
+        'number'
+      );
+    },
+    {
+      client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] },
+      cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] },
+    }
+  );
 });
diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts
index 15b934c56ef..34171d4611e 100644
--- a/packages/client/lib/commands/XTRIM.ts
+++ b/packages/client/lib/commands/XTRIM.ts
@@ -1,31 +1,62 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface XTrimOptions {
-    strategyModifier?: '=' | '~';
-    LIMIT?: number;
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
+import { StreamDeletionPolicy } from './common-stream.types';
+
+/**
+ * Options for the XTRIM command
+ * 
+ * @property strategyModifier - Exact ('=') or approximate ('~') trimming
+ * @property LIMIT - Maximum number of entries to trim in one call (Redis 6.2+)
+ * @property policy - Policy to apply when deleting entries (optional, defaults to KEEPREF)
+ */
+export interface XTrimOptions {
+  strategyModifier?: '=' | '~';
+  /** added in 6.2 */
+  LIMIT?: number;
+  /** added in 8.2 */
+  policy?: StreamDeletionPolicy;
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
+/**
+ * Command for trimming a stream to a specified length or minimum ID
+ */
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Constructs the XTRIM command to trim a stream by length or minimum ID
+   *
+   * @param parser - The command parser
+   * @param key - The stream key
+   * @param strategy - Trim by maximum length (MAXLEN) or minimum ID (MINID)
+   * @param threshold - Maximum length or minimum ID threshold
+   * @param options - Additional options for trimming
+   * @returns Number of entries removed from the stream
+   * @see https://redis.io/commands/xtrim/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     strategy: 'MAXLEN' | 'MINID',
     threshold: number,
     options?: XTrimOptions
-): RedisCommandArguments {
-    const args = ['XTRIM', key, strategy];
+  ) {
+    parser.push('XTRIM')
+    parser.pushKey(key);
+    parser.push(strategy);
 
     if (options?.strategyModifier) {
-        args.push(options.strategyModifier);
+      parser.push(options.strategyModifier);
     }
 
-    args.push(threshold.toString());
+    parser.push(threshold.toString());
 
     if (options?.LIMIT) {
-        args.push('LIMIT', options.LIMIT.toString());
+      parser.push('LIMIT', options.LIMIT.toString());
     }
 
-    return args;
-}
-
-export declare function transformReply(): number;
+    if (options?.policy) {
+      parser.push(options.policy);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZADD.spec.ts b/packages/client/lib/commands/ZADD.spec.ts
index 4f497bdca90..0e770693e3a 100644
--- a/packages/client/lib/commands/ZADD.spec.ts
+++ b/packages/client/lib/commands/ZADD.spec.ts
@@ -1,127 +1,146 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZADD';
+import ZADD from './ZADD';
+import { parseArgs } from './generic-transformers';
 
 describe('ZADD', () => {
-    describe('transformArguments', () => {
-        it('single member', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }),
-                ['ZADD', 'key', '1', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('single member', () => {
+      assert.deepEqual(
+        parseArgs(ZADD, 'key', {
+          value: '1',
+          score: 1
+        }),
+        ['ZADD', 'key', '1', '1']
+      );
+    });
+
+    it('multiple members', () => {
+      assert.deepEqual(
+        parseArgs(ZADD, 'key', [{
+          value: '1',
+          score: 1
+        }, {
+          value: '2',
+          score: 2
+        }]),
+        ['ZADD', 'key', '1', '1', '2', '2']
+      );
+    });
 
-        it('multiple members', () => {
-            assert.deepEqual(
-                transformArguments('key', [{
-                    value: '1',
-                    score: 1
-                }, {
-                    value: '2',
-                    score: 2
-                }]),
-                ['ZADD', 'key', '1', '1', '2', '2']
-            );
-        });
+    describe('with condition', () => {
+      it('condition property', () => {
+        assert.deepEqual(
+          parseArgs(ZADD, 'key', {
+            value: '1',
+            score: 1
+          }, {
+            condition: 'NX'
+          }),
+          ['ZADD', 'key', 'NX', '1', '1']
+        );
+      });
 
-        it('with NX', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    NX: true
-                }),
-                ['ZADD', 'key', 'NX', '1', '1']
-            );
-        });
+      it('with NX (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(ZADD, 'key', {
+            value: '1',
+            score: 1
+          }, {
+            NX: true
+          }),
+          ['ZADD', 'key', 'NX', '1', '1']
+        );
+      });
 
-        it('with XX', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    XX: true
-                }),
-                ['ZADD', 'key', 'XX', '1', '1']
-            );
-        });
+      it('with XX (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(ZADD, 'key', {
+            value: '1',
+            score: 1
+          }, {
+            XX: true
+          }),
+          ['ZADD', 'key', 'XX', '1', '1']
+        );
+      });
+    });
 
-        it('with GT', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    GT: true
-                }),
-                ['ZADD', 'key', 'GT', '1', '1']
-            );
-        });
+    describe('with comparison', () => {
+      it('with LT', () => {
+        assert.deepEqual(
+          parseArgs(ZADD, 'key', {
+            value: '1',
+            score: 1
+          }, {
+            comparison: 'LT'
+          }),
+          ['ZADD', 'key', 'LT', '1', '1']
+        );
+      });
 
-        it('with LT', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    LT: true
-                }),
-                ['ZADD', 'key', 'LT', '1', '1']
-            );
-        });
+      it('with LT (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(ZADD, 'key', {
+            value: '1',
+            score: 1
+          }, {
+            LT: true
+          }),
+          ['ZADD', 'key', 'LT', '1', '1']
+        );
+      });
 
-        it('with CH', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    CH: true
-                }),
-                ['ZADD', 'key', 'CH', '1', '1']
-            );
-        });
+      it('with GT (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(ZADD, 'key', {
+            value: '1',
+            score: 1
+          }, {
+            GT: true
+          }),
+          ['ZADD', 'key', 'GT', '1', '1']
+        );
+      });
+    });    
 
-        it('with INCR', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    INCR: true
-                }),
-                ['ZADD', 'key', 'INCR', '1', '1']
-            );
-        });
+    it('with CH', () => {
+      assert.deepEqual(
+        parseArgs(ZADD, 'key', {
+          value: '1',
+          score: 1
+        }, {
+          CH: true
+        }),
+        ['ZADD', 'key', 'CH', '1', '1']
+      );
+    });
 
-        it('with XX, GT, CH, INCR', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    value: '1',
-                    score: 1
-                }, {
-                    XX: true,
-                    GT: true,
-                    CH: true,
-                    INCR: true
-                }),
-                ['ZADD', 'key', 'XX', 'GT', 'CH', 'INCR', '1', '1']
-            );
-        });
+    it('with condition, comparison, CH', () => {
+      assert.deepEqual(
+        parseArgs(ZADD, 'key', {
+          value: '1',
+          score: 1
+        }, {
+          condition: 'XX',
+          comparison: 'LT',
+          CH: true
+        }),
+        ['ZADD', 'key', 'XX', 'LT', 'CH', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zAdd', async client => {
-        assert.equal(
-            await client.zAdd('key', {
-                value: '1',
-                score: 1
-            }),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zAdd', async client => {
+    assert.equal(
+      await client.zAdd('key', {
+        value: 'a',
+        score: 1
+      }),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZADD.ts b/packages/client/lib/commands/ZADD.ts
index 9ac67d59cce..d53835d44d1 100644
--- a/packages/client/lib/commands/ZADD.ts
+++ b/packages/client/lib/commands/ZADD.ts
@@ -1,71 +1,110 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformNumberInfinityArgument, ZMember } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface NX {
-    NX?: true;
-}
-
-interface XX {
-    XX?: true;
-}
-
-interface LT {
-    LT?: true;
-}
-
-interface GT {
-    GT?: true;
-}
-
-interface CH {
-    CH?: true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { SortedSetMember, transformDoubleArgument, transformDoubleReply } from './generic-transformers';
+
+/**
+ * Options for the ZADD command
+ */
+export interface ZAddOptions {
+  condition?: 'NX' | 'XX';
+  /**
+   * @deprecated Use `{ condition: 'NX' }` instead.
+   */
+  NX?: boolean;
+  /**
+   * @deprecated Use `{ condition: 'XX' }` instead.
+   */
+  XX?: boolean;
+  comparison?: 'LT' | 'GT';
+  /**
+   * @deprecated Use `{ comparison: 'LT' }` instead.
+   */
+  LT?: boolean;
+  /**
+   * @deprecated Use `{ comparison: 'GT' }` instead.
+   */
+  GT?: boolean;
+  CH?: boolean;
 }
 
-interface INCR {
-    INCR?: true;
-}
-
-type ZAddOptions = (NX | (XX & LT & GT)) & CH & INCR;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    members: ZMember | Array<ZMember>,
+/**
+ * Command for adding members to a sorted set
+ */
+export default {
+  /**
+   * Constructs the ZADD command to add one or more members to a sorted set
+   *
+   * @param parser - The command parser
+   * @param key - The sorted set key
+   * @param members - One or more members to add with their scores
+   * @param options - Additional options for adding members
+   * @returns Number of new members added (or changed members if CH is set)
+   * @see https://redis.io/commands/zadd/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    members: SortedSetMember | Array<SortedSetMember>,
     options?: ZAddOptions
-): RedisCommandArguments {
-    const args = ['ZADD', key];
-
-    if ((<NX>options)?.NX) {
-        args.push('NX');
-    } else {
-        if ((<XX>options)?.XX) {
-            args.push('XX');
-        }
-
-        if ((<GT>options)?.GT) {
-            args.push('GT');
-        } else if ((<LT>options)?.LT) {
-            args.push('LT');
-        }
+  ) {
+    parser.push('ZADD');
+    parser.pushKey(key);
+
+    if (options?.condition) {
+      parser.push(options.condition);
+    } else if (options?.NX) {
+      parser.push('NX');
+    } else if (options?.XX) {
+      parser.push('XX');
+    } 
+
+    if (options?.comparison) {
+      parser.push(options.comparison);
+    } else if (options?.LT) {
+      parser.push('LT');
+    } else if (options?.GT) {
+      parser.push('GT');
     }
 
-    if ((<CH>options)?.CH) {
-        args.push('CH');
+    if (options?.CH) {
+      parser.push('CH');
     }
 
-    if ((<INCR>options)?.INCR) {
-        args.push('INCR');
+    pushMembers(parser, members);
+  },
+  transformReply: transformDoubleReply
+} as const satisfies Command;
+
+/**
+ * Helper function to push sorted set members to the command
+ * 
+ * @param parser - The command parser
+ * @param members - One or more members with their scores
+ */
+export function pushMembers(
+  parser: CommandParser,
+  members: SortedSetMember | Array<SortedSetMember>) {
+  if (Array.isArray(members)) {
+    for (const member of members) {
+      pushMember(parser, member);
     }
-
-    for (const { score, value } of (Array.isArray(members) ? members : [members])) {
-        args.push(
-            transformNumberInfinityArgument(score),
-            value
-        );
-    }
-
-    return args;
+  } else {
+    pushMember(parser, members);
+  }
 }
 
-export { transformNumberInfinityReply as transformReply } from './generic-transformers';
+/**
+ * Helper function to push a single sorted set member to the command
+ * 
+ * @param parser - The command parser
+ * @param member - Member with its score
+ */
+function pushMember(
+  parser: CommandParser,
+  member: SortedSetMember
+) {
+  parser.push(
+    transformDoubleArgument(member.score),
+    member.value
+  );
+}
diff --git a/packages/client/lib/commands/ZADD_INCR.spec.ts b/packages/client/lib/commands/ZADD_INCR.spec.ts
new file mode 100644
index 00000000000..df9ac87f449
--- /dev/null
+++ b/packages/client/lib/commands/ZADD_INCR.spec.ts
@@ -0,0 +1,94 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ZADD_INCR from './ZADD_INCR';
+import { parseArgs } from './generic-transformers';
+
+describe('ZADD INCR', () => {
+  describe('transformArguments', () => {
+    it('single member', () => {
+      assert.deepEqual(
+        parseArgs(ZADD_INCR, 'key', {
+          value: '1',
+          score: 1
+        }),
+        ['ZADD', 'key', 'INCR', '1', '1']
+      );
+    });
+
+    it('multiple members', () => {
+      assert.deepEqual(
+        parseArgs(ZADD_INCR, 'key', [{
+          value: '1',
+          score: 1
+        }, {
+          value: '2',
+          score: 2
+        }]),
+        ['ZADD', 'key', 'INCR', '1', '1', '2', '2']
+      );
+    });
+
+    it('with condition', () => {
+      assert.deepEqual(
+        parseArgs(ZADD_INCR, 'key', {
+          value: '1',
+          score: 1
+        }, {
+          condition: 'NX'
+        }),
+        ['ZADD', 'key', 'NX', 'INCR', '1', '1']
+      );
+    });
+
+    it('with comparison', () => {
+      assert.deepEqual(
+        parseArgs(ZADD_INCR, 'key', {
+          value: '1',
+          score: 1
+        }, {
+          comparison: 'LT'
+        }),
+        ['ZADD', 'key', 'LT', 'INCR', '1', '1']
+      );
+    });    
+
+    it('with CH', () => {
+      assert.deepEqual(
+        parseArgs(ZADD_INCR, 'key', {
+          value: '1',
+          score: 1
+        }, {
+          CH: true
+        }),
+        ['ZADD', 'key', 'CH', 'INCR', '1', '1']
+      );
+    });
+
+    it('with condition, comparison, CH', () => {
+      assert.deepEqual(
+        parseArgs(ZADD_INCR, 'key', {
+          value: '1',
+          score: 1
+        }, {
+          condition: 'XX',
+          comparison: 'LT',
+          CH: true
+        }),
+        ['ZADD', 'key', 'XX', 'LT', 'CH', 'INCR', '1', '1']
+      );
+    });
+  });
+
+  testUtils.testAll('zAddIncr', async client => {
+    assert.equal(
+      await client.zAddIncr('key', {
+        value: 'a',
+        score: 1
+      }),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/ZADD_INCR.ts b/packages/client/lib/commands/ZADD_INCR.ts
new file mode 100644
index 00000000000..73e40efe5c8
--- /dev/null
+++ b/packages/client/lib/commands/ZADD_INCR.ts
@@ -0,0 +1,59 @@
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { pushMembers } from './ZADD';
+import { SortedSetMember, transformNullableDoubleReply } from './generic-transformers';
+
+/**
+ * Options for the ZADD INCR command
+ * 
+ * @property condition - Add condition: NX (only if not exists) or XX (only if exists)
+ * @property comparison - Score comparison: LT (less than) or GT (greater than)
+ * @property CH - Return the number of changed elements instead of added elements
+ */
+export interface ZAddOptions {
+  condition?: 'NX' | 'XX';
+  comparison?: 'LT' | 'GT';
+  CH?: boolean;
+}
+
+/**
+ * Command for incrementing the score of a member in a sorted set
+ */
+export default {
+  /**
+   * Constructs the ZADD command with INCR option to increment the score of a member
+   *
+   * @param parser - The command parser
+   * @param key - The sorted set key
+   * @param members - Member(s) whose score to increment
+   * @param options - Additional options for the increment operation
+   * @returns The new score of the member after increment (null if member does not exist with XX option)
+   * @see https://redis.io/commands/zadd/
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    members: SortedSetMember | Array<SortedSetMember>,
+    options?: ZAddOptions
+  ) {
+    parser.push('ZADD');
+    parser.pushKey(key);
+
+    if (options?.condition) {
+      parser.push(options.condition);
+    }
+
+    if (options?.comparison) {
+      parser.push(options.comparison);
+    }
+
+    if (options?.CH) {
+      parser.push('CH');
+    }
+
+    parser.push('INCR');
+
+    pushMembers(parser, members);
+  },
+  transformReply: transformNullableDoubleReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZCARD.spec.ts b/packages/client/lib/commands/ZCARD.spec.ts
index 2e90da772b6..44adec0833a 100644
--- a/packages/client/lib/commands/ZCARD.spec.ts
+++ b/packages/client/lib/commands/ZCARD.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZCARD';
+import ZCARD from './ZCARD';
+import { parseArgs } from './generic-transformers';
 
 describe('ZCARD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['ZCARD', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZCARD, 'key'),
+      ['ZCARD', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.zCard', async client => {
-        assert.equal(
-            await client.zCard('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zCard', async client => {
+    assert.equal(
+      await client.zCard('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZCARD.ts b/packages/client/lib/commands/ZCARD.ts
index f208c181369..d2e0f8df5e2 100644
--- a/packages/client/lib/commands/ZCARD.ts
+++ b/packages/client/lib/commands/ZCARD.ts
@@ -1,11 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['ZCARD', key];
-}
-
-export declare function transformReply(): number;
+/**
+ * Command for getting the number of members in a sorted set
+ */
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Constructs the ZCARD command to get the cardinality (number of members) of a sorted set
+   *
+   * @param parser - The command parser
+   * @param key - The sorted set key
+   * @returns Number of members in the sorted set
+   * @see https://redis.io/commands/zcard/
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('ZCARD');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZCOUNT.spec.ts b/packages/client/lib/commands/ZCOUNT.spec.ts
index e185ed3cd45..5d279d7a4ca 100644
--- a/packages/client/lib/commands/ZCOUNT.spec.ts
+++ b/packages/client/lib/commands/ZCOUNT.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZCOUNT';
+import ZCOUNT from './ZCOUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('ZCOUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 1),
-            ['ZCOUNT', 'key', '0', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZCOUNT, 'key', 0, 1),
+      ['ZCOUNT', 'key', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.zCount', async client => {
-        assert.equal(
-            await client.zCount('key', 0, 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zCount', async client => {
+    assert.equal(
+      await client.zCount('key', 0, 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZCOUNT.ts b/packages/client/lib/commands/ZCOUNT.ts
index f9700cc9099..0ac473eb710 100644
--- a/packages/client/lib/commands/ZCOUNT.ts
+++ b/packages/client/lib/commands/ZCOUNT.ts
@@ -1,21 +1,29 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: RedisCommandArgument | number,
-    max: RedisCommandArgument | number
-): RedisCommandArguments {
-    return [
-        'ZCOUNT',
-        key,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of elements in the sorted set with a score between min and max.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum score to count from (inclusive).
+   * @param max - Maximum score to count to (inclusive).
+   */
+  parseCommand(
+    parser: CommandParser, 
+    key: RedisArgument,
+    min: number | RedisArgument,
+    max: number | RedisArgument
+  ) {
+    parser.push('ZCOUNT');
+    parser.pushKey(key);
+    parser.push(
+      transformStringDoubleArgument(min), 
+      transformStringDoubleArgument(max)
+    );
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZDIFF.spec.ts b/packages/client/lib/commands/ZDIFF.spec.ts
index 8bb1a101f53..4914df3e978 100644
--- a/packages/client/lib/commands/ZDIFF.spec.ts
+++ b/packages/client/lib/commands/ZDIFF.spec.ts
@@ -1,30 +1,34 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZDIFF';
+import ZDIFF from './ZDIFF';
+import { parseArgs } from './generic-transformers';
 
 describe('ZDIFF', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['ZDIFF', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ZDIFF, 'key'),
+        ['ZDIFF', '1', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZDIFF', '2', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ZDIFF, ['1', '2']),
+        ['ZDIFF', '2', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zDiff', async client => {
-        assert.deepEqual(
-            await client.zDiff('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zDiff', async client => {
+    assert.deepEqual(
+      await client.zDiff('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZDIFF.ts b/packages/client/lib/commands/ZDIFF.ts
index f3818a139f1..f52492c2bca 100644
--- a/packages/client/lib/commands/ZDIFF.ts
+++ b/packages/client/lib/commands/ZDIFF.ts
@@ -1,14 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    keys: Array<RedisCommandArgument> | RedisCommandArgument
-): RedisCommandArguments {
-    return pushVerdictArgument(['ZDIFF'], keys);
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the difference between the first sorted set and all the successive sorted sets.
+   * @param parser - The Redis command parser.
+   * @param keys - Keys of the sorted sets.
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    parser.push('ZDIFF');
+    parser.pushKeysLength(keys);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZDIFFSTORE.spec.ts b/packages/client/lib/commands/ZDIFFSTORE.spec.ts
index c63902b2666..7f380cfc532 100644
--- a/packages/client/lib/commands/ZDIFFSTORE.spec.ts
+++ b/packages/client/lib/commands/ZDIFFSTORE.spec.ts
@@ -1,30 +1,34 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZDIFFSTORE';
+import ZDIFFSTORE from './ZDIFFSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZDIFFSTORE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key'),
-                ['ZDIFFSTORE', 'destination', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ZDIFFSTORE, 'destination', 'key'),
+        ['ZDIFFSTORE', 'destination', '1', 'key']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['ZDIFFSTORE', 'destination', '2', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ZDIFFSTORE, 'destination', ['1', '2']),
+        ['ZDIFFSTORE', 'destination', '2', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zDiffStore', async client => {
-        assert.equal(
-            await client.zDiffStore('destination', 'key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zDiffStore', async client => {
+    assert.equal(
+      await client.zDiffStore('{tag}destination', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZDIFFSTORE.ts b/packages/client/lib/commands/ZDIFFSTORE.ts
index 3b9af9511c5..87407421fb0 100644
--- a/packages/client/lib/commands/ZDIFFSTORE.ts
+++ b/packages/client/lib/commands/ZDIFFSTORE.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    destination: RedisCommandArgument,
-    keys: Array<RedisCommandArgument> | RedisCommandArgument
-): RedisCommandArguments {
-    return pushVerdictArgument(['ZDIFFSTORE', destination], keys);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Computes the difference between the first and all successive sorted sets and stores it in a new key.
+   * @param parser - The Redis command parser.
+   * @param destination - Destination key where the result will be stored.
+   * @param inputKeys - Keys of the sorted sets to find the difference between.
+   */
+  parseCommand(parser: CommandParser, destination: RedisArgument, inputKeys: RedisVariadicArgument) {
+    parser.push('ZDIFFSTORE');
+    parser.pushKey(destination);
+    parser.pushKeysLength(inputKeys);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts
index 3b9cb725aaa..bea639f223e 100644
--- a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts
+++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts
@@ -1,30 +1,34 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZDIFF_WITHSCORES';
+import ZDIFF_WITHSCORES from './ZDIFF_WITHSCORES';
+import { parseArgs } from './generic-transformers';
 
 describe('ZDIFF WITHSCORES', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['ZDIFF', '1', 'key', 'WITHSCORES']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ZDIFF_WITHSCORES, 'key'),
+        ['ZDIFF', '1', 'key', 'WITHSCORES']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZDIFF', '2', '1', '2', 'WITHSCORES']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ZDIFF_WITHSCORES, ['1', '2']),
+        ['ZDIFF', '2', '1', '2', 'WITHSCORES']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zDiffWithScores', async client => {
-        assert.deepEqual(
-            await client.zDiffWithScores('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zDiffWithScores', async client => {
+    assert.deepEqual(
+      await client.zDiffWithScores('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts
index 9caa13c9f8b..6cb661b652a 100644
--- a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts
+++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArguments } from '.';
-import { transformArguments as transformZDiffArguments } from './ZDIFF';
+import { CommandParser } from '../client/parser';
+import { Command } from '../RESP/types';
+import { RedisVariadicArgument, transformSortedSetReply } from './generic-transformers';
+import ZDIFF from './ZDIFF';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZDIFF';
 
-export function transformArguments(...args: Parameters<typeof transformZDiffArguments>): RedisCommandArguments {
-    return [
-        ...transformZDiffArguments(...args),
-        'WITHSCORES'
-    ];
-}
-
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: ZDIFF.IS_READ_ONLY,
+  /**
+   * Returns the difference between the first sorted set and all successive sorted sets with their scores.
+   * @param parser - The Redis command parser.
+   * @param keys - Keys of the sorted sets.
+   */
+  parseCommand(parser: CommandParser, keys: RedisVariadicArgument) {
+    ZDIFF.parseCommand(parser, keys);
+    parser.push('WITHSCORES');
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZINCRBY.spec.ts b/packages/client/lib/commands/ZINCRBY.spec.ts
index bf2a34b0965..8f6c5141252 100644
--- a/packages/client/lib/commands/ZINCRBY.spec.ts
+++ b/packages/client/lib/commands/ZINCRBY.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZINCRBY';
+import ZINCRBY from './ZINCRBY';
+import { parseArgs } from './generic-transformers';
 
 describe('ZINCRBY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1, 'member'),
-            ['ZINCRBY', 'key', '1', 'member']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZINCRBY, 'key', 1, 'member'),
+      ['ZINCRBY', 'key', '1', 'member']
+    );
+  });
 
-    testUtils.testWithClient('client.zIncrBy', async client => {
-        assert.equal(
-            await client.zIncrBy('destination', 1, 'member'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zIncrBy', async client => {
+    assert.equal(
+      await client.zIncrBy('destination', 1, 'member'),
+      1
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZINCRBY.ts b/packages/client/lib/commands/ZINCRBY.ts
index 68d89351391..30692fffb59 100644
--- a/packages/client/lib/commands/ZINCRBY.ts
+++ b/packages/client/lib/commands/ZINCRBY.ts
@@ -1,19 +1,24 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformNumberInfinityArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformDoubleArgument, transformDoubleReply } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  /**
+   * Increments the score of a member in a sorted set by the specified increment.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param increment - Value to increment the score by.
+   * @param member - Member whose score should be incremented.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     increment: number,
-    member: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'ZINCRBY',
-        key,
-        transformNumberInfinityArgument(increment),
-        member
-    ];
-}
-
-export { transformNumberInfinityReply as transformReply } from './generic-transformers';
+    member: RedisArgument
+  ) {
+    parser.push('ZINCRBY');
+    parser.pushKey(key);
+    parser.push(transformDoubleArgument(increment), member);
+  },
+  transformReply: transformDoubleReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZINTER.spec.ts b/packages/client/lib/commands/ZINTER.spec.ts
index 4d2d86c8869..73df0935de9 100644
--- a/packages/client/lib/commands/ZINTER.spec.ts
+++ b/packages/client/lib/commands/ZINTER.spec.ts
@@ -1,58 +1,66 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZINTER';
+import ZINTER from './ZINTER';
+import { parseArgs } from './generic-transformers';
 
 describe('ZINTER', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('key (string)', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['ZINTER', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('key (string)', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER, 'key'),
+        ['ZINTER', '1', 'key']
+      );
+    });
 
-        it('keys (array)', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZINTER', '2', '1', '2']
-            );
-        });
+    it('keys (Array<string>)', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER, ['1', '2']),
+        ['ZINTER', '2', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    WEIGHTS: [1]
-                }),
-                ['ZINTER', '1', 'key', 'WEIGHTS', '1']
-            );
-        });
+    it('key & weight', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER, {
+          key: 'key',
+          weight: 1
+        }),
+        ['ZINTER', '1', 'key', 'WEIGHTS', '1']
+      );
+    });
 
-        it('with AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('keys & weights', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER, [{
+          key: 'a',
+          weight: 1
+        }, {
+          key: 'b',
+          weight: 2
+        }]),
+        ['ZINTER', '2', 'a', 'b', 'WEIGHTS', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS, AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    WEIGHTS: [1],
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('with AGGREGATE', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER, 'key', {
+          AGGREGATE: 'SUM'
+        }),
+        ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zInter', async client => {
-        assert.deepEqual(
-            await client.zInter('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zInter', async client => {
+    assert.deepEqual(
+      await client.zInter('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZINTER.ts b/packages/client/lib/commands/ZINTER.ts
index 88d7f801882..30d6716293e 100644
--- a/packages/client/lib/commands/ZINTER.ts
+++ b/packages/client/lib/commands/ZINTER.ts
@@ -1,33 +1,43 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { ZKeys, parseZKeysArguments } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
+export type ZInterKeyAndWeight = {
+  key: RedisArgument;
+  weight: number;
+};
 
-export const IS_READ_ONLY = true;
+export type ZInterKeys<T> = T | [T, ...Array<T>];
 
-interface ZInterOptions {
-    WEIGHTS?: Array<number>;
-    AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
-}
-
-export function transformArguments(
-    keys: Array<RedisCommandArgument> | RedisCommandArgument,
-    options?: ZInterOptions
-): RedisCommandArguments {
-    const args = pushVerdictArgument(['ZINTER'], keys);
+export type ZInterKeysType = ZInterKeys<RedisArgument> | ZInterKeys<ZInterKeyAndWeight>;
 
-    if (options?.WEIGHTS) {
-        args.push(
-            'WEIGHTS',
-            ...options.WEIGHTS.map(weight => weight.toString())
-        );
-    }
+export interface ZInterOptions {
+  AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
+}
 
-    if (options?.AGGREGATE) {
-        args.push('AGGREGATE', options.AGGREGATE);
-    }
+export function parseZInterArguments(
+  parser: CommandParser,
+  keys: ZKeys,
+  options?: ZInterOptions
+) {
+  parseZKeysArguments(parser, keys);
 
-    return args;
+  if (options?.AGGREGATE) {
+    parser.push('AGGREGATE', options.AGGREGATE);
+  }
 }
 
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Intersects multiple sorted sets and returns the result as a new sorted set.
+   * @param parser - The Redis command parser.
+   * @param keys - Keys of the sorted sets to intersect.
+   * @param options - Optional parameters for the intersection operation.
+   */
+  parseCommand(parser: CommandParser, keys: ZInterKeysType, options?: ZInterOptions) {
+    parser.push('ZINTER');
+    parseZInterArguments(parser, keys, options);
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZINTERCARD.spec.ts b/packages/client/lib/commands/ZINTERCARD.spec.ts
index 492c1a90433..5204872a2d0 100644
--- a/packages/client/lib/commands/ZINTERCARD.spec.ts
+++ b/packages/client/lib/commands/ZINTERCARD.spec.ts
@@ -1,30 +1,45 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZINTERCARD';
+import ZINTERCARD from './ZINTERCARD';
+import { parseArgs } from './generic-transformers';
 
 describe('ZINTERCARD', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZINTERCARD', '2', '1', '2']
-            );
-        });
-
-        it('with limit', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2'], 1),
-                ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZINTERCARD, ['1', '2']),
+        ['ZINTERCARD', '2', '1', '2']
+      );
     });
 
-    testUtils.testWithClient('client.zInterCard', async client => {
+    describe('with LIMIT', () => {
+      it('plain number (backwards compatibility)', () => {
+        assert.deepEqual(
+          parseArgs(ZINTERCARD, ['1', '2'], 1),
+          ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1']
+        );
+      });
+
+      it('{ LIMIT: number }', () => {
         assert.deepEqual(
-            await client.zInterCard('key'),
-            0
+          parseArgs(ZINTERCARD, ['1', '2'], {
+            LIMIT: 1
+          }),
+          ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1']
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
+    });
+  });
+
+  testUtils.testAll('zInterCard', async client => {
+    assert.deepEqual(
+      await client.zInterCard('key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZINTERCARD.ts b/packages/client/lib/commands/ZINTERCARD.ts
index ff45ab2aa01..7673b0f0a69 100644
--- a/packages/client/lib/commands/ZINTERCARD.ts
+++ b/packages/client/lib/commands/ZINTERCARD.ts
@@ -1,21 +1,33 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
+export interface ZInterCardOptions {
+  LIMIT?: number;
+}
 
-export function transformArguments(
-    keys: Array<RedisCommandArgument> | RedisCommandArgument,
-    limit?: number
-): RedisCommandArguments {
-    const args = pushVerdictArgument(['ZINTERCARD'], keys);
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the cardinality of the intersection of multiple sorted sets.
+   * @param parser - The Redis command parser.
+   * @param keys - Keys of the sorted sets to intersect.
+   * @param options - Limit option or options object with limit.
+   */
+  parseCommand(
+    parser: CommandParser,
+    keys: RedisVariadicArgument,
+    options?: ZInterCardOptions['LIMIT'] | ZInterCardOptions
+  ) {
+    parser.push('ZINTERCARD');
+    parser.pushKeysLength(keys);
 
-    if (limit) {
-        args.push('LIMIT', limit.toString());
+    // backwards compatibility
+    if (typeof options === 'number') {
+      parser.push('LIMIT', options.toString());
+    } else if (options?.LIMIT) {
+      parser.push('LIMIT', options.LIMIT.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZINTERSTORE.spec.ts b/packages/client/lib/commands/ZINTERSTORE.spec.ts
index 224961f0786..c6b448ab908 100644
--- a/packages/client/lib/commands/ZINTERSTORE.spec.ts
+++ b/packages/client/lib/commands/ZINTERSTORE.spec.ts
@@ -1,56 +1,64 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZINTERSTORE';
+import ZINTERSTORE from './ZINTERSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZINTERSTORE', () => {
-    describe('transformArguments', () => {
-        it('key (string)', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key'),
-                ['ZINTERSTORE', 'destination', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('key (string)', () => {
+      assert.deepEqual(
+        parseArgs(ZINTERSTORE, 'destination', 'source'),
+        ['ZINTERSTORE', 'destination', '1', 'source']
+      );
+    });
 
-        it('keys (array)', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['ZINTERSTORE', 'destination', '2', '1', '2']
-            );
-        });
+    it('keys (Array<string>)', () => {
+      assert.deepEqual(
+        parseArgs(ZINTERSTORE, 'destination', ['1', '2']),
+        ['ZINTERSTORE', 'destination', '2', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key', {
-                    WEIGHTS: [1]
-                }),
-                ['ZINTERSTORE', 'destination', '1', 'key', 'WEIGHTS', '1']
-            );
-        });
+    it('key & weight', () => {
+      assert.deepEqual(
+        parseArgs(ZINTERSTORE, 'destination', {
+          key: 'source',
+          weight: 1
+        }),
+        ['ZINTERSTORE', 'destination', '1', 'source', 'WEIGHTS', '1']
+      );
+    });
 
-        it('with AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key', {
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZINTERSTORE', 'destination', '1', 'key', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('keys & weights', () => {
+      assert.deepEqual(
+        parseArgs(ZINTERSTORE, 'destination', [{
+          key: 'a',
+          weight: 1
+        }, {
+          key: 'b',
+          weight: 2
+        }]),
+        ['ZINTERSTORE', 'destination', '2', 'a', 'b', 'WEIGHTS', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS, AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key', {
-                    WEIGHTS: [1],
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZINTERSTORE', 'destination', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('with AGGREGATE', () => {
+      assert.deepEqual(
+        parseArgs(ZINTERSTORE, 'destination', 'source', {
+          AGGREGATE: 'SUM'
+        }),
+        ['ZINTERSTORE', 'destination', '1', 'source', 'AGGREGATE', 'SUM']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zInterStore', async client => {
-        assert.equal(
-            await client.zInterStore('destination', 'key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zInterStore', async client => {
+    assert.equal(
+      await client.zInterStore('{tag}destination', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZINTERSTORE.ts b/packages/client/lib/commands/ZINTERSTORE.ts
index 540f10ae2d8..1405b70287b 100644
--- a/packages/client/lib/commands/ZINTERSTORE.ts
+++ b/packages/client/lib/commands/ZINTERSTORE.ts
@@ -1,32 +1,27 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { ZKeys } from './generic-transformers';
+import { parseZInterArguments, ZInterOptions } from './ZINTER';
 
-interface ZInterStoreOptions {
-    WEIGHTS?: Array<number>;
-    AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
-}
-
-export function transformArguments(
-    destination: RedisCommandArgument,
-    keys: Array<RedisCommandArgument> | RedisCommandArgument,
-    options?: ZInterStoreOptions
-): RedisCommandArguments {
-    const args = pushVerdictArgument(['ZINTERSTORE', destination], keys);
-
-    if (options?.WEIGHTS) {
-        args.push(
-            'WEIGHTS',
-            ...options.WEIGHTS.map(weight => weight.toString())
-        );
-    }
-
-    if (options?.AGGREGATE) {
-        args.push('AGGREGATE', options.AGGREGATE);
-    }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Stores the result of intersection of multiple sorted sets in a new sorted set.
+   * @param parser - The Redis command parser.
+   * @param destination - Destination key where the result will be stored.
+   * @param keys - Keys of the sorted sets to intersect.
+   * @param options - Optional parameters for the intersection operation.
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    keys: ZKeys,
+    options?: ZInterOptions
+  ) {
+    parser.push('ZINTERSTORE');
+    parser.pushKey(destination);
+    parseZInterArguments(parser, keys, options);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts
index 0eaeb26a244..234b250b143 100644
--- a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts
+++ b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts
@@ -1,58 +1,66 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZINTER_WITHSCORES';
+import ZINTER_WITHSCORES from './ZINTER_WITHSCORES';
+import { parseArgs } from './generic-transformers';
 
 describe('ZINTER WITHSCORES', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('key (string)', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['ZINTER', '1', 'key', 'WITHSCORES']
-            );
-        });
+  describe('transformArguments', () => {
+    it('key (string)', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER_WITHSCORES, 'key'),
+        ['ZINTER', '1', 'key', 'WITHSCORES']
+      );
+    });
 
-        it('keys (array)', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZINTER', '2', '1', '2', 'WITHSCORES']
-            );
-        });
+    it('keys (Array<string>)', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER_WITHSCORES, ['1', '2']),
+        ['ZINTER', '2', '1', '2', 'WITHSCORES']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    WEIGHTS: [1]
-                }),
-                ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES']
-            );
-        });
+    it('key & weight', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER_WITHSCORES, {
+          key: 'key',
+          weight: 1
+        }),
+        ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES']
+      );
+    });
 
-        it('with AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES']
-            );
-        });
+    it('keys & weights', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER_WITHSCORES, [{
+          key: 'a',
+          weight: 1
+        }, {
+          key: 'b',
+          weight: 2
+        }]),
+        ['ZINTER', '2', 'a', 'b', 'WEIGHTS', '1', '2', 'WITHSCORES']
+      );
+    });
 
-        it('with WEIGHTS, AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    WEIGHTS: [1],
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM', 'WITHSCORES']
-            );
-        });
+    it('with AGGREGATE', () => {
+      assert.deepEqual(
+        parseArgs(ZINTER_WITHSCORES, 'key', {
+          AGGREGATE: 'SUM'
+        }),
+        ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zInterWithScores', async client => {
-        assert.deepEqual(
-            await client.zInterWithScores('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zInterWithScores', async client => {
+    assert.deepEqual(
+      await client.zInterWithScores('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.ts
index c9416e9222a..40ba3ce4287 100644
--- a/packages/client/lib/commands/ZINTER_WITHSCORES.ts
+++ b/packages/client/lib/commands/ZINTER_WITHSCORES.ts
@@ -1,13 +1,17 @@
-import { RedisCommandArguments } from '.';
-import { transformArguments as transformZInterArguments } from './ZINTER';
+import { Command } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
+import ZINTER from './ZINTER';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZINTER';
 
-export function transformArguments(...args: Parameters<typeof transformZInterArguments>): RedisCommandArguments {
-    return [
-        ...transformZInterArguments(...args),
-        'WITHSCORES'
-    ];
-}
-
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: ZINTER.IS_READ_ONLY,
+  /**
+   * Intersects multiple sorted sets and returns the result with scores.
+   * @param args - Same parameters as ZINTER command.
+   */
+  parseCommand(...args: Parameters<typeof ZINTER.parseCommand>) {
+    ZINTER.parseCommand(...args);
+    args[0].push('WITHSCORES');
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZLEXCOUNT.spec.ts b/packages/client/lib/commands/ZLEXCOUNT.spec.ts
index 85809f1a9a9..78c7411affd 100644
--- a/packages/client/lib/commands/ZLEXCOUNT.spec.ts
+++ b/packages/client/lib/commands/ZLEXCOUNT.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZLEXCOUNT';
+import ZLEXCOUNT from './ZLEXCOUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('ZLEXCOUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '[a', '[b'),
-            ['ZLEXCOUNT', 'key', '[a', '[b']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZLEXCOUNT, 'key', '[a', '[b'),
+      ['ZLEXCOUNT', 'key', '[a', '[b']
+    );
+  });
 
-    testUtils.testWithClient('client.zLexCount', async client => {
-        assert.equal(
-            await client.zLexCount('key', '[a', '[b'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zLexCount', async client => {
+    assert.equal(
+      await client.zLexCount('key', '[a', '[b'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZLEXCOUNT.ts b/packages/client/lib/commands/ZLEXCOUNT.ts
index e2fbcdbb42b..97bed9f6014 100644
--- a/packages/client/lib/commands/ZLEXCOUNT.ts
+++ b/packages/client/lib/commands/ZLEXCOUNT.ts
@@ -1,20 +1,26 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: RedisCommandArgument,
-    max: RedisCommandArgument
-): RedisCommandArguments {
-    return [
-        'ZLEXCOUNT',
-        key,
-        min,
-        max
-    ];
-}
-
-export declare function transformReply(): number;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of elements in the sorted set between the lexicographical range specified by min and max.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum lexicographical value (inclusive).
+   * @param max - Maximum lexicographical value (inclusive).
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    min: RedisArgument,
+    max: RedisArgument
+  ) {
+    parser.push('ZLEXCOUNT');
+    parser.pushKey(key);
+    parser.push(min);
+    parser.push(max);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZMPOP.spec.ts b/packages/client/lib/commands/ZMPOP.spec.ts
index 9a0c9676c20..c15a53b7313 100644
--- a/packages/client/lib/commands/ZMPOP.spec.ts
+++ b/packages/client/lib/commands/ZMPOP.spec.ts
@@ -1,32 +1,56 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZMPOP';
+import ZMPOP from './ZMPOP';
+import { parseArgs } from './generic-transformers';
 
 describe('ZMPOP', () => {
-    testUtils.isVersionGreaterThanHook([7]);
+  testUtils.isVersionGreaterThanHook([7]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', 'MIN'),
-                ['ZMPOP', '1', 'key', 'MIN']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZMPOP, 'key', 'MIN'),
+        ['ZMPOP', '1', 'key', 'MIN']
+      );
+    });
 
-        it('with score and count', () => {
-            assert.deepEqual(
-                transformArguments('key', 'MIN', {
-                    COUNT: 2
-                }),
-                ['ZMPOP', '1', 'key', 'MIN', 'COUNT', '2']
-            );
-        });
+    it('with count', () => {
+      assert.deepEqual(
+        parseArgs(ZMPOP, 'key', 'MIN', {
+          COUNT: 2
+        }),
+        ['ZMPOP', '1', 'key', 'MIN', 'COUNT', '2']
+      );
     });
+  });
+
+  testUtils.testAll('zmPop - null', async client => {
+    assert.equal(
+      await client.zmPop('key', 'MIN'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('zmPop - with members', async client => {
+    const members = [{
+      value: '1',
+      score: 1
+    }];
 
-    testUtils.testWithClient('client.zmPop', async client => {
-        assert.deepEqual(
-            await client.zmPop('key', 'MIN'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    const [, reply] = await Promise.all([
+      client.zAdd('key', members),
+      client.zmPop('key', 'MIN')
+    ]);
+
+    assert.deepEqual(reply, {
+      key: 'key',
+      members
+    });
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZMPOP.ts b/packages/client/lib/commands/ZMPOP.ts
index 0baa46bbf0b..fe766ddd13a 100644
--- a/packages/client/lib/commands/ZMPOP.ts
+++ b/packages/client/lib/commands/ZMPOP.ts
@@ -1,34 +1,72 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { SortedSetSide, transformSortedSetMemberReply, transformZMPopArguments, ZMember, ZMPopOptions } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NullReply, TuplesReply, BlobStringReply, DoubleReply, ArrayReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types';
+import { RedisVariadicArgument, SortedSetSide, transformSortedSetReply, transformDoubleReply, Tail } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
-
-export function transformArguments(
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    side: SortedSetSide,
-    options?: ZMPopOptions
-): RedisCommandArguments {
-    return transformZMPopArguments(
-        ['ZMPOP'],
-        keys,
-        side,
-        options
-    );
+export interface ZMPopOptions {
+  COUNT?: number;
 }
 
-type ZMPopRawReply = null | [
-    key: string,
-    elements: Array<[RedisCommandArgument, RedisCommandArgument]>
-];
+export type ZMPopRawReply = NullReply | TuplesReply<[
+  key: BlobStringReply,
+  members: ArrayReply<TuplesReply<[
+    value: BlobStringReply,
+    score: DoubleReply
+  ]>>
+]>;
 
-type ZMPopReply = null | {
-    key: string,
-    elements: Array<ZMember>
-};
+export function parseZMPopArguments(
+  parser: CommandParser,
+  keys: RedisVariadicArgument,
+  side: SortedSetSide,
+  options?: ZMPopOptions
+) {
+  parser.pushKeysLength(keys);
 
-export function transformReply(reply: ZMPopRawReply): ZMPopReply {
-    return reply === null ? null : {
-        key: reply[0],
-        elements: reply[1].map(transformSortedSetMemberReply)
-    };
+  parser.push(side);
+
+  if (options?.COUNT) {
+    parser.push('COUNT', options.COUNT.toString());
+  }
 }
+
+export type ZMPopArguments = Tail<Parameters<typeof parseZMPopArguments>>;
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns up to count members with the highest/lowest scores from the first non-empty sorted set.
+   * @param parser - The Redis command parser.
+   * @param keys - Keys of the sorted sets to pop from.
+   * @param side - Side to pop from (MIN or MAX).
+   * @param options - Optional parameters including COUNT.
+   */
+  parseCommand(
+    parser: CommandParser,
+    keys: RedisVariadicArgument,
+    side: SortedSetSide,
+    options?: ZMPopOptions
+  ) {
+    parser.push('ZMPOP');
+    parseZMPopArguments(parser, keys, side, options)
+  },
+  transformReply: {
+    2(reply: UnwrapReply<Resp2Reply<ZMPopRawReply>>, preserve?: any, typeMapping?: TypeMapping) {
+      return reply === null ? null : {
+        key: reply[0],
+        members: (reply[1] as unknown as UnwrapReply<typeof reply[1]>).map(member => {
+          const [value, score] = member as unknown as UnwrapReply<typeof member>;
+          return {
+            value,
+            score: transformDoubleReply[2](score, preserve, typeMapping)
+          };
+        })
+      };
+    },
+    3(reply: UnwrapReply<ZMPopRawReply>) {
+      return reply === null ? null : {
+        key: reply[0],
+        members: transformSortedSetReply[3](reply[1])
+      };
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZMSCORE.spec.ts b/packages/client/lib/commands/ZMSCORE.spec.ts
index 228c8e9d6f6..6c6d2946e00 100644
--- a/packages/client/lib/commands/ZMSCORE.spec.ts
+++ b/packages/client/lib/commands/ZMSCORE.spec.ts
@@ -1,30 +1,34 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZMSCORE';
+import ZMSCORE from './ZMSCORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZMSCORE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member'),
-                ['ZMSCORE', 'key', 'member']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ZMSCORE, 'key', 'member'),
+        ['ZMSCORE', 'key', 'member']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['ZMSCORE', 'key', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ZMSCORE, 'key', ['1', '2']),
+        ['ZMSCORE', 'key', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zmScore', async client => {
-        assert.deepEqual(
-            await client.zmScore('key', 'member'),
-            [null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zmScore', async client => {
+    assert.deepEqual(
+      await client.zmScore('key', 'member'),
+      [null]
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZMSCORE.ts b/packages/client/lib/commands/ZMSCORE.ts
index 6c8c9dace31..0275e8d98db 100644
--- a/packages/client/lib/commands/ZMSCORE.ts
+++ b/packages/client/lib/commands/ZMSCORE.ts
@@ -1,15 +1,25 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, NullReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
+import { createTransformNullableDoubleReplyResp2Func, RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['ZMSCORE', key], member);
-}
-
-export { transformNumberInfinityNullArrayReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the scores associated with the specified members in the sorted set stored at key.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param member - One or more members to get scores for.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) {
+    parser.push('ZMSCORE');
+    parser.pushKey(key);
+    parser.pushVariadic(member);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<ArrayReply<NullReply | BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      return reply.map(createTransformNullableDoubleReplyResp2Func(preserve, typeMapping));
+    },
+    3: undefined as unknown as () => ArrayReply<NullReply | DoubleReply>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZPOPMAX.spec.ts b/packages/client/lib/commands/ZPOPMAX.spec.ts
index 18fba23a3e9..1796647df86 100644
--- a/packages/client/lib/commands/ZPOPMAX.spec.ts
+++ b/packages/client/lib/commands/ZPOPMAX.spec.ts
@@ -1,41 +1,40 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './ZPOPMAX';
+import ZPOPMAX from './ZPOPMAX';
+import { parseArgs } from './generic-transformers';
 
 describe('ZPOPMAX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['ZPOPMAX', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZPOPMAX, 'key'),
+      ['ZPOPMAX', 'key']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply(['value', '1']),
-            {
-                value: 'value',
-                score: 1
-            }
-        );
-    });
+  testUtils.testAll('zPopMax - null', async client => {
+    assert.equal(
+      await client.zPopMax('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 
-    describe('client.zPopMax', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.zPopMax('key'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zPopMax - with member', async client => {
+    const member = {
+      value: 'value',
+      score: 1
+    };
 
-        testUtils.testWithClient('member', async client => {
-            const member = { score: 1, value: 'value' },
-                [, zPopMaxReply] = await Promise.all([
-                    client.zAdd('key', member),
-                    client.zPopMax('key')
-                ]);
+    const [, reply] = await Promise.all([
+      client.zAdd('key', member),
+      client.zPopMax('key')
+    ]);
 
-            assert.deepEqual(zPopMaxReply, member);
-        }, GLOBAL.SERVERS.OPEN);
-    });
+    assert.deepEqual(reply, member);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZPOPMAX.ts b/packages/client/lib/commands/ZPOPMAX.ts
index 811166a690c..fd7b7cf9f94 100644
--- a/packages/client/lib/commands/ZPOPMAX.ts
+++ b/packages/client/lib/commands/ZPOPMAX.ts
@@ -1,12 +1,34 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
+import { transformDoubleReply } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns the member with the highest score in the sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('ZPOPMAX');
+    parser.pushKey(key);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<TuplesReply<[] | [BlobStringReply, BlobStringReply]>>, preserve?: any, typeMapping?: TypeMapping) => {
+      if (reply.length === 0) return null;
 
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return [
-        'ZPOPMAX',
-        key
-    ];
-}
+      return {
+        value: reply[0],
+        score: transformDoubleReply[2](reply[1], preserve, typeMapping),
+      };
+    },
+    3: (reply: UnwrapReply<TuplesReply<[] | [BlobStringReply, DoubleReply]>>) => {
+      if (reply.length === 0) return null;
 
-export { transformSortedSetMemberNullReply as transformReply } from './generic-transformers';
+      return {
+        value: reply[0],
+        score: reply[1]
+      };
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts
index b282d0d3199..dd9d85dbd36 100644
--- a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts
+++ b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts
@@ -1,19 +1,33 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZPOPMAX_COUNT';
+import ZPOPMAX_COUNT from './ZPOPMAX_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('ZPOPMAX COUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['ZPOPMAX', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZPOPMAX_COUNT, 'key', 1),
+      ['ZPOPMAX', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.zPopMaxCount', async client => {
-        assert.deepEqual(
-            await client.zPopMaxCount('key', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zPopMaxCount', async client => {
+    const members = [{
+      value: '1',
+      score: 1
+    }, {
+      value: '2',
+      score: 2
+    }];
+
+    const [ , reply] = await Promise.all([
+      client.zAdd('key', members),
+      client.zPopMaxCount('key', members.length)
+    ]);
+
+    assert.deepEqual(reply, members.reverse());
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.ts
index 875bcfb9147..50f347acf3e 100644
--- a/packages/client/lib/commands/ZPOPMAX_COUNT.ts
+++ b/packages/client/lib/commands/ZPOPMAX_COUNT.ts
@@ -1,16 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformZPopMaxArguments } from './ZPOPMAX';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
 
-export { FIRST_KEY_INDEX } from './ZPOPMAX';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return [
-        ...transformZPopMaxArguments(key),
-        count.toString()
-    ];
-}
-
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns up to count members with the highest scores in the sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param count - Number of members to pop.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    parser.push('ZPOPMAX');
+    parser.pushKey(key);
+    parser.push(count.toString());
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZPOPMIN.spec.ts b/packages/client/lib/commands/ZPOPMIN.spec.ts
index 624b7054404..653a4e70a92 100644
--- a/packages/client/lib/commands/ZPOPMIN.spec.ts
+++ b/packages/client/lib/commands/ZPOPMIN.spec.ts
@@ -1,41 +1,40 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './ZPOPMIN';
+import ZPOPMIN from './ZPOPMIN';
+import { parseArgs } from './generic-transformers';
 
 describe('ZPOPMIN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['ZPOPMIN', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZPOPMIN, 'key'),
+      ['ZPOPMIN', 'key']
+    );
+  });
 
-    it('transformReply', () => {
-        assert.deepEqual(
-            transformReply(['value', '1']),
-            {
-                value: 'value',
-                score: 1
-            }
-        );
-    });
+  testUtils.testAll('zPopMin - null', async client => {
+    assert.equal(
+      await client.zPopMin('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 
-    describe('client.zPopMin', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.zPopMin('key'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zPopMax - with member', async client => {
+    const member = {
+      value: 'value',
+      score: 1
+    };
 
-        testUtils.testWithClient('member', async client => {
-            const member = { score: 1, value: 'value' },
-                [, zPopMinReply] = await Promise.all([
-                    client.zAdd('key', member),
-                    client.zPopMin('key')
-                ]);
+    const [, reply] = await Promise.all([
+      client.zAdd('key', member),
+      client.zPopMin('key')
+    ]);
 
-            assert.deepEqual(zPopMinReply, member);
-        }, GLOBAL.SERVERS.OPEN);
-    });
+    assert.deepEqual(reply, member);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZPOPMIN.ts b/packages/client/lib/commands/ZPOPMIN.ts
index 053ffd2d2ce..2de4977da7f 100644
--- a/packages/client/lib/commands/ZPOPMIN.ts
+++ b/packages/client/lib/commands/ZPOPMIN.ts
@@ -1,12 +1,17 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import ZPOPMAX from './ZPOPMAX';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return [
-        'ZPOPMIN',
-        key
-    ];
-}
-
-export { transformSortedSetMemberNullReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns the member with the lowest score in the sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('ZPOPMIN');
+    parser.pushKey(key);
+  },
+  transformReply: ZPOPMAX.transformReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts
index 6d40002ab72..126a3cc1e9a 100644
--- a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts
+++ b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts
@@ -1,19 +1,33 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZPOPMIN_COUNT';
+import ZPOPMIN_COUNT from './ZPOPMIN_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('ZPOPMIN COUNT', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['ZPOPMIN', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZPOPMIN_COUNT, 'key', 1),
+      ['ZPOPMIN', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.zPopMinCount', async client => {
-        assert.deepEqual(
-            await client.zPopMinCount('key', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zPopMinCount', async client => {
+    const members = [{
+      value: '1',
+      score: 1
+    }, {
+      value: '2',
+      score: 2
+    }];
+
+    const [ , reply] = await Promise.all([
+      client.zAdd('key', members),
+      client.zPopMinCount('key', members.length)
+    ]);
+
+    assert.deepEqual(reply, members);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.SERVERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.ts
index 54125ade0ac..24e084b2aef 100644
--- a/packages/client/lib/commands/ZPOPMIN_COUNT.ts
+++ b/packages/client/lib/commands/ZPOPMIN_COUNT.ts
@@ -1,16 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformZPopMinArguments } from './ZPOPMIN';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
 
-export { FIRST_KEY_INDEX } from './ZPOPMIN';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return [
-        ...transformZPopMinArguments(key),
-        count.toString()
-    ];
-}
-
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns up to count members with the lowest scores in the sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param count - Number of members to pop.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    parser.push('ZPOPMIN');
+    parser.pushKey(key);
+    parser.push(count.toString());
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANDMEMBER.spec.ts b/packages/client/lib/commands/ZRANDMEMBER.spec.ts
index c57d26f830e..a25ea79f8e1 100644
--- a/packages/client/lib/commands/ZRANDMEMBER.spec.ts
+++ b/packages/client/lib/commands/ZRANDMEMBER.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANDMEMBER';
+import ZRANDMEMBER from './ZRANDMEMBER';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANDMEMBER', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['ZRANDMEMBER', 'key']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZRANDMEMBER, 'key'),
+      ['ZRANDMEMBER', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.zRandMember', async client => {
-        assert.equal(
-            await client.zRandMember('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRandMember', async client => {
+    assert.equal(
+      await client.zRandMember('key'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANDMEMBER.ts b/packages/client/lib/commands/ZRANDMEMBER.ts
index 00420872c0c..ed0a529da5e 100644
--- a/packages/client/lib/commands/ZRANDMEMBER.ts
+++ b/packages/client/lib/commands/ZRANDMEMBER.ts
@@ -1,11 +1,16 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: RedisCommandArgument): RedisCommandArguments {
-    return ['ZRANDMEMBER', key];
-}
-
-export declare function transformReply(): RedisCommandArgument | null;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns a random member from a sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('ZRANDMEMBER');
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => BlobStringReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts
index 10db0727b23..eee0d454975 100644
--- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts
+++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANDMEMBER_COUNT';
+import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANDMEMBER COUNT', () => {
-    testUtils.isVersionGreaterThanHook([6, 2, 5]);
+  testUtils.isVersionGreaterThanHook([6, 2, 5]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['ZRANDMEMBER', 'key', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZRANDMEMBER_COUNT, 'key', 1),
+      ['ZRANDMEMBER', 'key', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.zRandMemberCount', async client => {
-        assert.deepEqual(
-            await client.zRandMemberCount('key', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRandMemberCount', async client => {
+    assert.deepEqual(
+      await client.zRandMemberCount('key', 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts
index 3aa91902c62..f201f9c236a 100644
--- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts
+++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts
@@ -1,16 +1,18 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformArguments as transformZRandMemberArguments } from './ZRANDMEMBER';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import ZRANDMEMBER from './ZRANDMEMBER';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER';
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    count: number
-): RedisCommandArguments {
-    return [
-        ...transformZRandMemberArguments(key),
-        count.toString()
-    ];
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  IS_READ_ONLY: ZRANDMEMBER.IS_READ_ONLY,
+  /**
+   * Returns one or more random members from a sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param count - Number of members to return.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    ZRANDMEMBER.parseCommand(parser, key);
+    parser.push(count.toString());
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts
index 5b5ec1f500f..3be3b92aeef 100644
--- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts
+++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts
@@ -1,21 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANDMEMBER_COUNT_WITHSCORES';
+import ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANDMEMBER COUNT WITHSCORES', () => {
-    testUtils.isVersionGreaterThanHook([6, 2, 5]);
+  testUtils.isVersionGreaterThanHook([6, 2, 5]);
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 1),
-            ['ZRANDMEMBER', 'key', '1', 'WITHSCORES']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZRANDMEMBER_COUNT_WITHSCORES, 'key', 1),
+      ['ZRANDMEMBER', 'key', '1', 'WITHSCORES']
+    );
+  });
 
-    testUtils.testWithClient('client.zRandMemberCountWithScores', async client => {
-        assert.deepEqual(
-            await client.zRandMemberCountWithScores('key', 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRandMemberCountWithScores', async client => {
+    assert.deepEqual(
+      await client.zRandMemberCountWithScores('key', 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts
index cc9d2bc26ee..3792bce794f 100644
--- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts
+++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArguments } from '.';
-import { transformArguments as transformZRandMemberCountArguments } from './ZRANDMEMBER_COUNT';
+import { CommandParser } from '../client/parser';
+import { Command, RedisArgument } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
+import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER_COUNT';
-
-export function transformArguments(...args: Parameters<typeof transformZRandMemberCountArguments>): RedisCommandArguments {
-    return [
-        ...transformZRandMemberCountArguments(...args),
-        'WITHSCORES'
-    ];
-}
-
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+export default {
+  IS_READ_ONLY: ZRANDMEMBER_COUNT.IS_READ_ONLY,
+  /**
+   * Returns one or more random members with their scores from a sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param count - Number of members to return.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, count: number) {
+    ZRANDMEMBER_COUNT.parseCommand(parser, key, count);
+    parser.push('WITHSCORES');
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANGE.spec.ts b/packages/client/lib/commands/ZRANGE.spec.ts
index a280aff0033..a780e4ef613 100644
--- a/packages/client/lib/commands/ZRANGE.spec.ts
+++ b/packages/client/lib/commands/ZRANGE.spec.ts
@@ -1,74 +1,78 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANGE';
+import ZRANGE from './ZRANGE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANGE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1),
-                ['ZRANGE', 'src', '0', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE, 'src', 0, 1),
+        ['ZRANGE', 'src', '0', '1']
+      );
+    });
 
-        it('with BYSCORE', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    BY: 'SCORE'
-                }),
-                ['ZRANGE', 'src', '0', '1', 'BYSCORE']
-            );
-        });
+    it('with BYSCORE', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE, 'src', 0, 1, {
+          BY: 'SCORE'
+        }),
+        ['ZRANGE', 'src', '0', '1', 'BYSCORE']
+      );
+    });
 
-        it('with BYLEX', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    BY: 'LEX'
-                }),
-                ['ZRANGE', 'src', '0', '1', 'BYLEX']
-            );
-        });
+    it('with BYLEX', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE, 'src', 0, 1, {
+          BY: 'LEX'
+        }),
+        ['ZRANGE', 'src', '0', '1', 'BYLEX']
+      );
+    });
 
-        it('with REV', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    REV: true
-                }),
-                ['ZRANGE', 'src', '0', '1', 'REV']
-            );
-        });
+    it('with REV', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE, 'src', 0, 1, {
+          REV: true
+        }),
+        ['ZRANGE', 'src', '0', '1', 'REV']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGE', 'src', '0', '1', 'LIMIT', '0', '1']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE, 'src', 0, 1, {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGE', 'src', '0', '1', 'LIMIT', '0', '1']
+      );
+    });
 
-        it('with BY & REV & LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    BY: 'SCORE',
-                    REV: true,
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1']
-            );
-        });
+    it('with BY & REV & LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE, 'src', 0, 1, {
+          BY: 'SCORE',
+          REV: true,
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zRange', async client => {
-        assert.deepEqual(
-            await client.zRange('src', 0, 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRange', async client => {
+    assert.deepEqual(
+      await client.zRange('src', 0, 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANGE.ts b/packages/client/lib/commands/ZRANGE.ts
index 83f09aaa1b0..43801289bde 100644
--- a/packages/client/lib/commands/ZRANGE.ts
+++ b/packages/client/lib/commands/ZRANGE.ts
@@ -1,51 +1,72 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export interface ZRangeOptions {
+  BY?: 'SCORE' | 'LEX';
+  REV?: boolean;
+  LIMIT?: {
+    offset: number;
+    count: number;
+  };
+}
 
-export const IS_READ_ONLY = true;
+export function zRangeArgument(
+  min: RedisArgument | number,
+  max: RedisArgument | number,
+  options?: ZRangeOptions
+) {
+  const args = [
+    transformStringDoubleArgument(min),
+    transformStringDoubleArgument(max)
+  ]
 
-interface ZRangeOptions {
-    BY?: 'SCORE' | 'LEX';
-    REV?: true;
-    LIMIT?: {
-        offset: number;
-        count: number;
-    };
-}
+  switch (options?.BY) {
+    case 'SCORE':
+      args.push('BYSCORE');
+      break;
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: RedisCommandArgument | number,
-    max: RedisCommandArgument | number,
-    options?: ZRangeOptions
-): RedisCommandArguments {
-    const args = [
-        'ZRANGE',
-        key,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
-
-    switch (options?.BY) {
-        case 'SCORE':
-            args.push('BYSCORE');
-            break;
-
-        case 'LEX':
-            args.push('BYLEX');
-            break;
-    }
-
-    if (options?.REV) {
-        args.push('REV');
-    }
-
-    if (options?.LIMIT) {
-        args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
-    }
-
-    return args;
+    case 'LEX':
+      args.push('BYLEX');
+      break;
+  }
+
+  if (options?.REV) {
+    args.push('REV');
+  }
+
+  if (options?.LIMIT) {
+    args.push(
+      'LIMIT',
+      options.LIMIT.offset.toString(),
+      options.LIMIT.count.toString()
+    );
+  }
+
+  return args;
 }
 
-export declare function transformReply(): Array<RedisCommandArgument>;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the specified range of elements in the sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum index, score or lexicographical value.
+   * @param max - Maximum index, score or lexicographical value.
+   * @param options - Optional parameters for range retrieval (BY, REV, LIMIT).
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    min: RedisArgument | number,
+    max: RedisArgument | number,
+    options?: ZRangeOptions
+  ) {
+    parser.push('ZRANGE');
+    parser.pushKey(key);
+    parser.pushVariadic(zRangeArgument(min, max, options))
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts
index fe7b7d5a16e..942e184661a 100644
--- a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts
+++ b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts
@@ -1,33 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANGEBYLEX';
+import ZRANGEBYLEX from './ZRANGEBYLEX';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANGEBYLEX', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('src', '-', '+'),
-                ['ZRANGEBYLEX', 'src', '-', '+']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGEBYLEX, 'src', '-', '+'),
+        ['ZRANGEBYLEX', 'src', '-', '+']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', '-', '+', {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGEBYLEX', 'src', '-', '+', 'LIMIT', '0', '1']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGEBYLEX, 'src', '-', '+', {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGEBYLEX', 'src', '-', '+', 'LIMIT', '0', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zRangeByLex', async client => {
-        assert.deepEqual(
-            await client.zRangeByLex('src', '-', '+'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRangeByLex', async client => {
+    assert.deepEqual(
+      await client.zRangeByLex('src', '-', '+'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANGEBYLEX.ts b/packages/client/lib/commands/ZRANGEBYLEX.ts
index d6e621a562f..e069fa55b4b 100644
--- a/packages/client/lib/commands/ZRANGEBYLEX.ts
+++ b/packages/client/lib/commands/ZRANGEBYLEX.ts
@@ -1,35 +1,42 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
 
 export interface ZRangeByLexOptions {
-    LIMIT?: {
-        offset: number;
-        count: number;
-    };
+  LIMIT?: {
+    offset: number;
+    count: number;
+  };
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: RedisCommandArgument,
-    max: RedisCommandArgument,
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns all the elements in the sorted set at key with a lexicographical value between min and max.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum lexicographical value.
+   * @param max - Maximum lexicographical value.
+   * @param options - Optional parameters including LIMIT.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    min: RedisArgument,
+    max: RedisArgument,
     options?: ZRangeByLexOptions
-): RedisCommandArguments {
-    const args = [
-        'ZRANGEBYLEX',
-        key,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
+  ) {
+    parser.push('ZRANGEBYLEX');
+    parser.pushKey(key);
+    parser.push(
+      transformStringDoubleArgument(min),
+      transformStringDoubleArgument(max)
+    );
 
     if (options?.LIMIT) {
-        args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
+      parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts
index a3484326306..364882f21a9 100644
--- a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts
+++ b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts
@@ -1,33 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANGEBYSCORE';
+import ZRANGEBYSCORE from './ZRANGEBYSCORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANGEBYSCORE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1),
-                ['ZRANGEBYSCORE', 'src', '0', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGEBYSCORE, 'src', 0, 1),
+        ['ZRANGEBYSCORE', 'src', '0', '1']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGEBYSCORE, 'src', 0, 1, {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zRangeByScore', async client => {
-        assert.deepEqual(
-            await client.zRangeByScore('src', 0, 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRangeByScore', async client => {
+    assert.deepEqual(
+      await client.zRangeByScore('src', 0, 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.ts b/packages/client/lib/commands/ZRANGEBYSCORE.ts
index 5ab7d7ac727..80bc8bc2b6c 100644
--- a/packages/client/lib/commands/ZRANGEBYSCORE.ts
+++ b/packages/client/lib/commands/ZRANGEBYSCORE.ts
@@ -1,35 +1,44 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
 
 export interface ZRangeByScoreOptions {
-    LIMIT?: {
-        offset: number;
-        count: number;
-    };
+  LIMIT?: {
+    offset: number;
+    count: number;
+  };
 }
 
-export function transformArguments(
-    key: RedisCommandArgument,
+export declare function transformReply(): Array<RedisArgument>;
+
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns all the elements in the sorted set with a score between min and max.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum score.
+   * @param max - Maximum score.
+   * @param options - Optional parameters including LIMIT.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     min: string | number,
     max: string | number,
     options?: ZRangeByScoreOptions
-): RedisCommandArguments {
-    const args = [
-        'ZRANGEBYSCORE',
-        key,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
+  ) {
+    parser.push('ZRANGEBYSCORE');
+    parser.pushKey(key);
+    parser.push(
+      transformStringDoubleArgument(min),
+      transformStringDoubleArgument(max)
+    );
 
     if (options?.LIMIT) {
-        args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
+      parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts
index 3552d3e2535..191eaa4e34f 100644
--- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts
+++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts
@@ -1,33 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANGEBYSCORE_WITHSCORES';
+import ZRANGEBYSCORE_WITHSCORES from './ZRANGEBYSCORE_WITHSCORES';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANGEBYSCORE WITHSCORES', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1),
-                ['ZRANGEBYSCORE', 'src', '0', '1', 'WITHSCORES']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGEBYSCORE_WITHSCORES, 'src', 0, 1),
+        ['ZRANGEBYSCORE', 'src', '0', '1', 'WITHSCORES']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGEBYSCORE_WITHSCORES, 'src', 0, 1, {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zRangeByScoreWithScores', async client => {
-        assert.deepEqual(
-            await client.zRangeByScoreWithScores('src', 0, 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRangeByScoreWithScores', async client => {
+    assert.deepEqual(
+      await client.zRangeByScoreWithScores('src', 0, 1),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts
index c7266f1c062..9cea5bd7b83 100644
--- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts
+++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts
@@ -1,18 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { ZRangeByScoreOptions, transformArguments as transformZRangeByScoreArguments } from './ZRANGEBYSCORE';
+import { Command } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
+import ZRANGEBYSCORE from './ZRANGEBYSCORE';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGEBYSCORE';
+export default {
+  CACHEABLE: ZRANGEBYSCORE.CACHEABLE,
+  IS_READ_ONLY: ZRANGEBYSCORE.IS_READ_ONLY,
+  /**
+   * Returns all the elements in the sorted set with a score between min and max, with their scores.
+   * @param args - Same parameters as the ZRANGEBYSCORE command.
+   */
+  parseCommand(...args: Parameters<typeof ZRANGEBYSCORE.parseCommand>) {
+    const parser = args[0];
 
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: string | number,
-    max: string | number,
-    options?: ZRangeByScoreOptions
-): RedisCommandArguments {
-    return [
-        ...transformZRangeByScoreArguments(key, min, max, options),
-        'WITHSCORES'
-    ];
-}
-
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+    ZRANGEBYSCORE.parseCommand(...args);
+    parser.push('WITHSCORES');
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANGESTORE.spec.ts b/packages/client/lib/commands/ZRANGESTORE.spec.ts
index 7af253e539f..c9708efd6fd 100644
--- a/packages/client/lib/commands/ZRANGESTORE.spec.ts
+++ b/packages/client/lib/commands/ZRANGESTORE.spec.ts
@@ -1,92 +1,82 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './ZRANGESTORE';
+import ZRANGESTORE from './ZRANGESTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANGESTORE', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('dst', 'src', 0, 1),
-                ['ZRANGESTORE', 'dst', 'src', '0', '1']
-            );
-        });
-
-        it('with BYSCORE', () => {
-            assert.deepEqual(
-                transformArguments('dst', 'src', 0, 1, {
-                    BY: 'SCORE'
-                }),
-                ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYSCORE']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1),
+        ['ZRANGESTORE', 'destination', 'source', '0', '1']
+      );
+    });
 
-        it('with BYLEX', () => {
-            assert.deepEqual(
-                transformArguments('dst', 'src', 0, 1, {
-                    BY: 'LEX'
-                }),
-                ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYLEX']
-            );
-        });
+    it('with BYSCORE', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, {
+          BY: 'SCORE'
+        }),
+        ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYSCORE']
+      );
+    });
 
-        it('with REV', () => {
-            assert.deepEqual(
-                transformArguments('dst', 'src', 0, 1, {
-                    REV: true
-                }),
-                ['ZRANGESTORE', 'dst', 'src', '0', '1', 'REV']
-            );
-        });
+    it('with BYLEX', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, {
+          BY: 'LEX'
+        }),
+        ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYLEX']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('dst', 'src', 0, 1, {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGESTORE', 'dst', 'src', '0', '1', 'LIMIT', '0', '1']
-            );
-        });
+    it('with REV', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, {
+          REV: true
+        }),
+        ['ZRANGESTORE', 'destination', 'source', '0', '1', 'REV']
+      );
+    });
 
-        it('with BY & REV & LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('dst', 'src', 0, 1, {
-                    BY: 'SCORE',
-                    REV: true,
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    },
-                    WITHSCORES: true
-                }),
-                ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1', 'WITHSCORES']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGESTORE', 'destination', 'source', '0', '1', 'LIMIT', '0', '1']
+      );
     });
 
-    describe('transformReply', () => {
-        it('should throw TypeError when reply is not a number', () => {
-            assert.throws(
-                // eslint-disable-next-line @typescript-eslint/no-explicit-any
-                () => (transformReply as any)([]),
-                TypeError
-            );
-        });
+    it('with BY & REV & LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, {
+          BY: 'SCORE',
+          REV: true,
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zRangeStore', async client => {
-        await client.zAdd('src', {
-            score: 0.5,
-            value: 'value'
-        });
+  testUtils.testWithClient('client.zRangeStore', async client => {
+    const [, reply] = await Promise.all([
+      client.zAdd('{tag}source', {
+        score: 1,
+        value: '1'
+      }),
+      client.zRangeStore('{tag}destination', '{tag}source', 0, 1)
+    ]);
 
-        assert.equal(
-            await client.zRangeStore('dst', 'src', 0, 1),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 1);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ZRANGESTORE.ts b/packages/client/lib/commands/ZRANGESTORE.ts
index 28067ceabe0..bd3e260e32c 100644
--- a/packages/client/lib/commands/ZRANGESTORE.ts
+++ b/packages/client/lib/commands/ZRANGESTORE.ts
@@ -1,62 +1,60 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
-
-export const FIRST_KEY_INDEX = 1;
-
-interface ZRangeStoreOptions {
-    BY?: 'SCORE' | 'LEX';
-    REV?: true;
-    LIMIT?: {
-        offset: number;
-        count: number;
-    };
-    WITHSCORES?: true;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
+
+export interface ZRangeStoreOptions {
+  BY?: 'SCORE' | 'LEX';
+  REV?: true;
+  LIMIT?: {
+    offset: number;
+    count: number;
+  };
 }
 
-export function transformArguments(
-    dst: RedisCommandArgument,
-    src: RedisCommandArgument,
-    min: RedisCommandArgument | number,
-    max: RedisCommandArgument | number,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Stores the result of a range operation on a sorted set into a new sorted set.
+   * @param parser - The Redis command parser.
+   * @param destination - Destination key where the result will be stored.
+   * @param source - Key of the source sorted set.
+   * @param min - Minimum index, score or lexicographical value.
+   * @param max - Maximum index, score or lexicographical value.
+   * @param options - Optional parameters for the range operation (BY, REV, LIMIT).
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    source: RedisArgument,
+    min: RedisArgument | number,
+    max: RedisArgument | number,
     options?: ZRangeStoreOptions
-): RedisCommandArguments {
-    const args = [
-        'ZRANGESTORE',
-        dst,
-        src,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
+  ) {
+    parser.push('ZRANGESTORE');
+    parser.pushKey(destination);
+    parser.pushKey(source);
+    parser.push(
+      transformStringDoubleArgument(min), 
+      transformStringDoubleArgument(max)
+    );
 
     switch (options?.BY) {
-        case 'SCORE':
-            args.push('BYSCORE');
-            break;
+      case 'SCORE':
+        parser.push('BYSCORE');
+        break;
 
-        case 'LEX':
-            args.push('BYLEX');
-            break;
+      case 'LEX':
+        parser.push('BYLEX');
+        break;
     }
 
     if (options?.REV) {
-        args.push('REV');
+      parser.push('REV');
     }
 
     if (options?.LIMIT) {
-        args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
-    }
-
-    if (options?.WITHSCORES) {
-        args.push('WITHSCORES');
-    }
-
-    return args;
-}
-
-export function transformReply(reply: number): number {
-    if (typeof reply !== 'number') {
-        throw new TypeError(`Upgrade to Redis 6.2.5 and up (https://github.com/redis/redis/pull/9089)`);
+      parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString());
     }
-
-    return reply;
-}
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts
index d9b07e19dda..e3009a6eadb 100644
--- a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts
+++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts
@@ -1,65 +1,76 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANGE_WITHSCORES';
+import ZRANGE_WITHSCORES from './ZRANGE_WITHSCORES';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANGE WITHSCORES', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1),
-                ['ZRANGE', 'src', '0', '1', 'WITHSCORES']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1),
+        ['ZRANGE', 'src', '0', '1', 'WITHSCORES']
+      );
+    });
 
-        it('with BY', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    BY: 'SCORE'
-                }),
-                ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'WITHSCORES']
-            );
-        });
+    it('with BY', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, {
+          BY: 'SCORE'
+        }),
+        ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'WITHSCORES']
+      );
+    });
 
-        it('with REV', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    REV: true
-                }),
-                ['ZRANGE', 'src', '0', '1', 'REV', 'WITHSCORES']
-            );
-        });
+    it('with REV', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, {
+          REV: true
+        }),
+        ['ZRANGE', 'src', '0', '1', 'REV', 'WITHSCORES']
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES']
-            );
-        });
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, {
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES']
+      );
+    });
 
-        it('with BY & REV & LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('src', 0, 1, {
-                    BY: 'SCORE',
-                    REV: true,
-                    LIMIT: {
-                        offset: 0,
-                        count: 1
-                    }
-                }),
-                ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1', 'WITHSCORES']
-            );
-        });
+    it('with BY & REV & LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, {
+          BY: 'SCORE',
+          REV: true,
+          LIMIT: {
+            offset: 0,
+            count: 1
+          }
+        }),
+        ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1', 'WITHSCORES']
+      );
     });
+  });
+
+  testUtils.testAll('zRangeWithScores', async client => {
+    const members = [{
+      value: '1',
+      score: 1
+    }];
+
+    const [, reply] = await Promise.all([
+      client.zAdd('key', members),
+      client.zRangeWithScores('key', 0, 1)
+    ]);
 
-    testUtils.testWithClient('client.zRangeWithScores', async client => {
-        assert.deepEqual(
-            await client.zRangeWithScores('src', 0, 1),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, members);
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts
index 23ea4d6337c..e85af4be08e 100644
--- a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts
+++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts
@@ -1,13 +1,20 @@
-import { RedisCommandArguments } from '.';
-import { transformArguments as transformZRangeArguments } from './ZRANGE';
+import { Command } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
+import ZRANGE from './ZRANGE';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGE';
+export default {
+  CACHEABLE: ZRANGE.CACHEABLE,
+  IS_READ_ONLY: ZRANGE.IS_READ_ONLY,
+  /**
+   * Returns the specified range of elements in the sorted set with their scores.
+   * @param args - Same parameters as the ZRANGE command.
+   */
+  parseCommand(...args: Parameters<typeof ZRANGE.parseCommand>) {
+    const parser = args[0];
 
-export function transformArguments(...args: Parameters<typeof transformZRangeArguments>): RedisCommandArguments {
-    return [
-        ...transformZRangeArguments(...args),
-        'WITHSCORES'
-    ];
-}
+    ZRANGE.parseCommand(...args);
+    parser.push('WITHSCORES');
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
 
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
diff --git a/packages/client/lib/commands/ZRANK.spec.ts b/packages/client/lib/commands/ZRANK.spec.ts
index 0c81517a7d6..480f75f66e1 100644
--- a/packages/client/lib/commands/ZRANK.spec.ts
+++ b/packages/client/lib/commands/ZRANK.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZRANK';
+import ZRANK from './ZRANK';
+import { parseArgs } from './generic-transformers';
 
 describe('ZRANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member'),
-            ['ZRANK', 'key', 'member']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZRANK, 'key', 'member'),
+      ['ZRANK', 'key', 'member']
+    );
+  });
 
-    testUtils.testWithClient('client.zRank', async client => {
-        assert.equal(
-            await client.zRank('key', 'member'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRank', async client => {
+    assert.equal(
+      await client.zRank('key', 'member'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZRANK.ts b/packages/client/lib/commands/ZRANK.ts
index 33439ea4b55..73329aa2a59 100644
--- a/packages/client/lib/commands/ZRANK.ts
+++ b/packages/client/lib/commands/ZRANK.ts
@@ -1,14 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument
-): RedisCommandArguments {
-    return ['ZRANK', key, member];
-}
-
-export declare function transformReply(): number | null;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the rank of a member in the sorted set, with scores ordered from low to high.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param member - Member to get the rank for.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) {
+    parser.push('ZRANK');
+    parser.pushKey(key);
+    parser.push(member);
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts
new file mode 100644
index 00000000000..9fa7cb1f6fd
--- /dev/null
+++ b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts
@@ -0,0 +1,47 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ZRANK_WITHSCORE from './ZRANK_WITHSCORE';
+import { parseArgs } from './generic-transformers';
+
+describe('ZRANK WITHSCORE', () => {
+  testUtils.isVersionGreaterThanHook([7, 2]);
+
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZRANK_WITHSCORE, 'key', 'member'),
+      ['ZRANK', 'key', 'member', 'WITHSCORE']
+    );
+  });
+
+  testUtils.testAll('zRankWithScore - null', async client => {
+    assert.equal(
+      await client.zRankWithScore('key', 'member'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+
+  testUtils.testAll('zRankWithScore - with member', async client => {
+    const member = {
+      value: '1',
+      score: 1
+    }
+
+    const [, reply] = await Promise.all([
+      client.zAdd('key', member),
+      client.zRankWithScore('key', member.value)
+    ])
+    assert.deepEqual(
+      reply,
+      {
+        rank: 0,
+        score: 1
+      }
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.ts
new file mode 100644
index 00000000000..6f6537f4087
--- /dev/null
+++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts
@@ -0,0 +1,35 @@
+import { NullReply, TuplesReply, NumberReply, BlobStringReply, DoubleReply, UnwrapReply, Command } from '../RESP/types';
+import ZRANK from './ZRANK';
+
+export default {
+  CACHEABLE: ZRANK.CACHEABLE,
+  IS_READ_ONLY: ZRANK.IS_READ_ONLY,
+  /**
+   * Returns the rank of a member in the sorted set with its score.
+   * @param args - Same parameters as the ZRANK command.
+   */
+  parseCommand(...args: Parameters<typeof ZRANK.parseCommand>) {
+    const parser = args[0];
+
+    ZRANK.parseCommand(...args);
+    parser.push('WITHSCORE');
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<NullReply | TuplesReply<[NumberReply, BlobStringReply]>>) => {
+      if (reply === null) return null;
+
+      return {
+        rank: reply[0],
+        score: Number(reply[1])
+      };
+    },
+    3: (reply: UnwrapReply<NullReply | TuplesReply<[BlobStringReply, DoubleReply]>>) => {
+      if (reply === null) return null;
+
+      return {
+        rank: reply[0],
+        score: reply[1]
+      };
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZREM.spec.ts b/packages/client/lib/commands/ZREM.spec.ts
index 3ac001708a0..ac65b3d0139 100644
--- a/packages/client/lib/commands/ZREM.spec.ts
+++ b/packages/client/lib/commands/ZREM.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZREM';
+import ZREM from './ZREM';
+import { parseArgs } from './generic-transformers';
 
 describe('ZREM', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('key', 'member'),
-                ['ZREM', 'key', 'member']
-            );
-        });
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(ZREM, 'key', 'member'),
+        ['ZREM', 'key', 'member']
+      );
+    });
 
-        it('array', () => {
-            assert.deepEqual(
-                transformArguments('key', ['1', '2']),
-                ['ZREM', 'key', '1', '2']
-            );
-        });
+    it('array', () => {
+      assert.deepEqual(
+        parseArgs(ZREM, 'key', ['1', '2']),
+        ['ZREM', 'key', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zRem', async client => {
-        assert.equal(
-            await client.zRem('key', 'member'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRem', async client => {
+    assert.equal(
+      await client.zRem('key', 'member'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZREM.ts b/packages/client/lib/commands/ZREM.ts
index 7ab92c4a78f..960b47a36fd 100644
--- a/packages/client/lib/commands/ZREM.ts
+++ b/packages/client/lib/commands/ZREM.ts
@@ -1,13 +1,23 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArguments } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { RedisVariadicArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    return pushVerdictArguments(['ZREM', key], member);
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes the specified members from the sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param member - One or more members to remove.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    member: RedisVariadicArgument
+  ) {
+    parser.push('ZREM');
+    parser.pushKey(key);
+    parser.pushVariadic(member);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts
index b59c9e9f3b0..b141b7679ee 100644
--- a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts
+++ b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZREMRANGEBYLEX';
+import ZREMRANGEBYLEX from './ZREMRANGEBYLEX';
+import { parseArgs } from './generic-transformers';
 
 describe('ZREMRANGEBYLEX', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '[a', '[b'),
-            ['ZREMRANGEBYLEX', 'key', '[a', '[b']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZREMRANGEBYLEX, 'key', '[a', '[b'),
+      ['ZREMRANGEBYLEX', 'key', '[a', '[b']
+    );
+  });
 
-    testUtils.testWithClient('client.zRemRangeByLex', async client => {
-        assert.equal(
-            await client.zRemRangeByLex('key', '[a', '[b'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRemRangeByLex', async client => {
+    assert.equal(
+      await client.zRemRangeByLex('key', '[a', '[b'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.ts
index f1f3908f538..434dcc6aac0 100644
--- a/packages/client/lib/commands/ZREMRANGEBYLEX.ts
+++ b/packages/client/lib/commands/ZREMRANGEBYLEX.ts
@@ -1,19 +1,28 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { NumberReply, Command, RedisArgument } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: RedisCommandArgument | number,
-    max: RedisCommandArgument | number
-): RedisCommandArguments {
-    return [
-        'ZREMRANGEBYLEX',
-        key,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes all elements in the sorted set with lexicographical values between min and max.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum lexicographical value.
+   * @param max - Maximum lexicographical value.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    min: RedisArgument | number,
+    max: RedisArgument | number
+  ) {
+    parser.push('ZREMRANGEBYLEX');
+    parser.pushKey(key);
+    parser.push(
+      transformStringDoubleArgument(min),
+      transformStringDoubleArgument(max)
+    );
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts
index c659dadb790..19f54466c20 100644
--- a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts
+++ b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZREMRANGEBYRANK';
+import ZREMRANGEBYRANK from './ZREMRANGEBYRANK';
+import { parseArgs } from './generic-transformers';
 
 describe('ZREMRANGEBYRANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 1),
-            ['ZREMRANGEBYRANK', 'key', '0', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZREMRANGEBYRANK, 'key', 0, 1),
+      ['ZREMRANGEBYRANK', 'key', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.zRemRangeByRank', async client => {
-        assert.equal(
-            await client.zRemRangeByRank('key', 0, 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRemRangeByRank', async client => {
+    assert.equal(
+      await client.zRemRangeByRank('key', 0, 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.ts
index c50d06e3bf6..90ab6b3aefe 100644
--- a/packages/client/lib/commands/ZREMRANGEBYRANK.ts
+++ b/packages/client/lib/commands/ZREMRANGEBYRANK.ts
@@ -1,13 +1,27 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes all elements in the sorted set with rank between start and stop.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param start - Minimum rank (starting from 0).
+   * @param stop - Maximum rank.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
     start: number,
     stop: number
-): RedisCommandArguments {
-    return ['ZREMRANGEBYRANK', key, start.toString(), stop.toString()];
-}
-
-export declare function transformReply(): number;
+  ) {      
+    parser.push('ZREMRANGEBYRANK');
+    parser.pushKey(key);
+    parser.push(
+      start.toString(), 
+      stop.toString()
+    );
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts
index 988fd7690c9..856692ef8f5 100644
--- a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts
+++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZREMRANGEBYSCORE';
+import { parseArgs } from './generic-transformers';
+import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE';
 
 describe('ZREMRANGEBYSCORE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 0, 1),
-            ['ZREMRANGEBYSCORE', 'key', '0', '1']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZREMRANGEBYSCORE, 'key', 0, 1),
+      ['ZREMRANGEBYSCORE', 'key', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.zRemRangeByScore', async client => {
-        assert.equal(
-            await client.zRemRangeByScore('key', 0, 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRemRangeByScore', async client => {
+    assert.equal(
+      await client.zRemRangeByScore('key', 0, 1),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts
index 12d1eff811e..e78c57ea656 100644
--- a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts
+++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts
@@ -1,19 +1,28 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { transformStringNumberInfinityArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command } from '../RESP/types';
+import { transformStringDoubleArgument } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    min: RedisCommandArgument | number,
-    max: RedisCommandArgument | number,
-): RedisCommandArguments {
-    return [
-        'ZREMRANGEBYSCORE',
-        key,
-        transformStringNumberInfinityArgument(min),
-        transformStringNumberInfinityArgument(max)
-    ];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes all elements in the sorted set with scores between min and max.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param min - Minimum score.
+   * @param max - Maximum score.
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    min: RedisArgument | number,
+    max: RedisArgument | number,
+  ) {
+    parser.push('ZREMRANGEBYSCORE');
+    parser.pushKey(key);
+    parser.push(
+      transformStringDoubleArgument(min),
+      transformStringDoubleArgument(max)
+    );
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZREVRANK.spec.ts b/packages/client/lib/commands/ZREVRANK.spec.ts
index d9fef0d70a4..c89f528eb1c 100644
--- a/packages/client/lib/commands/ZREVRANK.spec.ts
+++ b/packages/client/lib/commands/ZREVRANK.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZREVRANK';
+import { parseArgs } from './generic-transformers';
+import ZREVRANK from './ZREVRANK';
 
 describe('ZREVRANK', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member'),
-            ['ZREVRANK', 'key', 'member']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZREVRANK, 'key', 'member'),
+      ['ZREVRANK', 'key', 'member']
+    );
+  });
 
-    testUtils.testWithClient('client.zRevRank', async client => {
-        assert.equal(
-            await client.zRevRank('key', 'member'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zRevRank', async client => {
+    assert.equal(
+      await client.zRevRank('key', 'member'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZREVRANK.ts b/packages/client/lib/commands/ZREVRANK.ts
index b88936c0c92..f2f79e570cc 100644
--- a/packages/client/lib/commands/ZREVRANK.ts
+++ b/packages/client/lib/commands/ZREVRANK.ts
@@ -1,14 +1,19 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { CommandParser } from '../client/parser';
+import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument
-): RedisCommandArguments {
-    return ['ZREVRANK', key, member];
-}
-
-export declare function transformReply(): number | null;
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the rank of a member in the sorted set, with scores ordered from high to low.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param member - Member to get the rank for.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) {
+    parser.push('ZREVRANK');
+    parser.pushKey(key);
+    parser.push(member);
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZSCAN.spec.ts b/packages/client/lib/commands/ZSCAN.spec.ts
index afa221a1ef3..f8064aea41e 100644
--- a/packages/client/lib/commands/ZSCAN.spec.ts
+++ b/packages/client/lib/commands/ZSCAN.spec.ts
@@ -1,77 +1,53 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './ZSCAN';
+import { parseArgs } from './generic-transformers';
+import ZSCAN from './ZSCAN';
 
 describe('ZSCAN', () => {
-    describe('transformArguments', () => {
-        it('cusror only', () => {
-            assert.deepEqual(
-                transformArguments('key', 0),
-                ['ZSCAN', 'key', '0']
-            );
-        });
-
-        it('with MATCH', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern'
-                }),
-                ['ZSCAN', 'key', '0', 'MATCH', 'pattern']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    COUNT: 1
-                }),
-                ['ZSCAN', 'key', '0', 'COUNT', '1']
-            );
-        });
+  describe('transformArguments', () => {
+    it('cusror only', () => {
+      assert.deepEqual(
+        parseArgs(ZSCAN, 'key', '0'),
+        ['ZSCAN', 'key', '0']
+      );
+    });
 
-        it('with MATCH & COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', 0, {
-                    MATCH: 'pattern',
-                    COUNT: 1
-                }),
-                ['ZSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
-            );
-        });
+    it('with MATCH', () => {
+      assert.deepEqual(
+        parseArgs(ZSCAN, 'key', '0', {
+          MATCH: 'pattern'
+        }),
+        ['ZSCAN', 'key', '0', 'MATCH', 'pattern']
+      );
     });
 
-    describe('transformReply', () => {
-        it('without members', () => {
-            assert.deepEqual(
-                transformReply(['0', []]),
-                {
-                    cursor: 0,
-                    members: []
-                }
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(ZSCAN, 'key', '0', {
+          COUNT: 1
+        }),
+        ['ZSCAN', 'key', '0', 'COUNT', '1']
+      );
+    });
 
-        it('with members', () => {
-            assert.deepEqual(
-                transformReply(['0', ['member', '-inf']]),
-                {
-                    cursor: 0,
-                    members: [{
-                        value: 'member',
-                        score: -Infinity
-                    }]
-                }
-            );
-        });
+    it('with MATCH & COUNT', () => {
+      assert.deepEqual(
+        parseArgs(ZSCAN, 'key', '0', {
+          MATCH: 'pattern',
+          COUNT: 1
+        }),
+        ['ZSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zScan', async client => {
-        assert.deepEqual(
-            await client.zScan('key', 0),
-            {
-                cursor: 0,
-                members: []
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('zScan', async client => {
+    assert.deepEqual(
+      await client.zScan('key', '0'),
+      {
+        cursor: '0',
+        members: []
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/client/lib/commands/ZSCAN.ts b/packages/client/lib/commands/ZSCAN.ts
index f6fa17c2d4e..2790db5e023 100644
--- a/packages/client/lib/commands/ZSCAN.ts
+++ b/packages/client/lib/commands/ZSCAN.ts
@@ -1,39 +1,36 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { ScanOptions, transformNumberInfinityReply, pushScanArguments, ZMember } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { ScanCommonOptions, parseScanArguments } from './SCAN';
+import { transformSortedSetReply } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    cursor: number,
-    options?: ScanOptions
-): RedisCommandArguments {
-    return pushScanArguments([
-        'ZSCAN',
-        key
-    ], cursor, options);
+export interface HScanEntry {
+  field: BlobStringReply;
+  value: BlobStringReply;
 }
 
-type ZScanRawReply = [RedisCommandArgument, Array<RedisCommandArgument>];
-
-interface ZScanReply {
-    cursor: number;
-    members: Array<ZMember>;
-}
-
-export function transformReply([cursor, rawMembers]: ZScanRawReply): ZScanReply {
-    const parsedMembers: Array<ZMember> = [];
-    for (let i = 0; i < rawMembers.length; i += 2) {
-        parsedMembers.push({
-            value: rawMembers[i],
-            score: transformNumberInfinityReply(rawMembers[i + 1])
-        });
-    }
-
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Incrementally iterates over a sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param cursor - Cursor position to start the scan from.
+   * @param options - Optional scan parameters (COUNT, MATCH, TYPE).
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    cursor: RedisArgument,
+    options?: ScanCommonOptions
+  ) {
+    parser.push('ZSCAN');
+    parser.pushKey(key);
+    parseScanArguments(parser, cursor, options);
+  },
+  transformReply([cursor, rawMembers]: [BlobStringReply, ArrayReply<BlobStringReply>]) {
     return {
-        cursor: Number(cursor),
-        members: parsedMembers
+      cursor,
+      members: transformSortedSetReply[2](rawMembers)
     };
-}
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZSCORE.spec.ts b/packages/client/lib/commands/ZSCORE.spec.ts
index fe2a1c6a7c5..4229ab7aac0 100644
--- a/packages/client/lib/commands/ZSCORE.spec.ts
+++ b/packages/client/lib/commands/ZSCORE.spec.ts
@@ -1,19 +1,23 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZSCORE';
+import ZSCORE from './ZSCORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZSCORE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'member'),
-            ['ZSCORE', 'key', 'member']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ZSCORE, 'key', 'member'),
+      ['ZSCORE', 'key', 'member']
+    );
+  });
 
-    testUtils.testWithClient('client.zScore', async client => {
-        assert.equal(
-            await client.zScore('key', 'member'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zScore', async client => {
+    assert.equal(
+      await client.zScore('key', 'member'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZSCORE.ts b/packages/client/lib/commands/ZSCORE.ts
index 118abc10850..8b44154f44d 100644
--- a/packages/client/lib/commands/ZSCORE.ts
+++ b/packages/client/lib/commands/ZSCORE.ts
@@ -1,14 +1,21 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
 
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '../client/parser';
+import { RedisArgument, Command } from '../RESP/types';
+import { transformNullableDoubleReply } from './generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: RedisCommandArgument,
-    member: RedisCommandArgument
-): RedisCommandArguments {
-    return ['ZSCORE', key, member];
-}
-
-export { transformNumberInfinityNullReply as transformReply } from './generic-transformers';
+export default {
+  CACHEABLE: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the score of a member in a sorted set.
+   * @param parser - The Redis command parser.
+   * @param key - Key of the sorted set.
+   * @param member - Member to get the score for.
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) {
+    parser.push('ZSCORE');
+    parser.pushKey(key);
+    parser.push(member);
+  },
+  transformReply: transformNullableDoubleReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZUNION.spec.ts b/packages/client/lib/commands/ZUNION.spec.ts
index c53498cbf65..b4dbb4de603 100644
--- a/packages/client/lib/commands/ZUNION.spec.ts
+++ b/packages/client/lib/commands/ZUNION.spec.ts
@@ -1,48 +1,66 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZUNION';
+import ZUNION from './ZUNION';
+import { parseArgs } from './generic-transformers';
 
 describe('ZUNION', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('key (string)', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['ZUNION', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('key (string)', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION, 'key'),
+        ['ZUNION', '1', 'key']
+      );
+    });
 
-        it('keys (array)', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZUNION', '2', '1', '2']
-            );
-        });
+    it('keys (Array<string>)', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION, ['1', '2']),
+        ['ZUNION', '2', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    WEIGHTS: [1]
-                }),
-                ['ZUNION', '1', 'key', 'WEIGHTS', '1']
-            );
-        });
+    it('key & weight', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION, {
+          key: 'key',
+          weight: 1
+        }),
+        ['ZUNION', '1', 'key', 'WEIGHTS', '1']
+      );
+    });
+
+    it('keys & weights', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION, [{
+          key: 'a',
+          weight: 1
+        }, {
+          key: 'b',
+          weight: 2
+        }]),
+        ['ZUNION', '2', 'a', 'b', 'WEIGHTS', '1', '2']
+      );
+    });
 
-        it('with AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('with AGGREGATE', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION, 'key', {
+          AGGREGATE: 'SUM'
+        }),
+        ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zUnion', async client => {
-        assert.deepEqual(
-            await client.zUnion('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zUnion', async client => {
+    assert.deepEqual(
+      await client.zUnion('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZUNION.ts b/packages/client/lib/commands/ZUNION.ts
index f329348cc8b..6497d0d8e8b 100644
--- a/packages/client/lib/commands/ZUNION.ts
+++ b/packages/client/lib/commands/ZUNION.ts
@@ -1,30 +1,26 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
+import { ZKeys, parseZKeysArguments } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 2;
-
-export const IS_READ_ONLY = true;
-
-interface ZUnionOptions {
-    WEIGHTS?: Array<number>;
-    AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
+export interface ZUnionOptions {
+  AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
 }
 
-export function transformArguments(
-    keys: Array<RedisCommandArgument> | RedisCommandArgument,
-    options?: ZUnionOptions
-): RedisCommandArguments {
-    const args = pushVerdictArgument(['ZUNION'], keys);
-
-    if (options?.WEIGHTS) {
-        args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString()));
-    }
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the union of multiple sorted sets.
+   * @param parser - The Redis command parser.
+   * @param keys - Keys of the sorted sets to combine.
+   * @param options - Optional parameters for the union operation.
+   */
+  parseCommand(parser: CommandParser, keys: ZKeys, options?: ZUnionOptions) {
+    parser.push('ZUNION');
+    parseZKeysArguments(parser, keys);
 
     if (options?.AGGREGATE) {
-        args.push('AGGREGATE', options.AGGREGATE);
+      parser.push('AGGREGATE', options.AGGREGATE);
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<RedisCommandArgument>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZUNIONSTORE.spec.ts b/packages/client/lib/commands/ZUNIONSTORE.spec.ts
index 8f11828b221..a369a649311 100644
--- a/packages/client/lib/commands/ZUNIONSTORE.spec.ts
+++ b/packages/client/lib/commands/ZUNIONSTORE.spec.ts
@@ -1,56 +1,64 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZUNIONSTORE';
+import ZUNIONSTORE from './ZUNIONSTORE';
+import { parseArgs } from './generic-transformers';
 
 describe('ZUNIONSTORE', () => {
-    describe('transformArguments', () => {
-        it('key (string)', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key'),
-                ['ZUNIONSTORE', 'destination', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('key (string)', () => {
+      assert.deepEqual(
+        parseArgs(ZUNIONSTORE, 'destination', 'source'),
+        ['ZUNIONSTORE', 'destination', '1', 'source']
+      );
+    });
 
-        it('keys (array)', () => {
-            assert.deepEqual(
-                transformArguments('destination', ['1', '2']),
-                ['ZUNIONSTORE', 'destination', '2', '1', '2']
-            );
-        });
+    it('keys (Array<string>)', () => {
+      assert.deepEqual(
+        parseArgs(ZUNIONSTORE, 'destination', ['1', '2']),
+        ['ZUNIONSTORE', 'destination', '2', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key', {
-                    WEIGHTS: [1]
-                }),
-                ['ZUNIONSTORE', 'destination', '1', 'key', 'WEIGHTS', '1']
-            );
-        });
+    it('key & weight', () => {
+      assert.deepEqual(
+        parseArgs(ZUNIONSTORE, 'destination', {
+          key: 'source',
+          weight: 1
+        }),
+        ['ZUNIONSTORE', 'destination', '1', 'source', 'WEIGHTS', '1']
+      );
+    });
 
-        it('with AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key', {
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZUNIONSTORE', 'destination', '1', 'key', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('keys & weights', () => {
+      assert.deepEqual(
+        parseArgs(ZUNIONSTORE, 'destination', [{
+          key: 'a',
+          weight: 1
+        }, {
+          key: 'b',
+          weight: 2
+        }]),
+        ['ZUNIONSTORE', 'destination', '2', 'a', 'b', 'WEIGHTS', '1', '2']
+      );
+    });
 
-        it('with WEIGHTS, AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('destination', 'key', {
-                    WEIGHTS: [1],
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZUNIONSTORE', 'destination', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM']
-            );
-        });
+    it('with AGGREGATE', () => {
+      assert.deepEqual(
+        parseArgs(ZUNIONSTORE, 'destination', 'source', {
+          AGGREGATE: 'SUM'
+        }),
+        ['ZUNIONSTORE', 'destination', '1', 'source', 'AGGREGATE', 'SUM']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zUnionStore', async client => {
-        assert.equal(
-            await client.zUnionStore('destination', 'key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zUnionStore', async client => {
+    assert.equal(
+      await client.zUnionStore('{tag}destination', '{tag}key'),
+      0
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZUNIONSTORE.ts b/packages/client/lib/commands/ZUNIONSTORE.ts
index 2a42e21bc87..9de766e8b06 100644
--- a/packages/client/lib/commands/ZUNIONSTORE.ts
+++ b/packages/client/lib/commands/ZUNIONSTORE.ts
@@ -1,29 +1,33 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
-import { pushVerdictArgument } from './generic-transformers';
+import { CommandParser } from '../client/parser';
+import { RedisArgument, NumberReply, Command, } from '../RESP/types';
+import { ZKeys, parseZKeysArguments } from './generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface ZUnionOptions {
-    WEIGHTS?: Array<number>;
-    AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
+export interface ZUnionOptions {
+  AGGREGATE?: 'SUM' | 'MIN' | 'MAX';
 }
 
-export function transformArguments(
-    destination: RedisCommandArgument,
-    keys: Array<RedisCommandArgument> | RedisCommandArgument,
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Stores the union of multiple sorted sets in a new sorted set.
+   * @param parser - The Redis command parser.
+   * @param destination - Destination key where the result will be stored.
+   * @param keys - Keys of the sorted sets to combine.
+   * @param options - Optional parameters for the union operation.
+   */
+  parseCommand(
+    parser: CommandParser,
+    destination: RedisArgument,
+    keys: ZKeys,
     options?: ZUnionOptions
-): RedisCommandArguments {
-    const args = pushVerdictArgument(['ZUNIONSTORE', destination], keys);
-
-    if (options?.WEIGHTS) {
-        args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString()));
-    }
-
+  ): any {
+    parser.push('ZUNIONSTORE');
+    parser.pushKey(destination);
+    parseZKeysArguments(parser, keys);
+    
     if (options?.AGGREGATE) {
-        args.push('AGGREGATE', options.AGGREGATE);
+      parser.push('AGGREGATE', options.AGGREGATE);
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts
index 3786a97963d..dee735fc99f 100644
--- a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts
+++ b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts
@@ -1,48 +1,66 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ZUNION_WITHSCORES';
+import ZUNION_WITHSCORES from './ZUNION_WITHSCORES';
+import { parseArgs } from './generic-transformers';
 
 describe('ZUNION WITHSCORES', () => {
-    testUtils.isVersionGreaterThanHook([6, 2]);
+  testUtils.isVersionGreaterThanHook([6, 2]);
 
-    describe('transformArguments', () => {
-        it('key (string)', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['ZUNION', '1', 'key', 'WITHSCORES']
-            );
-        });
+  describe('transformArguments', () => {
+    it('key (string)', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION_WITHSCORES, 'key'),
+        ['ZUNION', '1', 'key', 'WITHSCORES']
+      );
+    });
 
-        it('keys (array)', () => {
-            assert.deepEqual(
-                transformArguments(['1', '2']),
-                ['ZUNION', '2', '1', '2', 'WITHSCORES']
-            );
-        });
+    it('keys (Array<string>)', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION_WITHSCORES, ['1', '2']),
+        ['ZUNION', '2', '1', '2', 'WITHSCORES']
+      );
+    });
 
-        it('with WEIGHTS', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    WEIGHTS: [1]
-                }),
-                ['ZUNION', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES']
-            );
-        });
+    it('key & weight', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION_WITHSCORES, {
+          key: 'key',
+          weight: 1
+        }),
+        ['ZUNION', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES']
+      );
+    });
+
+    it('keys & weights', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION_WITHSCORES, [{
+          key: 'a',
+          weight: 1
+        }, {
+          key: 'b',
+          weight: 2
+        }]),
+        ['ZUNION', '2', 'a', 'b', 'WEIGHTS', '1', '2', 'WITHSCORES']
+      );
+    });
 
-        it('with AGGREGATE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    AGGREGATE: 'SUM'
-                }),
-                ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES']
-            );
-        });
+    it('with AGGREGATE', () => {
+      assert.deepEqual(
+        parseArgs(ZUNION_WITHSCORES, 'key', {
+          AGGREGATE: 'SUM'
+        }),
+        ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.zUnionWithScores', async client => {
-        assert.deepEqual(
-            await client.zUnionWithScores('key'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testAll('zUnionWithScores', async client => {
+    assert.deepEqual(
+      await client.zUnionWithScores('key'),
+      []
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
 });
diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.ts
index 168cc929ac8..af93a4eb1c0 100644
--- a/packages/client/lib/commands/ZUNION_WITHSCORES.ts
+++ b/packages/client/lib/commands/ZUNION_WITHSCORES.ts
@@ -1,13 +1,19 @@
-import { RedisCommandArguments } from '.';
-import { transformArguments as transformZUnionArguments } from './ZUNION';
+import { Command } from '../RESP/types';
+import { transformSortedSetReply } from './generic-transformers';
+import ZUNION from './ZUNION';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZUNION';
 
-export function transformArguments(...args: Parameters<typeof transformZUnionArguments>): RedisCommandArguments {
-    return [
-        ...transformZUnionArguments(...args),
-        'WITHSCORES'
-    ];
-}
+export default {
+  IS_READ_ONLY: ZUNION.IS_READ_ONLY,
+  /**
+   * Returns the union of multiple sorted sets with their scores.
+   * @param args - Same parameters as the ZUNION command.
+   */
+  parseCommand(...args: Parameters<typeof ZUNION.parseCommand>) {
+    const parser = args[0];
 
-export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers';
+    ZUNION.parseCommand(...args);
+    parser.push('WITHSCORES');
+  },
+  transformReply: transformSortedSetReply
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/common-stream.types.ts b/packages/client/lib/commands/common-stream.types.ts
new file mode 100644
index 00000000000..60955b6e3c3
--- /dev/null
+++ b/packages/client/lib/commands/common-stream.types.ts
@@ -0,0 +1,28 @@
+/** Common stream deletion policies
+ *
+ *  Added in Redis 8.2
+ */
+export const STREAM_DELETION_POLICY = {
+  /** Preserve references (default) */
+  KEEPREF: "KEEPREF",
+  /** Delete all references */
+  DELREF: "DELREF",
+  /** Only acknowledged entries */
+  ACKED: "ACKED",
+} as const;
+
+export type StreamDeletionPolicy =
+  (typeof STREAM_DELETION_POLICY)[keyof typeof STREAM_DELETION_POLICY];
+
+/** Common reply codes for stream deletion operations */
+export const STREAM_DELETION_REPLY_CODES = {
+  /** ID not found */
+  NOT_FOUND: -1,
+  /** Entry deleted */
+  DELETED: 1,
+  /** Dangling references */
+  DANGLING_REFS: 2,
+} as const;
+
+export type StreamDeletionReplyCode =
+  (typeof STREAM_DELETION_REPLY_CODES)[keyof typeof STREAM_DELETION_REPLY_CODES];
diff --git a/packages/client/lib/commands/generic-transformers.spec.ts b/packages/client/lib/commands/generic-transformers.spec.ts
index 60caf26eaad..5f990d4e34d 100644
--- a/packages/client/lib/commands/generic-transformers.spec.ts
+++ b/packages/client/lib/commands/generic-transformers.spec.ts
@@ -1,718 +1,685 @@
-import { strict as assert } from 'assert';
-import {
-    transformBooleanReply,
-    transformBooleanArrayReply,
-    pushScanArguments,
-    transformNumberInfinityReply,
-    transformNumberInfinityNullReply,
-    transformNumberInfinityArgument,
-    transformStringNumberInfinityArgument,
-    transformTuplesReply,
-    transformStreamMessagesReply,
-    transformStreamMessagesNullReply,
-    transformStreamsMessagesReply,
-    transformSortedSetWithScoresReply,
-    pushGeoCountArgument,
-    pushGeoSearchArguments,
-    GeoReplyWith,
-    transformGeoMembersWithReply,
-    transformEXAT,
-    transformPXAT,
-    pushEvalArguments,
-    pushVerdictArguments,
-    pushVerdictNumberArguments,
-    pushVerdictArgument,
-    pushOptionalVerdictArgument,
-    transformCommandReply,
-    CommandFlags,
-    CommandCategories,
-    pushSlotRangesArguments
-} from './generic-transformers';
-
-describe('Generic Transformers', () => {
-    describe('transformBooleanReply', () => {
-        it('0', () => {
-            assert.equal(
-                transformBooleanReply(0),
-                false
-            );
-        });
-
-        it('1', () => {
-            assert.equal(
-                transformBooleanReply(1),
-                true
-            );
-        });
-    });
-
-    describe('transformBooleanArrayReply', () => {
-        it('empty array', () => {
-            assert.deepEqual(
-                transformBooleanArrayReply([]),
-                []
-            );
-        });
-
-        it('0, 1', () => {
-            assert.deepEqual(
-                transformBooleanArrayReply([0, 1]),
-                [false, true]
-            );
-        });
-    });
-
-    describe('pushScanArguments', () => {
-        it('cusror only', () => {
-            assert.deepEqual(
-                pushScanArguments([], 0),
-                ['0']
-            );
-        });
-
-        it('with MATCH', () => {
-            assert.deepEqual(
-                pushScanArguments([], 0, {
-                    MATCH: 'pattern'
-                }),
-                ['0', 'MATCH', 'pattern']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                pushScanArguments([], 0, {
-                    COUNT: 1
-                }),
-                ['0', 'COUNT', '1']
-            );
-        });
-
-        it('with MATCH & COUNT', () => {
-            assert.deepEqual(
-                pushScanArguments([], 0, {
-                    MATCH: 'pattern',
-                    COUNT: 1
-                }),
-                ['0', 'MATCH', 'pattern', 'COUNT', '1']
-            );
-        });
-    });
-
-    describe('transformNumberInfinityReply', () => {
-        it('0.5', () => {
-            assert.equal(
-                transformNumberInfinityReply('0.5'),
-                0.5
-            );
-        });
-
-        it('+inf', () => {
-            assert.equal(
-                transformNumberInfinityReply('+inf'),
-                Infinity
-            );
-        });
-
-        it('-inf', () => {
-            assert.equal(
-                transformNumberInfinityReply('-inf'),
-                -Infinity
-            );
-        });
-    });
-
-    describe('transformNumberInfinityNullReply', () => {
-        it('null', () => {
-            assert.equal(
-                transformNumberInfinityNullReply(null),
-                null
-            );
-        });
-
-        it('1', () => {
-            assert.equal(
-                transformNumberInfinityNullReply('1'),
-                1
-            );
-        });
-    });
-
-    describe('transformNumberInfinityArgument', () => {
-        it('0.5', () => {
-            assert.equal(
-                transformNumberInfinityArgument(0.5),
-                '0.5'
-            );
-        });
-
-        it('Infinity', () => {
-            assert.equal(
-                transformNumberInfinityArgument(Infinity),
-                '+inf'
-            );
-        });
-
-        it('-Infinity', () => {
-            assert.equal(
-                transformNumberInfinityArgument(-Infinity),
-                '-inf'
-            );
-        });
-    });
-
-    describe('transformStringNumberInfinityArgument', () => {
-        it("'0.5'", () => {
-            assert.equal(
-                transformStringNumberInfinityArgument('0.5'),
-                '0.5'
-            );
-        });
-
-        it('0.5', () => {
-            assert.equal(
-                transformStringNumberInfinityArgument(0.5),
-                '0.5'
-            );
-        });
-    });
-
-    it('transformTuplesReply', () => {
-        assert.deepEqual(
-            transformTuplesReply(['key1', 'value1', 'key2', 'value2']),
-            Object.create(null, {
-                key1: {
-                    value: 'value1',
-                    configurable: true,
-                    enumerable: true
-                },
-                key2: {
-                    value: 'value2',
-                    configurable: true,
-                    enumerable: true
-                }
-            })
-        );
-    });
-
-    it('transformStreamMessagesReply', () => {
-        assert.deepEqual(
-            transformStreamMessagesReply([['0-0', ['0key', '0value']], ['1-0', ['1key', '1value']]]),
-            [{
-                id: '0-0',
-                message: Object.create(null, {
-                    '0key': {
-                        value: '0value',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }, {
-                id: '1-0',
-                message: Object.create(null, {
-                    '1key': {
-                        value: '1value',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }]
-        );
-    });
-
-    it('transformStreamMessagesNullReply', () => {
-        assert.deepEqual(
-            transformStreamMessagesNullReply([null, ['0-0', ['0key', '0value']]]),
-            [null, {
-                id: '0-0',
-                message: Object.create(null, {
-                    '0key': {
-                        value: '0value',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }]
-        );
-    });
-
-    it('transformStreamMessagesNullReply', () => {
-        assert.deepEqual(
-            transformStreamMessagesNullReply([null, ['0-1', ['11key', '11value']]]),
-            [null, {
-                id: '0-1',
-                message: Object.create(null, {
-                    '11key': {
-                        value: '11value',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })
-            }]
-        );
-    });
-
-    describe('transformStreamsMessagesReply', () => {
-        it('null', () => {
-            assert.equal(
-                transformStreamsMessagesReply(null),
-                null
-            );
-        });
-
-        it('with messages', () => {
-            assert.deepEqual(
-                transformStreamsMessagesReply([['stream1', [['0-1', ['11key', '11value']], ['1-1', ['12key', '12value']]]], ['stream2', [['0-2', ['2key1', '2value1', '2key2', '2value2']]]]]),
-                [{
-                    name: 'stream1',
-                    messages: [{
-                        id: '0-1',
-                        message: Object.create(null, {
-                            '11key': {
-                                value: '11value',
-                                configurable: true,
-                                enumerable: true
-                            }
-                        })
-                    }, {
-                        id: '1-1',
-                        message: Object.create(null, {
-                            '12key': {
-                                value: '12value',
-                                configurable: true,
-                                enumerable: true
-                            }
-                        })
-                    }]
-                }, {
-                    name: 'stream2',
-                    messages: [{
-                        id: '0-2',
-                        message: Object.create(null, {
-                            '2key1': {
-                                value: '2value1',
-                                configurable: true,
-                                enumerable: true
-                            },
-                            '2key2': {
-                                value: '2value2',
-                                configurable: true,
-                                enumerable: true
-                            }
-                        })
-                    }]
-                }]
-            );
-        });
-    });
-
-    it('transformSortedSetWithScoresReply', () => {
-        assert.deepEqual(
-            transformSortedSetWithScoresReply(['member1', '0.5', 'member2', '+inf', 'member3', '-inf']),
-            [{
-                value: 'member1',
-                score: 0.5
-            }, {
-                value: 'member2',
-                score: Infinity
-            }, {
-                value: 'member3',
-                score: -Infinity
-            }]
-        );
-    });
-
-    describe('pushGeoCountArgument', () => {
-        it('undefined', () => {
-            assert.deepEqual(
-                pushGeoCountArgument([], undefined),
-                []
-            );
-        });
-
-        it('number', () => {
-            assert.deepEqual(
-                pushGeoCountArgument([], 1),
-                ['COUNT', '1']
-            );
-        });
-
-        describe('with COUNT', () => {
-            it('number', () => {
-                assert.deepEqual(
-                    pushGeoCountArgument([], 1),
-                    ['COUNT', '1']
-                );
-            });
-
-            describe('object', () => {
-                it('value', () => {
-                    assert.deepEqual(
-                        pushGeoCountArgument([], { value: 1 }),
-                        ['COUNT', '1']
-                    );
-                });
-
-                it('value, ANY', () => {
-                    assert.deepEqual(
-                        pushGeoCountArgument([], {
-                            value: 1,
-                            ANY: true
-                        }),
-                        ['COUNT', '1', 'ANY']
-                    );
-                });
-            });
-        });
-    });
-
-    describe('pushGeoSearchArguments', () => {
-        it('FROMMEMBER, BYRADIUS', () => {
-            assert.deepEqual(
-                pushGeoSearchArguments([], 'key', 'member', {
-                    radius: 1,
-                    unit: 'm'
-                }),
-                ['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
-            );
-        });
-
-        it('FROMLONLAT, BYBOX', () => {
-            assert.deepEqual(
-                pushGeoSearchArguments([], 'key', {
-                    longitude: 1,
-                    latitude: 2
-                }, {
-                    width: 1,
-                    height: 2,
-                    unit: 'm'
-                }),
-                ['key', 'FROMLONLAT', '1', '2', 'BYBOX', '1', '2', 'm']
-            );
-        });
-
-        it('with SORT', () => {
-            assert.deepEqual(
-                pushGeoSearchArguments([], 'key', 'member', {
-                    radius: 1,
-                    unit: 'm'
-                }, {
-                    SORT: 'ASC'
-                }),
-                ['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC']
-            );
-        });
-    });
-
-    describe('transformGeoMembersWithReply', () => {
-        it('DISTANCE', () => {
-            assert.deepEqual(
-                transformGeoMembersWithReply([
-                    [
-                        '1',
-                        '2'
-                    ],
-                    [
-                        '3',
-                        '4'
-                    ]
-                ], [GeoReplyWith.DISTANCE]),
-                [{
-                    member: '1',
-                    distance: '2'
-                }, {
-                    member: '3',
-                    distance: '4'
-                }]
-            );
-        });
-
-        it('HASH', () => {
-            assert.deepEqual(
-                transformGeoMembersWithReply([
-                    [
-                        '1',
-                        2
-                    ],
-                    [
-                        '3',
-                        4
-                    ]
-                ], [GeoReplyWith.HASH]),
-                [{
-                    member: '1',
-                    hash: 2
-                }, {
-                    member: '3',
-                    hash: 4
-                }]
-            );
-        });
-
-        it('COORDINATES', () => {
-            assert.deepEqual(
-                transformGeoMembersWithReply([
-                    [
-                        '1',
-                        [
-                            '2',
-                            '3'
-                        ]
-                    ],
-                    [
-                        '4',
-                        [
-                            '5',
-                            '6'
-                        ]
-                    ]
-                ], [GeoReplyWith.COORDINATES]),
-                [{
-                    member: '1',
-                    coordinates: {
-                        longitude: '2',
-                        latitude: '3'
-                    }
-                }, {
-                    member: '4',
-                    coordinates: {
-                        longitude: '5',
-                        latitude: '6'
-                    }
-                }]
-            );
-        });
-
-        it('DISTANCE, HASH, COORDINATES', () => {
-            assert.deepEqual(
-                transformGeoMembersWithReply([
-                    [
-                        '1',
-                        '2',
-                        3,
-                        [
-                            '4',
-                            '5'
-                        ]
-                    ],
-                    [
-                        '6',
-                        '7',
-                        8,
-                        [
-                            '9',
-                            '10'
-                        ]
-                    ]
-                ], [GeoReplyWith.DISTANCE, GeoReplyWith.HASH, GeoReplyWith.COORDINATES]),
-                [{
-                    member: '1',
-                    distance: '2',
-                    hash: 3,
-                    coordinates: {
-                        longitude: '4',
-                        latitude: '5'
-                    }
-                }, {
-                    member: '6',
-                    distance: '7',
-                    hash: 8,
-                    coordinates: {
-                        longitude: '9',
-                        latitude: '10'
-                    }
-                }]
-            );
-        });
-    });
-
-    describe('transformEXAT', () => {
-        it('number', () => {
-            assert.equal(
-                transformEXAT(1),
-                '1'
-            );
-        });
-
-        it('date', () => {
-            const d = new Date();
-            assert.equal(
-                transformEXAT(d),
-                Math.floor(d.getTime() / 1000).toString()
-            );
-        });
-    });
-
-    describe('transformPXAT', () => {
-        it('number', () => {
-            assert.equal(
-                transformPXAT(1),
-                '1'
-            );
-        });
-
-        it('date', () => {
-            const d = new Date();
-            assert.equal(
-                transformPXAT(d),
-                d.getTime().toString()
-            );
-        });
-    });
-
-    describe('pushEvalArguments', () => {
-        it('empty', () => {
-            assert.deepEqual(
-                pushEvalArguments([]),
-                ['0']
-            );
-        });
-
-        it('with keys', () => {
-            assert.deepEqual(
-                pushEvalArguments([], {
-                    keys: ['key']
-                }),
-                ['1', 'key']
-            );
-        });
-
-        it('with arguments', () => {
-            assert.deepEqual(
-                pushEvalArguments([], {
-                    arguments: ['argument']
-                }),
-                ['0', 'argument']
-            );
-        });
-
-        it('with keys and arguments', () => {
-            assert.deepEqual(
-                pushEvalArguments([], {
-                    keys: ['key'],
-                    arguments: ['argument']
-                }),
-                ['1', 'key', 'argument']
-            );
-        });
-    });
-
-    describe('pushVerdictArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                pushVerdictArguments([], 'string'),
-                ['string']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                pushVerdictArguments([], ['1', '2']),
-                ['1', '2']
-            );
-        });
-    });
-
-    describe('pushVerdictNumberArguments', () => {
-        it('number', () => {
-            assert.deepEqual(
-                pushVerdictNumberArguments([], 0),
-                ['0']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                pushVerdictNumberArguments([], [0, 1]),
-                ['0', '1']
-            );
-        });
-    });
-
-    describe('pushVerdictArgument', () => {
-        it('string', () => {
-            assert.deepEqual(
-                pushVerdictArgument([], 'string'),
-                ['1', 'string']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                pushVerdictArgument([], ['1', '2']),
-                ['2', '1', '2']
-            );
-        });
-    });
-
-    describe('pushOptionalVerdictArgument', () => {
-        it('undefined', () => {
-            assert.deepEqual(
-                pushOptionalVerdictArgument([], 'name', undefined),
-                []
-            );
-        });
-
-        it('string', () => {
-            assert.deepEqual(
-                pushOptionalVerdictArgument([], 'name', 'string'),
-                ['name', '1', 'string']
-            );
-        });
-
-        it('array', () => {
-            assert.deepEqual(
-                pushOptionalVerdictArgument([], 'name', ['1', '2']),
-                ['name', '2', '1', '2']
-            );
-        });
-    });
-
-    it('transformCommandReply', () => {
-        assert.deepEqual(
-            transformCommandReply([
-                'ping',
-                -1,
-                [CommandFlags.STALE, CommandFlags.FAST],
-                0,
-                0,
-                0,
-                [CommandCategories.FAST, CommandCategories.CONNECTION]
-            ]),
-            {
-                name: 'ping',
-                arity: -1,
-                flags: new Set([CommandFlags.STALE, CommandFlags.FAST]),
-                firstKeyIndex: 0,
-                lastKeyIndex: 0,
-                step: 0,
-                categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION])
-            }
-        );
-    });
-
-    describe('pushSlotRangesArguments', () => {
-        it('single range', () => {
-            assert.deepEqual(
-                pushSlotRangesArguments([], {
-                    start: 0,
-                    end: 1
-                }),
-                ['0', '1']
-            );
-        });
-
-        it('multiple ranges', () => {
-            assert.deepEqual(
-                pushSlotRangesArguments([], [{
-                    start: 0,
-                    end: 1
-                }, {
-                    start: 2,
-                    end: 3
-                }]),
-                ['0', '1', '2', '3']
-            );
-        });
-    });
-});
+// import { strict as assert } from 'node:assert';
+// import {
+//     transformBooleanReply,
+//     transformBooleanArrayReply,
+//     pushScanArguments,
+//     transformNumberInfinityReply,
+//     transformNumberInfinityNullReply,
+//     transformNumberInfinityArgument,
+//     transformStringNumberInfinityArgument,
+//     transformTuplesReply,
+//     transformStreamMessagesReply,
+//     transformStreamsMessagesReply,
+//     transformSortedSetWithScoresReply,
+//     pushGeoCountArgument,
+//     pushGeoSearchArguments,
+//     GeoReplyWith,
+//     transformGeoMembersWithReply,
+//     transformEXAT,
+//     transformPXAT,
+//     pushEvalArguments,
+//     pushVariadicArguments,
+//     pushVariadicNumberArguments,
+//     pushVariadicArgument,
+//     pushOptionalVariadicArgument,
+//     transformCommandReply,
+//     CommandFlags,
+//     CommandCategories,
+//     pushSlotRangesArguments
+// } from './generic-transformers';
+
+// describe('Generic Transformers', () => {
+//     describe('transformBooleanReply', () => {
+//         it('0', () => {
+//             assert.equal(
+//                 transformBooleanReply(0),
+//                 false
+//             );
+//         });
+
+//         it('1', () => {
+//             assert.equal(
+//                 transformBooleanReply(1),
+//                 true
+//             );
+//         });
+//     });
+
+//     describe('transformBooleanArrayReply', () => {
+//         it('empty array', () => {
+//             assert.deepEqual(
+//                 transformBooleanArrayReply([]),
+//                 []
+//             );
+//         });
+
+//         it('0, 1', () => {
+//             assert.deepEqual(
+//                 transformBooleanArrayReply([0, 1]),
+//                 [false, true]
+//             );
+//         });
+//     });
+
+//     describe('pushScanArguments', () => {
+//         it('cusror only', () => {
+//             assert.deepEqual(
+//                 pushScanArguments([], 0),
+//                 ['0']
+//             );
+//         });
+
+//         it('with MATCH', () => {
+//             assert.deepEqual(
+//                 pushScanArguments([], 0, {
+//                     MATCH: 'pattern'
+//                 }),
+//                 ['0', 'MATCH', 'pattern']
+//             );
+//         });
+
+//         it('with COUNT', () => {
+//             assert.deepEqual(
+//                 pushScanArguments([], 0, {
+//                     COUNT: 1
+//                 }),
+//                 ['0', 'COUNT', '1']
+//             );
+//         });
+
+//         it('with MATCH & COUNT', () => {
+//             assert.deepEqual(
+//                 pushScanArguments([], 0, {
+//                     MATCH: 'pattern',
+//                     COUNT: 1
+//                 }),
+//                 ['0', 'MATCH', 'pattern', 'COUNT', '1']
+//             );
+//         });
+//     });
+
+//     describe('transformNumberInfinityReply', () => {
+//         it('0.5', () => {
+//             assert.equal(
+//                 transformNumberInfinityReply('0.5'),
+//                 0.5
+//             );
+//         });
+
+//         it('+inf', () => {
+//             assert.equal(
+//                 transformNumberInfinityReply('+inf'),
+//                 Infinity
+//             );
+//         });
+
+//         it('-inf', () => {
+//             assert.equal(
+//                 transformNumberInfinityReply('-inf'),
+//                 -Infinity
+//             );
+//         });
+//     });
+
+//     describe('transformNumberInfinityNullReply', () => {
+//         it('null', () => {
+//             assert.equal(
+//                 transformNumberInfinityNullReply(null),
+//                 null
+//             );
+//         });
+
+//         it('1', () => {
+//             assert.equal(
+//                 transformNumberInfinityNullReply('1'),
+//                 1
+//             );
+//         });
+//     });
+
+//     describe('transformNumberInfinityArgument', () => {
+//         it('0.5', () => {
+//             assert.equal(
+//                 transformNumberInfinityArgument(0.5),
+//                 '0.5'
+//             );
+//         });
+
+//         it('Infinity', () => {
+//             assert.equal(
+//                 transformNumberInfinityArgument(Infinity),
+//                 '+inf'
+//             );
+//         });
+
+//         it('-Infinity', () => {
+//             assert.equal(
+//                 transformNumberInfinityArgument(-Infinity),
+//                 '-inf'
+//             );
+//         });
+//     });
+
+//     describe('transformStringNumberInfinityArgument', () => {
+//         it("'0.5'", () => {
+//             assert.equal(
+//                 transformStringNumberInfinityArgument('0.5'),
+//                 '0.5'
+//             );
+//         });
+
+//         it('0.5', () => {
+//             assert.equal(
+//                 transformStringNumberInfinityArgument(0.5),
+//                 '0.5'
+//             );
+//         });
+//     });
+
+//     it('transformTuplesReply', () => {
+//         assert.deepEqual(
+//             transformTuplesReply(['key1', 'value1', 'key2', 'value2']),
+//             Object.create(null, {
+//                 key1: {
+//                     value: 'value1',
+//                     configurable: true,
+//                     enumerable: true
+//                 },
+//                 key2: {
+//                     value: 'value2',
+//                     configurable: true,
+//                     enumerable: true
+//                 }
+//             })
+//         );
+//     });
+
+//     it('transformStreamMessagesReply', () => {
+//         assert.deepEqual(
+//             transformStreamMessagesReply([['0-0', ['0key', '0value']], ['1-0', ['1key', '1value']]]),
+//             [{
+//                 id: '0-0',
+//                 message: Object.create(null, {
+//                     '0key': {
+//                         value: '0value',
+//                         configurable: true,
+//                         enumerable: true
+//                     }
+//                 })
+//             }, {
+//                 id: '1-0',
+//                 message: Object.create(null, {
+//                     '1key': {
+//                         value: '1value',
+//                         configurable: true,
+//                         enumerable: true
+//                     }
+//                 })
+//             }]
+//         );
+//     });
+
+//     describe('transformStreamsMessagesReply', () => {
+//         it('null', () => {
+//             assert.equal(
+//                 transformStreamsMessagesReply(null),
+//                 null
+//             );
+//         });
+
+//         it('with messages', () => {
+//             assert.deepEqual(
+//                 transformStreamsMessagesReply([['stream1', [['0-1', ['11key', '11value']], ['1-1', ['12key', '12value']]]], ['stream2', [['0-2', ['2key1', '2value1', '2key2', '2value2']]]]]),
+//                 [{
+//                     name: 'stream1',
+//                     messages: [{
+//                         id: '0-1',
+//                         message: Object.create(null, {
+//                             '11key': {
+//                                 value: '11value',
+//                                 configurable: true,
+//                                 enumerable: true
+//                             }
+//                         })
+//                     }, {
+//                         id: '1-1',
+//                         message: Object.create(null, {
+//                             '12key': {
+//                                 value: '12value',
+//                                 configurable: true,
+//                                 enumerable: true
+//                             }
+//                         })
+//                     }]
+//                 }, {
+//                     name: 'stream2',
+//                     messages: [{
+//                         id: '0-2',
+//                         message: Object.create(null, {
+//                             '2key1': {
+//                                 value: '2value1',
+//                                 configurable: true,
+//                                 enumerable: true
+//                             },
+//                             '2key2': {
+//                                 value: '2value2',
+//                                 configurable: true,
+//                                 enumerable: true
+//                             }
+//                         })
+//                     }]
+//                 }]
+//             );
+//         });
+//     });
+
+//     it('transformSortedSetWithScoresReply', () => {
+//         assert.deepEqual(
+//             transformSortedSetWithScoresReply(['member1', '0.5', 'member2', '+inf', 'member3', '-inf']),
+//             [{
+//                 value: 'member1',
+//                 score: 0.5
+//             }, {
+//                 value: 'member2',
+//                 score: Infinity
+//             }, {
+//                 value: 'member3',
+//                 score: -Infinity
+//             }]
+//         );
+//     });
+
+//     describe('pushGeoCountArgument', () => {
+//         it('undefined', () => {
+//             assert.deepEqual(
+//                 pushGeoCountArgument([], undefined),
+//                 []
+//             );
+//         });
+
+//         it('number', () => {
+//             assert.deepEqual(
+//                 pushGeoCountArgument([], 1),
+//                 ['COUNT', '1']
+//             );
+//         });
+
+//         describe('with COUNT', () => {
+//             it('number', () => {
+//                 assert.deepEqual(
+//                     pushGeoCountArgument([], 1),
+//                     ['COUNT', '1']
+//                 );
+//             });
+
+//             describe('object', () => {
+//                 it('value', () => {
+//                     assert.deepEqual(
+//                         pushGeoCountArgument([], { value: 1 }),
+//                         ['COUNT', '1']
+//                     );
+//                 });
+
+//                 it('value, ANY', () => {
+//                     assert.deepEqual(
+//                         pushGeoCountArgument([], {
+//                             value: 1,
+//                             ANY: true
+//                         }),
+//                         ['COUNT', '1', 'ANY']
+//                     );
+//                 });
+//             });
+//         });
+//     });
+
+//     describe('pushGeoSearchArguments', () => {
+//         it('FROMMEMBER, BYRADIUS', () => {
+//             assert.deepEqual(
+//                 pushGeoSearchArguments([], 'key', 'member', {
+//                     radius: 1,
+//                     unit: 'm'
+//                 }),
+//                 ['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
+//             );
+//         });
+
+//         it('FROMLONLAT, BYBOX', () => {
+//             assert.deepEqual(
+//                 pushGeoSearchArguments([], 'key', {
+//                     longitude: 1,
+//                     latitude: 2
+//                 }, {
+//                     width: 1,
+//                     height: 2,
+//                     unit: 'm'
+//                 }),
+//                 ['key', 'FROMLONLAT', '1', '2', 'BYBOX', '1', '2', 'm']
+//             );
+//         });
+
+//         it('with SORT', () => {
+//             assert.deepEqual(
+//                 pushGeoSearchArguments([], 'key', 'member', {
+//                     radius: 1,
+//                     unit: 'm'
+//                 }, {
+//                     SORT: 'ASC'
+//                 }),
+//                 ['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC']
+//             );
+//         });
+//     });
+
+//     describe('transformGeoMembersWithReply', () => {
+//         it('DISTANCE', () => {
+//             assert.deepEqual(
+//                 transformGeoMembersWithReply([
+//                     [
+//                         '1',
+//                         '2'
+//                     ],
+//                     [
+//                         '3',
+//                         '4'
+//                     ]
+//                 ], [GeoReplyWith.DISTANCE]),
+//                 [{
+//                     member: '1',
+//                     distance: '2'
+//                 }, {
+//                     member: '3',
+//                     distance: '4'
+//                 }]
+//             );
+//         });
+
+//         it('HASH', () => {
+//             assert.deepEqual(
+//                 transformGeoMembersWithReply([
+//                     [
+//                         '1',
+//                         2
+//                     ],
+//                     [
+//                         '3',
+//                         4
+//                     ]
+//                 ], [GeoReplyWith.HASH]),
+//                 [{
+//                     member: '1',
+//                     hash: 2
+//                 }, {
+//                     member: '3',
+//                     hash: 4
+//                 }]
+//             );
+//         });
+
+//         it('COORDINATES', () => {
+//             assert.deepEqual(
+//                 transformGeoMembersWithReply([
+//                     [
+//                         '1',
+//                         [
+//                             '2',
+//                             '3'
+//                         ]
+//                     ],
+//                     [
+//                         '4',
+//                         [
+//                             '5',
+//                             '6'
+//                         ]
+//                     ]
+//                 ], [GeoReplyWith.COORDINATES]),
+//                 [{
+//                     member: '1',
+//                     coordinates: {
+//                         longitude: '2',
+//                         latitude: '3'
+//                     }
+//                 }, {
+//                     member: '4',
+//                     coordinates: {
+//                         longitude: '5',
+//                         latitude: '6'
+//                     }
+//                 }]
+//             );
+//         });
+
+//         it('DISTANCE, HASH, COORDINATES', () => {
+//             assert.deepEqual(
+//                 transformGeoMembersWithReply([
+//                     [
+//                         '1',
+//                         '2',
+//                         3,
+//                         [
+//                             '4',
+//                             '5'
+//                         ]
+//                     ],
+//                     [
+//                         '6',
+//                         '7',
+//                         8,
+//                         [
+//                             '9',
+//                             '10'
+//                         ]
+//                     ]
+//                 ], [GeoReplyWith.DISTANCE, GeoReplyWith.HASH, GeoReplyWith.COORDINATES]),
+//                 [{
+//                     member: '1',
+//                     distance: '2',
+//                     hash: 3,
+//                     coordinates: {
+//                         longitude: '4',
+//                         latitude: '5'
+//                     }
+//                 }, {
+//                     member: '6',
+//                     distance: '7',
+//                     hash: 8,
+//                     coordinates: {
+//                         longitude: '9',
+//                         latitude: '10'
+//                     }
+//                 }]
+//             );
+//         });
+//     });
+
+//     describe('transformEXAT', () => {
+//         it('number', () => {
+//             assert.equal(
+//                 transformEXAT(1),
+//                 '1'
+//             );
+//         });
+
+//         it('date', () => {
+//             const d = new Date();
+//             assert.equal(
+//                 transformEXAT(d),
+//                 Math.floor(d.getTime() / 1000).toString()
+//             );
+//         });
+//     });
+
+//     describe('transformPXAT', () => {
+//         it('number', () => {
+//             assert.equal(
+//                 transformPXAT(1),
+//                 '1'
+//             );
+//         });
+
+//         it('date', () => {
+//             const d = new Date();
+//             assert.equal(
+//                 transformPXAT(d),
+//                 d.getTime().toString()
+//             );
+//         });
+//     });
+
+//     describe('pushEvalArguments', () => {
+//         it('empty', () => {
+//             assert.deepEqual(
+//                 pushEvalArguments([]),
+//                 ['0']
+//             );
+//         });
+
+//         it('with keys', () => {
+//             assert.deepEqual(
+//                 pushEvalArguments([], {
+//                     keys: ['key']
+//                 }),
+//                 ['1', 'key']
+//             );
+//         });
+
+//         it('with arguments', () => {
+//             assert.deepEqual(
+//                 pushEvalArguments([], {
+//                     arguments: ['argument']
+//                 }),
+//                 ['0', 'argument']
+//             );
+//         });
+
+//         it('with keys and arguments', () => {
+//             assert.deepEqual(
+//                 pushEvalArguments([], {
+//                     keys: ['key'],
+//                     arguments: ['argument']
+//                 }),
+//                 ['1', 'key', 'argument']
+//             );
+//         });
+//     });
+
+//     describe('pushVariadicArguments', () => {
+//         it('string', () => {
+//             assert.deepEqual(
+//                 pushVariadicArguments([], 'string'),
+//                 ['string']
+//             );
+//         });
+
+//         it('array', () => {
+//             assert.deepEqual(
+//                 pushVariadicArguments([], ['1', '2']),
+//                 ['1', '2']
+//             );
+//         });
+//     });
+
+//     describe('pushVariadicNumberArguments', () => {
+//         it('number', () => {
+//             assert.deepEqual(
+//                 pushVariadicNumberArguments([], 0),
+//                 ['0']
+//             );
+//         });
+
+//         it('array', () => {
+//             assert.deepEqual(
+//                 pushVariadicNumberArguments([], [0, 1]),
+//                 ['0', '1']
+//             );
+//         });
+//     });
+
+//     describe('pushVariadicArgument', () => {
+//         it('string', () => {
+//             assert.deepEqual(
+//                 pushVariadicArgument([], 'string'),
+//                 ['1', 'string']
+//             );
+//         });
+
+//         it('array', () => {
+//             assert.deepEqual(
+//                 pushVariadicArgument([], ['1', '2']),
+//                 ['2', '1', '2']
+//             );
+//         });
+//     });
+
+//     describe('pushOptionalVariadicArgument', () => {
+//         it('undefined', () => {
+//             assert.deepEqual(
+//                 pushOptionalVariadicArgument([], 'name', undefined),
+//                 []
+//             );
+//         });
+
+//         it('string', () => {
+//             assert.deepEqual(
+//                 pushOptionalVariadicArgument([], 'name', 'string'),
+//                 ['name', '1', 'string']
+//             );
+//         });
+
+//         it('array', () => {
+//             assert.deepEqual(
+//                 pushOptionalVariadicArgument([], 'name', ['1', '2']),
+//                 ['name', '2', '1', '2']
+//             );
+//         });
+//     });
+
+//     it('transformCommandReply', () => {
+//         assert.deepEqual(
+//             transformCommandReply([
+//                 'ping',
+//                 -1,
+//                 [CommandFlags.STALE, CommandFlags.FAST],
+//                 0,
+//                 0,
+//                 0,
+//                 [CommandCategories.FAST, CommandCategories.CONNECTION]
+//             ]),
+//             {
+//                 name: 'ping',
+//                 arity: -1,
+//                 flags: new Set([CommandFlags.STALE, CommandFlags.FAST]),
+//                 firstKeyIndex: 0,
+//                 lastKeyIndex: 0,
+//                 step: 0,
+//                 categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION])
+//             }
+//         );
+//     });
+
+//     describe('pushSlotRangesArguments', () => {
+//         it('single range', () => {
+//             assert.deepEqual(
+//                 pushSlotRangesArguments([], {
+//                     start: 0,
+//                     end: 1
+//                 }),
+//                 ['0', '1']
+//             );
+//         });
+
+//         it('multiple ranges', () => {
+//             assert.deepEqual(
+//                 pushSlotRangesArguments([], [{
+//                     start: 0,
+//                     end: 1
+//                 }, {
+//                     start: 2,
+//                     end: 3
+//                 }]),
+//                 ['0', '1', '2', '3']
+//             );
+//         });
+//     });
+// });
diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts
index 4cf610a036e..022339e4bb7 100644
--- a/packages/client/lib/commands/generic-transformers.ts
+++ b/packages/client/lib/commands/generic-transformers.ts
@@ -1,697 +1,682 @@
-import { RedisCommandArgument, RedisCommandArguments } from '.';
+import { BasicCommandParser, CommandParser } from '../client/parser';
+import { RESP_TYPES } from '../RESP/decoder';
+import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, TuplesReply, MapReply, TypeMapping, Command } from '../RESP/types';
 
-export function transformBooleanReply(reply: number): boolean {
-    return reply === 1;
+export function isNullReply(reply: unknown): reply is NullReply {
+  return reply === null;
 }
 
-export function transformBooleanArrayReply(reply: Array<number>): Array<boolean> {
-    return reply.map(transformBooleanReply);
+export function isArrayReply(reply: unknown): reply is ArrayReply<unknown> {
+  return Array.isArray(reply);
 }
 
-export type BitValue = 0 | 1;
+export const transformBooleanReply = {
+  2: (reply: NumberReply<0 | 1>) => reply as unknown as UnwrapReply<typeof reply> === 1,
+  3: undefined as unknown as () => BooleanReply
+};
 
-export interface ScanOptions {
-    MATCH?: string;
-    COUNT?: number;
-}
+export const transformBooleanArrayReply = {
+  2: (reply: ArrayReply<NumberReply<0 | 1>>) => {
+    return (reply as unknown as UnwrapReply<typeof reply>).map(transformBooleanReply[2]);
+  },
+  3: undefined as unknown as () => ArrayReply<BooleanReply>
+};
 
-export function pushScanArguments(
-    args: RedisCommandArguments,
-    cursor: number,
-    options?: ScanOptions
-): RedisCommandArguments {
-    args.push(cursor.toString());
+export type BitValue = 0 | 1;
 
-    if (options?.MATCH) {
-        args.push('MATCH', options.MATCH);
-    }
+export function transformDoubleArgument(num: number): string {
+  switch (num) {
+    case Infinity:
+      return '+inf';
+
+    case -Infinity:
+      return '-inf';
+
+    default:
+      return num.toString();
+  }
+}
+
+export function transformStringDoubleArgument(num: RedisArgument | number): RedisArgument {
+  if (typeof num !== 'number') return num;
+
+  return transformDoubleArgument(num);
+}
+
+export const transformDoubleReply = {
+  2: (reply: BlobStringReply, preserve?: any, typeMapping?: TypeMapping): DoubleReply => {
+    const double = typeMapping ? typeMapping[RESP_TYPES.DOUBLE] : undefined;
+    
+    switch (double) {
+      case String: {
+        return reply as unknown as DoubleReply;
+      }
+      default: {
+        let ret: number;
+
+        switch (reply.toString()) {
+          case 'inf':
+          case '+inf':
+            ret = Infinity;
+      
+          case '-inf':
+            ret = -Infinity;
+    
+          case 'nan':
+            ret = NaN;
+      
+          default:
+            ret = Number(reply);
+        }
 
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
+        return ret as unknown as DoubleReply;
+      }
     }
+  },
+  3: undefined as unknown as () => DoubleReply
+};
 
-    return args;
+export function createTransformDoubleReplyResp2Func(preserve?: any, typeMapping?: TypeMapping) {
+  return (reply: BlobStringReply) => {
+    return transformDoubleReply[2](reply, preserve, typeMapping);
+  }
 }
 
-export function transformNumberInfinityReply(reply: RedisCommandArgument): number {
-    switch (reply.toString()) {
-        case '+inf':
-            return Infinity;
-
-        case '-inf':
-            return -Infinity;
+export const transformDoubleArrayReply = {
+  2: (reply: Array<BlobStringReply>, preserve?: any, typeMapping?: TypeMapping) => {
+    return reply.map(createTransformDoubleReplyResp2Func(preserve, typeMapping));
+  },
+  3: undefined as unknown as () => ArrayReply<DoubleReply>
+}
 
-        default:
-            return Number(reply);
-    }
+export function createTransformNullableDoubleReplyResp2Func(preserve?: any, typeMapping?: TypeMapping) {
+  return (reply: BlobStringReply | NullReply) => {
+    return transformNullableDoubleReply[2](reply, preserve, typeMapping);
+  }
 }
 
-export function transformNumberInfinityNullReply(reply: RedisCommandArgument | null): number | null {
+export const transformNullableDoubleReply = {
+  2: (reply: BlobStringReply | NullReply, preserve?: any, typeMapping?: TypeMapping) => {
     if (reply === null) return null;
+  
+    return transformDoubleReply[2](reply as BlobStringReply, preserve, typeMapping);
+  },
+  3: undefined as unknown as () => DoubleReply | NullReply
+};
 
-    return transformNumberInfinityReply(reply);
-}
-
-export function transformNumberInfinityNullArrayReply(reply: Array<RedisCommandArgument | null>): Array<number | null> {
-    return reply.map(transformNumberInfinityNullReply);
+export interface Stringable {
+  toString(): string;
 }
 
-export function transformNumberInfinityArgument(num: number): string {
-    switch (num) {
-        case Infinity:
-            return '+inf';
+export function transformTuplesToMap<T>(
+  reply: UnwrapReply<ArrayReply<any>>,
+  func: (elem: any) => T,
+) {
+  const message = Object.create(null);
 
-        case -Infinity:
-            return '-inf';
+  for (let i = 0; i < reply.length; i+= 2) {
+    message[reply[i].toString()] = func(reply[i + 1]);
+  }
 
-        default:
-            return num.toString();
-    }
+  return message;
 }
 
-export function transformStringNumberInfinityArgument(num: RedisCommandArgument | number): RedisCommandArgument {
-    if (typeof num !== 'number') return num;
-
-    return transformNumberInfinityArgument(num);
+export function createTransformTuplesReplyFunc<T extends Stringable>(preserve?: any, typeMapping?: TypeMapping) {
+  return (reply: ArrayReply<T>) => {
+    return transformTuplesReply<T>(reply, preserve, typeMapping);
+  };
 }
 
-export function transformTuplesReply(
-    reply: Array<RedisCommandArgument>
-): Record<string, RedisCommandArgument> {
-    const message = Object.create(null);
-
-    for (let i = 0; i < reply.length; i += 2) {
-        message[reply[i].toString()] = reply[i + 1];
-    }
+export function transformTuplesReply<T extends Stringable>(
+  reply: ArrayReply<T>,
+  preserve?: any,
+  typeMapping?: TypeMapping
+): MapReply<T , T> {
+  const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined;
 
-    return message;
-}
+  const inferred = reply as unknown as UnwrapReply<typeof reply>
 
-export interface StreamMessageReply {
-    id: RedisCommandArgument;
-    message: Record<string, RedisCommandArgument>;
-}
+  switch (mapType) {
+    case Array: {
+      return reply as unknown as MapReply<T, T>;
+    }
+    case Map: {
+      const ret = new Map<string, BlobStringReply>;
 
-export function transformStreamMessageReply([id, message]: Array<any>): StreamMessageReply {
-    return {
-        id,
-        message: transformTuplesReply(message)
-    };
-}
+      for (let i = 0; i < inferred.length; i += 2) {
+        ret.set(inferred[i].toString(), inferred[i + 1] as any);
+      }
 
-export function transformStreamMessageNullReply(reply: Array<any>): StreamMessageReply | null {
-    if (reply === null) return null;
-    return transformStreamMessageReply(reply);
-}
+      return ret as unknown as MapReply<T, T>;;
+    }
+    default: {
+      const ret: Record<string, BlobStringReply> = Object.create(null);
 
+      for (let i = 0; i < inferred.length; i += 2) {
+        ret[inferred[i].toString()] = inferred[i + 1] as any;
+      }
 
-export type StreamMessagesReply = Array<StreamMessageReply>;
-export function transformStreamMessagesReply(reply: Array<any>): StreamMessagesReply {
-    return reply.map(transformStreamMessageReply);
+      return ret as unknown as MapReply<T, T>;;
+    }
+  }
 }
 
-export type StreamMessagesNullReply = Array<StreamMessageReply | null>;
-export function transformStreamMessagesNullReply(reply: Array<any>): StreamMessagesNullReply {
-    return reply.map(transformStreamMessageNullReply);
+export interface SortedSetMember {
+  value: RedisArgument;
+  score: number;
 }
 
-export type StreamsMessagesReply = Array<{
-    name: RedisCommandArgument;
-    messages: StreamMessagesReply;
-}> | null;
-
-export function transformStreamsMessagesReply(reply: Array<any> | null): StreamsMessagesReply | null {
-    if (reply === null) return null;
+export type SortedSetSide = 'MIN' | 'MAX';
 
-    return reply.map(([name, rawMessages]) => ({
-        name,
-        messages: transformStreamMessagesReply(rawMessages)
-    }));
-}
+export const transformSortedSetReply = {
+  2: (reply: ArrayReply<BlobStringReply>, preserve?: any, typeMapping?: TypeMapping) => {
+    const inferred = reply as unknown as UnwrapReply<typeof reply>,
+      members = [];
+    for (let i = 0; i < inferred.length; i += 2) {
+      members.push({
+        value: inferred[i],
+        score: transformDoubleReply[2](inferred[i + 1], preserve, typeMapping)
+      });
+    }
 
-export interface ZMember {
-    score: number;
-    value: RedisCommandArgument;
+    return members;
+  },
+  3: (reply: ArrayReply<TuplesReply<[BlobStringReply, DoubleReply]>>) => {
+    return (reply as unknown as UnwrapReply<typeof reply>).map(member => {
+      const [value, score] = member as unknown as UnwrapReply<typeof member>;
+      return {
+        value,
+        score
+      };
+    });
+  }
 }
 
-export function transformSortedSetMemberNullReply(
-    reply: [RedisCommandArgument, RedisCommandArgument] | []
-): ZMember | null {
-    if (!reply.length) return null;
+export type ListSide = 'LEFT' | 'RIGHT';
 
-    return transformSortedSetMemberReply(reply);
+export function transformEXAT(EXAT: number | Date): string {
+  return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString();
 }
 
-export function transformSortedSetMemberReply(
-    reply: [RedisCommandArgument, RedisCommandArgument]
-): ZMember {
-    return {
-        value: reply[0],
-        score: transformNumberInfinityReply(reply[1])
-    };
+export function transformPXAT(PXAT: number | Date): string {
+  return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString();
 }
 
-export function transformSortedSetWithScoresReply(reply: Array<RedisCommandArgument>): Array<ZMember> {
-    const members = [];
-
-    for (let i = 0; i < reply.length; i += 2) {
-        members.push({
-            value: reply[i],
-            score: transformNumberInfinityReply(reply[i + 1])
-        });
-    }
-
-    return members;
+export interface EvalOptions {
+  keys?: Array<string>;
+  arguments?: Array<string>;
 }
 
-export type SortedSetSide = 'MIN' | 'MAX';
-
-export interface ZMPopOptions {
-    COUNT?: number;
+export function evalFirstKeyIndex(options?: EvalOptions): string | undefined {
+  return options?.keys?.[0];
 }
 
-export function transformZMPopArguments(
-    args: RedisCommandArguments,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    side: SortedSetSide,
-    options?: ZMPopOptions
-): RedisCommandArguments {
-    pushVerdictArgument(args, keys);
-
-    args.push(side);
+export function pushEvalArguments(args: Array<string>, options?: EvalOptions): Array<string> {
+  if (options?.keys) {
+    args.push(
+      options.keys.length.toString(),
+      ...options.keys
+    );
+  } else {
+    args.push('0');
+  }
 
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
-    }
+  if (options?.arguments) {
+    args.push(...options.arguments);
+  }
 
-    return args;
+  return args;
 }
 
-export type ListSide = 'LEFT' | 'RIGHT';
+export function pushVariadicArguments(args: CommandArguments, value: RedisVariadicArgument): CommandArguments {
+  if (Array.isArray(value)) {
+    // https://github.com/redis/node-redis/pull/2160
+    args = args.concat(value);
+  } else {
+    args.push(value);
+  }
 
-export interface LMPopOptions {
-    COUNT?: number;
+  return args;
 }
 
-export function transformLMPopArguments(
-    args: RedisCommandArguments,
-    keys: RedisCommandArgument | Array<RedisCommandArgument>,
-    side: ListSide,
-    options?: LMPopOptions
-): RedisCommandArguments {
-    pushVerdictArgument(args, keys);
-
-    args.push(side);
-
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
+export function pushVariadicNumberArguments(
+  args: CommandArguments,
+  value: number | Array<number>
+): CommandArguments {
+  if (Array.isArray(value)) {
+    for (const item of value) {
+      args.push(item.toString());
     }
+  } else {
+    args.push(value.toString());
+  }
 
-    return args;
+  return args;
 }
 
-type GeoCountArgument = number | {
-    value: number;
-    ANY?: true
-};
+export type RedisVariadicArgument = RedisArgument | Array<RedisArgument>;
 
-export function pushGeoCountArgument(
-    args: RedisCommandArguments,
-    count: GeoCountArgument | undefined
-): RedisCommandArguments {
-    if (typeof count === 'number') {
-        args.push('COUNT', count.toString());
-    } else if (count) {
-        args.push('COUNT', count.value.toString());
-
-        if (count.ANY) {
-            args.push('ANY');
-        }
-    }
+export function pushVariadicArgument(
+  args: Array<RedisArgument>,
+  value: RedisVariadicArgument
+): CommandArguments {
+  if (Array.isArray(value)) {
+    args.push(value.length.toString(), ...value);
+  } else {
+    args.push('1', value);
+  }
 
-    return args;
+  return args;
 }
 
-export type GeoUnits = 'm' | 'km' | 'mi' | 'ft';
+export function parseOptionalVariadicArgument(
+  parser: CommandParser,
+  name: RedisArgument,
+  value?: RedisVariadicArgument
+) {
+  if (value === undefined) return;
 
-export interface GeoCoordinates {
-    longitude: string | number;
-    latitude: string | number;
-}
-
-type GeoSearchFromMember = string;
-
-export type GeoSearchFrom = GeoSearchFromMember | GeoCoordinates;
+  parser.push(name);
 
-interface GeoSearchByRadius {
-    radius: number;
-    unit: GeoUnits;
+  parser.pushVariadicWithLength(value);
 }
 
-interface GeoSearchByBox {
-    width: number;
-    height: number;
-    unit: GeoUnits;
+export enum CommandFlags {
+  WRITE = 'write', // command may result in modifications
+  READONLY = 'readonly', // command will never modify keys
+  DENYOOM = 'denyoom', // reject command if currently out of memory
+  ADMIN = 'admin', // server admin command
+  PUBSUB = 'pubsub', // pubsub-related command
+  NOSCRIPT = 'noscript', // deny this command from scripts
+  RANDOM = 'random', // command has random results, dangerous for scripts
+  SORT_FOR_SCRIPT = 'sort_for_script', // if called from script, sort output
+  LOADING = 'loading', // allow command while database is loading
+  STALE = 'stale', // allow command while replica has stale data
+  SKIP_MONITOR = 'skip_monitor', // do not show this command in MONITOR
+  ASKING = 'asking', // cluster related - accept even if importing
+  FAST = 'fast', // command operates in constant or log(N) time. Used for latency monitoring.
+  MOVABLEKEYS = 'movablekeys' // keys have no pre-determined position. You must discover keys yourself.
 }
 
-export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox;
-
-export interface GeoSearchOptions {
-    SORT?: 'ASC' | 'DESC';
-    COUNT?: GeoCountArgument;
+export enum CommandCategories {
+  KEYSPACE = '@keyspace',
+  READ = '@read',
+  WRITE = '@write',
+  SET = '@set',
+  SORTEDSET = '@sortedset',
+  LIST = '@list',
+  HASH = '@hash',
+  STRING = '@string',
+  BITMAP = '@bitmap',
+  HYPERLOGLOG = '@hyperloglog',
+  GEO = '@geo',
+  STREAM = '@stream',
+  PUBSUB = '@pubsub',
+  ADMIN = '@admin',
+  FAST = '@fast',
+  SLOW = '@slow',
+  BLOCKING = '@blocking',
+  DANGEROUS = '@dangerous',
+  CONNECTION = '@connection',
+  TRANSACTION = '@transaction',
+  SCRIPTING = '@scripting'
 }
 
-export function pushGeoSearchArguments(
-    args: RedisCommandArguments,
-    key: RedisCommandArgument,
-    from: GeoSearchFrom,
-    by: GeoSearchBy,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    args.push(key);
-
-    if (typeof from === 'string') {
-        args.push('FROMMEMBER', from);
-    } else {
-        args.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString());
-    }
-
-    if ('radius' in by) {
-        args.push('BYRADIUS', by.radius.toString());
-    } else {
-        args.push('BYBOX', by.width.toString(), by.height.toString());
-    }
-
-    args.push(by.unit);
-
-    if (options?.SORT) {
-        args.push(options.SORT);
-    }
+export type CommandRawReply = [
+  name: string,
+  arity: number,
+  flags: Array<CommandFlags>,
+  firstKeyIndex: number,
+  lastKeyIndex: number,
+  step: number,
+  categories: Array<CommandCategories>
+];
 
-    pushGeoCountArgument(args, options?.COUNT);
+export type CommandReply = {
+  name: string,
+  arity: number,
+  flags: Set<CommandFlags>,
+  firstKeyIndex: number,
+  lastKeyIndex: number,
+  step: number,
+  categories: Set<CommandCategories>
+};
 
-    return args;
+export function transformCommandReply(
+  this: void,
+  [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply
+): CommandReply {
+  return {
+    name,
+    arity,
+    flags: new Set(flags),
+    firstKeyIndex,
+    lastKeyIndex,
+    step,
+    categories: new Set(categories)
+  };
 }
 
-export function pushGeoRadiusArguments(
-    args: RedisCommandArguments,
-    key: RedisCommandArgument,
-    from: GeoSearchFrom,
-    radius: number,
-    unit: GeoUnits,
-    options?: GeoSearchOptions
-): RedisCommandArguments {
-    args.push(key);
-
-    if (typeof from === 'string') {
-        args.push(from);
-    } else {
-        args.push(
-            from.longitude.toString(),
-            from.latitude.toString()
-        );
-    }
-
-    args.push(
-        radius.toString(),
-        unit
-    );
-
-    if (options?.SORT) {
-        args.push(options.SORT);
-    }
+export enum RedisFunctionFlags {
+  NO_WRITES = 'no-writes',
+  ALLOW_OOM = 'allow-oom',
+  ALLOW_STALE = 'allow-stale',
+  NO_CLUSTER = 'no-cluster'
+}
 
-    pushGeoCountArgument(args, options?.COUNT);
+export type FunctionListRawItemReply = [
+  'library_name',
+  string,
+  'engine',
+  string,
+  'functions',
+  Array<[
+    'name',
+    string,
+    'description',
+    string | null,
+    'flags',
+    Array<RedisFunctionFlags>
+  ]>
+];
 
-    return args;
+export interface FunctionListItemReply {
+  libraryName: string;
+  engine: string;
+  functions: Array<{
+    name: string;
+    description: string | null;
+    flags: Array<RedisFunctionFlags>;
+  }>;
 }
 
-export interface GeoRadiusStoreOptions extends GeoSearchOptions {
-    STOREDIST?: boolean;
+export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply {
+  return {
+    libraryName: reply[1],
+    engine: reply[3],
+    functions: reply[5].map(fn => ({
+      name: fn[1],
+      description: fn[3],
+      flags: fn[5]
+    }))
+  };
 }
 
-export function pushGeoRadiusStoreArguments(
-    args: RedisCommandArguments,
-    key: RedisCommandArgument,
-    from: GeoSearchFrom,
-    radius: number,
-    unit: GeoUnits,
-    destination: RedisCommandArgument,
-    options?: GeoRadiusStoreOptions
-): RedisCommandArguments {
-    pushGeoRadiusArguments(args, key, from, radius, unit, options);
+export interface SlotRange {
+  start: number;
+  end: number;
+}
 
-    if (options?.STOREDIST) {
-        args.push('STOREDIST', destination);
-    } else {
-        args.push('STORE', destination);
+function parseSlotRangeArguments(
+  parser: CommandParser,
+  range: SlotRange
+): void {
+  parser.push(
+    range.start.toString(),
+    range.end.toString()
+  );
+}
+
+export function parseSlotRangesArguments(
+  parser: CommandParser,
+  ranges: SlotRange | Array<SlotRange>
+) {
+  if (Array.isArray(ranges)) {
+    for (const range of ranges) {
+      parseSlotRangeArguments(parser, range);
     }
-
-    return args;
+  } else {
+    parseSlotRangeArguments(parser, ranges);
+  }
 }
 
-export enum GeoReplyWith {
-    DISTANCE = 'WITHDIST',
-    HASH = 'WITHHASH',
-    COORDINATES = 'WITHCOORD'
+export type RawRangeReply = [
+  start: number,
+  end: number
+];
+
+export interface RangeReply {
+  start: number;
+  end: number;
 }
 
-export interface GeoReplyWithMember {
-    member: string;
-    distance?: number;
-    hash?: string;
-    coordinates?: {
-        longitude: string;
-        latitude: string;
-    };
+export function transformRangeReply([start, end]: RawRangeReply): RangeReply {
+  return {
+    start,
+    end
+  };
 }
 
-export function transformGeoMembersWithReply(reply: Array<Array<any>>, replyWith: Array<GeoReplyWith>): Array<GeoReplyWithMember> {
-    const replyWithSet = new Set(replyWith);
+export type ZKeyAndWeight = {
+  key: RedisArgument;
+  weight: number;
+};
 
-    let index = 0;
-    const distanceIndex = replyWithSet.has(GeoReplyWith.DISTANCE) && ++index,
-        hashIndex = replyWithSet.has(GeoReplyWith.HASH) && ++index,
-        coordinatesIndex = replyWithSet.has(GeoReplyWith.COORDINATES) && ++index;
+export type ZVariadicKeys<T> = T | [T, ...Array<T>];
 
-    return reply.map(member => {
-        const transformedMember: GeoReplyWithMember = {
-            member: member[0]
-        };
+export type ZKeys = ZVariadicKeys<RedisArgument> | ZVariadicKeys<ZKeyAndWeight>;
 
-        if (distanceIndex) {
-            transformedMember.distance = member[distanceIndex];
-        }
+export function parseZKeysArguments(
+  parser: CommandParser,
+  keys: ZKeys
+) {
+  if (Array.isArray(keys)) {
+    parser.push(keys.length.toString());
 
-        if (hashIndex) {
-            transformedMember.hash = member[hashIndex];
+    if (keys.length) {
+      if (isPlainKeys(keys)) {
+        parser.pushKeys(keys);
+      } else {
+        for (let i = 0; i < keys.length; i++) {
+          parser.pushKey(keys[i].key)
         }
-
-        if (coordinatesIndex) {
-            const [longitude, latitude] = member[coordinatesIndex];
-            transformedMember.coordinates = {
-                longitude,
-                latitude
-            };
+        parser.push('WEIGHTS');
+        for (let i = 0; i < keys.length; i++) {
+          parser.push(transformDoubleArgument(keys[i].weight));
         }
+      }
+    }
+  } else {
+    parser.push('1');
 
-        return transformedMember;
-    });
-}
-
-export function transformEXAT(EXAT: number | Date): string {
-    return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString();
-}
-
-export function transformPXAT(PXAT: number | Date): string {
-    return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString();
+    if (isPlainKey(keys)) {
+      parser.pushKey(keys);
+    } else {
+      parser.pushKey(keys.key);
+      parser.push('WEIGHTS', transformDoubleArgument(keys.weight));
+    }
+  }
 }
 
-export interface EvalOptions {
-    keys?: Array<string>;
-    arguments?: Array<string>;
+function isPlainKey(key: RedisArgument | ZKeyAndWeight): key is RedisArgument {
+  return typeof key === 'string' || key instanceof Buffer;
 }
 
-export function evalFirstKeyIndex(options?: EvalOptions): string | undefined {
-    return options?.keys?.[0];
+function isPlainKeys(keys: Array<RedisArgument> | Array<ZKeyAndWeight>): keys is Array<RedisArgument> {
+  return isPlainKey(keys[0]);
 }
 
-export function pushEvalArguments(args: Array<string>, options?: EvalOptions): Array<string> {
-    if (options?.keys) {
-        args.push(
-            options.keys.length.toString(),
-            ...options.keys
-        );
-    } else {
-        args.push('0');
-    }
+export type Tail<T extends unknown[]> = T extends [infer Head, ...infer Tail] ? Tail : never;
 
-    if (options?.arguments) {
-        args.push(...options.arguments);
-    }
+/**
+ * @deprecated
+ */
+export function parseArgs(command: Command, ...args: Array<any>): CommandArguments {
+  const parser = new BasicCommandParser();
+  command.parseCommand!(parser, ...args);
 
-    return args;
+  const redisArgs: CommandArguments = parser.redisArgs;
+  if (parser.preserve) {
+    redisArgs.preserve = parser.preserve;
+  }
+  return redisArgs;
 }
 
-export function pushVerdictArguments(args: RedisCommandArguments, value: RedisCommandArgument | Array<RedisCommandArgument>): RedisCommandArguments  {
-    if (Array.isArray(value)) {
-        // https://github.com/redis/node-redis/pull/2160
-        args = args.concat(value);
-    } else {
-        args.push(value);
-    }
-
-    return args;
-}
+export type StreamMessageRawReply = TuplesReply<[
+  id: BlobStringReply,
+  message: ArrayReply<BlobStringReply>
+]>;
 
-export function pushVerdictNumberArguments(
-    args: RedisCommandArguments,
-    value: number | Array<number>
-): RedisCommandArguments  {
-    if (Array.isArray(value)) {
-        for (const item of value) {
-            args.push(item.toString());
-        }
-    } else {
-        args.push(value.toString());
-    }
+export type StreamMessageReply = {
+  id: BlobStringReply,
+  message: MapReply<BlobStringReply | string, BlobStringReply>,
+};
 
-    return args;
+export function transformStreamMessageReply(typeMapping: TypeMapping | undefined, reply: StreamMessageRawReply): StreamMessageReply {
+  const [ id, message ] = reply as unknown as UnwrapReply<typeof reply>;
+  return {
+    id: id,
+    message: transformTuplesReply(message, undefined, typeMapping)
+  };
 }
 
-export function pushVerdictArgument(
-    args: RedisCommandArguments,
-    value: RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    if (Array.isArray(value)) {
-        args.push(value.length.toString(), ...value);
-    } else {
-        args.push('1', value);
-    }
-
-    return args;
+export function transformStreamMessageNullReply(typeMapping: TypeMapping | undefined, reply: StreamMessageRawReply | NullReply) {
+  return isNullReply(reply) ? reply : transformStreamMessageReply(typeMapping, reply);
 }
 
-export function pushOptionalVerdictArgument(
-    args: RedisCommandArguments,
-    name: RedisCommandArgument,
-    value: undefined | RedisCommandArgument | Array<RedisCommandArgument>
-): RedisCommandArguments {
-    if (value === undefined) return args;
-
-    args.push(name);
-
-    return pushVerdictArgument(args, value);
-}
+export type StreamMessagesReply = Array<StreamMessageReply>;
 
-export enum CommandFlags {
-    WRITE = 'write', // command may result in modifications
-    READONLY = 'readonly', // command will never modify keys
-    DENYOOM = 'denyoom', // reject command if currently out of memory
-    ADMIN = 'admin', // server admin command
-    PUBSUB = 'pubsub', // pubsub-related command
-    NOSCRIPT = 'noscript', // deny this command from scripts
-    RANDOM = 'random', // command has random results, dangerous for scripts
-    SORT_FOR_SCRIPT = 'sort_for_script', // if called from script, sort output
-    LOADING = 'loading', // allow command while database is loading
-    STALE = 'stale', // allow command while replica has stale data
-    SKIP_MONITOR = 'skip_monitor', // do not show this command in MONITOR
-    ASKING = 'asking', // cluster related - accept even if importing
-    FAST = 'fast', // command operates in constant or log(N) time. Used for latency monitoring.
-    MOVABLEKEYS = 'movablekeys' // keys have no pre-determined position. You must discover keys yourself.
-}
+export type StreamsMessagesReply = Array<{
+  name: BlobStringReply | string;
+  messages: StreamMessagesReply;
+}> | null;
 
-export enum CommandCategories {
-    KEYSPACE = '@keyspace',
-    READ = '@read',
-    WRITE = '@write',
-    SET = '@set',
-    SORTEDSET = '@sortedset',
-    LIST = '@list',
-    HASH = '@hash',
-    STRING = '@string',
-    BITMAP = '@bitmap',
-    HYPERLOGLOG = '@hyperloglog',
-    GEO = '@geo',
-    STREAM = '@stream',
-    PUBSUB = '@pubsub',
-    ADMIN = '@admin',
-    FAST = '@fast',
-    SLOW = '@slow',
-    BLOCKING = '@blocking',
-    DANGEROUS = '@dangerous',
-    CONNECTION = '@connection',
-    TRANSACTION = '@transaction',
-    SCRIPTING = '@scripting'
-}
+export function transformStreamMessagesReply(
+  r: ArrayReply<StreamMessageRawReply>,
+  typeMapping?: TypeMapping
+): StreamMessagesReply {
+  const reply = r as unknown as UnwrapReply<typeof r>;
+
+  return reply.map(transformStreamMessageReply.bind(undefined, typeMapping));
+}
+
+type StreamMessagesRawReply = TuplesReply<[name: BlobStringReply, ArrayReply<StreamMessageRawReply>]>;
+type StreamsMessagesRawReply2 = ArrayReply<StreamMessagesRawReply>;
+
+export function transformStreamsMessagesReplyResp2(
+  reply: UnwrapReply<StreamsMessagesRawReply2 | NullReply>,
+  preserve?: any,
+  typeMapping?: TypeMapping
+): StreamsMessagesReply | NullReply { 
+  // FUTURE: resposne type if resp3 was working, reverting to old v4 for now
+  //: MapReply<BlobStringReply | string, StreamMessagesReply> | NullReply {
+  if (reply === null) return null as unknown as NullReply;
+
+  switch (typeMapping? typeMapping[RESP_TYPES.MAP] : undefined) {
+/* FUTURE: a response type for when resp3 is working properly
+    case Map: {
+      const ret = new Map<string, StreamMessagesReply>();
+
+      for (let i=0; i < reply.length; i++) {
+        const stream = reply[i] as unknown as UnwrapReply<StreamMessagesRawReply>;
+    
+        const name = stream[0];
+        const rawMessages = stream[1];
+    
+        ret.set(name.toString(), transformStreamMessagesReply(rawMessages, typeMapping));
+      }
+    
+      return ret as unknown as MapReply<string, StreamMessagesReply>;
+    }
+    case Array: {
+      const ret: Array<BlobStringReply | StreamMessagesReply> = [];
+
+      for (let i=0; i < reply.length; i++) {
+        const stream = reply[i] as unknown as UnwrapReply<StreamMessagesRawReply>;
+    
+        const name = stream[0];
+        const rawMessages = stream[1];
+    
+        ret.push(name); 
+        ret.push(transformStreamMessagesReply(rawMessages, typeMapping));
+      }
+
+      return ret as unknown as MapReply<string, StreamMessagesReply>;
+    }
+    default: {
+      const ret: Record<string, StreamMessagesReply> = Object.create(null);
+
+      for (let i=0; i < reply.length; i++) {
+        const stream = reply[i] as unknown as UnwrapReply<StreamMessagesRawReply>;
+    
+        const name = stream[0] as unknown as UnwrapReply<BlobStringReply>;
+        const rawMessages = stream[1];
+    
+        ret[name.toString()] = transformStreamMessagesReply(rawMessages);
+      }
+    
+      return ret as unknown as MapReply<string, StreamMessagesReply>;
+    }
+*/
+    // V4 compatible response type
+    default: {
+      const ret: StreamsMessagesReply = [];
 
-export type CommandRawReply = [
-    name: string,
-    arity: number,
-    flags: Array<CommandFlags>,
-    firstKeyIndex: number,
-    lastKeyIndex: number,
-    step: number,
-    categories: Array<CommandCategories>
-];
+      for (let i=0; i < reply.length; i++) {
+        const stream = reply[i] as unknown as UnwrapReply<StreamMessagesRawReply>;
 
-export type CommandReply = {
-    name: string,
-    arity: number,
-    flags: Set<CommandFlags>,
-    firstKeyIndex: number,
-    lastKeyIndex: number,
-    step: number,
-    categories: Set<CommandCategories>
-};
+        ret.push({
+          name: stream[0],
+          messages: transformStreamMessagesReply(stream[1])
+        });
+      }
 
-export function transformCommandReply(
-    this: void,
-    [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply
-): CommandReply {
-    return {
-        name,
-        arity,
-        flags: new Set(flags),
-        firstKeyIndex,
-        lastKeyIndex,
-        step,
-        categories: new Set(categories)
-    };
+      return ret;
+    }
+  }
 }
 
-export enum RedisFunctionFlags {
-    NO_WRITES = 'no-writes',
-    ALLOW_OOM = 'allow-oom',
-    ALLOW_STALE = 'allow-stale',
-    NO_CLUSTER = 'no-cluster'
-}
+type StreamsMessagesRawReply3 = MapReply<BlobStringReply, ArrayReply<StreamMessageRawReply>>;
 
-export type FunctionListRawItemReply = [
-    'library_name',
-    string,
-    'engine',
-    string,
-    'functions',
-    Array<[
-        'name',
-        string,
-        'description',
-        string | null,
-        'flags',
-        Array<RedisFunctionFlags>
-    ]>
-];
+export function transformStreamsMessagesReplyResp3(reply: UnwrapReply<StreamsMessagesRawReply3 | NullReply>): MapReply<BlobStringReply, StreamMessagesReply> | NullReply {
+  if (reply === null) return null as unknown as NullReply;
+  
+  if (reply instanceof Map) {
+    const ret = new Map<string, StreamMessagesReply>();
 
-export interface FunctionListItemReply {
-    libraryName: string;
-    engine: string;
-    functions: Array<{
-        name: string;
-        description: string | null;
-        flags: Array<RedisFunctionFlags>;
-    }>;
-}
+    for (const [n, rawMessages] of reply) {
+      const name = n as unknown as UnwrapReply<BlobStringReply>;
 
-export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply {
-    return {
-        libraryName: reply[1],
-        engine: reply[3],
-        functions: reply[5].map(fn => ({
-            name: fn[1],
-            description: fn[3],
-            flags: fn[5]
-        }))
-    };
-}
-
-export interface SortOptions {
-    BY?: string;
-    LIMIT?: {
-        offset: number;
-        count: number;
-    },
-    GET?: string | Array<string>;
-    DIRECTION?: 'ASC' | 'DESC';
-    ALPHA?: true;
-}
-
-export function pushSortArguments(
-    args: RedisCommandArguments,
-    options?: SortOptions
-): RedisCommandArguments {
-    if (options?.BY) {
-        args.push('BY', options.BY);
+      ret.set(name.toString(), transformStreamMessagesReply(rawMessages));
     }
 
-    if (options?.LIMIT) {
-        args.push(
-            'LIMIT',
-            options.LIMIT.offset.toString(),
-            options.LIMIT.count.toString()
-        );
-    }
+    return ret as unknown as MapReply<BlobStringReply, StreamMessagesReply>
+  } else if (reply instanceof Array) {
+    const ret = [];
 
-    if (options?.GET) {
-        for (const pattern of (typeof options.GET === 'string' ? [options.GET] : options.GET)) {
-            args.push('GET', pattern);
-        }
-    }
+    for (let i=0; i < reply.length; i += 2) {
+      const name = reply[i] as BlobStringReply;
+      const rawMessages = reply[i+1] as ArrayReply<StreamMessageRawReply>;
 
-    if (options?.DIRECTION) {
-        args.push(options.DIRECTION);
+      ret.push(name);
+      ret.push(transformStreamMessagesReply(rawMessages));
     }
 
-    if (options?.ALPHA) {
-        args.push('ALPHA');
+    return ret as unknown as MapReply<BlobStringReply, StreamMessagesReply>
+  } else {
+    const ret = Object.create(null);
+    for (const [name, rawMessages] of Object.entries(reply)) {
+      ret[name] = transformStreamMessagesReply(rawMessages);
     }
 
-    return args;
+    return ret as unknown as MapReply<BlobStringReply, StreamMessagesReply>
+  }
 }
 
-export interface SlotRange {
-    start: number;
-    end: number;
-}
-
-function pushSlotRangeArguments(
-    args: RedisCommandArguments,
-    range: SlotRange
-): void {
-    args.push(
-        range.start.toString(),
-        range.end.toString()
-    );
-}
-
-export function pushSlotRangesArguments(
-    args: RedisCommandArguments,
-    ranges: SlotRange | Array<SlotRange>
-): RedisCommandArguments {
-    if (Array.isArray(ranges)) {
-        for (const range of ranges) {
-            pushSlotRangeArguments(args, range);
-        }
-    } else {
-        pushSlotRangeArguments(args, ranges);
-    }
+export type RedisJSON = null | boolean | number | string | Date | Array<RedisJSON> | {
+  [key: string]: RedisJSON;
+  [key: number]: RedisJSON;
+};
 
-    return args;
+export function transformRedisJsonArgument(json: RedisJSON): string {
+  return JSON.stringify(json);
 }
 
-export type RawRangeReply = [
-    start: number,
-    end: number
-];
-
-export interface RangeReply {
-    start: number;
-    end: number;
+export function transformRedisJsonReply(json: BlobStringReply): RedisJSON {
+  const res = JSON.parse((json as unknown as UnwrapReply<typeof json>).toString());
+  return res;
 }
 
-export function transformRangeReply([start, end]: RawRangeReply): RangeReply {
-    return {
-        start,
-        end
-    };
+export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON {
+  return isNullReply(json) ? json : transformRedisJsonReply(json);
 }
diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts
index 60f9720c8d1..4614c8b282b 100644
--- a/packages/client/lib/commands/index.ts
+++ b/packages/client/lib/commands/index.ts
@@ -1,91 +1,1089 @@
-import { ClientCommandOptions } from '../client';
-import { CommandOptions } from '../command-options';
-import { RedisScriptConfig, SHA1 } from '../lua-script';
+import type { RedisCommands } from '../RESP/types';
+import ACL_CAT from './ACL_CAT';
+import ACL_DELUSER from './ACL_DELUSER';
+import ACL_DRYRUN from './ACL_DRYRUN';
+import ACL_GENPASS from './ACL_GENPASS';
+import ACL_GETUSER from './ACL_GETUSER';
+import ACL_LIST from './ACL_LIST';
+import ACL_LOAD from './ACL_LOAD';
+import ACL_LOG_RESET from './ACL_LOG_RESET';
+import ACL_LOG from './ACL_LOG';
+import ACL_SAVE from './ACL_SAVE';
+import ACL_SETUSER from './ACL_SETUSER';
+import ACL_USERS from './ACL_USERS';
+import ACL_WHOAMI from './ACL_WHOAMI';
+import APPEND from './APPEND';
+import ASKING from './ASKING';
+import AUTH from './AUTH';
+import BGREWRITEAOF from './BGREWRITEAOF';
+import BGSAVE from './BGSAVE';
+import BITCOUNT from './BITCOUNT';
+import BITFIELD_RO from './BITFIELD_RO';
+import BITFIELD from './BITFIELD';
+import BITOP from './BITOP';
+import BITPOS from './BITPOS';
+import BLMOVE from './BLMOVE';
+import BLMPOP from './BLMPOP';
+import BLPOP from './BLPOP';
+import BRPOP from './BRPOP';
+import BRPOPLPUSH from './BRPOPLPUSH';
+import BZMPOP from './BZMPOP';
+import BZPOPMAX from './BZPOPMAX';
+import BZPOPMIN from './BZPOPMIN';
+import CLIENT_CACHING from './CLIENT_CACHING';
+import CLIENT_GETNAME from './CLIENT_GETNAME';
+import CLIENT_GETREDIR from './CLIENT_GETREDIR';
+import CLIENT_ID from './CLIENT_ID';
+import CLIENT_INFO from './CLIENT_INFO';
+import CLIENT_KILL from './CLIENT_KILL';
+import CLIENT_LIST from './CLIENT_LIST';
+import CLIENT_NO_EVICT from './CLIENT_NO-EVICT';
+import CLIENT_NO_TOUCH from './CLIENT_NO-TOUCH';
+import CLIENT_PAUSE from './CLIENT_PAUSE';
+import CLIENT_SETNAME from './CLIENT_SETNAME';
+import CLIENT_TRACKING from './CLIENT_TRACKING';
+import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO';
+import CLIENT_UNPAUSE from './CLIENT_UNPAUSE';
+import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS';
+import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE';
+import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH';
+import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS';
+import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT';
+import CLUSTER_DELSLOTS from './CLUSTER_DELSLOTS';
+import CLUSTER_DELSLOTSRANGE from './CLUSTER_DELSLOTSRANGE';
+import CLUSTER_FAILOVER from './CLUSTER_FAILOVER';
+import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS';
+import CLUSTER_FORGET from './CLUSTER_FORGET';
+import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT';
+import CLUSTER_INFO from './CLUSTER_INFO';
+import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT';
+import CLUSTER_LINKS from './CLUSTER_LINKS';
+import CLUSTER_MEET from './CLUSTER_MEET';
+import CLUSTER_MYID from './CLUSTER_MYID';
+import CLUSTER_MYSHARDID from './CLUSTER_MYSHARDID';
+import CLUSTER_NODES from './CLUSTER_NODES';
+import CLUSTER_REPLICAS from './CLUSTER_REPLICAS';
+import CLUSTER_REPLICATE from './CLUSTER_REPLICATE';
+import CLUSTER_RESET from './CLUSTER_RESET';
+import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG';
+import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH';
+import CLUSTER_SETSLOT from './CLUSTER_SETSLOT';
+import CLUSTER_SLOTS from './CLUSTER_SLOTS';
+import COMMAND_COUNT from './COMMAND_COUNT';
+import COMMAND_GETKEYS from './COMMAND_GETKEYS';
+import COMMAND_GETKEYSANDFLAGS from './COMMAND_GETKEYSANDFLAGS';
+import COMMAND_INFO from './COMMAND_INFO';
+import COMMAND_LIST from './COMMAND_LIST';
+import COMMAND from './COMMAND';
+import CONFIG_GET from './CONFIG_GET';
+import CONFIG_RESETASTAT from './CONFIG_RESETSTAT';
+import CONFIG_REWRITE from './CONFIG_REWRITE';
+import CONFIG_SET from './CONFIG_SET';
+import COPY from './COPY';
+import DBSIZE from './DBSIZE';
+import DECR from './DECR';
+import DECRBY from './DECRBY';
+import DEL from './DEL';
+import DUMP from './DUMP';
+import ECHO from './ECHO';
+import EVAL_RO from './EVAL_RO';
+import EVAL from './EVAL';
+import EVALSHA_RO from './EVALSHA_RO';
+import EVALSHA from './EVALSHA';
+import GEOADD from './GEOADD';
+import GEODIST from './GEODIST';
+import GEOHASH from './GEOHASH';
+import GEOPOS from './GEOPOS';
+import GEORADIUS_RO_WITH from './GEORADIUS_RO_WITH';
+import GEORADIUS_RO from './GEORADIUS_RO';
+import GEORADIUS_STORE from './GEORADIUS_STORE';
+import GEORADIUS_WITH from './GEORADIUS_WITH';
+import GEORADIUS from './GEORADIUS';
+import GEORADIUSBYMEMBER_RO_WITH from './GEORADIUSBYMEMBER_RO_WITH';
+import GEORADIUSBYMEMBER_RO from './GEORADIUSBYMEMBER_RO';
+import GEORADIUSBYMEMBER_STORE from './GEORADIUSBYMEMBER_STORE';
+import GEORADIUSBYMEMBER_WITH from './GEORADIUSBYMEMBER_WITH';
+import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER';
+import GEOSEARCH_WITH from './GEOSEARCH_WITH';
+import GEOSEARCH from './GEOSEARCH';
+import GEOSEARCHSTORE from './GEOSEARCHSTORE';
+import GET from './GET';
+import GETBIT from './GETBIT';
+import GETDEL from './GETDEL';
+import GETEX from './GETEX';
+import GETRANGE from './GETRANGE';
+import GETSET from './GETSET';
+import EXISTS from './EXISTS';
+import EXPIRE from './EXPIRE';
+import EXPIREAT from './EXPIREAT';
+import EXPIRETIME from './EXPIRETIME';
+import FLUSHALL from './FLUSHALL';
+import FLUSHDB from './FLUSHDB';
+import FCALL from './FCALL';
+import FCALL_RO from './FCALL_RO';
+import FUNCTION_DELETE from './FUNCTION_DELETE';
+import FUNCTION_DUMP from './FUNCTION_DUMP';
+import FUNCTION_FLUSH from './FUNCTION_FLUSH';
+import FUNCTION_KILL from './FUNCTION_KILL';
+import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE';
+import FUNCTION_LIST from './FUNCTION_LIST';
+import FUNCTION_LOAD from './FUNCTION_LOAD';
+import FUNCTION_RESTORE from './FUNCTION_RESTORE';
+import FUNCTION_STATS from './FUNCTION_STATS';
+import HDEL from './HDEL';
+import HELLO from './HELLO';
+import HEXISTS from './HEXISTS';
+import HEXPIRE from './HEXPIRE';
+import HEXPIREAT from './HEXPIREAT';
+import HEXPIRETIME from './HEXPIRETIME';
+import HGET from './HGET';
+import HGETALL from './HGETALL';
+import HGETDEL from './HGETDEL';
+import HGETEX from './HGETEX';
+import HINCRBY from './HINCRBY';
+import HINCRBYFLOAT from './HINCRBYFLOAT';
+import HKEYS from './HKEYS';
+import HLEN from './HLEN';
+import HMGET from './HMGET';
+import HPERSIST from './HPERSIST';
+import HPEXPIRE from './HPEXPIRE';
+import HPEXPIREAT from './HPEXPIREAT';
+import HPEXPIRETIME from './HPEXPIRETIME';
+import HPTTL from './HPTTL';
+import HRANDFIELD_COUNT_WITHVALUES from './HRANDFIELD_COUNT_WITHVALUES';
+import HRANDFIELD_COUNT from './HRANDFIELD_COUNT';
+import HRANDFIELD from './HRANDFIELD';
+import HSCAN from './HSCAN';
+import HSCAN_NOVALUES from './HSCAN_NOVALUES';
+import HSET from './HSET';
+import HSETEX from './HSETEX';
+import HSETNX from './HSETNX';
+import HSTRLEN from './HSTRLEN';
+import HTTL from './HTTL';
+import HVALS from './HVALS';
+import INCR from './INCR';
+import INCRBY from './INCRBY';
+import INCRBYFLOAT from './INCRBYFLOAT';
+import INFO from './INFO';
+import KEYS from './KEYS';
+import LASTSAVE from './LASTSAVE';
+import LATENCY_DOCTOR from './LATENCY_DOCTOR';
+import LATENCY_GRAPH from './LATENCY_GRAPH';
+import LATENCY_HISTORY from './LATENCY_HISTORY';
+import LATENCY_LATEST from './LATENCY_LATEST';
+import LCS_IDX_WITHMATCHLEN from './LCS_IDX_WITHMATCHLEN';
+import LCS_IDX from './LCS_IDX';
+import LCS_LEN from './LCS_LEN';
+import LCS from './LCS';
+import LINDEX from './LINDEX';
+import LINSERT from './LINSERT';
+import LLEN from './LLEN';
+import LMOVE from './LMOVE';
+import LMPOP from './LMPOP';
+import LOLWUT from './LOLWUT';
+import LPOP_COUNT from './LPOP_COUNT';
+import LPOP from './LPOP';
+import LPOS_COUNT from './LPOS_COUNT';
+import LPOS from './LPOS';
+import LPUSH from './LPUSH';
+import LPUSHX from './LPUSHX';
+import LRANGE from './LRANGE';
+import LREM from './LREM';
+import LSET from './LSET';
+import LTRIM from './LTRIM';
+import MEMORY_DOCTOR from './MEMORY_DOCTOR';
+import MEMORY_MALLOC_STATS from './MEMORY_MALLOC-STATS';
+import MEMORY_PURGE from './MEMORY_PURGE';
+import MEMORY_STATS from './MEMORY_STATS';
+import MEMORY_USAGE from './MEMORY_USAGE';
+import MGET from './MGET';
+import MIGRATE from './MIGRATE';
+import MODULE_LIST from './MODULE_LIST';
+import MODULE_LOAD from './MODULE_LOAD';
+import MODULE_UNLOAD from './MODULE_UNLOAD';
+import MOVE from './MOVE';
+import MSET from './MSET';
+import MSETNX from './MSETNX';
+import OBJECT_ENCODING from './OBJECT_ENCODING';
+import OBJECT_FREQ from './OBJECT_FREQ';
+import OBJECT_IDLETIME from './OBJECT_IDLETIME';
+import OBJECT_REFCOUNT from './OBJECT_REFCOUNT';
+import PERSIST from './PERSIST';
+import PEXPIRE from './PEXPIRE';
+import PEXPIREAT from './PEXPIREAT';
+import PEXPIRETIME from './PEXPIRETIME';
+import PFADD from './PFADD';
+import PFCOUNT from './PFCOUNT';
+import PFMERGE from './PFMERGE';
+import PING from './PING';
+import PSETEX from './PSETEX';
+import PTTL from './PTTL';
+import PUBLISH from './PUBLISH';
+import PUBSUB_CHANNELS from './PUBSUB_CHANNELS';
+import PUBSUB_NUMPAT from './PUBSUB_NUMPAT';
+import PUBSUB_NUMSUB from './PUBSUB_NUMSUB';
+import PUBSUB_SHARDNUMSUB from './PUBSUB_SHARDNUMSUB';
+import PUBSUB_SHARDCHANNELS from './PUBSUB_SHARDCHANNELS';
+import RANDOMKEY from './RANDOMKEY';
+import READONLY from './READONLY';
+import RENAME from './RENAME';
+import RENAMENX from './RENAMENX';
+import REPLICAOF from './REPLICAOF';
+import RESTORE_ASKING from './RESTORE-ASKING';
+import RESTORE from './RESTORE';
+import ROLE from './ROLE';
+import RPOP_COUNT from './RPOP_COUNT';
+import RPOP from './RPOP';
+import RPOPLPUSH from './RPOPLPUSH';
+import RPUSH from './RPUSH';
+import RPUSHX from './RPUSHX';
+import SADD from './SADD';
+import SCAN from './SCAN';
+import SCARD from './SCARD';
+import SCRIPT_DEBUG from './SCRIPT_DEBUG';
+import SCRIPT_EXISTS from './SCRIPT_EXISTS';
+import SCRIPT_FLUSH from './SCRIPT_FLUSH';
+import SCRIPT_KILL from './SCRIPT_KILL';
+import SCRIPT_LOAD from './SCRIPT_LOAD';
+import SDIFF from './SDIFF';
+import SDIFFSTORE from './SDIFFSTORE';
+import SET from './SET';
+import SETBIT from './SETBIT';
+import SETEX from './SETEX';
+import SETNX from './SETNX';
+import SETRANGE from './SETRANGE';
+import SINTER from './SINTER';
+import SINTERCARD from './SINTERCARD';
+import SINTERSTORE from './SINTERSTORE';
+import SISMEMBER from './SISMEMBER';
+import SMEMBERS from './SMEMBERS';
+import SMISMEMBER from './SMISMEMBER';
+import SMOVE from './SMOVE';
+import SORT_RO from './SORT_RO';
+import SORT_STORE from './SORT_STORE';
+import SORT from './SORT';
+import SPOP_COUNT from './SPOP_COUNT';
+import SPOP from './SPOP';
+import SPUBLISH from './SPUBLISH';
+import SRANDMEMBER_COUNT from './SRANDMEMBER_COUNT';
+import SRANDMEMBER from './SRANDMEMBER';
+import SREM from './SREM';
+import SSCAN from './SSCAN';
+import STRLEN from './STRLEN';
+import SUNION from './SUNION';
+import SUNIONSTORE from './SUNIONSTORE';
+import SWAPDB from './SWAPDB';
+import TIME from './TIME';
+import TOUCH from './TOUCH';
+import TTL from './TTL';
+import TYPE from './TYPE';
+import UNLINK from './UNLINK';
+import WAIT from './WAIT';
+import XACK from './XACK';
+import XACKDEL from './XACKDEL';
+import XADD_NOMKSTREAM from './XADD_NOMKSTREAM';
+import XADD from './XADD';
+import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID';
+import XAUTOCLAIM from './XAUTOCLAIM';
+import XCLAIM_JUSTID from './XCLAIM_JUSTID';
+import XCLAIM from './XCLAIM';
+import XDEL from './XDEL';
+import XDELEX from './XDELEX';
+import XGROUP_CREATE from './XGROUP_CREATE';
+import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER';
+import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER';
+import XGROUP_DESTROY from './XGROUP_DESTROY';
+import XGROUP_SETID from './XGROUP_SETID';
+import XINFO_CONSUMERS from './XINFO_CONSUMERS';
+import XINFO_GROUPS from './XINFO_GROUPS';
+import XINFO_STREAM from './XINFO_STREAM';
+import XLEN from './XLEN';
+import XPENDING_RANGE from './XPENDING_RANGE';
+import XPENDING from './XPENDING';
+import XRANGE from './XRANGE';
+import XREAD from './XREAD';
+import XREADGROUP from './XREADGROUP';
+import XREVRANGE from './XREVRANGE';
+import XSETID from './XSETID';
+import XTRIM from './XTRIM';
+import ZADD_INCR from './ZADD_INCR';
+import ZADD from './ZADD';
+import ZCARD from './ZCARD';
+import ZCOUNT from './ZCOUNT';
+import ZDIFF_WITHSCORES from './ZDIFF_WITHSCORES';
+import ZDIFF from './ZDIFF';
+import ZDIFFSTORE from './ZDIFFSTORE';
+import ZINCRBY from './ZINCRBY';
+import ZINTER_WITHSCORES from './ZINTER_WITHSCORES';
+import ZINTER from './ZINTER';
+import ZINTERCARD from './ZINTERCARD';
+import ZINTERSTORE from './ZINTERSTORE';
+import ZLEXCOUNT from './ZLEXCOUNT';
+import ZMPOP from './ZMPOP';
+import ZMSCORE from './ZMSCORE';
+import ZPOPMAX_COUNT from './ZPOPMAX_COUNT';
+import ZPOPMAX from './ZPOPMAX';
+import ZPOPMIN_COUNT from './ZPOPMIN_COUNT';
+import ZPOPMIN from './ZPOPMIN';
+import ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES';
+import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT';
+import ZRANDMEMBER from './ZRANDMEMBER';
+import ZRANGE_WITHSCORES from './ZRANGE_WITHSCORES';
+import ZRANGE from './ZRANGE';
+import ZRANGEBYLEX from './ZRANGEBYLEX';
+import ZRANGEBYSCORE_WITHSCORES from './ZRANGEBYSCORE_WITHSCORES';
+import ZRANGEBYSCORE from './ZRANGEBYSCORE';
+import ZRANGESTORE from './ZRANGESTORE';
+import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE';
+import ZRANK_WITHSCORE from './ZRANK_WITHSCORE';
+import ZRANK from './ZRANK';
+import ZREM from './ZREM';
+import ZREMRANGEBYLEX from './ZREMRANGEBYLEX';
+import ZREMRANGEBYRANK from './ZREMRANGEBYRANK';
+import ZREVRANK from './ZREVRANK';
+import ZSCAN from './ZSCAN';
+import ZSCORE from './ZSCORE';
+import ZUNION_WITHSCORES from './ZUNION_WITHSCORES';
+import ZUNION from './ZUNION';
+import ZUNIONSTORE from './ZUNIONSTORE';
+import VADD from './VADD';
+import VCARD from './VCARD';
+import VDIM from './VDIM';
+import VEMB from './VEMB';
+import VEMB_RAW from './VEMB_RAW';
+import VGETATTR from './VGETATTR';
+import VINFO from './VINFO';
+import VLINKS from './VLINKS';
+import VLINKS_WITHSCORES from './VLINKS_WITHSCORES';
+import VRANDMEMBER from './VRANDMEMBER';
+import VREM from './VREM';
+import VSETATTR from './VSETATTR';
+import VSIM from './VSIM';
+import VSIM_WITHSCORES from './VSIM_WITHSCORES';
 
-export type RedisCommandRawReply = string | number | Buffer | null | undefined | Array<RedisCommandRawReply>;
-
-export type RedisCommandArgument = string | Buffer;
-
-export type RedisCommandArguments = Array<RedisCommandArgument> & { preserve?: unknown };
-
-export interface RedisCommand {
-    FIRST_KEY_INDEX?: number | ((...args: Array<any>) => RedisCommandArgument | undefined);
-    IS_READ_ONLY?: boolean;
-    TRANSFORM_LEGACY_REPLY?: boolean;
-    transformArguments(this: void, ...args: Array<any>): RedisCommandArguments;
-    transformReply?(this: void, reply: any, preserved?: any): any;
-}
-
-export type RedisCommandReply<C extends RedisCommand> =
-    C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply;
-
-export type ConvertArgumentType<Type, ToType> =
-    Type extends RedisCommandArgument ? (
-        Type extends (string & ToType) ? Type : ToType
-    ) : (
-        Type extends Set<infer Member> ? Set<ConvertArgumentType<Member, ToType>> : (
-            Type extends Map<infer Key, infer Value> ? Map<Key, ConvertArgumentType<Value, ToType>> : (
-                Type extends Array<infer Member> ? Array<ConvertArgumentType<Member, ToType>> : (
-                    Type extends Date ? Type : (
-                        Type extends Record<PropertyKey, any> ? {
-                            [Property in keyof Type]: ConvertArgumentType<Type[Property], ToType>
-                        } : Type
-                    )
-                )
-            )
-        )
-    );
-
-export type RedisCommandSignature<
-    Command extends RedisCommand,
-    Params extends Array<unknown> = Parameters<Command['transformArguments']>
-> = <Options extends CommandOptions<ClientCommandOptions>>(
-    ...args: Params | [options: Options, ...rest: Params]
-) => Promise<
-    ConvertArgumentType<
-        RedisCommandReply<Command>,
-        Options['returnBuffers'] extends true ? Buffer : string
-    >
->;
-
-export interface RedisCommands {
-    [command: string]: RedisCommand;
-}
-
-export interface RedisModule {
-    [command: string]: RedisCommand;
-}
-
-export interface RedisModules {
-    [module: string]: RedisModule;
-}
-
-export interface RedisFunction extends RedisCommand {
-    NUMBER_OF_KEYS?: number;
-}
-
-export interface RedisFunctionLibrary {
-    [fn: string]: RedisFunction;
-}
-
-export interface RedisFunctions {
-    [library: string]: RedisFunctionLibrary;
-}
-
-export type RedisScript = RedisScriptConfig & SHA1;
-
-export interface RedisScripts {
-    [script: string]: RedisScript;
-}
-
-export interface RedisExtensions<
-    M extends RedisModules = RedisModules,
-    F extends RedisFunctions = RedisFunctions,
-    S extends RedisScripts = RedisScripts
-> {
-    modules?: M;
-    functions?: F;
-    scripts?: S;
-}
-
-export type ExcludeMappedString<S> = string extends S ? never : S;
+export default {
+  ACL_CAT,
+  aclCat: ACL_CAT,
+  ACL_DELUSER,
+  aclDelUser: ACL_DELUSER,
+  ACL_DRYRUN,
+  aclDryRun: ACL_DRYRUN,
+  ACL_GENPASS,
+  aclGenPass: ACL_GENPASS,
+  ACL_GETUSER,
+  aclGetUser: ACL_GETUSER,
+  ACL_LIST,
+  aclList: ACL_LIST,
+  ACL_LOAD,
+  aclLoad: ACL_LOAD,
+  ACL_LOG_RESET,
+  aclLogReset: ACL_LOG_RESET,
+  ACL_LOG,
+  aclLog: ACL_LOG,
+  ACL_SAVE,
+  aclSave: ACL_SAVE,
+  ACL_SETUSER,
+  aclSetUser: ACL_SETUSER,
+  ACL_USERS,
+  aclUsers: ACL_USERS,
+  ACL_WHOAMI,
+  aclWhoAmI: ACL_WHOAMI,
+  APPEND,
+  append: APPEND,
+  ASKING,
+  asking: ASKING,
+  AUTH,
+  auth: AUTH,
+  BGREWRITEAOF,
+  bgRewriteAof: BGREWRITEAOF,
+  BGSAVE,
+  bgSave: BGSAVE,
+  BITCOUNT,
+  bitCount: BITCOUNT,
+  BITFIELD_RO,
+  bitFieldRo: BITFIELD_RO,
+  BITFIELD,
+  bitField: BITFIELD,
+  BITOP,
+  bitOp: BITOP,
+  BITPOS,
+  bitPos: BITPOS,
+  BLMOVE,
+  blMove: BLMOVE,
+  BLMPOP,
+  blmPop: BLMPOP,
+  BLPOP,
+  blPop: BLPOP,
+  BRPOP,
+  brPop: BRPOP,
+  BRPOPLPUSH,
+  brPopLPush: BRPOPLPUSH,
+  BZMPOP,
+  bzmPop: BZMPOP,
+  BZPOPMAX,
+  bzPopMax: BZPOPMAX,
+  BZPOPMIN,
+  bzPopMin: BZPOPMIN,
+  CLIENT_CACHING,
+  clientCaching: CLIENT_CACHING,
+  CLIENT_GETNAME,
+  clientGetName: CLIENT_GETNAME,
+  CLIENT_GETREDIR,
+  clientGetRedir: CLIENT_GETREDIR,
+  CLIENT_ID,
+  clientId: CLIENT_ID,
+  CLIENT_INFO,
+  clientInfo: CLIENT_INFO,
+  CLIENT_KILL,
+  clientKill: CLIENT_KILL,
+  CLIENT_LIST,
+  clientList: CLIENT_LIST,
+  'CLIENT_NO-EVICT': CLIENT_NO_EVICT,
+  clientNoEvict: CLIENT_NO_EVICT,
+  'CLIENT_NO-TOUCH': CLIENT_NO_TOUCH,
+  clientNoTouch: CLIENT_NO_TOUCH,
+  CLIENT_PAUSE,
+  clientPause: CLIENT_PAUSE,
+  CLIENT_SETNAME,
+  clientSetName: CLIENT_SETNAME,
+  CLIENT_TRACKING,
+  clientTracking: CLIENT_TRACKING,
+  CLIENT_TRACKINGINFO,
+  clientTrackingInfo: CLIENT_TRACKINGINFO,
+  CLIENT_UNPAUSE,
+  clientUnpause: CLIENT_UNPAUSE,
+  CLUSTER_ADDSLOTS,
+  clusterAddSlots: CLUSTER_ADDSLOTS,
+  CLUSTER_ADDSLOTSRANGE,
+  clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE,
+  CLUSTER_BUMPEPOCH,
+  clusterBumpEpoch: CLUSTER_BUMPEPOCH,
+  'CLUSTER_COUNT-FAILURE-REPORTS': CLUSTER_COUNT_FAILURE_REPORTS,
+  clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS,
+  CLUSTER_COUNTKEYSINSLOT,
+  clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT,
+  CLUSTER_DELSLOTS,
+  clusterDelSlots: CLUSTER_DELSLOTS,
+  CLUSTER_DELSLOTSRANGE,
+  clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE,
+  CLUSTER_FAILOVER,
+  clusterFailover: CLUSTER_FAILOVER,
+  CLUSTER_FLUSHSLOTS,
+  clusterFlushSlots: CLUSTER_FLUSHSLOTS,
+  CLUSTER_FORGET,
+  clusterForget: CLUSTER_FORGET,
+  CLUSTER_GETKEYSINSLOT,
+  clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT,
+  CLUSTER_INFO,
+  clusterInfo: CLUSTER_INFO,
+  CLUSTER_KEYSLOT,
+  clusterKeySlot: CLUSTER_KEYSLOT,
+  CLUSTER_LINKS,
+  clusterLinks: CLUSTER_LINKS,
+  CLUSTER_MEET,
+  clusterMeet: CLUSTER_MEET,
+  CLUSTER_MYID,
+  clusterMyId: CLUSTER_MYID,
+  CLUSTER_MYSHARDID,
+  clusterMyShardId: CLUSTER_MYSHARDID,
+  CLUSTER_NODES,
+  clusterNodes: CLUSTER_NODES,
+  CLUSTER_REPLICAS,
+  clusterReplicas: CLUSTER_REPLICAS,
+  CLUSTER_REPLICATE,
+  clusterReplicate: CLUSTER_REPLICATE,
+  CLUSTER_RESET,
+  clusterReset: CLUSTER_RESET,
+  CLUSTER_SAVECONFIG,
+  clusterSaveConfig: CLUSTER_SAVECONFIG,
+  'CLUSTER_SET-CONFIG-EPOCH': CLUSTER_SET_CONFIG_EPOCH,
+  clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH,
+  CLUSTER_SETSLOT,
+  clusterSetSlot: CLUSTER_SETSLOT,
+  CLUSTER_SLOTS,
+  clusterSlots: CLUSTER_SLOTS,
+  COMMAND_COUNT,
+  commandCount: COMMAND_COUNT,
+  COMMAND_GETKEYS,
+  commandGetKeys: COMMAND_GETKEYS,
+  COMMAND_GETKEYSANDFLAGS,
+  commandGetKeysAndFlags: COMMAND_GETKEYSANDFLAGS,
+  COMMAND_INFO,
+  commandInfo: COMMAND_INFO,
+  COMMAND_LIST,
+  commandList: COMMAND_LIST,
+  COMMAND,
+  command: COMMAND,
+  CONFIG_GET,
+  configGet: CONFIG_GET,
+  CONFIG_RESETASTAT,
+  configResetStat: CONFIG_RESETASTAT,
+  CONFIG_REWRITE,
+  configRewrite: CONFIG_REWRITE,
+  CONFIG_SET,
+  configSet: CONFIG_SET,
+  COPY,
+  copy: COPY,
+  DBSIZE,
+  dbSize: DBSIZE,
+  DECR,
+  decr: DECR,
+  DECRBY,
+  decrBy: DECRBY,
+  DEL,
+  del: DEL,
+  DUMP,
+  dump: DUMP,
+  ECHO,
+  echo: ECHO,
+  EVAL_RO,
+  evalRo: EVAL_RO,
+  EVAL,
+  eval: EVAL,
+  EVALSHA_RO,
+  evalShaRo: EVALSHA_RO,
+  EVALSHA,
+  evalSha: EVALSHA,
+  EXISTS,
+  exists: EXISTS,
+  EXPIRE,
+  expire: EXPIRE,
+  EXPIREAT,
+  expireAt: EXPIREAT,
+  EXPIRETIME,
+  expireTime: EXPIRETIME,
+  FLUSHALL,
+  flushAll: FLUSHALL,
+  FLUSHDB,
+  flushDb: FLUSHDB,
+  FCALL,
+  fCall: FCALL,
+  FCALL_RO,
+  fCallRo: FCALL_RO,
+  FUNCTION_DELETE,
+  functionDelete: FUNCTION_DELETE,
+  FUNCTION_DUMP,
+  functionDump: FUNCTION_DUMP,
+  FUNCTION_FLUSH,
+  functionFlush: FUNCTION_FLUSH,
+  FUNCTION_KILL,
+  functionKill: FUNCTION_KILL,
+  FUNCTION_LIST_WITHCODE,
+  functionListWithCode: FUNCTION_LIST_WITHCODE,
+  FUNCTION_LIST,
+  functionList: FUNCTION_LIST,
+  FUNCTION_LOAD,
+  functionLoad: FUNCTION_LOAD,
+  FUNCTION_RESTORE,
+  functionRestore: FUNCTION_RESTORE,
+  FUNCTION_STATS,
+  functionStats: FUNCTION_STATS,
+  GEOADD,
+  geoAdd: GEOADD,
+  GEODIST,
+  geoDist: GEODIST,
+  GEOHASH,
+  geoHash: GEOHASH,
+  GEOPOS,
+  geoPos: GEOPOS,
+  GEORADIUS_RO_WITH,
+  geoRadiusRoWith: GEORADIUS_RO_WITH,
+  GEORADIUS_RO,
+  geoRadiusRo: GEORADIUS_RO,
+  GEORADIUS_STORE,
+  geoRadiusStore: GEORADIUS_STORE,
+  GEORADIUS_WITH,
+  geoRadiusWith: GEORADIUS_WITH,
+  GEORADIUS,
+  geoRadius: GEORADIUS,
+  GEORADIUSBYMEMBER_RO_WITH,
+  geoRadiusByMemberRoWith: GEORADIUSBYMEMBER_RO_WITH,
+  GEORADIUSBYMEMBER_RO,
+  geoRadiusByMemberRo: GEORADIUSBYMEMBER_RO,
+  GEORADIUSBYMEMBER_STORE,
+  geoRadiusByMemberStore: GEORADIUSBYMEMBER_STORE,
+  GEORADIUSBYMEMBER_WITH,
+  geoRadiusByMemberWith: GEORADIUSBYMEMBER_WITH,
+  GEORADIUSBYMEMBER,
+  geoRadiusByMember: GEORADIUSBYMEMBER,
+  GEOSEARCH_WITH,
+  geoSearchWith: GEOSEARCH_WITH,
+  GEOSEARCH,
+  geoSearch: GEOSEARCH,
+  GEOSEARCHSTORE,
+  geoSearchStore: GEOSEARCHSTORE,
+  GET,
+  get: GET,
+  GETBIT,
+  getBit: GETBIT,
+  GETDEL,
+  getDel: GETDEL,
+  GETEX,
+  getEx: GETEX,
+  GETRANGE,
+  getRange: GETRANGE,
+  GETSET,
+  getSet: GETSET,
+  HDEL,
+  hDel: HDEL,
+  HELLO,
+  hello: HELLO,
+  HEXISTS,
+  hExists: HEXISTS,
+  HEXPIRE,
+  hExpire: HEXPIRE,
+  HEXPIREAT,
+  hExpireAt: HEXPIREAT,
+  HEXPIRETIME,
+  hExpireTime: HEXPIRETIME,
+  HGET,
+  hGet: HGET,
+  HGETALL,
+  hGetAll: HGETALL,
+  HGETDEL,
+  hGetDel: HGETDEL,
+  HGETEX,
+  hGetEx: HGETEX,
+  HINCRBY,
+  hIncrBy: HINCRBY,
+  HINCRBYFLOAT,
+  hIncrByFloat: HINCRBYFLOAT,
+  HKEYS,
+  hKeys: HKEYS,
+  HLEN,
+  hLen: HLEN,
+  HMGET,
+  hmGet: HMGET,
+  HPERSIST,
+  hPersist: HPERSIST,
+  HPEXPIRE,
+  hpExpire: HPEXPIRE,
+  HPEXPIREAT,
+  hpExpireAt: HPEXPIREAT,
+  HPEXPIRETIME,
+  hpExpireTime: HPEXPIRETIME,
+  HPTTL,
+  hpTTL: HPTTL,
+  HRANDFIELD_COUNT_WITHVALUES,
+  hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
+  HRANDFIELD_COUNT,
+  hRandFieldCount: HRANDFIELD_COUNT,
+  HRANDFIELD,
+  hRandField: HRANDFIELD,
+  HSCAN,
+  hScan: HSCAN,
+  HSCAN_NOVALUES,
+  hScanNoValues: HSCAN_NOVALUES,
+  HSET,
+  hSet: HSET,
+  HSETEX,
+  hSetEx: HSETEX,
+  HSETNX,
+  hSetNX: HSETNX,
+  HSTRLEN,
+  hStrLen: HSTRLEN,
+  HTTL,
+  hTTL: HTTL,
+  HVALS,
+  hVals: HVALS,
+  INCR,
+  incr: INCR,
+  INCRBY,
+  incrBy: INCRBY,
+  INCRBYFLOAT,
+  incrByFloat: INCRBYFLOAT,
+  INFO,
+  info: INFO,
+  KEYS,
+  keys: KEYS,
+  LASTSAVE,
+  lastSave: LASTSAVE,
+  LATENCY_DOCTOR,
+  latencyDoctor: LATENCY_DOCTOR,
+  LATENCY_GRAPH,
+  latencyGraph: LATENCY_GRAPH,
+  LATENCY_HISTORY,
+  latencyHistory: LATENCY_HISTORY,
+  LATENCY_LATEST,
+  latencyLatest: LATENCY_LATEST,
+  LCS_IDX_WITHMATCHLEN,
+  lcsIdxWithMatchLen: LCS_IDX_WITHMATCHLEN,
+  LCS_IDX,
+  lcsIdx: LCS_IDX,
+  LCS_LEN,
+  lcsLen: LCS_LEN,
+  LCS,
+  lcs: LCS,
+  LINDEX,
+  lIndex: LINDEX,
+  LINSERT,
+  lInsert: LINSERT,
+  LLEN,
+  lLen: LLEN,
+  LMOVE,
+  lMove: LMOVE,
+  LMPOP,
+  lmPop: LMPOP,
+  LOLWUT,
+  LPOP_COUNT,
+  lPopCount: LPOP_COUNT,
+  LPOP,
+  lPop: LPOP,
+  LPOS_COUNT,
+  lPosCount: LPOS_COUNT,
+  LPOS,
+  lPos: LPOS,
+  LPUSH,
+  lPush: LPUSH,
+  LPUSHX,
+  lPushX: LPUSHX,
+  LRANGE,
+  lRange: LRANGE,
+  LREM,
+  lRem: LREM,
+  LSET,
+  lSet: LSET,
+  LTRIM,
+  lTrim: LTRIM,
+  MEMORY_DOCTOR,
+  memoryDoctor: MEMORY_DOCTOR,
+  'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS,
+  memoryMallocStats: MEMORY_MALLOC_STATS,
+  MEMORY_PURGE,
+  memoryPurge: MEMORY_PURGE,
+  MEMORY_STATS,
+  memoryStats: MEMORY_STATS,
+  MEMORY_USAGE,
+  memoryUsage: MEMORY_USAGE,
+  MGET,
+  mGet: MGET,
+  MIGRATE,
+  migrate: MIGRATE,
+  MODULE_LIST,
+  moduleList: MODULE_LIST,
+  MODULE_LOAD,
+  moduleLoad: MODULE_LOAD,
+  MODULE_UNLOAD,
+  moduleUnload: MODULE_UNLOAD,
+  MOVE,
+  move: MOVE,
+  MSET,
+  mSet: MSET,
+  MSETNX,
+  mSetNX: MSETNX,
+  OBJECT_ENCODING,
+  objectEncoding: OBJECT_ENCODING,
+  OBJECT_FREQ,
+  objectFreq: OBJECT_FREQ,
+  OBJECT_IDLETIME,
+  objectIdleTime: OBJECT_IDLETIME,
+  OBJECT_REFCOUNT,
+  objectRefCount: OBJECT_REFCOUNT,
+  PERSIST,
+  persist: PERSIST,
+  PEXPIRE,
+  pExpire: PEXPIRE,
+  PEXPIREAT,
+  pExpireAt: PEXPIREAT,
+  PEXPIRETIME,
+  pExpireTime: PEXPIRETIME,
+  PFADD,
+  pfAdd: PFADD,
+  PFCOUNT,
+  pfCount: PFCOUNT,
+  PFMERGE,
+  pfMerge: PFMERGE,
+  PING,
+  /**
+   * ping jsdoc
+   */
+  ping: PING,
+  PSETEX,
+  pSetEx: PSETEX,
+  PTTL,
+  pTTL: PTTL,
+  PUBLISH,
+  publish: PUBLISH,
+  PUBSUB_CHANNELS,
+  pubSubChannels: PUBSUB_CHANNELS,
+  PUBSUB_NUMPAT,
+  pubSubNumPat: PUBSUB_NUMPAT,
+  PUBSUB_NUMSUB,
+  pubSubNumSub: PUBSUB_NUMSUB,
+  PUBSUB_SHARDNUMSUB,
+  pubSubShardNumSub: PUBSUB_SHARDNUMSUB,
+  PUBSUB_SHARDCHANNELS,
+  pubSubShardChannels: PUBSUB_SHARDCHANNELS,
+  RANDOMKEY,
+  randomKey: RANDOMKEY,
+  READONLY,
+  readonly: READONLY,
+  RENAME,
+  rename: RENAME,
+  RENAMENX,
+  renameNX: RENAMENX,
+  REPLICAOF,
+  replicaOf: REPLICAOF,
+  'RESTORE-ASKING': RESTORE_ASKING,
+  restoreAsking: RESTORE_ASKING,
+  RESTORE,
+  restore: RESTORE,
+  RPOP_COUNT,
+  rPopCount: RPOP_COUNT,
+  ROLE,
+  role: ROLE,
+  RPOP,
+  rPop: RPOP,
+  RPOPLPUSH,
+  rPopLPush: RPOPLPUSH,
+  RPUSH,
+  rPush: RPUSH,
+  RPUSHX,
+  rPushX: RPUSHX,
+  SADD,
+  sAdd: SADD,
+  SCAN,
+  scan: SCAN,
+  SCARD,
+  sCard: SCARD,
+  SCRIPT_DEBUG,
+  scriptDebug: SCRIPT_DEBUG,
+  SCRIPT_EXISTS,
+  scriptExists: SCRIPT_EXISTS,
+  SCRIPT_FLUSH,
+  scriptFlush: SCRIPT_FLUSH,
+  SCRIPT_KILL,
+  scriptKill: SCRIPT_KILL,
+  SCRIPT_LOAD,
+  scriptLoad: SCRIPT_LOAD,
+  SDIFF,
+  sDiff: SDIFF,
+  SDIFFSTORE,
+  sDiffStore: SDIFFSTORE,
+  SET,
+  set: SET,
+  SETBIT,
+  setBit: SETBIT,
+  SETEX,
+  setEx: SETEX,
+  SETNX,
+  setNX: SETNX,
+  SETRANGE,
+  setRange: SETRANGE,
+  SINTER,
+  sInter: SINTER,
+  SINTERCARD,
+  sInterCard: SINTERCARD,
+  SINTERSTORE,
+  sInterStore: SINTERSTORE,
+  SISMEMBER,
+  sIsMember: SISMEMBER,
+  SMEMBERS,
+  sMembers: SMEMBERS,
+  SMISMEMBER,
+  smIsMember: SMISMEMBER,
+  SMOVE,
+  sMove: SMOVE,
+  SORT_RO,
+  sortRo: SORT_RO,
+  SORT_STORE,
+  sortStore: SORT_STORE,
+  SORT,
+  sort: SORT,
+  SPOP_COUNT,
+  sPopCount: SPOP_COUNT,
+  SPOP,
+  sPop: SPOP,
+  SPUBLISH,
+  sPublish: SPUBLISH,
+  SRANDMEMBER_COUNT,
+  sRandMemberCount: SRANDMEMBER_COUNT,
+  SRANDMEMBER,
+  sRandMember: SRANDMEMBER,
+  SREM,
+  sRem: SREM,
+  SSCAN,
+  sScan: SSCAN,
+  STRLEN,
+  strLen: STRLEN,
+  SUNION,
+  sUnion: SUNION,
+  SUNIONSTORE,
+  sUnionStore: SUNIONSTORE,
+  SWAPDB,
+  swapDb: SWAPDB,
+  TIME,
+  time: TIME,
+  TOUCH,
+  touch: TOUCH,
+  TTL,
+  ttl: TTL,
+  TYPE,
+  type: TYPE,
+  UNLINK,
+  unlink: UNLINK,
+  WAIT,
+  wait: WAIT,
+  XACK,
+  xAck: XACK,
+  XACKDEL,
+  xAckDel: XACKDEL,
+  XADD_NOMKSTREAM,
+  xAddNoMkStream: XADD_NOMKSTREAM,
+  XADD,
+  xAdd: XADD,
+  XAUTOCLAIM_JUSTID,
+  xAutoClaimJustId: XAUTOCLAIM_JUSTID,
+  XAUTOCLAIM,
+  xAutoClaim: XAUTOCLAIM,
+  XCLAIM_JUSTID,
+  xClaimJustId: XCLAIM_JUSTID,
+  XCLAIM,
+  xClaim: XCLAIM,
+  XDEL,
+  xDel: XDEL,
+  XDELEX,
+  xDelEx: XDELEX,
+  XGROUP_CREATE,
+  xGroupCreate: XGROUP_CREATE,
+  XGROUP_CREATECONSUMER,
+  xGroupCreateConsumer: XGROUP_CREATECONSUMER,
+  XGROUP_DELCONSUMER,
+  xGroupDelConsumer: XGROUP_DELCONSUMER,
+  XGROUP_DESTROY,
+  xGroupDestroy: XGROUP_DESTROY,
+  XGROUP_SETID,
+  xGroupSetId: XGROUP_SETID,
+  XINFO_CONSUMERS,
+  xInfoConsumers: XINFO_CONSUMERS,
+  XINFO_GROUPS,
+  xInfoGroups: XINFO_GROUPS,
+  XINFO_STREAM,
+  xInfoStream: XINFO_STREAM,
+  XLEN,
+  xLen: XLEN,
+  XPENDING_RANGE,
+  xPendingRange: XPENDING_RANGE,
+  XPENDING,
+  xPending: XPENDING,
+  XRANGE,
+  xRange: XRANGE,
+  XREAD,
+  xRead: XREAD,
+  XREADGROUP,
+  xReadGroup: XREADGROUP,
+  XREVRANGE,
+  xRevRange: XREVRANGE,
+  XSETID,
+  xSetId: XSETID,
+  XTRIM,
+  xTrim: XTRIM,
+  ZADD_INCR,
+  zAddIncr: ZADD_INCR,
+  ZADD,
+  zAdd: ZADD,
+  ZCARD,
+  zCard: ZCARD,
+  ZCOUNT,
+  zCount: ZCOUNT,
+  ZDIFF_WITHSCORES,
+  zDiffWithScores: ZDIFF_WITHSCORES,
+  ZDIFF,
+  zDiff: ZDIFF,
+  ZDIFFSTORE,
+  zDiffStore: ZDIFFSTORE,
+  ZINCRBY,
+  zIncrBy: ZINCRBY,
+  ZINTER_WITHSCORES,
+  zInterWithScores: ZINTER_WITHSCORES,
+  ZINTER,
+  zInter: ZINTER,
+  ZINTERCARD,
+  zInterCard: ZINTERCARD,
+  ZINTERSTORE,
+  zInterStore: ZINTERSTORE,
+  ZLEXCOUNT,
+  zLexCount: ZLEXCOUNT,
+  ZMPOP,
+  zmPop: ZMPOP,
+  ZMSCORE,
+  zmScore: ZMSCORE,
+  ZPOPMAX_COUNT,
+  zPopMaxCount: ZPOPMAX_COUNT,
+  ZPOPMAX,
+  zPopMax: ZPOPMAX,
+  ZPOPMIN_COUNT,
+  zPopMinCount: ZPOPMIN_COUNT,
+  ZPOPMIN,
+  zPopMin: ZPOPMIN,
+  ZRANDMEMBER_COUNT_WITHSCORES,
+  zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES,
+  ZRANDMEMBER_COUNT,
+  zRandMemberCount: ZRANDMEMBER_COUNT,
+  ZRANDMEMBER,
+  zRandMember: ZRANDMEMBER,
+  ZRANGE_WITHSCORES,
+  zRangeWithScores: ZRANGE_WITHSCORES,
+  ZRANGE,
+  zRange: ZRANGE,
+  ZRANGEBYLEX,
+  zRangeByLex: ZRANGEBYLEX,
+  ZRANGEBYSCORE_WITHSCORES,
+  zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES,
+  ZRANGEBYSCORE,
+  zRangeByScore: ZRANGEBYSCORE,
+  ZRANGESTORE,
+  zRangeStore: ZRANGESTORE,
+  ZRANK_WITHSCORE,
+  zRankWithScore: ZRANK_WITHSCORE,
+  ZRANK,
+  zRank: ZRANK,
+  ZREM,
+  zRem: ZREM,
+  ZREMRANGEBYLEX,
+  zRemRangeByLex: ZREMRANGEBYLEX,
+  ZREMRANGEBYRANK,
+  zRemRangeByRank: ZREMRANGEBYRANK,
+  ZREMRANGEBYSCORE,
+  zRemRangeByScore: ZREMRANGEBYSCORE,
+  ZREVRANK,
+  zRevRank: ZREVRANK,
+  ZSCAN,
+  zScan: ZSCAN,
+  ZSCORE,
+  zScore: ZSCORE,
+  ZUNION_WITHSCORES,
+  zUnionWithScores: ZUNION_WITHSCORES,
+  ZUNION,
+  zUnion: ZUNION,
+  ZUNIONSTORE,
+  zUnionStore: ZUNIONSTORE,
+  VADD,
+  vAdd: VADD,
+  VCARD,
+  vCard: VCARD,
+  VDIM,
+  vDim: VDIM,
+  VEMB,
+  vEmb: VEMB,
+  VEMB_RAW,
+  vEmbRaw: VEMB_RAW,
+  VGETATTR,
+  vGetAttr: VGETATTR,
+  VINFO,
+  vInfo: VINFO,
+  VLINKS,
+  vLinks: VLINKS,
+  VLINKS_WITHSCORES,
+  vLinksWithScores: VLINKS_WITHSCORES,
+  VRANDMEMBER,
+  vRandMember: VRANDMEMBER,
+  VREM,
+  vRem: VREM,
+  VSETATTR,
+  vSetAttr: VSETATTR,
+  VSIM,
+  vSim: VSIM,
+  VSIM_WITHSCORES,
+  vSimWithScores: VSIM_WITHSCORES
+} as const satisfies RedisCommands;
diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts
index aa97d9cf26d..db37ec1a9ba 100644
--- a/packages/client/lib/errors.ts
+++ b/packages/client/lib/errors.ts
@@ -1,84 +1,94 @@
-import { RedisCommandRawReply } from './commands';
-
 export class AbortError extends Error {
-    constructor() {
-        super('The command was aborted');
-    }
+  constructor() {
+    super('The command was aborted');
+  }
 }
 
 export class WatchError extends Error {
-    constructor() {
-        super('One (or more) of the watched keys has been changed');
-    }
+  constructor(message = 'One (or more) of the watched keys has been changed') {
+    super(message);
+  }
 }
 
 export class ConnectionTimeoutError extends Error {
-    constructor() {
-        super('Connection timeout');
-    }
+  constructor() {
+    super('Connection timeout');
+  }
+}
+
+export class SocketTimeoutError extends Error {
+  constructor(timeout: number) {
+    super(`Socket timeout timeout. Expecting data, but didn't receive any in ${timeout}ms.`);
+  }
 }
 
 export class ClientClosedError extends Error {
-    constructor() {
-        super('The client is closed');
-    }
+  constructor() {
+    super('The client is closed');
+  }
 }
 
 export class ClientOfflineError extends Error {
-    constructor() {
-        super('The client is offline');
-    }
+  constructor() {
+    super('The client is offline');
+  }
 }
 
 export class DisconnectsClientError extends Error {
-    constructor() {
-        super('Disconnects client');
-    }
+  constructor() {
+    super('Disconnects client');
+  }
 }
 
 export class SocketClosedUnexpectedlyError extends Error {
-    constructor() {
-        super('Socket closed unexpectedly');
-    }
+  constructor() {
+    super('Socket closed unexpectedly');
+  }
 }
 
 export class RootNodesUnavailableError extends Error {
-    constructor() {
-        super('All the root nodes are unavailable');
-    }
+  constructor() {
+    super('All the root nodes are unavailable');
+  }
 }
 
 export class ReconnectStrategyError extends Error {
-    originalError: Error;
-    socketError: unknown;
+  originalError: Error;
+  socketError: unknown;
 
-    constructor(originalError: Error, socketError: unknown) {
-        super(originalError.message);
-        this.originalError = originalError;
-        this.socketError = socketError;
-    }
+  constructor(originalError: Error, socketError: unknown) {
+    super(originalError.message);
+    this.originalError = originalError;
+    this.socketError = socketError;
+  }
 }
 
 export class ErrorReply extends Error {
-    constructor(message: string) {
-        super(message);
-        this.stack = undefined;
-    }
+  constructor(message: string) {
+    super(message);
+    this.stack = undefined;
+  }
 }
 
+export class SimpleError extends ErrorReply {}
+
+export class BlobError extends ErrorReply {}
+
+export class TimeoutError extends Error {}
+
 export class MultiErrorReply extends ErrorReply {
-    replies;
-    errorIndexes;
+  replies: Array<ErrorReply>;
+  errorIndexes: Array<number>;
 
-    constructor(replies: Array<RedisCommandRawReply | ErrorReply>, errorIndexes: Array<number>) {
-        super(`${errorIndexes.length} commands failed, see .replies and .errorIndexes for more information`);
-        this.replies = replies;
-        this.errorIndexes = errorIndexes;
-    }
+  constructor(replies: Array<ErrorReply>, errorIndexes: Array<number>) {
+    super(`${errorIndexes.length} commands failed, see .replies and .errorIndexes for more information`);
+    this.replies = replies;
+    this.errorIndexes = errorIndexes;
+  }
 
-    *errors() {
-        for (const index of this.errorIndexes) {
-            yield this.replies[index];
-        }
+  *errors() {
+    for (const index of this.errorIndexes) {
+        yield this.replies[index];
     }
+  }
 }
diff --git a/packages/client/lib/lua-script.ts b/packages/client/lib/lua-script.ts
index da19417ec25..6d395b71232 100644
--- a/packages/client/lib/lua-script.ts
+++ b/packages/client/lib/lua-script.ts
@@ -1,22 +1,22 @@
-import { createHash } from 'crypto';
-import { RedisCommand } from './commands';
+import { createHash } from 'node:crypto';
+import { Command } from './RESP/types';
 
-export interface RedisScriptConfig extends RedisCommand {
-    SCRIPT: string;
-    NUMBER_OF_KEYS?: number;
+export type RedisScriptConfig = Command & {
+  SCRIPT: string | Buffer;
+  NUMBER_OF_KEYS?: number;
 }
 
 export interface SHA1 {
-    SHA1: string;
+  SHA1: string;
 }
 
 export function defineScript<S extends RedisScriptConfig>(script: S): S & SHA1 {
-    return {
-        ...script,
-        SHA1: scriptSha1(script.SCRIPT)
-    };
+  return {
+    ...script,
+    SHA1: scriptSha1(script.SCRIPT)
+  };
 }
 
-export function scriptSha1(script: string): string {
-    return createHash('sha1').update(script).digest('hex');
+export function scriptSha1(script: RedisScriptConfig['SCRIPT']): string {
+  return createHash('sha1').update(script).digest('hex');
 }
diff --git a/packages/client/lib/multi-command.spec.ts b/packages/client/lib/multi-command.spec.ts
index b0f79c6e157..7e77f88d10b 100644
--- a/packages/client/lib/multi-command.spec.ts
+++ b/packages/client/lib/multi-command.spec.ts
@@ -1,96 +1,77 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import RedisMultiCommand from './multi-command';
-import { WatchError } from './errors';
 import { SQUARE_SCRIPT } from './client/index.spec';
 
 describe('Multi Command', () => {
-    it('generateChainId', () => {
-        assert.equal(
-            typeof RedisMultiCommand.generateChainId(),
-            'symbol'
-        );
-    });
-
-    it('addCommand', () => {
-        const multi = new RedisMultiCommand();
-        multi.addCommand(['PING']);
-
-        assert.deepEqual(
-            multi.queue[0].args,
-            ['PING']
-        );
-    });
+  it('addCommand', () => {
+    const multi = new RedisMultiCommand();
+    multi.addCommand(['PING']);
 
-    it('addScript', () => {
-        const multi = new RedisMultiCommand();
+    assert.deepEqual(
+      multi.queue[0].args,
+      ['PING']
+    );
+  });
 
-        multi.addScript(SQUARE_SCRIPT, ['1']);
-        assert.equal(
-            multi.scriptsInUse.has(SQUARE_SCRIPT.SHA1),
-            true
-        );
-        assert.deepEqual(
-            multi.queue[0].args,
-            ['EVAL', SQUARE_SCRIPT.SCRIPT, '0', '1']
-        );
+  describe('addScript', () => {
+    const multi = new RedisMultiCommand();
 
-        multi.addScript(SQUARE_SCRIPT, ['2']);
-        assert.equal(
-            multi.scriptsInUse.has(SQUARE_SCRIPT.SHA1),
-            true
-        );
-        assert.deepEqual(
-            multi.queue[1].args,
-            ['EVALSHA', SQUARE_SCRIPT.SHA1, '0', '2']
-        );
+    it('should use EVAL', () => {
+      multi.addScript(SQUARE_SCRIPT, ['1']);
+      assert.deepEqual(
+        Array.from(multi.queue.at(-1).args),
+        ['EVAL', SQUARE_SCRIPT.SCRIPT, '1', '1']
+      );
     });
 
-    describe('exec', () => {
-        it('without commands', () => {
-            assert.deepEqual(
-                new RedisMultiCommand().queue,
-                []
-            );
-        });
+    it('should use EVALSHA', () => {
+      multi.addScript(SQUARE_SCRIPT, ['2']);
+      assert.deepEqual(
+        Array.from(multi.queue.at(-1).args),
+        ['EVALSHA', SQUARE_SCRIPT.SHA1, '1', '2']
+      );
+    });
 
-        it('with commands', () => {
-            const multi = new RedisMultiCommand();
-            multi.addCommand(['PING']);
+    it('without NUMBER_OF_KEYS', () => {
+      multi.addScript({
+        ...SQUARE_SCRIPT,
+        NUMBER_OF_KEYS: undefined
+      }, ['2']);
+      assert.deepEqual(
+        Array.from(multi.queue.at(-1).args),
+        ['EVALSHA', SQUARE_SCRIPT.SHA1, '2']
+      );
+    });
+  });
 
-            assert.deepEqual(
-                multi.queue,
-                [{
-                    args: ['PING'],
-                    transformReply: undefined
-                }]
-            );
-        });
+  describe('exec', () => {
+    it('without commands', () => {
+      assert.deepEqual(
+        new RedisMultiCommand().queue,
+        []
+      );
     });
 
-    describe('handleExecReplies', () => {
-        it('WatchError', () => {
-            assert.throws(
-                () => new RedisMultiCommand().handleExecReplies([null]),
-                WatchError
-            );
-        });
+    it('with commands', () => {
+      const multi = new RedisMultiCommand();
+      multi.addCommand(['PING']);
 
-        it('with replies', () => {
-            const multi = new RedisMultiCommand();
-            multi.addCommand(['PING']);
-            assert.deepEqual(
-                multi.handleExecReplies(['OK', 'QUEUED', ['PONG']]),
-                ['PONG']
-            );
-        });
+      assert.deepEqual(
+        multi.queue,
+        [{
+          args: ['PING'],
+          transformReply: undefined
+        }]
+      );
     });
+  });
 
-    it('transformReplies', () => {
-        const multi = new RedisMultiCommand();
-        multi.addCommand(['PING'], (reply: string) => reply.substring(0, 2));
-        assert.deepEqual(
-            multi.transformReplies(['PONG']),
-            ['PO']
-        );
-    });
+  it('transformReplies', () => {
+    const multi = new RedisMultiCommand();
+    multi.addCommand(['PING'], (reply: string) => reply.substring(0, 2));
+    assert.deepEqual(
+      multi.transformReplies(['PONG']),
+      ['PO']
+    );
+  });
 });
diff --git a/packages/client/lib/multi-command.ts b/packages/client/lib/multi-command.ts
index 642c2ea36c0..3d45a02fb4d 100644
--- a/packages/client/lib/multi-command.ts
+++ b/packages/client/lib/multi-command.ts
@@ -1,95 +1,70 @@
-import { fCallArguments } from './commander';
-import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunction, RedisScript } from './commands';
-import { ErrorReply, MultiErrorReply, WatchError } from './errors';
+import { CommandArguments, RedisScript, ReplyUnion, TransformReply, TypeMapping } from './RESP/types';
+import { ErrorReply, MultiErrorReply } from './errors';
+
+export type MULTI_REPLY = {
+  GENERIC: 'generic';
+  TYPED: 'typed';
+};
+
+export type MultiReply = MULTI_REPLY[keyof MULTI_REPLY];
+
+export type MultiReplyType<T extends MultiReply, REPLIES> = T extends MULTI_REPLY['TYPED'] ? REPLIES : Array<ReplyUnion>;
 
 export interface RedisMultiQueuedCommand {
-    args: RedisCommandArguments;
-    transformReply?: RedisCommand['transformReply'];
+  args: CommandArguments;
+  transformReply?: TransformReply;
 }
 
 export default class RedisMultiCommand {
-    static generateChainId(): symbol {
-        return Symbol('RedisMultiCommand Chain Id');
-    }
+  private readonly typeMapping?: TypeMapping;
 
-    readonly queue: Array<RedisMultiQueuedCommand> = [];
+  constructor(typeMapping?: TypeMapping) {
+    this.typeMapping = typeMapping;
+  }
 
-    readonly scriptsInUse = new Set<string>();
+  readonly queue: Array<RedisMultiQueuedCommand> = [];
 
-    addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): void {
-        this.queue.push({
-            args,
-            transformReply
-        });
-    }
+  readonly scriptsInUse = new Set<string>();
 
-    addFunction(name: string, fn: RedisFunction, args: Array<unknown>): RedisCommandArguments {
-        const transformedArguments = fCallArguments(
-            name,
-            fn,
-            fn.transformArguments(...args)
-        );
-        this.queue.push({
-            args: transformedArguments,
-            transformReply: fn.transformReply
-        });
-        return transformedArguments;
-    }
-
-    addScript(script: RedisScript, args: Array<unknown>): RedisCommandArguments {
-        const transformedArguments: RedisCommandArguments = [];
-        if (this.scriptsInUse.has(script.SHA1)) {
-            transformedArguments.push(
-                'EVALSHA',
-                script.SHA1
-            );
-        } else {
-            this.scriptsInUse.add(script.SHA1);
-            transformedArguments.push(
-                'EVAL',
-                script.SCRIPT
-            );
-        }
+  addCommand(args: CommandArguments, transformReply?: TransformReply) {
+    this.queue.push({
+      args,
+      transformReply
+    });
+  }
 
-        if (script.NUMBER_OF_KEYS !== undefined) {
-            transformedArguments.push(script.NUMBER_OF_KEYS.toString());
-        }
-
-        const scriptArguments = script.transformArguments(...args);
-        transformedArguments.push(...scriptArguments);
-        if (scriptArguments.preserve) {
-            transformedArguments.preserve = scriptArguments.preserve;
-        }
-
-        this.addCommand(
-            transformedArguments,
-            script.transformReply
-        );
+  addScript(script: RedisScript, args: CommandArguments, transformReply?: TransformReply) {
+    const redisArgs: CommandArguments = [];
+    redisArgs.preserve = args.preserve;
+    if (this.scriptsInUse.has(script.SHA1)) {
+      redisArgs.push('EVALSHA', script.SHA1);
+    } else {
+      this.scriptsInUse.add(script.SHA1);
+      redisArgs.push('EVAL', script.SCRIPT);
+    }
 
-        return transformedArguments;
+    if (script.NUMBER_OF_KEYS !== undefined) {
+      redisArgs.push(script.NUMBER_OF_KEYS.toString());
     }
 
-    handleExecReplies(rawReplies: Array<RedisCommandRawReply | ErrorReply>): Array<RedisCommandRawReply> {
-        const execReply = rawReplies[rawReplies.length - 1] as (null | Array<RedisCommandRawReply>);
-        if (execReply === null) {
-            throw new WatchError();
-        }
+    redisArgs.push(...args);
 
-        return this.transformReplies(execReply);
-    }
+    this.addCommand(redisArgs, transformReply);
+  }
+  
+  transformReplies(rawReplies: Array<unknown>): Array<unknown> {
+    const errorIndexes: Array<number> = [],
+      replies = rawReplies.map((reply, i) => {
+        if (reply instanceof ErrorReply) {
+          errorIndexes.push(i);
+          return reply;
+        }
 
-    transformReplies(rawReplies: Array<RedisCommandRawReply | ErrorReply>): Array<RedisCommandRawReply> {
-        const errorIndexes: Array<number> = [],
-            replies = rawReplies.map((reply, i) => {
-                if (reply instanceof ErrorReply) {
-                    errorIndexes.push(i);
-                    return reply;
-                }
-                const { transformReply, args } = this.queue[i];
-                return transformReply ? transformReply(reply, args.preserve) : reply;
-            });
+        const { transformReply, args } = this.queue[i];
+        return transformReply ? transformReply(reply, args.preserve, this.typeMapping) : reply;
+      });
 
-        if (errorIndexes.length) throw new MultiErrorReply(replies, errorIndexes);
-        return replies;
-    }
+    if (errorIndexes.length) throw new MultiErrorReply(replies, errorIndexes);
+    return replies;
+  }
 }
diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts
new file mode 100644
index 00000000000..842b86a0596
--- /dev/null
+++ b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts
@@ -0,0 +1,18 @@
+import { RedisArgument, MapReply, BlobStringReply, Command } from '../../RESP/types';
+import { CommandParser } from '../../client/parser';
+import { transformTuplesReply } from '../../commands/generic-transformers';
+
+export default {
+  /**
+   * Returns information about the specified master.
+   * @param parser - The Redis command parser.
+   * @param dbname - Name of the master.
+   */
+  parseCommand(parser: CommandParser, dbname: RedisArgument) {
+    parser.push('SENTINEL', 'MASTER', dbname);
+  },
+  transformReply: {
+    2: transformTuplesReply<BlobStringReply>,
+    3: undefined as unknown as () => MapReply<BlobStringReply, BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts
new file mode 100644
index 00000000000..eed4f7e7233
--- /dev/null
+++ b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts
@@ -0,0 +1,17 @@
+import { CommandParser } from '../../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types';
+
+export default {
+  /**
+   * Instructs a Sentinel to monitor a new master with the specified parameters.
+   * @param parser - The Redis command parser.
+   * @param dbname - Name that identifies the master.
+   * @param host - Host of the master.
+   * @param port - Port of the master.
+   * @param quorum - Number of Sentinels that need to agree to trigger a failover.
+   */
+  parseCommand(parser: CommandParser, dbname: RedisArgument, host: RedisArgument, port: RedisArgument, quorum: RedisArgument) {
+    parser.push('SENTINEL', 'MONITOR', dbname, host, port, quorum);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'> 
+} as const satisfies Command;
diff --git a/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts
new file mode 100644
index 00000000000..4228a2123d9
--- /dev/null
+++ b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts
@@ -0,0 +1,29 @@
+import { CommandParser } from '../../client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, MapReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types';
+import { transformTuplesReply } from '../../commands/generic-transformers';
+
+export default {
+  /**
+   * Returns a list of replicas for the specified master.
+   * @param parser - The Redis command parser.
+   * @param dbname - Name of the master.
+   */
+  parseCommand(parser: CommandParser, dbname: RedisArgument) {
+    parser.push('SENTINEL', 'REPLICAS', dbname);
+  },
+  transformReply: {
+    2: (reply: ArrayReply<ArrayReply<BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      const inferred = reply as unknown as UnwrapReply<typeof reply>;
+      const initial: Array<MapReply<BlobStringReply, BlobStringReply>> = [];
+
+      return inferred.reduce(
+        (sentinels: Array<MapReply<BlobStringReply, BlobStringReply>>, x: ArrayReply<BlobStringReply>) => {
+          sentinels.push(transformTuplesReply(x, undefined, typeMapping)); 
+          return sentinels;
+        }, 
+        initial
+      );
+    },
+    3: undefined as unknown as () => ArrayReply<MapReply<BlobStringReply, BlobStringReply>>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts
new file mode 100644
index 00000000000..20cccbb76b6
--- /dev/null
+++ b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts
@@ -0,0 +1,29 @@
+import { CommandParser } from '../../client/parser';
+import { RedisArgument, ArrayReply, MapReply, BlobStringReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types';
+import { transformTuplesReply } from '../../commands/generic-transformers';
+
+export default {
+  /**
+   * Returns a list of Sentinel instances for the specified master.
+   * @param parser - The Redis command parser.
+   * @param dbname - Name of the master.
+   */
+  parseCommand(parser: CommandParser, dbname: RedisArgument) {
+    parser.push('SENTINEL', 'SENTINELS', dbname);
+  },
+  transformReply: {
+    2: (reply: ArrayReply<ArrayReply<BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      const inferred = reply as unknown as UnwrapReply<typeof reply>;
+      const initial: Array<MapReply<BlobStringReply, BlobStringReply>> = [];
+      
+      return inferred.reduce(
+        (sentinels: Array<MapReply<BlobStringReply, BlobStringReply>>, x: ArrayReply<BlobStringReply>) => {
+          sentinels.push(transformTuplesReply(x, undefined, typeMapping)); 
+          return sentinels;
+        }, 
+        initial
+      );
+    },
+    3: undefined as unknown as () => ArrayReply<MapReply<BlobStringReply, BlobStringReply>>
+  }
+} as const satisfies Command;
diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SET.ts b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts
new file mode 100644
index 00000000000..b2881c14e5a
--- /dev/null
+++ b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts
@@ -0,0 +1,24 @@
+import { CommandParser } from '../../client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types';
+
+export type SentinelSetOptions = Array<{
+  option: RedisArgument;
+  value: RedisArgument;
+}>;
+
+export default {
+  /**
+   * Sets configuration parameters for a specific master.
+   * @param parser - The Redis command parser.
+   * @param dbname - Name of the master.
+   * @param options - Configuration options to set as option-value pairs.
+   */
+  parseCommand(parser: CommandParser, dbname: RedisArgument, options: SentinelSetOptions) {
+    parser.push('SENTINEL', 'SET', dbname);
+
+    for (const option of options) {
+      parser.push(option.option, option.value);
+    }
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'> 
+} as const satisfies Command;
diff --git a/packages/client/lib/sentinel/commands/index.ts b/packages/client/lib/sentinel/commands/index.ts
new file mode 100644
index 00000000000..1fc16f872f6
--- /dev/null
+++ b/packages/client/lib/sentinel/commands/index.ts
@@ -0,0 +1,19 @@
+import { RedisCommands } from '../../RESP/types';
+import SENTINEL_MASTER from './SENTINEL_MASTER';
+import SENTINEL_MONITOR from './SENTINEL_MONITOR';
+import SENTINEL_REPLICAS from './SENTINEL_REPLICAS';
+import SENTINEL_SENTINELS from './SENTINEL_SENTINELS';
+import SENTINEL_SET from './SENTINEL_SET';
+
+export default {
+  SENTINEL_SENTINELS,
+  sentinelSentinels: SENTINEL_SENTINELS,
+  SENTINEL_MASTER,
+  sentinelMaster: SENTINEL_MASTER,
+  SENTINEL_REPLICAS,
+  sentinelReplicas: SENTINEL_REPLICAS,
+  SENTINEL_MONITOR,
+  sentinelMonitor: SENTINEL_MONITOR,
+  SENTINEL_SET,
+  sentinelSet: SENTINEL_SET
+} as const satisfies RedisCommands;
diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts
new file mode 100644
index 00000000000..ef1702eab13
--- /dev/null
+++ b/packages/client/lib/sentinel/index.spec.ts
@@ -0,0 +1,1044 @@
+import { strict as assert } from 'node:assert';
+import { setTimeout } from 'node:timers/promises';
+import testUtils, { GLOBAL, MATH_FUNCTION } from '../test-utils';
+import { RESP_TYPES } from '../RESP/decoder';
+import { WatchError } from "../errors";
+import { RedisSentinelConfig, SentinelFramework } from "./test-util";
+import { RedisSentinelEvent, RedisSentinelType, RedisSentinelClientType, RedisNode } from "./types";
+import RedisSentinel from "./index";
+import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, NumberReply } from '../RESP/types';
+import { promisify } from 'node:util';
+import { exec } from 'node:child_process';
+import { BasicPooledClientSideCache } from '../client/cache'
+import { once } from 'node:events'
+const execAsync = promisify(exec);
+
+describe('RedisSentinel', () => {
+  describe('initialization', () => {
+    describe('clientSideCache validation', () => {
+      const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
+      const options = {
+        name: 'mymaster',
+        sentinelRootNodes: [
+          { host: 'localhost', port: 26379 }
+        ]
+      };
+
+      it('should throw error when clientSideCache is enabled with RESP 2', () => {
+        assert.throws(
+          () => RedisSentinel.create({
+            ...options,
+            clientSideCache: clientSideCacheConfig,
+            RESP: 2 as const,
+          }),
+          new Error('Client Side Caching is only supported with RESP3')
+        );
+      });
+
+      it('should throw error when clientSideCache is enabled with RESP undefined', () => {
+        assert.throws(
+          () => RedisSentinel.create({
+            ...options,
+            clientSideCache: clientSideCacheConfig,
+          }),
+          new Error('Client Side Caching is only supported with RESP3')
+        );
+      });
+
+      it('should not throw when clientSideCache is enabled with RESP 3', () => {
+        assert.doesNotThrow(() =>
+          RedisSentinel.create({
+            ...options,
+            clientSideCache: clientSideCacheConfig,
+            RESP: 3 as const,
+          })
+        );
+      });
+
+      testUtils.testWithClientSentinel('should successfully connect to sentinel', async () => {
+      }, {
+        ...GLOBAL.SENTINEL.OPEN,
+        sentinelOptions: {
+          RESP: 3,
+          clientSideCache: { ttl: 0, maxEntries: 0, evictPolicy: 'LRU'},
+        },
+      })
+
+    });
+  });
+});
+
+[GLOBAL.SENTINEL.OPEN, GLOBAL.SENTINEL.PASSWORD].forEach(testOptions => {
+  const passIndex = testOptions.serverArguments.indexOf('--requirepass')+1;
+  let password: string | undefined = undefined;
+  if (passIndex != 0) {
+    password = testOptions.serverArguments[passIndex];
+  }
+
+  describe(`test with password - ${password}`, () => {
+    testUtils.testWithClientSentinel('client should be authenticated', async sentinel => {
+      await assert.doesNotReject(sentinel.set('x', 1));
+    }, testOptions);
+
+    testUtils.testWithClientSentinel('try to connect multiple times', async sentinel => {
+      await assert.rejects(sentinel.connect());
+    }, testOptions);
+
+
+    testUtils.testWithClientSentinel('should respect type mapping', async sentinel => {
+      const typeMapped = sentinel.withTypeMapping({
+        [RESP_TYPES.SIMPLE_STRING]: Buffer
+      });
+
+      const resp = await typeMapped.ping();
+      assert.deepEqual(resp, Buffer.from('PONG'));
+    }, testOptions);
+
+    testUtils.testWithClientSentinel('many readers', async sentinel => {
+      await sentinel.set("x", 1);
+      for (let i = 0; i < 10; i++) {
+        if (await sentinel.get("x") == "1") {
+          break;
+        }
+        await setTimeout(1000);
+      }
+
+      const promises: Array<Promise<string | null>> = [];
+      for (let i = 0; i < 500; i++) {
+        promises.push(sentinel.get("x"));
+      }
+
+      const resp = await Promise.all(promises);
+      assert.equal(resp.length, 500);
+      for (let i = 0; i < 500; i++) {
+        assert.equal(resp[i], "1", `failed on match at ${i}`);
+      }
+    }, testOptions);
+
+    testUtils.testWithClientSentinel('use', async sentinel => {
+      await sentinel.use(
+        async (client: any ) => {
+          await assert.doesNotReject(client.get('x'));
+        }
+      );
+    }, testOptions);
+
+    testUtils.testWithClientSentinel('watch does not carry over leases', async sentinel => {
+      assert.equal(await sentinel.use(client => client.watch("x")), 'OK')
+      assert.equal(await sentinel.use(client => client.set('x', 1)), 'OK');
+      assert.deepEqual(await sentinel.use(client => client.multi().get('x').exec()), ['1']);
+    }, testOptions);
+
+    testUtils.testWithClientSentinel('plain pubsub - channel', async sentinel => {
+      let pubSubResolve;
+      const pubSubPromise = new Promise((res) => {
+        pubSubResolve = res;
+      });
+
+      let tester = false;
+      await sentinel.subscribe('test', () => {
+        tester = true;
+        pubSubResolve(1);
+      })
+
+      await sentinel.publish('test', 'hello world');
+      await pubSubPromise;
+      assert.equal(tester, true);
+
+      // now unsubscribe
+      tester = false;
+      await sentinel.unsubscribe('test')
+      await sentinel.publish('test', 'hello world');
+      await setTimeout(1000);
+
+      assert.equal(tester, false);
+    }, testOptions);
+
+    testUtils.testWithClientSentinel('plain pubsub - pattern', async sentinel => {
+      let pubSubResolve;
+      const pubSubPromise = new Promise((res) => {
+        pubSubResolve = res;
+      });
+
+      let tester = false;
+      await sentinel.pSubscribe('test*', () => {
+        tester = true;
+        pubSubResolve(1);
+      })
+
+      await sentinel.publish('testy', 'hello world');
+      await pubSubPromise;
+      assert.equal(tester, true);
+
+      // now unsubscribe
+      tester = false;
+      await sentinel.pUnsubscribe('test*');
+      await sentinel.publish('testy', 'hello world');
+      await setTimeout(1000);
+
+      assert.equal(tester, false);
+  }, testOptions)
+  });
+});
+
+describe(`test with scripts`, () => {
+  testUtils.testWithClientSentinel('with script', async sentinel => {
+    const [, reply] = await Promise.all([
+      sentinel.set('key', '2'),
+      sentinel.square('key')
+    ]);
+
+    assert.equal(reply, 4);
+  }, GLOBAL.SENTINEL.WITH_SCRIPT);
+
+  testUtils.testWithClientSentinel('with script multi', async sentinel => {
+    const reply = await sentinel.multi().set('key', 2).square('key').exec();
+    assert.deepEqual(reply, ['OK', 4]);
+  }, GLOBAL.SENTINEL.WITH_SCRIPT);
+
+  testUtils.testWithClientSentinel('use with script', async sentinel => {
+    const reply = await sentinel.use(
+      async (client: any) => {
+        assert.equal(await client.set('key', '2'), 'OK');
+        assert.equal(await client.get('key'), '2');
+        return client.square('key')
+      }
+    );
+  }, GLOBAL.SENTINEL.WITH_SCRIPT)
+});
+
+describe(`test with functions`, () => {
+  testUtils.testWithClientSentinel('with function', async sentinel => {
+    await sentinel.functionLoad(
+      MATH_FUNCTION.code,
+      { REPLACE: true }
+    );
+
+    await sentinel.set('key', '2');
+    const resp = await sentinel.math.square('key');
+
+    assert.equal(resp, 4);
+  }, GLOBAL.SENTINEL.WITH_FUNCTION);
+
+  testUtils.testWithClientSentinel('with function multi', async sentinel => {
+    await sentinel.functionLoad(
+      MATH_FUNCTION.code,
+      { REPLACE: true }
+    );
+
+    const reply = await sentinel.multi().set('key', 2).math.square('key').exec();
+    assert.deepEqual(reply, ['OK', 4]);
+  }, GLOBAL.SENTINEL.WITH_FUNCTION);
+
+  testUtils.testWithClientSentinel('use with function', async sentinel => {
+    await sentinel.functionLoad(
+      MATH_FUNCTION.code,
+      { REPLACE: true }
+    );
+
+    const reply = await sentinel.use(
+      async (client: any) => {
+        await client.set('key', '2');
+        return client.math.square('key');
+      }
+    );
+
+    assert.equal(reply, 4);
+  }, GLOBAL.SENTINEL.WITH_FUNCTION);
+});
+
+describe(`test with modules`, () => {
+  testUtils.testWithClientSentinel('with module', async sentinel => {
+    const resp = await sentinel.bf.add('key', 'item')
+    assert.equal(resp, true);
+  }, GLOBAL.SENTINEL.WITH_MODULE);
+
+  testUtils.testWithClientSentinel('with module multi', async sentinel => {
+    const resp = await sentinel.multi().bf.add('key', 'item').exec();
+    assert.deepEqual(resp, [true]);
+  }, GLOBAL.SENTINEL.WITH_MODULE);
+
+  testUtils.testWithClientSentinel('use with module', async sentinel => {
+    const reply = await sentinel.use(
+      async (client: any) => {
+        return client.bf.add('key', 'item');
+      }
+    );
+
+    assert.equal(reply, true);
+  }, GLOBAL.SENTINEL.WITH_MODULE);
+});
+
+describe(`test with replica pool size 1`, () => {
+  testUtils.testWithClientSentinel('client lease', async sentinel => {
+    sentinel.on("error", () => { });
+
+    const clientLease = await sentinel.acquire();
+    clientLease.set('x', 456);
+
+    let matched = false;
+    /* waits for replication */
+    for (let i = 0; i < 15; i++) {
+      try {
+        assert.equal(await sentinel.get("x"), '456');
+        matched = true;
+        break;
+      } catch (err) {
+        await setTimeout(1000);
+      }
+    }
+
+    clientLease.release();
+
+    assert.equal(matched, true);
+  }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1);
+
+  testUtils.testWithClientSentinel('block on pool', async sentinel => {
+    const promise = sentinel.use(
+      async client => {
+        await setTimeout(1000);
+        return await client.get("x");
+      }
+    )
+
+    await sentinel.set("x", 1);
+    assert.equal(await promise, null);
+  }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1);
+
+  testUtils.testWithClientSentinel('pipeline', async sentinel => {
+    const resp = await sentinel.multi().set('x', 1).get('x').execAsPipeline();
+    assert.deepEqual(resp, ['OK', '1']);
+  }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1);
+});
+
+describe(`test with masterPoolSize 2, reserve client true`, () => {
+  // TODO: flaky test, sometimes fails with `promise1 === null`
+  testUtils.testWithClientSentinel('reserve client, takes a client out of pool', async sentinel => {
+    const promise1 = sentinel.use(
+      async client => {
+        const val = await client.get("x");
+        await client.set("x", 2);
+        return val;
+      }
+    )
+
+    const promise2 = sentinel.use(
+      async client => {
+        return client.get("x");
+      }
+    )
+
+    await sentinel.set("x", 1);
+    assert.equal(await promise1, "1");
+    assert.equal(await promise2, "2");
+  }, Object.assign(GLOBAL.SENTINEL.WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2, {skipTest: true}));
+});
+
+describe(`test with masterPoolSize 2`, () => {
+  testUtils.testWithClientSentinel('multple clients', async sentinel => {
+    sentinel.on("error", () => { });
+
+    const promise = sentinel.use(
+      async client => {
+        await sentinel!.set("x", 1);
+        await client.get("x");
+      }
+    )
+
+    await assert.doesNotReject(promise);
+  }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
+
+  testUtils.testWithClientSentinel('use - watch - clean', async sentinel => {
+    let promise = sentinel.use(async (client) => {
+      await client.set("x", 1);
+      await client.watch("x");
+      return client.multi().get("x").exec();
+    });
+
+    assert.deepEqual(await promise, ['1']);
+  }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
+
+  testUtils.testWithClientSentinel('use - watch - dirty', async sentinel => {
+    let promise = sentinel.use(async (client) => {
+      await client.set('x', 1);
+      await client.watch('x');
+      await sentinel!.set('x', 2);
+      return client.multi().get('x').exec();
+    });
+
+    await assert.rejects(promise, new WatchError());
+  }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
+
+  testUtils.testWithClientSentinel('lease - watch - clean', async sentinel => {
+    const leasedClient = await sentinel.acquire();
+    await leasedClient.set('x', 1);
+    await leasedClient.watch('x');
+    assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1'])
+  }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
+
+  testUtils.testWithClientSentinel('lease - watch - dirty', async sentinel => {
+    const leasedClient = await sentinel.acquire();
+    await leasedClient.set('x', 1);
+    await leasedClient.watch('x');
+    await leasedClient.set('x', 2);
+
+    await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError());
+  }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
+});
+
+async function steadyState(frame: SentinelFramework) {
+  // wait a bit to ensure that sentinels are seeing eachother
+  await setTimeout(2000)
+  let checkedMaster = false;
+  let checkedReplicas = false;
+  while (!checkedMaster || !checkedReplicas) {
+    if (!checkedMaster) {
+      const master = await frame.sentinelMaster();
+      if (master?.flags === 'master') {
+        checkedMaster = true;
+      }
+    }
+    if (!checkedReplicas) {
+      const replicas = (await frame.sentinelReplicas());
+      checkedReplicas = true;
+      for (const replica of replicas!) {
+        checkedReplicas &&= (replica.flags === 'slave');
+      }
+    }
+  }
+  let nodeResolve, nodeReject;
+  const nodePromise = new Promise((res, rej) => {
+    nodeResolve = res;
+    nodeReject = rej;
+  })
+  const seenNodes = new Set<number>();
+  let sentinel: RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined;
+  const tracer = [];
+  try {
+    sentinel = frame.getSentinelClient({ replicaPoolSize: 1, scanInterval: 2000 }, false)
+      .on('topology-change', (event: RedisSentinelEvent) => {
+        if (event.type == "MASTER_CHANGE" || event.type == "REPLICA_ADD") {
+          seenNodes.add(event.node.port);
+          if (seenNodes.size == frame.getAllNodesPort().length) {
+            nodeResolve();
+          }
+        }
+      }).on('error', err => { });
+    sentinel.setTracer(tracer);
+    await sentinel.connect();
+    await nodePromise;
+
+    await sentinel.flushAll();
+  } finally {
+    if (sentinel !== undefined) {
+      sentinel.destroy();
+    }
+  }
+}
+
+describe('legacy tests', () => {
+  const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined };
+  const frame = new SentinelFramework(config);
+  let tracer = new Array<string>();
+  let stopMeasuringBlocking = false;
+  let longestDelta = 0;
+  let longestTestDelta = 0;
+  let last: number;
+
+
+  describe('Sentinel Client', function () {
+    let sentinel: RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined;
+
+    beforeEach(async function () {
+      this.timeout(15000);
+
+      last = Date.now();
+
+      function deltaMeasurer() {
+        const delta = Date.now() - last;
+        if (delta > longestDelta) {
+          longestDelta = delta;
+        }
+        if (delta > longestTestDelta) {
+          longestTestDelta = delta;
+        }
+        if (!stopMeasuringBlocking) {
+          last = Date.now();
+          setImmediate(deltaMeasurer);
+        }
+      }
+      setImmediate(deltaMeasurer);
+      await frame.spawnRedisSentinel();
+      await frame.getAllRunning();
+      await steadyState(frame);
+      longestTestDelta = 0;
+    })
+
+    afterEach(async function () {
+      this.timeout(60000);
+      // avoid errors in afterEach that end testing
+      if (sentinel !== undefined) {
+        sentinel.on('error', () => { });
+      }
+
+      if (this!.currentTest!.state === 'failed') {
+        console.log(`longest event loop blocked delta: ${longestDelta}`);
+        console.log(`longest event loop blocked in failing test: ${longestTestDelta}`);
+        console.log("trace:");
+        for (const line of tracer) {
+          console.log(line);
+        }
+        console.log(`sentinel object state:`)
+        console.log(`master: ${JSON.stringify(sentinel?.getMasterNode())}`)
+        console.log(`replicas: ${JSON.stringify(sentinel?.getReplicaNodes().entries)}`)
+        const results = await Promise.all([
+          frame.sentinelSentinels(),
+          frame.sentinelMaster(),
+          frame.sentinelReplicas()
+        ])
+
+        console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`);
+        console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`);
+        console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`);
+        const { stdout, stderr } = await execAsync("docker ps -a");
+        console.log(`docker stdout:\n${stdout}`);
+        const ids = frame.getAllDockerIds();
+        console.log("docker logs");
+        for (const [id, port] of ids) {
+          console.log(`${id}/${port}\n`);
+          const { stdout, stderr } = await execAsync(`docker logs ${id}`, {maxBuffer: 8192 * 8192 * 4});
+          console.log(stdout);
+        }
+      }
+      tracer.length = 0;
+
+      if (sentinel !== undefined) {
+        await sentinel.destroy();
+        sentinel = undefined;
+      }
+
+      stopMeasuringBlocking = true;
+
+      await frame.cleanup();
+    })
+
+    it('use', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient({ replicaPoolSize: 1 });
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+
+      await sentinel.use(
+        async (client: RedisSentinelClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>, ) => {
+          const masterNode = sentinel!.getMasterNode();
+          await frame.stopNode(masterNode!.port.toString());
+          await assert.doesNotReject(client.get('x'));
+        }
+      );
+    });
+
+    // stops master to force sentinel to update
+    it('stop master', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient();
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+
+      tracer.push(`connected`);
+
+      let masterChangeResolve;
+      const masterChangePromise = new Promise((res) => {
+        masterChangeResolve = res;
+      })
+
+      const masterNode = await sentinel.getMasterNode();
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) {
+          tracer.push(`got expected master change event`);
+          masterChangeResolve(event.node);
+        }
+      });
+
+      tracer.push(`stopping master node`);
+      await frame.stopNode(masterNode!.port.toString());
+      tracer.push(`stopped master node`);
+
+      tracer.push(`waiting on master change promise`);
+      const newMaster = await masterChangePromise as RedisNode;
+      tracer.push(`got new master node of ${newMaster.port}`);
+      assert.notEqual(masterNode!.port, newMaster.port);
+    });
+
+    // if master changes, client should make sure user knows watches are invalid
+    it('watch across master change', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient({ masterPoolSize: 2 });
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+
+      tracer.push("connected");
+
+      const client = await sentinel.acquire();
+      tracer.push("acquired lease");
+
+      await client.set("x", 1);
+      await client.watch("x");
+
+      tracer.push("did a watch on lease");
+
+      let resolve;
+      const promise = new Promise((res) => {
+        resolve = res;
+      })
+
+      const masterNode = sentinel.getMasterNode();
+      tracer.push(`got masterPort as ${masterNode!.port}`);
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) {
+          tracer.push("resolving promise");
+          resolve(event.node);
+        }
+      });
+
+      tracer.push("stopping master node");
+      await frame.stopNode(masterNode!.port.toString());
+      tracer.push("stopped master node and waiting on promise");
+
+      const newMaster = await promise as RedisNode;
+      tracer.push(`promise returned, newMaster = ${JSON.stringify(newMaster)}`);
+      assert.notEqual(masterNode!.port, newMaster.port);
+      tracer.push(`newMaster does not equal old master`);
+
+      tracer.push(`waiting to assert that a multi/exec now fails`);
+      await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction"));
+      tracer.push(`asserted that a multi/exec now fails`);
+    });
+
+    // same as above, but set a watch before and after master change, shouldn't change the fact that watches are invalid
+    it('watch before and after master change', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient({ masterPoolSize: 2 });
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+      tracer.push("connected");
+
+      const client = await sentinel.acquire();
+      tracer.push("got leased client");
+      await client.set("x", 1);
+      await client.watch("x");
+
+      tracer.push("set and watched x");
+
+      let resolve;
+      const promise = new Promise((res) => {
+        resolve = res;
+      })
+
+      const masterNode = sentinel.getMasterNode();
+      tracer.push(`initial masterPort = ${masterNode!.port} `);
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) {
+          tracer.push("got a master change event that is not the same as before");
+          resolve(event.node);
+        }
+      });
+
+      tracer.push("stopping master");
+      await frame.stopNode(masterNode!.port.toString());
+      tracer.push("stopped master");
+
+      tracer.push("waiting on master change promise");
+      const newMaster = await promise as RedisNode;
+      tracer.push(`got master change port as ${newMaster.port}`);
+      assert.notEqual(masterNode!.port, newMaster.port);
+
+      tracer.push("watching again, shouldn't matter");
+      await client.watch("y");
+
+      tracer.push("expecting multi to be rejected");
+      await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction"));
+      tracer.push("multi was rejected");
+    });
+
+
+    // pubsub continues to work, even with a master change
+    it('pubsub - channel - with master change', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient();
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+      tracer.push(`connected`);
+
+      let pubSubResolve;
+      const pubSubPromise = new Promise((res) => {
+        pubSubResolve = res;
+      })
+
+      let tester = false;
+      await sentinel.subscribe('test', () => {
+        tracer.push(`got pubsub message`);
+        tester = true;
+        pubSubResolve(1);
+      })
+
+      let masterChangeResolve;
+      const masterChangePromise = new Promise((res) => {
+        masterChangeResolve = res;
+      })
+
+      const masterNode = sentinel.getMasterNode();
+      tracer.push(`got masterPort as ${masterNode!.port}`);
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) {
+          tracer.push("got a master change event that is not the same as before");
+          masterChangeResolve(event.node);
+        }
+      });
+
+      tracer.push("stopping master");
+      await frame.stopNode(masterNode!.port.toString());
+      tracer.push("stopped master and waiting on change promise");
+
+      const newMaster = await masterChangePromise as RedisNode;
+      tracer.push(`got master change port as ${newMaster.port}`);
+      assert.notEqual(masterNode!.port, newMaster.port);
+
+      tracer.push(`publishing pubsub message`);
+      await sentinel.publish('test', 'hello world');
+      tracer.push(`published pubsub message and waiting pn pubsub promise`);
+      await pubSubPromise;
+      tracer.push(`got pubsub promise`);
+
+      assert.equal(tester, true);
+
+      // now unsubscribe
+      tester = false
+      await sentinel.unsubscribe('test')
+      await sentinel.publish('test', 'hello world');
+      await setTimeout(1000);
+
+      assert.equal(tester, false);
+    });
+
+    it('pubsub - pattern - with master change', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient();
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+      tracer.push(`connected`);
+
+      let pubSubResolve;
+      const pubSubPromise = new Promise((res) => {
+        pubSubResolve = res;
+      })
+
+      let tester = false;
+      await sentinel.pSubscribe('test*', () => {
+        tracer.push(`got pubsub message`);
+        tester = true;
+        pubSubResolve(1);
+      })
+
+      let masterChangeResolve;
+      const masterChangePromise = new Promise((res) => {
+        masterChangeResolve = res;
+      })
+
+      const masterNode = sentinel.getMasterNode();
+      tracer.push(`got masterPort as ${masterNode!.port}`);
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) {
+          tracer.push("got a master change event that is not the same as before");
+          masterChangeResolve(event.node);
+        }
+      });
+
+      tracer.push("stopping master");
+      await frame.stopNode(masterNode!.port.toString());
+      tracer.push("stopped master and waiting on master change promise");
+
+      const newMaster = await masterChangePromise as RedisNode;
+      tracer.push(`got master change port as ${newMaster.port}`);
+      assert.notEqual(masterNode!.port, newMaster.port);
+
+      tracer.push(`publishing pubsub message`);
+      await sentinel.publish('testy', 'hello world');
+      tracer.push(`published pubsub message and waiting on pubsub promise`);
+      await pubSubPromise;
+      tracer.push(`got pubsub promise`);
+      assert.equal(tester, true);
+
+      // now unsubscribe
+      tester = false
+      await sentinel.pUnsubscribe('test*');
+      await sentinel.publish('testy', 'hello world');
+      await setTimeout(1000);
+
+      assert.equal(tester, false);
+    });
+
+    // if we stop a node, the comand should "retry" until we reconfigure topology and execute on new topology
+    it('command immeaditely after stopping master', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient();
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+
+      tracer.push("connected");
+
+      let masterChangeResolve;
+      const masterChangePromise = new Promise((res) => {
+        masterChangeResolve = res;
+      })
+
+      const masterNode = sentinel.getMasterNode();
+      tracer.push(`original master port = ${masterNode!.port}`);
+
+      let changeCount = 0;
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) {
+          changeCount++;
+          tracer.push(`got topology-change event we expected`);
+          masterChangeResolve(event.node);
+        }
+      });
+
+      tracer.push(`stopping masterNode`);
+      await frame.stopNode(masterNode!.port.toString());
+      tracer.push(`stopped masterNode`);
+      assert.equal(await sentinel.set('x', 123), 'OK');
+      tracer.push(`did the set operation`);
+      const presumamblyNewMaster = sentinel.getMasterNode();
+      tracer.push(`new master node seems to be ${presumamblyNewMaster?.port} and waiting on master change promise`);
+
+      const newMaster = await masterChangePromise as RedisNode;
+      tracer.push(`got new masternode event saying master is at ${newMaster.port}`);
+      assert.notEqual(masterNode!.port, newMaster.port);
+
+      tracer.push(`doing the get`);
+      const val = await sentinel.get('x');
+      tracer.push(`did the get and got ${val}`);
+      const newestMaster = sentinel.getMasterNode()
+      tracer.push(`after get, we see master as ${newestMaster?.port}`);
+
+      switch (changeCount) {
+        case 1:
+          // if we only changed masters once, we should have the proper value
+          assert.equal(val, '123');
+          break;
+        case 2:
+          // we changed masters twice quickly, so probably didn't replicate
+          // therefore, this is soewhat flakey, but the above is the common case
+          assert(val == '123' || val == null);
+          break;
+        default:
+          assert(false, "unexpected case");
+      }
+    });
+
+    it('shutdown sentinel node', async function () {
+      this.timeout(60000);
+      sentinel = frame.getSentinelClient();
+      sentinel.setTracer(tracer);
+      sentinel.on("error", () => { });
+      await sentinel.connect();
+      tracer.push("connected");
+
+      let sentinelChangeResolve;
+      const sentinelChangePromise = new Promise((res) => {
+        sentinelChangeResolve = res;
+      })
+
+      const sentinelNode = sentinel.getSentinelNode();
+      tracer.push(`sentinelNode = ${sentinelNode?.port}`)
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "SENTINEL_CHANGE") {
+          tracer.push("got sentinel change event");
+          sentinelChangeResolve(event.node);
+        }
+      });
+
+      tracer.push("Stopping sentinel node");
+      await frame.stopSentinel(sentinelNode!.port.toString());
+      tracer.push("Stopped sentinel node and waiting on sentinel change promise");
+      const newSentinel = await sentinelChangePromise as RedisNode;
+      tracer.push("got sentinel change promise");
+      assert.notEqual(sentinelNode!.port, newSentinel.port);
+    });
+
+    it('timer works, and updates sentinel list', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient({ scanInterval: 1000 });
+      sentinel.setTracer(tracer);
+      await sentinel.connect();
+      tracer.push("connected");
+
+      let sentinelChangeResolve;
+      const sentinelChangePromise = new Promise((res) => {
+        sentinelChangeResolve = res;
+      })
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "SENTINE_LIST_CHANGE" && event.size == 4) {
+          tracer.push(`got sentinel list change event with right size`);
+          sentinelChangeResolve(event.size);
+        }
+      });
+
+      tracer.push(`adding sentinel`);
+      await frame.addSentinel();
+      tracer.push(`added sentinel and waiting on sentinel change promise`);
+      const newSentinelSize = await sentinelChangePromise as number;
+
+      assert.equal(newSentinelSize, 4);
+    });
+
+    it('stop replica, bring back replica', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient({ replicaPoolSize: 1 });
+      sentinel.setTracer(tracer);
+      sentinel.on('error', err => { });
+      await sentinel.connect();
+      tracer.push("connected");
+
+      let sentinelRemoveResolve;
+      const sentinelRemovePromise = new Promise((res) => {
+        sentinelRemoveResolve = res;
+      })
+
+      const replicaPort = await frame.getRandonNonMasterNode();
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "REPLICA_REMOVE") {
+          if (event.node.port.toString() == replicaPort) {
+            tracer.push("got expected replica removed event");
+            sentinelRemoveResolve(event.node);
+          } else {
+            tracer.push(`got replica removed event for a different node: ${event.node.port}`);
+          }
+        }
+      });
+
+      tracer.push(`replicaPort = ${replicaPort} and stopping it`);
+      await frame.stopNode(replicaPort);
+      tracer.push("stopped replica and waiting on sentinel removed promise");
+      const stoppedNode = await sentinelRemovePromise as RedisNode;
+      tracer.push("got removed promise");
+      assert.equal(stoppedNode.port, Number(replicaPort));
+
+      let sentinelRestartedResolve;
+      const sentinelRestartedPromise = new Promise((res) => {
+        sentinelRestartedResolve = res;
+      })
+
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "REPLICA_ADD") {
+          tracer.push("got replica added event");
+          sentinelRestartedResolve(event.node);
+        }
+      });
+
+      tracer.push("restarting replica");
+      await frame.restartNode(replicaPort);
+      tracer.push("restarted replica and waiting on restart promise");
+      const restartedNode = await sentinelRestartedPromise as RedisNode;
+      tracer.push("got restarted promise");
+      assert.equal(restartedNode.port, Number(replicaPort));
+    })
+
+    it('add a node / new replica', async function () {
+      this.timeout(60000);
+
+      sentinel = frame.getSentinelClient({ scanInterval: 2000, replicaPoolSize: 1 });
+      sentinel.setTracer(tracer);
+      // need to handle errors, as the spawning a new docker node can cause existing connections to time out
+      sentinel.on('error', err => { });
+      await sentinel.connect();
+      tracer.push("connected");
+
+      let nodeAddedResolve: (value: RedisNode) => void;
+      const nodeAddedPromise = new Promise((res) => {
+        nodeAddedResolve = res as (value: RedisNode) => void;
+      });
+
+      const portSet = new Set<number>();
+      for (const port of frame.getAllNodesPort()) {
+        portSet.add(port);
+      }
+
+      // "on" and not "once" as due to connection timeouts, can happen multiple times, and want right one
+      sentinel.on('topology-change', (event: RedisSentinelEvent) => {
+        tracer.push(`got topology-change event: ${JSON.stringify(event)}`);
+        if (event.type === "REPLICA_ADD") {
+          if (!portSet.has(event.node.port)) {
+            tracer.push("got expected replica added event");
+            nodeAddedResolve(event.node);
+          }
+        }
+      });
+
+      tracer.push("adding node");
+      await frame.addNode();
+      tracer.push("added node and waiting on added promise");
+      await nodeAddedPromise;
+    })
+
+    it('with client side caching', async function() {
+      this.timeout(30000);
+      const csc = new BasicPooledClientSideCache();
+
+      sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3 as const}, RESP: 3 as const, clientSideCache: csc, masterPoolSize: 5});
+      await sentinel.connect();
+
+      await sentinel.set('x', 1);
+      await sentinel.get('x');
+      await sentinel.get('x');
+      await sentinel.get('x');
+      await sentinel.get('x');
+
+      assert.equal(1, csc.stats().missCount);
+      assert.equal(3, csc.stats().hitCount);
+
+      const invalidatePromise = once(csc, 'invalidate');
+      await sentinel.set('x', 2);
+      await invalidatePromise;
+      await sentinel.get('x');
+      await sentinel.get('x');
+      await sentinel.get('x');
+      await sentinel.get('x');
+
+      assert.equal(csc.stats().missCount, 2);
+      assert.equal(csc.stats().hitCount, 6);
+    })
+  });
+});
diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts
new file mode 100644
index 00000000000..b3f3bbf0b8d
--- /dev/null
+++ b/packages/client/lib/sentinel/index.ts
@@ -0,0 +1,1577 @@
+import { EventEmitter } from 'node:events';
+import { CommandArguments, RedisFunctions, RedisModules, RedisScripts, ReplyUnion, RespVersions, TypeMapping } from '../RESP/types';
+import RedisClient, { RedisClientOptions, RedisClientType } from '../client';
+import { CommandOptions } from '../client/commands-queue';
+import { attachConfig } from '../commander';
+import COMMANDS from '../commands';
+import { ClientErrorEvent, NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, ProxySentinelClient, RedisNode, RedisSentinelClientType, RedisSentinelEvent, RedisSentinelOptions, RedisSentinelType, SentinelCommander } from './types';
+import { clientSocketToNode, createCommand, createFunctionCommand, createModuleCommand, createNodeList, createScriptCommand, parseNode } from './utils';
+import { RedisMultiQueuedCommand } from '../multi-command';
+import RedisSentinelMultiCommand, { RedisSentinelMultiCommandType } from './multi-commands';
+import { PubSubListener } from '../client/pub-sub';
+import { PubSubProxy } from './pub-sub-proxy';
+import { setTimeout } from 'node:timers/promises';
+import RedisSentinelModule from './module'
+import { RedisVariadicArgument } from '../commands/generic-transformers';
+import { WaitQueue } from './wait-queue';
+import { TcpNetConnectOpts } from 'node:net';
+import { RedisTcpSocketOptions } from '../client/socket';
+import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../client/cache';
+
+interface ClientInfo {
+  id: number;
+}
+
+export class RedisSentinelClient<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> {
+  #clientInfo: ClientInfo | undefined;
+  #internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>;
+  readonly _self: RedisSentinelClient<M, F, S, RESP, TYPE_MAPPING>;
+
+  /**
+   * Indicates if the client connection is open
+   *
+   * @returns `true` if the client connection is open, `false` otherwise
+   */
+
+  get isOpen() {
+    return this._self.#internal.isOpen;
+  }
+
+  /**
+   * Indicates if the client connection is ready to accept commands
+   *
+   * @returns `true` if the client connection is ready, `false` otherwise
+   */
+  get isReady() {
+    return this._self.#internal.isReady;
+  }
+
+  /**
+   * Gets the command options configured for this client
+   *
+   * @returns The command options for this client or `undefined` if none were set
+   */
+  get commandOptions() {
+    return this._self.#commandOptions;
+  }
+
+  #commandOptions?: CommandOptions<TYPE_MAPPING>;
+
+  constructor(
+    internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>,
+    clientInfo: ClientInfo,
+    commandOptions?: CommandOptions<TYPE_MAPPING>
+  ) {
+    this._self = this;
+    this.#internal = internal;
+    this.#clientInfo = clientInfo;
+    this.#commandOptions = commandOptions;
+  }
+
+  static factory<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(config?: SentinelCommander<M, F, S, RESP, TYPE_MAPPING>) {
+    const SentinelClient = attachConfig({
+      BaseClass: RedisSentinelClient,
+      commands: COMMANDS,
+      createCommand: createCommand<ProxySentinelClient>,
+      createModuleCommand: createModuleCommand<NamespaceProxySentinelClient>,
+      createFunctionCommand: createFunctionCommand<NamespaceProxySentinelClient>,
+      createScriptCommand: createScriptCommand<ProxySentinelClient>,
+      config
+    });
+
+    SentinelClient.prototype.Multi = RedisSentinelMultiCommand.extend(config);
+
+    return (
+      internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>,
+      clientInfo: ClientInfo,
+      commandOptions?: CommandOptions<TYPE_MAPPING>
+    ) => {
+      // returning a "proxy" to prevent the namespaces._self to leak between "proxies"
+      return Object.create(new SentinelClient(internal, clientInfo, commandOptions)) as RedisSentinelClientType<M, F, S, RESP, TYPE_MAPPING>;
+    };
+  }
+
+  static create<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(
+    options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>,
+    internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>,
+    clientInfo: ClientInfo,
+    commandOptions?: CommandOptions<TYPE_MAPPING>,
+  ) {
+    return RedisSentinelClient.factory(options)(internal, clientInfo, commandOptions);
+  }
+
+  withCommandOptions<
+    OPTIONS extends CommandOptions<TYPE_MAPPING>,
+    TYPE_MAPPING extends TypeMapping
+  >(options: OPTIONS) {
+    const proxy = Object.create(this);
+    proxy._commandOptions = options;
+    return proxy as RedisSentinelClientType<
+      M,
+      F,
+      S,
+      RESP,
+      TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
+    >;
+  }
+
+  private _commandOptionsProxy<
+    K extends keyof CommandOptions,
+    V extends CommandOptions[K]
+  >(
+    key: K,
+    value: V
+  ) {
+    const proxy = Object.create(this);
+    proxy._commandOptions = Object.create(this._self.#commandOptions ?? null);
+    proxy._commandOptions[key] = value;
+    return proxy as RedisSentinelClientType<
+      M,
+      F,
+      S,
+      RESP,
+      K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
+    >;
+  }
+
+  /**
+   * Override the `typeMapping` command option
+   */
+  withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
+    return this._commandOptionsProxy('typeMapping', typeMapping);
+  }
+
+  async _execute<T>(
+    isReadonly: boolean | undefined,
+    fn: (client: RedisClient<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>) => Promise<T>
+  ): Promise<T> {
+    if (this._self.#clientInfo === undefined) {
+      throw new Error("Attempted execution on released RedisSentinelClient lease");
+    }
+
+    return await this._self.#internal.execute(fn, this._self.#clientInfo);
+  }
+
+  async sendCommand<T = ReplyUnion>(
+    isReadonly: boolean | undefined,
+    args: CommandArguments,
+    options?: CommandOptions,
+  ): Promise<T> {
+    return this._execute(
+      isReadonly,
+      client => client.sendCommand(args, options)
+    );
+  }
+
+  /**
+   * @internal
+   */
+  async _executePipeline(
+    isReadonly: boolean | undefined,
+    commands: Array<RedisMultiQueuedCommand>
+  ) {
+    return this._execute(
+      isReadonly,
+      client => client._executePipeline(commands)
+    );
+  }
+
+  /**f
+    * @internal
+    */
+  async _executeMulti(
+    isReadonly: boolean | undefined,
+    commands: Array<RedisMultiQueuedCommand>
+  ) {
+    return this._execute(
+      isReadonly,
+      client => client._executeMulti(commands)
+    );
+  }
+
+  MULTI(): RedisSentinelMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING> {
+    return new (this as any).Multi(this);
+  }
+
+  multi = this.MULTI;
+
+  WATCH(key: RedisVariadicArgument) {
+    if (this._self.#clientInfo === undefined) {
+      throw new Error("Attempted execution on released RedisSentinelClient lease");
+    }
+
+    return this._execute(
+      false,
+      client => client.watch(key)
+    )
+  }
+
+  watch = this.WATCH;
+
+  UNWATCH() {
+    if (this._self.#clientInfo === undefined) {
+      throw new Error('Attempted execution on released RedisSentinelClient lease');
+    }
+
+    return this._execute(
+      false,
+      client => client.unwatch()
+    )
+  }
+
+  unwatch = this.UNWATCH;
+
+  /**
+   * Releases the client lease back to the pool
+   *
+   * After calling this method, the client instance should no longer be used as it
+   * will be returned to the client pool and may be given to other operations.
+   *
+   * @returns A promise that resolves when the client is ready to be reused, or undefined
+   *          if the client was immediately ready
+   * @throws Error if the lease has already been released
+   */
+  release() {
+    if (this._self.#clientInfo === undefined) {
+      throw new Error('RedisSentinelClient lease already released');
+    }
+
+    const result = this._self.#internal.releaseClientLease(this._self.#clientInfo);
+    this._self.#clientInfo = undefined;
+    return result;
+  }
+}
+
+export default class RedisSentinel<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> extends EventEmitter {
+  readonly _self: RedisSentinel<M, F, S, RESP, TYPE_MAPPING>;
+
+  #internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>;
+  #options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>;
+
+  /**
+   * Indicates if the sentinel connection is open
+   *
+   * @returns `true` if the sentinel connection is open, `false` otherwise
+   */
+  get isOpen() {
+    return this._self.#internal.isOpen;
+  }
+
+  /**
+   * Indicates if the sentinel connection is ready to accept commands
+   *
+   * @returns `true` if the sentinel connection is ready, `false` otherwise
+   */
+  get isReady() {
+    return this._self.#internal.isReady;
+  }
+
+  get commandOptions() {
+    return this._self.#commandOptions;
+  }
+
+  #commandOptions?: CommandOptions<TYPE_MAPPING>;
+
+  #trace: (msg: string) => unknown = () => { };
+
+  #reservedClientInfo?: ClientInfo;
+  #masterClientCount = 0;
+  #masterClientInfo?: ClientInfo;
+
+  get clientSideCache() {
+    return this._self.#internal.clientSideCache;
+  }
+
+  constructor(options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    super();
+
+    this._self = this;
+
+    this.#options = options;
+
+    if (options.commandOptions) {
+      this.#commandOptions = options.commandOptions;
+    }
+
+    this.#internal = new RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>(options);
+    this.#internal.on('error', err => this.emit('error', err));
+
+    /* pass through underling events */
+    /* TODO: perhaps make this a struct and one vent, instead of multiple events */
+    this.#internal.on('topology-change', (event: RedisSentinelEvent) => {
+      if (!this.emit('topology-change', event)) {
+        this._self.#trace(`RedisSentinel: re-emit for topology-change for ${event.type} event returned false`);
+      }
+    });
+  }
+
+  static factory<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(config?: SentinelCommander<M, F, S, RESP, TYPE_MAPPING>) {
+    const Sentinel = attachConfig({
+      BaseClass: RedisSentinel,
+      commands: COMMANDS,
+      createCommand: createCommand<ProxySentinel>,
+      createModuleCommand: createModuleCommand<NamespaceProxySentinel>,
+      createFunctionCommand: createFunctionCommand<NamespaceProxySentinel>,
+      createScriptCommand: createScriptCommand<ProxySentinel>,
+      config
+    });
+
+    Sentinel.prototype.Multi = RedisSentinelMultiCommand.extend(config);
+
+    return (options: Omit<RedisSentinelOptions, keyof Exclude<typeof config, undefined>>) => {
+      // returning a "proxy" to prevent the namespaces.self to leak between "proxies"
+      return Object.create(new Sentinel(options)) as RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>;
+    };
+  }
+
+  static create<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    return RedisSentinel.factory(options)(options);
+  }
+
+  withCommandOptions<
+    OPTIONS extends CommandOptions<TYPE_MAPPING>,
+    TYPE_MAPPING extends TypeMapping,
+  >(options: OPTIONS) {
+    const proxy = Object.create(this);
+    proxy._commandOptions = options;
+    return proxy as RedisSentinelType<
+      M,
+      F,
+      S,
+      RESP,
+      TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
+    >;
+  }
+
+  private _commandOptionsProxy<
+    K extends keyof CommandOptions,
+    V extends CommandOptions[K]
+  >(
+    key: K,
+    value: V
+  ) {
+    const proxy = Object.create(this);
+    // Create new commandOptions object with the inherited properties
+    proxy._self.#commandOptions = {
+      ...(this._self.#commandOptions || {}),
+      [key]: value
+    };
+    return proxy as RedisSentinelType<
+      M,
+      F,
+      S,
+      RESP,
+      K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
+    >;
+  }
+
+  /**
+   * Override the `typeMapping` command option
+   */
+  withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
+    return this._commandOptionsProxy('typeMapping', typeMapping);
+  }
+
+  async connect() {
+    await this._self.#internal.connect();
+
+    if (this._self.#options.reserveClient) {
+      this._self.#reservedClientInfo = await this._self.#internal.getClientLease();
+    }
+
+    return this as unknown as RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>;
+  }
+
+  async _execute<T>(
+    isReadonly: boolean | undefined,
+    fn: (client: RedisClient<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>) => Promise<T>
+  ): Promise<T> {
+    let clientInfo: ClientInfo | undefined;
+    if (!isReadonly || !this._self.#internal.useReplicas) {
+      if (this._self.#reservedClientInfo) {
+        clientInfo = this._self.#reservedClientInfo;
+      } else {
+        this._self.#masterClientInfo ??= await this._self.#internal.getClientLease();
+        clientInfo = this._self.#masterClientInfo;
+        this._self.#masterClientCount++;
+      }
+    }
+
+    try {
+      return await this._self.#internal.execute(fn, clientInfo);
+    } finally {
+      if (
+        clientInfo !== undefined &&
+        clientInfo === this._self.#masterClientInfo &&
+        --this._self.#masterClientCount === 0
+      ) {
+        const promise = this._self.#internal.releaseClientLease(clientInfo);
+        this._self.#masterClientInfo = undefined;
+        if (promise) await promise;
+      }
+    }
+  }
+
+  async use<T>(fn: (sentinelClient: RedisSentinelClientType<M, F, S, RESP, TYPE_MAPPING>) => Promise<T>) {
+    const clientInfo = await this._self.#internal.getClientLease();
+
+    try {
+      return await fn(
+        RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions)
+      );
+    } finally {
+      const promise = this._self.#internal.releaseClientLease(clientInfo);
+      if (promise) await promise;
+    }
+  }
+
+  async sendCommand<T = ReplyUnion>(
+    isReadonly: boolean | undefined,
+    args: CommandArguments,
+    options?: CommandOptions,
+  ): Promise<T> {
+    return this._execute(
+      isReadonly,
+      client => client.sendCommand(args, options)
+    );
+  }
+
+  /**
+   * @internal
+   */
+  async _executePipeline(
+    isReadonly: boolean | undefined,
+    commands: Array<RedisMultiQueuedCommand>
+  ) {
+    return this._execute(
+      isReadonly,
+      client => client._executePipeline(commands)
+    );
+  }
+
+  /**f
+    * @internal
+    */
+  async _executeMulti(
+    isReadonly: boolean | undefined,
+    commands: Array<RedisMultiQueuedCommand>
+  ) {
+    return this._execute(
+      isReadonly,
+      client => client._executeMulti(commands)
+    );
+  }
+
+  MULTI(): RedisSentinelMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING> {
+    return new (this as any).Multi(this);
+  }
+
+  multi = this.MULTI;
+
+  async close() {
+    return this._self.#internal.close();
+  }
+
+  destroy() {
+    return this._self.#internal.destroy();
+  }
+
+  async SUBSCRIBE<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this._self.#internal.subscribe(channels, listener, bufferMode);
+  }
+
+  subscribe = this.SUBSCRIBE;
+
+  async UNSUBSCRIBE<T extends boolean = false>(
+    channels?: string | Array<string>,
+    listener?: PubSubListener<boolean>,
+    bufferMode?: T
+  ) {
+    return this._self.#internal.unsubscribe(channels, listener, bufferMode);
+  }
+
+  unsubscribe = this.UNSUBSCRIBE;
+
+  async PSUBSCRIBE<T extends boolean = false>(
+    patterns: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this._self.#internal.pSubscribe(patterns, listener, bufferMode);
+  }
+
+  pSubscribe = this.PSUBSCRIBE;
+
+  async PUNSUBSCRIBE<T extends boolean = false>(
+    patterns?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this._self.#internal.pUnsubscribe(patterns, listener, bufferMode);
+  }
+
+  pUnsubscribe = this.PUNSUBSCRIBE;
+
+  /**
+   * Acquires a master client lease for exclusive operations
+   *
+   * Used when multiple commands need to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`).
+   * The returned client must be released after use with the `release()` method.
+   *
+   * @returns A promise that resolves to a Redis client connected to the master node
+   * @example
+   * ```javascript
+   * const clientLease = await sentinel.acquire();
+   *
+   * try {
+   *   await clientLease.watch('key');
+   *   const resp = await clientLease.multi()
+   *     .get('key')
+   *     .exec();
+   * } finally {
+   *   clientLease.release();
+   * }
+   * ```
+   */
+  async acquire(): Promise<RedisSentinelClientType<M, F, S, RESP, TYPE_MAPPING>> {
+    const clientInfo = await this._self.#internal.getClientLease();
+    return RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions);
+  }
+
+  getSentinelNode(): RedisNode | undefined {
+    return this._self.#internal.getSentinelNode();
+  }
+
+  getMasterNode(): RedisNode | undefined {
+    return this._self.#internal.getMasterNode();
+  }
+
+  getReplicaNodes(): Map<RedisNode, number> {
+    return this._self.#internal.getReplicaNodes();
+  }
+
+  setTracer(tracer?: Array<string>) {
+    if (tracer) {
+      this._self.#trace = (msg: string) => { tracer.push(msg) };
+    } else {
+      this._self.#trace = () => { };
+    }
+
+    this._self.#internal.setTracer(tracer);
+  }
+}
+
+class RedisSentinelInternal<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> extends EventEmitter {
+  #isOpen = false;
+
+  get isOpen() {
+    return this.#isOpen;
+  }
+
+  #isReady = false;
+
+  get isReady() {
+    return this.#isReady;
+  }
+
+  readonly #name: string;
+  readonly #nodeClientOptions: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING, RedisTcpSocketOptions>;
+  readonly #sentinelClientOptions: RedisClientOptions<typeof RedisSentinelModule, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisTcpSocketOptions>;
+  readonly #scanInterval: number;
+  readonly #passthroughClientErrorEvents: boolean;
+  readonly #RESP?: RespVersions;
+
+  #anotherReset = false;
+
+  #configEpoch: number = 0;
+
+  #sentinelRootNodes: Array<RedisNode>;
+  #sentinelClient?: RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
+
+  #masterClients: Array<RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>> = [];
+  #masterClientQueue: WaitQueue<number>;
+  readonly #masterPoolSize: number;
+
+  #replicaClients: Array<RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>> = [];
+  #replicaClientsIdx: number = 0;
+  readonly #replicaPoolSize: number;
+
+  get useReplicas() {
+    return this.#replicaPoolSize > 0;
+  }
+
+  #connectPromise?: Promise<void>;
+  #maxCommandRediscovers: number;
+  readonly #pubSubProxy: PubSubProxy;
+
+  #scanTimer?: NodeJS.Timeout
+
+  #destroy = false;
+
+  #trace: (msg: string) => unknown = () => { };
+
+  #clientSideCache?: PooledClientSideCacheProvider;
+  get clientSideCache() {
+    return this.#clientSideCache;
+  }
+
+  #validateOptions(options?: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    if (options?.clientSideCache && options?.RESP !== 3) {
+      throw new Error('Client Side Caching is only supported with RESP3');
+    }
+  }
+
+  constructor(options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>) {
+    super();
+
+    this.#validateOptions(options);
+
+    this.#name = options.name;
+
+    this.#RESP = options.RESP;
+    this.#sentinelRootNodes = Array.from(options.sentinelRootNodes);
+    this.#maxCommandRediscovers = options.maxCommandRediscovers ?? 16;
+    this.#masterPoolSize = options.masterPoolSize ?? 1;
+    this.#replicaPoolSize = options.replicaPoolSize ?? 0;
+    this.#scanInterval = options.scanInterval ?? 0;
+    this.#passthroughClientErrorEvents = options.passthroughClientErrorEvents ?? false;
+
+    this.#nodeClientOptions = (options.nodeClientOptions ? {...options.nodeClientOptions} : {}) as RedisClientOptions<M, F, S, RESP, TYPE_MAPPING, RedisTcpSocketOptions>;
+    if (this.#nodeClientOptions.url !== undefined) {
+      throw new Error("invalid nodeClientOptions for Sentinel");
+    }
+
+    if (options.clientSideCache) {
+      if (options.clientSideCache instanceof PooledClientSideCacheProvider) {
+        this.#clientSideCache = this.#nodeClientOptions.clientSideCache = options.clientSideCache;
+      } else {
+        const cscConfig = options.clientSideCache;
+        this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig);
+//        this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig);
+      }
+    }
+
+    this.#sentinelClientOptions = options.sentinelClientOptions ? Object.assign({} as RedisClientOptions<typeof RedisSentinelModule, F, S, RESP, TYPE_MAPPING, RedisTcpSocketOptions>, options.sentinelClientOptions) : {};
+    this.#sentinelClientOptions.modules = RedisSentinelModule;
+
+    if (this.#sentinelClientOptions.url !== undefined) {
+      throw new Error("invalid sentinelClientOptions for Sentinel");
+    }
+
+    this.#masterClientQueue = new WaitQueue();
+    for (let i = 0; i < this.#masterPoolSize; i++) {
+      this.#masterClientQueue.push(i);
+    }
+
+    /* persistent object for life of sentinel object */
+    this.#pubSubProxy = new PubSubProxy(
+      this.#nodeClientOptions,
+      err => this.emit('error', err)
+    );
+  }
+
+  #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: undefined | false) {
+    return RedisClient.create({
+      //first take the globally set RESP
+      RESP: this.#RESP,
+      //then take the client options, which can in theory overwrite it
+      ...clientOptions,
+      socket: {
+        ...clientOptions.socket,
+        host: node.host,
+        port: node.port,
+        reconnectStrategy
+      }
+    });
+  }
+
+  /**
+   * Gets a client lease from the master client pool
+   *
+   * @returns A client info object or a promise that resolves to a client info object
+   *          when a client becomes available
+   */
+  getClientLease(): ClientInfo | Promise<ClientInfo> {
+    const id = this.#masterClientQueue.shift();
+    if (id !== undefined) {
+      return { id };
+    }
+
+    return this.#masterClientQueue.wait().then(id => ({ id }));
+  }
+
+  /**
+   * Releases a client lease back to the pool
+   *
+   * If the client was used for a transaction that might have left it in a dirty state,
+   * it will be reset before being returned to the pool.
+   *
+   * @param clientInfo The client info object representing the client to release
+   * @returns A promise that resolves when the client is ready to be reused, or undefined
+   *          if the client was immediately ready or no longer exists
+   */
+  releaseClientLease(clientInfo: ClientInfo) {
+    const client = this.#masterClients[clientInfo.id];
+    // client can be undefined if releasing in middle of a reconfigure
+    if (client !== undefined) {
+      const dirtyPromise = client.resetIfDirty();
+      if (dirtyPromise) {
+        return dirtyPromise
+          .then(() => this.#masterClientQueue.push(clientInfo.id));
+      }
+    }
+
+    this.#masterClientQueue.push(clientInfo.id);
+  }
+
+  async connect() {
+    if (this.#isOpen) {
+      throw new Error("already attempting to open")
+    }
+
+    try {
+      this.#isOpen = true;
+
+      this.#connectPromise = this.#connect();
+      await this.#connectPromise;
+      this.#isReady = true;
+    } finally {
+      this.#connectPromise = undefined;
+      if (this.#scanInterval > 0) {
+        this.#scanTimer = setInterval(this.#reset.bind(this), this.#scanInterval);
+      }
+    }
+  }
+
+  async #connect() {
+    let count = 0;
+    while (true) {
+      this.#trace("starting connect loop");
+
+      count+=1;
+      if (this.#destroy) {
+        this.#trace("in #connect and want to destroy")
+        return;
+      }
+      try {
+        this.#anotherReset = false;
+        await this.transform(this.analyze(await this.observe()));
+        if (this.#anotherReset) {
+          this.#trace("#connect: anotherReset is true, so continuing");
+          continue;
+        }
+
+        this.#trace("#connect: returning");
+        return;
+      } catch (e: any) {
+        this.#trace(`#connect: exception ${e.message}`);
+        if (!this.#isReady && count > this.#maxCommandRediscovers) {
+          throw e;
+        }
+
+        if (e.message !== 'no valid master node') {
+          console.log(e);
+        }
+        await setTimeout(1000);
+      } finally {
+        this.#trace("finished connect");
+      }
+    }
+  }
+
+  async execute<T>(
+    fn: (client: RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>) => Promise<T>,
+    clientInfo?: ClientInfo
+  ): Promise<T> {
+    let iter = 0;
+
+    while (true) {
+      if (this.#connectPromise !== undefined) {
+        await this.#connectPromise;
+      }
+
+      const client = this.#getClient(clientInfo);
+
+      if (!client.isReady) {
+        await this.#reset();
+        continue;
+      }
+      const sockOpts = client.options?.socket as TcpNetConnectOpts | undefined;
+      this.#trace("attemping to send command to " + sockOpts?.host + ":" + sockOpts?.port)
+
+      try {
+        /*
+                // force testing of READONLY errors
+                if (clientInfo !== undefined) {
+                  if (Math.floor(Math.random() * 10) < 1) {
+                    console.log("throwing READONLY error");
+                    throw new Error("READONLY You can't write against a read only replica.");
+                  }
+                }
+        */
+        return await fn(client);
+      } catch (err) {
+        if (++iter > this.#maxCommandRediscovers || !(err instanceof Error)) {
+          throw err;
+        }
+
+        /*
+          rediscover and retry if doing a command against a "master"
+          a) READONLY error (topology has changed) but we haven't been notified yet via pubsub
+          b) client is "not ready" (disconnected), which means topology might have changed, but sentinel might not see it yet
+        */
+        if (clientInfo !== undefined && (err.message.startsWith('READONLY') || !client.isReady)) {
+          await this.#reset();
+          continue;
+        }
+
+        throw err;
+      }
+    }
+  }
+
+  async #createPubSub(client: RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>) {
+    /* Whenever sentinels or slaves get added, or when slave configuration changes, reconfigure */
+    await client.pSubscribe(['switch-master', '[-+]sdown', '+slave', '+sentinel', '[-+]odown', '+slave-reconf-done'], (message, channel) => {
+      this.#handlePubSubControlChannel(channel, message);
+    }, true);
+
+    return client;
+  }
+
+  async #handlePubSubControlChannel(channel: Buffer, message: Buffer) {
+    this.#trace("pubsub control channel message on " + channel);
+    this.#reset();
+  }
+
+  // if clientInfo is defined, it corresponds to a master client in the #masterClients array, otherwise loop around replicaClients
+  #getClient(clientInfo?: ClientInfo): RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> {
+    if (clientInfo !== undefined) {
+      return this.#masterClients[clientInfo.id];
+    }
+
+    if (this.#replicaClientsIdx >= this.#replicaClients.length) {
+      this.#replicaClientsIdx = 0;
+    }
+
+    if (this.#replicaClients.length == 0) {
+      throw new Error("no replicas available for read");
+    }
+
+    return this.#replicaClients[this.#replicaClientsIdx++];
+  }
+
+  async #reset() {
+    /* closing / don't reset */
+    if (this.#isReady == false || this.#destroy == true) {
+      return;
+    }
+
+    // already in #connect()
+    if (this.#connectPromise !== undefined) {
+      this.#anotherReset = true;
+      return await this.#connectPromise;
+    }
+
+    try {
+      this.#connectPromise = this.#connect();
+      return await this.#connectPromise;
+    } finally {
+      this.#trace("finished reconfgure");
+      this.#connectPromise = undefined;
+    }
+  }
+
+  async close() {
+    this.#destroy = true;
+
+    if (this.#connectPromise != undefined) {
+      await this.#connectPromise;
+    }
+
+    this.#isReady = false;
+
+    this.#clientSideCache?.onPoolClose();
+
+    if (this.#scanTimer) {
+      clearInterval(this.#scanTimer);
+      this.#scanTimer = undefined;
+    }
+
+    const promises = [];
+
+    if (this.#sentinelClient !== undefined) {
+      if (this.#sentinelClient.isOpen) {
+        promises.push(this.#sentinelClient.close());
+      }
+      this.#sentinelClient = undefined;
+    }
+
+    for (const client of this.#masterClients) {
+      if (client.isOpen) {
+        promises.push(client.close());
+      }
+    }
+
+    this.#masterClients = [];
+
+    for (const client of this.#replicaClients) {
+      if (client.isOpen) {
+        promises.push(client.close());
+      }
+    }
+
+    this.#replicaClients = [];
+
+    await Promise.all(promises);
+
+    this.#pubSubProxy.destroy();
+
+    this.#isOpen = false;
+  }
+
+  // destroy has to be async because its stopping others async events, timers and the like
+  // and shouldn't return until its finished.
+  async destroy() {
+    this.#destroy = true;
+
+    if (this.#connectPromise != undefined) {
+      await this.#connectPromise;
+    }
+
+    this.#isReady = false;
+
+    this.#clientSideCache?.onPoolClose();
+
+    if (this.#scanTimer) {
+      clearInterval(this.#scanTimer);
+      this.#scanTimer = undefined;
+    }
+
+    if (this.#sentinelClient !== undefined) {
+      if (this.#sentinelClient.isOpen) {
+        this.#sentinelClient.destroy();
+      }
+      this.#sentinelClient = undefined;
+    }
+
+    for (const client of this.#masterClients) {
+      if (client.isOpen) {
+        client.destroy();
+      }
+    }
+    this.#masterClients = [];
+
+    for (const client of this.#replicaClients) {
+      if (client.isOpen) {
+        client.destroy();
+      }
+    }
+    this.#replicaClients = [];
+
+    this.#pubSubProxy.destroy();
+
+    this.#isOpen = false
+    this.#destroy = false;
+  }
+
+  async subscribe<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this.#pubSubProxy.subscribe(channels, listener, bufferMode);
+  }
+
+  async unsubscribe<T extends boolean = false>(
+    channels?: string | Array<string>,
+    listener?: PubSubListener<boolean>,
+    bufferMode?: T
+  ) {
+    return this.#pubSubProxy.unsubscribe(channels, listener, bufferMode);
+  }
+
+  async pSubscribe<T extends boolean = false>(
+    patterns: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this.#pubSubProxy.pSubscribe(patterns, listener, bufferMode);
+  }
+
+  async pUnsubscribe<T extends boolean = false>(
+    patterns?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this.#pubSubProxy.pUnsubscribe(patterns, listener, bufferMode);
+  }
+
+  // observe/analyze/transform remediation functions
+  async observe() {
+    for (const node of this.#sentinelRootNodes) {
+      let client: RedisClientType<typeof RedisSentinelModule, {}, {}, RespVersions, {}> | undefined;
+      try {
+        this.#trace(`observe: trying to connect to sentinel: ${node.host}:${node.port}`)
+        client = this.#createClient(node, this.#sentinelClientOptions, false) as unknown as RedisClientType<typeof RedisSentinelModule, {}, {}, RespVersions, {}>;
+        client.on('error', (err) => this.emit('error', `obseve client error: ${err}`));
+        await client.connect();
+        this.#trace(`observe: connected to sentinel`)
+
+        const [sentinelData, masterData, replicaData] = await Promise.all([
+          client.sentinel.sentinelSentinels(this.#name),
+          client.sentinel.sentinelMaster(this.#name),
+          client.sentinel.sentinelReplicas(this.#name)
+        ]);
+
+        this.#trace("observe: got all sentinel data");
+
+        const ret = {
+          sentinelConnected: node,
+          sentinelData: sentinelData,
+          masterData: masterData,
+          replicaData: replicaData,
+          currentMaster: this.getMasterNode(),
+          currentReplicas: this.getReplicaNodes(),
+          currentSentinel: this.getSentinelNode(),
+          replicaPoolSize: this.#replicaPoolSize,
+          useReplicas: this.useReplicas
+        }
+
+        return ret;
+      } catch (err) {
+        this.#trace(`observe: error ${err}`);
+        this.emit('error', err);
+      } finally {
+        if (client !== undefined && client.isOpen) {
+          this.#trace(`observe: destroying sentinel client`);
+          client.destroy();
+        }
+      }
+    }
+
+    this.#trace(`observe: none of the sentinels are available`);
+    throw new Error('None of the sentinels are available');
+  }
+
+  analyze(observed: Awaited<ReturnType<RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>["observe"]>>) {
+    let master = parseNode(observed.masterData);
+    if (master === undefined) {
+      this.#trace(`analyze: no valid master node because ${observed.masterData.flags}`);
+      throw new Error("no valid master node");
+    }
+
+    if (master.host === observed.currentMaster?.host && master.port === observed.currentMaster?.port) {
+      this.#trace(`analyze: master node hasn't changed from ${observed.currentMaster?.host}:${observed.currentMaster?.port}`);
+      master = undefined;
+    } else {
+      this.#trace(`analyze: master node has changed to ${master.host}:${master.port} from ${observed.currentMaster?.host}:${observed.currentMaster?.port}`);
+    }
+
+    let sentinel: RedisNode | undefined = observed.sentinelConnected;
+    if (sentinel.host === observed.currentSentinel?.host && sentinel.port === observed.currentSentinel.port) {
+      this.#trace(`analyze: sentinel node hasn't changed`);
+      sentinel = undefined;
+    } else {
+      this.#trace(`analyze: sentinel node has changed to ${sentinel.host}:${sentinel.port}`);
+    }
+
+    const replicasToClose: Array<RedisNode> = [];
+    const replicasToOpen = new Map<RedisNode, number>();
+
+    const desiredSet = new Set<string>();
+    const seen = new Set<string>();
+
+    if (observed.useReplicas) {
+      const replicaList = createNodeList(observed.replicaData)
+
+      for (const node of replicaList) {
+        desiredSet.add(JSON.stringify(node));
+      }
+
+      for (const [node, value] of observed.currentReplicas) {
+        if (!desiredSet.has(JSON.stringify(node))) {
+          replicasToClose.push(node);
+          this.#trace(`analyze: adding ${node.host}:${node.port} to replicsToClose`);
+        } else {
+          seen.add(JSON.stringify(node));
+          if (value != observed.replicaPoolSize) {
+            replicasToOpen.set(node, observed.replicaPoolSize - value);
+            this.#trace(`analyze: adding ${node.host}:${node.port} to replicsToOpen`);
+          }
+        }
+      }
+
+      for (const node of replicaList) {
+        if (!seen.has(JSON.stringify(node))) {
+          replicasToOpen.set(node, observed.replicaPoolSize);
+          this.#trace(`analyze: adding ${node.host}:${node.port} to replicsToOpen`);
+        }
+      }
+    }
+
+    const ret = {
+      sentinelList: [observed.sentinelConnected].concat(createNodeList(observed.sentinelData)),
+      epoch: Number(observed.masterData['config-epoch']),
+
+      sentinelToOpen: sentinel,
+      masterToOpen: master,
+      replicasToClose: replicasToClose,
+      replicasToOpen: replicasToOpen,
+    };
+
+    return ret;
+  }
+
+  async transform(analyzed: ReturnType<RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>["analyze"]>) {
+    this.#trace("transform: enter");
+
+    let promises: Array<Promise<any>> = [];
+
+    if (analyzed.sentinelToOpen) {
+      this.#trace(`transform: opening a new sentinel`);
+      if (this.#sentinelClient !== undefined && this.#sentinelClient.isOpen) {
+        this.#trace(`transform: destroying old sentinel as open`);
+        this.#sentinelClient.destroy()
+        this.#sentinelClient = undefined;
+      } else {
+        this.#trace(`transform: not destroying old sentinel as not open`);
+      }
+
+      this.#trace(`transform: creating new sentinel to ${analyzed.sentinelToOpen.host}:${analyzed.sentinelToOpen.port}`);
+      const node = analyzed.sentinelToOpen;
+      const client = this.#createClient(analyzed.sentinelToOpen, this.#sentinelClientOptions, false);
+      client.on('error', (err: Error) => {
+        if (this.#passthroughClientErrorEvents) {
+          this.emit('error', new Error(`Sentinel Client (${node.host}:${node.port}): ${err.message}`, { cause: err }));
+        }
+        const event: ClientErrorEvent = {
+          type: 'SENTINEL',
+          node: clientSocketToNode(client.options!.socket!),
+          error: err
+        };
+        this.emit('client-error', event);
+        this.#reset();
+      });
+      this.#sentinelClient = client;
+
+      this.#trace(`transform: adding sentinel client connect() to promise list`);
+      const promise = this.#sentinelClient.connect().then((client) => { return this.#createPubSub(client) });
+      promises.push(promise);
+
+      this.#trace(`created sentinel client to ${analyzed.sentinelToOpen.host}:${analyzed.sentinelToOpen.port}`);
+      const event: RedisSentinelEvent = {
+        type: "SENTINEL_CHANGE",
+        node: analyzed.sentinelToOpen
+      }
+      this.#trace(`transform: emiting topology-change event for sentinel_change`);
+      if (!this.emit('topology-change', event)) {
+        this.#trace(`transform: emit for topology-change for sentinel_change returned false`);
+      }
+    }
+
+    if (analyzed.masterToOpen) {
+      this.#trace(`transform: opening a new master`);
+      const masterPromises = [];
+      const masterWatches: Array<boolean> = [];
+
+      this.#trace(`transform: destroying old masters if open`);
+      for (const client of this.#masterClients) {
+        masterWatches.push(client.isWatching || client.isDirtyWatch);
+
+        if (client.isOpen) {
+          client.destroy()
+        }
+      }
+
+      this.#masterClients = [];
+
+      this.#trace(`transform: creating all master clients and adding connect promises`);
+      for (let i = 0; i < this.#masterPoolSize; i++) {
+        const node = analyzed.masterToOpen;
+        const client = this.#createClient(analyzed.masterToOpen, this.#nodeClientOptions);
+        client.on('error', (err: Error) => {
+          if (this.#passthroughClientErrorEvents) {
+            this.emit('error', new Error(`Master Client (${node.host}:${node.port}): ${err.message}`, { cause: err }));
+          }
+          const event: ClientErrorEvent = {
+            type: "MASTER",
+            node: clientSocketToNode(client.options!.socket!),
+            error: err
+          };
+          this.emit('client-error', event);
+        });
+
+        if (masterWatches[i]) {
+          client.setDirtyWatch("sentinel config changed in middle of a WATCH Transaction");
+        }
+        this.#masterClients.push(client);
+        masterPromises.push(client.connect());
+
+        this.#trace(`created master client to ${analyzed.masterToOpen.host}:${analyzed.masterToOpen.port}`);
+      }
+
+      this.#trace(`transform: adding promise to change #pubSubProxy node`);
+      masterPromises.push(this.#pubSubProxy.changeNode(analyzed.masterToOpen));
+      promises.push(...masterPromises);
+      const event: RedisSentinelEvent = {
+        type: "MASTER_CHANGE",
+        node: analyzed.masterToOpen
+      }
+      this.#trace(`transform: emiting topology-change event for master_change`);
+      if (!this.emit('topology-change', event)) {
+        this.#trace(`transform: emit for topology-change for master_change returned false`);
+      }
+      this.#configEpoch++;
+    }
+
+    const replicaCloseSet = new Set<string>();
+    for (const node of analyzed.replicasToClose) {
+      const str = JSON.stringify(node);
+      replicaCloseSet.add(str);
+    }
+
+    const newClientList: Array<RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>> = [];
+    const removedSet = new Set<string>();
+
+    for (const replica of this.#replicaClients) {
+      const node = clientSocketToNode(replica.options!.socket!);
+      const str = JSON.stringify(node);
+
+      if (replicaCloseSet.has(str) || !replica.isOpen) {
+        if (replica.isOpen) {
+          const sockOpts = replica.options?.socket as TcpNetConnectOpts | undefined;
+          this.#trace(`destroying replica client to ${sockOpts?.host}:${sockOpts?.port}`);
+          replica.destroy()
+        }
+        if (!removedSet.has(str)) {
+          const event: RedisSentinelEvent = {
+            type: "REPLICA_REMOVE",
+            node: node
+          }
+          this.emit('topology-change', event);
+          removedSet.add(str);
+        }
+      } else {
+        newClientList.push(replica);
+      }
+    }
+    this.#replicaClients = newClientList;
+
+    if (analyzed.replicasToOpen.size != 0) {
+      for (const [node, size] of analyzed.replicasToOpen) {
+        for (let i = 0; i < size; i++) {
+          const client = this.#createClient(node, this.#nodeClientOptions);
+          client.on('error', (err: Error) => {
+            if (this.#passthroughClientErrorEvents) {
+              this.emit('error', new Error(`Replica Client (${node.host}:${node.port}): ${err.message}`, { cause: err }));
+            }
+            const event: ClientErrorEvent = {
+              type: "REPLICA",
+              node: clientSocketToNode(client.options!.socket!),
+              error: err
+            };
+            this.emit('client-error', event);
+          });
+
+          this.#replicaClients.push(client);
+          promises.push(client.connect());
+
+          this.#trace(`created replica client to ${node.host}:${node.port}`);
+        }
+        const event: RedisSentinelEvent = {
+          type: "REPLICA_ADD",
+          node: node
+        }
+        this.emit('topology-change', event);
+      }
+    }
+
+    if (analyzed.sentinelList.length != this.#sentinelRootNodes.length) {
+      this.#sentinelRootNodes = analyzed.sentinelList;
+      const event: RedisSentinelEvent = {
+        type: "SENTINE_LIST_CHANGE",
+        size: analyzed.sentinelList.length
+      }
+      this.emit('topology-change', event);
+    }
+
+    await Promise.all(promises);
+    this.#trace("transform: exit");
+  }
+
+  // introspection functions
+  getMasterNode(): RedisNode | undefined {
+    if (this.#masterClients.length == 0) {
+      return undefined;
+    }
+
+    for (const master of this.#masterClients) {
+      if (master.isReady) {
+        return clientSocketToNode(master.options!.socket!);
+      }
+    }
+
+    return undefined;
+  }
+
+  getSentinelNode(): RedisNode | undefined {
+    if (this.#sentinelClient === undefined) {
+      return undefined;
+    }
+
+    return clientSocketToNode(this.#sentinelClient.options!.socket!);
+  }
+
+  getReplicaNodes(): Map<RedisNode, number> {
+    const ret = new Map<RedisNode, number>();
+    const initialMap = new Map<string, number>();
+
+    for (const replica of this.#replicaClients) {
+      const node = clientSocketToNode(replica.options!.socket!);
+      const hash = JSON.stringify(node);
+
+      if (replica.isReady) {
+        initialMap.set(hash, (initialMap.get(hash) ?? 0) + 1);
+      } else {
+        if (!initialMap.has(hash)) {
+          initialMap.set(hash, 0);
+        }
+      }
+    }
+
+    for (const [key, value] of initialMap) {
+      ret.set(JSON.parse(key) as RedisNode, value);
+    }
+
+    return ret;
+  }
+
+  setTracer(tracer?: Array<string>) {
+    if (tracer) {
+      this.#trace = (msg: string) => { tracer.push(msg) };
+    } else {
+      // empty function is faster than testing if something is defined or not
+      this.#trace = () => { };
+    }
+  }
+}
+
+export class RedisSentinelFactory extends EventEmitter {
+  options: RedisSentinelOptions;
+  #sentinelRootNodes: Array<RedisNode>;
+  #replicaIdx: number = -1;
+
+  constructor(options: RedisSentinelOptions) {
+    super();
+
+    this.options = options;
+    this.#sentinelRootNodes = options.sentinelRootNodes;
+  }
+
+  async updateSentinelRootNodes() {
+    for (const node of this.#sentinelRootNodes) {
+      const client = RedisClient.create({
+        ...this.options.sentinelClientOptions,
+        socket: {
+          ...this.options.sentinelClientOptions?.socket,
+          host: node.host,
+          port: node.port,
+          reconnectStrategy: false
+        },
+        modules: RedisSentinelModule
+      }).on('error', (err) => this.emit(`updateSentinelRootNodes: ${err}`));
+      try {
+        await client.connect();
+      } catch {
+        if (client.isOpen) {
+          client.destroy();
+        }
+        continue;
+      }
+
+      try {
+        const sentinelData = await client.sentinel.sentinelSentinels(this.options.name);
+        this.#sentinelRootNodes = [node].concat(createNodeList(sentinelData));
+        return;
+      } finally {
+        client.destroy();
+      }
+    }
+
+    throw new Error("Couldn't connect to any sentinel node");
+  }
+
+  async getMasterNode() {
+    let connected = false;
+
+    for (const node of this.#sentinelRootNodes) {
+      const client = RedisClient.create({
+        ...this.options.sentinelClientOptions,
+        socket: {
+          ...this.options.sentinelClientOptions?.socket,
+          host: node.host,
+          port: node.port,
+          reconnectStrategy: false
+        },
+        modules: RedisSentinelModule
+      }).on('error', err => this.emit(`getMasterNode: ${err}`));
+
+      try {
+        await client.connect();
+      } catch {
+        if (client.isOpen) {
+          client.destroy();
+        }
+        continue;
+      }
+
+      connected = true;
+
+      try {
+        const masterData = await client.sentinel.sentinelMaster(this.options.name);
+
+        let master = parseNode(masterData);
+        if (master === undefined) {
+          continue;
+        }
+
+        return master;
+      } finally {
+        client.destroy();
+      }
+    }
+
+    if (connected) {
+      throw new Error("Master Node Not Enumerated");
+    }
+
+    throw new Error("couldn't connect to any sentinels");
+  }
+
+  async getMasterClient() {
+    const master = await this.getMasterNode();
+    return RedisClient.create({
+      ...this.options.nodeClientOptions,
+      socket: {
+        ...this.options.nodeClientOptions?.socket,
+        host: master.host,
+        port: master.port
+      }
+    });
+  }
+
+  async getReplicaNodes() {
+    let connected = false;
+
+    for (const node of this.#sentinelRootNodes) {
+      const client = RedisClient.create({
+        ...this.options.sentinelClientOptions,
+        socket: {
+          ...this.options.sentinelClientOptions?.socket,
+          host: node.host,
+          port: node.port,
+          reconnectStrategy: false
+        },
+        modules: RedisSentinelModule
+      }).on('error', err => this.emit(`getReplicaNodes: ${err}`));
+
+      try {
+        await client.connect();
+      } catch {
+        if (client.isOpen) {
+          client.destroy();
+        }
+        continue;
+      }
+
+      connected = true;
+
+      try {
+        const replicaData = await client.sentinel.sentinelReplicas(this.options.name);
+
+        const replicas = createNodeList(replicaData);
+        if (replicas.length == 0) {
+          continue;
+        }
+
+        return replicas;
+      } finally {
+        client.destroy();
+      }
+    }
+
+    if (connected) {
+      throw new Error("No Replicas Nodes Enumerated");
+    }
+
+    throw new Error("couldn't connect to any sentinels");
+  }
+
+  async getReplicaClient() {
+    const replicas = await this.getReplicaNodes();
+    if (replicas.length == 0) {
+      throw new Error("no available replicas");
+    }
+
+    this.#replicaIdx++;
+    if (this.#replicaIdx >= replicas.length) {
+      this.#replicaIdx = 0;
+    }
+
+    return RedisClient.create({
+      ...this.options.nodeClientOptions,
+      socket: {
+        ...this.options.nodeClientOptions?.socket,
+        host: replicas[this.#replicaIdx].host,
+        port: replicas[this.#replicaIdx].port
+      }
+    });
+  }
+}
diff --git a/packages/client/lib/sentinel/module.ts b/packages/client/lib/sentinel/module.ts
new file mode 100644
index 00000000000..e6e98e72f6d
--- /dev/null
+++ b/packages/client/lib/sentinel/module.ts
@@ -0,0 +1,7 @@
+
+import { RedisModules } from '../RESP/types';
+import sentinel from './commands';
+
+export default {
+  sentinel
+} as const satisfies RedisModules;
diff --git a/packages/client/lib/sentinel/multi-commands.ts b/packages/client/lib/sentinel/multi-commands.ts
new file mode 100644
index 00000000000..e70dc45c790
--- /dev/null
+++ b/packages/client/lib/sentinel/multi-commands.ts
@@ -0,0 +1,250 @@
+import COMMANDS from '../commands';
+import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType } from '../multi-command';
+import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types';
+import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander';
+import { RedisSentinelType } from './types';
+import { BasicCommandParser } from '../client/parser';
+import { Tail } from '../commands/generic-transformers';
+
+type CommandSignature<
+  REPLIES extends Array<unknown>,
+  C extends Command,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (...args: Tail<Parameters<C['parseCommand']>>) => RedisSentinelMultiCommandType<
+  [...REPLIES, ReplyWithTypeMapping<CommandReply<C, RESP>, TYPE_MAPPING>],
+  M,
+  F,
+  S,
+  RESP,
+  TYPE_MAPPING
+>;
+
+type WithCommands<
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof typeof COMMANDS]: CommandSignature<REPLIES, (typeof COMMANDS)[P], M, F, S, RESP, TYPE_MAPPING>;
+};
+
+type WithModules<
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof M]: {
+    [C in keyof M[P]]: CommandSignature<REPLIES, M[P][C], M, F, S, RESP, TYPE_MAPPING>;
+  };
+};
+
+type WithFunctions<
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [L in keyof F]: {
+    [C in keyof F[L]]: CommandSignature<REPLIES, F[L][C], M, F, S, RESP, TYPE_MAPPING>;
+  };
+};
+
+type WithScripts<
+  REPLIES extends Array<unknown>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof S]: CommandSignature<REPLIES, S[P], M, F, S, RESP, TYPE_MAPPING>;
+};
+
+export type RedisSentinelMultiCommandType<
+  REPLIES extends Array<any>,
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = (
+  RedisSentinelMultiCommand<REPLIES> &
+  WithCommands<REPLIES, M, F, S, RESP, TYPE_MAPPING> & 
+  WithModules<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
+  WithFunctions<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
+  WithScripts<REPLIES, M, F, S, RESP, TYPE_MAPPING>
+);
+
+export default class RedisSentinelMultiCommand<REPLIES = []> {
+  private static _createCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return function (this: RedisSentinelMultiCommand, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this.addCommand(
+        command.IS_READ_ONLY,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  private static _createModuleCommand(command: Command, resp: RespVersions) {
+    const transformReply = getTransformReply(command, resp);
+
+    return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      command.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this._self.addCommand(
+        command.IS_READ_ONLY,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  private static _createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
+    const prefix = functionArgumentsPrefix(name, fn);
+    const transformReply = getTransformReply(fn, resp);
+
+    return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      parser.push(...prefix);
+      fn.parseCommand(parser, ...args);
+
+      const redisArgs: CommandArguments = parser.redisArgs;
+      redisArgs.preserve = parser.preserve;
+
+      return this._self.addCommand(
+        fn.IS_READ_ONLY,
+        redisArgs,
+        transformReply
+      );
+    };
+  }
+
+  private static _createScriptCommand(script: RedisScript, resp: RespVersions) {
+    const transformReply = getTransformReply(script, resp);
+
+    return function (this: RedisSentinelMultiCommand, ...args: Array<unknown>) {
+      const parser = new BasicCommandParser();
+      script.parseCommand(parser, ...args);
+
+      const scriptArgs: CommandArguments = parser.redisArgs;
+      scriptArgs.preserve = parser.preserve;
+
+      return this.#addScript(
+        script.IS_READ_ONLY,
+        script,
+        scriptArgs,
+        transformReply
+      );
+    };
+  }
+
+  static extend<
+    M extends RedisModules = Record<string, never>,
+    F extends RedisFunctions = Record<string, never>,
+    S extends RedisScripts = Record<string, never>,
+    RESP extends RespVersions = 2
+  >(config?: CommanderConfig<M, F, S, RESP>) {
+    return attachConfig({
+      BaseClass: RedisSentinelMultiCommand,
+      commands: COMMANDS,
+      createCommand: RedisSentinelMultiCommand._createCommand,
+      createModuleCommand: RedisSentinelMultiCommand._createModuleCommand,
+      createFunctionCommand: RedisSentinelMultiCommand._createFunctionCommand,
+      createScriptCommand: RedisSentinelMultiCommand._createScriptCommand,
+      config
+    });
+  }
+
+  readonly #multi = new RedisMultiCommand();
+  readonly #sentinel: RedisSentinelType
+  #isReadonly: boolean | undefined = true;
+
+  constructor(sentinel: RedisSentinelType, typeMapping: TypeMapping) {
+    this.#multi = new RedisMultiCommand(typeMapping);
+    this.#sentinel = sentinel;
+  }
+
+  #setState(
+    isReadonly: boolean | undefined,
+  ) {
+    this.#isReadonly &&= isReadonly;
+  }
+
+  addCommand(
+    isReadonly: boolean | undefined,
+    args: CommandArguments,
+    transformReply?: TransformReply
+  ) {
+    this.#setState(isReadonly);
+    this.#multi.addCommand(args, transformReply);
+    return this;
+  }
+
+  #addScript(
+    isReadonly: boolean | undefined,
+    script: RedisScript,
+    args: CommandArguments,
+    transformReply?: TransformReply
+  ) {
+    this.#setState(isReadonly);
+    this.#multi.addScript(script, args, transformReply);
+
+    return this;
+  }
+
+  async exec<T extends MultiReply = MULTI_REPLY['GENERIC']>(execAsPipeline = false) {
+    if (execAsPipeline) return this.execAsPipeline<T>();
+
+    return this.#multi.transformReplies(
+      await this.#sentinel._executeMulti(
+        this.#isReadonly,
+        this.#multi.queue
+      )
+    ) as MultiReplyType<T, REPLIES>;
+  }
+
+  EXEC = this.exec;
+
+  execTyped(execAsPipeline = false) {
+    return this.exec<MULTI_REPLY['TYPED']>(execAsPipeline);
+  }
+
+  async execAsPipeline<T extends MultiReply = MULTI_REPLY['GENERIC']>() {
+    if (this.#multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
+
+    return this.#multi.transformReplies(
+      await this.#sentinel._executePipeline(
+        this.#isReadonly,
+        this.#multi.queue
+      )
+    ) as MultiReplyType<T, REPLIES>;
+  }
+
+  execAsPipelineTyped() {
+    return this.execAsPipeline<MULTI_REPLY['TYPED']>();
+  }
+}
diff --git a/packages/client/lib/sentinel/pub-sub-proxy.ts b/packages/client/lib/sentinel/pub-sub-proxy.ts
new file mode 100644
index 00000000000..68a6c3b58e6
--- /dev/null
+++ b/packages/client/lib/sentinel/pub-sub-proxy.ts
@@ -0,0 +1,209 @@
+import EventEmitter from 'node:events';
+import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
+import { RedisClientOptions } from '../client';
+import { PUBSUB_TYPE, PubSubListener, PubSubTypeListeners } from '../client/pub-sub';
+import { RedisNode } from './types';
+import RedisClient from '../client';
+
+type Client = RedisClient<
+  RedisModules,
+  RedisFunctions,
+  RedisScripts,
+  RespVersions,
+  TypeMapping
+>;
+
+type Subscriptions = Record<
+  PUBSUB_TYPE['CHANNELS'] | PUBSUB_TYPE['PATTERNS'],
+  PubSubTypeListeners
+>;
+
+type PubSubState = {
+  client: Client;
+  connectPromise: Promise<Client | undefined> | undefined;
+};
+
+type OnError = (err: unknown) => unknown;
+
+export class PubSubProxy extends EventEmitter {
+  #clientOptions;
+  #onError;
+
+  #node?: RedisNode;
+  #state?: PubSubState;
+  #subscriptions?: Subscriptions;
+
+  constructor(clientOptions: RedisClientOptions, onError: OnError) {
+    super();
+
+    this.#clientOptions = clientOptions;
+    this.#onError = onError;
+  }
+
+  #createClient() {
+    if (this.#node === undefined) {
+      throw new Error("pubSubProxy: didn't define node to do pubsub against");
+    }
+
+    return new RedisClient({
+      ...this.#clientOptions,
+      socket: {
+        ...this.#clientOptions.socket,
+        host: this.#node.host,
+        port: this.#node.port
+      }
+    });
+  }
+
+  async #initiatePubSubClient(withSubscriptions = false) {
+    const client = this.#createClient()
+      .on('error', this.#onError);
+
+    const connectPromise = client.connect()
+      .then(async client => {
+        if (this.#state?.client !== client) {
+          // if pubsub was deactivated while connecting (`this.#pubSubClient === undefined`)
+          // or if the node changed (`this.#pubSubClient.client !== client`)
+          client.destroy();
+          return this.#state?.connectPromise;
+        }
+
+        if (withSubscriptions && this.#subscriptions) {
+          await Promise.all([
+            client.extendPubSubListeners(PUBSUB_TYPE.CHANNELS, this.#subscriptions[PUBSUB_TYPE.CHANNELS]),
+            client.extendPubSubListeners(PUBSUB_TYPE.PATTERNS, this.#subscriptions[PUBSUB_TYPE.PATTERNS])
+          ]);
+        }
+
+        if (this.#state.client !== client) {
+          // if the node changed (`this.#pubSubClient.client !== client`)
+          client.destroy();
+          return this.#state?.connectPromise;
+        }
+
+        this.#state!.connectPromise = undefined;
+        return client;
+      })
+      .catch(err => {
+        this.#state = undefined;
+        throw err;
+      });
+
+    this.#state = {
+      client,
+      connectPromise
+    };
+
+    return connectPromise;
+  }
+
+  #getPubSubClient() {
+    if (!this.#state) return this.#initiatePubSubClient();
+
+    return (
+      this.#state.connectPromise ??
+      this.#state.client  
+    );
+  }
+
+  async changeNode(node: RedisNode) {
+    this.#node = node;
+
+    if (!this.#state) return;
+
+    // if `connectPromise` is undefined, `this.#subscriptions` is already set
+    // and `this.#state.client` might not have the listeners set yet
+    if (this.#state.connectPromise === undefined) {
+      this.#subscriptions = {
+        [PUBSUB_TYPE.CHANNELS]: this.#state.client.getPubSubListeners(PUBSUB_TYPE.CHANNELS),
+        [PUBSUB_TYPE.PATTERNS]: this.#state.client.getPubSubListeners(PUBSUB_TYPE.PATTERNS)
+      };
+
+      this.#state.client.destroy();
+    }
+
+    await this.#initiatePubSubClient(true);
+  }
+
+  #executeCommand<T>(fn: (client: Client) => T) {
+    const client = this.#getPubSubClient();
+    if (client instanceof RedisClient) {
+      return fn(client);
+    }
+
+    return client.then(client => {
+      // if pubsub was deactivated while connecting
+      if (client === undefined) return;
+
+      return fn(client);
+    }).catch(err => {
+      if (this.#state?.client.isPubSubActive) {
+        this.#state.client.destroy();
+        this.#state = undefined;
+      }
+
+      throw err;
+    });
+  }
+
+  subscribe<T extends boolean = false>(
+    channels: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this.#executeCommand(
+      client => client.SUBSCRIBE(channels, listener, bufferMode)
+    );
+  }
+
+  #unsubscribe<T>(fn: (client: Client) => Promise<T>) {
+    return this.#executeCommand(async client => {
+      const reply = await fn(client);
+
+      if (!client.isPubSubActive) {
+        client.destroy();
+        this.#state = undefined;
+      }
+  
+      return reply;
+    });
+  }
+
+  async unsubscribe<T extends boolean = false>(
+    channels?: string | Array<string>,
+    listener?: PubSubListener<boolean>,
+    bufferMode?: T
+  ) {
+    return this.#unsubscribe(client => client.UNSUBSCRIBE(channels, listener, bufferMode));
+  }
+
+  async pSubscribe<T extends boolean = false>(
+    patterns: string | Array<string>,
+    listener: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this.#executeCommand(
+      client => client.PSUBSCRIBE(patterns, listener, bufferMode)
+    );
+  }
+
+  async pUnsubscribe<T extends boolean = false>(
+    patterns?: string | Array<string>,
+    listener?: PubSubListener<T>,
+    bufferMode?: T
+  ) {
+    return this.#unsubscribe(client => client.PUNSUBSCRIBE(patterns, listener, bufferMode));
+  }
+
+  destroy() {
+    this.#subscriptions = undefined;
+    if (this.#state === undefined) return;
+    
+    // `connectPromise` already handles the case of `this.#pubSubState = undefined`
+    if (!this.#state.connectPromise) {
+      this.#state.client.destroy();
+    }
+    
+    this.#state = undefined;
+  }
+}
diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts
new file mode 100644
index 00000000000..7c4752a8852
--- /dev/null
+++ b/packages/client/lib/sentinel/test-util.ts
@@ -0,0 +1,512 @@
+import { createConnection, Socket } from 'node:net';
+import { setTimeout } from 'node:timers/promises';
+import { once } from 'node:events';
+import { promisify } from 'node:util';
+import { exec } from 'node:child_process';
+import { RedisSentinelOptions, RedisSentinelType } from './types';
+import RedisClient, {RedisClientType} from '../client';
+import RedisSentinel from '.';
+import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
+const execAsync = promisify(exec);
+import RedisSentinelModule from './module'
+import TestUtils from '@redis/test-utils';
+import { DEBUG_MODE_ARGS } from '../test-utils'
+interface ErrorWithCode extends Error {
+  code: string;
+}
+
+async function isPortAvailable(port: number): Promise<boolean> {
+  var socket: Socket | undefined = undefined;
+  try {
+    socket = createConnection({ port });
+    await once(socket, 'connect');
+  } catch (err) {
+    if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
+      return true;
+    }
+  } finally {
+    if (socket !== undefined) {
+      socket.end();
+    }
+  }
+
+  return false;
+}
+
+const portIterator = (async function* (): AsyncIterableIterator<number> {
+  for (let i = 6379; i < 65535; i++) {
+    if (await isPortAvailable(i)) {
+      yield i;
+    }
+  }
+
+  throw new Error('All ports are in use');
+})();
+
+export interface RedisServerDockerConfig {
+  image: string;
+  version: string;
+}
+
+export interface RedisServerDocker {
+  port: number;
+  dockerId: string;
+}
+
+abstract class DockerBase {
+  async spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array<string>, environment?: string): Promise<RedisServerDocker> {
+    const port = (await portIterator.next()).value;
+    let cmdLine = `docker run --init -d --network host `;
+    if (environment !== undefined) {
+      cmdLine += `-e ${environment} `;
+    }
+    cmdLine += `${image}:${version} ${serverArguments.join(' ')}`;
+    cmdLine = cmdLine.replace('{port}', `--port ${port.toString()}`);
+    // console.log("spawnRedisServerDocker: cmdLine = " + cmdLine);
+    const { stdout, stderr } = await execAsync(cmdLine);
+
+    if (!stdout) {
+      throw new Error(`docker run error - ${stderr}`);
+    }
+
+    while (await isPortAvailable(port)) {
+      await setTimeout(50);
+    }
+
+    return {
+      port,
+      dockerId: stdout.trim()
+    };
+  }
+
+  async dockerRemove(dockerId: string): Promise<void> {
+    try {
+      await this.dockerStop(dockerId); ``
+    } catch (err) {
+      // its ok if stop failed, as we are just going to remove, will just be slower
+      console.log(`dockerStop failed in remove: ${err}`);
+    }
+
+    const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
+    if (stderr) {
+      console.log("docker rm failed");
+      throw new Error(`docker rm error - ${stderr}`);
+    }
+  }
+
+  async dockerStop(dockerId: string): Promise<void> {
+    /* this is an optimization to get around slow docker stop times, but will fail if container is already stopped */
+    try {
+      await execAsync(`docker exec ${dockerId} /bin/bash -c "kill -SIGINT 1"`);
+    } catch (err) {
+      /* this will fail if container is already not running, can be ignored */
+    }
+
+    let ret = await execAsync(`docker stop ${dockerId}`);
+    if (ret.stderr) {
+      throw new Error(`docker stop error - ${ret.stderr}`);
+    }
+  }
+
+  async dockerStart(dockerId: string): Promise<void> {
+    const { stderr } = await execAsync(`docker start ${dockerId}`);
+    if (stderr) {
+      throw new Error(`docker start error - ${stderr}`);
+    }
+  }
+}
+
+export interface RedisSentinelConfig {
+  numberOfNodes?: number;
+  nodeDockerConfig?: RedisServerDockerConfig;
+  nodeServerArguments?: Array<string>
+
+  numberOfSentinels?: number;
+  sentinelDockerConfig?: RedisServerDockerConfig;
+  sentinelServerArgument?: Array<string>
+
+  sentinelName: string;
+
+  password?: string;
+}
+
+type ArrayElement<ArrayType extends readonly unknown[]> =
+  ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
+
+export interface SentinelController {
+  getMaster(): Promise<string>;
+  getMasterPort(): Promise<number>;
+  getRandomNode(): string;
+  getRandonNonMasterNode(): Promise<string>;
+  getNodePort(id: string): number;
+  getAllNodesPort(): Array<number>;
+  getSentinelPort(id: string): number;
+  getAllSentinelsPort(): Array<number>;
+  getSetinel(i: number): string;
+  stopNode(id: string): Promise<void>;
+  restartNode(id: string): Promise<void>;
+  stopSentinel(id: string): Promise<void>;
+  restartSentinel(id: string): Promise<void>;
+  getSentinelClient(opts?: Partial<RedisSentinelOptions<{}, {}, {}, 2, {}>>): RedisSentinelType<{}, {}, {}, 2, {}>;
+}
+
+export class SentinelFramework extends DockerBase {
+  #testUtils: TestUtils;
+  #nodeList: Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>> = [];
+  /* port -> docker info/client */
+  #nodeMap: Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>>>>;
+  #sentinelList: Awaited<ReturnType<SentinelFramework['spawnRedisSentinelSentinels']>> = [];
+  /* port -> docker info/client */
+  #sentinelMap: Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelSentinels']>>>>;
+
+  config: RedisSentinelConfig;
+
+  #spawned: boolean = false;
+
+  get spawned() {
+    return this.#spawned;
+  }
+
+  constructor(config: RedisSentinelConfig) {
+    super();
+
+    this.config = config;
+    this.#testUtils = TestUtils.createFromConfig({
+      dockerImageName: 'redislabs/client-libs-test',
+      dockerImageVersionArgument: 'redis-version',
+      defaultDockerVersion: '8.2-rc1'
+    });
+    this.#nodeMap = new Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>>>>();
+    this.#sentinelMap = new Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelSentinels']>>>>();
+  }
+
+  getSentinelClient(opts?: Partial<RedisSentinelOptions<RedisModules, 
+    RedisFunctions,
+    RedisScripts,
+    RespVersions, 
+    TypeMapping>>, errors = true) {
+    if (opts?.sentinelRootNodes !== undefined) {
+      throw new Error("cannot specify sentinelRootNodes here");
+    }
+    if (opts?.name !== undefined) {
+      throw new Error("cannot specify sentinel db name here");
+    }
+
+    const options: RedisSentinelOptions<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> = {
+      ...opts,
+      name: this.config.sentinelName,
+      sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.port } }),
+      passthroughClientErrorEvents: errors
+    }
+
+    if (this.config.password !== undefined) {
+      if (!options.nodeClientOptions) {
+        options.nodeClientOptions = {};
+      }
+      options.nodeClientOptions.password = this.config.password;
+
+      if (!options.sentinelClientOptions) {
+        options.sentinelClientOptions = {};
+      }
+      options.sentinelClientOptions = {password: this.config.password};
+    }
+
+    return RedisSentinel.create(options);
+  }
+
+  async spawnRedisSentinel() {
+    if (this.#spawned) {
+      return;
+    }
+
+    if (this.#nodeMap.size != 0 || this.#sentinelMap.size != 0) {
+      throw new Error("inconsistent state with partial setup");
+    }
+
+    this.#nodeList = await this.spawnRedisSentinelNodes(2);
+    this.#nodeList.map((value) => this.#nodeMap.set(value.port.toString(), value));
+
+    this.#sentinelList = await this.spawnRedisSentinelSentinels(this.#nodeList[0].port, 3)
+    this.#sentinelList.map((value) => this.#sentinelMap.set(value.port.toString(), value));
+
+    this.#spawned = true;
+  }
+
+  async cleanup() {
+    if (!this.#spawned) {
+      return;
+    }
+
+    return Promise.all(
+      [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map(
+        async ({ dockerId }) => {
+          this.dockerRemove(dockerId);
+        }
+      )
+    ).finally(async () => {
+      this.#spawned = false;
+      this.#nodeMap.clear();
+      this.#sentinelMap.clear();
+    });
+  }
+
+  protected async spawnRedisSentinelNodes(replicasCount: number) {
+    const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS})
+    
+    const replicas: Array<RedisServerDocker> = []
+    for (let i = 0; i < replicasCount; i++) {
+      const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS})
+      replicas.push(replica)
+
+      const client = RedisClient.create({
+        socket: {
+          port: replica.port
+        }
+      })
+
+      await client.connect();
+      await client.replicaOf("127.0.0.1", master.port);
+      await client.close();
+    }
+
+    return [
+      master,
+      ...replicas
+    ]
+  }
+
+  protected async spawnRedisSentinelSentinels(masterPort: number, sentinels: number) {
+    return this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, masterPort, this.config.sentinelName, sentinels)
+  }
+
+  async getAllRunning() {
+    for (const port of this.getAllNodesPort()) {
+      let first = true;
+      while (await isPortAvailable(port)) {        
+        if (!first) {
+          console.log(`problematic restart ${port}`);
+          await setTimeout(500);
+        } else {
+          first = false;
+        }
+        await this.restartNode(port.toString());
+      }
+    }
+
+    for (const port of this.getAllSentinelsPort()) {
+      let first = true;
+      while (await isPortAvailable(port)) {        
+        if (!first) {
+          await setTimeout(500);
+        } else {
+          first = false;
+        }
+        await this.restartSentinel(port.toString());
+      }
+    }
+  }
+
+  async addSentinel() {
+    const nodes = await this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, this.#nodeList[0].port, this.config.sentinelName, 1)
+    this.#sentinelList.push(nodes[0]);
+    this.#sentinelMap.set(nodes[0].port.toString(), nodes[0]);
+  }
+
+  async addNode() {
+    const masterPort = await this.getMasterPort();
+    const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS})
+
+    const client = RedisClient.create({
+      socket: {
+        port: replica.port
+      }
+    })
+
+    await client.connect();
+    await client.replicaOf("127.0.0.1", masterPort);
+    await client.close();
+    
+
+    this.#nodeList.push(replica);
+    this.#nodeMap.set(replica.port.toString(), replica);
+  }
+
+  async getMaster(tracer?: Array<string>): Promise<string | undefined> {
+    const client = RedisClient.create({
+      name: this.config.sentinelName, 
+      socket: {
+        host: "127.0.0.1", 
+        port: this.#sentinelList[0].port,
+      },
+      modules: RedisSentinelModule,
+    });
+    await client.connect()
+    const info = await client.sentinel.sentinelMaster(this.config.sentinelName);
+    await client.close()
+
+    const master = this.#nodeMap.get(info.port);
+    if (master === undefined) {
+      throw new Error(`couldn't find master node for ${info.port}`);
+    }
+
+    if (tracer) {
+      tracer.push(`getMaster: master port is either ${info.port} or ${master.port}`);
+    }
+
+    return info.port;
+  }
+
+  async getMasterPort(tracer?: Array<string>): Promise<number> {
+    const data = await this.getMaster(tracer)
+
+    return this.#nodeMap.get(data!)!.port;
+  }
+
+  getRandomNode() {
+    return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].port.toString();
+  }
+
+  async getRandonNonMasterNode(): Promise<string> {
+    const masterPort = await this.getMasterPort();
+    while (true) {
+      const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)];
+      if (node.port != masterPort) {
+        return node.port.toString();
+      }
+    }
+  }
+
+  async stopNode(id: string) {
+//    console.log(`stopping node ${id}`);
+    let node = this.#nodeMap.get(id);
+    if (node === undefined) {
+      throw new Error("unknown node: " + id);
+    }
+
+    return await this.dockerStop(node.dockerId);
+  }
+
+  async restartNode(id: string) {
+    let node = this.#nodeMap.get(id);
+    if (node === undefined) {
+      throw new Error("unknown node: " + id);
+    }
+
+    await this.dockerStart(node.dockerId);
+  }
+
+  async stopSentinel(id: string) {
+    let sentinel = this.#sentinelMap.get(id);
+    if (sentinel === undefined) {
+      throw new Error("unknown sentinel: " + id);
+    }
+
+    return await this.dockerStop(sentinel.dockerId);
+  }
+
+  async restartSentinel(id: string) {
+    let sentinel = this.#sentinelMap.get(id);
+    if (sentinel === undefined) {
+      throw new Error("unknown sentinel: " + id);
+    }
+
+    await this.dockerStart(sentinel.dockerId);
+  }
+
+  getNodePort(id: string) {
+    let node = this.#nodeMap.get(id);
+    if (node === undefined) {
+      throw new Error("unknown node: " + id);
+    }
+
+    return node.port;
+  }
+
+  getAllNodesPort() {
+    let ports: Array<number> = [];
+    for (const node of this.#nodeList) {
+      ports.push(node.port);
+    }
+
+    return ports
+  }
+
+  getAllDockerIds() {
+    let ids = new Map<string, number>();
+    for (const node of this.#nodeList) {
+      ids.set(node.dockerId, node.port);
+    }
+
+    return ids;
+  }
+
+  getSentinelPort(id: string) {
+    let sentinel = this.#sentinelMap.get(id);
+    if (sentinel === undefined) {
+      throw new Error("unknown sentinel: " + id);
+    }
+
+    return sentinel.port;
+  }
+
+  getAllSentinelsPort() {
+    let ports: Array<number> = [];
+    for (const sentinel of this.#sentinelList) {
+      ports.push(sentinel.port);
+    }
+
+    return ports
+  }
+
+  getSetinel(i: number): string {
+    return this.#sentinelList[i].port.toString();
+  }
+
+  async sentinelSentinels() {
+    const client = RedisClient.create({
+      name: this.config.sentinelName, 
+      socket: {
+        host: "127.0.0.1", 
+        port: this.#sentinelList[0].port,
+      },
+      modules: RedisSentinelModule,
+    });
+    await client.connect()
+    const sentinels = client.sentinel.sentinelSentinels(this.config.sentinelName)
+    await client.close()
+
+    return sentinels
+  }
+
+  async sentinelMaster() {
+    const client = RedisClient.create({
+      name: this.config.sentinelName, 
+      socket: {
+        host: "127.0.0.1", 
+        port: this.#sentinelList[0].port,
+      },
+      modules: RedisSentinelModule,
+    });
+    await client.connect()
+    const master = client.sentinel.sentinelMaster(this.config.sentinelName)
+    await client.close()
+
+    return master
+  }
+
+  async sentinelReplicas() {
+    const client = RedisClient.create({
+      name: this.config.sentinelName, 
+      socket: {
+        host: "127.0.0.1", 
+        port: this.#sentinelList[0].port,
+      },
+      modules: RedisSentinelModule,
+    });
+    await client.connect()
+    const replicas = client.sentinel.sentinelReplicas(this.config.sentinelName)
+    await client.close()
+
+    return replicas
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts
new file mode 100644
index 00000000000..e72f2eec2a0
--- /dev/null
+++ b/packages/client/lib/sentinel/types.ts
@@ -0,0 +1,219 @@
+import { RedisClientOptions } from '../client';
+import { CommandOptions } from '../client/commands-queue';
+import { CommandSignature, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
+import COMMANDS from '../commands';
+import RedisSentinel, { RedisSentinelClient } from '.';
+import { RedisTcpSocketOptions } from '../client/socket';
+import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/cache';
+
+export interface RedisNode {
+  host: string;
+  port: number;
+}
+
+export interface RedisSentinelOptions<
+  M extends RedisModules = RedisModules,
+  F extends RedisFunctions = RedisFunctions,
+  S extends RedisScripts = RedisScripts,
+  RESP extends RespVersions = RespVersions,
+  TYPE_MAPPING extends TypeMapping = TypeMapping
+> extends SentinelCommander<M, F, S, RESP, TYPE_MAPPING> {
+  /**
+   * The sentinel identifier for a particular database cluster
+   */
+  name: string;
+  /**
+   * An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server
+   */
+  sentinelRootNodes: Array<RedisNode>;
+  /**
+   * The maximum number of times a command will retry due to topology changes.
+   */
+  maxCommandRediscovers?: number;
+  // TODO: omit properties that users shouldn't be able to specify for sentinel at this level
+  /**
+   * The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with
+   */
+  nodeClientOptions?: RedisClientOptions<RedisModules, RedisFunctions, RedisScripts, RESP, TYPE_MAPPING, RedisTcpSocketOptions>;
+  // TODO: omit properties that users shouldn't be able to specify for sentinel at this level
+  /**
+   * The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with
+   */
+  sentinelClientOptions?: RedisClientOptions<RedisModules, RedisFunctions, RedisScripts, RESP, TYPE_MAPPING, RedisTcpSocketOptions>;
+  /**
+   * The number of clients connected to the master node
+   */
+  masterPoolSize?: number;
+  /**
+   * The number of clients connected to each replica node.
+   * When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes.
+   */
+  replicaPoolSize?: number;
+  /**
+   * Interval in milliseconds to periodically scan for changes in the sentinel topology.
+   * The client will query the sentinel for changes at this interval.
+   * 
+   * Default: 10000 (10 seconds)
+   */
+  scanInterval?: number;
+  /**
+   * When `true`, error events from client instances inside the sentinel will be propagated to the sentinel instance.
+   * This allows handling all client errors through a single error handler on the sentinel instance.
+   * 
+   * Default: false
+   */
+  passthroughClientErrorEvents?: boolean;
+  /**
+   * When `true`, one client will be reserved for the sentinel object.
+   * When `false`, the sentinel object will wait for the first available client from the pool.
+   */
+  reserveClient?: boolean;
+  /**
+   * Client Side Caching configuration for the pool.
+   * 
+   * Enables Redis Servers and Clients to work together to cache results from commands 
+   * sent to a server. The server will notify the client when cached results are no longer valid.
+   * In pooled mode, the cache is shared across all clients in the pool.
+   * 
+   * Note: Client Side Caching is only supported with RESP3.
+   * 
+   * @example Anonymous cache configuration
+   * ```
+   * const client = createSentinel({
+   *   clientSideCache: {
+   *     ttl: 0,
+   *     maxEntries: 0,
+   *     evictPolicy: "LRU"
+   *   },
+   *   minimum: 5
+   * });
+   * ```
+   * 
+   * @example Using a controllable cache
+   * ```
+   * const cache = new BasicPooledClientSideCache({
+   *   ttl: 0,
+   *   maxEntries: 0,
+   *   evictPolicy: "LRU"
+   * });
+   * const client = createSentinel({
+   *   clientSideCache: cache,
+   *   minimum: 5
+   * });
+   * ```
+   */
+  clientSideCache?: PooledClientSideCacheProvider | ClientSideCacheConfig;
+}
+
+export interface SentinelCommander<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping,
+  // POLICIES extends CommandPolicies
+> extends CommanderConfig<M, F, S, RESP> {
+  commandOptions?: CommandOptions<TYPE_MAPPING>;
+}
+
+export type RedisSentinelClientOptions = Omit<
+  RedisClientOptions,
+  keyof SentinelCommander<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping/*, CommandPolicies*/>
+>;
+
+type WithCommands<
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
+};
+
+type WithModules<
+  M extends RedisModules,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof M]: {
+    [C in keyof M[P]]: CommandSignature<M[P][C], RESP, TYPE_MAPPING>;
+  };
+};
+
+type WithFunctions<
+  F extends RedisFunctions,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [L in keyof F]: {
+    [C in keyof F[L]]: CommandSignature<F[L][C], RESP, TYPE_MAPPING>;
+  };
+};
+
+type WithScripts<
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> = {
+  [P in keyof S]: CommandSignature<S[P], RESP, TYPE_MAPPING>;
+};
+
+export type RedisSentinelClientType<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {},
+> = (
+  RedisSentinelClient<M, F, S, RESP, TYPE_MAPPING> &
+  WithCommands<RESP, TYPE_MAPPING> &
+  WithModules<M, RESP, TYPE_MAPPING> &
+  WithFunctions<F, RESP, TYPE_MAPPING> &
+  WithScripts<S, RESP, TYPE_MAPPING>
+);
+
+export type RedisSentinelType<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {},
+  // POLICIES extends CommandPolicies = {}
+> = (
+  RedisSentinel<M, F, S, RESP, TYPE_MAPPING> &
+  WithCommands<RESP, TYPE_MAPPING> &
+  WithModules<M, RESP, TYPE_MAPPING> &
+  WithFunctions<F, RESP, TYPE_MAPPING> &
+  WithScripts<S, RESP, TYPE_MAPPING>
+);
+
+export interface SentinelCommandOptions<
+  TYPE_MAPPING extends TypeMapping = TypeMapping
+> extends CommandOptions<TYPE_MAPPING> {}
+
+export type ProxySentinel = RedisSentinel<any, any, any, any, any>;
+export type ProxySentinelClient = RedisSentinelClient<any, any, any, any, any>;
+export type NamespaceProxySentinel = { _self: ProxySentinel };
+export type NamespaceProxySentinelClient = { _self: ProxySentinelClient };
+
+export type NodeInfo = {
+  ip: any,
+  port: any,
+  flags: any,
+};
+
+export type RedisSentinelEvent = NodeChangeEvent | SizeChangeEvent;
+ 
+export type NodeChangeEvent = {
+  type: "SENTINEL_CHANGE" | "MASTER_CHANGE" | "REPLICA_ADD" | "REPLICA_REMOVE";
+  node: RedisNode;
+}
+
+export type SizeChangeEvent = {
+  type: "SENTINE_LIST_CHANGE";
+  size: Number;
+}
+
+export type ClientErrorEvent = {
+  type: 'MASTER' | 'REPLICA' | 'SENTINEL' | 'PUBSUBPROXY';
+  node: RedisNode;
+  error: Error;
+}
diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts
new file mode 100644
index 00000000000..c124981e257
--- /dev/null
+++ b/packages/client/lib/sentinel/utils.ts
@@ -0,0 +1,98 @@
+import { ArrayReply, Command, RedisFunction, RedisScript, RespVersions, UnwrapReply } from '../RESP/types';
+import { BasicCommandParser } from '../client/parser';
+import { RedisSocketOptions, RedisTcpSocketOptions } from '../client/socket';
+import { functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
+import { NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, ProxySentinelClient, RedisNode } from './types';
+
+/* TODO: should use map interface, would need a transform reply probably? as resp2 is list form, which this depends on */
+export function parseNode(node: Record<string, string>): RedisNode | undefined{
+
+  if (node.flags.includes("s_down") || node.flags.includes("disconnected") || node.flags.includes("failover_in_progress")) {
+    return undefined;
+  }
+
+  return { host: node.ip, port: Number(node.port) };
+}
+
+export function createNodeList(nodes: UnwrapReply<ArrayReply<Record<string, string>>>) {
+  var nodeList: Array<RedisNode> = [];
+
+  for (const nodeData of nodes) {
+    const node = parseNode(nodeData)
+    if (node === undefined) {
+      continue;
+    }
+    nodeList.push(node);
+  }
+
+  return nodeList;
+}
+
+export function clientSocketToNode(socket: RedisSocketOptions): RedisNode {
+  const s = socket as RedisTcpSocketOptions;
+
+  return {
+    host: s.host!,
+    port: s.port!
+  }
+}
+
+export function createCommand<T extends ProxySentinel | ProxySentinelClient>(command: Command, resp: RespVersions) {
+  const transformReply = getTransformReply(command, resp);
+
+  return async function (this: T, ...args: Array<unknown>) {
+    const parser = new BasicCommandParser();
+    command.parseCommand(parser, ...args);
+
+    return this._self._execute(
+      command.IS_READ_ONLY,
+      client => client._executeCommand(command, parser, this.commandOptions, transformReply)
+    );
+  };
+}
+
+export function createFunctionCommand<T extends NamespaceProxySentinel | NamespaceProxySentinelClient>(name: string, fn: RedisFunction, resp: RespVersions) {
+  const prefix = functionArgumentsPrefix(name, fn);
+  const transformReply = getTransformReply(fn, resp);
+
+  return async function (this: T, ...args: Array<unknown>) {
+    const parser = new BasicCommandParser();
+    parser.push(...prefix);
+    fn.parseCommand(parser, ...args);
+
+    return this._self._execute(
+      fn.IS_READ_ONLY,
+      client => client._executeCommand(fn, parser, this._self.commandOptions, transformReply)
+    );
+  }
+};
+
+export function createModuleCommand<T extends NamespaceProxySentinel | NamespaceProxySentinelClient>(command: Command, resp: RespVersions) {
+  const transformReply = getTransformReply(command, resp);
+
+  return async function (this: T, ...args: Array<unknown>) {
+    const parser = new BasicCommandParser();
+    command.parseCommand(parser, ...args);
+
+    return this._self._execute(
+      command.IS_READ_ONLY,
+      client => client._executeCommand(command, parser, this._self.commandOptions, transformReply)
+    );
+  }
+};
+
+export function createScriptCommand<T extends ProxySentinel | ProxySentinelClient>(script: RedisScript, resp: RespVersions) {
+  const prefix = scriptArgumentsPrefix(script);
+  const transformReply = getTransformReply(script, resp);
+
+  return async function (this: T, ...args: Array<unknown>) {
+    const parser = new BasicCommandParser();
+    parser.push(...prefix);
+    script.parseCommand(parser, ...args);
+
+    return this._self._execute(
+      script.IS_READ_ONLY,
+      client => client._executeScript(script, parser, this.commandOptions, transformReply)
+    );
+  };
+}
diff --git a/packages/client/lib/sentinel/wait-queue.ts b/packages/client/lib/sentinel/wait-queue.ts
new file mode 100644
index 00000000000..138801eb4d9
--- /dev/null
+++ b/packages/client/lib/sentinel/wait-queue.ts
@@ -0,0 +1,24 @@
+import { SinglyLinkedList } from '../client/linked-list';
+
+export class WaitQueue<T> {
+  #list = new SinglyLinkedList<T>();
+  #queue = new SinglyLinkedList<(item: T) => unknown>();
+
+  push(value: T) {
+    const resolve = this.#queue.shift();
+    if (resolve !== undefined) {
+      resolve(value);
+      return;
+    }
+
+    this.#list.push(value);
+  }
+
+  shift() {
+    return this.#list.shift();
+  }
+
+  wait() {
+    return new Promise<T>(resolve => this.#queue.push(resolve));
+  }
+}
diff --git a/packages/client/lib/single-entry-cache.spec.ts b/packages/client/lib/single-entry-cache.spec.ts
new file mode 100644
index 00000000000..ef535738eac
--- /dev/null
+++ b/packages/client/lib/single-entry-cache.spec.ts
@@ -0,0 +1,85 @@
+import assert from 'node:assert';
+import SingleEntryCache from './single-entry-cache';
+
+describe('SingleEntryCache', () => {
+  let cache: SingleEntryCache;
+  beforeEach(() => {
+    cache = new SingleEntryCache();
+  });
+  
+  it('should return undefined when getting from empty cache', () => {
+    assert.strictEqual(cache.get({ key: 'value' }), undefined);
+  });
+
+  it('should return the cached instance when getting with the same key object', () => {
+    const keyObj = { key: 'value' };
+    const instance = { data: 'test data' };
+    
+    cache.set(keyObj, instance);
+    assert.strictEqual(cache.get(keyObj), instance);
+  });
+
+  it('should return undefined when getting with a different key object', () => {
+    const keyObj1 = { key: 'value1' };
+    const keyObj2 = { key: 'value2' };
+    const instance = { data: 'test data' };
+    
+    cache.set(keyObj1, instance);
+    assert.strictEqual(cache.get(keyObj2), undefined);
+  });
+
+  it('should update the cached instance when setting with the same key object', () => {
+    const keyObj = { key: 'value' };
+    const instance1 = { data: 'test data 1' };
+    const instance2 = { data: 'test data 2' };
+    
+    cache.set(keyObj, instance1);
+    assert.strictEqual(cache.get(keyObj), instance1);
+    
+    cache.set(keyObj, instance2);
+    assert.strictEqual(cache.get(keyObj), instance2);
+  });
+
+  it('should handle undefined key object', () => {
+    const instance = { data: 'test data' };
+    
+    cache.set(undefined, instance);
+    assert.strictEqual(cache.get(undefined), instance);
+  });
+
+  it('should handle complex objects as keys', () => {
+    const keyObj = { 
+      id: 123,
+      nested: { 
+        prop: 'value',
+        array: [1, 2, 3]
+      }
+    };
+    const instance = { data: 'complex test data' };
+    
+    cache.set(keyObj, instance);
+    assert.strictEqual(cache.get(keyObj), instance);
+  });
+
+  it('should consider objects with same properties but different order as different keys', () => {
+    const keyObj1 = { a: 1, b: 2 };
+    const keyObj2 = { b: 2, a: 1 }; // Same properties but different order
+    const instance = { data: 'test data' };
+    
+    cache.set(keyObj1, instance);
+    
+    assert.strictEqual(cache.get(keyObj2), undefined);
+  });
+
+  it('should handle circular structures', () => {
+    const keyObj: any = {};
+    keyObj.self = keyObj;
+
+    const instance = { data: 'test data' };
+    
+    cache.set(keyObj, instance);
+    
+    assert.strictEqual(cache.get(keyObj), instance);
+  });
+
+});
diff --git a/packages/client/lib/single-entry-cache.ts b/packages/client/lib/single-entry-cache.ts
new file mode 100644
index 00000000000..5c65df96660
--- /dev/null
+++ b/packages/client/lib/single-entry-cache.ts
@@ -0,0 +1,37 @@
+export default class SingleEntryCache<K, V> {
+  #cached?: V;
+  #serializedKey?: string;
+
+  /**
+   * Retrieves an instance from the cache based on the provided key object.
+   *
+   * @param keyObj - The key object to look up in the cache.
+   * @returns The cached instance if found, undefined otherwise.
+   *
+   * @remarks
+   * This method uses JSON.stringify for comparison, which may not work correctly
+   * if the properties in the key object are rearranged or reordered.
+   */
+  get(keyObj?: K): V | undefined {
+    return JSON.stringify(keyObj, makeCircularReplacer()) === this.#serializedKey ? this.#cached : undefined;
+  }
+
+  set(keyObj:  K | undefined, obj: V) {
+    this.#cached = obj;
+    this.#serializedKey = JSON.stringify(keyObj, makeCircularReplacer());
+  }
+}
+
+function makeCircularReplacer() {
+  const seen = new WeakSet();
+  return function serialize(_: string, value: any) {
+    if (value && typeof value === 'object') {
+      if (seen.has(value)) {
+        return 'circular';
+      }
+      seen.add(value);
+      return value;
+    }
+    return value;
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts
index fbbac3e0b71..86b6ed294ac 100644
--- a/packages/client/lib/test-utils.ts
+++ b/packages/client/lib/test-utils.ts
@@ -1,63 +1,198 @@
 import TestUtils from '@redis/test-utils';
 import { SinonSpy } from 'sinon';
-import { promiseTimeout } from './utils';
-
-const utils = new TestUtils({
-  dockerImageName: 'redis',
+import { setTimeout } from 'node:timers/promises';
+import { CredentialsProvider } from './authx';
+import { Command, NumberReply } from './RESP/types';
+import { BasicCommandParser, CommandParser } from './client/parser';
+import { defineScript } from './lua-script';
+import RedisBloomModules from '@redis/bloom';
+const utils = TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
   dockerImageVersionArgument: 'redis-version',
-  defaultDockerVersion: '7.4-rc2'
+  defaultDockerVersion: '8.2-rc1'
 });
 
 export default utils;
 
-const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ?
+export const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ?
   ['--enable-debug-command', 'yes'] :
   [];
 
-export const GLOBAL = {
-    SERVERS: {
-        OPEN: {
-            serverArguments: [...DEBUG_MODE_ARGS]
-        },
-        PASSWORD: {
-            serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
-            clientOptions: {
-                password: 'password'
-            }
+const asyncBasicAuthCredentialsProvider: CredentialsProvider =
+  {
+    type: 'async-credentials-provider',
+    credentials: async () => ({ password: 'password' })
+  } as const;
+
+const streamingCredentialsProvider: CredentialsProvider =
+  {
+    type: 'streaming-credentials-provider',
+
+    subscribe : (observable) => ( Promise.resolve([
+     { password: 'password' },
+      {
+       dispose: () => {
+          console.log('disposing credentials provider subscription');
         }
+      }
+    ])),
+
+    onReAuthenticationError: (error) => {
+      console.error('re-authentication error', error);
+    }
+
+  } as const;
+
+const SQUARE_SCRIPT = defineScript({
+  SCRIPT:
+    `local number = redis.call('GET', KEYS[1])
+    return number * number`,
+  NUMBER_OF_KEYS: 1,
+  FIRST_KEY_INDEX: 0,
+  parseCommand(parser: CommandParser, key: string) {
+    parser.pushKey(key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+});
+
+export const MATH_FUNCTION = {
+  name: 'math',
+  engine: 'LUA',
+  code:
+    `#!LUA name=math
+    redis.register_function {
+      function_name = "square",
+      callback = function(keys, args)
+        local number = redis.call('GET', keys[1])
+        return number * number
+      end,
+      flags = { "no-writes" }
+    }`,
+  library: {
+    square: {
+      NAME: 'square',
+      IS_READ_ONLY: true,
+      NUMBER_OF_KEYS: 1,
+      FIRST_KEY_INDEX: 0,
+      parseCommand(parser: CommandParser, key: string) {
+        parser.pushKey(key);
+      },
+      transformReply: undefined as unknown as () => NumberReply
+    }
+  }
+};
+
+export const GLOBAL = {
+  SERVERS: {
+    OPEN: {
+      serverArguments: [...DEBUG_MODE_ARGS]
+    },
+    PASSWORD: {
+      serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+      clientOptions: {
+        password: 'password'
+      }
+    },
+    OPEN_RESP_3: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      clientOptions: {
+        RESP: 3,
+      }
+    },
+    ASYNC_BASIC_AUTH: {
+      serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+      clientOptions: {
+        credentialsProvider: asyncBasicAuthCredentialsProvider
+      }
     },
-    CLUSTERS: {
-        OPEN: {
-            serverArguments: [...DEBUG_MODE_ARGS]
-        },
-        PASSWORD: {
-            serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
-            clusterConfiguration: {
-                defaults: {
-                    password: 'password'
-                }
-            }
-        },
-        WITH_REPLICAS: {
-            serverArguments: [...DEBUG_MODE_ARGS],
-            numberOfMasters: 2,
-            numberOfReplicas: 1,
-            clusterConfiguration: {
-                useReplicas: true
-            }
+    STREAMING_AUTH: {
+      serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+      clientOptions: {
+        credentialsProvider: streamingCredentialsProvider
+      }
+    }
+  },
+  CLUSTERS: {
+    OPEN: {
+      serverArguments: [...DEBUG_MODE_ARGS]
+    },
+    PASSWORD: {
+      serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+      clusterConfiguration: {
+        defaults: {
+          password: 'password'
         }
+      }
+    },
+    WITH_REPLICAS: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      numberOfMasters: 2,
+      numberOfReplicas: 1,
+      clusterConfiguration: {
+        useReplicas: true
+      }
+    }
+  },
+  SENTINEL: {
+    OPEN: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+    },
+    PASSWORD: {
+      serverArguments: ['--requirepass', 'test_password', ...DEBUG_MODE_ARGS],
+    },
+    WITH_SCRIPT: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      scripts: {
+        square: SQUARE_SCRIPT,
+      },
+    },
+    WITH_FUNCTION: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      functions: {
+        math: MATH_FUNCTION.library,
+      },
+    },
+    WITH_MODULE: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      modules: RedisBloomModules,
+    },
+    WITH_REPLICA_POOL_SIZE_1: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      replicaPoolSize: 1,
+    },
+    WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      masterPoolSize: 2,
+      reserveClient: true,
+    },
+    WITH_MASTER_POOL_SIZE_2: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      masterPoolSize: 2,
     }
+  }
 };
 
 export async function waitTillBeenCalled(spy: SinonSpy): Promise<void> {
-    const start = process.hrtime.bigint(),
-        calls = spy.callCount;
+  const start = process.hrtime.bigint(),
+    calls = spy.callCount;
 
-    do {
-        if (process.hrtime.bigint() - start > 1_000_000_000) {
-            throw new Error('Waiting for more than 1 second');
-        }
+  do {
+    if (process.hrtime.bigint() - start > 1_000_000_000) {
+      throw new Error('Waiting for more than 1 second');
+    }
+
+    await setTimeout(50);
+  } while (spy.callCount === calls);
+}
+
+export const BLOCKING_MIN_VALUE = (
+  utils.isVersionGreaterThan([7]) ? Number.MIN_VALUE :
+  utils.isVersionGreaterThan([6]) ? 0.01 :
+  1
+);
 
-        await promiseTimeout(50);
-    } while (spy.callCount === calls);
+export function parseFirstKey(command: Command, ...args: Array<any>) {
+  const parser = new BasicCommandParser();
+  command.parseCommand!(parser, ...args);
+  return parser.firstKey;
 }
diff --git a/packages/client/lib/utils.ts b/packages/client/lib/utils.ts
deleted file mode 100644
index 55bed419813..00000000000
--- a/packages/client/lib/utils.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function promiseTimeout(ms: number): Promise<void> {
-    return new Promise(resolve => setTimeout(resolve, ms));
-}
diff --git a/packages/client/package.json b/packages/client/package.json
index e344edd52c3..091ddaf5c91 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,42 +1,27 @@
 {
   "name": "@redis/client",
-  "version": "1.6.0",
+  "version": "5.7.0",
   "license": "MIT",
   "main": "./dist/index.js",
   "types": "./dist/index.d.ts",
   "files": [
-    "dist/"
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
   ],
   "scripts": {
-    "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
-    "build": "tsc",
-    "lint": "eslint ./*.ts ./lib/**/*.ts",
-    "documentation": "typedoc"
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
+    "release": "release-it"
   },
   "dependencies": {
-    "cluster-key-slot": "1.1.2",
-    "generic-pool": "3.9.0",
-    "yallist": "4.0.0"
+    "cluster-key-slot": "1.1.2"
   },
   "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
     "@redis/test-utils": "*",
-    "@types/node": "^20.6.2",
-    "@types/sinon": "^10.0.16",
-    "@types/yallist": "^4.0.1",
-    "@typescript-eslint/eslint-plugin": "^6.7.2",
-    "@typescript-eslint/parser": "^6.7.2",
-    "eslint": "^8.49.0",
-    "nyc": "^15.1.0",
-    "release-it": "^16.1.5",
-    "sinon": "^16.0.0",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typedoc": "^0.25.1",
-    "typescript": "^5.2.2"
+    "@types/sinon": "^17.0.3",
+    "sinon": "^17.0.1"
   },
   "engines": {
-    "node": ">=14"
+    "node": ">= 18"
   },
   "repository": {
     "type": "git",
diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json
index c71595c5702..b1f7b44d915 100644
--- a/packages/client/tsconfig.json
+++ b/packages/client/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "outDir": "./dist"
+    "outDir": "./dist",
   },
   "include": [
     "./index.ts",
@@ -10,17 +10,15 @@
   ],
   "exclude": [
     "./lib/test-utils.ts",
-    "./lib/**/*.spec.ts"
+    "./lib/**/*.spec.ts",
+    "./lib/sentinel/test-util.ts"
   ],
-  "ts-node": {
-    "transpileOnly": true
-  },
   "typedocOptions": {
     "entryPoints": [
       "./index.ts",
       "./lib"
     ],
     "entryPointStrategy": "expand",
-    "out": "../../documentation/client"
+    "out": "../../documentation/client",
   }
 }
diff --git a/packages/entraid/.nycrc.json b/packages/entraid/.nycrc.json
new file mode 100644
index 00000000000..848af2b5a27
--- /dev/null
+++ b/packages/entraid/.nycrc.json
@@ -0,0 +1,10 @@
+{
+  "extends": "@istanbuljs/nyc-config-typescript",
+  "exclude": [
+    "integration-tests",
+    "samples",
+    "dist",
+    "**/*.spec.ts",
+    "lib/test-utils.ts"
+  ]
+}
diff --git a/packages/entraid/.release-it.json b/packages/entraid/.release-it.json
new file mode 100644
index 00000000000..a0609a242f7
--- /dev/null
+++ b/packages/entraid/.release-it.json
@@ -0,0 +1,22 @@
+{
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
+  "git": {
+    "tagName": "entraid@${version}",
+    "tagMatch": "entraid@*",
+    "commitMessage": "Release ${tagName}",
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
+  },
+  "plugins": {
+    "@release-it/bumper": {
+      "out": {
+        "file": "package.json",
+        "path": ["peerDependencies.@redis/client"],
+        "versionPrefix": "^"
+      }
+    }
+  }
+}
diff --git a/packages/entraid/README.md b/packages/entraid/README.md
new file mode 100644
index 00000000000..733cf895a7e
--- /dev/null
+++ b/packages/entraid/README.md
@@ -0,0 +1,188 @@
+# @redis/entraid
+
+Secure token-based authentication for Redis clients using Microsoft Entra ID (formerly Azure Active Directory).
+
+## Features
+
+- Token-based authentication using Microsoft Entra ID
+- Automatic token refresh before expiration
+- Automatic re-authentication of all connections after token refresh
+- Support for multiple authentication flows:
+  - Managed identities (system-assigned and user-assigned)
+  - Service principals (with or without certificates)
+  - Authorization Code with PKCE flow
+  - DefaultAzureCredential from @azure/identity
+- Built-in retry mechanisms for transient failures
+
+## Installation
+
+
+```bash
+npm install "@redis/client@5.0.0-next.7"
+npm install "@redis/entraid@5.0.0-next.7"
+```
+
+## Getting Started
+
+The first step to using @redis/entraid is choosing the right credentials provider for your authentication needs. The `EntraIdCredentialsProviderFactory` class provides several factory methods to create the appropriate provider:
+
+- `createForSystemAssignedManagedIdentity`: Use when your application runs in Azure with a system-assigned managed identity
+- `createForUserAssignedManagedIdentity`: Use when your application runs in Azure with a user-assigned managed identity
+- `createForClientCredentials`: Use when authenticating with a service principal using client secret
+- `createForClientCredentialsWithCertificate`: Use when authenticating with a service principal using a certificate
+- `createForAuthorizationCodeWithPKCE`: Use for interactive authentication flows in user applications
+- `createForDefaultAzureCredential`: Use when you want to leverage Azure Identity's DefaultAzureCredential
+
+## Usage Examples
+
+### Service Principal Authentication
+
+```typescript
+import { createClient } from '@redis/client';
+import { EntraIdCredentialsProviderFactory } from '@redis/entraid';
+
+const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({
+  clientId: 'your-client-id',
+  clientSecret: 'your-client-secret',
+  authorityConfig: {
+    type: 'multi-tenant',
+    tenantId: 'your-tenant-id'
+  },
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8 // Refresh token after 80% of its lifetime
+  }
+});
+
+const client = createClient({
+  url: 'redis://your-host',
+  credentialsProvider: provider
+});
+
+await client.connect();
+```
+
+### System-Assigned Managed Identity
+
+```typescript
+const provider = EntraIdCredentialsProviderFactory.createForSystemAssignedManagedIdentity({
+  clientId: 'your-client-id',
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8
+  }
+});
+```
+
+### User-Assigned Managed Identity
+
+```typescript
+const provider = EntraIdCredentialsProviderFactory.createForUserAssignedManagedIdentity({
+  clientId: 'your-client-id',
+  userAssignedClientId: 'your-user-assigned-client-id',
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8
+  }
+});
+```
+
+### DefaultAzureCredential Authentication
+
+tip: see a real sample here: [samples/interactive-browser/index.ts](./samples/interactive-browser/index.ts)
+
+The DefaultAzureCredential from @azure/identity provides a simplified authentication experience that automatically tries different authentication methods based on the environment. This is especially useful for applications that need to work in different environments (local development, CI/CD, and production).
+
+```typescript
+import { createClient } from '@redis/client';
+import { getDefaultAzureCredential } from '@azure/identity';
+import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '@redis/entraid';
+
+// Create a DefaultAzureCredential instance
+const credential = getDefaultAzureCredential();
+
+// Create a provider using DefaultAzureCredential
+const provider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
+  // Use the same parameters you would pass to credential.getToken()
+  credential,
+  scopes: REDIS_SCOPE_DEFAULT, // The Redis scope
+  // Optional additional parameters for getToken
+  options: {
+    // Any options you would normally pass to credential.getToken()
+  },
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8
+  }
+});
+
+const client = createClient({
+  url: 'redis://your-host',
+  credentialsProvider: provider
+});
+
+await client.connect();
+```
+
+#### Important Notes on Using DefaultAzureCredential
+
+When using the `createForDefaultAzureCredential` method, you need to:
+
+1. Create your own instance of `DefaultAzureCredential`
+2. Pass the same parameters to the factory method that you would use with the `getToken()` method:
+   - `scopes`: The Redis scope (use the exported `REDIS_SCOPE_DEFAULT` constant)
+   - `options`: Any additional options for the getToken method
+
+This factory method creates a wrapper around DefaultAzureCredential that adapts it to the Redis client's
+authentication system, while maintaining all the flexibility of the original Azure Identity authentication.
+
+## Important Limitations
+
+### RESP2 PUB/SUB Limitations
+
+When using RESP2 (Redis Serialization Protocol 2), there are important limitations with PUB/SUB:
+
+- **No Re-Authentication in PUB/SUB Mode**: In RESP2, once a connection enters PUB/SUB mode, the socket is blocked and cannot process out-of-band commands like AUTH. This means that connections in PUB/SUB mode cannot be re-authenticated when tokens are refreshed.
+- **Connection Eviction**: As a result, PUB/SUB connections will be evicted by the Redis proxy when their tokens expire. The client will need to establish new connections with fresh tokens.
+
+### Transaction Safety
+
+When using token-based authentication, special care must be taken with Redis transactions. The token manager runs in the background and may attempt to re-authenticate connections at any time by sending AUTH commands. This can interfere with manually constructed transactions.
+
+#### ✅ Recommended: Use the Official Transaction API
+
+Always use the official transaction API provided by the client:
+
+```typescript
+// Correct way to handle transactions
+const multi = client.multi();
+multi.set('key1', 'value1');
+multi.set('key2', 'value2');
+await multi.exec();
+```
+
+#### ❌ Avoid: Manual Transaction Construction
+
+Do not manually construct transactions by sending individual MULTI/EXEC commands:
+
+```typescript
+// Incorrect and potentially dangerous
+await client.sendCommand(['MULTI']);
+await client.sendCommand(['SET', 'key1', 'value1']);
+await client.sendCommand(['SET', 'key2', 'value2']);
+await client.sendCommand(['EXEC']); // Risk of AUTH command being injected before EXEC
+```
+
+## Error Handling
+
+The provider includes built-in retry mechanisms for transient errors:
+
+```typescript
+const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({
+  // ... other config ...
+  tokenManagerConfig: {
+    retry: {
+      maxAttempts: 3,
+      initialDelayMs: 100,
+      maxDelayMs: 1000,
+      backoffMultiplier: 2
+    }
+  }
+});
+```
diff --git a/packages/entraid/index.ts b/packages/entraid/index.ts
new file mode 100644
index 00000000000..303b5dc6e14
--- /dev/null
+++ b/packages/entraid/index.ts
@@ -0,0 +1 @@
+export * from './lib/index'
\ No newline at end of file
diff --git a/packages/entraid/integration-tests/entraid-integration.spec.ts b/packages/entraid/integration-tests/entraid-integration.spec.ts
new file mode 100644
index 00000000000..4d078a01ede
--- /dev/null
+++ b/packages/entraid/integration-tests/entraid-integration.spec.ts
@@ -0,0 +1,262 @@
+import { DefaultAzureCredential, EnvironmentCredential } from '@azure/identity';
+import { BasicAuth } from '@redis/client/dist/lib/authx';
+import { createClient } from '@redis/client';
+import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '../lib/entra-id-credentials-provider-factory';
+import { strict as assert } from 'node:assert';
+import { spy, SinonSpy } from 'sinon';
+import { randomUUID } from 'crypto';
+import { loadFromFile, RedisEndpointsConfig } from '@redis/test-utils/lib/cae-client-testing';
+import { EntraidCredentialsProvider } from '../lib/entraid-credentials-provider';
+import * as crypto from 'node:crypto';
+
+describe('EntraID Integration Tests', () => {
+
+  it('client configured with client secret should be able to authenticate/re-authenticate', async () => {
+    const config = await readConfigFromEnv();
+    await runAuthenticationTest(() =>
+      EntraIdCredentialsProviderFactory.createForClientCredentials({
+        clientId: config.clientId,
+        clientSecret: config.clientSecret,
+        authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId },
+        tokenManagerConfig: {
+          expirationRefreshRatio: 0.0001
+        }
+      })
+    );
+  });
+
+  it('client configured with client certificate should be able to authenticate/re-authenticate', async () => {
+    const config = await readConfigFromEnv();
+    await runAuthenticationTest(() =>
+      EntraIdCredentialsProviderFactory.createForClientCredentialsWithCertificate({
+        clientId: config.clientId,
+        certificate: convertCertsForMSAL(config.cert, config.privateKey),
+        authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId },
+        tokenManagerConfig: {
+          expirationRefreshRatio: 0.0001
+        }
+      })
+    );
+  });
+
+  it('client with system managed identity should be able to authenticate/re-authenticate', async () => {
+    const config = await readConfigFromEnv();
+    await runAuthenticationTest(() =>
+      EntraIdCredentialsProviderFactory.createForSystemAssignedManagedIdentity({
+        clientId: config.clientId,
+        authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId },
+        tokenManagerConfig: {
+          expirationRefreshRatio: 0.00001
+        }
+      })
+    );
+  });
+
+  it('client with DefaultAzureCredential should be able to authenticate/re-authenticate', async () => {
+
+    const azureCredential = new DefaultAzureCredential();
+
+    await runAuthenticationTest(() =>
+        EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
+          credential: azureCredential,
+          scopes: REDIS_SCOPE_DEFAULT,
+          tokenManagerConfig: {
+            expirationRefreshRatio: 0.00001
+          }
+        })
+      , { testingDefaultAzureCredential: true });
+  });
+
+  it('client with EnvironmentCredential should be able to authenticate/re-authenticate', async () => {
+    const envCredential = new EnvironmentCredential();
+
+    await runAuthenticationTest(() =>
+        EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
+          credential: envCredential,
+          scopes: REDIS_SCOPE_DEFAULT,
+          tokenManagerConfig: {
+            expirationRefreshRatio: 0.00001
+          }
+        })
+      , { testingDefaultAzureCredential: true });
+  });
+
+  interface TestConfig {
+    clientId: string;
+    clientSecret: string;
+    authority: string;
+    tenantId: string;
+    redisScopes: string;
+    cert: string;
+    privateKey: string;
+    userAssignedManagedId: string;
+    endpoints: RedisEndpointsConfig;
+  }
+
+  const readConfigFromEnv = async (): Promise<TestConfig> => {
+    const requiredEnvVars = {
+      AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID,
+      AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET,
+      AZURE_AUTHORITY: process.env.AZURE_AUTHORITY,
+      AZURE_TENANT_ID: process.env.AZURE_TENANT_ID,
+      AZURE_REDIS_SCOPES: process.env.AZURE_REDIS_SCOPES,
+      AZURE_CERT: process.env.AZURE_CERT,
+      AZURE_PRIVATE_KEY: process.env.AZURE_PRIVATE_KEY,
+      AZURE_USER_ASSIGNED_MANAGED_ID: process.env.AZURE_USER_ASSIGNED_MANAGED_ID,
+      REDIS_ENDPOINTS_CONFIG_PATH: process.env.REDIS_ENDPOINTS_CONFIG_PATH
+    };
+
+    Object.entries(requiredEnvVars).forEach(([key, value]) => {
+      if (value == undefined) {
+        throw new Error(`${key} environment variable must be set`);
+      }
+    });
+
+    return {
+      endpoints: await loadFromFile(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH as string),
+      clientId: requiredEnvVars.AZURE_CLIENT_ID as string,
+      clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET as string,
+      authority: requiredEnvVars.AZURE_AUTHORITY as string,
+      tenantId: requiredEnvVars.AZURE_TENANT_ID as string,
+      redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES as string,
+      cert: requiredEnvVars.AZURE_CERT as string,
+      privateKey: requiredEnvVars.AZURE_PRIVATE_KEY as string,
+      userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID as string
+    };
+  };
+
+  interface TokenDetail {
+    token: string;
+    exp: number;
+    iat: number;
+    lifetime: number;
+    uti: string;
+  }
+
+  const setupTestClient = async (credentialsProvider: EntraidCredentialsProvider) => {
+    const config = await readConfigFromEnv();
+    const client = createClient({
+      url: config.endpoints['standalone-entraid-acl'].endpoints[0],
+      credentialsProvider
+    });
+
+    const clientInstance = (client as any)._self;
+    const reAuthSpy: SinonSpy = spy(clientInstance, 'reAuthenticate');
+
+    return { client, reAuthSpy };
+  };
+
+  const runClientOperations = async (client: any) => {
+    const startTime = Date.now();
+    while (Date.now() - startTime < 1000) {
+      const key = randomUUID();
+      await client.set(key, 'value');
+      const value = await client.get(key);
+      assert.equal(value, 'value');
+      await client.del(key);
+    }
+  };
+
+  /**
+   * Validates authentication tokens generated during re-authentication
+   *
+   * @param reAuthSpy - The Sinon spy on the reAuthenticate method
+   * @param skipUniqueCheckForDefaultAzureCredential - Skip the unique check for DefaultAzureCredential as there are no guarantees that the tokens will be unique
+   * if the test is using default azure credential
+   */
+  const validateTokens = (reAuthSpy: SinonSpy, skipUniqueCheckForDefaultAzureCredential: boolean) => {
+    assert(reAuthSpy.callCount >= 1,
+      `reAuthenticate should have been called at least once, but was called ${reAuthSpy.callCount} times`);
+
+    const tokenDetails: TokenDetail[] = reAuthSpy.getCalls().map(call => {
+      const creds = call.args[0] as BasicAuth;
+      if (!creds.password) {
+        throw new Error('Expected password to be set in BasicAuth credentials');
+      }
+      const tokenPayload = JSON.parse(
+        Buffer.from(creds.password.split('.')[1], 'base64').toString()
+      );
+
+      return {
+        token: creds.password,
+        exp: tokenPayload.exp,
+        iat: tokenPayload.iat,
+        lifetime: tokenPayload.exp - tokenPayload.iat,
+        uti: tokenPayload.uti
+      };
+    });
+
+    // we can't guarantee that the tokens will be unique when using DefaultAzureCredential
+    if (!skipUniqueCheckForDefaultAzureCredential) {
+      // Verify unique tokens
+      const uniqueTokens = new Set(tokenDetails.map(detail => detail.token));
+      assert.equal(
+        uniqueTokens.size,
+        reAuthSpy.callCount,
+        `Expected ${reAuthSpy.callCount} different tokens, but got ${uniqueTokens.size} unique tokens`
+      );
+
+      // Verify all tokens are not cached (i.e. have the same lifetime)
+      const uniqueLifetimes = new Set(tokenDetails.map(detail => detail.lifetime));
+      assert.equal(
+        uniqueLifetimes.size,
+        1,
+        `Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${(Array.from(uniqueLifetimes).join(','))} seconds`
+      );
+
+      // Verify that all tokens have different uti (unique token identifier)
+      const uniqueUti = new Set(tokenDetails.map(detail => detail.uti));
+      assert.equal(
+        uniqueUti.size,
+        reAuthSpy.callCount,
+        `Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${(Array.from(uniqueUti).join(','))}`
+      );
+    }
+  };
+
+  const runAuthenticationTest = async (setupCredentialsProvider: () => any, options: {
+    testingDefaultAzureCredential: boolean
+  } = { testingDefaultAzureCredential: false }) => {
+    const { client, reAuthSpy } = await setupTestClient(setupCredentialsProvider());
+
+    try {
+      await client.connect();
+      await runClientOperations(client);
+      validateTokens(reAuthSpy, options.testingDefaultAzureCredential);
+    } finally {
+      await client.destroy();
+    }
+  };
+
+});
+
+function getCertificate(certBase64) {
+  try {
+    const decodedCert = Buffer.from(certBase64, 'base64');
+    const cert = new crypto.X509Certificate(decodedCert);
+    return cert;
+  } catch (error) {
+    console.error('Error parsing certificate:', error);
+    throw error;
+  }
+}
+
+function getCertificateThumbprint(certBase64) {
+  const cert = getCertificate(certBase64);
+  return cert.fingerprint.replace(/:/g, '');
+}
+
+function convertCertsForMSAL(certBase64, privateKeyBase64) {
+  const thumbprint = getCertificateThumbprint(certBase64);
+
+  const privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----`;
+
+  return {
+    thumbprint: thumbprint,
+    privateKey: privateKeyPEM,
+    x5c: certBase64
+  }
+
+}
+
+
diff --git a/packages/entraid/lib/azure-identity-provider.ts b/packages/entraid/lib/azure-identity-provider.ts
new file mode 100644
index 00000000000..d522c9d4b89
--- /dev/null
+++ b/packages/entraid/lib/azure-identity-provider.ts
@@ -0,0 +1,22 @@
+import type { AccessToken } from '@azure/core-auth';
+
+import { IdentityProvider, TokenResponse } from '@redis/client/dist/lib/authx';
+
+export class AzureIdentityProvider implements IdentityProvider<AccessToken> {
+  private readonly getToken: () => Promise<AccessToken>;
+
+  constructor(getToken: () => Promise<AccessToken>) {
+    this.getToken = getToken;
+  }
+
+  async requestToken(): Promise<TokenResponse<AccessToken>> {
+    const result = await this.getToken();
+    return {
+      token: result,
+      ttlMs: result.expiresOnTimestamp - Date.now()
+    };
+  }
+
+}
+
+
diff --git a/packages/entraid/lib/entra-id-credentials-provider-factory.ts b/packages/entraid/lib/entra-id-credentials-provider-factory.ts
new file mode 100644
index 00000000000..98a3a11078a
--- /dev/null
+++ b/packages/entraid/lib/entra-id-credentials-provider-factory.ts
@@ -0,0 +1,417 @@
+import type { GetTokenOptions, TokenCredential } from '@azure/core-auth';
+import { NetworkError } from '@azure/msal-common';
+import {
+  LogLevel,
+  ManagedIdentityApplication,
+  ManagedIdentityConfiguration,
+  AuthenticationResult,
+  PublicClientApplication,
+  ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo
+} from '@azure/msal-node';
+import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError, BasicAuth } from '@redis/client/dist/lib/authx';
+import { AzureIdentityProvider } from './azure-identity-provider';
+import { AuthenticationResponse, DEFAULT_CREDENTIALS_MAPPER, EntraidCredentialsProvider, OID_CREDENTIALS_MAPPER } from './entraid-credentials-provider';
+import { MSALIdentityProvider } from './msal-identity-provider';
+
+/**
+ * This class is used to create credentials providers for different types of authentication flows.
+ */
+export class EntraIdCredentialsProviderFactory {
+
+  /**
+   * This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities.
+   *
+   * @param params
+   * @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID,
+   * full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication.
+   *
+   */
+  public static createManagedIdentityProvider(
+    params: CredentialParams, userAssignedClientId?: string
+  ): EntraidCredentialsProvider {
+    const config: ManagedIdentityConfiguration = {
+      // For user-assigned identity, include the client ID
+      ...(userAssignedClientId && {
+        managedIdentityIdParams: {
+          userAssignedClientId
+        }
+      }),
+      system: {
+        loggerOptions
+      }
+    };
+
+    const client = new ManagedIdentityApplication(config);
+
+    const idp = new MSALIdentityProvider(
+      () => client.acquireToken({
+        resource: params.scopes?.[0] ?? REDIS_SCOPE,
+        forceRefresh: true
+      }).then(x => x === null ? Promise.reject('Token is null') : x)
+    );
+
+    return new EntraidCredentialsProvider(
+      new TokenManager(idp, params.tokenManagerConfig),
+      idp,
+      {
+        onReAuthenticationError: params.onReAuthenticationError,
+        credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER,
+        onRetryableError: params.onRetryableError
+      }
+    );
+  }
+
+  /**
+   * This method is used to create a credentials provider for system-assigned managed identities.
+   * @param params
+   */
+  static createForSystemAssignedManagedIdentity(
+    params: CredentialParams
+  ): EntraidCredentialsProvider {
+    return this.createManagedIdentityProvider(params);
+  }
+
+  /**
+   * This method is used to create a credentials provider for user-assigned managed identities.
+   * It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration.
+   * @param params
+   */
+  static createForUserAssignedManagedIdentity(
+    params: CredentialParams & { userAssignedClientId: string }
+  ): EntraidCredentialsProvider {
+    return this.createManagedIdentityProvider(params, params.userAssignedClientId);
+  }
+
+  static #createForClientCredentials(
+    authConfig: NodeAuthOptions,
+    params: CredentialParams
+  ): EntraidCredentialsProvider {
+    const config: Configuration = {
+      auth: {
+        ...authConfig,
+        authority: this.getAuthority(params.authorityConfig ?? { type: 'default' })
+      },
+      system: {
+        loggerOptions
+      }
+    };
+
+    const client = new ConfidentialClientApplication(config);
+
+    const idp = new MSALIdentityProvider(
+      () => client.acquireTokenByClientCredential({
+        skipCache: true,
+        scopes: params.scopes ?? [REDIS_SCOPE_DEFAULT]
+      }).then(x => x === null ? Promise.reject('Token is null') : x)
+    );
+
+    return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp,
+      {
+        onReAuthenticationError: params.onReAuthenticationError,
+        credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER,
+        onRetryableError: params.onRetryableError
+      });
+  }
+
+  /**
+   * This method is used to create a credentials provider for service principals using certificate.
+   * @param params
+   */
+  static createForClientCredentialsWithCertificate(
+    params: ClientCredentialsWithCertificateParams
+  ): EntraidCredentialsProvider {
+    return this.#createForClientCredentials(
+      {
+        clientId: params.clientId,
+        clientCertificate: params.certificate
+      },
+      params
+    );
+  }
+
+  /**
+   * This method is used to create a credentials provider for service principals using client secret.
+   * @param params
+   */
+  static createForClientCredentials(
+    params: ClientSecretCredentialsParams
+  ): EntraidCredentialsProvider {
+    return this.#createForClientCredentials(
+      {
+        clientId: params.clientId,
+        clientSecret: params.clientSecret
+      },
+      params
+    );
+  }
+
+  /**
+   * This method is used to create a credentials provider using DefaultAzureCredential.
+   *
+   * The user needs to create a configured instance of DefaultAzureCredential ( or any other class that implements TokenCredential )and pass it to this method.
+   *
+   * The default credentials mapper for this method is OID_CREDENTIALS_MAPPER which extracts the object ID from JWT
+   * encoded token.
+   *
+   * Depending on the actual flow that DefaultAzureCredential uses, the user may need to provide different
+   * credential mapper via the credentialsMapper parameter.
+   *
+   */
+  static createForDefaultAzureCredential(
+    {
+      credential,
+      scopes,
+      options,
+      tokenManagerConfig,
+      onReAuthenticationError,
+      credentialsMapper,
+      onRetryableError
+    }: DefaultAzureCredentialsParams
+  ): EntraidCredentialsProvider {
+
+    const idp = new AzureIdentityProvider(
+      () => credential.getToken(scopes, options).then(x => x === null ? Promise.reject('Token is null') : x)
+    );
+
+    return new EntraidCredentialsProvider(new TokenManager(idp, tokenManagerConfig), idp,
+      {
+        onReAuthenticationError: onReAuthenticationError,
+        credentialsMapper: credentialsMapper ?? OID_CREDENTIALS_MAPPER,
+        onRetryableError: onRetryableError
+      });
+  }
+
+  /**
+   * This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
+   * @param params
+   */
+  static createForAuthorizationCodeWithPKCE(
+    params: AuthCodePKCEParams
+  ): {
+    getPKCECodes: () => Promise<{
+      verifier: string;
+      challenge: string;
+      challengeMethod: string;
+    }>;
+    getAuthCodeUrl: (
+      pkceCodes: { challenge: string; challengeMethod: string }
+    ) => Promise<string>;
+    createCredentialsProvider: (
+      params: PKCEParams
+    ) => EntraidCredentialsProvider;
+  } {
+
+    const requiredScopes = ['user.read', 'offline_access'];
+    const scopes = [...new Set([...(params.scopes || []), ...requiredScopes])];
+
+    const authFlow = AuthCodeFlowHelper.create({
+      clientId: params.clientId,
+      redirectUri: params.redirectUri,
+      scopes: scopes,
+      authorityConfig: params.authorityConfig
+    });
+
+    return {
+      getPKCECodes: AuthCodeFlowHelper.generatePKCE,
+      getAuthCodeUrl: (pkceCodes) => authFlow.getAuthCodeUrl(pkceCodes),
+      createCredentialsProvider: (pkceParams) => {
+
+        // This is used to store the initial credentials account to be used
+        // for silent token acquisition after the initial token acquisition.
+        let initialCredentialsAccount: AccountInfo | null = null;
+
+        const idp = new MSALIdentityProvider(
+          async () => {
+            if (!initialCredentialsAccount) {
+              let authResult = await authFlow.acquireTokenByCode(pkceParams);
+              initialCredentialsAccount = authResult.account;
+              return authResult;
+            } else {
+              return authFlow.client.acquireTokenSilent({
+                forceRefresh: true,
+                account: initialCredentialsAccount,
+                scopes
+              });
+            }
+
+          }
+        );
+        const tm = new TokenManager(idp, params.tokenManagerConfig);
+        return new EntraidCredentialsProvider(tm, idp, {
+          onReAuthenticationError: params.onReAuthenticationError,
+          credentialsMapper: params.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER,
+          onRetryableError: params.onRetryableError
+        });
+      }
+    };
+  }
+
+  static getAuthority(config: AuthorityConfig): string {
+    switch (config.type) {
+      case 'multi-tenant':
+        return `https://login.microsoftonline.com/${config.tenantId}`;
+      case 'custom':
+        return config.authorityUrl;
+      case 'default':
+        return 'https://login.microsoftonline.com/common';
+      default:
+        throw new Error('Invalid authority configuration');
+    }
+  }
+
+}
+
+export const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default';
+export const REDIS_SCOPE = 'https://redis.azure.com'
+
+export type AuthorityConfig =
+  | { type: 'multi-tenant'; tenantId: string }
+  | { type: 'custom'; authorityUrl: string }
+  | { type: 'default' };
+
+export type PKCEParams = {
+  code: string;
+  verifier: string;
+  clientInfo?: string;
+}
+
+export type CredentialParams = {
+  clientId: string;
+  scopes?: string[];
+  authorityConfig?: AuthorityConfig;
+
+  tokenManagerConfig: TokenManagerConfig
+  onReAuthenticationError?: (error: ReAuthenticationError) => void
+  credentialsMapper?: (token: AuthenticationResponse) => BasicAuth
+  onRetryableError?: (error: string) => void
+}
+
+export type DefaultAzureCredentialsParams = {
+  scopes: string | string[],
+  options?: GetTokenOptions,
+  credential: TokenCredential
+  tokenManagerConfig: TokenManagerConfig
+  onReAuthenticationError?: (error: ReAuthenticationError) => void
+  credentialsMapper?: (token: AuthenticationResponse) => BasicAuth
+  onRetryableError?: (error: string) => void
+}
+
+export type AuthCodePKCEParams = CredentialParams & {
+  redirectUri: string;
+};
+
+export type ClientSecretCredentialsParams = CredentialParams & {
+  clientSecret: string;
+};
+
+export type ClientCredentialsWithCertificateParams = CredentialParams & {
+  certificate: {
+    thumbprint: string;
+    privateKey: string;
+    x5c?: string;
+  };
+};
+
+const loggerOptions = {
+  loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) {
+    if (!containsPii) console.log(message);
+  },
+  piiLoggingEnabled: false,
+  logLevel: LogLevel.Error
+}
+
+/**
+ * The most important part of the RetryPolicy is the `isRetryable` function. This function is used to determine if a request should be retried based
+ * on the error returned from the identity provider. The default for is to retry on network errors only.
+ */
+export const DEFAULT_RETRY_POLICY: RetryPolicy = {
+  // currently only retry on network errors
+  isRetryable: (error: unknown) => error instanceof NetworkError,
+  maxAttempts: 10,
+  initialDelayMs: 100,
+  maxDelayMs: 100000,
+  backoffMultiplier: 2,
+  jitterPercentage: 0.1
+
+};
+
+export const DEFAULT_TOKEN_MANAGER_CONFIG: TokenManagerConfig = {
+  retry: DEFAULT_RETRY_POLICY,
+  expirationRefreshRatio: 0.7 // Refresh token when 70% of the token has expired
+}
+
+/**
+ * This class is used to help with the Authorization Code Flow with PKCE.
+ * It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider.
+ */
+export class AuthCodeFlowHelper {
+  private constructor(
+    readonly client: PublicClientApplication,
+    readonly scopes: string[],
+    readonly redirectUri: string
+  ) {}
+
+  async getAuthCodeUrl(pkceCodes: {
+    challenge: string;
+    challengeMethod: string;
+  }): Promise<string> {
+    const authCodeUrlParameters: AuthorizationUrlRequest = {
+      scopes: this.scopes,
+      redirectUri: this.redirectUri,
+      codeChallenge: pkceCodes.challenge,
+      codeChallengeMethod: pkceCodes.challengeMethod
+    };
+
+    return this.client.getAuthCodeUrl(authCodeUrlParameters);
+  }
+
+  async acquireTokenByCode(params: PKCEParams): Promise<AuthenticationResult> {
+    const tokenRequest: AuthorizationCodeRequest = {
+      code: params.code,
+      scopes: this.scopes,
+      redirectUri: this.redirectUri,
+      codeVerifier: params.verifier,
+      clientInfo: params.clientInfo
+    };
+
+    return this.client.acquireTokenByCode(tokenRequest);
+  }
+
+  static async generatePKCE(): Promise<{
+    verifier: string;
+    challenge: string;
+    challengeMethod: string;
+  }> {
+    const cryptoProvider = new CryptoProvider();
+    const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
+    return {
+      verifier,
+      challenge,
+      challengeMethod: 'S256'
+    };
+  }
+
+  static create(params: {
+    clientId: string;
+    redirectUri: string;
+    scopes?: string[];
+    authorityConfig?: AuthorityConfig;
+  }): AuthCodeFlowHelper {
+    const config = {
+      auth: {
+        clientId: params.clientId,
+        authority: EntraIdCredentialsProviderFactory.getAuthority(params.authorityConfig ?? { type: 'default' })
+      },
+      system: {
+        loggerOptions
+      }
+    };
+
+    return new AuthCodeFlowHelper(
+      new PublicClientApplication(config),
+      params.scopes ?? ['user.read'],
+      params.redirectUri
+    );
+  }
+}
+
diff --git a/packages/entraid/lib/entraid-credentials-provider.spec.ts b/packages/entraid/lib/entraid-credentials-provider.spec.ts
new file mode 100644
index 00000000000..1bdf4e9b65f
--- /dev/null
+++ b/packages/entraid/lib/entraid-credentials-provider.spec.ts
@@ -0,0 +1,199 @@
+import { AuthenticationResult } from '@azure/msal-node';
+import { IdentityProvider, TokenManager, TokenResponse, BasicAuth } from '@redis/client/dist/lib/authx';
+import { EntraidCredentialsProvider } from './entraid-credentials-provider';
+import { setTimeout } from 'timers/promises';
+import { strict as assert } from 'node:assert';
+import { GLOBAL, testUtils } from './test-utils'
+
+
+describe('EntraID authentication in cluster mode', () => {
+
+  testUtils.testWithCluster('sendCommand', async cluster => {
+    assert.equal(
+      await cluster.sendCommand(undefined, true, ['PING']),
+      'PONG'
+    );
+  }, GLOBAL.CLUSTERS.PASSWORD_WITH_REPLICAS);
+})
+
+describe('EntraID CredentialsProvider Subscription Behavior', () => {
+
+  it('should properly handle token refresh sequence for multiple subscribers', async () => {
+    const networkDelay = 20;
+    const tokenTTL = 100;
+    const refreshRatio = 0.5; // Refresh at 50% of TTL
+
+    const idp = new SequenceEntraIDProvider(tokenTTL, networkDelay);
+    const tokenManager = new TokenManager<AuthenticationResult>(idp, {
+      expirationRefreshRatio: refreshRatio
+    });
+    const entraid = new EntraidCredentialsProvider(tokenManager, idp);
+
+    // Create two initial subscribers
+    const subscriber1 = new TestSubscriber('subscriber1');
+    const subscriber2 = new TestSubscriber('subscriber2');
+
+    assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions');
+
+    // Start the first two subscriptions almost simultaneously
+    const [sub1Initial, sub2Initial] = await Promise.all([
+      entraid.subscribe(subscriber1),
+      entraid.subscribe(subscriber2)]
+    );
+
+    assertCredentials(sub1Initial[0], 'initial-token', 'Subscriber 1 should receive initial token');
+    assertCredentials(sub2Initial[0], 'initial-token', 'Subscriber 2 should receive initial token');
+
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 2, 'There should be 2 subscriptions');
+
+    // add a third subscriber after a very short delay
+    const subscriber3 = new TestSubscriber('subscriber3');
+    await setTimeout(1);
+    const sub3Initial = await entraid.subscribe(subscriber3)
+
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 3, 'There should be 3 subscriptions');
+
+    // make sure the third subscriber gets the initial token as well
+    assertCredentials(sub3Initial[0], 'initial-token', 'Subscriber 3 should receive initial token');
+
+    // Wait for first refresh (50% of TTL + network delay + small buffer)
+    await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15);
+
+    // All 3 subscribers should receive refresh-token-1
+    assertCredentials(subscriber1.credentials[0], 'refresh-token-1', 'Subscriber 1 should receive first refresh token');
+    assertCredentials(subscriber2.credentials[0], 'refresh-token-1', 'Subscriber 2 should receive first refresh token');
+    assertCredentials(subscriber3.credentials[0], 'refresh-token-1', 'Subscriber 3 should receive first refresh token');
+
+    // Add a late subscriber - should immediately get refresh-token-1
+    const subscriber4 = new TestSubscriber('subscriber4');
+    const sub4Initial = await entraid.subscribe(subscriber4);
+
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 4, 'There should be 4 subscriptions');
+
+    assertCredentials(sub4Initial[0], 'refresh-token-1', 'Late subscriber should receive refresh-token-1');
+
+    // Wait for second refresh
+    await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15);
+
+    assertCredentials(subscriber1.credentials[1], 'refresh-token-2', 'Subscriber 1 should receive second refresh token');
+    assertCredentials(subscriber2.credentials[1], 'refresh-token-2', 'Subscriber 2 should receive second refresh token');
+    assertCredentials(subscriber3.credentials[1], 'refresh-token-2', 'Subscriber 3 should receive second refresh token');
+
+    assertCredentials(subscriber4.credentials[0], 'refresh-token-2', 'Subscriber 4 should receive second refresh token');
+
+    // Verify refreshes happen after minimum expected time
+    const minimumRefreshInterval = tokenTTL * 0.4; // 40% of TTL as safety margin
+
+    verifyRefreshTiming(subscriber1, minimumRefreshInterval);
+    verifyRefreshTiming(subscriber2, minimumRefreshInterval);
+    verifyRefreshTiming(subscriber3, minimumRefreshInterval);
+    verifyRefreshTiming(subscriber4, minimumRefreshInterval);
+
+    // Cleanup
+
+    assert.equal(tokenManager.isRunning(), true);
+    sub1Initial[1].dispose();
+    sub2Initial[1].dispose();
+    sub3Initial[1].dispose();
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 1, 'There should be 1 subscriptions');
+    sub4Initial[1].dispose();
+    assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions');
+    assert.equal(tokenManager.isRunning(), false)
+  });
+
+  const verifyRefreshTiming = (
+    subscriber: TestSubscriber,
+    expectedMinimumInterval: number,
+    message?: string
+  ) => {
+    const intervals = [];
+    for (let i = 1; i < subscriber.timestamps.length; i++) {
+      intervals.push(subscriber.timestamps[i] - subscriber.timestamps[i - 1]);
+    }
+
+    intervals.forEach((interval, index) => {
+      assert.ok(
+        interval > expectedMinimumInterval,
+        message || `Refresh ${index + 1} for ${subscriber.name} should happen after minimum interval of ${expectedMinimumInterval}ms`
+      );
+    });
+  };
+
+  class SequenceEntraIDProvider implements IdentityProvider<AuthenticationResult> {
+    private currentIndex = 0;
+
+    constructor(
+      private readonly tokenTTL: number = 100,
+      private tokenDeliveryDelayMs: number = 0,
+      private readonly tokenSequence: AuthenticationResult[] = [
+        {
+          accessToken: 'initial-token',
+          uniqueId: 'test-user'
+        } as AuthenticationResult,
+        {
+          accessToken: 'refresh-token-1',
+          uniqueId: 'test-user'
+        } as AuthenticationResult,
+        {
+          accessToken: 'refresh-token-2',
+          uniqueId: 'test-user'
+        } as AuthenticationResult
+      ]
+    ) {}
+
+    setTokenDeliveryDelay(delayMs: number): void {
+      this.tokenDeliveryDelayMs = delayMs;
+    }
+
+    async requestToken(): Promise<TokenResponse<AuthenticationResult>> {
+      if (this.tokenDeliveryDelayMs > 0) {
+        await setTimeout(this.tokenDeliveryDelayMs);
+      }
+
+      if (this.currentIndex >= this.tokenSequence.length) {
+        throw new Error('No more tokens in sequence');
+      }
+
+      return {
+        token: this.tokenSequence[this.currentIndex++],
+        ttlMs: this.tokenTTL
+      };
+    }
+  }
+
+  class TestSubscriber {
+    public readonly credentials: Array<BasicAuth> = [];
+    public readonly errors: Error[] = [];
+    public readonly timestamps: number[] = [];
+
+    constructor(public readonly name: string = 'unnamed') {}
+
+    onNext = (creds: BasicAuth) => {
+      this.credentials.push(creds);
+      this.timestamps.push(Date.now());
+    }
+
+    onError = (error: Error) => {
+      this.errors.push(error);
+    }
+  }
+
+  /**
+   * Assert that the actual credentials match the expected token
+   * @param actual
+   * @param expectedToken
+   * @param message
+   */
+  const assertCredentials = (actual: BasicAuth, expectedToken: string, message: string) => {
+    assert.deepEqual(actual, {
+      username: 'test-user',
+      password: expectedToken
+    }, message);
+  };
+});
\ No newline at end of file
diff --git a/packages/entraid/lib/entraid-credentials-provider.ts b/packages/entraid/lib/entraid-credentials-provider.ts
new file mode 100644
index 00000000000..465c9e8a975
--- /dev/null
+++ b/packages/entraid/lib/entraid-credentials-provider.ts
@@ -0,0 +1,195 @@
+import { AuthenticationResult } from '@azure/msal-common/node';
+import { AccessToken } from '@azure/core-auth';
+import {
+  BasicAuth, StreamingCredentialsProvider, IdentityProvider, TokenManager,
+  ReAuthenticationError, StreamingCredentialsListener, IDPError, Token, Disposable
+} from '@redis/client/dist/lib/authx';
+
+/**
+ * A streaming credentials provider that uses the Entraid identity provider to provide credentials.
+ * Please use one of the factory functions in `entraid-credetfactories.ts` to create an instance of this class for the different
+ * type of authentication flows.
+ */
+
+export type AuthenticationResponse = AuthenticationResult | AccessToken
+
+export class EntraidCredentialsProvider implements StreamingCredentialsProvider {
+  readonly type = 'streaming-credentials-provider';
+
+  readonly #listeners: Set<StreamingCredentialsListener<BasicAuth>> = new Set();
+
+  #tokenManagerDisposable: Disposable | null = null;
+  #isStarting: boolean = false;
+
+  #pendingSubscribers: Array<{
+    resolve: (value: [BasicAuth, Disposable]) => void;
+    reject: (error: Error) => void;
+    pendingListener: StreamingCredentialsListener<BasicAuth>;
+  }> = [];
+
+  constructor(
+    public readonly tokenManager: TokenManager<AuthenticationResponse>,
+    public readonly idp: IdentityProvider<AuthenticationResponse>,
+    private readonly options: {
+      onReAuthenticationError?: (error: ReAuthenticationError) => void;
+      credentialsMapper?: (token: AuthenticationResponse) => BasicAuth;
+      onRetryableError?: (error: string) => void;
+    } = {}
+  ) {
+    this.onReAuthenticationError = options.onReAuthenticationError ?? DEFAULT_ERROR_HANDLER;
+    this.#credentialsMapper = options.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER;
+  }
+
+  async subscribe(
+    listener: StreamingCredentialsListener<BasicAuth>
+  ): Promise<[BasicAuth, Disposable]> {
+
+    const currentToken = this.tokenManager.getCurrentToken();
+
+    if (currentToken) {
+      return [this.#credentialsMapper(currentToken.value), this.#createDisposable(listener)];
+    }
+
+    if (this.#isStarting) {
+      return new Promise((resolve, reject) => {
+        this.#pendingSubscribers.push({ resolve, reject, pendingListener: listener });
+      });
+    }
+
+    this.#isStarting = true;
+    try {
+      const initialToken = await this.#startTokenManagerAndObtainInitialToken();
+
+      this.#pendingSubscribers.forEach(({ resolve, pendingListener }) => {
+        resolve([this.#credentialsMapper(initialToken.value), this.#createDisposable(pendingListener)]);
+      });
+      this.#pendingSubscribers = [];
+
+      return [this.#credentialsMapper(initialToken.value), this.#createDisposable(listener)];
+    } finally {
+      this.#isStarting = false;
+    }
+  }
+
+  onReAuthenticationError: (error: ReAuthenticationError) => void;
+
+  #credentialsMapper: (token: AuthenticationResponse) => BasicAuth;
+
+  #createTokenManagerListener(subscribers: Set<StreamingCredentialsListener<BasicAuth>>) {
+    return {
+      onError: (error: IDPError): void => {
+        if (!error.isRetryable) {
+          subscribers.forEach(listener => listener.onError(error));
+        } else {
+          this.options.onRetryableError?.(error.message);
+        }
+      },
+      onNext: (token: { value: AuthenticationResult | AccessToken }): void => {
+        const credentials = this.#credentialsMapper(token.value);
+        subscribers.forEach(listener => listener.onNext(credentials));
+      }
+    };
+  }
+
+  #createDisposable(listener: StreamingCredentialsListener<BasicAuth>): Disposable {
+    this.#listeners.add(listener);
+
+    return {
+      dispose: () => {
+        this.#listeners.delete(listener);
+        if (this.#listeners.size === 0 && this.#tokenManagerDisposable) {
+          this.#tokenManagerDisposable.dispose();
+          this.#tokenManagerDisposable = null;
+        }
+      }
+    };
+  }
+
+  async #startTokenManagerAndObtainInitialToken(): Promise<Token<AuthenticationResponse>> {
+    const { ttlMs, token: initialToken } = await this.idp.requestToken();
+
+    const token = this.tokenManager.wrapAndSetCurrentToken(initialToken, ttlMs);
+    this.#tokenManagerDisposable = this.tokenManager.start(
+      this.#createTokenManagerListener(this.#listeners),
+      this.tokenManager.calculateRefreshTime(token)
+    );
+    return token;
+  }
+
+  public hasActiveSubscriptions(): boolean {
+    return this.#tokenManagerDisposable !== null && this.#listeners.size > 0;
+  }
+
+  public getSubscriptionsCount(): number {
+    return this.#listeners.size;
+  }
+
+  public getTokenManager() {
+    return this.tokenManager;
+  }
+
+  public getCurrentCredentials(): BasicAuth | null {
+    const currentToken = this.tokenManager.getCurrentToken();
+    return currentToken ? this.#credentialsMapper(currentToken.value) : null;
+  }
+
+}
+
+export const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResponse): BasicAuth => {
+  if (isAuthenticationResult(token)) {
+    return {
+      username: token.uniqueId,
+      password: token.accessToken
+    }
+  } else {
+    return OID_CREDENTIALS_MAPPER(token)
+  }
+};
+
+const DEFAULT_ERROR_HANDLER = (error: ReAuthenticationError) =>
+  console.error('ReAuthenticationError', error);
+
+export const OID_CREDENTIALS_MAPPER = (token: (AuthenticationResult | AccessToken)) => {
+
+  if (isAuthenticationResult(token)) {
+    // Client credentials flow is app-only authentication (no user context),
+    // so only access token is provided without user-specific claims (uniqueId, idToken, ...)
+    // this means that we need to extract the oid from the access token manually
+    const accessToken = JSON.parse(Buffer.from(token.accessToken.split('.')[1], 'base64').toString());
+
+    return ({
+      username: accessToken.oid,
+      password: token.accessToken
+    })
+  } else {
+    const accessToken = JSON.parse(Buffer.from(token.token.split('.')[1], 'base64').toString());
+
+    return ({
+      username: accessToken.oid,
+      password: token.token
+    })
+  }
+
+}
+
+/**
+ * Type guard to check if a token is an MSAL AuthenticationResult
+ * 
+ * @param auth - The token to check
+ * @returns true if the token is an AuthenticationResult
+ */
+export function isAuthenticationResult(auth: AuthenticationResult | AccessToken): auth is AuthenticationResult {
+  return typeof (auth as AuthenticationResult).accessToken === 'string' && 
+         !('token' in auth)
+}
+
+/**
+ * Type guard to check if a token is an Azure Identity AccessToken
+ * 
+ * @param auth - The token to check
+ * @returns true if the token is an AccessToken
+ */
+export function isAccessToken(auth: AuthenticationResult | AccessToken): auth is AccessToken {
+  return typeof (auth as AccessToken).token === 'string' && 
+         !('accessToken' in auth);
+}
\ No newline at end of file
diff --git a/packages/entraid/lib/index.ts b/packages/entraid/lib/index.ts
new file mode 100644
index 00000000000..4873c9935c5
--- /dev/null
+++ b/packages/entraid/lib/index.ts
@@ -0,0 +1,3 @@
+export * from './entra-id-credentials-provider-factory';
+export * from './entraid-credentials-provider';
+export * from './msal-identity-provider';
\ No newline at end of file
diff --git a/packages/entraid/lib/msal-identity-provider.ts b/packages/entraid/lib/msal-identity-provider.ts
new file mode 100644
index 00000000000..0f15e01fcdc
--- /dev/null
+++ b/packages/entraid/lib/msal-identity-provider.ts
@@ -0,0 +1,25 @@
+import {
+  AuthenticationResult
+} from '@azure/msal-node';
+import { IdentityProvider, TokenResponse } from '@redis/client/dist/lib/authx';
+
+export class MSALIdentityProvider implements IdentityProvider<AuthenticationResult> {
+  private readonly getToken: () => Promise<AuthenticationResult>;
+
+  constructor(getToken: () => Promise<AuthenticationResult>) {
+    this.getToken = getToken;
+  }
+
+  async requestToken(): Promise<TokenResponse<AuthenticationResult>> {
+    const result = await this.getToken();
+
+    if (!result?.accessToken || !result?.expiresOn) {
+      throw new Error('Invalid token response');
+    }
+    return {
+      token: result,
+      ttlMs: result.expiresOn.getTime() - Date.now()
+    };
+  }
+
+}
diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts
new file mode 100644
index 00000000000..e5d977a6b40
--- /dev/null
+++ b/packages/entraid/lib/test-utils.ts
@@ -0,0 +1,46 @@
+import { AuthenticationResult } from '@azure/msal-node';
+import { IdentityProvider, StreamingCredentialsProvider, TokenManager, TokenResponse } from '@redis/client/dist/lib/authx';
+import TestUtils from '@redis/test-utils';
+import { EntraidCredentialsProvider } from './entraid-credentials-provider';
+
+export const testUtils = TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '8.2-rc1'
+});
+
+const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ?
+  ['--enable-debug-command', 'yes'] :
+  [];
+
+const idp: IdentityProvider<AuthenticationResult> = {
+  requestToken(): Promise<TokenResponse<AuthenticationResult>> {
+    // @ts-ignore
+    return Promise.resolve({
+      ttlMs: 100000,
+      token: {
+        accessToken: 'password'
+      }
+    })
+  }
+}
+
+const tokenManager = new TokenManager<AuthenticationResult>(idp, { expirationRefreshRatio: 0.8 });
+const entraIdCredentialsProvider: StreamingCredentialsProvider = new EntraidCredentialsProvider(tokenManager, idp)
+
+const PASSWORD_WITH_REPLICAS = {
+  serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+  numberOfMasters: 2,
+  numberOfReplicas: 1,
+  clusterConfiguration: {
+    defaults: {
+      credentialsProvider: entraIdCredentialsProvider
+    }
+  }
+}
+
+export const GLOBAL = {
+  CLUSTERS: {
+    PASSWORD_WITH_REPLICAS
+  }
+}
diff --git a/packages/entraid/package.json b/packages/entraid/package.json
new file mode 100644
index 00000000000..cb57d9074c3
--- /dev/null
+++ b/packages/entraid/package.json
@@ -0,0 +1,50 @@
+{
+  "name": "@redis/entraid",
+  "version": "5.7.0",
+  "license": "MIT",
+  "main": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
+  ],
+  "scripts": {
+    "clean": "rimraf dist",
+    "build": "npm run clean && tsc",
+    "start:auth-pkce": "tsx --tsconfig tsconfig.samples.json ./samples/auth-code-pkce/index.ts",
+    "start:interactive-browser": "tsx --tsconfig tsconfig.samples.json ./samples/interactive-browser/index.ts",
+    "test-integration": "mocha -r tsx --tsconfig tsconfig.integration-tests.json './integration-tests/**/*.spec.ts'",
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
+    "release": "release-it"
+  },
+  "dependencies": {
+    "@azure/identity": "^4.7.0",
+    "@azure/msal-node": "^2.16.1"
+  },
+  "peerDependencies": {
+    "@redis/client": "^5.7.0"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.21",
+    "@types/express-session": "^1.18.0",
+    "@types/node": "^22.9.0",
+    "dotenv": "^16.3.1",
+    "express": "^4.21.1",
+    "express-session": "^1.18.1",
+    "@redis/test-utils": "*"
+  },
+  "engines": {
+    "node": ">= 18"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/redis/node-redis.git"
+  },
+  "bugs": {
+    "url": "https://github.com/redis/node-redis/issues"
+  },
+  "homepage": "https://github.com/redis/node-redis/tree/master/packages/entraid",
+  "keywords": [
+    "redis"
+  ]
+}
diff --git a/packages/entraid/samples/auth-code-pkce/index.ts b/packages/entraid/samples/auth-code-pkce/index.ts
new file mode 100644
index 00000000000..25429269c44
--- /dev/null
+++ b/packages/entraid/samples/auth-code-pkce/index.ts
@@ -0,0 +1,153 @@
+import express, { Request, Response } from 'express';
+import session from 'express-session';
+import dotenv from 'dotenv';
+import { DEFAULT_TOKEN_MANAGER_CONFIG, EntraIdCredentialsProviderFactory } from '../../lib/entra-id-credentials-provider-factory';
+
+dotenv.config();
+
+if (!process.env.SESSION_SECRET) {
+  throw new Error('SESSION_SECRET environment variable must be set');
+}
+
+interface PKCESession extends session.Session {
+  pkceCodes?: {
+    verifier: string;
+    challenge: string;
+    challengeMethod: string;
+  };
+}
+
+interface AuthRequest extends Request {
+  session: PKCESession;
+}
+
+const app = express();
+
+const sessionConfig = {
+  secret: process.env.SESSION_SECRET,
+  resave: false,
+  saveUninitialized: false,
+  cookie: {
+    secure: process.env.NODE_ENV === 'production', // Only use secure in production
+    httpOnly: true,
+    sameSite: 'lax',
+    maxAge: 3600000 // 1 hour
+  }
+} as const;
+
+app.use(session(sessionConfig));
+
+if (!process.env.MSAL_CLIENT_ID || !process.env.MSAL_TENANT_ID) {
+  throw new Error('MSAL_CLIENT_ID and MSAL_TENANT_ID environment variables must be set');
+}
+
+// Initialize MSAL provider with authorization code PKCE flow
+const {
+  getPKCECodes,
+  createCredentialsProvider,
+  getAuthCodeUrl
+} = EntraIdCredentialsProviderFactory.createForAuthorizationCodeWithPKCE({
+  clientId: process.env.MSAL_CLIENT_ID,
+  redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/redirect',
+  authorityConfig: { type: 'multi-tenant', tenantId: process.env.MSAL_TENANT_ID },
+  tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG
+});
+
+app.get('/login', async (req: AuthRequest, res: Response) => {
+  try {
+    // Generate PKCE Codes before starting the authorization flow
+    const pkceCodes = await getPKCECodes();
+
+    // Store PKCE codes in session
+    req.session.pkceCodes = pkceCodes
+
+    await new Promise<void>((resolve, reject) => {
+      req.session.save((err) => {
+        if (err) reject(err);
+        else resolve();
+      });
+    });
+
+    const authUrl = await getAuthCodeUrl({
+      challenge: pkceCodes.challenge,
+      challengeMethod: pkceCodes.challengeMethod
+    });
+
+    res.redirect(authUrl);
+  } catch (error) {
+    console.error('Login flow failed:', error);
+    res.status(500).send('Authentication failed');
+  }
+});
+
+app.get('/redirect', async (req: AuthRequest, res: Response) => {
+  try {
+
+    // The authorization code is in req.query.code
+    const { code, client_info } = req.query;
+    const { pkceCodes } = req.session;
+
+    if (!pkceCodes) {
+      console.error('Session state:', {
+        hasSession: !!req.session,
+        sessionID: req.sessionID,
+        pkceCodes: req.session.pkceCodes
+      });
+      return res.status(400).send('PKCE codes not found in session');
+    }
+
+    // Check both possible error scenarios
+    if (req.query.error) {
+      console.error('OAuth error:', req.query.error, req.query.error_description);
+      return res.status(400).send(`OAuth error: ${req.query.error_description || req.query.error}`);
+    }
+
+    if (!code) {
+      console.error('Missing authorization code. Query parameters received:', req.query);
+      return res.status(400).send('Authorization code not found in request. Query params: ' + JSON.stringify(req.query));
+    }
+
+    // Configure with the received code
+    const entraidCredentialsProvider = createCredentialsProvider(
+      {
+        code: code as string,
+        verifier: pkceCodes.verifier,
+        clientInfo: client_info as string | undefined
+      },
+    );
+
+    const initialCredentials = entraidCredentialsProvider.subscribe({
+      onNext: (token) => {
+        console.log('Token acquired:', token);
+      },
+      onError: (error) => {
+        console.error('Token acquisition failed:', error);
+      }
+    });
+
+    const [credentials] = await initialCredentials;
+
+    console.log('Credentials acquired:', credentials)
+
+    // Clear sensitive data
+    delete req.session.pkceCodes;
+
+    await new Promise<void>((resolve, reject) => {
+      req.session.save((err) => {
+        if (err) reject(err);
+        else resolve();
+      });
+    });
+
+    res.json({ message: 'Authentication successful' });
+  } catch (error) {
+    console.error('Token acquisition failed:', error);
+    res.status(500).send('Failed to acquire token');
+  }
+});
+
+const PORT = process.env.PORT || 3000;
+app.listen(PORT, () => {
+  console.log(`Server running on port ${PORT}`);
+  console.log(`Login URL: http://localhost:${PORT}/login`);
+});
\ No newline at end of file
diff --git a/packages/entraid/samples/interactive-browser/index.ts b/packages/entraid/samples/interactive-browser/index.ts
new file mode 100644
index 00000000000..f458ad9e190
--- /dev/null
+++ b/packages/entraid/samples/interactive-browser/index.ts
@@ -0,0 +1,111 @@
+import express, { Request, Response } from 'express';
+import session from 'express-session';
+import dotenv from 'dotenv';
+import { DEFAULT_TOKEN_MANAGER_CONFIG, EntraIdCredentialsProviderFactory } from '../../lib/entra-id-credentials-provider-factory';
+import { InteractiveBrowserCredential } from '@azure/identity';
+
+dotenv.config();
+
+if (!process.env.SESSION_SECRET) {
+  throw new Error('SESSION_SECRET environment variable must be set');
+}
+
+const app = express();
+
+const sessionConfig = {
+  secret: process.env.SESSION_SECRET,
+  resave: false,
+  saveUninitialized: false,
+  cookie: {
+    secure: process.env.NODE_ENV === 'production', // Only use secure in production
+    httpOnly: true,
+    sameSite: 'lax',
+    maxAge: 3600000 // 1 hour
+  }
+} as const;
+
+app.use(session(sessionConfig));
+
+if (!process.env.MSAL_CLIENT_ID || !process.env.MSAL_TENANT_ID) {
+  throw new Error('MSAL_CLIENT_ID and MSAL_TENANT_ID environment variables must be set');
+}
+
+
+app.get('/login', async (req: Request, res: Response) => {
+  try {
+    // Create an instance of InteractiveBrowserCredential
+    const credential = new InteractiveBrowserCredential({
+      clientId: process.env.MSAL_CLIENT_ID!,
+      tenantId: process.env.MSAL_TENANT_ID!,
+      loginStyle: 'popup',
+      redirectUri: 'http://localhost:3000/redirect'
+    });
+
+    // Create Redis client using the EntraID credentials provider
+    const entraidCredentialsProvider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
+      credential,
+      scopes: ['user.read'],
+      tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG
+    });
+
+    // Subscribe to credentials updates
+    const initialCredentials = entraidCredentialsProvider.subscribe({
+      onNext: (token) => {
+        // Never log the full token in production
+        console.log('Token acquired successfully');
+        console.log('Username:', token.username);
+ 
+      },
+      onError: (error) => {
+        console.error('Token acquisition failed:', error);
+      }
+    });
+
+    // Wait for the initial credentials
+    const [credentials] = await initialCredentials;
+
+    // Return success response
+    res.json({
+      status: 'success',
+      message: 'Authentication successful',
+      credentials: {
+        username: credentials.username,
+        password: credentials.password
+      }
+    });
+  } catch (error) {
+    console.error('Authentication failed:', error);
+    res.status(500).json({
+      status: 'error',
+      message: 'Authentication failed',
+      error: error instanceof Error ? error.message : String(error)
+    });
+  }
+});
+
+// Create a simple status page
+app.get('/', (req: Request, res: Response) => {
+  res.send(`
+    <html>
+      <head>
+        <title>Interactive Browser Credential Demo</title>
+        <style>
+          body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
+          .button { display: inline-block; padding: 10px 20px; background: #0078d4; color: white; text-decoration: none; border-radius: 4px; }
+        </style>
+      </head>
+      <body>
+        <h1>Interactive Browser Credential Demo</h1>
+        <p>This example demonstrates using the InteractiveBrowserCredential from @azure/identity to authenticate with Microsoft Entra ID.</p>
+        <p>When you click the button below, you'll be redirected to the Microsoft login page.</p>
+        <a href="/login" class="button">Login with Microsoft</a>
+      </body>
+    </html>
+  `);
+});
+
+const PORT = process.env.PORT || 3000;
+app.listen(PORT, () => {
+  console.log(`Server running on port ${PORT}`);
+  console.log(`Open http://localhost:${PORT} in your browser to start`);
+});
diff --git a/packages/entraid/tsconfig.integration-tests.json b/packages/entraid/tsconfig.integration-tests.json
new file mode 100644
index 00000000000..5d15f4f2753
--- /dev/null
+++ b/packages/entraid/tsconfig.integration-tests.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "include": [
+    "./integration-tests/**/*.ts",
+    "./lib/**/*.ts"
+  ],
+  "compilerOptions": {
+    "noEmit": true
+  },
+}
\ No newline at end of file
diff --git a/packages/graph/tsconfig.json b/packages/entraid/tsconfig.json
similarity index 64%
rename from packages/graph/tsconfig.json
rename to packages/entraid/tsconfig.json
index 9d17cb63371..47100f5b87d 100644
--- a/packages/graph/tsconfig.json
+++ b/packages/entraid/tsconfig.json
@@ -4,17 +4,18 @@
     "outDir": "./dist"
   },
   "include": [
-    "./lib/**/*.ts"
+    "./lib/**/*.ts",
+    "./index.ts"
   ],
   "exclude": [
-    "./lib/test-utils.ts",
-    "./lib/**/*.spec.ts"
+    "./lib/**/*.spec.ts",
+    "./lib/test-util.ts",
   ],
   "typedocOptions": {
     "entryPoints": [
       "./lib"
     ],
     "entryPointStrategy": "expand",
-    "out": "../../documentation/graph"
+    "out": "../../documentation/entraid"
   }
 }
diff --git a/packages/entraid/tsconfig.samples.json b/packages/entraid/tsconfig.samples.json
new file mode 100644
index 00000000000..0eb936369ff
--- /dev/null
+++ b/packages/entraid/tsconfig.samples.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "include": [
+    "./samples/**/*.ts",
+    "./lib/**/*.ts"
+  ],
+  "compilerOptions": {
+    "noEmit": true
+  }
+}
\ No newline at end of file
diff --git a/packages/graph/.nycrc.json b/packages/graph/.nycrc.json
deleted file mode 100644
index 367a89ad32c..00000000000
--- a/packages/graph/.nycrc.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "extends": "@istanbuljs/nyc-config-typescript",
-  "exclude": ["dist", "**/*.spec.ts", "lib/test-utils.ts"]
-}
diff --git a/packages/graph/.release-it.json b/packages/graph/.release-it.json
deleted file mode 100644
index 530d8f355d4..00000000000
--- a/packages/graph/.release-it.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "git": {
-    "tagName": "graph@${version}",
-    "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
-  },
-  "npm": {
-    "publishArgs": ["--access", "public"]
-  }
-}
diff --git a/packages/graph/README.md b/packages/graph/README.md
deleted file mode 100644
index 4c712bfd820..00000000000
--- a/packages/graph/README.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# @redis/graph
-
-Example usage:
-```javascript
-import { createClient, Graph } from 'redis';
-
-const client = createClient();
-client.on('error', (err) => console.log('Redis Client Error', err));
-
-await client.connect();
-
-const graph = new Graph(client, 'graph');
-
-await graph.query(
-  'CREATE (:Rider { name: $riderName })-[:rides]->(:Team { name: $teamName })',
-  {
-    params: {
-      riderName: 'Buzz Aldrin',
-      teamName: 'Apollo'
-    }
-  }
-);
-
-const result = await graph.roQuery(
-  'MATCH (r:Rider)-[:rides]->(t:Team { name: $name }) RETURN r.name AS name',
-  {
-    params: {
-      name: 'Apollo'
-    }
-  }
-);
-
-console.log(result.data); // [{ name: 'Buzz Aldrin' }]
-```
diff --git a/packages/graph/lib/commands/CONFIG_GET.spec.ts b/packages/graph/lib/commands/CONFIG_GET.spec.ts
deleted file mode 100644
index 6e1fa74e219..00000000000
--- a/packages/graph/lib/commands/CONFIG_GET.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CONFIG_GET';
-
-describe('CONFIG GET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('TIMEOUT'),
-            ['GRAPH.CONFIG', 'GET', 'TIMEOUT']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.configGet', async client => {
-        assert.deepEqual(
-            await client.graph.configGet('TIMEOUT'),
-            [
-                'TIMEOUT',
-                0
-            ]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/CONFIG_GET.ts b/packages/graph/lib/commands/CONFIG_GET.ts
deleted file mode 100644
index ce80a1148ed..00000000000
--- a/packages/graph/lib/commands/CONFIG_GET.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const IS_READ_ONLY = true;
-
-export function transformArguments(configKey: string): Array<string> {
-    return ['GRAPH.CONFIG', 'GET', configKey];
-}
-
-type ConfigItem = [
-    configKey: string,
-    value: number
-];
-
-export declare function transformReply(): ConfigItem | Array<ConfigItem>;
diff --git a/packages/graph/lib/commands/CONFIG_SET.spec.ts b/packages/graph/lib/commands/CONFIG_SET.spec.ts
deleted file mode 100644
index 51dce0a8cd9..00000000000
--- a/packages/graph/lib/commands/CONFIG_SET.spec.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CONFIG_SET';
-
-describe('CONFIG SET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('TIMEOUT', 0),
-            ['GRAPH.CONFIG', 'SET', 'TIMEOUT', '0']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.configSet', async client => {
-        assert.equal(
-            await client.graph.configSet('TIMEOUT', 0),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts
deleted file mode 100644
index ac81449ad15..00000000000
--- a/packages/graph/lib/commands/CONFIG_SET.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export function transformArguments(configKey: string, value: number): Array<string> {
-    return [
-        'GRAPH.CONFIG',
-        'SET',
-        configKey,
-        value.toString()
-    ];
-}
-
-export declare function transformReply(): 'OK';
diff --git a/packages/graph/lib/commands/DELETE.spec.ts b/packages/graph/lib/commands/DELETE.spec.ts
deleted file mode 100644
index e51ac2bfab8..00000000000
--- a/packages/graph/lib/commands/DELETE.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DELETE';
-
-describe('', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['GRAPH.DELETE', 'key']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.delete', async client => {
-        await client.graph.query('key', 'RETURN 1');
-
-        assert.equal(
-            typeof await client.graph.delete('key'),
-            'string'
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/DELETE.ts b/packages/graph/lib/commands/DELETE.ts
deleted file mode 100644
index 240708143c6..00000000000
--- a/packages/graph/lib/commands/DELETE.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: string): Array<string> {
-    return ['GRAPH.DELETE', key];
-}
-
-export declare function transformReply(): string;
diff --git a/packages/graph/lib/commands/EXPLAIN.spec.ts b/packages/graph/lib/commands/EXPLAIN.spec.ts
deleted file mode 100644
index 86d89b212cb..00000000000
--- a/packages/graph/lib/commands/EXPLAIN.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './EXPLAIN';
-
-describe('EXPLAIN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'RETURN 0'),
-            ['GRAPH.EXPLAIN', 'key', 'RETURN 0']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.explain', async client => {
-        const [, reply] = await Promise.all([
-            client.graph.query('key', 'RETURN 0'), // make sure to create a graph first
-            client.graph.explain('key', 'RETURN 0')
-        ]);
-        assert.ok(Array.isArray(reply));
-        assert.ok(!reply.find(x => typeof x !== 'string'));
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/EXPLAIN.ts b/packages/graph/lib/commands/EXPLAIN.ts
deleted file mode 100644
index ebea9ca900d..00000000000
--- a/packages/graph/lib/commands/EXPLAIN.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, query: string): Array<string> {
-    return ['GRAPH.EXPLAIN', key, query];
-}
-
-export declare function transformReply(): Array<string>;
diff --git a/packages/graph/lib/commands/LIST.spec.ts b/packages/graph/lib/commands/LIST.spec.ts
deleted file mode 100644
index d4fab0358b9..00000000000
--- a/packages/graph/lib/commands/LIST.spec.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './LIST';
-
-describe('LIST', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['GRAPH.LIST']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.list', async client => {
-        assert.deepEqual(
-            await client.graph.list(),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/LIST.ts b/packages/graph/lib/commands/LIST.ts
deleted file mode 100644
index 1939d43d889..00000000000
--- a/packages/graph/lib/commands/LIST.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const IS_READ_ONLY = true;
-
-export function transformArguments(): Array<string> {
-    return ['GRAPH.LIST'];
-}
-
-export declare function transformReply(): Array<string>;
diff --git a/packages/graph/lib/commands/PROFILE.spec.ts b/packages/graph/lib/commands/PROFILE.spec.ts
deleted file mode 100644
index 80857eb0ab9..00000000000
--- a/packages/graph/lib/commands/PROFILE.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './PROFILE';
-
-describe('PROFILE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'RETURN 0'),
-            ['GRAPH.PROFILE', 'key', 'RETURN 0']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.profile', async client => {
-        const reply = await client.graph.profile('key', 'RETURN 0');
-        assert.ok(Array.isArray(reply));
-        assert.ok(!reply.find(x => typeof x !== 'string'));
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/PROFILE.ts b/packages/graph/lib/commands/PROFILE.ts
deleted file mode 100644
index c964452f497..00000000000
--- a/packages/graph/lib/commands/PROFILE.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, query: string): Array<string> {
-    return ['GRAPH.PROFILE', key, query];
-}
-
-export declare function transformReply(): Array<string>;
diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts
deleted file mode 100644
index c8a9a20372b..00000000000
--- a/packages/graph/lib/commands/QUERY.spec.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './QUERY';
-
-describe('QUERY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'query'),
-            ['GRAPH.QUERY', 'key', 'query']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.query', async client => {
-        const { data } = await client.graph.query('key', 'RETURN 0');
-        assert.deepEqual(data, [[0]]);
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts
deleted file mode 100644
index 741cc6a3601..00000000000
--- a/packages/graph/lib/commands/QUERY.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands/index';
-import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(
-    graph: RedisCommandArgument,
-    query: RedisCommandArgument,
-    options?: QueryOptionsBackwardCompatible,
-    compact?: boolean
-): RedisCommandArguments {
-    return pushQueryArguments(
-        ['GRAPH.QUERY'],
-        graph,
-        query,
-        options,
-        compact
-    );
-}
-
-type Headers = Array<string>;
-
-type Data = Array<string | number | null | Data>;
-
-type Metadata = Array<string>;
-
-type QueryRawReply = [
-    headers: Headers,
-    data: Data,
-    metadata: Metadata
-] | [
-    metadata: Metadata
-];
-
-export type QueryReply = {
-    headers: undefined;
-    data: undefined;
-    metadata: Metadata;
-} | {
-    headers: Headers;
-    data: Data;
-    metadata: Metadata;
-};
-
-export function transformReply(reply: QueryRawReply): QueryReply {
-    return reply.length === 1 ? {
-        headers: undefined,
-        data: undefined,
-        metadata: reply[0]
-    } : {
-        headers: reply[0],
-        data: reply[1],
-        metadata: reply[2]
-    };
-}
diff --git a/packages/graph/lib/commands/RO_QUERY.spec.ts b/packages/graph/lib/commands/RO_QUERY.spec.ts
deleted file mode 100644
index 1d76b1bd652..00000000000
--- a/packages/graph/lib/commands/RO_QUERY.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RO_QUERY';
-
-describe('RO_QUERY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'query'),
-            ['GRAPH.RO_QUERY', 'key', 'query']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.roQuery', async client => {
-        const [, { data }] = await Promise.all([
-            client.graph.query('key', 'RETURN 0'), // make sure to create a graph first
-            client.graph.roQuery('key', 'RETURN 0')
-        ]);
-        assert.deepEqual(data, [[0]]);
-    }, GLOBAL.SERVERS.OPEN);
-});
\ No newline at end of file
diff --git a/packages/graph/lib/commands/RO_QUERY.ts b/packages/graph/lib/commands/RO_QUERY.ts
deleted file mode 100644
index d4dda9dee27..00000000000
--- a/packages/graph/lib/commands/RO_QUERY.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.';
-
-export { FIRST_KEY_INDEX } from './QUERY';
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    graph: RedisCommandArgument,
-    query: RedisCommandArgument,
-    options?: QueryOptionsBackwardCompatible,
-    compact?: boolean
-): RedisCommandArguments {
-    return pushQueryArguments(
-        ['GRAPH.RO_QUERY'],
-        graph,
-        query,
-        options,
-        compact
-    );
-}
-
-export { transformReply } from './QUERY';
diff --git a/packages/graph/lib/commands/SLOWLOG.spec.ts b/packages/graph/lib/commands/SLOWLOG.spec.ts
deleted file mode 100644
index e3083b994d6..00000000000
--- a/packages/graph/lib/commands/SLOWLOG.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SLOWLOG';
-
-describe('SLOWLOG', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['GRAPH.SLOWLOG', 'key']
-        );
-    });
-
-    testUtils.testWithClient('client.graph.slowLog', async client => {
-        await client.graph.query('key', 'RETURN 1');
-        const reply = await client.graph.slowLog('key');
-		assert.equal(reply.length, 1);
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts
deleted file mode 100644
index 6ae87af89bf..00000000000
--- a/packages/graph/lib/commands/SLOWLOG.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export const IS_READ_ONLY = true;
-
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: string) {
-    return ['GRAPH.SLOWLOG', key];
-}
-
-type SlowLogRawReply = Array<[
-    timestamp: string,
-    command: string,
-    query: string,
-    took: string
-]>;
-
-type SlowLogReply = Array<{
-    timestamp: Date;
-    command: string;
-    query: string;
-    took: number;
-}>;
-
-export function transformReply(logs: SlowLogRawReply): SlowLogReply {
-    return logs.map(([timestamp, command, query, took]) => ({
-        timestamp: new Date(Number(timestamp) * 1000),
-        command,
-        query,
-        took: Number(took)
-    }));
-}
diff --git a/packages/graph/lib/commands/index.spec.ts b/packages/graph/lib/commands/index.spec.ts
deleted file mode 100644
index a688c49dd39..00000000000
--- a/packages/graph/lib/commands/index.spec.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { strict as assert } from 'assert';
-import { pushQueryArguments } from '.';
-
-describe('pushQueryArguments', () => {
-  it('simple', () => {
-    assert.deepEqual(
-      pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query'),
-      ['GRAPH.QUERY', 'graph', 'query']
-    );
-  });
-
-  describe('params', () => {
-    it('all types', () => {
-      assert.deepEqual(
-        pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', {
-          params: {
-            null: null,
-            string: '"\\',
-            number: 0,
-            boolean: false,
-            array: [0],
-            object: {a: 0}
-          }
-        }),
-        ['GRAPH.QUERY', 'graph', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query']
-      );
-    });
-
-    it('TypeError', () => {
-      assert.throws(() => {
-        pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', {
-          params: {
-            a: undefined as any
-          }
-        })
-      }, TypeError);
-    });
-  });
-
-  it('TIMEOUT backward compatible', () => {
-    assert.deepEqual(
-      pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', 1),
-      ['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1']
-    );
-  });
-
-  it('TIMEOUT', () => {
-    assert.deepEqual(
-      pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', {
-        TIMEOUT: 1
-      }),
-      ['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1']
-    );
-  });
-
-  it('compact', () => {
-    assert.deepEqual(
-      pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', undefined, true),
-      ['GRAPH.QUERY', 'graph', 'query', '--compact']
-    );
-  });
-});
diff --git a/packages/graph/lib/commands/index.ts b/packages/graph/lib/commands/index.ts
deleted file mode 100644
index 2acf9089ee6..00000000000
--- a/packages/graph/lib/commands/index.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import * as CONFIG_GET from './CONFIG_GET';
-import * as CONFIG_SET from './CONFIG_SET';;
-import * as DELETE from './DELETE';
-import * as EXPLAIN from './EXPLAIN';
-import * as LIST from './LIST';
-import * as PROFILE from './PROFILE';
-import * as QUERY from './QUERY';
-import * as RO_QUERY from './RO_QUERY';
-import * as SLOWLOG from './SLOWLOG';
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-
-export default {
-    CONFIG_GET,
-    configGet: CONFIG_GET,
-    CONFIG_SET,
-    configSet: CONFIG_SET,
-    DELETE,
-    delete: DELETE,
-    EXPLAIN,
-    explain: EXPLAIN,
-    LIST,
-    list: LIST,
-    PROFILE,
-    profile: PROFILE,
-    QUERY,
-    query: QUERY,
-    RO_QUERY,
-    roQuery: RO_QUERY,
-    SLOWLOG,
-    slowLog: SLOWLOG
-};
-
-type QueryParam = null | string | number | boolean | QueryParams | Array<QueryParam>;
-
-type QueryParams = {
-    [key: string]: QueryParam;
-};
-
-export interface QueryOptions {
-    params?: QueryParams;
-    TIMEOUT?: number;
-}
-
-export type QueryOptionsBackwardCompatible = QueryOptions | number;
-
-export function pushQueryArguments(
-    args: RedisCommandArguments,
-    graph: RedisCommandArgument,
-    query: RedisCommandArgument,
-    options?: QueryOptionsBackwardCompatible,
-    compact?: boolean
-): RedisCommandArguments {
-    args.push(graph);
-
-    if (typeof options === 'number') {
-        args.push(query);
-        pushTimeout(args, options);
-    } else {
-        args.push(
-            options?.params ?
-                `CYPHER ${queryParamsToString(options.params)} ${query}` :
-                query
-        );
-
-        if (options?.TIMEOUT !== undefined) {
-            pushTimeout(args, options.TIMEOUT);
-        }
-    }
-
-    if (compact) {
-        args.push('--compact');
-    }
-
-    return args;
-}
-
-function pushTimeout(args: RedisCommandArguments, timeout: number): void {
-    args.push('TIMEOUT', timeout.toString());
-}
-
-function queryParamsToString(params: QueryParams): string {
-    const parts = [];
-    for (const [key, value] of Object.entries(params)) {
-        parts.push(`${key}=${queryParamToString(value)}`);
-    }
-    return parts.join(' ');
-}
-
-function queryParamToString(param: QueryParam): string {
-    if (param === null) {
-        return 'null';
-    }
-
-    switch (typeof param) {
-        case 'string':
-            return `"${param.replace(/["\\]/g, '\\$&')}"`;
-
-        case 'number':
-        case 'boolean':
-            return param.toString();
-    }
-
-    if (Array.isArray(param)) {
-        return `[${param.map(queryParamToString).join(',')}]`;
-    } else if (typeof param === 'object') {
-        const body = [];
-        for (const [key, value] of Object.entries(param)) {
-            body.push(`${key}:${queryParamToString(value)}`);
-        }
-        return `{${body.join(',')}}`;
-    } else {
-        throw new TypeError(`Unexpected param type ${typeof param} ${param}`)
-    }
-}
diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts
deleted file mode 100644
index 495c6d17a8a..00000000000
--- a/packages/graph/lib/graph.spec.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { strict as assert } from 'assert';
-import testUtils, { GLOBAL } from './test-utils';
-import Graph from './graph';
-
-describe('Graph', () => {
-    testUtils.testWithClient('null', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN null AS key');
-
-        assert.deepEqual(
-            data,
-            [{ key: null }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('string', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN "string" AS key');
-
-        assert.deepEqual(
-            data,
-            [{ key: 'string' }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('integer', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN 0 AS key');
-
-        assert.deepEqual(
-            data,
-            [{ key: 0 }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('boolean', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN false AS key');
-
-        assert.deepEqual(
-            data,
-            [{ key: false }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('double', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN 0.1 AS key');
-
-        assert.deepEqual(
-            data,
-            [{ key: 0.1 }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('array', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN [null] AS key');
-
-        assert.deepEqual(
-            data,
-            [{ key: [null] }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('edge', async client => {
-        const graph = new Graph(client as any, 'graph');
-
-        // check with and without metadata cache
-        for (let i = 0; i < 2; i++) {
-            const { data } = await graph.query<any>('CREATE ()-[edge :edge]->() RETURN edge');
-            assert.ok(Array.isArray(data));
-            assert.equal(data.length, 1);
-            assert.equal(typeof data[0].edge.id, 'number');
-            assert.equal(data[0].edge.relationshipType, 'edge');
-            assert.equal(typeof data[0].edge.sourceId, 'number');
-            assert.equal(typeof data[0].edge.destinationId, 'number');
-            assert.deepEqual(data[0].edge.properties, {});
-        }
-
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('node', async client => {
-        const graph = new Graph(client as any, 'graph');
-
-        // check with and without metadata cache
-        for (let i = 0; i < 2; i++) {
-            const { data } = await graph.query<any>('CREATE (node :node { p: 0 }) RETURN node');
-            assert.ok(Array.isArray(data));
-            assert.equal(data.length, 1);
-            assert.equal(typeof data[0].node.id, 'number');
-            assert.deepEqual(data[0].node.labels, ['node']);
-            assert.deepEqual(data[0].node.properties, { p: 0 });
-        }
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('path', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            [, { data }] = await Promise.all([
-                await graph.query('CREATE ()-[:edge]->()'),
-                await graph.roQuery<any>('MATCH path = ()-[:edge]->() RETURN path')
-            ]);
-
-        assert.ok(Array.isArray(data));
-        assert.equal(data.length, 1);
-
-        assert.ok(Array.isArray(data[0].path.nodes));
-        assert.equal(data[0].path.nodes.length, 2);
-        for (const node of data[0].path.nodes) {
-            assert.equal(typeof node.id, 'number');
-            assert.deepEqual(node.labels, []);
-            assert.deepEqual(node.properties, {});
-        }
-
-        assert.ok(Array.isArray(data[0].path.edges));
-        assert.equal(data[0].path.edges.length, 1);
-        for (const edge of data[0].path.edges) {
-            assert.equal(typeof edge.id, 'number');
-            assert.equal(edge.relationshipType, 'edge');
-            assert.equal(typeof edge.sourceId, 'number');
-            assert.equal(typeof edge.destinationId, 'number');
-            assert.deepEqual(edge.properties, {});
-        }
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('map', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN { key: "value" } AS map');
-
-        assert.deepEqual(data, [{
-            map: {
-                key: 'value'
-            }
-        }]);
-    }, GLOBAL.SERVERS.OPEN);
-
-    testUtils.testWithClient('point', async client => {
-        const graph = new Graph(client as any, 'graph'),
-            { data } = await graph.query('RETURN point({ latitude: 1, longitude: 2 }) AS point');
-
-        assert.deepEqual(data, [{
-            point: {
-                latitude: 1,
-                longitude: 2
-            }
-        }]);
-    }, GLOBAL.SERVERS.OPEN);
-});
diff --git a/packages/graph/lib/graph.ts b/packages/graph/lib/graph.ts
deleted file mode 100644
index a95338bd8f3..00000000000
--- a/packages/graph/lib/graph.ts
+++ /dev/null
@@ -1,359 +0,0 @@
-import { RedisClientType } from '@redis/client/dist/lib/client/index';
-import { RedisCommandArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/commands';
-import { QueryOptions } from './commands';
-import { QueryReply } from './commands/QUERY';
-
-interface GraphMetadata {
-    labels: Array<string>;
-    relationshipTypes: Array<string>;
-    propertyKeys: Array<string>;
-}
-
-// https://github.com/RedisGraph/RedisGraph/blob/master/src/resultset/formatters/resultset_formatter.h#L20
-enum GraphValueTypes {
-    UNKNOWN = 0,
-    NULL = 1,
-    STRING = 2,
-    INTEGER = 3,
-    BOOLEAN = 4,
-    DOUBLE = 5,
-    ARRAY = 6,
-    EDGE = 7,
-    NODE = 8,
-    PATH = 9,
-    MAP = 10,
-    POINT = 11
-}
-
-type GraphEntityRawProperties = Array<[
-    id: number,
-    ...value: GraphRawValue
-]>;
-
-type GraphEdgeRawValue = [
-    GraphValueTypes.EDGE,
-    [
-        id: number,
-        relationshipTypeId: number,
-        sourceId: number,
-        destinationId: number,
-        properties: GraphEntityRawProperties
-    ]
-];
-
-type GraphNodeRawValue = [
-    GraphValueTypes.NODE,
-    [
-        id: number,
-        labelIds: Array<number>,
-        properties: GraphEntityRawProperties
-    ]
-];
-
-type GraphPathRawValue = [
-    GraphValueTypes.PATH,
-    [
-        nodes: [
-            GraphValueTypes.ARRAY,
-            Array<GraphNodeRawValue>
-        ],
-        edges: [
-            GraphValueTypes.ARRAY,
-            Array<GraphEdgeRawValue>
-        ]
-    ]
-];
-
-type GraphMapRawValue = [
-    GraphValueTypes.MAP,
-    Array<string | GraphRawValue>
-];
-
-type GraphRawValue = [
-    GraphValueTypes.NULL,
-    null
-] | [
-    GraphValueTypes.STRING,
-    string
-] | [
-    GraphValueTypes.INTEGER,
-    number
-] | [
-    GraphValueTypes.BOOLEAN,
-    string
-] | [
-    GraphValueTypes.DOUBLE,
-    string
-] | [
-    GraphValueTypes.ARRAY,
-    Array<GraphRawValue>
-] | GraphEdgeRawValue | GraphNodeRawValue | GraphPathRawValue | GraphMapRawValue | [
-    GraphValueTypes.POINT,
-    [
-        latitude: string,
-        longitude: string
-    ]
-];
-
-type GraphEntityProperties = Record<string, GraphValue>;
-
-interface GraphEdge {
-    id: number;
-    relationshipType: string;
-    sourceId: number;
-    destinationId: number;
-    properties: GraphEntityProperties;
-}
-
-interface GraphNode {
-    id: number;
-    labels: Array<string>;
-    properties: GraphEntityProperties;
-}
-
-interface GraphPath {
-    nodes: Array<GraphNode>;
-    edges: Array<GraphEdge>;
-}
-
-type GraphMap = {
-    [key: string]: GraphValue;
-};
-
-type GraphValue = null | string | number | boolean | Array<GraphValue> | {
-} | GraphEdge | GraphNode | GraphPath | GraphMap | {
-    latitude: string;
-    longitude: string;
-};
-
-export type GraphReply<T> = Omit<QueryReply, 'headers' | 'data'> & {
-    data?: Array<T>;
-};
-
-export type GraphClientType = RedisClientType<{
-    graph: {
-        query: typeof import('./commands/QUERY'),
-        roQuery: typeof import('./commands/RO_QUERY')
-    }
-}, RedisFunctions, RedisScripts>;
-
-export default class Graph {
-    #client: GraphClientType;
-    #name: string;
-    #metadata?: GraphMetadata;
-
-    constructor(
-        client: GraphClientType,
-        name: string
-    ) {
-        this.#client = client;
-        this.#name = name;
-    }
-
-    async query<T>(
-        query: RedisCommandArgument,
-        options?: QueryOptions
-    ) {
-        return this.#parseReply<T>(
-            await this.#client.graph.query(
-                this.#name,
-                query,
-                options,
-                true
-            )
-        );
-    }
-
-    async roQuery<T>(
-        query: RedisCommandArgument,
-        options?: QueryOptions
-    ) {
-        return this.#parseReply<T>(
-            await this.#client.graph.roQuery(
-                this.#name,
-                query,
-                options,
-                true
-            )
-        );
-    }
-
-    #setMetadataPromise?: Promise<GraphMetadata>;
-
-    #updateMetadata(): Promise<GraphMetadata> {
-        this.#setMetadataPromise ??= this.#setMetadata()
-            .finally(() => this.#setMetadataPromise = undefined);
-        return this.#setMetadataPromise;
-    }
-
-    // DO NOT use directly, use #updateMetadata instead
-    async #setMetadata(): Promise<GraphMetadata> {
-        const [labels, relationshipTypes, propertyKeys] = await Promise.all([
-            this.#client.graph.roQuery(this.#name, 'CALL db.labels()'),
-            this.#client.graph.roQuery(this.#name, 'CALL db.relationshipTypes()'),
-            this.#client.graph.roQuery(this.#name, 'CALL db.propertyKeys()')
-        ]);
-
-        this.#metadata = {
-            labels: this.#cleanMetadataArray(labels.data as Array<[string]>),
-            relationshipTypes: this.#cleanMetadataArray(relationshipTypes.data as Array<[string]>),
-            propertyKeys: this.#cleanMetadataArray(propertyKeys.data as Array<[string]>)
-        };
-
-        return this.#metadata;
-    }
-
-    #cleanMetadataArray(arr: Array<[string]>): Array<string> {
-        return arr.map(([value]) => value);
-    }
-
-    #getMetadata<T extends keyof GraphMetadata>(
-        key: T,
-        id: number
-    ): GraphMetadata[T][number] | Promise<GraphMetadata[T][number]> {
-        return this.#metadata?.[key][id] ?? this.#getMetadataAsync(key, id);
-    }
-
-    // DO NOT use directly, use #getMetadata instead
-    async #getMetadataAsync<T extends keyof GraphMetadata>(
-        key: T,
-        id: number
-    ): Promise<GraphMetadata[T][number]> {
-        const value = (await this.#updateMetadata())[key][id];
-        if (value === undefined) throw new Error(`Cannot find value from ${key}[${id}]`);
-        return value;
-    }
-
-    async #parseReply<T>(reply: QueryReply): Promise<GraphReply<T>> {
-        if (!reply.data) return reply;
-
-        const promises: Array<Promise<unknown>> = [],
-            parsed = {
-                metadata: reply.metadata,
-                data: reply.data!.map((row: any) => {
-                    const data: Record<string, GraphValue> = {};
-                    for (let i = 0; i < row.length; i++) {
-                        data[reply.headers[i][1]] = this.#parseValue(row[i], promises);
-                    }
-
-                    return data as unknown as T;
-                })
-            };
-
-        if (promises.length) await Promise.all(promises);
-
-        return parsed;
-    }
-
-    #parseValue([valueType, value]: GraphRawValue, promises: Array<Promise<unknown>>): GraphValue {
-        switch (valueType) {
-            case GraphValueTypes.NULL:
-                return null;
-
-            case GraphValueTypes.STRING:
-            case GraphValueTypes.INTEGER:
-                return value;
-
-            case GraphValueTypes.BOOLEAN:
-                return value === 'true';
-
-            case GraphValueTypes.DOUBLE:
-                return parseFloat(value);
-
-            case GraphValueTypes.ARRAY:
-                return value.map(x => this.#parseValue(x, promises));
-
-            case GraphValueTypes.EDGE:
-                return this.#parseEdge(value, promises);
-
-            case GraphValueTypes.NODE:
-                return this.#parseNode(value, promises);
-
-            case GraphValueTypes.PATH:
-                return {
-                    nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)),
-                    edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises))
-                };
-
-            case GraphValueTypes.MAP:
-                const map: GraphMap = {};
-                for (let i = 0; i < value.length; i++) {
-                    map[value[i++] as string] = this.#parseValue(value[i] as GraphRawValue, promises);
-                }
-
-                return map;
-
-            case GraphValueTypes.POINT:
-                return {
-                    latitude: parseFloat(value[0]),
-                    longitude: parseFloat(value[1])
-                };
-
-            default:
-                throw new Error(`unknown scalar type: ${valueType}`);
-        }
-    }
-
-    #parseEdge([
-        id,
-        relationshipTypeId,
-        sourceId,
-        destinationId,
-        properties
-    ]: GraphEdgeRawValue[1], promises: Array<Promise<unknown>>): GraphEdge {
-        const edge = {
-            id,
-            sourceId,
-            destinationId,
-            properties: this.#parseProperties(properties, promises)
-        } as GraphEdge;
-
-        const relationshipType = this.#getMetadata('relationshipTypes', relationshipTypeId);
-        if (relationshipType instanceof Promise) {
-            promises.push(
-                relationshipType.then(value => edge.relationshipType = value)
-            );
-        } else {
-            edge.relationshipType = relationshipType;
-        }
-
-        return edge;
-    }
-
-    #parseNode([
-        id,
-        labelIds,
-        properties
-    ]: GraphNodeRawValue[1], promises: Array<Promise<unknown>>): GraphNode {
-        const labels = new Array<string>(labelIds.length);
-        for (let i = 0; i < labelIds.length; i++) {
-            const value = this.#getMetadata('labels', labelIds[i]);
-            if (value instanceof Promise) {
-                promises.push(value.then(value => labels[i] = value));
-            } else {
-                labels[i] = value;
-            }
-        }
-
-        return {
-            id,
-            labels,
-            properties: this.#parseProperties(properties, promises)
-        };
-    }
-
-    #parseProperties(raw: GraphEntityRawProperties, promises: Array<Promise<unknown>>): GraphEntityProperties {
-        const parsed: GraphEntityProperties = {};
-        for (const [id, type, value] of raw) {
-            const parsedValue = this.#parseValue([type, value] as GraphRawValue, promises),
-                key = this.#getMetadata('propertyKeys', id);
-            if (key instanceof Promise) {
-                promises.push(key.then(key => parsed[key] = parsedValue));
-            } else {
-                parsed[key] = parsedValue;
-            }
-        }
-
-        return parsed;
-    }
-}
diff --git a/packages/graph/lib/index.ts b/packages/graph/lib/index.ts
deleted file mode 100644
index e9f15ab1fd9..00000000000
--- a/packages/graph/lib/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from './commands';
-export { default as Graph } from './graph';
diff --git a/packages/graph/lib/test-utils.ts b/packages/graph/lib/test-utils.ts
deleted file mode 100644
index 56c0af56a2e..00000000000
--- a/packages/graph/lib/test-utils.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import TestUtils from '@redis/test-utils';
-import RedisGraph from '.';
-
-export default new TestUtils({
-    dockerImageName: 'redislabs/redisgraph',
-    dockerImageVersionArgument: 'redisgraph-version'
-});
-
-export const GLOBAL = {
-    SERVERS: {
-        OPEN: {
-            serverArguments: ['--loadmodule /usr/lib/redis/modules/redisgraph.so'],
-            clientOptions: {
-                modules: {
-                    graph: RedisGraph
-                }
-            }
-        }
-    }
-};
diff --git a/packages/graph/package.json b/packages/graph/package.json
deleted file mode 100644
index 95cce6b8a86..00000000000
--- a/packages/graph/package.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
-  "name": "@redis/graph",
-  "version": "1.1.1",
-  "license": "MIT",
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
-  "files": [
-    "dist/"
-  ],
-  "scripts": {
-    "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
-    "build": "tsc",
-    "documentation": "typedoc"
-  },
-  "peerDependencies": {
-    "@redis/client": "^1.0.0"
-  },
-  "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
-    "@redis/test-utils": "*",
-    "@types/node": "^20.6.2",
-    "nyc": "^15.1.0",
-    "release-it": "^16.1.5",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typedoc": "^0.25.1",
-    "typescript": "^5.2.2"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git://github.com/redis/node-redis.git"
-  },
-  "bugs": {
-    "url": "https://github.com/redis/node-redis/issues"
-  },
-  "homepage": "https://github.com/redis/node-redis/tree/master/packages/graph",
-  "keywords": [
-    "redis",
-    "RedisGraph"
-  ]
-}
diff --git a/packages/json/.npmignore b/packages/json/.npmignore
deleted file mode 100644
index bbef2b404fb..00000000000
--- a/packages/json/.npmignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.nyc_output/
-coverage/
-lib/
-.nycrc.json
-.release-it.json
-tsconfig.json
diff --git a/packages/json/.release-it.json b/packages/json/.release-it.json
index ab495a49b13..a384e3e3d4d 100644
--- a/packages/json/.release-it.json
+++ b/packages/json/.release-it.json
@@ -1,10 +1,22 @@
 {
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
   "git": {
     "tagName": "json@${version}",
+    "tagMatch": "json@*",
     "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
   },
-  "npm": {
-    "publishArgs": ["--access", "public"]
+  "plugins": {
+    "@release-it/bumper": {
+      "out": {
+        "file": "package.json",
+        "path": ["peerDependencies.@redis/client"],
+        "versionPrefix": "^"
+      }
+    }
   }
 }
diff --git a/packages/json/README.md b/packages/json/README.md
index e7f70174116..86996a68370 100644
--- a/packages/json/README.md
+++ b/packages/json/README.md
@@ -1,12 +1,14 @@
 # @redis/json
 
-This package provides support for the [RedisJSON](https://redis.io/docs/stack/json/) module, which adds JSON as a native data type to Redis.  It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RedisJSON commands.
+This package provides support for the [RedisJSON](https://redis.io/docs/data-types/json/) module, which adds JSON as a native data type to Redis.
 
-To use these extra commands, your Redis server must have the RedisJSON module installed.
+Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis).
+
+:warning: To use these extra commands, your Redis server must have the RedisJSON module installed.
 
 ## Usage
 
-For a complete example, see [`managing-json.js`](https://github.com/redis/node-redis/blob/master/examples/managing-json.js) in the Node Redis examples folder.
+For a complete example, see [`managing-json.js`](https://github.com/redis/node-redis/blob/master/examples/managing-json.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples).
 
 ### Storing JSON Documents in Redis
 
@@ -15,33 +17,27 @@ The [`JSON.SET`](https://redis.io/commands/json.set/) command stores a JSON valu
 Here, we'll store a JSON document in the root of the Redis key "`mydoc`":
 
 ```javascript
-import { createClient } from 'redis';
-
-...
 await client.json.set('noderedis:jsondata', '$', {
   name: 'Roberta McDonald',
-  pets: [
-    {
+  pets: [{
     name: 'Rex',
     species: 'dog',
     age: 3,
     isMammal: true
-    },
-    {
+  }, {
     name: 'Goldie',
     species: 'fish',
     age: 2,
     isMammal: false
-    }
-  ]
+  }]
 });
 ```
 
-For more information about RedisJSON's path syntax, [check out the documentation](https://redis.io/docs/stack/json/path/).
+For more information about RedisJSON's path syntax, [check out the documentation](https://redis.io/docs/data-types/json/path/).
 
 ### Retrieving JSON Documents from Redis
 
-With RedisJSON, we can retrieve all or part(s) of a JSON document using the [`JSON.GET`](https://redis.io/commands/json.get/) command and one or more JSON Paths.  Let's get the name and age of one of the pets:
+With RedisJSON, we can retrieve all or part(s) of a JSON document using the [`JSON.GET`](https://redis.io/commands/json.get/) command and one or more JSON Paths. Let's get the name and age of one of the pets:
 
 ```javascript
 const results = await client.json.get('noderedis:jsondata', {
diff --git a/packages/json/lib/commands/ARRAPPEND.spec.ts b/packages/json/lib/commands/ARRAPPEND.spec.ts
index ab53837a000..b2c22e0b9c0 100644
--- a/packages/json/lib/commands/ARRAPPEND.spec.ts
+++ b/packages/json/lib/commands/ARRAPPEND.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ARRAPPEND';
+import ARRAPPEND from './ARRAPPEND';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ARRAPPEND', () => {
-    describe('transformArguments', () => {
-        it('single JSON', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 1),
-                ['JSON.ARRAPPEND', 'key', '$', '1']
-            );
-        });
+describe('JSON.ARRAPPEND', () => {
+  describe('transformArguments', () => {
+    it('single element', () => {
+      assert.deepEqual(
+        parseArgs(ARRAPPEND, 'key', '$', 'value'),
+        ['JSON.ARRAPPEND', 'key', '$', '"value"']
+      );
+    });
 
-        it('multiple JSONs', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 1, 2),
-                ['JSON.ARRAPPEND', 'key', '$', '1', '2']
-            );
-        });
+    it('multiple elements', () => {
+      assert.deepEqual(
+        parseArgs(ARRAPPEND, 'key', '$', 1, 2),
+        ['JSON.ARRAPPEND', 'key', '$', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.arrAppend', async client => {
-        await client.json.set('key', '$', []);
+  testUtils.testWithClient('client.json.arrAppend', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', []),
+      client.json.arrAppend('key', '$', 'value')
+    ]);
 
-        assert.deepEqual(
-            await client.json.arrAppend('key', '$', 1),
-            [1]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [1]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts
index 2935d192996..b98d1532b4e 100644
--- a/packages/json/lib/commands/ARRAPPEND.ts
+++ b/packages/json/lib/commands/ARRAPPEND.ts
@@ -1,15 +1,33 @@
-import { RedisJSON, transformRedisJsonArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Appends one or more values to the end of an array in a JSON document.
+   * Returns the new array length after append, or null if the path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key to append to
+   * @param path - Path to the array in the JSON document
+   * @param json - The first value to append
+   * @param jsons - Additional values to append
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    path: RedisArgument,
+    json: RedisJSON,
+    ...jsons: Array<RedisJSON>
+  ) {
+    parser.push('JSON.ARRAPPEND');
+    parser.pushKey(key);
+    parser.push(path, transformRedisJsonArgument(json));
 
-export function transformArguments(key: string, path: string, ...jsons: Array<RedisJSON>): Array<string> {
-    const args = ['JSON.ARRAPPEND', key, path];
-
-    for (const json of jsons) {
-        args.push(transformRedisJsonArgument(json));
+    for (let i = 0; i < jsons.length; i++) {
+      parser.push(transformRedisJsonArgument(jsons[i]));
     }
-
-    return args;
-}
-
-export declare function transformReply(): number | Array<number>;
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/ARRINDEX.spec.ts b/packages/json/lib/commands/ARRINDEX.spec.ts
index 7a47d67126a..3c1377354f1 100644
--- a/packages/json/lib/commands/ARRINDEX.spec.ts
+++ b/packages/json/lib/commands/ARRINDEX.spec.ts
@@ -1,37 +1,49 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ARRINDEX';
+import ARRINDEX from './ARRINDEX';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ARRINDEX', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'json'),
-                ['JSON.ARRINDEX', 'key', '$', '"json"']
-            );
-        });
-
-        it('with start', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'json', 1),
-                ['JSON.ARRINDEX', 'key', '$', '"json"', '1']
-            );
-        });
-
-        it('with start, end', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'json', 1, 2),
-                ['JSON.ARRINDEX', 'key', '$', '"json"', '1', '2']
-            );
-        });
+describe('JSON.ARRINDEX', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ARRINDEX, 'key', '$', 'value'),
+        ['JSON.ARRINDEX', 'key', '$', '"value"']
+      );
     });
 
-    testUtils.testWithClient('client.json.arrIndex', async client => {
-        await client.json.set('key', '$', []);
+    describe('with range', () => {
+      it('start only', () => {
+        assert.deepEqual(
+          parseArgs(ARRINDEX, 'key', '$', 'value', {
+            range: {
+              start: 0
+            }
+          }),
+          ['JSON.ARRINDEX', 'key', '$', '"value"', '0']
+        );
+      });
 
+      it('with start and stop', () => {
         assert.deepEqual(
-            await client.json.arrIndex('key', '$', 'json'),
-            [-1]
+          parseArgs(ARRINDEX, 'key', '$', 'value', {
+            range: {
+              start: 0,
+              stop: 1
+            }
+          }),
+          ['JSON.ARRINDEX', 'key', '$', '"value"', '0', '1']
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
+    });
+  });
+
+  testUtils.testWithClient('client.json.arrIndex', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', []),
+      client.json.arrIndex('key', '$', 'value')
+    ]);
+
+    assert.deepEqual(reply, [-1]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts
index 5860b59cb3c..1437fab4d56 100644
--- a/packages/json/lib/commands/ARRINDEX.ts
+++ b/packages/json/lib/commands/ARRINDEX.ts
@@ -1,21 +1,46 @@
-import { RedisJSON, transformRedisJsonArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
+export interface JsonArrIndexOptions {
+  range?: {
+    start: number;
+    stop?: number;
+  };
+}
 
-export function transformArguments(key: string, path: string, json: RedisJSON, start?: number, stop?: number): Array<string> {
-    const args = ['JSON.ARRINDEX', key, path, transformRedisJsonArgument(json)];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the index of the first occurrence of a value in a JSON array.
+   * If the specified value is not found, it returns -1, or null if the path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the array
+   * @param path - Path to the array in the JSON document
+   * @param json - The value to search for
+   * @param options - Optional range parameters for the search
+   * @param options.range.start - Starting index for the search
+   * @param options.range.stop - Optional ending index for the search
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    path: RedisArgument,
+    json: RedisJSON,
+    options?: JsonArrIndexOptions
+  ) {
+    parser.push('JSON.ARRINDEX');
+    parser.pushKey(key);
+    parser.push(path, transformRedisJsonArgument(json));
 
-    if (start !== undefined && start !== null) {
-        args.push(start.toString());
+    if (options?.range) {
+      parser.push(options.range.start.toString());
 
-        if (stop !== undefined && stop !== null) {
-            args.push(stop.toString());
-        }
+      if (options.range.stop !== undefined) {
+        parser.push(options.range.stop.toString());
+      }
     }
-
-    return args;
-}
-
-export declare function transformReply(): number | Array<number>;
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/ARRINSERT.spec.ts b/packages/json/lib/commands/ARRINSERT.spec.ts
index 4b9d58b2cac..bf9c8a2a051 100644
--- a/packages/json/lib/commands/ARRINSERT.spec.ts
+++ b/packages/json/lib/commands/ARRINSERT.spec.ts
@@ -1,30 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ARRINSERT';
+import ARRINSERT from './ARRINSERT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ARRINSERT', () => {
-    describe('transformArguments', () => {
-        it('single JSON', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 0, 'json'),
-                ['JSON.ARRINSERT', 'key', '$', '0', '"json"']
-            );
-        });
+describe('JSON.ARRINSERT', () => {
+  describe('transformArguments', () => {
+    it('single element', () => {
+      assert.deepEqual(
+        parseArgs(ARRINSERT, 'key', '$', 0, 'value'),
+        ['JSON.ARRINSERT', 'key', '$', '0', '"value"']
+      );
+    });
 
-        it('multiple JSONs', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 0, '1', '2'),
-                ['JSON.ARRINSERT', 'key', '$', '0', '"1"', '"2"']
-            );
-        });
+    it('multiple elements', () => {
+      assert.deepEqual(
+        parseArgs(ARRINSERT, 'key', '$', 0, '1', '2'),
+        ['JSON.ARRINSERT', 'key', '$', '0', '"1"', '"2"']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.arrInsert', async client => {
-        await client.json.set('key', '$', []);
+  testUtils.testWithClient('client.json.arrInsert', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', []),
+      client.json.arrInsert('key', '$', 0, 'value')
+    ]);
 
-        assert.deepEqual(
-            await client.json.arrInsert('key', '$', 0, 'json'),
-            [1]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [1]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts
index 85857657019..7a5ab945892 100644
--- a/packages/json/lib/commands/ARRINSERT.ts
+++ b/packages/json/lib/commands/ARRINSERT.ts
@@ -1,15 +1,35 @@
-import { RedisJSON, transformRedisJsonArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Inserts one or more values into an array at the specified index.
+   * Returns the new array length after insert, or null if the path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the array
+   * @param path - Path to the array in the JSON document
+   * @param index - The position where to insert the values
+   * @param json - The first value to insert
+   * @param jsons - Additional values to insert
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    path: RedisArgument,
+    index: number,
+    json: RedisJSON,
+    ...jsons: Array<RedisJSON>
+  ) {
+    parser.push('JSON.ARRINSERT');
+    parser.pushKey(key);
+    parser.push(path, index.toString(), transformRedisJsonArgument(json));
 
-export function transformArguments(key: string, path: string, index: number, ...jsons: Array<RedisJSON>): Array<string> {
-    const args = ['JSON.ARRINSERT', key, path, index.toString()];
-
-    for (const json of jsons) {
-        args.push(transformRedisJsonArgument(json));
+    for (let i = 0; i < jsons.length; i++) {
+      parser.push(transformRedisJsonArgument(jsons[i]));
     }
-
-    return args;
-}
-
-export declare function transformReply(): number | Array<number>;
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/ARRLEN.spec.ts b/packages/json/lib/commands/ARRLEN.spec.ts
index f0a3ec40a4c..dcf7d35acb0 100644
--- a/packages/json/lib/commands/ARRLEN.spec.ts
+++ b/packages/json/lib/commands/ARRLEN.spec.ts
@@ -1,30 +1,33 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ARRLEN';
+import ARRLEN from './ARRLEN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ARRLEN', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.ARRLEN', 'key']
-            );
-        });
+describe('JSON.ARRLEN', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ARRLEN, 'key'),
+        ['JSON.ARRLEN', 'key']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.ARRLEN', 'key', '$']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(ARRLEN, 'key', {
+          path: '$'
+        }),
+        ['JSON.ARRLEN', 'key', '$']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.arrLen', async client => {
-        await client.json.set('key', '$', []);
+  testUtils.testWithClient('client.json.arrLen', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', []),
+      client.json.arrLen('key')
+    ]);
 
-        assert.deepEqual(
-            await client.json.arrLen('key', '$'),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 0);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/ARRLEN.ts b/packages/json/lib/commands/ARRLEN.ts
index 818397b7f8d..f2111986c0e 100644
--- a/packages/json/lib/commands/ARRLEN.ts
+++ b/packages/json/lib/commands/ARRLEN.ts
@@ -1,15 +1,27 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.ARRLEN', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonArrLenOptions {
+  path?: RedisArgument;
 }
 
-export declare function transformReply(): number | Array<number>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the length of an array in a JSON document.
+   * Returns null if the path does not exist or the value is not an array.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the array
+   * @param options - Optional parameters
+   * @param options.path - Path to the array in the JSON document
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonArrLenOptions) {
+    parser.push('JSON.ARRLEN');
+    parser.pushKey(key);
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/ARRPOP.spec.ts b/packages/json/lib/commands/ARRPOP.spec.ts
index 7c2ec365eb6..f823e7fc08a 100644
--- a/packages/json/lib/commands/ARRPOP.spec.ts
+++ b/packages/json/lib/commands/ARRPOP.spec.ts
@@ -1,57 +1,67 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ARRPOP';
-
-describe('ARRPOP', () => {
-    describe('transformArguments', () => {
-        it('key', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.ARRPOP', 'key']
-            );
-        });
-
-        it('key, path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.ARRPOP', 'key', '$']
-            );
-        });
-
-        it('key, path, index', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 0),
-                ['JSON.ARRPOP', 'key', '$', '0']
-            );
-        });
+import ARRPOP from './ARRPOP';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('JSON.ARRPOP', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(ARRPOP, 'key'),
+        ['JSON.ARRPOP', 'key']
+      );
+    });
+
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(ARRPOP, 'key', {
+          path: '$'
+        }),
+        ['JSON.ARRPOP', 'key', '$']
+      );
     });
 
-    describe('client.json.arrPop', () => {
-        testUtils.testWithClient('null', async client => {
-            await client.json.set('key', '.', []);
-
-            assert.equal(
-                await client.json.arrPop('key', '.'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('with value', async client => {
-            await client.json.set('key', '.', ['value']);
-
-            assert.equal(
-                await client.json.arrPop('key', '.'),
-                'value'
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('array', async client => {
-            await client.json.set('key', '$', ['value']);
-
-            assert.deepEqual(
-                await client.json.arrPop('key', '$'),
-                ['value']
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with path and index', () => {
+      assert.deepEqual(
+        parseArgs(ARRPOP, 'key', {
+          path: '$',
+          index: 0
+        }),
+        ['JSON.ARRPOP', 'key', '$', '0']
+      );
     });
+  });
+
+  describe('client.json.arrPop', () => {
+    testUtils.testWithClient('without path and value', async client => {
+      const [, reply] = await Promise.all([
+        client.json.set('key', '$', []),
+        client.json.arrPop('key')
+      ]);
+
+      assert.equal(reply, null);
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('. path with value', async client => {
+      const [, reply] = await Promise.all([
+        client.json.set('key', '.', ['value']),
+        client.json.arrPop('key', {
+          path: '.'
+        })
+      ]);
+
+      assert.equal(reply, 'value');
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('$ path with value', async client => {
+      const [, reply] = await Promise.all([
+        client.json.set('key', '$', ['value']),
+        client.json.arrPop('key', {
+          path: '$'
+        })
+      ]);
+
+      assert.deepEqual(reply, ['value']);
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts
index 18830c0d314..88e4da96980 100644
--- a/packages/json/lib/commands/ARRPOP.ts
+++ b/packages/json/lib/commands/ARRPOP.ts
@@ -1,27 +1,40 @@
-import { RedisJSON, transformRedisJsonNullReply } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
+import { isArrayReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: string, path?: string, index?: number): Array<string> {
-    const args = ['JSON.ARRPOP', key];
-
-    if (path) {
-        args.push(path);
-
-        if (index !== undefined && index !== null) {
-            args.push(index.toString());
-        }
-    }
-
-    return args;
+export interface RedisArrPopOptions {
+  path: RedisArgument;
+  index?: number;
 }
 
-export function transformReply(reply: null | string | Array<null | string>): null | RedisJSON | Array<RedisJSON> {
-    if (reply === null) return null;
-
-    if (Array.isArray(reply)) {
-        return reply.map(transformRedisJsonNullReply);
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Removes and returns an element from an array in a JSON document.
+   * Returns null if the path does not exist or the value is not an array.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the array
+   * @param options - Optional parameters
+   * @param options.path - Path to the array in the JSON document
+   * @param options.index - Optional index to pop from. Default is -1 (last element)
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: RedisArrPopOptions) {
+    parser.push('JSON.ARRPOP');
+    parser.pushKey(key);
+
+    if (options) {
+      parser.push(options.path);
+
+      if (options.index !== undefined) {
+        parser.push(options.index.toString());
+      }
     }
+  },
+  transformReply(reply: NullReply | BlobStringReply | ArrayReply<NullReply | BlobStringReply>) {
+    return isArrayReply(reply) ?
+      (reply as unknown as UnwrapReply<typeof reply>).map(item => transformRedisJsonNullReply(item)) :
+      transformRedisJsonNullReply(reply);
+  }
+} as const satisfies Command;
 
-    return transformRedisJsonNullReply(reply);
-}
diff --git a/packages/json/lib/commands/ARRTRIM.spec.ts b/packages/json/lib/commands/ARRTRIM.spec.ts
index c254e1b6a03..e346716e8df 100644
--- a/packages/json/lib/commands/ARRTRIM.spec.ts
+++ b/packages/json/lib/commands/ARRTRIM.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ARRTRIM';
+import ARRTRIM from './ARRTRIM';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ARRTRIM', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '$', 0, 1),
-            ['JSON.ARRTRIM', 'key', '$', '0', '1']
-        );
-    });
+describe('JSON.ARRTRIM', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ARRTRIM, 'key', '$', 0, 1),
+      ['JSON.ARRTRIM', 'key', '$', '0', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.json.arrTrim', async client => {
-        await client.json.set('key', '$', []);
+  testUtils.testWithClient('client.json.arrTrim', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', []),
+      client.json.arrTrim('key', '$', 0, 1)
+    ]);
 
-        assert.deepEqual(
-            await client.json.arrTrim('key', '$', 0, 1),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [0]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/ARRTRIM.ts b/packages/json/lib/commands/ARRTRIM.ts
index 2de444eeebd..bfcb1da14aa 100644
--- a/packages/json/lib/commands/ARRTRIM.ts
+++ b/packages/json/lib/commands/ARRTRIM.ts
@@ -1,7 +1,22 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path: string, start: number, stop: number): Array<string> {
-    return ['JSON.ARRTRIM', key, path, start.toString(), stop.toString()];
-}
-
-export declare function transformReply(): number | Array<number>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Trims an array in a JSON document to include only elements within the specified range.
+   * Returns the new array length after trimming, or null if the path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the array
+   * @param path - Path to the array in the JSON document
+   * @param start - Starting index (inclusive)
+   * @param stop - Ending index (inclusive)
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, start: number, stop: number) {
+    parser.push('JSON.ARRTRIM');
+    parser.pushKey(key);
+    parser.push(path, start.toString(), stop.toString());
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/CLEAR.spec.ts b/packages/json/lib/commands/CLEAR.spec.ts
new file mode 100644
index 00000000000..c1786cc1dde
--- /dev/null
+++ b/packages/json/lib/commands/CLEAR.spec.ts
@@ -0,0 +1,33 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import CLEAR from './CLEAR';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('JSON.CLEAR', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CLEAR, 'key'),
+        ['JSON.CLEAR', 'key']
+      );
+    });
+
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(CLEAR, 'key', {
+          path: '$'
+        }),
+        ['JSON.CLEAR', 'key', '$']
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.json.clear', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', null),
+      client.json.clear('key')
+    ]);
+
+    assert.equal(reply, 0);
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/json/lib/commands/CLEAR.ts b/packages/json/lib/commands/CLEAR.ts
new file mode 100644
index 00000000000..281c5e6abae
--- /dev/null
+++ b/packages/json/lib/commands/CLEAR.ts
@@ -0,0 +1,28 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+
+export interface JsonClearOptions {
+  path?: RedisArgument;
+}
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Clears container values (arrays/objects) in a JSON document.
+   * Returns the number of values cleared (0 or 1), or null if the path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to the container to clear
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonClearOptions) {
+    parser.push('JSON.CLEAR');
+    parser.pushKey(key);
+
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts b/packages/json/lib/commands/DEBUG_MEMORY.spec.ts
index 468c994f2f5..09c29328d8e 100644
--- a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts
+++ b/packages/json/lib/commands/DEBUG_MEMORY.spec.ts
@@ -1,28 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DEBUG_MEMORY';
+import DEBUG_MEMORY from './DEBUG_MEMORY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DEBUG MEMORY', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.DEBUG', 'MEMORY', 'key']
-            );
-        });
+describe('JSON.DEBUG MEMORY', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(DEBUG_MEMORY, 'key'),
+        ['JSON.DEBUG', 'MEMORY', 'key']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.DEBUG', 'MEMORY', 'key', '$']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(DEBUG_MEMORY, 'key', {
+          path: '$'
+        }),
+        ['JSON.DEBUG', 'MEMORY', 'key', '$']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.arrTrim', async client => {
-        assert.deepEqual(
-            await client.json.debugMemory('key', '$'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.debugMemory', async client => {
+    assert.equal(
+      await client.json.debugMemory('key'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/DEBUG_MEMORY.ts b/packages/json/lib/commands/DEBUG_MEMORY.ts
index da60b1d9529..cf0f2c8f215 100644
--- a/packages/json/lib/commands/DEBUG_MEMORY.ts
+++ b/packages/json/lib/commands/DEBUG_MEMORY.ts
@@ -1,13 +1,28 @@
-export const FIRST_KEY_INDEX = 2;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.DEBUG', 'MEMORY', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonDebugMemoryOptions {
+  path?: RedisArgument;
 }
 
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Reports memory usage details for a JSON document value.
+   * Returns size in bytes of the value, or null if the key or path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to the value to examine
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDebugMemoryOptions) {
+    parser.push('JSON.DEBUG', 'MEMORY');
+    parser.pushKey(key);
+
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/DEL.spec.ts b/packages/json/lib/commands/DEL.spec.ts
index a957b9584ac..a008c3b9b2b 100644
--- a/packages/json/lib/commands/DEL.spec.ts
+++ b/packages/json/lib/commands/DEL.spec.ts
@@ -1,28 +1,32 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DEL';
+import DEL from './DEL';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DEL', () => {
-    describe('transformArguments', () => {
-        it('key', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.DEL', 'key']
-            );
-        });
+describe('JSON.DEL', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(DEL, 'key'),
+        ['JSON.DEL', 'key']
+      );
+    });
 
-        it('key, path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$.path'),
-                ['JSON.DEL', 'key', '$.path']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(DEL, 'key', {
+          path: '$.path'
+        }),
+        ['JSON.DEL', 'key', '$.path']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.del', async client => {
-        assert.deepEqual(
-            await client.json.del('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.del', async client => {
+    assert.equal(
+      await client.json.del('key'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
+
diff --git a/packages/json/lib/commands/DEL.ts b/packages/json/lib/commands/DEL.ts
index 090d4dbe853..4d768927088 100644
--- a/packages/json/lib/commands/DEL.ts
+++ b/packages/json/lib/commands/DEL.ts
@@ -1,13 +1,28 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.DEL', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonDelOptions {
+  path?: RedisArgument
 }
 
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Deletes a value from a JSON document.
+   * Returns the number of paths deleted (0 or 1), or null if the key does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to the value to delete
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDelOptions) {
+    parser.push('JSON.DEL');
+    parser.pushKey(key);
+
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/FORGET.spec.ts b/packages/json/lib/commands/FORGET.spec.ts
index 923bb997fc8..888fff5659b 100644
--- a/packages/json/lib/commands/FORGET.spec.ts
+++ b/packages/json/lib/commands/FORGET.spec.ts
@@ -1,28 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './FORGET';
+import FORGET from './FORGET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('FORGET', () => {
-    describe('transformArguments', () => {
-        it('key', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.FORGET', 'key']
-            );
-        });
+describe('JSON.FORGET', () => {
+  describe('transformArguments', () => {
+    it('key', () => {
+      assert.deepEqual(
+        parseArgs(FORGET, 'key'),
+        ['JSON.FORGET', 'key']
+      );
+    });
 
-        it('key, path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$.path'),
-                ['JSON.FORGET', 'key', '$.path']
-            );
-        });
+    it('key, path', () => {
+      assert.deepEqual(
+        parseArgs(FORGET, 'key', {
+          path: '$.path'
+        }),
+        ['JSON.FORGET', 'key', '$.path']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.forget', async client => {
-        assert.deepEqual(
-            await client.json.forget('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.forget', async client => {
+    assert.equal(
+      await client.json.forget('key'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/FORGET.ts b/packages/json/lib/commands/FORGET.ts
index cb2df3d605d..ea924c74247 100644
--- a/packages/json/lib/commands/FORGET.ts
+++ b/packages/json/lib/commands/FORGET.ts
@@ -1,13 +1,28 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.FORGET', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonForgetOptions {
+  path?: RedisArgument;
 }
 
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Alias for JSON.DEL - Deletes a value from a JSON document.
+   * Returns the number of paths deleted (0 or 1), or null if the key does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters 
+   * @param options.path - Path to the value to delete
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonForgetOptions) {
+    parser.push('JSON.FORGET');
+    parser.pushKey(key);
+
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/GET.spec.ts b/packages/json/lib/commands/GET.spec.ts
index ed831689a93..6b4f44871cb 100644
--- a/packages/json/lib/commands/GET.spec.ts
+++ b/packages/json/lib/commands/GET.spec.ts
@@ -1,78 +1,44 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GET';
-
-describe('GET', () => {
-    describe('transformArguments', () => {
-        describe('path', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('key', { path: '$' }),
-                    ['JSON.GET', 'key', '$']
-                );
-            });
-
-            it('array', () => {
-                assert.deepEqual(
-                    transformArguments('key', { path: ['$.1', '$.2'] }),
-                    ['JSON.GET', 'key', '$.1', '$.2']
-                );
-            });
-        });
-
-        it('key', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.GET', 'key']
-            );
-        });
-
-        it('INDENT', () => {
-            assert.deepEqual(
-                transformArguments('key', { INDENT: 'indent' }),
-                ['JSON.GET', 'key', 'INDENT', 'indent']
-            );
-        });
+import GET from './GET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('JSON.GET', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(GET, 'key'),
+        ['JSON.GET', 'key']
+      );
+    });
 
-        it('NEWLINE', () => {
-            assert.deepEqual(
-                transformArguments('key', { NEWLINE: 'newline' }),
-                ['JSON.GET', 'key', 'NEWLINE', 'newline']
-            );
-        });
+    describe('with path', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(GET, 'key', { path: '$' }),
+          ['JSON.GET', 'key', '$']
+        );
+      });
 
-        it('SPACE', () => {
-            assert.deepEqual(
-                transformArguments('key', { SPACE: 'space' }),
-                ['JSON.GET', 'key', 'SPACE', 'space']
-            );
-        });
+      it('array', () => {
+        assert.deepEqual(
+          parseArgs(GET, 'key', { path: ['$.1', '$.2'] }),
+          ['JSON.GET', 'key', '$.1', '$.2']
+        );
+      });
+    });
+  });
 
-        it('NOESCAPE', () => {
-            assert.deepEqual(
-                transformArguments('key', { NOESCAPE: true }),
-                ['JSON.GET', 'key', 'NOESCAPE']
-            );
-        });
+  testUtils.testWithClient('client.json.get', async client => {
+    assert.equal(
+      await client.json.get('key'),
+      null
+    );
 
-        it('INDENT, NEWLINE, SPACE, NOESCAPE, path', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    path: '$.path',
-                    INDENT: 'indent',
-                    NEWLINE: 'newline',
-                    SPACE: 'space',
-                    NOESCAPE: true
-                }),
-                ['JSON.GET', 'key', '$.path', 'INDENT', 'indent', 'NEWLINE', 'newline', 'SPACE', 'space', 'NOESCAPE']
-            );
-        });
-    });
+    await client.json.set('noderedis:users:1', '$', { name: 'Alice', age: 32, })
+    const res = await client.json.get('noderedis:users:1');
+    assert.equal(typeof res, 'object')
+    assert.deepEqual(res, { name: 'Alice', age: 32, })
 
-    testUtils.testWithClient('client.json.get', async client => {
-        assert.equal(
-            await client.json.get('key'),
-            null
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts
index 21bad09568b..14ec46a53af 100644
--- a/packages/json/lib/commands/GET.ts
+++ b/packages/json/lib/commands/GET.ts
@@ -1,42 +1,32 @@
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface GetOptions {
-    path?: string | Array<string>;
-    INDENT?: string;
-    NEWLINE?: string;
-    SPACE?: string;
-    NOESCAPE?: true;
+export interface JsonGetOptions {
+  path?: RedisVariadicArgument;
 }
 
-export function transformArguments(key: string, options?: GetOptions): RedisCommandArguments {
-    let args: RedisCommandArguments = ['JSON.GET', key];
-
-    if (options?.path) {
-        args = pushVerdictArguments(args, options.path);
-    }
-
-    if (options?.INDENT) {
-        args.push('INDENT', options.INDENT);
-    }
-
-    if (options?.NEWLINE) {
-        args.push('NEWLINE', options.NEWLINE);
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Gets values from a JSON document.
+   * Returns the value at the specified path, or null if the key or path does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path(s) to the value(s) to retrieve
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    options?: JsonGetOptions
+  ) {
+    parser.push('JSON.GET');
+    parser.pushKey(key);
+    if (options?.path !== undefined) {
+      parser.pushVariadic(options.path);
     }
-
-    if (options?.SPACE) {
-        args.push('SPACE', options.SPACE);
-    }
-
-    if (options?.NOESCAPE) {
-        args.push('NOESCAPE');
-    }
-
-    return args;
-}
-
-export { transformRedisJsonNullReply as transformReply } from '.';
+  },
+  transformReply: transformRedisJsonNullReply
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/json/lib/commands/MERGE.spec.ts b/packages/json/lib/commands/MERGE.spec.ts
index ee5e6fff86d..30a092035c5 100644
--- a/packages/json/lib/commands/MERGE.spec.ts
+++ b/packages/json/lib/commands/MERGE.spec.ts
@@ -1,21 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MERGE';
+import MERGE from './MERGE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MERGE', () => {
-    testUtils.isVersionGreaterThanHook([2, 6]);
+describe('JSON.MERGE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MERGE, 'key', '$', 'value'),
+      ['JSON.MERGE', 'key', '$', '"value"']
+    );
+  });
 
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '$', 1),
-            ['JSON.MERGE', 'key', '$', '1']
-        );
-    });
-
-    testUtils.testWithClient('client.json.merge', async client => {
-        assert.equal(
-            await client.json.merge('key', '$', 'json'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.merge', async client => {
+    assert.equal(
+      await client.json.merge('key', '$', 'value'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts
index 81cce7f006b..1a4b54fc4ba 100644
--- a/packages/json/lib/commands/MERGE.ts
+++ b/packages/json/lib/commands/MERGE.ts
@@ -1,9 +1,22 @@
-import { RedisJSON, transformRedisJsonArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: string, path: string, json: RedisJSON): Array<string> {
-    return ['JSON.MERGE', key, path, transformRedisJsonArgument(json)];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Merges a given JSON value into a JSON document.
+   * Returns OK on success, or null if the key does not exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param path - Path to merge into
+   * @param value - JSON value to merge
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, value: RedisJSON) {
+    parser.push('JSON.MERGE');
+    parser.pushKey(key);
+    parser.push(path, transformRedisJsonArgument(value));
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/MGET.spec.ts b/packages/json/lib/commands/MGET.spec.ts
index 456e160dd50..2d8efafde71 100644
--- a/packages/json/lib/commands/MGET.spec.ts
+++ b/packages/json/lib/commands/MGET.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MGET';
+import MGET from './MGET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MGET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(['1', '2'], '$'),
-            ['JSON.MGET', '1', '2', '$']
-        );
-    });
+describe('JSON.MGET', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MGET, ['1', '2'], '$'),
+      ['JSON.MGET', '1', '2', '$']
+    );
+  });
 
-    testUtils.testWithClient('client.json.mGet', async client => {
-        assert.deepEqual(
-            await client.json.mGet(['1', '2'], '$'),
-            [null, null]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.mGet', async client => {
+    assert.deepEqual(
+      await client.json.mGet(['1', '2'], '$'),
+      [null, null]
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts
index 34ca8da289f..01a7783b922 100644
--- a/packages/json/lib/commands/MGET.ts
+++ b/packages/json/lib/commands/MGET.ts
@@ -1,17 +1,23 @@
-import { RedisJSON, transformRedisJsonNullReply } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(keys: Array<string>, path: string): Array<string> {
-    return [
-        'JSON.MGET',
-        ...keys,
-        path
-    ];
-}
-
-export function transformReply(reply: Array<string | null>): Array<RedisJSON | null> {
-    return reply.map(transformRedisJsonNullReply);
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets values at a specific path from multiple JSON documents.
+   * Returns an array of values at the path from each key, null for missing keys/paths.
+   * 
+   * @param parser - The Redis command parser
+   * @param keys - Array of keys containing JSON documents
+   * @param path - Path to retrieve from each document
+   */
+  parseCommand(parser: CommandParser, keys: Array<RedisArgument>, path: RedisArgument) {
+    parser.push('JSON.MGET');
+    parser.pushKeys(keys);
+    parser.push(path);
+  },
+  transformReply(reply: UnwrapReply<ArrayReply<NullReply | BlobStringReply>>) {
+    return reply.map(json => transformRedisJsonNullReply(json))
+  }
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/MSET.spec.ts b/packages/json/lib/commands/MSET.spec.ts
index 53d4d822505..38e8b077e81 100644
--- a/packages/json/lib/commands/MSET.spec.ts
+++ b/packages/json/lib/commands/MSET.spec.ts
@@ -1,35 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MSET';
+import MSET from './MSET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MSET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments([{
-                key: '1',
-                path: '$',
-                value: 1
-            }, {
-                key: '2',
-                path: '$',
-                value: '2'
-            }]),
-            ['JSON.MSET', '1', '$', '1', '2', '$', '"2"']
-        );
-    });
+describe('JSON.MSET', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MSET, [{
+        key: '1',
+        path: '$',
+        value: 1
+      }, {
+        key: '2',
+        path: '$',
+        value: '2'
+      }]),
+      ['JSON.MSET', '1', '$', '1', '2', '$', '"2"']
+    );
+  });
 
-    testUtils.testWithClient('client.json.mSet', async client => {
-        assert.deepEqual(
-            await client.json.mSet([{
-                key: '1',
-                path: '$',
-                value: 1
-            }, {
-                key: '2',
-                path: '$',
-                value: '2'
-            }]),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.mSet', async client => {
+    assert.equal(
+      await client.json.mSet([{
+        key: '1',
+        path: '$',
+        value: 1
+      }, {
+        key: '2',
+        path: '$',
+        value: '2'
+      }]),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts
index 67228f264d2..81e8d4c6bdf 100644
--- a/packages/json/lib/commands/MSET.ts
+++ b/packages/json/lib/commands/MSET.ts
@@ -1,28 +1,32 @@
-import { RedisJSON, transformRedisJsonArgument } from '.';
-import { RedisCommandArgument } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface JsonMSetItem {
-    key: RedisCommandArgument;
-    path: RedisCommandArgument;
-    value: RedisJSON;
+export interface JsonMSetItem {
+  key: RedisArgument;
+  path: RedisArgument;
+  value: RedisJSON;
 }
 
-export function transformArguments(items: Array<JsonMSetItem>): Array<string> {
-  
-    const args = new Array(1 + items.length * 3);
-    args[0] = 'JSON.MSET';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Sets multiple JSON values in multiple documents.
+   * Returns OK on success.
+   * 
+   * @param parser - The Redis command parser
+   * @param items - Array of objects containing key, path, and value to set
+   * @param items[].key - The key containing the JSON document
+   * @param items[].path - Path in the document to set
+   * @param items[].value - JSON value to set at the path
+   */
+  parseCommand(parser: CommandParser, items: Array<JsonMSetItem>) {
+    parser.push('JSON.MSET');
 
-    let argsIndex = 1;
     for (let i = 0; i < items.length; i++) {
-        const item = items[i];
-        args[argsIndex++] = item.key;
-        args[argsIndex++] = item.path;
-        args[argsIndex++] = transformRedisJsonArgument(item.value);
+      parser.pushKey(items[i].key);
+      parser.push(items[i].path, transformRedisJsonArgument(items[i].value));
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/NUMINCRBY.spec.ts b/packages/json/lib/commands/NUMINCRBY.spec.ts
index 56dede68bde..b438069e80f 100644
--- a/packages/json/lib/commands/NUMINCRBY.spec.ts
+++ b/packages/json/lib/commands/NUMINCRBY.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './NUMINCRBY';
+import NUMINCRBY from './NUMINCRBY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('NUMINCRBY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '$', 1),
-            ['JSON.NUMINCRBY', 'key', '$', '1']
-        );
-    });
+describe('JSON.NUMINCRBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(NUMINCRBY, 'key', '$', 1),
+      ['JSON.NUMINCRBY', 'key', '$', '1']
+    );
+  });
 
-    testUtils.testWithClient('client.json.numIncrBy', async client => {
-        await client.json.set('key', '$', 0);
+  testUtils.testWithClient('client.json.numIncrBy', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', 0),
+      client.json.numIncrBy('key', '$', 1)
+    ]);
 
-        assert.deepEqual(
-            await client.json.numIncrBy('key', '$', 1),
-            [1]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [1]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/NUMINCRBY.ts b/packages/json/lib/commands/NUMINCRBY.ts
index e3d8887ea3d..d8884385354 100644
--- a/packages/json/lib/commands/NUMINCRBY.ts
+++ b/packages/json/lib/commands/NUMINCRBY.ts
@@ -1,7 +1,26 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, DoubleReply, NullReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path: string, by: number): Array<string> {
-    return ['JSON.NUMINCRBY', key, path, by.toString()];
-}
-
-export { transformNumbersReply as transformReply } from '.';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Increments a numeric value stored in a JSON document by a given number.
+   * Returns the value after increment, or null if the key/path doesn't exist or value is not numeric.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param path - Path to the numeric value
+   * @param by - Amount to increment by
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) {
+    parser.push('JSON.NUMINCRBY');
+    parser.pushKey(key);
+    parser.push(path, by.toString());
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<BlobStringReply>) => {
+      return JSON.parse(reply.toString()) as number | Array<null | number>;
+    },
+    3: undefined as unknown as () => ArrayReply<NumberReply | DoubleReply | NullReply>
+  }
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/NUMMULTBY.spec.ts b/packages/json/lib/commands/NUMMULTBY.spec.ts
index 3e2581a3cd8..24ee932e952 100644
--- a/packages/json/lib/commands/NUMMULTBY.spec.ts
+++ b/packages/json/lib/commands/NUMMULTBY.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './NUMMULTBY';
+import NUMMULTBY from './NUMMULTBY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('NUMMULTBY', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '$', 2),
-            ['JSON.NUMMULTBY', 'key', '$', '2']
-        );
-    });
+describe('JSON.NUMMULTBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(NUMMULTBY, 'key', '$', 2),
+      ['JSON.NUMMULTBY', 'key', '$', '2']
+    );
+  });
 
-    testUtils.testWithClient('client.json.numMultBy', async client => {
-        await client.json.set('key', '$', 1);
+  testUtils.testWithClient('client.json.numMultBy', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', 1),
+      client.json.numMultBy('key', '$', 2)
+    ]);
 
-        assert.deepEqual(
-            await client.json.numMultBy('key', '$', 2),
-            [2]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [2]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/NUMMULTBY.ts b/packages/json/lib/commands/NUMMULTBY.ts
index 2082916619a..22b25640d4b 100644
--- a/packages/json/lib/commands/NUMMULTBY.ts
+++ b/packages/json/lib/commands/NUMMULTBY.ts
@@ -1,7 +1,22 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import NUMINCRBY from './NUMINCRBY';
 
-export function transformArguments(key: string, path: string, by: number): Array<string> {
-    return ['JSON.NUMMULTBY', key, path, by.toString()];
-}
-
-export { transformNumbersReply as transformReply } from '.';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Multiplies a numeric value stored in a JSON document by a given number.
+   * Returns the value after multiplication, or null if the key/path doesn't exist or value is not numeric.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param path - Path to the numeric value
+   * @param by - Amount to multiply by
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) {
+    parser.push('JSON.NUMMULTBY');
+    parser.pushKey(key);
+    parser.push(path, by.toString());
+  },
+  transformReply: NUMINCRBY.transformReply
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/OBJKEYS.spec.ts b/packages/json/lib/commands/OBJKEYS.spec.ts
index 6288c112392..0d2176248e4 100644
--- a/packages/json/lib/commands/OBJKEYS.spec.ts
+++ b/packages/json/lib/commands/OBJKEYS.spec.ts
@@ -1,28 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './OBJKEYS';
+import OBJKEYS from './OBJKEYS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('OBJKEYS', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.OBJKEYS', 'key']
-            );
-        });
+describe('JSON.OBJKEYS', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(OBJKEYS, 'key'),
+        ['JSON.OBJKEYS', 'key']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.OBJKEYS', 'key', '$']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(OBJKEYS, 'key', {
+          path: '$'
+        }),
+        ['JSON.OBJKEYS', 'key', '$']
+      );
     });
+  });
 
-    // testUtils.testWithClient('client.json.objKeys', async client => {
-    //     assert.deepEqual(
-    //         await client.json.objKeys('key', '$'),
-    //         [null]
-    //     );
-    // }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.objKeys', async client => {
+    assert.equal(
+      await client.json.objKeys('key'),
+      null
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/OBJKEYS.ts b/packages/json/lib/commands/OBJKEYS.ts
index a9465c9160c..9f8abdc42c4 100644
--- a/packages/json/lib/commands/OBJKEYS.ts
+++ b/packages/json/lib/commands/OBJKEYS.ts
@@ -1,13 +1,27 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.OBJKEYS', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonObjKeysOptions {
+  path?: RedisArgument;
 }
 
-export declare function transformReply(): Array<string> | null | Array<Array<string> | null>;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Returns the keys in the object stored in a JSON document.
+   * Returns array of keys, array of arrays for multiple paths, or null if path doesn't exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to the object to examine
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjKeysOptions) {
+    parser.push('JSON.OBJKEYS');
+    parser.pushKey(key);
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply> | ArrayReply<ArrayReply<BlobStringReply> | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/OBJLEN.spec.ts b/packages/json/lib/commands/OBJLEN.spec.ts
index 35b6589c875..a5664a4d6bc 100644
--- a/packages/json/lib/commands/OBJLEN.spec.ts
+++ b/packages/json/lib/commands/OBJLEN.spec.ts
@@ -1,28 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './OBJLEN';
+import OBJLEN from './OBJLEN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('OBJLEN', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.OBJLEN', 'key']
-            );
-        });
+describe('JSON.OBJLEN', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(OBJLEN, 'key'),
+        ['JSON.OBJLEN', 'key']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.OBJLEN', 'key', '$']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(OBJLEN, 'key', {
+          path: '$'
+        }),
+        ['JSON.OBJLEN', 'key', '$']
+      );
     });
+  });
 
-    // testUtils.testWithClient('client.json.objLen', async client => {
-    //     assert.equal(
-    //         await client.json.objLen('key', '$'),
-    //         [null]
-    //     );
-    // }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.objLen', async client => {
+    assert.equal(
+      await client.json.objLen('key'),
+      null
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/OBJLEN.ts b/packages/json/lib/commands/OBJLEN.ts
index aa800e97f71..0cee11770c1 100644
--- a/packages/json/lib/commands/OBJLEN.ts
+++ b/packages/json/lib/commands/OBJLEN.ts
@@ -1,13 +1,27 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.OBJLEN', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonObjLenOptions {
+  path?: RedisArgument;
 }
 
-export declare function transformReply(): number | null | Array<number | null>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the number of keys in the object stored in a JSON document.
+   * Returns length of object, array of lengths for multiple paths, or null if path doesn't exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to the object to examine
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjLenOptions) {
+    parser.push('JSON.OBJLEN');
+    parser.pushKey(key);
+    if (options?.path !== undefined) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/RESP.spec.ts b/packages/json/lib/commands/RESP.spec.ts
index 8b70962d1c5..2cb3e9e15c3 100644
--- a/packages/json/lib/commands/RESP.spec.ts
+++ b/packages/json/lib/commands/RESP.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RESP';
+import RESP from './RESP';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('RESP', () => {
     describe('transformArguments', () => {
         it('without path', () => {
             assert.deepEqual(
-                transformArguments('key'),
+                parseArgs(RESP, 'key'),
                 ['JSON.RESP', 'key']
             );
         });
 
         it('with path', () => {
             assert.deepEqual(
-                transformArguments('key', '$'),
+                parseArgs(RESP, 'key', '$'),
                 ['JSON.RESP', 'key', '$']
             );
         });
diff --git a/packages/json/lib/commands/RESP.ts b/packages/json/lib/commands/RESP.ts
index fcf54cd3537..79cc2dac8d1 100644
--- a/packages/json/lib/commands/RESP.ts
+++ b/packages/json/lib/commands/RESP.ts
@@ -1,15 +1,24 @@
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.RESP', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
-}
+import { CommandParser } from "@redis/client/dist/lib/client/parser";
+import { Command, RedisArgument } from "@redis/client/dist/lib/RESP/types";
 
 type RESPReply = Array<string | number | RESPReply>;
 
-export declare function transformReply(): RESPReply;
+export default {
+    IS_READ_ONLY: true,
+    /**
+     * Returns the JSON value at the specified path in RESP (Redis Serialization Protocol) format.
+     * Returns the value in RESP form, useful for language-independent processing.
+     * 
+     * @param parser - The Redis command parser
+     * @param key - The key containing the JSON document
+     * @param path - Optional path to the value in the document
+     */
+    parseCommand(parser: CommandParser, key: RedisArgument, path?: string) {
+      parser.push('JSON.RESP');
+      parser.pushKey(key);
+      if (path !== undefined) {
+        parser.push(path);
+      }
+    },
+    transformReply: undefined as unknown as () => RESPReply
+  } as const satisfies Command;
\ No newline at end of file
diff --git a/packages/json/lib/commands/SET.spec.ts b/packages/json/lib/commands/SET.spec.ts
index 8f8586a2047..7bd927f08e4 100644
--- a/packages/json/lib/commands/SET.spec.ts
+++ b/packages/json/lib/commands/SET.spec.ts
@@ -1,35 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SET';
+import SET from './SET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SET', () => {
-    describe('transformArguments', () => {
-        it('transformArguments', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'json'),
-                ['JSON.SET', 'key', '$', '"json"']
-            );
-        });
+describe('JSON.SET', () => {
+  describe('transformArguments', () => {
+    it('transformArguments', () => {
+      assert.deepEqual(
+        parseArgs(SET, 'key', '$', 'json'),
+        ['JSON.SET', 'key', '$', '"json"']
+      );
+    });
 
-        it('NX', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'json', { NX: true }),
-                ['JSON.SET', 'key', '$', '"json"', 'NX']
-            );
-        });
+    it('NX', () => {
+      assert.deepEqual(
+        parseArgs(SET, 'key', '$', 'json', { NX: true }),
+        ['JSON.SET', 'key', '$', '"json"', 'NX']
+      );
+    });
 
-        it('XX', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'json', { XX: true }),
-                ['JSON.SET', 'key', '$', '"json"', 'XX']
-            );
-        });
+    it('XX', () => {
+      assert.deepEqual(
+        parseArgs(SET, 'key', '$', 'json', { XX: true }),
+        ['JSON.SET', 'key', '$', '"json"', 'XX']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.mGet', async client => {
-        assert.equal(
-            await client.json.set('key', '$', 'json'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.set', async client => {
+    assert.equal(
+      await client.json.set('key', '$', 'json'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts
index f50a42bf5db..9ab680b4898 100644
--- a/packages/json/lib/commands/SET.ts
+++ b/packages/json/lib/commands/SET.ts
@@ -1,25 +1,52 @@
-import { RedisJSON, transformRedisJsonArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface NX {
-    NX: true;
-}
-
-interface XX {
-    XX: true;
+export interface JsonSetOptions {
+  condition?: 'NX' | 'XX';
+  /**
+   * @deprecated Use `{ condition: 'NX' }` instead.
+   */
+  NX?: boolean;
+  /**
+   * @deprecated Use `{ condition: 'XX' }` instead.
+   */
+  XX?: boolean;
 }
 
-export function transformArguments(key: string, path: string, json: RedisJSON, options?: NX | XX): Array<string> {
-    const args = ['JSON.SET', key, path, transformRedisJsonArgument(json)];
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Sets a JSON value at a specific path in a JSON document.
+   * Returns OK on success, or null if condition (NX/XX) is not met.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param path - Path in the document to set
+   * @param json - JSON value to set at the path
+   * @param options - Optional parameters
+   * @param options.condition - Set condition: NX (only if doesn't exist) or XX (only if exists)
+   * @deprecated options.NX - Use options.condition instead
+   * @deprecated options.XX - Use options.condition instead
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    path: RedisArgument,
+    json: RedisJSON,
+    options?: JsonSetOptions
+  ) {
+    parser.push('JSON.SET');
+    parser.pushKey(key);
+    parser.push(path, transformRedisJsonArgument(json));
 
-    if ((<NX>options)?.NX) {
-        args.push('NX');
-    } else if ((<XX>options)?.XX) {
-        args.push('XX');
+    if (options?.condition) {
+      parser.push(options?.condition);
+    } else if (options?.NX) {
+      parser.push('NX');
+    } else if (options?.XX) {
+      parser.push('XX');
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK' | null;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | NullReply
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/STRAPPEND.spec.ts b/packages/json/lib/commands/STRAPPEND.spec.ts
index a37eaa1d91c..ebd539130e1 100644
--- a/packages/json/lib/commands/STRAPPEND.spec.ts
+++ b/packages/json/lib/commands/STRAPPEND.spec.ts
@@ -1,30 +1,33 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './STRAPPEND';
+import STRAPPEND from './STRAPPEND';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('STRAPPEND', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key', 'append'),
-                ['JSON.STRAPPEND', 'key', '"append"']
-            );
-        });
+describe('JSON.STRAPPEND', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(STRAPPEND, 'key', 'append'),
+        ['JSON.STRAPPEND', 'key', '"append"']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$', 'append'),
-                ['JSON.STRAPPEND', 'key', '$', '"append"']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(STRAPPEND, 'key', 'append', {
+          path: '$'
+        }),
+        ['JSON.STRAPPEND', 'key', '$', '"append"']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.strAppend', async client => {
-        await client.json.set('key', '$', '');
+  testUtils.testWithClient('client.json.strAppend', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', ''),
+      client.json.strAppend('key', 'append')
+    ]);
 
-        assert.deepEqual(
-            await client.json.strAppend('key', '$', 'append'),
-            [6]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, 6);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts
index eea384c93fd..b3115f684c3 100644
--- a/packages/json/lib/commands/STRAPPEND.ts
+++ b/packages/json/lib/commands/STRAPPEND.ts
@@ -1,21 +1,32 @@
-import { transformRedisJsonArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types';
+import { transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
-
-type AppendArguments = [key: string, append: string];
-
-type AppendWithPathArguments = [key: string, path: string, append: string];
+export interface JsonStrAppendOptions {
+  path?: RedisArgument;
+}
 
-export function transformArguments(...[key, pathOrAppend, append]: AppendArguments | AppendWithPathArguments): Array<string> {
-    const args = ['JSON.STRAPPEND', key];
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Appends a string to a string value stored in a JSON document.
+   * Returns new string length after append, or null if the path doesn't exist or value is not a string.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param append - String to append
+   * @param options - Optional parameters
+   * @param options.path - Path to the string value
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, append: string, options?: JsonStrAppendOptions) {
+    parser.push('JSON.STRAPPEND');
+    parser.pushKey(key);
 
-    if (append !== undefined && append !== null) {
-        args.push(pathOrAppend, transformRedisJsonArgument(append));
-    } else {
-        args.push(transformRedisJsonArgument(pathOrAppend));
+    if (options?.path !== undefined) {
+      parser.push(options.path);
     }
 
-    return args;
-}
-
-export declare function transformReply(): number | Array<number>;
+    parser.push(transformRedisJsonArgument(append));
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NullReply | NumberReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/STRLEN.spec.ts b/packages/json/lib/commands/STRLEN.spec.ts
index cf163d3c19e..b6881b5bd52 100644
--- a/packages/json/lib/commands/STRLEN.spec.ts
+++ b/packages/json/lib/commands/STRLEN.spec.ts
@@ -1,30 +1,33 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './STRLEN';
+import STRLEN from './STRLEN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('STRLEN', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.STRLEN', 'key']
-            );
-        });
+describe('JSON.STRLEN', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(STRLEN, 'key'),
+        ['JSON.STRLEN', 'key']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.STRLEN', 'key', '$']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(STRLEN, 'key', {
+          path: '$'
+        }),
+        ['JSON.STRLEN', 'key', '$']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.json.strLen', async client => {
-        await client.json.set('key', '$', '');
+  testUtils.testWithClient('client.json.strLen', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', ''),
+      client.json.strLen('key')
+    ]);
 
-        assert.deepEqual(
-            await client.json.strLen('key', '$'),
-            [0]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, 0);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/STRLEN.ts b/packages/json/lib/commands/STRLEN.ts
index 93f5d563baf..ca1923d7c6a 100644
--- a/packages/json/lib/commands/STRLEN.ts
+++ b/packages/json/lib/commands/STRLEN.ts
@@ -1,15 +1,28 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const IS_READ_ONLY = true;
+export interface JsonStrLenOptions {
+  path?: RedisArgument;
+}
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.STRLEN', key];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the length of a string value stored in a JSON document.
+   * Returns string length, array of lengths for multiple paths, or null if path doesn't exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to the string value
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonStrLenOptions) {
+    parser.push('JSON.STRLEN');
+    parser.pushKey(key);
 
-    if (path) {
-        args.push(path);
+    if (options?.path) {
+      parser.push(options.path);
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/TOGGLE.spec.ts b/packages/json/lib/commands/TOGGLE.spec.ts
new file mode 100644
index 00000000000..173c7708f4a
--- /dev/null
+++ b/packages/json/lib/commands/TOGGLE.spec.ts
@@ -0,0 +1,22 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import TOGGLE from './TOGGLE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('JSON.TOGGLE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(TOGGLE, 'key', '$'),
+      ['JSON.TOGGLE', 'key', '$']
+    );
+  });
+
+  testUtils.testWithClient('client.json.toggle', async client => {
+    const [, reply] = await Promise.all([
+      client.json.set('key', '$', true),
+      client.json.toggle('key', '$')
+    ]);
+
+    assert.deepEqual(reply, [0]);
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/json/lib/commands/TOGGLE.ts b/packages/json/lib/commands/TOGGLE.ts
new file mode 100644
index 00000000000..2d93d391164
--- /dev/null
+++ b/packages/json/lib/commands/TOGGLE.ts
@@ -0,0 +1,20 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/dist/lib/RESP/types';
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Toggles a boolean value stored in a JSON document.
+   * Returns 1 if value was toggled to true, 0 if toggled to false, or null if path doesn't exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param path - Path to the boolean value
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument) {
+    parser.push('JSON.TOGGLE');
+    parser.pushKey(key);
+    parser.push(path);
+  },
+  transformReply: undefined as unknown as () => NumberReply | NullReply | ArrayReply<NumberReply | NullReply>
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/TYPE.spec.ts b/packages/json/lib/commands/TYPE.spec.ts
index 5cecfb827a7..1b6ad109816 100644
--- a/packages/json/lib/commands/TYPE.spec.ts
+++ b/packages/json/lib/commands/TYPE.spec.ts
@@ -1,28 +1,31 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './TYPE';
+import TYPE from './TYPE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TYPE', () => {
-    describe('transformArguments', () => {
-        it('without path', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['JSON.TYPE', 'key']
-            );
-        });
+describe('JSON.TYPE', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(TYPE, 'key'),
+        ['JSON.TYPE', 'key']
+      );
+    });
 
-        it('with path', () => {
-            assert.deepEqual(
-                transformArguments('key', '$'),
-                ['JSON.TYPE', 'key', '$']
-            );
-        });
+    it('with path', () => {
+      assert.deepEqual(
+        parseArgs(TYPE, 'key', {
+          path: '$'
+        }),
+        ['JSON.TYPE', 'key', '$']
+      );
     });
+  });
 
-    // testUtils.testWithClient('client.json.type', async client => {
-    //     assert.deepEqual(
-    //         await client.json.type('key', '$'),
-    //         [null]
-    //     );
-    // }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.json.type', async client => {
+    assert.equal(
+      await client.json.type('key'),
+      null
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/json/lib/commands/TYPE.ts b/packages/json/lib/commands/TYPE.ts
index 7fd55f625dc..758335a7361 100644
--- a/packages/json/lib/commands/TYPE.ts
+++ b/packages/json/lib/commands/TYPE.ts
@@ -1,13 +1,34 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string, path?: string): Array<string> {
-    const args = ['JSON.TYPE', key];
-
-    if (path) {
-        args.push(path);
-    }
-
-    return args;
+export interface JsonTypeOptions {
+  path?: RedisArgument;
 }
 
-export declare function transformReply(): string | null | Array<string | null>;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Returns the type of JSON value at a specific path in a JSON document.
+   * Returns the type as a string, array of types for multiple paths, or null if path doesn't exist.
+   * 
+   * @param parser - The Redis command parser
+   * @param key - The key containing the JSON document
+   * @param options - Optional parameters
+   * @param options.path - Path to examine
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonTypeOptions) {
+    parser.push('JSON.TYPE');
+    parser.pushKey(key);
+
+    if (options?.path) {
+      parser.push(options.path);
+    }
+  },
+  transformReply: {
+    2: undefined as unknown as () => NullReply | BlobStringReply | ArrayReply<BlobStringReply | NullReply>,
+    // TODO: RESP3 wraps the response in another array, but only returns 1 
+    3: (reply: UnwrapReply<ArrayReply<NullReply | BlobStringReply | ArrayReply<BlobStringReply | NullReply>>>) => {
+      return reply[0];
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts
index 9d0a82ec271..0e29bdd648d 100644
--- a/packages/json/lib/commands/index.ts
+++ b/packages/json/lib/commands/index.ts
@@ -1,96 +1,86 @@
-import * as ARRAPPEND from './ARRAPPEND';
-import * as ARRINDEX from './ARRINDEX';
-import * as ARRINSERT from './ARRINSERT';
-import * as ARRLEN from './ARRLEN';
-import * as ARRPOP from './ARRPOP';
-import * as ARRTRIM from './ARRTRIM';
-import * as DEBUG_MEMORY from './DEBUG_MEMORY';
-import * as DEL from './DEL';
-import * as FORGET from './FORGET';
-import * as GET from './GET';
-import * as MERGE from './MERGE';
-import * as MGET from './MGET';
-import * as MSET from './MSET';
-import * as NUMINCRBY from './NUMINCRBY';
-import * as NUMMULTBY from './NUMMULTBY';
-import * as OBJKEYS from './OBJKEYS';
-import * as OBJLEN from './OBJLEN';
-import * as RESP from './RESP';
-import * as SET from './SET';
-import * as STRAPPEND from './STRAPPEND';
-import * as STRLEN from './STRLEN';
-import * as TYPE from './TYPE';
+import ARRAPPEND from './ARRAPPEND';
+import ARRINDEX from './ARRINDEX';
+import ARRINSERT from './ARRINSERT';
+import ARRLEN from './ARRLEN';
+import ARRPOP from './ARRPOP';
+import ARRTRIM from './ARRTRIM';
+import CLEAR from './CLEAR';
+import DEBUG_MEMORY from './DEBUG_MEMORY';
+import DEL from './DEL';
+import FORGET from './FORGET';
+import GET from './GET';
+import MERGE from './MERGE';
+import MGET from './MGET';
+import MSET from './MSET';
+import NUMINCRBY from './NUMINCRBY';
+import NUMMULTBY from './NUMMULTBY';
+import OBJKEYS from './OBJKEYS';
+import OBJLEN from './OBJLEN';
+// import RESP from './RESP';
+import SET from './SET';
+import STRAPPEND from './STRAPPEND';
+import STRLEN from './STRLEN';
+import TOGGLE from './TOGGLE';
+import TYPE from './TYPE';
+
+// Re-export helper types and functions from client package
+export type { RedisJSON } from '@redis/client/dist/lib/commands/generic-transformers';
+export { transformRedisJsonArgument, transformRedisJsonReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
 export default {
-    ARRAPPEND,
-    arrAppend: ARRAPPEND,
-    ARRINDEX,
-    arrIndex: ARRINDEX,
-    ARRINSERT,
-    arrInsert: ARRINSERT,
-    ARRLEN,
-    arrLen: ARRLEN,
-    ARRPOP,
-    arrPop: ARRPOP,
-    ARRTRIM,
-    arrTrim: ARRTRIM,
-    DEBUG_MEMORY,
-    debugMemory: DEBUG_MEMORY,
-    DEL,
-    del: DEL,
-    FORGET,
-    forget: FORGET,
-    GET,
-    get: GET,
-    MERGE,
-    merge: MERGE,
-    MGET,
-    mGet: MGET,
-    MSET,
-    mSet: MSET,
-    NUMINCRBY,
-    numIncrBy: NUMINCRBY,
-    NUMMULTBY,
-    numMultBy: NUMMULTBY,
-    OBJKEYS,
-    objKeys: OBJKEYS,
-    OBJLEN,
-    objLen: OBJLEN,
-    RESP,
-    resp: RESP,
-    SET,
-    set: SET,
-    STRAPPEND,
-    strAppend: STRAPPEND,
-    STRLEN,
-    strLen: STRLEN,
-    TYPE,
-    type: TYPE
+  ARRAPPEND,
+  arrAppend: ARRAPPEND,
+  ARRINDEX,
+  arrIndex: ARRINDEX,
+  ARRINSERT,
+  arrInsert: ARRINSERT,
+  ARRLEN,
+  arrLen: ARRLEN,
+  ARRPOP,
+  arrPop: ARRPOP,
+  ARRTRIM,
+  arrTrim: ARRTRIM,
+  CLEAR,
+  clear: CLEAR,
+  DEBUG_MEMORY,
+  debugMemory: DEBUG_MEMORY,
+  DEL,
+  del: DEL,
+  FORGET,
+  forget: FORGET,
+  GET,
+  get: GET,
+  MERGE,
+  merge: MERGE,
+  MGET,
+  mGet: MGET,
+  MSET,
+  mSet: MSET,
+  NUMINCRBY,
+  numIncrBy: NUMINCRBY,
+  /**
+   * @deprecated since JSON version 2.0
+   */
+  NUMMULTBY,
+  /**
+   * @deprecated since JSON version 2.0
+   */
+  numMultBy: NUMMULTBY,
+  OBJKEYS,
+  objKeys: OBJKEYS,
+  OBJLEN,
+  objLen: OBJLEN,
+  // RESP,
+  // resp: RESP,
+  SET,
+  set: SET,
+  STRAPPEND,
+  strAppend: STRAPPEND,
+  STRLEN,
+  strLen: STRLEN,
+  TOGGLE,
+  toggle: TOGGLE,
+  TYPE,
+  type: TYPE
 };
 
-// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-interface RedisJSONArray extends Array<RedisJSON> {}
-interface RedisJSONObject {
-    [key: string]: RedisJSON;
-    [key: number]: RedisJSON;
-}
-export type RedisJSON = null | boolean | number | string | Date | RedisJSONArray | RedisJSONObject;
-
-export function transformRedisJsonArgument(json: RedisJSON): string {
-    return JSON.stringify(json);
-}
-
-export function transformRedisJsonReply(json: string): RedisJSON {
-    return JSON.parse(json);
-}
-
-export function transformRedisJsonNullReply(json: string | null): RedisJSON | null {
-    if (json === null) return null;
-
-    return transformRedisJsonReply(json);
-}
-
-export function transformNumbersReply(reply: string): number | Array<number> {
-    return JSON.parse(reply);
-}
diff --git a/packages/json/lib/index.ts b/packages/json/lib/index.ts
index bc0e103e8c8..1993f9ef42d 100644
--- a/packages/json/lib/index.ts
+++ b/packages/json/lib/index.ts
@@ -1 +1,2 @@
 export { default } from './commands';
+export type { RedisJSON } from './commands';
diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts
index 55426890e00..cba2d95e737 100644
--- a/packages/json/lib/test-utils.ts
+++ b/packages/json/lib/test-utils.ts
@@ -1,21 +1,21 @@
 import TestUtils from '@redis/test-utils';
 import RedisJSON from '.';
 
-export default new TestUtils({
-    dockerImageName: 'redislabs/rejson',
-    dockerImageVersionArgument: 'rejson-version',
-    defaultDockerVersion: '2.6.9'
+export default TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '8.2-rc1'
 });
 
 export const GLOBAL = {
-    SERVERS: {
-        OPEN: {
-            serverArguments: ['--loadmodule /usr/lib/redis/modules/rejson.so'],
-            clientOptions: {
-                modules: {
-                    json: RedisJSON
-                }
-            }
+  SERVERS: {
+    OPEN: {
+      serverArguments: [],
+      clientOptions: {
+        modules: {
+          json: RedisJSON
         }
+      }
     }
+  }
 };
diff --git a/packages/json/package.json b/packages/json/package.json
index ad60cc13c26..fac6209c360 100644
--- a/packages/json/package.json
+++ b/packages/json/package.json
@@ -1,30 +1,25 @@
 {
   "name": "@redis/json",
-  "version": "1.0.7",
+  "version": "5.7.0",
   "license": "MIT",
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
+  "main": "./dist/lib/index.js",
+  "types": "./dist/lib/index.d.ts",
   "files": [
-    "dist/"
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
   ],
   "scripts": {
-    "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
-    "build": "tsc",
-    "documentation": "typedoc"
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
+    "release": "release-it"
   },
   "peerDependencies": {
-    "@redis/client": "^1.0.0"
+    "@redis/client": "^5.7.0"
   },
   "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
-    "@redis/test-utils": "*",
-    "@types/node": "^20.6.2",
-    "nyc": "^15.1.0",
-    "release-it": "^16.1.5",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typedoc": "^0.25.1",
-    "typescript": "^5.2.2"
+    "@redis/test-utils": "*"
+  },
+  "engines": {
+    "node": ">= 18"
   },
   "repository": {
     "type": "git",
diff --git a/packages/redis/.release-it.json b/packages/redis/.release-it.json
new file mode 100644
index 00000000000..af127d79ae0
--- /dev/null
+++ b/packages/redis/.release-it.json
@@ -0,0 +1,27 @@
+{
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
+  "git": {
+    "tagName": "redis@${version}",
+    "tagMatch": "redis@*",
+    "commitMessage": "Release ${tagName}",
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
+  },
+  "plugins": {
+    "@release-it/bumper": {
+      "out": {
+        "file": "package.json",
+        "path": [
+          "dependencies.@redis/client",
+          "dependencies.@redis/bloom",
+          "dependencies.@redis/json",
+          "dependencies.@redis/search",
+          "dependencies.@redis/time-series"
+        ]
+      }
+    }
+  }
+}
diff --git a/packages/redis/README.md b/packages/redis/README.md
new file mode 100644
index 00000000000..ab6b4707e6f
--- /dev/null
+++ b/packages/redis/README.md
@@ -0,0 +1,330 @@
+# Node-Redis
+
+[![Tests](https://img.shields.io/github/actions/workflow/status/redis/node-redis/tests.yml?branch=master)](https://github.com/redis/node-redis/actions/workflows/tests.yml)
+[![Coverage](https://codecov.io/gh/redis/node-redis/branch/master/graph/badge.svg?token=xcfqHhJC37)](https://codecov.io/gh/redis/node-redis)
+[![License](https://img.shields.io/github/license/redis/node-redis.svg)](https://github.com/redis/node-redis/blob/master/LICENSE)
+
+[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/redis)
+[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc)
+[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc)
+[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc)
+
+node-redis is a modern, high performance [Redis](https://redis.io) client for Node.js.
+
+## How do I Redis?
+
+[Learn for free at Redis University](https://university.redis.com/)
+
+[Build faster with the Redis Launchpad](https://launchpad.redis.com/)
+
+[Try the Redis Cloud](https://redis.com/try-free/)
+
+[Dive in developer tutorials](https://developer.redis.com/)
+
+[Join the Redis community](https://redis.com/community/)
+
+[Work at Redis](https://redis.com/company/careers/jobs/)
+
+## Installation
+
+Start a redis via docker:
+
+```bash
+docker run -p 6379:6379 -d redis:8.0-rc1
+```
+
+To install node-redis, simply:
+
+```bash
+npm install redis
+```
+> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands,
+> you can install the individual packages. See the list below.
+
+## Packages
+
+| Name                                           | Description                                                                                 |
+| ---------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis)                    | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules |
+| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client)           | The base clients (i.e `RedisClient`, `RedisCluster`, etc.)                                  |
+| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom)             | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands                     |
+| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json)               | [Redis JSON](https://redis.io/docs/data-types/json/) commands                               |
+| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search)           | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands                     |
+| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands                  |
+| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/packages/entraid)         | Secure token-based authentication for Redis clients using Microsoft Entra ID                |
+
+> Looking for a high-level library to handle object mapping?
+> See [redis-om-node](https://github.com/redis/redis-om-node)!
+
+
+## Usage
+
+### Basic Example
+
+```typescript
+import { createClient } from "redis";
+
+const client = await createClient()
+  .on("error", (err) => console.log("Redis Client Error", err))
+  .connect();
+
+await client.set("key", "value");
+const value = await client.get("key");
+client.destroy();
+```
+
+The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in
+the format `redis[s]://[[username][:password]@][host][:port][/db-number]`:
+
+```typescript
+createClient({
+  url: "redis://alice:foobared@awesome.redis.server:6380",
+});
+```
+
+You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in
+the [client configuration guide](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md).
+
+To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean.
+`client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it
+isn't (for example when the client is still connecting or reconnecting after a network error).
+
+### Redis Commands
+
+There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed
+using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`,
+etc.):
+
+```typescript
+// raw Redis commands
+await client.HSET("key", "field", "value");
+await client.HGETALL("key");
+
+// friendly JavaScript commands
+await client.hSet("key", "field", "value");
+await client.hGetAll("key");
+```
+
+Modifiers to commands are specified using a JavaScript object:
+
+```typescript
+await client.set("key", "value", {
+  EX: 10,
+  NX: true,
+});
+```
+
+Replies will be transformed into useful data structures:
+
+```typescript
+await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' }
+await client.hVals("key"); // ['value1', 'value2']
+```
+
+`Buffer`s are supported as well:
+
+```typescript
+const client = createClient().withTypeMapping({
+  [RESP_TYPES.BLOB_STRING]: Buffer
+});
+
+await client.hSet("key", "field", Buffer.from("value")); // 'OK'
+await client.hGet("key", "field"); // { field: <Buffer 76 61 6c 75 65> }
+
+```
+
+### Unsupported Redis Commands
+
+If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`:
+
+```typescript
+await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK'
+
+await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2']
+```
+
+### Transactions (Multi/Exec)
+
+Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When
+you're done, call `.exec()` and you'll get an array back with your results:
+
+```typescript
+await client.set("another-key", "another-value");
+
+const [setKeyReply, otherKeyValue] = await client
+  .multi()
+  .set("key", "value")
+  .get("another-key")
+  .exec(); // ['OK', 'another-value']
+```
+
+You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling
+`.watch()`. Your transaction will abort if any of the watched keys change.
+
+
+### Blocking Commands
+
+In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main"
+connection. However, there was no way to use the pool without a "main" connection:
+
+```javascript
+const client = await createClient()
+  .on("error", (err) => console.error(err))
+  .connect();
+
+await client.ping(client.commandOptions({ isolated: true }));
+```
+
+In v5 we've extracted this pool logic into its own class—`RedisClientPool`:
+
+```javascript
+const pool = await createClientPool()
+  .on("error", (err) => console.error(err))
+  .connect();
+
+await pool.ping();
+```
+
+
+### Pub/Sub
+
+See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md).
+
+### Scan Iterator
+
+[`SCAN`](https://redis.io/commands/scan) results can be looped over
+using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
+
+```typescript
+for await (const key of client.scanIterator()) {
+  // use the key!
+  await client.get(key);
+}
+```
+
+This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
+
+```typescript
+for await (const { field, value } of client.hScanIterator("hash")) {
+}
+for await (const member of client.sScanIterator("set")) {
+}
+for await (const { score, value } of client.zScanIterator("sorted-set")) {
+}
+```
+
+You can override the default options by providing a configuration object:
+
+```typescript
+client.scanIterator({
+  TYPE: "string", // `SCAN` only
+  MATCH: "patter*",
+  COUNT: 100,
+});
+```
+
+### Disconnecting
+
+The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead
+of sending a `QUIT` command to the server, the client can simply close the network connection.
+
+`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to
+`client.destroy()`.
+
+```typescript
+client.destroy();
+```
+### Client Side Caching
+
+Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid.
+
+```typescript
+// Enable client side caching with RESP3
+const client = createClient({
+  RESP: 3, 
+  clientSideCache: {
+    ttl: 0,             // Time-to-live (0 = no expiration)
+    maxEntries: 0,      // Maximum entries (0 = unlimited)
+    evictPolicy: "LRU"  // Eviction policy: "LRU" or "FIFO"
+  }
+});
+```
+
+See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage.
+
+### Auto-Pipelining
+
+Node Redis will automatically pipeline requests that are made during the same "tick".
+
+```typescript
+client.set("Tm9kZSBSZWRpcw==", "users:1");
+client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw==");
+```
+
+Of course, if you don't do something with your Promises you're certain to
+get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take
+advantage of auto-pipelining and handle your Promises, use `Promise.all()`.
+
+```typescript
+await Promise.all([
+  client.set("Tm9kZSBSZWRpcw==", "users:1"),
+  client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="),
+]);
+```
+
+### Programmability
+
+See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md).
+
+### Clustering
+
+Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster.
+
+### Events
+
+The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes:
+
+| Name                    | When                                                                               | Listener arguments                                        |
+| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- |
+| `connect`               | Initiating a connection to the server                                              | _No arguments_                                            |
+| `ready`                 | Client is ready to use                                                             | _No arguments_                                            |
+| `end`                   | Connection has been closed (via `.disconnect()`)                                   | _No arguments_                                            |
+| `error`                 | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)`                                          |
+| `reconnecting`          | Client is trying to reconnect to the server                                        | _No arguments_                                            |
+| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event)                          | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) |
+
+> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and
+> an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details.
+
+> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above.
+
+## Supported Redis versions
+
+Node Redis is supported with the following versions of Redis:
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 8.0.z   | :heavy_check_mark: |
+| 7.4.z   | :heavy_check_mark: |
+| 7.2.z   | :heavy_check_mark: |
+| < 7.2   | :x:                |
+
+> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support.
+
+## Migration
+
+- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md)
+- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md)
+- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md)
+
+## Contributing
+
+If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md).
+
+Thank you to all the people who already contributed to Node Redis!
+
+[![Contributors](https://contrib.rocks/image?repo=redis/node-redis)](https://github.com/redis/node-redis/graphs/contributors)
+
+## License
+
+This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE).
diff --git a/packages/redis/index.ts b/packages/redis/index.ts
new file mode 100644
index 00000000000..61da052ea20
--- /dev/null
+++ b/packages/redis/index.ts
@@ -0,0 +1,113 @@
+import {
+  RedisModules,
+  RedisFunctions,
+  RedisScripts,
+  RespVersions,
+  TypeMapping,
+  createClient as genericCreateClient,
+  RedisClientOptions,
+  RedisClientType as GenericRedisClientType,
+  createCluster as genericCreateCluster,
+  RedisClusterOptions,
+  RedisClusterType as genericRedisClusterType,
+  RedisSentinelOptions,
+  RedisSentinelType as genericRedisSentinelType,
+  createSentinel as genericCreateSentinel
+} from '@redis/client';
+import RedisBloomModules from '@redis/bloom';
+import RedisJSON from '@redis/json';
+import RediSearch from '@redis/search';
+import RedisTimeSeries from '@redis/time-series';
+
+export * from '@redis/client';
+export * from '@redis/bloom';
+export * from '@redis/json';
+export * from '@redis/search';
+export * from '@redis/time-series';
+
+const modules = {
+  ...RedisBloomModules,
+  json: RedisJSON,
+  ft: RediSearch,
+  ts: RedisTimeSeries
+};
+
+export type RedisDefaultModules = typeof modules;
+
+export type RedisClientType<
+  M extends RedisModules = RedisDefaultModules,
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> = GenericRedisClientType<M, F, S, RESP, TYPE_MAPPING>;
+
+export function createClient<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+>(
+  options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>
+): GenericRedisClientType<RedisDefaultModules & M, F, S, RESP, TYPE_MAPPING> {
+  return genericCreateClient({
+    ...options,
+    modules: {
+      ...modules,
+      ...(options?.modules as M)
+    }
+  });
+}
+
+export type RedisClusterType<
+  M extends RedisModules = RedisDefaultModules,
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> = genericRedisClusterType<M, F, S, RESP, TYPE_MAPPING>;
+
+export function createCluster<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+>(
+  options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING>
+): RedisClusterType<RedisDefaultModules & M, F, S, RESP, TYPE_MAPPING> {
+  return genericCreateCluster({
+    ...options,
+    modules: {
+      ...modules,
+      ...(options?.modules as M)
+    }
+  });
+}
+
+export type RedisSentinelType<
+  M extends RedisModules = RedisDefaultModules,
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+> = genericRedisSentinelType<M, F, S, RESP, TYPE_MAPPING>;
+
+export function createSentinel<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+>(
+  options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>
+): RedisSentinelType<RedisDefaultModules & M, F, S, RESP, TYPE_MAPPING> {
+  return genericCreateSentinel({
+    ...options,
+    modules: {
+      ...modules,
+      ...(options?.modules as M)
+    }
+  });
+}
diff --git a/packages/redis/package.json b/packages/redis/package.json
new file mode 100644
index 00000000000..a14f6ba10bc
--- /dev/null
+++ b/packages/redis/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "redis",
+  "description": "A modern, high performance Redis client",
+  "version": "5.7.0",
+  "license": "MIT",
+  "main": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
+  ],
+  "scripts": {
+    "release": "release-it"
+  },
+  "dependencies": {
+    "@redis/bloom": "5.7.0",
+    "@redis/client": "5.7.0",
+    "@redis/json": "5.7.0",
+    "@redis/search": "5.7.0",
+    "@redis/time-series": "5.7.0"
+  },
+  "engines": {
+    "node": ">= 18"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/redis/node-redis.git"
+  },
+  "bugs": {
+    "url": "https://github.com/redis/node-redis/issues"
+  },
+  "homepage": "https://github.com/redis/node-redis",
+  "keywords": [
+    "redis"
+  ]
+}
diff --git a/packages/redis/tsconfig.json b/packages/redis/tsconfig.json
new file mode 100644
index 00000000000..50da0ba733a
--- /dev/null
+++ b/packages/redis/tsconfig.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "outDir": "./dist"
+  },
+  "include": [
+    "./index.ts"
+  ]
+}
diff --git a/packages/search/.npmignore b/packages/search/.npmignore
deleted file mode 100644
index bbef2b404fb..00000000000
--- a/packages/search/.npmignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.nyc_output/
-coverage/
-lib/
-.nycrc.json
-.release-it.json
-tsconfig.json
diff --git a/packages/search/.release-it.json b/packages/search/.release-it.json
index 72cb1016ef4..85d55c087de 100644
--- a/packages/search/.release-it.json
+++ b/packages/search/.release-it.json
@@ -1,10 +1,22 @@
 {
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
   "git": {
     "tagName": "search@${version}",
+    "tagMatch": "search@*",
     "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
   },
-  "npm": {
-    "publishArgs": ["--access", "public"]
+  "plugins": {
+    "@release-it/bumper": {
+      "out": {
+        "file": "package.json",
+        "path": ["peerDependencies.@redis/client"],
+        "versionPrefix": "^"
+      }
+    }
   }
 }
diff --git a/packages/search/README.md b/packages/search/README.md
index 60186ba7f92..37597e6580a 100644
--- a/packages/search/README.md
+++ b/packages/search/README.md
@@ -1,27 +1,29 @@
 # @redis/search
 
-This package provides support for the [RediSearch](https://redisearch.io) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the RedisJSON module.  It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediSearch commands.
+This package provides support for the [RediSearch](https://redis.io/docs/interact/search-and-query/) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the [RedisJSON](https://redis.io/docs/data-types/json/) module.
 
-To use these extra commands, your Redis server must have the RediSearch module installed.  To index and query JSON documents, you'll also need to add the RedisJSON module.
+Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis).
+
+:warning: To use these extra commands, your Redis server must have the RediSearch module installed. To index and query JSON documents, you'll also need to add the RedisJSON module.
 
 ## Usage
 
-For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the Node Redis examples folder.
+For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples).
 
 ### Indexing and Querying Data in Redis Hashes
 
 #### Creating an Index
 
-Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in.  The [FT.CREATE](https://redis.io/commands/ft.create) command creates a RediSearch index.  Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`:
+Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in.  The [FT.CREATE](https://redis.io/commands/ft.create) command creates a RediSearch index. Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`:
 
 ```javascript
 await client.ft.create('idx:animals', {
   name: {
-    type: SchemaFieldTypes.TEXT,
+    type: SCHEMA_FIELD_TYPE.TEXT,
     SORTABLE: true
   },
-  species: SchemaFieldTypes.TAG,
-  age: SchemaFieldTypes.NUMERIC
+  species: SCHEMA_FIELD_TYPE.TAG,
+  age: SCHEMA_FIELD_TYPE.NUMERIC
 }, {
   ON: 'HASH',
   PREFIX: 'noderedis:animals'
@@ -89,15 +91,15 @@ One way we might choose to index these documents is as follows:
 ```javascript
 await client.ft.create('idx:users', {
   '$.name': {
-    type: SchemaFieldTypes.TEXT,
+    type: SCHEMA_FIELD_TYPE.TEXT,
     SORTABLE: 'UNF'
   },
   '$.age': {
-    type: SchemaFieldTypes.NUMERIC,
+    type: SCHEMA_FIELD_TYPE.NUMERIC,
     AS: 'age'
   },
   '$.coins': {
-    type: SchemaFieldTypes.NUMERIC,
+    type: SCHEMA_FIELD_TYPE.NUMERIC,
     AS: 'coins'
   }
 }, {
diff --git a/packages/search/lib/commands/AGGREGATE.spec.ts b/packages/search/lib/commands/AGGREGATE.spec.ts
index 5b34d7dc16f..420911c5600 100644
--- a/packages/search/lib/commands/AGGREGATE.spec.ts
+++ b/packages/search/lib/commands/AGGREGATE.spec.ts
@@ -1,516 +1,522 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { AggregateGroupByReducers, AggregateSteps, transformArguments } from './AGGREGATE';
-import { SchemaFieldTypes } from '.';
+import AGGREGATE from './AGGREGATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
+
+describe('AGGREGATE', () => { 
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE, 'index', '*'),
+        ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
+
+    it('with VERBATIM', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE, 'index', '*', {
+          VERBATIM: true
+        }),
+        ['FT.AGGREGATE', 'index', '*', 'VERBATIM', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-describe('AGGREGATE', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
+    it('with ADDSCORES', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE, 'index', '*', { ADDSCORES: true }),
+        ['FT.AGGREGATE', 'index', '*', 'ADDSCORES', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });  
+
+    describe('with LOAD', () => {
+      describe('single', () => {
+        describe('without alias', () => {
+          it('string', () => {
             assert.deepEqual(
-                transformArguments('index', '*'),
-                ['FT.AGGREGATE', 'index', '*']
+              parseArgs(AGGREGATE, 'index', '*', {
+                LOAD: '@property'
+              }),
+              ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
             );
-        });
+          });
 
-        it('with VERBATIM', () => {
+          it('{ identifier: string }', () => {
             assert.deepEqual(
-                transformArguments('index', '*', { VERBATIM: true }),
-                ['FT.AGGREGATE', 'index', '*', 'VERBATIM']
+              parseArgs(AGGREGATE, 'index', '*', {
+                LOAD: {
+                  identifier: '@property'
+                }
+              }),
+              ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
             );
+          });
         });
 
-        it('with ADDSCORES', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', { ADDSCORES: true }),
-                ['FT.AGGREGATE', 'index', '*', 'ADDSCORES']
-            );
+        it('with alias', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              LOAD: {
+                identifier: '@property',
+                AS: 'alias'
+              }
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias', 'DIALECT', DEFAULT_DIALECT]
+          );
         });
+      });
 
-        describe('with LOAD', () => {
-            describe('single', () => {
-                describe('without alias', () => {
-                    it('string', () => {
-                        assert.deepEqual(
-                            transformArguments('index', '*', { LOAD: '@property' }),
-                            ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property']
-                        );
-                    });
-
-                    it('{ identifier: string }', () => {
-                        assert.deepEqual(
-                            transformArguments('index', '*', {
-                                LOAD: {
-                                    identifier: '@property'
-                                }
-                            }),
-                            ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property']
-                        );
-                    });
-                });
-
-                it('with alias', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            LOAD: {
-                                identifier: '@property',
-                                AS: 'alias'
-                            }
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias']
-                    );
-                });
+      it('multiple', () => {
+        assert.deepEqual(
+          parseArgs(AGGREGATE, 'index', '*', {
+            LOAD: ['@1', '@2']
+          }),
+          ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
+    });
+
+    describe('with STEPS', () => {
+      describe('GROUPBY', () => {
+        describe('COUNT', () => {
+          describe('without properties', () => {
+            it('without alias', () => {
+              assert.deepEqual(
+                parseArgs(AGGREGATE, 'index', '*', {
+                  STEPS: [{
+                    type: 'GROUPBY',
+                    REDUCE: {
+                      type: 'COUNT'
+                    }
+                  }]
+                }),
+                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT]
+              );
+            });
+
+            it('with alias', () => {
+              assert.deepEqual(
+                parseArgs(AGGREGATE, 'index', '*', {
+                  STEPS: [{
+                    type: 'GROUPBY',
+                    REDUCE: {
+                      type: 'COUNT',
+                      AS: 'count'
+                    }
+                  }]
+                }),
+                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count', 'DIALECT', DEFAULT_DIALECT]
+              );
+            });
+          });
+
+          describe('with properties', () => {
+            it('single', () => {
+              assert.deepEqual(
+                parseArgs(AGGREGATE, 'index', '*', {
+                  STEPS: [{
+                    type: 'GROUPBY',
+                    properties: '@property',
+                    REDUCE: {
+                      type: 'COUNT'
+                    }
+                  }]
+                }),
+                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT]
+              );
             });
 
             it('multiple', () => {
-                assert.deepEqual(
-                    transformArguments('index', '*', { LOAD: ['@1', '@2'] }),
-                    ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2']
-                );
+              assert.deepEqual(
+                parseArgs(AGGREGATE, 'index', '*', {
+                  STEPS: [{
+                    type: 'GROUPBY',
+                    properties: ['@1', '@2'],
+                    REDUCE: {
+                      type: 'COUNT'
+                    }
+                  }]
+                }),
+                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT]
+              );
             });
+          });
         });
 
-        describe('with STEPS', () => {
-            describe('GROUPBY', () => {
-                describe('COUNT', () => {
-                    describe('without properties', () => {
-                        it('without alias', () => {
-                            assert.deepEqual(
-                                transformArguments('index', '*', {
-                                    STEPS: [{
-                                        type: AggregateSteps.GROUPBY,
-                                        REDUCE: {
-                                            type: AggregateGroupByReducers.COUNT
-                                        }
-                                    }]
-                                }),
-                                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0']
-                            );
-                        });
-
-                        it('with alias', () => {
-                            assert.deepEqual(
-                                transformArguments('index', '*', {
-                                    STEPS: [{
-                                        type: AggregateSteps.GROUPBY,
-                                        REDUCE: {
-                                            type: AggregateGroupByReducers.COUNT,
-                                            AS: 'count'
-                                        }
-                                    }]
-                                }),
-                                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count']
-                            );
-                        });
-                    });
-
-                    describe('with properties', () => {
-                        it('single', () => {
-                            assert.deepEqual(
-                                transformArguments('index', '*', {
-                                    STEPS: [{
-                                        type: AggregateSteps.GROUPBY,
-                                        properties: '@property',
-                                        REDUCE: {
-                                            type: AggregateGroupByReducers.COUNT
-                                        }
-                                    }]
-                                }),
-                                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0']
-                            );
-                        });
-
-                        it('multiple', () => {
-                            assert.deepEqual(
-                                transformArguments('index', '*', {
-                                    STEPS: [{
-                                        type: AggregateSteps.GROUPBY,
-                                        properties: ['@1', '@2'],
-                                        REDUCE: {
-                                            type: AggregateGroupByReducers.COUNT
-                                        }
-                                    }]
-                                }),
-                                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0']
-                            );
-                        });
-                    });
-                });
-
-                it('COUNT_DISTINCT', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.COUNT_DISTINCT,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property']
-                    );
-                });
-
-                it('COUNT_DISTINCTISH', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.COUNT_DISTINCTISH,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property']
-                    );
-                });
-
-                it('SUM', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.SUM,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property']
-                    );
-                });
-
-                it('MIN', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.MIN,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property']
-                    );
-                });
-
-                it('MAX', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.MAX,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property']
-                    );
-                });
-
-                it('AVG', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.AVG,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property']
-                    );
-                });
-
-                it('STDDEV', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.STDDEV,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property']
-                    );
-                });
-
-                it('QUANTILE', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.QUANTILE,
-                                    property: '@property',
-                                    quantile: 0.5
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5']
-                    );
-                });
-
-                it('TO_LIST', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.TO_LIST,
-                                    property: '@property'
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property']
-                    );
-                });
-
-                describe('FIRST_VALUE', () => {
-                    it('simple', () => {
-                        assert.deepEqual(
-                            transformArguments('index', '*', {
-                                STEPS: [{
-                                    type: AggregateSteps.GROUPBY,
-                                    REDUCE: {
-                                        type: AggregateGroupByReducers.FIRST_VALUE,
-                                        property: '@property'
-                                    }
-                                }]
-                            }),
-                            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property']
-                        );
-                    });
-
-                    describe('with BY', () => {
-                        describe('without direction', () => {
-                            it('string', () => {
-                                assert.deepEqual(
-                                    transformArguments('index', '*', {
-                                        STEPS: [{
-                                            type: AggregateSteps.GROUPBY,
-                                            REDUCE: {
-                                                type: AggregateGroupByReducers.FIRST_VALUE,
-                                                property: '@property',
-                                                BY: '@by'
-                                            }
-                                        }]
-                                    }),
-                                    ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by']
-                                );
-                            });
-
-
-                            it('{ property: string }', () => {
-                                assert.deepEqual(
-                                    transformArguments('index', '*', {
-                                        STEPS: [{
-                                            type: AggregateSteps.GROUPBY,
-                                            REDUCE: {
-                                                type: AggregateGroupByReducers.FIRST_VALUE,
-                                                property: '@property',
-                                                BY: {
-                                                    property: '@by'
-                                                }
-                                            }
-                                        }]
-                                    }),
-                                    ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by']
-                                );
-                            });
-                        });
-
-                        it('with direction', () => {
-                            assert.deepEqual(
-                                transformArguments('index', '*', {
-                                    STEPS: [{
-                                        type: AggregateSteps.GROUPBY,
-                                        REDUCE: {
-                                            type: AggregateGroupByReducers.FIRST_VALUE,
-                                            property: '@property',
-                                            BY: {
-                                                property: '@by',
-                                                direction: 'ASC'
-                                            }
-                                        }
-                                    }]
-                                }),
-                                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC']
-                            );
-                        });
-                    });
-                });
-
-                it('RANDOM_SAMPLE', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.GROUPBY,
-                                REDUCE: {
-                                    type: AggregateGroupByReducers.RANDOM_SAMPLE,
-                                    property: '@property',
-                                    sampleSize: 1
-                                }
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1']
-                    );
-                });
-            });
+        it('COUNT_DISTINCT', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'COUNT_DISTINCT',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
 
-            describe('SORTBY', () => {
-                it('string', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.SORTBY,
-                                BY: '@by'
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by']
-                    );
-                });
-
-                it('Array', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.SORTBY,
-                                BY: ['@1', '@2']
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2']
-                    );
-                });
-
-                it('with MAX', () => {
-                    assert.deepEqual(
-                        transformArguments('index', '*', {
-                            STEPS: [{
-                                type: AggregateSteps.SORTBY,
-                                BY: '@by',
-                                MAX: 1
-                            }]
-                        }),
-                        ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by', 'MAX', '1']
-                    );
-                });
-            });
+        it('COUNT_DISTINCTISH', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'COUNT_DISTINCTISH',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
 
-            describe('APPLY', () => {
-                assert.deepEqual(
-                    transformArguments('index', '*', {
-                        STEPS: [{
-                            type: AggregateSteps.APPLY,
-                            expression: '@field + 1',
-                            AS: 'as'
-                        }]
-                    }),
-                    ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as']
-                );
-            });
+        it('SUM', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'SUM',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+
+        it('MIN', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'MIN',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+
+        it('MAX', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'MAX',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+
+        it('AVG', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'AVG',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+        it('STDDEV', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'STDDEV',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+
+        it('QUANTILE', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'QUANTILE',
+                  property: '@property',
+                  quantile: 0.5
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
 
-            describe('LIMIT', () => {
+        it('TOLIST', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'TOLIST',
+                  property: '@property'
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+
+        describe('FIRST_VALUE', () => {
+          it('simple', () => {
+            assert.deepEqual(
+              parseArgs(AGGREGATE, 'index', '*', {
+                STEPS: [{
+                  type: 'GROUPBY',
+                  REDUCE: {
+                    type: 'FIRST_VALUE',
+                    property: '@property'
+                  }
+                }]
+              }),
+              ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property', 'DIALECT', DEFAULT_DIALECT]
+            );
+          });
+
+          describe('with BY', () => {
+            describe('without direction', () => {
+              it('string', () => {
                 assert.deepEqual(
-                    transformArguments('index', '*', {
-                        STEPS: [{
-                            type: AggregateSteps.LIMIT,
-                            from: 0,
-                            size: 1
-                        }]
-                    }),
-                    ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1']
+                  parseArgs(AGGREGATE, 'index', '*', {
+                    STEPS: [{
+                      type: 'GROUPBY',
+                      REDUCE: {
+                        type: 'FIRST_VALUE',
+                        property: '@property',
+                        BY: '@by'
+                      }
+                    }]
+                  }),
+                  ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by', 'DIALECT', DEFAULT_DIALECT]
                 );
-            });
+              });
+
 
-            describe('FILTER', () => {
+              it('{ property: string }', () => {
                 assert.deepEqual(
-                    transformArguments('index', '*', {
-                        STEPS: [{
-                            type: AggregateSteps.FILTER,
-                            expression: '@field != ""'
-                        }]
-                    }),
-                    ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""']
+                  parseArgs(AGGREGATE, 'index', '*', {
+                    STEPS: [{
+                      type: 'GROUPBY',
+                      REDUCE: {
+                        type: 'FIRST_VALUE',
+                        property: '@property',
+                        BY: {
+                          property: '@by'
+                        }
+                      }
+                    }]
+                  }),
+                  ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by', 'DIALECT', DEFAULT_DIALECT]
                 );
+              });
             });
-        });
 
-        it('with PARAMS', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', {
-                    PARAMS: {
-                        param: 'value'
+            it('with direction', () => {
+              assert.deepEqual(
+                parseArgs(AGGREGATE, 'index', '*', {
+                  STEPS: [{
+                    type: 'GROUPBY',
+                    REDUCE: {
+                      type: 'FIRST_VALUE',
+                      property: '@property',
+                      BY: {
+                        property: '@by',
+                        direction: 'ASC'
+                      }
                     }
+                  }]
                 }),
-                ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value']
-            );
+                ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC', 'DIALECT', DEFAULT_DIALECT]
+              );
+            });
+          });
         });
 
-        it('with DIALECT', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', {
-                    DIALECT: 1
-                }),
-                ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1']
-            );
+        it('RANDOM_SAMPLE', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'GROUPBY',
+                REDUCE: {
+                  type: 'RANDOM_SAMPLE',
+                  property: '@property',
+                  sampleSize: 1
+                }
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+      });
+
+      describe('SORTBY', () => {
+        it('string', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'SORTBY',
+                BY: '@by'
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT]
+          );
         });
 
-        it('with TIMEOUT', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', { TIMEOUT: 10 }),
-                ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10']
-            );
+        it('Array', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'SORTBY',
+                BY: ['@1', '@2']
+              }]
+            }),
+            ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT]
+          );
         });
-    });
 
-    testUtils.testWithClient('client.ft.aggregate', async client => {
-        await Promise.all([
-            client.ft.create('index', {
-                field: SchemaFieldTypes.NUMERIC
+        it('with MAX', () => {
+          assert.deepEqual(
+            parseArgs(AGGREGATE, 'index', '*', {
+              STEPS: [{
+                type: 'SORTBY',
+                BY: '@by',
+                MAX: 1
+              }]
             }),
-            client.hSet('1', 'field', '1'),
-            client.hSet('2', 'field', '2')
-        ]);
+            ['FT.AGGREGATE', 'index', '*', 'SORTBY', '3', '@by', 'MAX', '1', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+      });
 
+      describe('APPLY', () => {
         assert.deepEqual(
-            await client.ft.aggregate('index', '*', {
-                STEPS: [{
-                    type: AggregateSteps.GROUPBY,
-                    REDUCE: [{
-                        type: AggregateGroupByReducers.SUM,
-                        property: '@field',
-                        AS: 'sum'
-                    }, {
-                        type: AggregateGroupByReducers.AVG,
-                        property: '@field',
-                        AS: 'avg'
-                    }]
-                }]
-            }),
-            {
-                total: 1,
-                results: [
-                    Object.create(null, {
-                        sum: {
-                            value: '3',
-                            configurable: true,
-                            enumerable: true
-                        },
-                        avg: {
-                            value: '1.5',
-                            configurable: true,
-                            enumerable: true
-                        }
-                    })
-                ]
-            }
+          parseArgs(AGGREGATE, 'index', '*', {
+            STEPS: [{
+              type: 'APPLY',
+              expression: '@field + 1',
+              AS: 'as'
+            }]
+          }),
+          ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
+
+      describe('LIMIT', () => {
+        assert.deepEqual(
+          parseArgs(AGGREGATE, 'index', '*', {
+            STEPS: [{
+              type: 'LIMIT',
+              from: 0,
+              size: 1
+            }]
+          }),
+          ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
+
+      describe('FILTER', () => {
+        assert.deepEqual(
+          parseArgs(AGGREGATE, 'index', '*', {
+            STEPS: [{
+              type: 'FILTER',
+              expression: '@field != ""'
+            }]
+          }),
+          ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""', 'DIALECT', DEFAULT_DIALECT]
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
+    });
+
+    it('with PARAMS', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE, 'index', '*', {
+          PARAMS: {
+            param: 'value'
+          }
+        }),
+        ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
+
+    it('with DIALECT', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE, 'index', '*', {
+          DIALECT: 1
+        }),
+        ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1']
+      );
+    });
+
+    it('with TIMEOUT', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE, 'index', '*', { TIMEOUT: 10 }),
+        ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.ft.aggregate', async client => {
+    await Promise.all([
+      client.ft.create('index', {
+        field: 'NUMERIC'
+      }),
+      client.hSet('1', 'field', '1'),
+      client.hSet('2', 'field', '2')
+    ]);
+
+    assert.deepEqual(
+      await client.ft.aggregate('index', '*', {
+        STEPS: [{
+          type: 'GROUPBY',
+          REDUCE: [{
+            type: 'SUM',
+            property: '@field',
+            AS: 'sum'
+          }, {
+            type: 'AVG',
+            property: '@field',
+            AS: 'avg'
+          }]
+        }]
+      }),
+      {
+        total: 1,
+        results: [
+          Object.create(null, {
+            sum: {
+              value: '3',
+              configurable: true,
+              enumerable: true
+            },
+            avg: {
+              value: '1.5',
+              configurable: true,
+              enumerable: true
+            }
+          })
+        ]
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts
index 0cab9b25d48..9e8fb7810d6 100644
--- a/packages/search/lib/commands/AGGREGATE.ts
+++ b/packages/search/lib/commands/AGGREGATE.ts
@@ -1,316 +1,348 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArgument, transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
-import { Params, PropertyName, pushArgumentsWithLength, pushParamsArgs, pushSortByArguments, SortByProperty } from '.';
-
-export enum AggregateSteps {
-    GROUPBY = 'GROUPBY',
-    SORTBY = 'SORTBY',
-    APPLY = 'APPLY',
-    LIMIT = 'LIMIT',
-    FILTER = 'FILTER'
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
+import { RediSearchProperty } from './CREATE';
+import { FtSearchParams, parseParamsArgument } from './SEARCH';
+import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
+
+type LoadField = RediSearchProperty | {
+  identifier: RediSearchProperty;
+  AS?: RedisArgument;
 }
 
-interface AggregateStep<T extends AggregateSteps> {
-    type: T;
+export const FT_AGGREGATE_STEPS = {
+  GROUPBY: 'GROUPBY',
+  SORTBY: 'SORTBY',
+  APPLY: 'APPLY', 
+  LIMIT: 'LIMIT',
+  FILTER: 'FILTER'
+} as const;
+
+type FT_AGGREGATE_STEPS = typeof FT_AGGREGATE_STEPS;  
+
+export type FtAggregateStep = FT_AGGREGATE_STEPS[keyof FT_AGGREGATE_STEPS];
+
+interface AggregateStep<T extends FtAggregateStep> {
+  type: T;
 }
 
-export enum AggregateGroupByReducers {
-    COUNT = 'COUNT',
-    COUNT_DISTINCT = 'COUNT_DISTINCT',
-    COUNT_DISTINCTISH = 'COUNT_DISTINCTISH',
-    SUM = 'SUM',
-    MIN = 'MIN',
-    MAX = 'MAX',
-    AVG = 'AVG',
-    STDDEV = 'STDDEV',
-    QUANTILE = 'QUANTILE',
-    TOLIST = 'TOLIST',
-    TO_LIST = 'TOLIST',
-    FIRST_VALUE = 'FIRST_VALUE',
-    RANDOM_SAMPLE = 'RANDOM_SAMPLE'
+export const FT_AGGREGATE_GROUP_BY_REDUCERS = {
+  COUNT: 'COUNT',
+  COUNT_DISTINCT: 'COUNT_DISTINCT',
+  COUNT_DISTINCTISH: 'COUNT_DISTINCTISH',
+  SUM: 'SUM',
+  MIN: 'MIN',
+  MAX: 'MAX',
+  AVG: 'AVG',
+  STDDEV: 'STDDEV',
+  QUANTILE: 'QUANTILE',
+  TOLIST: 'TOLIST',
+  FIRST_VALUE: 'FIRST_VALUE',
+  RANDOM_SAMPLE: 'RANDOM_SAMPLE'
+} as const;
+
+type FT_AGGREGATE_GROUP_BY_REDUCERS = typeof FT_AGGREGATE_GROUP_BY_REDUCERS;
+
+export type FtAggregateGroupByReducer = FT_AGGREGATE_GROUP_BY_REDUCERS[keyof FT_AGGREGATE_GROUP_BY_REDUCERS];
+
+interface GroupByReducer<T extends FtAggregateGroupByReducer> {
+  type: T;
+  AS?: RedisArgument;
 }
 
-interface GroupByReducer<T extends AggregateGroupByReducers> {
-    type: T;
-    AS?: string;
+interface GroupByReducerWithProperty<T extends FtAggregateGroupByReducer> extends GroupByReducer<T> {
+  property: RediSearchProperty;
 }
 
-type CountReducer = GroupByReducer<AggregateGroupByReducers.COUNT>;
+type CountReducer = GroupByReducer<FT_AGGREGATE_GROUP_BY_REDUCERS['COUNT']>;
 
-interface CountDistinctReducer extends GroupByReducer<AggregateGroupByReducers.COUNT_DISTINCT> {
-    property: PropertyName;
-}
+type CountDistinctReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['COUNT_DISTINCT']>;
 
-interface CountDistinctishReducer extends GroupByReducer<AggregateGroupByReducers.COUNT_DISTINCTISH> {
-    property: PropertyName;
-}
+type CountDistinctishReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['COUNT_DISTINCTISH']>;
 
-interface SumReducer extends GroupByReducer<AggregateGroupByReducers.SUM> {
-    property: PropertyName;
-}
+type SumReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['SUM']>;
 
-interface MinReducer extends GroupByReducer<AggregateGroupByReducers.MIN> {
-    property: PropertyName;
-}
+type MinReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['MIN']>;
 
-interface MaxReducer extends GroupByReducer<AggregateGroupByReducers.MAX> {
-    property: PropertyName;
-}
+type MaxReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['MAX']>;
 
-interface AvgReducer extends GroupByReducer<AggregateGroupByReducers.AVG> {
-    property: PropertyName;
-}
+type AvgReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['AVG']>;
 
-interface StdDevReducer extends GroupByReducer<AggregateGroupByReducers.STDDEV> {
-    property: PropertyName;
-}
+type StdDevReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['STDDEV']>;
 
-interface QuantileReducer extends GroupByReducer<AggregateGroupByReducers.QUANTILE> {
-    property: PropertyName;
-    quantile: number;
+interface QuantileReducer extends GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['QUANTILE']> {
+  quantile: number;
 }
 
-interface ToListReducer extends GroupByReducer<AggregateGroupByReducers.TOLIST> {
-    property: PropertyName;
-}
+type ToListReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['TOLIST']>;
 
-interface FirstValueReducer extends GroupByReducer<AggregateGroupByReducers.FIRST_VALUE> {
-    property: PropertyName;
-    BY?: PropertyName | {
-        property: PropertyName;
-        direction?: 'ASC' | 'DESC';
-    };
+interface FirstValueReducer extends GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['FIRST_VALUE']> {
+  BY?: RediSearchProperty | {
+    property: RediSearchProperty;
+    direction?: 'ASC' | 'DESC';
+  };
 }
 
-interface RandomSampleReducer extends GroupByReducer<AggregateGroupByReducers.RANDOM_SAMPLE> {
-    property: PropertyName;
-    sampleSize: number;
+interface RandomSampleReducer extends GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['RANDOM_SAMPLE']> {
+  sampleSize: number;
 }
 
 type GroupByReducers = CountReducer | CountDistinctReducer | CountDistinctishReducer | SumReducer | MinReducer | MaxReducer | AvgReducer | StdDevReducer | QuantileReducer | ToListReducer | FirstValueReducer | RandomSampleReducer;
 
-interface GroupByStep extends AggregateStep<AggregateSteps.GROUPBY> {
-    properties?: PropertyName | Array<PropertyName>;
-    REDUCE: GroupByReducers | Array<GroupByReducers>;
+interface GroupByStep extends AggregateStep<FT_AGGREGATE_STEPS['GROUPBY']> {
+  properties?: RediSearchProperty | Array<RediSearchProperty>;
+  REDUCE: GroupByReducers | Array<GroupByReducers>;
 }
 
-interface SortStep extends AggregateStep<AggregateSteps.SORTBY> {
-    BY: SortByProperty | Array<SortByProperty>;
-    MAX?: number;
-}
+type SortByProperty = RedisArgument | {
+  BY: RediSearchProperty;
+  DIRECTION?: 'ASC' | 'DESC';
+};
 
-interface ApplyStep extends AggregateStep<AggregateSteps.APPLY> {
-    expression: string;
-    AS: string;
+interface SortStep extends AggregateStep<FT_AGGREGATE_STEPS['SORTBY']> {
+  BY: SortByProperty | Array<SortByProperty>;
+  MAX?: number;
 }
 
-interface LimitStep extends AggregateStep<AggregateSteps.LIMIT> {
-    from: number;
-    size: number;
+interface ApplyStep extends AggregateStep<FT_AGGREGATE_STEPS['APPLY']> {
+  expression: RedisArgument;
+  AS: RedisArgument;
 }
 
-interface FilterStep extends AggregateStep<AggregateSteps.FILTER> {
-    expression: string;
+interface LimitStep extends AggregateStep<FT_AGGREGATE_STEPS['LIMIT']> {
+  from: number;
+  size: number;
 }
 
-type LoadField = PropertyName | {
-    identifier: PropertyName;
-    AS?: string;
+interface FilterStep extends AggregateStep<FT_AGGREGATE_STEPS['FILTER']> {
+  expression: RedisArgument;
 }
 
-export interface AggregateOptions {
-    VERBATIM?: boolean;
-    ADDSCORES?: boolean;
-    LOAD?: LoadField | Array<LoadField>;
-    STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>;
-    PARAMS?: Params;
-    DIALECT?: number;
-    TIMEOUT?: number;
+export interface FtAggregateOptions {
+  VERBATIM?: boolean;
+  ADDSCORES?: boolean;
+  LOAD?: LoadField | Array<LoadField>;
+  TIMEOUT?: number;
+  STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>;
+  PARAMS?: FtSearchParams;
+  DIALECT?: number;
 }
 
-export const FIRST_KEY_INDEX = 1;
+export type AggregateRawReply = [
+  total: UnwrapReply<NumberReply>,
+  ...results: UnwrapReply<ArrayReply<ArrayReply<BlobStringReply>>>
+];
 
-export const IS_READ_ONLY = true;
+export interface AggregateReply {
+  total: number;
+  results: Array<MapReply<BlobStringReply, BlobStringReply>>;
+};
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: false,
+  /**
+   * Performs an aggregation query on a RediSearch index.
+   * @param parser - The command parser
+   * @param index - The index name to query
+   * @param query - The text query to use as filter, use * to indicate no filtering
+   * @param options - Optional parameters for aggregation:
+   *   - VERBATIM: disable stemming in query evaluation
+   *   - LOAD: specify fields to load from documents
+   *   - STEPS: sequence of aggregation steps (GROUPBY, SORTBY, APPLY, LIMIT, FILTER)
+   *   - PARAMS: bind parameters for query evaluation
+   *   - TIMEOUT: maximum time to run the query
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) {
+    parser.push('FT.AGGREGATE', index, query);
+
+    return parseAggregateOptions(parser, options);
+  },
+  transformReply: {
+    2: (rawReply: AggregateRawReply, preserve?: any, typeMapping?: TypeMapping): AggregateReply => {
+      const results: Array<MapReply<BlobStringReply, BlobStringReply>> = [];
+      for (let i = 1; i < rawReply.length; i++) {
+        results.push(
+          transformTuplesReply(rawReply[i] as ArrayReply<BlobStringReply>, preserve, typeMapping)
+        );
+      }
+  
+      return {
+        //  https://redis.io/docs/latest/commands/ft.aggregate/#return
+        //  FT.AGGREGATE returns an array reply where each row is an array reply and represents a single aggregate result.
+        // The integer reply at position 1 does not represent a valid value.
+        total: Number(rawReply[0]),
+        results
+      };
+    },
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
+
+export function parseAggregateOptions(parser: CommandParser , options?: FtAggregateOptions) {
+  if (options?.VERBATIM) {
+    parser.push('VERBATIM');
+  }
+
+  if (options?.ADDSCORES) {
+    parser.push('ADDSCORES');
+  }  
+
+  if (options?.LOAD) {
+    const args: Array<RedisArgument> = [];
+
+    if (Array.isArray(options.LOAD)) {
+      for (const load of options.LOAD) {
+        pushLoadField(args, load);
+      }
+    } else {
+      pushLoadField(args, options.LOAD);
+    }
 
-export function transformArguments(
-    index: string,
-    query: string,
-    options?: AggregateOptions
-): RedisCommandArguments {
-    return pushAggregatehOptions(
-        ['FT.AGGREGATE', index, query],
-        options
-    );
-}
+    parser.push('LOAD');
+    parser.pushVariadicWithLength(args);
+  }
+
+  if (options?.TIMEOUT !== undefined) {
+    parser.push('TIMEOUT', options.TIMEOUT.toString());
+  }
+
+  if (options?.STEPS) {
+    for (const step of options.STEPS) {
+      parser.push(step.type);
+      switch (step.type) {
+        case FT_AGGREGATE_STEPS.GROUPBY:
+          if (!step.properties) {
+            parser.push('0');
+          } else {
+            parser.pushVariadicWithLength(step.properties);
+          }
+
+          if (Array.isArray(step.REDUCE)) {
+            for (const reducer of step.REDUCE) {
+              parseGroupByReducer(parser, reducer);
+            }
+          } else {
+            parseGroupByReducer(parser, step.REDUCE);
+          }
 
-export function pushAggregatehOptions(
-    args: RedisCommandArguments,
-    options?: AggregateOptions
-): RedisCommandArguments {
-    if (options?.VERBATIM) {
-        args.push('VERBATIM');
-    }
+          break;
 
-    if (options?.ADDSCORES) {
-        args.push('ADDSCORES');
-    }
+        case FT_AGGREGATE_STEPS.SORTBY:
+          const args: Array<RedisArgument> = [];
 
-    if (options?.LOAD) {
-        args.push('LOAD');
-        pushArgumentsWithLength(args, () => {
-            if (Array.isArray(options.LOAD)) {
-                for (const load of options.LOAD) {
-                    pushLoadField(args, load);
-                }
-            } else {
-                pushLoadField(args, options.LOAD!);
+          if (Array.isArray(step.BY)) {
+            for (const by of step.BY) {
+              pushSortByProperty(args, by);
             }
-        });
-    }
+          } else {
+            pushSortByProperty(args, step.BY);
+          }
 
-    if (options?.STEPS) {
-        for (const step of options.STEPS) {
-            switch (step.type) {
-                case AggregateSteps.GROUPBY:
-                    args.push('GROUPBY');
-                    if (!step.properties) {
-                        args.push('0');
-                    } else {
-                        pushVerdictArgument(args, step.properties);
-                    }
-
-                    if (Array.isArray(step.REDUCE)) {
-                        for (const reducer of step.REDUCE) {
-                            pushGroupByReducer(args, reducer);
-                        }
-                    } else {
-                        pushGroupByReducer(args, step.REDUCE);
-                    }
-
-                    break;
-
-                case AggregateSteps.SORTBY:
-                    pushSortByArguments(args, 'SORTBY', step.BY);
-
-                    if (step.MAX) {
-                        args.push('MAX', step.MAX.toString());
-                    }
-
-                    break;
-
-                case AggregateSteps.APPLY:
-                    args.push('APPLY', step.expression, 'AS', step.AS);
-                    break;
-
-                case AggregateSteps.LIMIT:
-                    args.push('LIMIT', step.from.toString(), step.size.toString());
-                    break;
-
-                case AggregateSteps.FILTER:
-                    args.push('FILTER', step.expression);
-                    break;
-            }
-        }
-    }
+          if (step.MAX) {
+            args.push('MAX', step.MAX.toString());
+          }
 
-    pushParamsArgs(args, options?.PARAMS);
+          parser.pushVariadicWithLength(args);
 
-    if (options?.DIALECT) {
-        args.push('DIALECT', options.DIALECT.toString());
-    }
+          break;
+
+        case FT_AGGREGATE_STEPS.APPLY:
+          parser.push(step.expression, 'AS', step.AS);
+          break;
+
+        case FT_AGGREGATE_STEPS.LIMIT:
+          parser.push(step.from.toString(), step.size.toString());
+          break;
 
-    if (options?.TIMEOUT !== undefined) {
-        args.push('TIMEOUT', options.TIMEOUT.toString());
+        case FT_AGGREGATE_STEPS.FILTER:
+          parser.push(step.expression);
+          break;
+      }
     }
+  }
 
-    return args;
+  parseParamsArgument(parser, options?.PARAMS);
+
+  if (options?.DIALECT) {
+    parser.push('DIALECT', options.DIALECT.toString());
+  } else {
+    parser.push('DIALECT', DEFAULT_DIALECT);
+  }
 }
 
-function pushLoadField(args: RedisCommandArguments, toLoad: LoadField): void {
-    if (typeof toLoad === 'string') {
-        args.push(toLoad);
-    } else {
-        args.push(toLoad.identifier);
+function pushLoadField(args: Array<RedisArgument>, toLoad: LoadField) {
+  if (typeof toLoad === 'string' || toLoad instanceof Buffer) {
+    args.push(toLoad);
+  } else {
+    args.push(toLoad.identifier);
 
-        if (toLoad.AS) {
-            args.push('AS', toLoad.AS);
-        }
+    if (toLoad.AS) {
+      args.push('AS', toLoad.AS);
     }
+  }
 }
 
-function pushGroupByReducer(args: RedisCommandArguments, reducer: GroupByReducers): void {
-    args.push('REDUCE', reducer.type);
-
-    switch (reducer.type) {
-        case AggregateGroupByReducers.COUNT:
-            args.push('0');
-            break;
-
-        case AggregateGroupByReducers.COUNT_DISTINCT:
-        case AggregateGroupByReducers.COUNT_DISTINCTISH:
-        case AggregateGroupByReducers.SUM:
-        case AggregateGroupByReducers.MIN:
-        case AggregateGroupByReducers.MAX:
-        case AggregateGroupByReducers.AVG:
-        case AggregateGroupByReducers.STDDEV:
-        case AggregateGroupByReducers.TOLIST:
-            args.push('1', reducer.property);
-            break;
-
-        case AggregateGroupByReducers.QUANTILE:
-            args.push('2', reducer.property, reducer.quantile.toString());
-            break;
-
-        case AggregateGroupByReducers.FIRST_VALUE: {
-            pushArgumentsWithLength(args, () => {
-                args.push(reducer.property);
-
-                if (reducer.BY) {
-                    args.push('BY');
-                    if (typeof reducer.BY === 'string') {
-                        args.push(reducer.BY);
-                    } else {
-                        args.push(reducer.BY.property);
-
-                        if (reducer.BY.direction) {
-                            args.push(reducer.BY.direction);
-                        }
-                    }
-                }
-            });
-            break;
+function parseGroupByReducer(parser: CommandParser, reducer: GroupByReducers) {
+  parser.push('REDUCE', reducer.type);
+
+  switch (reducer.type) {
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT:
+      parser.push('0');
+      break;
+
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCT:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCTISH:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.SUM:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.MIN:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.MAX:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.AVG:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.STDDEV:
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST:
+      parser.push('1', reducer.property);
+      break;
+
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.QUANTILE:
+      parser.push('2', reducer.property, reducer.quantile.toString());
+      break;
+
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.FIRST_VALUE: {
+      const args: Array<RedisArgument> = [reducer.property];
+
+      if (reducer.BY) {
+        args.push('BY');
+        if (typeof reducer.BY === 'string' || reducer.BY instanceof Buffer) {
+          args.push(reducer.BY);
+        } else {
+          args.push(reducer.BY.property);
+          if (reducer.BY.direction) {
+            args.push(reducer.BY.direction);
+          }
         }
+      }
 
-        case AggregateGroupByReducers.RANDOM_SAMPLE:
-            args.push('2', reducer.property, reducer.sampleSize.toString());
-            break;
-    }
-
-    if (reducer.AS) {
-        args.push('AS', reducer.AS);
+      parser.pushVariadicWithLength(args);
+      break;
     }
-}
 
-export type AggregateRawReply = [
-    total: number,
-    ...results: Array<Array<RedisCommandArgument>>
-];
+    case FT_AGGREGATE_GROUP_BY_REDUCERS.RANDOM_SAMPLE:
+      parser.push('2', reducer.property, reducer.sampleSize.toString());
+      break;
+  }
 
-export interface AggregateReply {
-    total: number;
-    results: Array<Record<string, RedisCommandArgument>>;
+  if (reducer.AS) {
+    parser.push('AS', reducer.AS);
+  }
 }
 
-export function transformReply(rawReply: AggregateRawReply): AggregateReply {
-    const results: Array<Record<string, RedisCommandArgument>> = [];
-    for (let i = 1; i < rawReply.length; i++) {
-        results.push(
-            transformTuplesReply(rawReply[i] as Array<RedisCommandArgument>)
-        );
+function pushSortByProperty(args: Array<RedisArgument>, sortBy: SortByProperty) {
+  if (typeof sortBy === 'string' || sortBy instanceof Buffer) {
+    args.push(sortBy);
+  } else {
+    args.push(sortBy.BY);
+    if (sortBy.DIRECTION) {
+      args.push(sortBy.DIRECTION);
     }
-
-    return {
-        total: rawReply[0],
-        results
-    };
+  }
 }
diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts
index 65396f3f790..0e89346c49f 100644
--- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts
+++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts
@@ -1,37 +1,49 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './AGGREGATE_WITHCURSOR';
-import { SchemaFieldTypes } from '.';
+import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
 describe('AGGREGATE WITHCURSOR', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', '*'),
-                ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR']
-            );
-        });
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE_WITHCURSOR, 'index', '*'),
+        ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR']
+      );
+    });
+
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', {
+          COUNT: 1
+        }),
+        ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR', 'COUNT', '1']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', { COUNT: 1 }),
-                ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1']
-            );
-        });
+    it('with MAXIDLE', () => {
+      assert.deepEqual(
+        parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', {
+          MAXIDLE: 1
+        }),
+        ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR', 'MAXIDLE', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.aggregateWithCursor', async client => {
-        await client.ft.create('index', {
-            field: SchemaFieldTypes.NUMERIC
-        });
+  testUtils.testWithClient('client.ft.aggregateWithCursor', async client => {
+    await client.ft.create('index', {
+      field: 'NUMERIC'
+    });
 
-        assert.deepEqual(
-            await client.ft.aggregateWithCursor('index', '*'),
-            {
-                total: 0,
-                results: [],
-                cursor: 0
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(
+      await client.ft.aggregateWithCursor('index', '*'),
+      {
+        total: 0,
+        results: [],
+        cursor: 0
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts
index 63f6ee8f187..e1b0e42f9fe 100644
--- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts
+++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts
@@ -1,44 +1,54 @@
-import {
-    AggregateOptions,
-    AggregateRawReply,
-    AggregateReply,
-    transformArguments as transformAggregateArguments,
-    transformReply as transformAggregateReply
-} from './AGGREGATE';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/dist/lib/RESP/types';
+import AGGREGATE, { AggregateRawReply, AggregateReply, FtAggregateOptions } from './AGGREGATE';
 
-export { FIRST_KEY_INDEX, IS_READ_ONLY } from './AGGREGATE';
-
-interface AggregateWithCursorOptions extends AggregateOptions {
-    COUNT?: number;
+export interface FtAggregateWithCursorOptions extends FtAggregateOptions {
+  COUNT?: number;
+  MAXIDLE?: number;
 }
 
-export function transformArguments(
-    index: string,
-    query: string,
-    options?: AggregateWithCursorOptions
-) {
-    const args = transformAggregateArguments(index, query, options);
-
-    args.push('WITHCURSOR');
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
-    }
-
-    return args;
-}
 
 type AggregateWithCursorRawReply = [
-    result: AggregateRawReply,
-    cursor: number
+  result: AggregateRawReply,
+  cursor: NumberReply
 ];
 
-interface AggregateWithCursorReply extends AggregateReply {
-    cursor: number;
+export interface AggregateWithCursorReply extends AggregateReply {
+  cursor: NumberReply;
 }
 
-export function transformReply(reply: AggregateWithCursorRawReply): AggregateWithCursorReply {
-    return {
-        ...transformAggregateReply(reply[0]),
+export default {
+  IS_READ_ONLY: AGGREGATE.IS_READ_ONLY,
+  /**
+   * Performs an aggregation with a cursor for retrieving large result sets.
+   * @param parser - The command parser
+   * @param index - Name of the index to query
+   * @param query - The aggregation query
+   * @param options - Optional parameters:
+   *   - All options supported by FT.AGGREGATE
+   *   - COUNT: Number of results to return per cursor fetch
+   *   - MAXIDLE: Maximum idle time for cursor in milliseconds
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) {
+    AGGREGATE.parseCommand(parser, index, query, options);
+    parser.push('WITHCURSOR');
+
+    if (options?.COUNT !== undefined) {
+      parser.push('COUNT', options.COUNT.toString());
+    }
+
+    if(options?.MAXIDLE !== undefined) {
+      parser.push('MAXIDLE', options.MAXIDLE.toString());
+    }
+  },
+  transformReply: {
+    2: (reply: AggregateWithCursorRawReply): AggregateWithCursorReply => {
+      return {
+        ...AGGREGATE.transformReply[2](reply[0]),
         cursor: reply[1]
-    };
-}
+      };
+    },
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/ALIASADD.spec.ts b/packages/search/lib/commands/ALIASADD.spec.ts
index 7bb2452838b..b8332aed6a6 100644
--- a/packages/search/lib/commands/ALIASADD.spec.ts
+++ b/packages/search/lib/commands/ALIASADD.spec.ts
@@ -1,11 +1,25 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './ALIASADD';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ALIASADD from './ALIASADD';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ALIASADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('alias', 'index'),
-            ['FT.ALIASADD', 'alias', 'index']
-        );
-    });
+describe('FT.ALIASADD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ALIASADD, 'alias', 'index'),
+      ['FT.ALIASADD', 'alias', 'index']
+    );
+  });
+
+  testUtils.testWithClient('client.ft.aliasAdd', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.aliasAdd('alias', 'index')
+    ]);
+
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/ALIASADD.ts b/packages/search/lib/commands/ALIASADD.ts
index 552c1add695..7d3a03498e6 100644
--- a/packages/search/lib/commands/ALIASADD.ts
+++ b/packages/search/lib/commands/ALIASADD.ts
@@ -1,5 +1,17 @@
-export function transformArguments(name: string, index: string): Array<string> {
-    return ['FT.ALIASADD', name, index];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Adds an alias to a RediSearch index.
+   * @param parser - The command parser
+   * @param alias - The alias to add
+   * @param index - The index name to alias
+   */
+  parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) {
+    parser.push('FT.ALIASADD', alias, index);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/ALIASDEL.spec.ts b/packages/search/lib/commands/ALIASDEL.spec.ts
index 5255ba835db..19c2473f8cd 100644
--- a/packages/search/lib/commands/ALIASDEL.spec.ts
+++ b/packages/search/lib/commands/ALIASDEL.spec.ts
@@ -1,11 +1,26 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './ALIASDEL';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ALIASDEL from './ALIASDEL';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ALIASDEL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('alias', 'index'),
-            ['FT.ALIASDEL', 'alias', 'index']
-        );
-    });
+describe('FT.ALIASDEL', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ALIASDEL, 'alias'),
+      ['FT.ALIASDEL', 'alias']
+    );
+  });
+
+  testUtils.testWithClient('client.ft.aliasAdd', async client => {
+    const [, , reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.aliasAdd('alias', 'index'),
+      client.ft.aliasDel('alias')
+    ]);
+
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/ALIASDEL.ts b/packages/search/lib/commands/ALIASDEL.ts
index 434b4df3dea..3058be13997 100644
--- a/packages/search/lib/commands/ALIASDEL.ts
+++ b/packages/search/lib/commands/ALIASDEL.ts
@@ -1,5 +1,16 @@
-export function transformArguments(name: string, index: string): Array<string> {
-    return ['FT.ALIASDEL', name, index];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Removes an existing alias from a RediSearch index.
+   * @param parser - The command parser
+   * @param alias - The alias to remove
+   */
+  parseCommand(parser: CommandParser, alias: RedisArgument) {
+    parser.push('FT.ALIASDEL', alias);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/ALIASUPDATE.spec.ts b/packages/search/lib/commands/ALIASUPDATE.spec.ts
index 79421b1a20d..f23af30229c 100644
--- a/packages/search/lib/commands/ALIASUPDATE.spec.ts
+++ b/packages/search/lib/commands/ALIASUPDATE.spec.ts
@@ -1,11 +1,25 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './ALIASUPDATE';
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import ALIASUPDATE from './ALIASUPDATE';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ALIASUPDATE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('alias', 'index'),
-            ['FT.ALIASUPDATE', 'alias', 'index']
-        );
-    });
+describe('FT.ALIASUPDATE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(ALIASUPDATE, 'alias', 'index'),
+      ['FT.ALIASUPDATE', 'alias', 'index']
+    );
+  });
+
+  testUtils.testWithClient('client.ft.aliasUpdate', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.aliasUpdate('alias', 'index')
+    ]);
+
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/ALIASUPDATE.ts b/packages/search/lib/commands/ALIASUPDATE.ts
index ac64ef57c3f..35879ea79cb 100644
--- a/packages/search/lib/commands/ALIASUPDATE.ts
+++ b/packages/search/lib/commands/ALIASUPDATE.ts
@@ -1,5 +1,17 @@
-export function transformArguments(name: string, index: string): Array<string> {
-    return ['FT.ALIASUPDATE', name, index];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Updates the index pointed to by an existing alias.
+   * @param parser - The command parser
+   * @param alias - The existing alias to update
+   * @param index - The new index name that the alias should point to
+   */
+  parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) {
+    parser.push('FT.ALIASUPDATE', alias, index);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/ALTER.spec.ts b/packages/search/lib/commands/ALTER.spec.ts
index e9724757ad7..c34f7e045d5 100644
--- a/packages/search/lib/commands/ALTER.spec.ts
+++ b/packages/search/lib/commands/ALTER.spec.ts
@@ -1,37 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ALTER';
-import { SchemaFieldTypes } from '.';
+import ALTER from './ALTER';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ALTER', () => {
-    describe('transformArguments', () => {
-        it('with NOINDEX', () => {
-            assert.deepEqual(
-                transformArguments('index', {
-                    field: {
-                        type: SchemaFieldTypes.TEXT,
-                        NOINDEX: true,
-                        SORTABLE: 'UNF',
-                        AS: 'text'
-                    }
-                }),
-                ['FT.ALTER', 'index', 'SCHEMA', 'ADD', 'field', 'AS', 'text', 'TEXT', 'SORTABLE', 'UNF', 'NOINDEX']
-            );
-        });
+describe('FT.ALTER', () => {
+  describe('transformArguments', () => {
+    it('with NOINDEX', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'index', {
+          field: {
+            type: SCHEMA_FIELD_TYPE.TEXT,
+            NOINDEX: true,
+            SORTABLE: 'UNF',
+            AS: 'text'
+          }
+        }),
+        ['FT.ALTER', 'index', 'SCHEMA', 'ADD', 'field', 'AS', 'text', 'TEXT', 'SORTABLE', 'UNF', 'NOINDEX']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.create', async client => {
-        await Promise.all([
-            client.ft.create('index', {
-                title: SchemaFieldTypes.TEXT
-            }),
-        ]);
+  testUtils.testWithClient('client.ft.create', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        title: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.alter('index', {
+        body: SCHEMA_FIELD_TYPE.TEXT
+      })
+    ]);
 
-        assert.equal(
-            await client.ft.alter('index', {
-                body: SchemaFieldTypes.TEXT
-            }),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/ALTER.ts b/packages/search/lib/commands/ALTER.ts
index bb4c5202c65..05c1b799eb1 100644
--- a/packages/search/lib/commands/ALTER.ts
+++ b/packages/search/lib/commands/ALTER.ts
@@ -1,10 +1,19 @@
-import { RediSearchSchema, pushSchema } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RediSearchSchema, parseSchema } from './CREATE';
 
-export function transformArguments(index: string, schema: RediSearchSchema): Array<string> {
-    const args = ['FT.ALTER', index, 'SCHEMA', 'ADD'];
-    pushSchema(args, schema);
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Alters an existing RediSearch index schema by adding new fields.
+   * @param parser - The command parser
+   * @param index - The index to alter
+   * @param schema - The schema definition containing new fields to add
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema) {
+    parser.push('FT.ALTER', index, 'SCHEMA', 'ADD');
+    parseSchema(parser, schema);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/CONFIG_GET.spec.ts b/packages/search/lib/commands/CONFIG_GET.spec.ts
index 8614f443426..598a2a9ac41 100644
--- a/packages/search/lib/commands/CONFIG_GET.spec.ts
+++ b/packages/search/lib/commands/CONFIG_GET.spec.ts
@@ -1,25 +1,26 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CONFIG_GET';
+import CONFIG_GET from './CONFIG_GET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CONFIG GET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('TIMEOUT'),
-            ['FT.CONFIG', 'GET', 'TIMEOUT']
-        );
-    });
+describe('FT.CONFIG GET', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CONFIG_GET, 'TIMEOUT'),
+      ['FT.CONFIG', 'GET', 'TIMEOUT']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.configGet', async client => {
-        assert.deepEqual(
-            await client.ft.configGet('TIMEOUT'),
-            Object.create(null, {
-                TIMEOUT: {
-                    value: '500',
-                    configurable: true,
-                    enumerable: true
-                }
-            })
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft.configGet', async client => {
+    assert.deepEqual(
+      await client.ft.configGet('TIMEOUT'),
+      Object.create(null, {
+        TIMEOUT: {
+          value: '500',
+          configurable: true,
+          enumerable: true
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/CONFIG_GET.ts b/packages/search/lib/commands/CONFIG_GET.ts
index fbf1f1164b9..8073805c533 100644
--- a/packages/search/lib/commands/CONFIG_GET.ts
+++ b/packages/search/lib/commands/CONFIG_GET.ts
@@ -1,16 +1,24 @@
-export function transformArguments(option: string) {
-    return ['FT.CONFIG', 'GET', option];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-interface ConfigGetReply {
-    [option: string]: string | null;
-}
-
-export function transformReply(rawReply: Array<[string, string | null]>): ConfigGetReply {
-    const transformedReply: ConfigGetReply = Object.create(null);
-    for (const [key, value] of rawReply) {
-        transformedReply[key] = value;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets a RediSearch configuration option value.
+   * @param parser - The command parser
+   * @param option - The name of the configuration option to retrieve
+   */
+  parseCommand(parser: CommandParser, option: string) {
+    parser.push('FT.CONFIG', 'GET', option);
+  },
+  transformReply(reply: UnwrapReply<ArrayReply<TuplesReply<[BlobStringReply, BlobStringReply | NullReply]>>>) {
+    const transformedReply: Record<string, BlobStringReply | NullReply> = Object.create(null);
+    for (const item of reply) {
+      const [key, value] = item as unknown as UnwrapReply<typeof item>;
+      transformedReply[key.toString()] = value;
     }
 
     return transformedReply;
-}
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/CONFIG_SET.spec.ts b/packages/search/lib/commands/CONFIG_SET.spec.ts
index 59cb63a3d8e..c5922a28756 100644
--- a/packages/search/lib/commands/CONFIG_SET.spec.ts
+++ b/packages/search/lib/commands/CONFIG_SET.spec.ts
@@ -1,12 +1,56 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CONFIG_SET';
-
-describe('CONFIG SET', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('TIMEOUT', '500'),
-            ['FT.CONFIG', 'SET', 'TIMEOUT', '500']
-        );
-    });
+import CONFIG_SET from './CONFIG_SET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('FT.CONFIG SET', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CONFIG_SET, 'TIMEOUT', '500'),
+      ['FT.CONFIG', 'SET', 'TIMEOUT', '500']
+    );
+  });
+
+  testUtils.testWithClient('client.ft.configSet', async client => {
+    assert.deepEqual(
+      await client.ft.configSet('TIMEOUT', '500'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'setSearchConfigGloballyTest', async client => {
+
+    const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
+    assert.equal(await client.configSet('search-default-dialect', '3'),
+      'OK', 'CONFIG SET should return OK');
+
+    assert.deepEqual(
+      normalizeObject(await client.configGet('search-default-dialect')),
+      { 'search-default-dialect': '3' },
+      'CONFIG GET should return 3'
+    );
+
+    assert.deepEqual(
+      normalizeObject(await client.ft.configGet('DEFAULT_DIALECT')),
+      { 'DEFAULT_DIALECT': '3' },
+      'FT.CONFIG GET should return 3'
+    );
+
+    const ftConfigSetResult = await client.ft.configSet('DEFAULT_DIALECT', '2');
+    assert.equal(normalizeObject(ftConfigSetResult), 'OK', 'FT.CONFIG SET should return OK');
+
+    assert.deepEqual(
+      normalizeObject(await client.ft.configGet('DEFAULT_DIALECT')),
+      { 'DEFAULT_DIALECT': '2' },
+      'FT.CONFIG GET should return 2'
+    );
+
+    assert.deepEqual(
+      normalizeObject(await client.configGet('search-default-dialect')),
+      { 'search-default-dialect': '2' },
+      'CONFIG GET should return 22'
+    );
+
+  }, GLOBAL.SERVERS.OPEN);
+
 });
diff --git a/packages/search/lib/commands/CONFIG_SET.ts b/packages/search/lib/commands/CONFIG_SET.ts
index 93b76d79edf..c3c8cc7259b 100644
--- a/packages/search/lib/commands/CONFIG_SET.ts
+++ b/packages/search/lib/commands/CONFIG_SET.ts
@@ -1,5 +1,21 @@
-export function transformArguments(option: string, value: string): Array<string> {
-    return ['FT.CONFIG', 'SET', option, value];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): 'OK';
+// using `string & {}` to avoid TS widening the type to `string`
+// TODO
+type FtConfigProperties = 'a' | 'b' | (string & {}) | Buffer;
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Sets a RediSearch configuration option value.
+   * @param parser - The command parser
+   * @param property - The name of the configuration option to set
+   * @param value - The value to set for the configuration option
+   */
+  parseCommand(parser: CommandParser, property: FtConfigProperties, value: RedisArgument) {
+    parser.push('FT.CONFIG', 'SET', property, value);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/CREATE.spec.ts b/packages/search/lib/commands/CREATE.spec.ts
index 50c5c011c89..2c54d3d0235 100644
--- a/packages/search/lib/commands/CREATE.spec.ts
+++ b/packages/search/lib/commands/CREATE.spec.ts
@@ -1,490 +1,559 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CREATE';
-import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages, VectorAlgorithms, SCHEMA_GEO_SHAPE_COORD_SYSTEM } from '.';
+import CREATE, { SCHEMA_FIELD_TYPE, SCHEMA_TEXT_FIELD_PHONETIC, SCHEMA_VECTOR_FIELD_ALGORITHM, REDISEARCH_LANGUAGE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('FT.CREATE', () => {
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}),
+        ['FT.CREATE', 'index', 'SCHEMA']
+      );
+    });
 
-describe('CREATE', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('index', {}),
-                ['FT.CREATE', 'index', 'SCHEMA']
-            );
+    describe('with fields', () => {
+      describe('TEXT', () => {
+        it('without options', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: SCHEMA_FIELD_TYPE.TEXT
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT']
+          );
         });
 
-        describe('with fields', () => {
-            describe('TEXT', () => {
-                it('without options', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: SchemaFieldTypes.TEXT
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT']
-                    );
-                });
-
-                it('with NOSTEM', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                NOSTEM: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM']
-                    );
-                });
-
-                it('with WEIGHT', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                WEIGHT: 1
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1']
-                    );
-                });
-
-                it('with PHONETIC', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                PHONETIC: SchemaTextFieldPhonetics.DM_EN
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SchemaTextFieldPhonetics.DM_EN]
-                    );
-                });
-
-                it('with WITHSUFFIXTRIE', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                WITHSUFFIXTRIE: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE']
-                    );
-                });
-
-                it('with INDEXEMPTY', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                INDEXEMPTY: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXEMPTY']
-                    );
-                });
-            });
-
-            it('NUMERIC', () => {
-                assert.deepEqual(
-                    transformArguments('index', {
-                        field: SchemaFieldTypes.NUMERIC
-                    }),
-                    ['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC']
-                );
-            });
-
-            it('GEO', () => {
-                assert.deepEqual(
-                    transformArguments('index', {
-                        field: SchemaFieldTypes.GEO
-                    }),
-                    ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO']
-                );
-            });
-
-            describe('TAG', () => {
-                describe('without options', () => {
-                    it('SchemaFieldTypes.TAG', () => {
-                        assert.deepEqual(
-                            transformArguments('index', {
-                                field: SchemaFieldTypes.TAG
-                            }),
-                            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
-                        );
-                    });
-
-                    it('{ type: SchemaFieldTypes.TAG }', () => {
-                        assert.deepEqual(
-                            transformArguments('index', {
-                                field: {
-                                    type: SchemaFieldTypes.TAG
-                                }
-                            }),
-                            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
-                        );
-                    });
-                });
-
-                it('with SEPARATOR', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TAG,
-                                SEPARATOR: 'separator'
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPARATOR', 'separator']
-                    );
-                });
-
-                it('with CASESENSITIVE', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TAG,
-                                CASESENSITIVE: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE']
-                    );
-                });
-
-                it('with WITHSUFFIXTRIE', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TAG,
-                                WITHSUFFIXTRIE: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE']
-                    );
-                });
-
-                it('with INDEXEMPTY', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TAG,
-                                INDEXEMPTY: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY']
-                    );
-                });
-            });
-
-            describe('VECTOR', () => {
-                it('Flat algorithm', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.VECTOR,
-                                ALGORITHM: VectorAlgorithms.FLAT,
-                                TYPE: 'FLOAT32',
-                                DIM: 2,
-                                DISTANCE_METRIC: 'L2',
-                                INITIAL_CAP: 1000000,
-                                BLOCK_SIZE: 1000
-                            }
-                        }),
-                        [
-                            'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'FLAT', '10', 'TYPE',
-                            'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000',
-                            'BLOCK_SIZE', '1000'
-                        ]
-                    );
-                });
-
-                it('HNSW algorithm', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.VECTOR,
-                                ALGORITHM: VectorAlgorithms.HNSW,
-                                TYPE: 'FLOAT32',
-                                DIM: 2,
-                                DISTANCE_METRIC: 'L2',
-                                INITIAL_CAP: 1000000,
-                                M: 40,
-                                EF_CONSTRUCTION: 250,
-                                EF_RUNTIME: 20
-                            }
-                        }),
-                        [
-                            'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'HNSW', '14', 'TYPE',
-                            'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000',
-                            'M', '40', 'EF_CONSTRUCTION', '250', 'EF_RUNTIME', '20'
-                        ]
-                    );
-                });
-            });
-
-            describe('GEOSHAPE', () => {
-                describe('without options', () => {
-                    it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => {
-                        assert.deepEqual(
-                            transformArguments('index', {
-                                field: SchemaFieldTypes.GEOSHAPE
-                            }),
-                            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
-                        );
-                    });
-
-                    it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => {
-                        assert.deepEqual(
-                            transformArguments('index', {
-                                field: {
-                                    type: SchemaFieldTypes.GEOSHAPE
-                                }
-                            }),
-                            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
-                        );
-                    });
-                });
-
-                it('with COORD_SYSTEM', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.GEOSHAPE,
-                                COORD_SYSTEM: SCHEMA_GEO_SHAPE_COORD_SYSTEM.SPHERICAL
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL']
-                    );
-                });
-            });
-
-            describe('with generic options', () => {
-                it('with AS', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                AS: 'as'
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT']
-                    );
-                });
-
-                describe('with SORTABLE', () => {
-                    it('true', () => {
-                        assert.deepEqual(
-                            transformArguments('index', {
-                                field: {
-                                    type: SchemaFieldTypes.TEXT,
-                                    SORTABLE: true
-                                }
-                            }),
-                            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE']
-                        );
-                    });
-
-                    it('UNF', () => {
-                        assert.deepEqual(
-                            transformArguments('index', {
-                                field: {
-                                    type: SchemaFieldTypes.TEXT,
-                                    SORTABLE: 'UNF'
-                                }
-                            }),
-                            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF']
-                        );
-                    });
-                });
-
-                it('with NOINDEX', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                NOINDEX: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX']
-                    );
-                });
-
-                it('with INDEXMISSING', () => {
-                    assert.deepEqual(
-                        transformArguments('index', {
-                            field: {
-                                type: SchemaFieldTypes.TEXT,
-                                INDEXMISSING: true
-                            }
-                        }),
-                        ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING']
-                    );
-                });
-            });
+        it('with NOSTEM', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TEXT,
+                NOSTEM: true
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM']
+          );
         });
 
-        it('with ON', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    ON: 'HASH'
-                }),
-                ['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA']
-            );
+        it('with WEIGHT', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TEXT,
+                WEIGHT: 1
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1']
+          );
         });
 
-        describe('with PREFIX', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('index', {}, {
-                        PREFIX: 'prefix'
-                    }),
-                    ['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA']
-                );
-            });
-
-            it('Array', () => {
-                assert.deepEqual(
-                    transformArguments('index', {}, {
-                        PREFIX: ['1', '2']
-                    }),
-                    ['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA']
-                );
-            });
+        it('with PHONETIC', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TEXT,
+                PHONETIC: SCHEMA_TEXT_FIELD_PHONETIC.DM_EN
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SCHEMA_TEXT_FIELD_PHONETIC.DM_EN]
+          );
         });
 
-        it('with FILTER', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    FILTER: '@field != ""'
-                }),
-                ['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA']
-            );
+        it('with WITHSUFFIXTRIE', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TEXT,
+                WITHSUFFIXTRIE: true
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE']
+          );
         });
+      });
+
+      it('NUMERIC', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {
+            field: SCHEMA_FIELD_TYPE.NUMERIC
+          }),
+          ['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC']
+        );
+      });
+
+      it('GEO', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {
+            field: SCHEMA_FIELD_TYPE.GEO
+          }),
+          ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO']
+        );
+      });
 
-        it('with LANGUAGE', () => {
+      describe('TAG', () => {
+        describe('without options', () => {
+          it('SCHEMA_FIELD_TYPE.TAG', () => {
             assert.deepEqual(
-                transformArguments('index', {}, {
-                    LANGUAGE: RedisSearchLanguages.ARABIC
-                }),
-                ['FT.CREATE', 'index', 'LANGUAGE', RedisSearchLanguages.ARABIC, 'SCHEMA']
+              parseArgs(CREATE, 'index', {
+                field: SCHEMA_FIELD_TYPE.TAG
+              }),
+              ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
             );
-        });
+          });
 
-        it('with LANGUAGE_FIELD', () => {
+          it('{ type: SCHEMA_FIELD_TYPE.TAG }', () => {
             assert.deepEqual(
-                transformArguments('index', {}, {
-                    LANGUAGE_FIELD: '@field'
-                }),
-                ['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA']
+              parseArgs(CREATE, 'index', {
+                field: {
+                  type: SCHEMA_FIELD_TYPE.TAG
+                }
+              }),
+              ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
             );
+          });
         });
 
-        it('with SCORE', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    SCORE: 1
-                }),
-                ['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA']
-            );
+        it('with SEPARATOR', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TAG,
+                SEPARATOR: 'separator'
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPARATOR', 'separator']
+          );
         });
 
-        it('with SCORE_FIELD', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    SCORE_FIELD: '@field'
-                }),
-                ['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA']
-            );
+        it('with CASESENSITIVE', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TAG,
+                CASESENSITIVE: true
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE']
+          );
         });
 
-        it('with MAXTEXTFIELDS', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    MAXTEXTFIELDS: true
-                }),
-                ['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA']
-            );
+        it('with WITHSUFFIXTRIE', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TAG,
+                WITHSUFFIXTRIE: true
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE']
+          );
         });
 
-        it('with TEMPORARY', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    TEMPORARY: 1
-                }),
-                ['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA']
-            );
+	it('with INDEXEMPTY', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TAG,
+                INDEXEMPTY: true
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY']
+          );
         });
-
-        it('with NOOFFSETS', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    NOOFFSETS: true
-                }),
-                ['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA']
-            );
+      });
+
+      describe('VECTOR', () => {
+        it('Flat algorithm', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.VECTOR,
+                ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT,
+                TYPE: 'FLOAT32',
+                DIM: 2,
+                DISTANCE_METRIC: 'L2',
+                INITIAL_CAP: 1000000,
+                BLOCK_SIZE: 1000
+              }
+            }),
+            [
+              'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'FLAT', '10', 'TYPE',
+              'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000',
+              'BLOCK_SIZE', '1000'
+            ]
+          );
         });
 
-        it('with NOHL', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    NOHL: true
-                }),
-                ['FT.CREATE', 'index', 'NOHL', 'SCHEMA']
-            );
+        it('HNSW algorithm', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.VECTOR,
+                ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW,
+                TYPE: 'FLOAT32',
+                DIM: 2,
+                DISTANCE_METRIC: 'L2',
+                INITIAL_CAP: 1000000,
+                M: 40,
+                EF_CONSTRUCTION: 250,
+                EF_RUNTIME: 20
+              }
+            }),
+            [
+              'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'HNSW', '14', 'TYPE',
+              'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000',
+              'M', '40', 'EF_CONSTRUCTION', '250', 'EF_RUNTIME', '20'
+            ]
+          );
         });
+      });
 
-        it('with NOFIELDS', () => {
+      describe('GEOSHAPE', () => {
+        describe('without options', () => {
+          it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => {
             assert.deepEqual(
-                transformArguments('index', {}, {
-                    NOFIELDS: true
-                }),
-                ['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA']
+              parseArgs(CREATE, 'index', {
+                field: SCHEMA_FIELD_TYPE.GEOSHAPE
+              }),
+              ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
             );
-        });
+          });
 
-        it('with NOFREQS', () => {
+          it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => {
             assert.deepEqual(
-                transformArguments('index', {}, {
-                    NOFREQS: true
-                }),
-                ['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA']
+              parseArgs(CREATE, 'index', {
+                field: {
+                  type: SCHEMA_FIELD_TYPE.GEOSHAPE
+                }
+              }),
+              ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
             );
+          });
         });
 
-        it('with SKIPINITIALSCAN', () => {
-            assert.deepEqual(
-                transformArguments('index', {}, {
-                    SKIPINITIALSCAN: true
-                }),
-                ['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA']
-            );
+        it('with COORD_SYSTEM', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.GEOSHAPE,
+                COORD_SYSTEM: 'SPHERICAL'
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL']
+          );
+        });
+      });
+  
+      it('with AS', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {
+            field: {
+              type: SCHEMA_FIELD_TYPE.TEXT,
+              AS: 'as'
+            }
+          }),
+          ['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT']
+        );
+      });
+
+      describe('with SORTABLE', () => {
+        it('true', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TEXT,
+                SORTABLE: true
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE']
+          );
         });
 
-        describe('with STOPWORDS', () => {
-            it('string', () => {
-                assert.deepEqual(
-                    transformArguments('index', {}, {
-                        STOPWORDS: 'stopword'
-                    }),
-                    ['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA']
-                );
-            });
-
-            it('Array', () => {
-                assert.deepEqual(
-                    transformArguments('index', {}, {
-                        STOPWORDS: ['1', '2']
-                    }),
-                    ['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA']
-                );
-            });
+        it('UNF', () => {
+          assert.deepEqual(
+            parseArgs(CREATE, 'index', {
+              field: {
+                type: SCHEMA_FIELD_TYPE.TEXT,
+                SORTABLE: 'UNF'
+              }
+            }),
+            ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF']
+          );
         });
+      });
+
+      it('with NOINDEX', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {
+            field: {
+              type: SCHEMA_FIELD_TYPE.TEXT,
+              NOINDEX: true
+            }
+          }),
+          ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX']
+        );
+      });
+
+      it('with INDEXMISSING', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {
+            field: {
+              type: SCHEMA_FIELD_TYPE.TEXT,
+              INDEXMISSING: true
+            }
+          }),
+          ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING']
+        );
+      });
     });
 
-    testUtils.testWithClient('client.ft.create', async client => {
-        assert.equal(
-            await client.ft.create('index', {
-                field: SchemaFieldTypes.TEXT
-            }),
-            'OK'
+    it('with ON', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          ON: 'HASH'
+        }),
+        ['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA']
+      );
+    });
+
+    describe('with PREFIX', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {}, {
+            PREFIX: 'prefix'
+          }),
+          ['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA']
+        );
+      });
+
+      it('Array', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {}, {
+            PREFIX: ['1', '2']
+          }),
+          ['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA']
+        );
+      });
+    });
+
+    it('with FILTER', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          FILTER: '@field != ""'
+        }),
+        ['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA']
+      );
+    });
+
+    it('with LANGUAGE', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          LANGUAGE: REDISEARCH_LANGUAGE.ARABIC
+        }),
+        ['FT.CREATE', 'index', 'LANGUAGE', REDISEARCH_LANGUAGE.ARABIC, 'SCHEMA']
+      );
+    });
+
+    it('with LANGUAGE_FIELD', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          LANGUAGE_FIELD: '@field'
+        }),
+        ['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA']
+      );
+    });
+
+    it('with SCORE', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          SCORE: 1
+        }),
+        ['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA']
+      );
+    });
+
+    it('with SCORE_FIELD', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          SCORE_FIELD: '@field'
+        }),
+        ['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA']
+      );
+    });
+
+    it('with MAXTEXTFIELDS', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          MAXTEXTFIELDS: true
+        }),
+        ['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA']
+      );
+    });
+
+    it('with TEMPORARY', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          TEMPORARY: 1
+        }),
+        ['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA']
+      );
+    });
+
+    it('with NOOFFSETS', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          NOOFFSETS: true
+        }),
+        ['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA']
+      );
+    });
+
+    it('with NOHL', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          NOHL: true
+        }),
+        ['FT.CREATE', 'index', 'NOHL', 'SCHEMA']
+      );
+    });
+
+    it('with NOFIELDS', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          NOFIELDS: true
+        }),
+        ['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA']
+      );
+    });
+
+    it('with NOFREQS', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          NOFREQS: true
+        }),
+        ['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA']
+      );
+    });
+
+    it('with SKIPINITIALSCAN', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'index', {}, {
+          SKIPINITIALSCAN: true
+        }),
+        ['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA']
+      );
+    });
+
+    describe('with STOPWORDS', () => {
+      it('string', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {}, {
+            STOPWORDS: 'stopword'
+          }),
+          ['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA']
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
+
+      it('Array', () => {
+        assert.deepEqual(
+          parseArgs(CREATE, 'index', {}, {
+            STOPWORDS: ['1', '2']
+          }),
+          ['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA']
+        );
+      });
+    });
+  });
+
+  testUtils.testWithClient('client.ft.create', async client => {
+    assert.equal(
+      await client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.ft.create vector types big floats', async client => {
+    assert.equal(
+      await client.ft.create("index_float32", {
+        field: {
+          ALGORITHM: "FLAT",
+          TYPE: "FLOAT32",
+          DIM: 1,
+          DISTANCE_METRIC: 'COSINE',
+          type: 'VECTOR'
+        },
+      }),
+      "OK"
+    );
+
+    assert.equal(
+      await client.ft.create("index_float64", {
+        field: {
+          ALGORITHM: "FLAT",
+          TYPE: "FLOAT64",
+          DIM: 1,
+          DISTANCE_METRIC: 'COSINE',
+          type: 'VECTOR'
+        },
+      }),
+      "OK"
+    );
+  }, GLOBAL.SERVERS.OPEN);
+
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.create vector types small floats and ints', async client => {
+    assert.equal(
+      await client.ft.create("index_float16", {
+        field: {
+          ALGORITHM: "FLAT",
+          TYPE: "FLOAT16",
+          DIM: 1,
+          DISTANCE_METRIC: 'COSINE',
+          type: 'VECTOR'
+        },
+      }),
+      "OK"
+    );
+
+    assert.equal(
+      await client.ft.create("index_bloat16", {
+        field: {
+          ALGORITHM: "FLAT",
+          TYPE: "BFLOAT16",
+          DIM: 1,
+          DISTANCE_METRIC: 'COSINE',
+          type: 'VECTOR'
+        },
+      }),
+      "OK"
+    );
+   
+    assert.equal(
+      await client.ft.create("index_int8", {
+        field: {
+          ALGORITHM: "FLAT",
+          TYPE: "INT8",
+          DIM: 1,
+          DISTANCE_METRIC: 'COSINE',
+          type: 'VECTOR'
+        },
+      }),
+      "OK"
+    );
+
+    assert.equal(
+      await client.ft.create("index_uint8", {
+        field: {
+          ALGORITHM: "FLAT",
+          TYPE: "UINT8",
+          DIM: 1,
+          DISTANCE_METRIC: 'COSINE',
+          type: 'VECTOR'
+        },
+      }),
+      "OK"
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts
index 21662c28d7d..9f24a256fae 100644
--- a/packages/search/lib/commands/CREATE.ts
+++ b/packages/search/lib/commands/CREATE.ts
@@ -1,91 +1,377 @@
-import { pushOptionalVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers';
-import { RedisSearchLanguages, PropertyName, RediSearchSchema, pushSchema } from '.';
-
-interface CreateOptions {
-    ON?: 'HASH' | 'JSON';
-    PREFIX?: string | Array<string>;
-    FILTER?: string;
-    LANGUAGE?: RedisSearchLanguages;
-    LANGUAGE_FIELD?: PropertyName;
-    SCORE?: number;
-    SCORE_FIELD?: PropertyName;
-    // PAYLOAD_FIELD?: string;
-    MAXTEXTFIELDS?: true;
-    TEMPORARY?: number;
-    NOOFFSETS?: true;
-    NOHL?: true;
-    NOFIELDS?: true;
-    NOFREQS?: true;
-    SKIPINITIALSCAN?: true;
-    STOPWORDS?: string | Array<string>;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+
+export const SCHEMA_FIELD_TYPE = {
+  TEXT: 'TEXT',
+  NUMERIC: 'NUMERIC',
+  GEO: 'GEO',
+  TAG: 'TAG',
+  VECTOR: 'VECTOR',
+  GEOSHAPE: 'GEOSHAPE'
+} as const;
+
+export type SchemaFieldType = typeof SCHEMA_FIELD_TYPE[keyof typeof SCHEMA_FIELD_TYPE];
+
+interface SchemaField<T extends SchemaFieldType = SchemaFieldType> {
+  type: T;
+  AS?: RedisArgument;
+  INDEXMISSING?: boolean;
+}
+
+interface SchemaCommonField<T extends SchemaFieldType = SchemaFieldType> extends SchemaField<T> {
+  SORTABLE?: boolean | 'UNF'
+  NOINDEX?: boolean;
+}
+
+export const SCHEMA_TEXT_FIELD_PHONETIC = {
+  DM_EN: 'dm:en',
+  DM_FR: 'dm:fr',
+  FM_PT: 'dm:pt',
+  DM_ES: 'dm:es'
+} as const;
+
+export type SchemaTextFieldPhonetic = typeof SCHEMA_TEXT_FIELD_PHONETIC[keyof typeof SCHEMA_TEXT_FIELD_PHONETIC];
+
+interface SchemaTextField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['TEXT']> {
+  NOSTEM?: boolean;
+  WEIGHT?: number;
+  PHONETIC?: SchemaTextFieldPhonetic;
+  WITHSUFFIXTRIE?: boolean;
+  INDEXEMPTY?: boolean;
+}
+
+interface SchemaNumericField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['NUMERIC']> {}
+
+interface SchemaGeoField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['GEO']> {}
+
+interface SchemaTagField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['TAG']> {
+  SEPARATOR?: RedisArgument;
+  CASESENSITIVE?: boolean;
+  WITHSUFFIXTRIE?: boolean;
+  INDEXEMPTY?: boolean;
+}
+
+export const SCHEMA_VECTOR_FIELD_ALGORITHM = {
+  FLAT: 'FLAT',
+  HNSW: 'HNSW'
+} as const;
+
+export type SchemaVectorFieldAlgorithm = typeof SCHEMA_VECTOR_FIELD_ALGORITHM[keyof typeof SCHEMA_VECTOR_FIELD_ALGORITHM];
+
+interface SchemaVectorField extends SchemaField<typeof SCHEMA_FIELD_TYPE['VECTOR']> {
+  ALGORITHM: SchemaVectorFieldAlgorithm;
+  TYPE: 'FLOAT32' | 'FLOAT64' | 'BFLOAT16' | 'FLOAT16' | 'INT8' | 'UINT8';
+  DIM: number;
+  DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE';
+  INITIAL_CAP?: number;
+}
+
+interface SchemaFlatVectorField extends SchemaVectorField {
+  ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['FLAT'];
+  BLOCK_SIZE?: number;
+}
+
+interface SchemaHNSWVectorField extends SchemaVectorField {
+  ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['HNSW'];
+  M?: number;
+  EF_CONSTRUCTION?: number;
+  EF_RUNTIME?: number;
+}
+
+export const SCHEMA_GEO_SHAPE_COORD_SYSTEM = {
+  SPHERICAL: 'SPHERICAL',
+  FLAT: 'FLAT'
+} as const;
+
+export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM];
+
+interface SchemaGeoShapeField extends SchemaField<typeof SCHEMA_FIELD_TYPE['GEOSHAPE']> {
+  COORD_SYSTEM?: SchemaGeoShapeFieldCoordSystem;
+}
+
+export interface RediSearchSchema {
+  [field: string]: (
+    SchemaTextField |
+    SchemaNumericField |
+    SchemaGeoField |
+    SchemaTagField |
+    SchemaFlatVectorField |
+    SchemaHNSWVectorField |
+    SchemaGeoShapeField |
+    SchemaFieldType
+  );
+}
+
+function parseCommonSchemaFieldOptions(parser: CommandParser, fieldOptions: SchemaCommonField) {
+  if (fieldOptions.SORTABLE) {
+    parser.push('SORTABLE');
+
+    if (fieldOptions.SORTABLE === 'UNF') {
+      parser.push('UNF');
+    }
+  }
+
+  if (fieldOptions.NOINDEX) {
+    parser.push('NOINDEX');
+  }
+}
+
+export function parseSchema(parser: CommandParser, schema: RediSearchSchema) {
+  for (const [field, fieldOptions] of Object.entries(schema)) {
+    parser.push(field);
+
+    if (typeof fieldOptions === 'string') {
+      parser.push(fieldOptions);
+      continue;
+    }
+
+    if (fieldOptions.AS) {
+      parser.push('AS', fieldOptions.AS);
+    }
+
+    parser.push(fieldOptions.type);
+
+    if (fieldOptions.INDEXMISSING) {
+      parser.push('INDEXMISSING');
+    }
+
+    switch (fieldOptions.type) {
+      case SCHEMA_FIELD_TYPE.TEXT:
+        if (fieldOptions.NOSTEM) {
+          parser.push('NOSTEM');
+        }
+
+        if (fieldOptions.WEIGHT) {
+          parser.push('WEIGHT', fieldOptions.WEIGHT.toString());
+        }
+
+        if (fieldOptions.PHONETIC) {
+          parser.push('PHONETIC', fieldOptions.PHONETIC);
+        }
+
+        if (fieldOptions.WITHSUFFIXTRIE) {
+          parser.push('WITHSUFFIXTRIE');
+        }
+
+        if (fieldOptions.INDEXEMPTY) {
+          parser.push('INDEXEMPTY');
+        }
+
+        parseCommonSchemaFieldOptions(parser, fieldOptions)
+        break;
+
+      case SCHEMA_FIELD_TYPE.NUMERIC:
+      case SCHEMA_FIELD_TYPE.GEO:
+        parseCommonSchemaFieldOptions(parser, fieldOptions)
+        break;
+
+      case SCHEMA_FIELD_TYPE.TAG:
+        if (fieldOptions.SEPARATOR) {
+          parser.push('SEPARATOR', fieldOptions.SEPARATOR);
+        }
+
+        if (fieldOptions.CASESENSITIVE) {
+          parser.push('CASESENSITIVE');
+        }
+
+        if (fieldOptions.WITHSUFFIXTRIE) {
+          parser.push('WITHSUFFIXTRIE');
+        }
+
+        if (fieldOptions.INDEXEMPTY) {
+          parser.push('INDEXEMPTY');
+        }
+
+        parseCommonSchemaFieldOptions(parser, fieldOptions)
+        break;
+
+      case SCHEMA_FIELD_TYPE.VECTOR:
+        parser.push(fieldOptions.ALGORITHM);
+
+        const args: Array<RedisArgument> = [];
+
+        args.push(
+          'TYPE', fieldOptions.TYPE,
+          'DIM', fieldOptions.DIM.toString(),
+          'DISTANCE_METRIC', fieldOptions.DISTANCE_METRIC
+        );
+
+        if (fieldOptions.INITIAL_CAP) {
+          args.push('INITIAL_CAP', fieldOptions.INITIAL_CAP.toString());
+        }
+
+        switch (fieldOptions.ALGORITHM) {          
+          case SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT:
+            if (fieldOptions.BLOCK_SIZE) {
+              args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString());
+            }
+
+            break;
+
+          case SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW:
+            if (fieldOptions.M) {
+              args.push('M', fieldOptions.M.toString());
+            }
+
+            if (fieldOptions.EF_CONSTRUCTION) {
+              args.push('EF_CONSTRUCTION', fieldOptions.EF_CONSTRUCTION.toString());
+            }
+
+            if (fieldOptions.EF_RUNTIME) {
+              args.push('EF_RUNTIME', fieldOptions.EF_RUNTIME.toString());
+            }
+
+            break;
+        }
+        parser.pushVariadicWithLength(args);
+
+        break;
+    
+      case SCHEMA_FIELD_TYPE.GEOSHAPE:
+        if (fieldOptions.COORD_SYSTEM !== undefined) {
+          parser.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM);
+        }
+
+        break;
+    }
+  }
 }
 
-export function transformArguments(index: string, schema: RediSearchSchema, options?: CreateOptions): Array<string> {
-    const args = ['FT.CREATE', index];
+export const REDISEARCH_LANGUAGE = {
+  ARABIC: 'Arabic',
+  BASQUE: 'Basque',
+  CATALANA: 'Catalan',
+  DANISH: 'Danish',
+  DUTCH: 'Dutch',
+  ENGLISH: 'English',
+  FINNISH: 'Finnish',
+  FRENCH: 'French',
+  GERMAN: 'German',
+  GREEK: 'Greek',
+  HUNGARIAN: 'Hungarian',
+  INDONESAIN: 'Indonesian',
+  IRISH: 'Irish',
+  ITALIAN: 'Italian',
+  LITHUANIAN: 'Lithuanian',
+  NEPALI: 'Nepali',
+  NORWEIGAN: 'Norwegian',
+  PORTUGUESE: 'Portuguese',
+  ROMANIAN: 'Romanian',
+  RUSSIAN: 'Russian',
+  SPANISH: 'Spanish',
+  SWEDISH: 'Swedish',
+  TAMIL: 'Tamil',
+  TURKISH: 'Turkish',
+  CHINESE: 'Chinese'
+} as const;
+
+export type RediSearchLanguage = typeof REDISEARCH_LANGUAGE[keyof typeof REDISEARCH_LANGUAGE];
+
+export type RediSearchProperty = `${'@' | '$.'}${string}`;
+
+export interface CreateOptions {
+  ON?: 'HASH' | 'JSON';
+  PREFIX?: RedisVariadicArgument;
+  FILTER?: RedisArgument;
+  LANGUAGE?: RediSearchLanguage;
+  LANGUAGE_FIELD?: RediSearchProperty;
+  SCORE?: number;
+  SCORE_FIELD?: RediSearchProperty;
+  // PAYLOAD_FIELD?: string;
+  MAXTEXTFIELDS?: boolean;
+  TEMPORARY?: number;
+  NOOFFSETS?: boolean;
+  NOHL?: boolean;
+  NOFIELDS?: boolean;
+  NOFREQS?: boolean;
+  SKIPINITIALSCAN?: boolean;
+  STOPWORDS?: RedisVariadicArgument;
+}
+
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Creates a new search index with the given schema and options.
+   * @param parser - The command parser
+   * @param index - Name of the index to create
+   * @param schema - Index schema defining field names and types (TEXT, NUMERIC, GEO, TAG, VECTOR, GEOSHAPE)
+   * @param options - Optional parameters:
+   *   - ON: Type of container to index (HASH or JSON)
+   *   - PREFIX: Prefixes for document keys to index
+   *   - FILTER: Expression that filters indexed documents
+   *   - LANGUAGE/LANGUAGE_FIELD: Default language for indexing
+   *   - SCORE/SCORE_FIELD: Document ranking parameters
+   *   - MAXTEXTFIELDS: Index all text fields without specifying them
+   *   - TEMPORARY: Create a temporary index
+   *   - NOOFFSETS/NOHL/NOFIELDS/NOFREQS: Index optimization flags
+   *   - STOPWORDS: Custom stopword list
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) {
+    parser.push('FT.CREATE', index);
 
     if (options?.ON) {
-        args.push('ON', options.ON);
+      parser.push('ON', options.ON);
     }
 
-    pushOptionalVerdictArgument(args, 'PREFIX', options?.PREFIX);
+    parseOptionalVariadicArgument(parser, 'PREFIX', options?.PREFIX);
 
     if (options?.FILTER) {
-        args.push('FILTER', options.FILTER);
+      parser.push('FILTER', options.FILTER);
     }
 
     if (options?.LANGUAGE) {
-        args.push('LANGUAGE', options.LANGUAGE);
+      parser.push('LANGUAGE', options.LANGUAGE);
     }
 
     if (options?.LANGUAGE_FIELD) {
-        args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD);
+      parser.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD);
     }
 
     if (options?.SCORE) {
-        args.push('SCORE', options.SCORE.toString());
+      parser.push('SCORE', options.SCORE.toString());
     }
 
     if (options?.SCORE_FIELD) {
-        args.push('SCORE_FIELD', options.SCORE_FIELD);
+      parser.push('SCORE_FIELD', options.SCORE_FIELD);
     }
 
     // if (options?.PAYLOAD_FIELD) {
-    //     args.push('PAYLOAD_FIELD', options.PAYLOAD_FIELD);
+    //     parser.push('PAYLOAD_FIELD', options.PAYLOAD_FIELD);
     // }
 
     if (options?.MAXTEXTFIELDS) {
-        args.push('MAXTEXTFIELDS');
+      parser.push('MAXTEXTFIELDS');
     }
 
     if (options?.TEMPORARY) {
-        args.push('TEMPORARY', options.TEMPORARY.toString());
+      parser.push('TEMPORARY', options.TEMPORARY.toString());
     }
 
     if (options?.NOOFFSETS) {
-        args.push('NOOFFSETS');
+      parser.push('NOOFFSETS');
     }
 
     if (options?.NOHL) {
-        args.push('NOHL');
+      parser.push('NOHL');
     }
 
     if (options?.NOFIELDS) {
-        args.push('NOFIELDS');
+      parser.push('NOFIELDS');
     }
 
     if (options?.NOFREQS) {
-        args.push('NOFREQS');
+      parser.push('NOFREQS');
     }
 
     if (options?.SKIPINITIALSCAN) {
-        args.push('SKIPINITIALSCAN');
+      parser.push('SKIPINITIALSCAN');
     }
 
-    pushOptionalVerdictArgument(args, 'STOPWORDS', options?.STOPWORDS);
-    args.push('SCHEMA');
-    pushSchema(args, schema);
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+    parseOptionalVariadicArgument(parser, 'STOPWORDS', options?.STOPWORDS);
+    parser.push('SCHEMA');
+    parseSchema(parser, schema);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/CURSOR_DEL.spec.ts b/packages/search/lib/commands/CURSOR_DEL.spec.ts
index d89725ef80d..230a5fd0feb 100644
--- a/packages/search/lib/commands/CURSOR_DEL.spec.ts
+++ b/packages/search/lib/commands/CURSOR_DEL.spec.ts
@@ -1,33 +1,33 @@
-import { strict as assert } from 'assert';
-import { SchemaFieldTypes } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CURSOR_DEL';
+import CURSOR_DEL from './CURSOR_DEL';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CURSOR DEL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('index', 0),
-            ['FT.CURSOR', 'DEL', 'index', '0']
-        );
-    });
+describe('FT.CURSOR DEL', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(CURSOR_DEL, 'index', 0),
+      ['FT.CURSOR', 'DEL', 'index', '0']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.cursorDel', async client => {
-        const [ ,, { cursor } ] = await Promise.all([
-            client.ft.create('idx', {
-                field: {
-                    type: SchemaFieldTypes.TEXT
-                }
-            }),
-            client.hSet('key', 'field', 'value'),
-            client.ft.aggregateWithCursor('idx', '*', {
-                COUNT: 1
-            })
-        ]);
+  testUtils.testWithClient('client.ft.cursorDel', async client => {
+    const [, , { cursor }] = await Promise.all([
+      client.ft.create('idx', {
+        field: {
+          type: SCHEMA_FIELD_TYPE.TEXT
+        }
+      }),
+      client.hSet('key', 'field', 'value'),
+      client.ft.aggregateWithCursor('idx', '*', {
+        COUNT: 1
+      })
+    ]);
 
-
-        assert.equal(
-            await client.ft.cursorDel('idx', cursor),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(
+      await client.ft.cursorDel('idx', cursor),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/CURSOR_DEL.ts b/packages/search/lib/commands/CURSOR_DEL.ts
index 22c850f2a89..39d0dc8af01 100644
--- a/packages/search/lib/commands/CURSOR_DEL.ts
+++ b/packages/search/lib/commands/CURSOR_DEL.ts
@@ -1,14 +1,17 @@
-import { RedisCommandArgument } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export function transformArguments(index: RedisCommandArgument, cursorId: number) {
-    return [
-        'FT.CURSOR',
-        'DEL',
-        index,
-        cursorId.toString()
-    ];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Deletes a cursor from an index.
+   * @param parser - The command parser
+   * @param index - The index name that contains the cursor
+   * @param cursorId - The cursor ID to delete
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, cursorId: UnwrapReply<NumberReply>) {
+    parser.push('FT.CURSOR', 'DEL', index, cursorId.toString());
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/CURSOR_READ.spec.ts b/packages/search/lib/commands/CURSOR_READ.spec.ts
index bb68e2b6396..42dca0c5756 100644
--- a/packages/search/lib/commands/CURSOR_READ.spec.ts
+++ b/packages/search/lib/commands/CURSOR_READ.spec.ts
@@ -1,45 +1,45 @@
-import { strict as assert } from 'assert';
-import { SchemaFieldTypes } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CURSOR_READ';
+import CURSOR_READ from './CURSOR_READ';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CURSOR READ', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', 0),
-                ['FT.CURSOR', 'READ', 'index', '0']
-            );
-        });
+describe('FT.CURSOR READ', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(CURSOR_READ, 'index', '0'),
+        ['FT.CURSOR', 'READ', 'index', '0']
+      );
+    });
 
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('index', 0, { COUNT: 1 }),
-                ['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1']
-            );
-        });
+    it('with COUNT', () => {
+      assert.deepEqual(
+        parseArgs(CURSOR_READ, 'index', '0', {
+          COUNT: 1
+        }),
+        ['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.cursorRead', async client => {
-        const [, , { cursor }] = await Promise.all([
-            client.ft.create('idx', {
-                field: {
-                    type: SchemaFieldTypes.TEXT
-                }
-            }),
-            client.hSet('key', 'field', 'value'),
-            client.ft.aggregateWithCursor('idx', '*', {
-                COUNT: 1
-            })
-        ]);
+  testUtils.testWithClient('client.ft.cursorRead', async client => {
+    const [, , { cursor }] = await Promise.all([
+      client.ft.create('idx', {
+        field: 'TEXT'
+      }),
+      client.hSet('key', 'field', 'value'),
+      client.ft.aggregateWithCursor('idx', '*', {
+        COUNT: 1
+      })
+    ]);
 
-        assert.deepEqual(
-            await client.ft.cursorRead('idx', cursor),
-            {
-                total: 0,
-                results: [],
-                cursor: 0
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(
+      await client.ft.cursorRead('idx', cursor),
+      {
+        total: 0,
+        results: [],
+        cursor: 0
+      }
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/CURSOR_READ.ts b/packages/search/lib/commands/CURSOR_READ.ts
index 35cf1bc4f06..50ee5eafbd6 100644
--- a/packages/search/lib/commands/CURSOR_READ.ts
+++ b/packages/search/lib/commands/CURSOR_READ.ts
@@ -1,30 +1,29 @@
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
+import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface CursorReadOptions {
-    COUNT?: number;
+export interface FtCursorReadOptions {
+  COUNT?: number;
 }
 
-export function transformArguments(
-    index: RedisCommandArgument,
-    cursor: number,
-    options?: CursorReadOptions
-): RedisCommandArguments {
-    const args = [
-        'FT.CURSOR',
-        'READ',
-        index,
-        cursor.toString()
-    ];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Reads from an existing cursor to get more results from an index.
+   * @param parser - The command parser
+   * @param index - The index name that contains the cursor
+   * @param cursor - The cursor ID to read from
+   * @param options - Optional parameters:
+   *   - COUNT: Maximum number of results to return
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, cursor: UnwrapReply<NumberReply>, options?: FtCursorReadOptions) {
+    parser.push('FT.CURSOR', 'READ', index, cursor.toString());
 
-    if (options?.COUNT) {
-        args.push('COUNT', options.COUNT.toString());
+    if (options?.COUNT !== undefined) {
+      parser.push('COUNT', options.COUNT.toString());
     }
-
-    return args;
-}
-
-export { transformReply } from './AGGREGATE_WITHCURSOR';
+  },
+  transformReply: AGGREGATE_WITHCURSOR.transformReply,
+  unstableResp3: true
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/DICTADD.spec.ts b/packages/search/lib/commands/DICTADD.spec.ts
index b5f29dd4083..4707db02dcf 100644
--- a/packages/search/lib/commands/DICTADD.spec.ts
+++ b/packages/search/lib/commands/DICTADD.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DICTADD';
+import DICTADD from './DICTADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DICTADD', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('dictionary', 'term'),
-                ['FT.DICTADD', 'dictionary', 'term']
-            );
-        });
+describe('FT.DICTADD', () => {
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(DICTADD, 'dictionary', 'term'),
+        ['FT.DICTADD', 'dictionary', 'term']
+      );
+    });
 
-        it('Array', () => {
-            assert.deepEqual(
-                transformArguments('dictionary', ['1', '2']),
-                ['FT.DICTADD', 'dictionary', '1', '2']
-            );
-        });
+    it('Array', () => {
+      assert.deepEqual(
+        parseArgs(DICTADD, 'dictionary', ['1', '2']),
+        ['FT.DICTADD', 'dictionary', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.dictAdd', async client => {
-        assert.equal(
-            await client.ft.dictAdd('dictionary', 'term'),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft.dictAdd', async client => {
+    assert.equal(
+      await client.ft.dictAdd('dictionary', 'term'),
+      1
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/DICTADD.ts b/packages/search/lib/commands/DICTADD.ts
index 60af11fd41f..84936ff5f73 100644
--- a/packages/search/lib/commands/DICTADD.ts
+++ b/packages/search/lib/commands/DICTADD.ts
@@ -1,8 +1,19 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(dictionary: string, term: string | Array<string>): RedisCommandArguments {
-    return pushVerdictArguments(['FT.DICTADD', dictionary], term);
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Adds terms to a dictionary.
+   * @param parser - The command parser
+   * @param dictionary - Name of the dictionary to add terms to
+   * @param term - One or more terms to add to the dictionary
+   */
+  parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) {
+    parser.push('FT.DICTADD', dictionary);
+    parser.pushVariadic(term);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/DICTDEL.spec.ts b/packages/search/lib/commands/DICTDEL.spec.ts
index 5ffa6b6b84f..a9f997bdf38 100644
--- a/packages/search/lib/commands/DICTDEL.spec.ts
+++ b/packages/search/lib/commands/DICTDEL.spec.ts
@@ -1,28 +1,29 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DICTDEL';
+import DICTDEL from './DICTDEL';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DICTDEL', () => {
-    describe('transformArguments', () => {
-        it('string', () => {
-            assert.deepEqual(
-                transformArguments('dictionary', 'term'),
-                ['FT.DICTDEL', 'dictionary', 'term']
-            );
-        });
+describe('FT.DICTDEL', () => {
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        parseArgs(DICTDEL, 'dictionary', 'term'),
+        ['FT.DICTDEL', 'dictionary', 'term']
+      );
+    });
 
-        it('Array', () => {
-            assert.deepEqual(
-                transformArguments('dictionary', ['1', '2']),
-                ['FT.DICTDEL', 'dictionary', '1', '2']
-            );
-        });
+    it('Array', () => {
+      assert.deepEqual(
+        parseArgs(DICTDEL, 'dictionary', ['1', '2']),
+        ['FT.DICTDEL', 'dictionary', '1', '2']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.dictDel', async client => {
-        assert.equal(
-            await client.ft.dictDel('dictionary', 'term'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft.dictDel', async client => {
+    assert.equal(
+      await client.ft.dictDel('dictionary', 'term'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/DICTDEL.ts b/packages/search/lib/commands/DICTDEL.ts
index a1b728f1926..c39b03f45ef 100644
--- a/packages/search/lib/commands/DICTDEL.ts
+++ b/packages/search/lib/commands/DICTDEL.ts
@@ -1,8 +1,19 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export function transformArguments(dictionary: string, term: string | Array<string>): RedisCommandArguments {
-    return pushVerdictArguments(['FT.DICTDEL', dictionary], term);
-}
-
-export declare function transformReply(): number;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Deletes terms from a dictionary.
+   * @param parser - The command parser
+   * @param dictionary - Name of the dictionary to remove terms from
+   * @param term - One or more terms to delete from the dictionary
+   */
+  parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) {
+    parser.push('FT.DICTDEL', dictionary);
+    parser.pushVariadic(term);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/DICTDUMP.spec.ts b/packages/search/lib/commands/DICTDUMP.spec.ts
index 9896fb9440d..1a3faa9dc9d 100644
--- a/packages/search/lib/commands/DICTDUMP.spec.ts
+++ b/packages/search/lib/commands/DICTDUMP.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DICTDUMP';
+import DICTDUMP from './DICTDUMP';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DICTDUMP', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('dictionary'),
-            ['FT.DICTDUMP', 'dictionary']
-        );
-    });
+describe('FT.DICTDUMP', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DICTDUMP, 'dictionary'),
+      ['FT.DICTDUMP', 'dictionary']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.dictDump', async client => {
-        await client.ft.dictAdd('dictionary', 'string')
+  testUtils.testWithClient('client.ft.dictDump', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.dictAdd('dictionary', 'string'),
+      client.ft.dictDump('dictionary')
+    ]);
 
-        assert.deepEqual(
-            await client.ft.dictDump('dictionary'),
-            ['string']
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, ['string']);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts
index 1427bb42cb7..1ae40b4edb3 100644
--- a/packages/search/lib/commands/DICTDUMP.ts
+++ b/packages/search/lib/commands/DICTDUMP.ts
@@ -1,5 +1,19 @@
-export function transformArguments(dictionary: string): Array<string> {
-    return ['FT.DICTDUMP', dictionary];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns all terms in a dictionary.
+   * @param parser - The command parser
+   * @param dictionary - Name of the dictionary to dump
+   */
+  parseCommand(parser: CommandParser, dictionary: RedisArgument) {
+    parser.push('FT.DICTDUMP', dictionary);
+  },
+  transformReply: {
+    2: undefined as unknown as () => ArrayReply<BlobStringReply>,
+    3: undefined as unknown as () => SetReply<BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/DROPINDEX.spec.ts b/packages/search/lib/commands/DROPINDEX.spec.ts
index 6a60a5d851f..f1f0b0efddb 100644
--- a/packages/search/lib/commands/DROPINDEX.spec.ts
+++ b/packages/search/lib/commands/DROPINDEX.spec.ts
@@ -1,33 +1,34 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { SchemaFieldTypes } from '.';
-import { transformArguments } from './DROPINDEX';
+import DROPINDEX from './DROPINDEX';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DROPINDEX', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index'),
-                ['FT.DROPINDEX', 'index']
-            );
-        });
+describe('FT.DROPINDEX', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(DROPINDEX, 'index'),
+        ['FT.DROPINDEX', 'index']
+      );
+    });
 
-        it('with DD', () => {
-            assert.deepEqual(
-                transformArguments('index', { DD: true }),
-                ['FT.DROPINDEX', 'index', 'DD']
-            );
-        });
+    it('with DD', () => {
+      assert.deepEqual(
+        parseArgs(DROPINDEX, 'index', { DD: true }),
+        ['FT.DROPINDEX', 'index', 'DD']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.dropIndex', async client => {
-        await client.ft.create('index', {
-            field: SchemaFieldTypes.TEXT
-        });
+  testUtils.testWithClient('client.ft.dropIndex', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.dropIndex('index')
+    ]);
 
-        assert.equal(
-            await client.ft.dropIndex('index'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/DROPINDEX.ts b/packages/search/lib/commands/DROPINDEX.ts
index 7897a9dd82e..5b6e0dde786 100644
--- a/packages/search/lib/commands/DROPINDEX.ts
+++ b/packages/search/lib/commands/DROPINDEX.ts
@@ -1,15 +1,29 @@
-interface DropIndexOptions {
-    DD?: true;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+
+export interface FtDropIndexOptions {
+  DD?: true;
 }
 
-export function transformArguments(index: string, options?: DropIndexOptions): Array<string> {
-    const args = ['FT.DROPINDEX', index];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Deletes an index and all associated documents.
+   * @param parser - The command parser
+   * @param index - Name of the index to delete
+   * @param options - Optional parameters:
+   *   - DD: Also delete the indexed documents themselves
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, options?: FtDropIndexOptions) {
+    parser.push('FT.DROPINDEX', index);
 
     if (options?.DD) {
-        args.push('DD');
+      parser.push('DD');
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: {
+    2: undefined as unknown as () => SimpleStringReply<'OK'>,
+    3: undefined as unknown as () => NumberReply
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/EXPLAIN.spec.ts b/packages/search/lib/commands/EXPLAIN.spec.ts
index d24f5fe4ac5..d1691bc7c25 100644
--- a/packages/search/lib/commands/EXPLAIN.spec.ts
+++ b/packages/search/lib/commands/EXPLAIN.spec.ts
@@ -1,33 +1,48 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './EXPLAIN';
+import { strict as assert } from 'node:assert';
+import EXPLAIN from './EXPLAIN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import testUtils, { GLOBAL } from '../test-utils';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
 describe('EXPLAIN', () => {
-    describe('transformArguments', () => {
-        it('simple', () => {
-            assert.deepEqual(
-                transformArguments('index', '*'),
-                ['FT.EXPLAIN', 'index', '*']
-            );
-        });
+  describe('transformArguments', () => {
+    it('simple', () => {
+      assert.deepEqual(
+        parseArgs(EXPLAIN, 'index', '*'),
+        ['FT.EXPLAIN', 'index', '*', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with PARAMS', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', {
-                    PARAMS: {
-                        param: 'value'
-                    }
-                }),
-                ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value']
-            );
-        });
+    it('with PARAMS', () => {
+      assert.deepEqual(
+        parseArgs(EXPLAIN, 'index', '*', {
+          PARAMS: {
+            param: 'value'
+          }
+        }),
+        ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with DIALECT', () => {
-            assert.deepEqual(
-                transformArguments('index', '*', {
-                    DIALECT: 1
-                }),
-                ['FT.EXPLAIN', 'index', '*', 'DIALECT', '1']
-            );
-        });
+    it('with DIALECT', () => {
+      assert.deepEqual(
+        parseArgs(EXPLAIN, 'index', '*', {
+          DIALECT: 1
+        }),
+        ['FT.EXPLAIN', 'index', '*', 'DIALECT', '1']
+      );
     });
+  });
+
+  testUtils.testWithClient('client.ft.dropIndex', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.explain('index', '*')
+    ]);
+
+    assert.equal(reply, '<WILDCARD>\n');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts
index ab3935ff979..78d09ffeded 100644
--- a/packages/search/lib/commands/EXPLAIN.ts
+++ b/packages/search/lib/commands/EXPLAIN.ts
@@ -1,26 +1,40 @@
-import { Params, pushParamsArgs } from ".";
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { FtSearchParams, parseParamsArgument } from './SEARCH';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
-export const IS_READ_ONLY = true;
-
-interface ExplainOptions {
-    PARAMS?: Params;
-    DIALECT?: number;
+export interface FtExplainOptions {
+  PARAMS?: FtSearchParams;
+  DIALECT?: number;
 }
 
-export function transformArguments(
-    index: string,
-    query: string,
-    options?: ExplainOptions
-): Array<string> {
-    const args = ['FT.EXPLAIN', index, query];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the execution plan for a complex query.
+   * @param parser - The command parser
+   * @param index - Name of the index to explain query against
+   * @param query - The query string to explain
+   * @param options - Optional parameters:
+   *   - PARAMS: Named parameters to use in the query
+   *   - DIALECT: Version of query dialect to use (defaults to 1)
+   */
+  parseCommand(
+    parser: CommandParser,
+    index: RedisArgument,
+    query: RedisArgument,
+    options?: FtExplainOptions
+  ) {
+    parser.push('FT.EXPLAIN', index, query);
 
-    pushParamsArgs(args, options?.PARAMS);
+    parseParamsArgument(parser, options?.PARAMS);
 
     if (options?.DIALECT) {
-        args.push('DIALECT', options.DIALECT.toString());
+      parser.push('DIALECT', options.DIALECT.toString());
+    } else {
+      parser.push('DIALECT', DEFAULT_DIALECT);
     }
-
-    return args;
-}
-
-export declare function transformReply(): string;
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/EXPLAINCLI.spec.ts b/packages/search/lib/commands/EXPLAINCLI.spec.ts
index 238ef44eaaa..1812b674094 100644
--- a/packages/search/lib/commands/EXPLAINCLI.spec.ts
+++ b/packages/search/lib/commands/EXPLAINCLI.spec.ts
@@ -1,11 +1,20 @@
-import { strict as assert } from 'assert';
-import { transformArguments } from './EXPLAINCLI';
+import { strict as assert } from 'node:assert';
+import EXPLAINCLI from './EXPLAINCLI';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
 describe('EXPLAINCLI', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('index', '*'),
-            ['FT.EXPLAINCLI', 'index', '*']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(EXPLAINCLI, 'index', '*'),
+      ['FT.EXPLAINCLI', 'index', '*', 'DIALECT', DEFAULT_DIALECT]
+    );
+  });
+
+  it('with dialect', () => {
+    assert.deepEqual(
+      parseArgs(EXPLAINCLI, 'index', '*', {DIALECT: 1}),
+      ['FT.EXPLAINCLI', 'index', '*', 'DIALECT', '1']
+    );
+  });
 });
diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts
index db97fb9c8da..42e489ce10e 100644
--- a/packages/search/lib/commands/EXPLAINCLI.ts
+++ b/packages/search/lib/commands/EXPLAINCLI.ts
@@ -1,7 +1,35 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
-export function transformArguments(index: string, query: string): Array<string> {
-    return ['FT.EXPLAINCLI', index, query];
+export interface FtExplainCLIOptions {
+  DIALECT?: number;
 }
 
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the execution plan for a complex query in a more verbose format than FT.EXPLAIN.
+   * @param parser - The command parser
+   * @param index - Name of the index to explain query against
+   * @param query - The query string to explain
+   * @param options - Optional parameters:
+   *   - DIALECT: Version of query dialect to use (defaults to 1)
+   */
+  parseCommand(
+    parser: CommandParser, 
+    index: RedisArgument, 
+    query: RedisArgument,
+    options?: FtExplainCLIOptions
+  ) {
+    parser.push('FT.EXPLAINCLI', index, query);
+
+    if (options?.DIALECT) {
+      parser.push('DIALECT', options.DIALECT.toString());
+    } else {
+      parser.push('DIALECT', DEFAULT_DIALECT);
+    }
+  },
+  transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/INFO.spec.ts b/packages/search/lib/commands/INFO.spec.ts
index e026b44e264..b52e99ab9b0 100644
--- a/packages/search/lib/commands/INFO.spec.ts
+++ b/packages/search/lib/commands/INFO.spec.ts
@@ -1,99 +1,216 @@
-import { strict as assert } from 'assert';
-import { SchemaFieldTypes } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './INFO';
+import INFO, { InfoReply } from './INFO';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('INFO', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('index'),
-            ['FT.INFO', 'index']
-        );
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(INFO, 'index'),
+      ['FT.INFO', 'index']
+    );
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.info', async client => {
+
+    await client.ft.create('index', {
+      field: SCHEMA_FIELD_TYPE.TEXT
+    });
+    const ret = await client.ft.info('index');
+    assert.equal(ret.index_name, 'index');
+
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[7, 4, 2], [7, 4, 2]], 'client.ft.info', async client => {
+
+    await client.ft.create('index', {
+      field: SCHEMA_FIELD_TYPE.TEXT
+    });
+    const ret = await client.ft.info('index');
+    // effectively testing that stopwords_list is not in ret
+    assert.deepEqual(
+      ret,
+      {
+        index_name: 'index',
+        index_options: [],
+        index_definition: Object.create(null, {
+          default_score: {
+            value: '1',
+            configurable: true,
+            enumerable: true
+          },
+          key_type: {
+            value: 'HASH',
+            configurable: true,
+            enumerable: true
+          },
+          prefixes: {
+            value: [''],
+            configurable: true,
+            enumerable: true
+          }
+        }),
+        attributes: [Object.create(null, {
+          identifier: {
+            value: 'field',
+            configurable: true,
+            enumerable: true
+          },
+          attribute: {
+            value: 'field',
+            configurable: true,
+            enumerable: true
+          },
+          type: {
+            value: 'TEXT',
+            configurable: true,
+            enumerable: true
+          },
+          WEIGHT: {
+            value: '1',
+            configurable: true,
+            enumerable: true
+          }
+        })],
+        num_docs: 0,
+        max_doc_id: 0,
+        num_terms: 0,
+        num_records: 0,
+        inverted_sz_mb: 0,
+        vector_index_sz_mb: 0,
+        total_inverted_index_blocks: 0,
+        offset_vectors_sz_mb: 0,
+        doc_table_size_mb: 0,
+        sortable_values_size_mb: 0,
+        key_table_size_mb: 0,
+        records_per_doc_avg: NaN,
+        bytes_per_record_avg: NaN,
+        cleaning: 0,
+        offsets_per_term_avg: NaN,
+        offset_bits_per_record_avg: NaN,
+        geoshapes_sz_mb: 0,
+        hash_indexing_failures: 0,
+        indexing: 0,
+        percent_indexed: 1,
+        number_of_uses: 1,
+        tag_overhead_sz_mb: 0,
+        text_overhead_sz_mb: 0,
+        total_index_memory_sz_mb: 0,
+        total_indexing_time: 0,
+        gc_stats: {
+          bytes_collected: 0,
+          total_ms_run: 0,
+          total_cycles: 0,
+          average_cycle_time_ms: NaN,
+          last_run_time_ms: 0,
+          gc_numeric_trees_missed: 0,
+          gc_blocks_denied: 0
+        },
+        cursor_stats: {
+          global_idle: 0,
+          global_total: 0,
+          index_capacity: 128,
+          index_total: 0
+        },
+      }
+    );
+
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 2, 0]], 'client.ft.info', async client => {
+
+    await client.ft.create('index', {
+      field: SCHEMA_FIELD_TYPE.TEXT
     });
+    const ret = await client.ft.info('index');
+    // effectively testing that stopwords_list is not in ret
+    assert.deepEqual(
+      ret,
+      {
+        index_name: 'index',
+        index_options: [],
+        index_definition: Object.create(null, {
+          default_score: {
+            value: '1',
+            configurable: true,
+            enumerable: true
+          },
+          key_type: {
+            value: 'HASH',
+            configurable: true,
+            enumerable: true
+          },
+          prefixes: {
+            value: [''],
+            configurable: true,
+            enumerable: true
+          }
+        }),
+        attributes: [Object.create(null, {
+          identifier: {
+            value: 'field',
+            configurable: true,
+            enumerable: true
+          },
+          attribute: {
+            value: 'field',
+            configurable: true,
+            enumerable: true
+          },
+          type: {
+            value: 'TEXT',
+            configurable: true,
+            enumerable: true
+          },
+          WEIGHT: {
+            value: '1',
+            configurable: true,
+            enumerable: true
+          }
+        })],
+        num_docs: "0",
+        max_doc_id: "0",
+        num_terms: "0",
+        num_records: "0",
+        inverted_sz_mb: 0,
+        vector_index_sz_mb: 0,
+        total_inverted_index_blocks: "0",
+        offset_vectors_sz_mb: 0,
+        doc_table_size_mb: 0,
+        sortable_values_size_mb: 0,
+        key_table_size_mb: 0,
+        records_per_doc_avg: NaN,
+        bytes_per_record_avg: NaN,
+        cleaning: 0,
+        offsets_per_term_avg: NaN,
+        offset_bits_per_record_avg: NaN,
+        geoshapes_sz_mb: 0,
+        hash_indexing_failures: "0",
+        indexing: "0",
+        percent_indexed: 1,
+        number_of_uses: 1,
+        tag_overhead_sz_mb: 0,
+        text_overhead_sz_mb: 0,
+        total_index_memory_sz_mb: 0,
+        total_indexing_time: 0,
+        gc_stats: {
+          bytes_collected: 0,
+          total_ms_run: 0,
+          total_cycles: 0,
+          average_cycle_time_ms: NaN,
+          last_run_time_ms: 0,
+          gc_numeric_trees_missed: 0,
+          gc_blocks_denied: 0
+        },
+        cursor_stats: {
+          global_idle: 0,
+          global_total: 0,
+          index_capacity: 128,
+          index_total: 0
+        },
+      }
+    );
 
-    testUtils.testWithClient('client.ft.info', async client => {
-        await client.ft.create('index', {
-            field: SchemaFieldTypes.TEXT
-        });
-        assert.deepEqual(
-            await client.ft.info('index'),
-            {
-                indexName: 'index',
-                indexOptions: [],
-                indexDefinition: Object.create(null, {
-                    default_score: {
-                        value: '1',
-                        configurable: true,
-                        enumerable: true
-                    },
-                    key_type: {
-                        value: 'HASH',
-                        configurable: true,
-                        enumerable: true
-                    },
-                    prefixes: {
-                        value: [''],
-                        configurable: true,
-                        enumerable: true
-                    }
-                }),
-                attributes: [Object.create(null, {
-                    identifier: {
-                        value: 'field',
-                        configurable: true,
-                        enumerable: true
-                    },
-                    attribute: {
-                        value: 'field',
-                        configurable: true,
-                        enumerable: true
-                    },
-                    type: {
-                        value: 'TEXT',
-                        configurable: true,
-                        enumerable: true
-                    },
-                    WEIGHT: {
-                        value: '1',
-                        configurable: true,
-                        enumerable: true
-                    }
-                })],
-                numDocs: '0',
-                maxDocId: '0',
-                numTerms: '0',
-                numRecords: '0',
-                invertedSzMb: '0',
-                vectorIndexSzMb: '0',
-                totalInvertedIndexBlocks: '0',
-                offsetVectorsSzMb: '0',
-                docTableSizeMb: '0',
-                sortableValuesSizeMb: '0',
-                keyTableSizeMb: '0',
-                recordsPerDocAvg: '-nan',
-                bytesPerRecordAvg: '-nan',
-                offsetsPerTermAvg: '-nan',
-                offsetBitsPerRecordAvg: '-nan',
-                hashIndexingFailures: '0',
-                indexing: '0',
-                percentIndexed: '1',
-                gcStats: {
-                    bytesCollected: '0',
-                    totalMsRun: '0',
-                    totalCycles: '0',
-                    averageCycleTimeMs: '-nan',
-                    lastRunTimeMs: '0',
-                    gcNumericTreesMissed: '0',
-                    gcBlocksDenied: '0'
-                },
-                cursorStats: {
-                    globalIdle: 0,
-                    globalTotal: 0,
-                    indexCapacity: 128,
-                    idnexTotal: 0
-                },
-                stopWords: undefined
-            }
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts
index 269d12d51cf..03cf21edfd8 100644
--- a/packages/search/lib/commands/INFO.ts
+++ b/packages/search/lib/commands/INFO.ts
@@ -1,167 +1,169 @@
-import { RedisCommandArgument } from '@redis/client/dist/lib/commands';
-import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument } from "@redis/client";
+import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types";
+import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/dist/lib/commands/generic-transformers";
+import { TuplesReply } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(index: string): Array<string> {
-    return ['FT.INFO', index];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns information and statistics about an index.
+   * @param parser - The command parser
+   * @param index - Name of the index to get information about
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument) {
+    parser.push('FT.INFO', index);
+  },
+  transformReply: {
+    2: transformV2Reply,
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
+
+export interface InfoReply {
+  index_name: SimpleStringReply;
+  index_options: ArrayReply<SimpleStringReply>;
+  index_definition: MapReply<SimpleStringReply, SimpleStringReply>;
+  attributes: Array<MapReply<SimpleStringReply, SimpleStringReply>>;
+  num_docs: NumberReply
+  max_doc_id: NumberReply;
+  num_terms: NumberReply;
+  num_records: NumberReply;
+  inverted_sz_mb: DoubleReply;
+  vector_index_sz_mb: DoubleReply;
+  total_inverted_index_blocks: NumberReply;
+  offset_vectors_sz_mb: DoubleReply;
+  doc_table_size_mb: DoubleReply;
+  sortable_values_size_mb: DoubleReply;
+  key_table_size_mb: DoubleReply;
+  tag_overhead_sz_mb: DoubleReply;
+  text_overhead_sz_mb: DoubleReply;
+  total_index_memory_sz_mb: DoubleReply;
+  geoshapes_sz_mb: DoubleReply;
+  records_per_doc_avg: DoubleReply;
+  bytes_per_record_avg: DoubleReply;
+  offsets_per_term_avg: DoubleReply;
+  offset_bits_per_record_avg: DoubleReply;
+  hash_indexing_failures: NumberReply;
+  total_indexing_time: DoubleReply;
+  indexing: NumberReply;
+  percent_indexed: DoubleReply;
+  number_of_uses: NumberReply;
+  cleaning: NumberReply;
+  gc_stats: {
+    bytes_collected: DoubleReply;
+    total_ms_run: DoubleReply;
+    total_cycles: DoubleReply;
+    average_cycle_time_ms: DoubleReply;
+    last_run_time_ms: DoubleReply;
+    gc_numeric_trees_missed: DoubleReply;
+    gc_blocks_denied: DoubleReply;
+  };
+  cursor_stats: {
+    global_idle: NumberReply;
+    global_total: NumberReply;
+    index_capacity: NumberReply;
+    index_total: NumberReply;
+  };
+  stopwords_list?: ArrayReply<BlobStringReply> | TuplesReply<[NullReply]>;
 }
 
-type InfoRawReply = [
-    'index_name',
-    RedisCommandArgument,
-    'index_options',
-    Array<RedisCommandArgument>,
-    'index_definition',
-    Array<RedisCommandArgument>,
-    'attributes',
-    Array<Array<RedisCommandArgument>>,
-    'num_docs',
-    RedisCommandArgument,
-    'max_doc_id',
-    RedisCommandArgument,
-    'num_terms',
-    RedisCommandArgument,
-    'num_records',
-    RedisCommandArgument,
-    'inverted_sz_mb',
-    RedisCommandArgument,
-    'vector_index_sz_mb',
-    RedisCommandArgument,
-    'total_inverted_index_blocks',
-    RedisCommandArgument,
-    'offset_vectors_sz_mb',
-    RedisCommandArgument,
-    'doc_table_size_mb',
-    RedisCommandArgument,
-    'sortable_values_size_mb',
-    RedisCommandArgument,
-    'key_table_size_mb',
-    RedisCommandArgument,
-    'records_per_doc_avg',
-    RedisCommandArgument,
-    'bytes_per_record_avg',
-    RedisCommandArgument,
-    'offsets_per_term_avg',
-    RedisCommandArgument,
-    'offset_bits_per_record_avg',
-    RedisCommandArgument,
-    'hash_indexing_failures',
-    RedisCommandArgument,
-    'indexing',
-    RedisCommandArgument,
-    'percent_indexed',
-    RedisCommandArgument,
-    'gc_stats',
-    [
-        'bytes_collected',
-        RedisCommandArgument,
-        'total_ms_run',
-        RedisCommandArgument,
-        'total_cycles',
-        RedisCommandArgument,
-        'average_cycle_time_ms',
-        RedisCommandArgument,
-        'last_run_time_ms',
-        RedisCommandArgument,
-        'gc_numeric_trees_missed',
-        RedisCommandArgument,
-        'gc_blocks_denied',
-        RedisCommandArgument
-    ],
-    'cursor_stats',
-    [
-        'global_idle',
-        number,
-        'global_total',
-        number,
-        'index_capacity',
-        number,
-        'index_total',
-        number
-    ],
-    'stopwords_list'?,
-    Array<RedisCommandArgument>?
-];
+function transformV2Reply(reply: Array<any>, preserve?: any, typeMapping?: TypeMapping): InfoReply {
+  const myTransformFunc = createTransformTuplesReplyFunc<SimpleStringReply>(preserve, typeMapping);
 
-interface InfoReply {
-    indexName: RedisCommandArgument;
-    indexOptions: Array<RedisCommandArgument>;
-    indexDefinition: Record<string, RedisCommandArgument>;
-    attributes: Array<Record<string, RedisCommandArgument>>;
-    numDocs: RedisCommandArgument;
-    maxDocId: RedisCommandArgument;
-    numTerms: RedisCommandArgument;
-    numRecords: RedisCommandArgument;
-    invertedSzMb: RedisCommandArgument;
-    vectorIndexSzMb: RedisCommandArgument;
-    totalInvertedIndexBlocks: RedisCommandArgument;
-    offsetVectorsSzMb: RedisCommandArgument;
-    docTableSizeMb: RedisCommandArgument;
-    sortableValuesSizeMb: RedisCommandArgument;
-    keyTableSizeMb: RedisCommandArgument;
-    recordsPerDocAvg: RedisCommandArgument;
-    bytesPerRecordAvg: RedisCommandArgument;
-    offsetsPerTermAvg: RedisCommandArgument;
-    offsetBitsPerRecordAvg: RedisCommandArgument;
-    hashIndexingFailures: RedisCommandArgument;
-    indexing: RedisCommandArgument;
-    percentIndexed: RedisCommandArgument;
-    gcStats: {
-        bytesCollected: RedisCommandArgument;
-        totalMsRun: RedisCommandArgument;
-        totalCycles: RedisCommandArgument;
-        averageCycleTimeMs: RedisCommandArgument;
-        lastRunTimeMs: RedisCommandArgument;
-        gcNumericTreesMissed: RedisCommandArgument;
-        gcBlocksDenied: RedisCommandArgument;
-    };
-    cursorStats: {
-        globalIdle: number;
-        globalTotal: number;
-        indexCapacity: number;
-        idnexTotal: number;
-    };
-    stopWords: Array<RedisCommandArgument> | undefined;
-}
+  const ret = {} as unknown as InfoReply;
+
+  for (let i=0; i < reply.length; i += 2) {
+    const key = reply[i].toString() as keyof InfoReply;
+
+    switch (key) {
+      case 'index_name':
+      case 'index_options':
+      case 'num_docs':
+      case 'max_doc_id':
+      case 'num_terms':
+      case 'num_records':
+      case 'total_inverted_index_blocks':
+      case 'hash_indexing_failures':
+      case 'indexing':
+      case 'number_of_uses':
+      case 'cleaning':  
+      case 'stopwords_list':
+        ret[key] = reply[i+1];
+        break;
+      case 'inverted_sz_mb':
+      case 'vector_index_sz_mb':
+      case 'offset_vectors_sz_mb':
+      case 'doc_table_size_mb':
+      case 'sortable_values_size_mb':
+      case 'key_table_size_mb':
+      case 'text_overhead_sz_mb':
+      case 'tag_overhead_sz_mb':
+      case 'total_index_memory_sz_mb':
+      case 'geoshapes_sz_mb':
+      case 'records_per_doc_avg':
+      case 'bytes_per_record_avg':
+      case 'offsets_per_term_avg':
+      case 'offset_bits_per_record_avg':
+      case 'total_indexing_time':
+      case 'percent_indexed':        
+        ret[key] = transformDoubleReply[2](reply[i+1], undefined, typeMapping) as DoubleReply;
+        break;
+      case 'index_definition':
+        ret[key] = myTransformFunc(reply[i+1]);
+        break;
+      case 'attributes':
+        ret[key] = (reply[i+1] as Array<ArrayReply<SimpleStringReply>>).map(attribute => myTransformFunc(attribute));
+        break;
+      case 'gc_stats': {
+        const innerRet = {} as unknown as InfoReply['gc_stats'];
+
+        const array = reply[i+1];
+
+        for (let i=0; i < array.length; i += 2) {
+          const innerKey = array[i].toString() as keyof InfoReply['gc_stats'];
+
+          switch (innerKey) {
+            case 'bytes_collected':
+            case 'total_ms_run':
+            case 'total_cycles':
+            case 'average_cycle_time_ms':
+            case 'last_run_time_ms':
+            case 'gc_numeric_trees_missed':
+            case 'gc_blocks_denied':
+              innerRet[innerKey] = transformDoubleReply[2](array[i+1], undefined, typeMapping) as DoubleReply;
+              break;
+          }
+        }
+        
+        ret[key] = innerRet;
+        break;
+      }
+      case 'cursor_stats': {
+        const innerRet = {} as unknown as InfoReply['cursor_stats'];
+
+        const array = reply[i+1];
+
+        for (let i=0; i < array.length; i += 2) {
+          const innerKey = array[i].toString() as keyof InfoReply['cursor_stats'];
+
+          switch (innerKey) {
+            case 'global_idle':
+            case 'global_total':
+            case 'index_capacity':
+            case 'index_total':
+              innerRet[innerKey] = array[i+1];
+              break;
+          }
+        }
+
+        ret[key] = innerRet;
+        break;
+      }
+    }  
+  }
 
-export function transformReply(rawReply: InfoRawReply): InfoReply {
-    return {
-        indexName: rawReply[1],
-        indexOptions: rawReply[3],
-        indexDefinition: transformTuplesReply(rawReply[5]),
-        attributes: rawReply[7].map(attribute => transformTuplesReply(attribute)),
-        numDocs: rawReply[9],
-        maxDocId: rawReply[11],
-        numTerms: rawReply[13],
-        numRecords: rawReply[15],
-        invertedSzMb: rawReply[17],
-        vectorIndexSzMb: rawReply[19],
-        totalInvertedIndexBlocks: rawReply[21],
-        offsetVectorsSzMb: rawReply[23],
-        docTableSizeMb: rawReply[25],
-        sortableValuesSizeMb: rawReply[27],
-        keyTableSizeMb: rawReply[29],
-        recordsPerDocAvg: rawReply[31],
-        bytesPerRecordAvg: rawReply[33],
-        offsetsPerTermAvg: rawReply[35],
-        offsetBitsPerRecordAvg: rawReply[37],
-        hashIndexingFailures: rawReply[39],
-        indexing: rawReply[41],
-        percentIndexed: rawReply[43],
-        gcStats: {
-            bytesCollected: rawReply[45][1],
-            totalMsRun: rawReply[45][3],
-            totalCycles: rawReply[45][5],
-            averageCycleTimeMs: rawReply[45][7],
-            lastRunTimeMs: rawReply[45][9],
-            gcNumericTreesMissed: rawReply[45][11],
-            gcBlocksDenied: rawReply[45][13]
-        },
-        cursorStats: {
-            globalIdle: rawReply[47][1],
-            globalTotal: rawReply[47][3],
-            indexCapacity: rawReply[47][5],
-            idnexTotal: rawReply[47][7]
-        },
-        stopWords: rawReply[49]
-    };
+  return ret;
 }
diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts
index c3d6f990ff7..82783fbaba9 100644
--- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts
+++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts
@@ -1,46 +1,118 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { SchemaFieldTypes } from '.';
-import { transformArguments } from './PROFILE_AGGREGATE';
-import { AggregateSteps } from './AGGREGATE';
+import { FT_AGGREGATE_STEPS } from './AGGREGATE';
+import PROFILE_AGGREGATE from './PROFILE_AGGREGATE';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
 describe('PROFILE AGGREGATE', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query'),
-                ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query']
-            );
-        });
-
-        it('with options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { 
-                    LIMITED: true,
-                    VERBATIM: true,
-                    STEPS: [{
-                        type: AggregateSteps.SORTBY,
-                        BY: '@by'
-                    }]
-                }),
-                ['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query',
-                 'VERBATIM', 'SORTBY', '1', '@by']
-            );
-        });
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(PROFILE_AGGREGATE, 'index', 'query'),
+        ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT]
+      );
     });
 
-    testUtils.testWithClient('client.ft.search', async client => {
-        await Promise.all([
-            client.ft.create('index', {
-                field: SchemaFieldTypes.NUMERIC
-            }),
-            client.hSet('1', 'field', '1'),
-            client.hSet('2', 'field', '2')
-        ]);
-        
-        const res = await client.ft.profileAggregate('index', '*');
-        assert.ok(typeof res.profile.iteratorsProfile.counter === 'number');
-        assert.ok(typeof res.profile.parsingTime === 'string');
-        assert.ok(res.results.total == 1);
-    }, GLOBAL.SERVERS.OPEN);
+    it('with options', () => {
+      assert.deepEqual(
+        parseArgs(PROFILE_AGGREGATE, 'index', 'query', {
+          LIMITED: true,
+          VERBATIM: true,
+          STEPS: [{
+            type: FT_AGGREGATE_STEPS.SORTBY,
+            BY: '@by'
+          }]
+        }),
+        ['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query',
+          'VERBATIM', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.search', async client => {
+    await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.NUMERIC
+      }),
+      client.hSet('1', 'field', '1'),
+      client.hSet('2', 'field', '2')
+    ]);
+
+
+    const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
+    const res = await client.ft.profileAggregate('index', '*');
+
+    const normalizedRes = normalizeObject(res);
+    // TODO uncomment after https://redis.io/docs/latest/commands/ft.aggregate/#return
+    // starts returning valid values
+    // assert.equal(normalizedRes.results.total, 2);
+
+    assert.ok(normalizedRes.profile[0] === 'Shards');
+    assert.ok(Array.isArray(normalizedRes.profile[1]));
+    assert.ok(normalizedRes.profile[2] === 'Coordinator');
+    assert.ok(Array.isArray(normalizedRes.profile[3]));
+
+    const shardProfile = normalizedRes.profile[1][0];
+    assert.ok(shardProfile.includes('Total profile time'));
+    assert.ok(shardProfile.includes('Parsing time'));
+    assert.ok(shardProfile.includes('Pipeline creation time'));
+    assert.ok(shardProfile.includes('Warning'));
+    assert.ok(shardProfile.includes('Iterators profile'));
+
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => {
+    await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.NUMERIC
+      }),
+      client.hSet('1', 'field', '1'),
+      client.hSet('2', 'field', '2')
+    ]);
+
+    const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
+    const res = await client.ft.profileAggregate('index', '*');
+    const normalizedRes = normalizeObject(res);
+
+    // TODO uncomment after https://redis.io/docs/latest/commands/ft.aggregate/#return
+    // starts returning valid values
+    // assert.equal(normalizedRes.results.total, 2);
+
+    assert.ok(Array.isArray(normalizedRes.profile));
+    assert.equal(normalizedRes.profile[0][0], 'Total profile time');
+    assert.equal(normalizedRes.profile[1][0], 'Parsing time');
+    assert.equal(normalizedRes.profile[2][0], 'Pipeline creation time');
+    assert.equal(normalizedRes.profile[3][0], 'Warning');
+    assert.equal(normalizedRes.profile[4][0], 'Iterators profile');
+    assert.equal(normalizedRes.profile[5][0], 'Result processors profile');
+
+    const iteratorsProfile = normalizedRes.profile[4][1];
+    assert.equal(iteratorsProfile[0], 'Type');
+    assert.equal(iteratorsProfile[1], 'WILDCARD');
+    assert.equal(iteratorsProfile[2], 'Time');
+    assert.equal(iteratorsProfile[4], 'Counter');
+  }, GLOBAL.SERVERS.OPEN);
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], '[RESP3] client.ft.search', async client => {
+    await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.NUMERIC
+      }),
+      client.hSet('1', 'field', '1'),
+      client.hSet('2', 'field', '2')
+    ]);
+
+
+    const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
+    const res = await client.ft.profileAggregate('index', '*');
+
+    // TODO uncomment after https://redis.io/docs/latest/commands/ft.aggregate/#return
+    // starts returning valid values
+    // assert.equal(res.Results.total_results, 2);
+
+    const normalizedRes = normalizeObject(res);
+    assert.ok(normalizedRes.Profile.Shards);
+  }, GLOBAL.SERVERS.OPEN_3);
 });
diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts
index b28e06ade91..99aca95a698 100644
--- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts
+++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts
@@ -1,29 +1,44 @@
-import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE';
-import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, ReplyUnion, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
+import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from './AGGREGATE';
+import { ProfileOptions, ProfileRawReplyResp2, ProfileReplyResp2, } from './PROFILE_SEARCH';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Profiles the execution of an aggregation query for performance analysis.
+   * @param parser - The command parser
+   * @param index - Name of the index to profile query against
+   * @param query - The aggregation query to profile
+   * @param options - Optional parameters:
+   *   - LIMITED: Collect limited timing information only
+   *   - All options supported by FT.AGGREGATE command
+   */
+  parseCommand(
+    parser: CommandParser,
     index: string,
     query: string,
-    options?: ProfileOptions & AggregateOptions
-): Array<string> {
-    const args = ['FT.PROFILE', index, 'AGGREGATE'];
+    options?: ProfileOptions & FtAggregateOptions
+  ) {
+    parser.push('FT.PROFILE', index, 'AGGREGATE');
 
     if (options?.LIMITED) {
-        args.push('LIMITED');
+      parser.push('LIMITED');
     }
 
-    args.push('QUERY', query);
-    pushAggregatehOptions(args, options)
-    return args;
-}
-
-type ProfileAggeregateRawReply = ProfileRawReply<AggregateRawReply>;
+    parser.push('QUERY', query);
 
-export function transformReply(reply: ProfileAggeregateRawReply): ProfileReply {
-    return {
-        results: transformAggregateReply(reply[0]),
-        profile: transformProfile(reply[1])
-    };
-}
+    parseAggregateOptions(parser, options)
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<ProfileRawReplyResp2<AggregateRawReply>>): ProfileReplyResp2 => {
+      return {
+        results: AGGREGATE.transformReply[2](reply[0]),
+        profile: reply[1]
+      }
+    },
+    3: (reply: ReplyUnion): ReplyUnion => reply
+  },
+  unstableResp3: true
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts
index 6d7c5adda1e..419b879d00a 100644
--- a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts
+++ b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts
@@ -1,41 +1,95 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { SchemaFieldTypes } from '.';
-import { transformArguments } from './PROFILE_SEARCH';
+import PROFILE_SEARCH from './PROFILE_SEARCH';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
 describe('PROFILE SEARCH', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query'),
-                ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query']
-            );
-        });
-
-        it('with options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { 
-                    LIMITED: true,
-                    VERBATIM: true,
-                    INKEYS: 'key'
-                }),
-                ['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query',
-                 'VERBATIM', 'INKEYS', '1', 'key']
-            );
-        });
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(PROFILE_SEARCH, 'index', 'query'),
+        ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT]
+      );
     });
 
-    testUtils.testWithClient('client.ft.search', async client => {
-        await Promise.all([
-            client.ft.create('index', {
-                field: SchemaFieldTypes.NUMERIC
-            }),
-            client.hSet('1', 'field', '1')
-        ]);
-        
-        const res = await client.ft.profileSearch('index', '*');
-        assert.ok(typeof res.profile.iteratorsProfile.counter === 'number');
-        assert.ok(typeof res.profile.parsingTime === 'string');
-        assert.ok(res.results.total == 1);
-    }, GLOBAL.SERVERS.OPEN);
+    it('with options', () => {
+      assert.deepEqual(
+        parseArgs(PROFILE_SEARCH, 'index', 'query', {
+          LIMITED: true,
+          VERBATIM: true,
+          INKEYS: 'key'
+        }),
+        ['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query',
+          'VERBATIM', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
+  });
+
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.search', async client => {
+    await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.NUMERIC
+      }),
+      client.hSet('1', 'field', '1')
+    ]);
+
+    const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
+
+    const res = await client.ft.profileSearch('index', '*');
+
+    const normalizedRes = normalizeObject(res);
+    assert.equal(normalizedRes.results.total, 1);
+
+    assert.ok(normalizedRes.profile[0] === 'Shards');
+    assert.ok(Array.isArray(normalizedRes.profile[1]));
+    assert.ok(normalizedRes.profile[2] === 'Coordinator');
+    assert.ok(Array.isArray(normalizedRes.profile[3]));
+
+    const shardProfile = normalizedRes.profile[1][0];
+    assert.ok(shardProfile.includes('Total profile time'));
+    assert.ok(shardProfile.includes('Parsing time'));
+    assert.ok(shardProfile.includes('Pipeline creation time'));
+    assert.ok(shardProfile.includes('Warning'));
+    assert.ok(shardProfile.includes('Iterators profile'));
+    ;
+
+  }, GLOBAL.SERVERS.OPEN);
+
+
+
+
+
+  testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => {
+    await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.NUMERIC
+      }),
+      client.hSet('1', 'field', '1')
+    ]);
+
+    const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
+
+    const res = await client.ft.profileSearch('index', '*');
+
+    const normalizedRes = normalizeObject(res);
+    assert.equal(normalizedRes.results.total, 1);
+
+    assert.ok(Array.isArray(normalizedRes.profile));
+    assert.equal(normalizedRes.profile[0][0], 'Total profile time');
+    assert.equal(normalizedRes.profile[1][0], 'Parsing time');
+    assert.equal(normalizedRes.profile[2][0], 'Pipeline creation time');
+    assert.equal(normalizedRes.profile[3][0], 'Warning');
+    assert.equal(normalizedRes.profile[4][0], 'Iterators profile');
+    assert.equal(normalizedRes.profile[5][0], 'Result processors profile');
+
+    const iteratorsProfile = normalizedRes.profile[4][1];
+    assert.equal(iteratorsProfile[0], 'Type');
+    assert.equal(iteratorsProfile[1], 'WILDCARD');
+    assert.equal(iteratorsProfile[2], 'Time');
+    assert.equal(iteratorsProfile[4], 'Counter');
+
+  }, GLOBAL.SERVERS.OPEN);
+
 });
diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts
index 94fba8a6a54..cdbb12fcdd8 100644
--- a/packages/search/lib/commands/PROFILE_SEARCH.ts
+++ b/packages/search/lib/commands/PROFILE_SEARCH.ts
@@ -1,29 +1,60 @@
-import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH';
-import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, Command, RedisArgument, ReplyUnion, TuplesReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
+import { AggregateReply } from './AGGREGATE';
+import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from './SEARCH';
 
-export const IS_READ_ONLY = true;
+export type ProfileRawReplyResp2<T> = TuplesReply<[
+  T,
+  ArrayReply<ReplyUnion>
+]>;
 
-export function transformArguments(
-    index: string,
-    query: string,
-    options?: ProfileOptions & SearchOptions
-): RedisCommandArguments {
-    let args: RedisCommandArguments = ['FT.PROFILE', index, 'SEARCH'];
+type ProfileSearchResponseResp2 = ProfileRawReplyResp2<SearchRawReply>;
 
-    if (options?.LIMITED) {
-        args.push('LIMITED');
-    }
+export interface ProfileReplyResp2 {
+  results: SearchReply | AggregateReply;
+  profile: ReplyUnion;
+}
 
-    args.push('QUERY', query);
-    return pushSearchOptions(args, options);
+export interface ProfileOptions {
+  LIMITED?: true;
 }
 
-type ProfileSearchRawReply = ProfileRawReply<SearchRawReply>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Profiles the execution of a search query for performance analysis.
+   * @param parser - The command parser
+   * @param index - Name of the index to profile query against
+   * @param query - The search query to profile
+   * @param options - Optional parameters:
+   *   - LIMITED: Collect limited timing information only
+   *   - All options supported by FT.SEARCH command
+   */
+  parseCommand(
+    parser: CommandParser,
+    index: RedisArgument,
+    query: RedisArgument,
+    options?: ProfileOptions & FtSearchOptions
+  ) {
+    parser.push('FT.PROFILE', index, 'SEARCH');
 
-export function transformReply(reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply {
-    return {
-        results: transformSearchReply(reply[0], withoutDocuments),
-        profile: transformProfile(reply[1])
-    };
-}
+    if (options?.LIMITED) {
+      parser.push('LIMITED');
+    }
+
+    parser.push('QUERY', query);
+
+    parseSearchOptions(parser, options);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<ProfileSearchResponseResp2>): ProfileReplyResp2 => {
+      return {
+        results: SEARCH.transformReply[2](reply[0]),
+        profile: reply[1]
+      };
+    },
+    3: (reply: ReplyUnion): ReplyUnion => reply
+  },
+  unstableResp3: true
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts
index 931458b3a25..ab480808ffa 100644
--- a/packages/search/lib/commands/SEARCH.spec.ts
+++ b/packages/search/lib/commands/SEARCH.spec.ts
@@ -1,300 +1,330 @@
-import { strict as assert } from 'assert';
-import { RedisSearchLanguages, SchemaFieldTypes } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SEARCH';
+import SEARCH from './SEARCH';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
-describe('SEARCH', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query'),
-                ['FT.SEARCH', 'index', 'query']
-            );
-        });
-
-        it('with VERBATIM', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { VERBATIM: true }),
-                ['FT.SEARCH', 'index', 'query', 'VERBATIM']
-            );
-        });
-
-        it('with NOSTOPWORDS', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { NOSTOPWORDS: true }),
-                ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS']
-            );
-        });
 
-        it('with INKEYS', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { INKEYS: 'key' }),
-                ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key']
-            );
-        });
-
-        it('with INFIELDS', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { INFIELDS: 'field' }),
-                ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field']
-            );
-        });
+describe('FT.SEARCH', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query'),
+        ['FT.SEARCH', 'index', 'query', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with RETURN', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { RETURN: 'return' }),
-                ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return']
-            );
-        });
+    it('with VERBATIM', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          VERBATIM: true
+        }),
+        ['FT.SEARCH', 'index', 'query', 'VERBATIM', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        describe('with SUMMARIZE', () => {
-            it('true', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', { SUMMARIZE: true }),
-                    ['FT.SEARCH', 'index', 'query', 'SUMMARIZE']
-                );
-            });
+    it('with NOSTOPWORDS', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          NOSTOPWORDS: true
+        }),
+        ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-            describe('with FIELDS', () => {
-                it('string', () => {
-                    assert.deepEqual(
-                        transformArguments('index', 'query', {
-                            SUMMARIZE: {
-                                FIELDS: ['@field']
-                            }
-                        }),
-                        ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field']
-                    );
-                });
+    it('with INKEYS', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          INKEYS: 'key'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-                it('Array', () => {
-                    assert.deepEqual(
-                        transformArguments('index', 'query', {
-                            SUMMARIZE: {
-                                FIELDS: ['@1', '@2']
-                            }
-                        }),
-                        ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2']
-                    );
-                });
-            });
+    it('with INFIELDS', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          INFIELDS: 'field'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-            it('with FRAGS', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', {
-                        SUMMARIZE: {
-                            FRAGS: 1
-                        }
-                    }),
-                    ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1']
-                );
-            });
+    it('with RETURN', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          RETURN: 'return'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-            it('with LEN', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', {
-                        SUMMARIZE: {
-                            LEN: 1
-                        }
-                    }),
-                    ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1']
-                );
-            });
+    describe('with SUMMARIZE', () => {
+      it('true', () => {
+        assert.deepEqual(
+          parseArgs(SEARCH, 'index', 'query', {
+            SUMMARIZE: true
+          }),
+          ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
 
-            it('with SEPARATOR', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', {
-                        SUMMARIZE: {
-                            SEPARATOR: 'separator'
-                        }
-                    }),
-                    ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator']
-                );
-            });
+      describe('with FIELDS', () => {
+        it('string', () => {
+          assert.deepEqual(
+            parseArgs(SEARCH, 'index', 'query', {
+              SUMMARIZE: {
+                FIELDS: '@field'
+              }
+            }),
+            ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field', 'DIALECT', DEFAULT_DIALECT]
+          );
         });
 
-        describe('with HIGHLIGHT', () => {
-            it('true', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', { HIGHLIGHT: true }),
-                    ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT']
-                );
-            });
+        it('Array', () => {
+          assert.deepEqual(
+            parseArgs(SEARCH, 'index', 'query', {
+              SUMMARIZE: {
+                FIELDS: ['@1', '@2']
+              }
+            }),
+            ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT]
+          );
+        });
+      });
 
-            describe('with FIELDS', () => {
-                it('string', () => {
-                    assert.deepEqual(
-                        transformArguments('index', 'query', {
-                            HIGHLIGHT: {
-                                FIELDS: ['@field']
-                            }
-                        }),
-                        ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field']
-                    );
-                });
+      it('with FRAGS', () => {
+        assert.deepEqual(
+          parseArgs(SEARCH, 'index', 'query', {
+            SUMMARIZE: {
+              FRAGS: 1
+            }
+          }),
+          ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
 
-                it('Array', () => {
-                    assert.deepEqual(
-                        transformArguments('index', 'query', {
-                            HIGHLIGHT: {
-                                FIELDS: ['@1', '@2']
-                            }
-                        }),
-                        ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2']
-                    );
-                });
-            });
+      it('with LEN', () => {
+        assert.deepEqual(
+          parseArgs(SEARCH, 'index', 'query', {
+            SUMMARIZE: {
+              LEN: 1
+            }
+          }),
+          ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
 
-            it('with TAGS', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', {
-                        HIGHLIGHT: {
-                            TAGS: {
-                                open: 'open',
-                                close: 'close'
-                            }
-                        }
-                    }),
-                    ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close']
-                );
-            });
-        });
+      it('with SEPARATOR', () => {
+        assert.deepEqual(
+          parseArgs(SEARCH, 'index', 'query', {
+            SUMMARIZE: {
+              SEPARATOR: 'separator'
+            }
+          }),
+          ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
+    });
 
-        it('with SLOP', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { SLOP: 1 }),
-                ['FT.SEARCH', 'index', 'query', 'SLOP', '1']
-            );
-        });
+    describe('with HIGHLIGHT', () => {
+      it('true', () => {
+        assert.deepEqual(
+          parseArgs(SEARCH, 'index', 'query', {
+            HIGHLIGHT: true
+          }),
+          ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
 
-        it('with INORDER', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { INORDER: true }),
-                ['FT.SEARCH', 'index', 'query', 'INORDER']
-            );
+      describe('with FIELDS', () => {
+        it('string', () => {
+          assert.deepEqual(
+            parseArgs(SEARCH, 'index', 'query', {
+              HIGHLIGHT: {
+                FIELDS: ['@field']
+              }
+            }),
+            ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field', 'DIALECT', DEFAULT_DIALECT]
+          );
         });
 
-        it('with LANGUAGE', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { LANGUAGE: RedisSearchLanguages.ARABIC }),
-                ['FT.SEARCH', 'index', 'query', 'LANGUAGE', RedisSearchLanguages.ARABIC]
-            );
+        it('Array', () => {
+          assert.deepEqual(
+            parseArgs(SEARCH, 'index', 'query', {
+              HIGHLIGHT: {
+                FIELDS: ['@1', '@2']
+              }
+            }),
+            ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT]
+          );
         });
+      });
 
-        it('with EXPANDER', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { EXPANDER: 'expender' }),
-                ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender']
-            );
-        });
+      it('with TAGS', () => {
+        assert.deepEqual(
+          parseArgs(SEARCH, 'index', 'query', {
+            HIGHLIGHT: {
+              TAGS: {
+                open: 'open',
+                close: 'close'
+              }
+            }
+          }),
+          ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
+    });
 
-        it('with SCORER', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { SCORER: 'scorer' }),
-                ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer']
-            );
-        });
+    it('with SLOP', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          SLOP: 1
+        }),
+        ['FT.SEARCH', 'index', 'query', 'SLOP', '1', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with SORTBY', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { SORTBY: '@by' }),
-                ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by']
-            );
-        });
+    it('with TIMEOUT', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          TIMEOUT: 1
+        }),
+        ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with LIMIT', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', {
-                    LIMIT: {
-                        from: 0,
-                        size: 1
-                    }
-                }),
-                ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1']
-            );
-        });
+    it('with INORDER', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          INORDER: true
+        }),
+        ['FT.SEARCH', 'index', 'query', 'INORDER', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with PARAMS', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', {
-                    PARAMS: {
-                        param: 'value'
-                    }
-                }),
-                ['FT.SEARCH', 'index', 'query', 'PARAMS', '2', 'param', 'value']
-            );
-        });
+    it('with LANGUAGE', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          LANGUAGE: 'Arabic'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with DIALECT', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', {
-                    DIALECT: 1
-                }),
-                ['FT.SEARCH', 'index', 'query', 'DIALECT', '1']
-            );
-        });
+    it('with EXPANDER', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          EXPANDER: 'expender'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with TIMEOUT', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', {
-                    TIMEOUT: 5
-                }),
-                ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '5']
-            );
-        });
+    it('with SCORER', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          SCORER: 'scorer'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer', 'DIALECT', DEFAULT_DIALECT]
+      );
     });
 
-    describe('client.ft.search', () => {
-        testUtils.testWithClient('without optional options', async client => {
-            await Promise.all([
-                client.ft.create('index', {
-                    field: SchemaFieldTypes.NUMERIC
-                }),
-                client.hSet('1', 'field', '1')
-            ]);
+    it('with SORTBY', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          SORTBY: '@by'
+        }),
+        ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-            assert.deepEqual(
-                await client.ft.search('index', '*'),
-                {
-                    total: 1,
-                    documents: [{
-                        id: '1',
-                        value: Object.create(null, {
-                            field: {
-                                value: '1',
-                                configurable: true,
-                                enumerable: true
-                            }
-                        })
-                    }]
-                }
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with LIMIT', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          LIMIT: {
+            from: 0,
+            size: 1
+          }
+        }),
+        ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        testUtils.testWithClient('RETURN []', async client => {
-            await Promise.all([
-                client.ft.create('index', {
-                    field: SchemaFieldTypes.NUMERIC
-                }),
-                client.hSet('1', 'field', '1'),
-                client.hSet('2', 'field', '2')
-            ]);
+    it('with PARAMS', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          PARAMS: {
+            string: 'string',
+            buffer: Buffer.from('buffer'),
+            number: 1
+          }
+        }),
+        ['FT.SEARCH', 'index', 'query', 'PARAMS', '6', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-            assert.deepEqual(
-                await client.ft.search('index', '*', {
-                    RETURN: []
-                }),
-                {
-                    total: 2,
-                    documents: [{
-                        id: '1',
-                        value: Object.create(null)
-                    }, {
-                        id: '2',
-                        value: Object.create(null)
-                    }]
-                }
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with DIALECT', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH, 'index', 'query', {
+          DIALECT: 1
+        }),
+        ['FT.SEARCH', 'index', 'query', 'DIALECT', '1']
+      );
     });
+  });
+
+  describe('client.ft.search', () => {
+    testUtils.testWithClient('without optional options', async client => {
+      await Promise.all([
+        client.ft.create('index', {
+          field: 'TEXT'
+        }),
+        client.hSet('1', 'field', '1')
+      ]);
+
+      assert.deepEqual(
+        await client.ft.search('index', '*'),
+        {
+          total: 1,
+          documents: [{
+            id: '1',
+            value: Object.create(null, {
+              field: {
+                value: '1',
+                configurable: true,
+                enumerable: true
+              }
+            })
+          }]
+        }
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
+    testUtils.testWithClient('RETURN []', async client => {
+      await Promise.all([
+        client.ft.create('index', {
+          field: 'TEXT'
+        }),
+        client.hSet('1', 'field', '1'),
+        client.hSet('2', 'field', '2')
+      ]);
+
+      assert.deepEqual(
+        await client.ft.search('index', '*', {
+          RETURN: []
+        }),
+        {
+          total: 2,
+          documents: [{
+            id: '1',
+            value: Object.create(null)
+          }, {
+            id: '2',
+            value: Object.create(null)
+          }]
+        }
+      );
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts
index ff7ab7e201d..61e1d8d84d2 100644
--- a/packages/search/lib/commands/SEARCH.ts
+++ b/packages/search/lib/commands/SEARCH.ts
@@ -1,109 +1,245 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushSearchOptions, RedisSearchLanguages, Params, PropertyName, SortByProperty, SearchReply } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export interface SearchOptions {
-    VERBATIM?: true;
-    NOSTOPWORDS?: true;
-    // WITHSCORES?: true;
-    // WITHPAYLOADS?: true;
-    WITHSORTKEYS?: true;
-    // FILTER?: {
-    //     field: string;
-    //     min: number | string;
-    //     max: number | string;
-    // };
-    // GEOFILTER?: {
-    //     field: string;
-    //     lon: number;
-    //     lat: number;
-    //     radius: number;
-    //     unit: 'm' | 'km' | 'mi' | 'ft';
-    // };
-    INKEYS?: string | Array<string>;
-    INFIELDS?: string | Array<string>;
-    RETURN?: string | Array<string>;
-    SUMMARIZE?: true | {
-        FIELDS?: PropertyName | Array<PropertyName>;
-        FRAGS?: number;
-        LEN?: number;
-        SEPARATOR?: string;
-    };
-    HIGHLIGHT?: true | {
-        FIELDS?: PropertyName | Array<PropertyName>;
-        TAGS?: {
-            open: string;
-            close: string;
-        };
-    };
-    SLOP?: number;
-    INORDER?: true;
-    LANGUAGE?: RedisSearchLanguages;
-    EXPANDER?: string;
-    SCORER?: string;
-    // EXPLAINSCORE?: true; // TODO: WITHSCORES
-    // PAYLOAD?: ;
-    SORTBY?: SortByProperty;
-    // MSORTBY?: SortByProperty | Array<SortByProperty>;
-    LIMIT?: {
-        from: number | string;
-        size: number | string;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { RediSearchLanguage } from './CREATE';
+import { DEFAULT_DIALECT } from '../dialect/default';
+
+export type FtSearchParams = Record<string, RedisArgument | number>;
+
+export function parseParamsArgument(parser: CommandParser, params?: FtSearchParams) {
+  if (params) {
+    parser.push('PARAMS');
+
+    const args: Array<RedisArgument> = [];
+    for (const key in params) {
+      if (!Object.hasOwn(params, key)) continue;
+
+      const value = params[key];
+      args.push(
+        key,
+        typeof value === 'number' ? value.toString() : value
+      );
+    }
+
+    parser.pushVariadicWithLength(args);
+  }
+}
+
+export interface FtSearchOptions {
+  VERBATIM?: boolean;
+  NOSTOPWORDS?: boolean;
+  INKEYS?: RedisVariadicArgument;
+  INFIELDS?: RedisVariadicArgument;
+  RETURN?: RedisVariadicArgument;
+  SUMMARIZE?: boolean | {
+    FIELDS?: RedisArgument | Array<RedisArgument>;
+    FRAGS?: number;
+    LEN?: number;
+    SEPARATOR?: RedisArgument;
+  };
+  HIGHLIGHT?: boolean | {
+    FIELDS?: RedisArgument | Array<RedisArgument>;
+    TAGS?: {
+      open: RedisArgument;
+      close: RedisArgument;
     };
-    PARAMS?: Params;
-    DIALECT?: number;
-    TIMEOUT?: number;
+  };
+  SLOP?: number;
+  TIMEOUT?: number;
+  INORDER?: boolean;
+  LANGUAGE?: RediSearchLanguage;
+  EXPANDER?: RedisArgument;
+  SCORER?: RedisArgument;
+  SORTBY?: RedisArgument | {
+    BY: RedisArgument;
+    DIRECTION?: 'ASC' | 'DESC';
+  };
+  LIMIT?: {
+    from: number | RedisArgument;
+    size: number | RedisArgument;
+  };
+  PARAMS?: FtSearchParams;
+  DIALECT?: number;
 }
 
-export function transformArguments(
-    index: string,
-    query: string,
-    options?: SearchOptions
-): RedisCommandArguments {
-    return pushSearchOptions(
-        ['FT.SEARCH', index, query],
-        options
-    );
+export function parseSearchOptions(parser: CommandParser, options?: FtSearchOptions) {
+  if (options?.VERBATIM) {
+    parser.push('VERBATIM');
+  }
+
+  if (options?.NOSTOPWORDS) {
+    parser.push('NOSTOPWORDS');
+  }
+
+  parseOptionalVariadicArgument(parser, 'INKEYS', options?.INKEYS);
+  parseOptionalVariadicArgument(parser, 'INFIELDS', options?.INFIELDS);
+  parseOptionalVariadicArgument(parser, 'RETURN', options?.RETURN);
+
+  if (options?.SUMMARIZE) {
+    parser.push('SUMMARIZE');
+
+    if (typeof options.SUMMARIZE === 'object') {
+      parseOptionalVariadicArgument(parser, 'FIELDS', options.SUMMARIZE.FIELDS);
+
+      if (options.SUMMARIZE.FRAGS !== undefined) {
+        parser.push('FRAGS', options.SUMMARIZE.FRAGS.toString());
+      }
+
+      if (options.SUMMARIZE.LEN !== undefined) {
+        parser.push('LEN', options.SUMMARIZE.LEN.toString());
+      }
+
+      if (options.SUMMARIZE.SEPARATOR !== undefined) {
+        parser.push('SEPARATOR', options.SUMMARIZE.SEPARATOR);
+      }
+    }
+  }
+
+  if (options?.HIGHLIGHT) {
+    parser.push('HIGHLIGHT');
+
+    if (typeof options.HIGHLIGHT === 'object') {
+      parseOptionalVariadicArgument(parser, 'FIELDS', options.HIGHLIGHT.FIELDS);
+
+      if (options.HIGHLIGHT.TAGS) {
+        parser.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close);
+      }
+    }
+  }
+
+  if (options?.SLOP !== undefined) {
+    parser.push('SLOP', options.SLOP.toString());
+  }
+
+  if (options?.TIMEOUT !== undefined) {
+    parser.push('TIMEOUT', options.TIMEOUT.toString());
+  }
+
+  if (options?.INORDER) {
+    parser.push('INORDER');
+  }
+
+  if (options?.LANGUAGE) {
+    parser.push('LANGUAGE', options.LANGUAGE);
+  }
+
+  if (options?.EXPANDER) {
+    parser.push('EXPANDER', options.EXPANDER);
+  }
+
+  if (options?.SCORER) {
+    parser.push('SCORER', options.SCORER);
+  }
+
+  if (options?.SORTBY) {
+    parser.push('SORTBY');
+
+    if (typeof options.SORTBY === 'string' || options.SORTBY instanceof Buffer) {
+      parser.push(options.SORTBY);
+    } else {
+      parser.push(options.SORTBY.BY);
+
+      if (options.SORTBY.DIRECTION) {
+        parser.push(options.SORTBY.DIRECTION);
+      }
+    }
+  }
+
+  if (options?.LIMIT) {
+    parser.push('LIMIT', options.LIMIT.from.toString(), options.LIMIT.size.toString());
+  }
+
+  parseParamsArgument(parser, options?.PARAMS);
+
+  if (options?.DIALECT) {
+    parser.push('DIALECT', options.DIALECT.toString());
+  } else {
+    parser.push('DIALECT', DEFAULT_DIALECT);
+  }
 }
 
-export type SearchRawReply = Array<any>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Searches a RediSearch index with the given query.
+   * @param parser - The command parser
+   * @param index - The index name to search
+   * @param query - The text query to search. For syntax, see https://redis.io/docs/stack/search/reference/query_syntax
+   * @param options - Optional search parameters including:
+   *   - VERBATIM: do not try to use stemming for query expansion
+   *   - NOSTOPWORDS: do not filter stopwords from the query
+   *   - INKEYS/INFIELDS: restrict the search to specific keys/fields
+   *   - RETURN: limit which fields are returned
+   *   - SUMMARIZE/HIGHLIGHT: create search result highlights
+   *   - LIMIT: pagination control
+   *   - SORTBY: sort results by a specific field
+   *   - PARAMS: bind parameters to the query
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) {
+    parser.push('FT.SEARCH', index, query);
 
-export function transformReply(reply: SearchRawReply, withoutDocuments: boolean): SearchReply {
-    const documents = [];
-    let i = 1;
-    while (i < reply.length) {
+    parseSearchOptions(parser, options);
+  },
+  transformReply: {
+    2: (reply: SearchRawReply): SearchReply => {
+      const withoutDocuments = (reply[0] + 1 == reply.length)
+
+      const documents = [];
+      let i = 1;
+      while (i < reply.length) {
         documents.push({
-            id: reply[i++],
-            value: withoutDocuments ? Object.create(null) : documentValue(reply[i++])
+          id: reply[i++],
+          value: withoutDocuments ? Object.create(null) : documentValue(reply[i++])
         });
-    }
+      }
 
-    return {
+      return {
         total: reply[0],
         documents
-    };
+      };
+    },
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
+
+export type SearchRawReply = Array<any>;
+
+interface SearchDocumentValue {
+  [key: string]: string | number | null | Array<SearchDocumentValue> | SearchDocumentValue;
+}
+
+export interface SearchReply {
+  total: number;
+  documents: Array<{
+      id: string;
+      value: SearchDocumentValue;
+  }>;
 }
 
 function documentValue(tuples: any) {
-    const message = Object.create(null);
-
-    let i = 0;
-    while (i < tuples.length) {
-        const key = tuples[i++],
-            value = tuples[i++];
-        if (key === '$') { // might be a JSON reply
-            try {
-                Object.assign(message, JSON.parse(value));
-                continue;
-            } catch {
-                // set as a regular property if not a valid JSON
-            }
-        }
-
-        message[key] = value;
-    }
+  const message = Object.create(null);
 
+  if(!tuples) {
     return message;
+  }
+
+  let i = 0;
+  while (i < tuples.length) {
+      const key = tuples[i++],
+          value = tuples[i++];
+      if (key === '$') { // might be a JSON reply
+          try {
+              Object.assign(message, JSON.parse(value));
+              continue;
+          } catch {
+              // set as a regular property if not a valid JSON
+          }
+      }
+
+      message[key] = value;
+  }
+
+  return message;
 }
diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts
index da5a6feaba7..cd37409b5bb 100644
--- a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts
+++ b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts
@@ -1,45 +1,36 @@
 import { strict as assert } from 'assert';
-import { SchemaFieldTypes } from '.';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments, transformReply } from './SEARCH_NOCONTENT';
+import SEARCH_NOCONTENT from './SEARCH_NOCONTENT';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
-describe('SEARCH_NOCONTENT', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query'),
-                ['FT.SEARCH', 'index', 'query', 'NOCONTENT']
-            );
-        });
+describe('FT.SEARCH NOCONTENT', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(SEARCH_NOCONTENT, 'index', 'query'),
+        ['FT.SEARCH', 'index', 'query', 'DIALECT', DEFAULT_DIALECT, 'NOCONTENT']
+      );
     });
+  });
 
-    describe('transformReply', () => {
-        it('returns total and keys', () => {
-            assert.deepEqual(transformReply([3, '1', '2', '3']), {
-                total: 3,
-                documents: ['1', '2', '3']
-            })
-        });
-    });
-
-    describe('client.ft.searchNoContent', () => {
-        testUtils.testWithClient('returns total and keys', async client => {
-            await Promise.all([
-                client.ft.create('index', {
-                    field: SchemaFieldTypes.TEXT
-                }),
-                client.hSet('1', 'field', 'field1'),
-                client.hSet('2', 'field', 'field2'),
-                client.hSet('3', 'field', 'field3')
-            ]);
+  describe('client.ft.searchNoContent', () => {
+    testUtils.testWithClient('returns total and keys', async client => {
+      await Promise.all([
+        client.ft.create('index', {
+          field: 'TEXT'
+        }),
+        client.hSet('1', 'field', 'field1'),
+        client.hSet('2', 'field', 'field2')
+      ]);
 
-            assert.deepEqual(
-                await client.ft.searchNoContent('index', '*'),
-                {
-                    total: 3,
-                    documents: ['1','2','3']
-                }
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
+      assert.deepEqual(
+        await client.ft.searchNoContent('index', '*'),
+        {
+          total: 2,
+          documents: ['1', '2']
+        }
+      );
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts
index ab50ae2b9ff..2fcfd2b4166 100644
--- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts
+++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts
@@ -1,30 +1,34 @@
-import { RedisCommandArguments } from "@redis/client/dist/lib/commands";
-import { pushSearchOptions } from ".";
-import { SearchOptions, SearchRawReply } from "./SEARCH";
+import { Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types';
+import SEARCH, { SearchRawReply } from './SEARCH';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    index: string,
-    query: string,
-    options?: SearchOptions
-): RedisCommandArguments {
-    return pushSearchOptions(
-        ['FT.SEARCH', index, query, 'NOCONTENT'],
-        options
-    );
-}
-
-export interface SearchNoContentReply {
-    total: number;
-    documents: Array<string>;
-};
-
-export function transformReply(reply: SearchRawReply): SearchNoContentReply {
-    return {
+export default {
+  NOT_KEYED_COMMAND: SEARCH.NOT_KEYED_COMMAND,
+  IS_READ_ONLY: SEARCH.IS_READ_ONLY,
+  /**
+   * Performs a search query but returns only document ids without their contents.
+   * @param args - Same parameters as FT.SEARCH:
+   *   - parser: The command parser
+   *   - index: Name of the index to search
+   *   - query: The text query to search
+   *   - options: Optional search parameters
+   */
+  parseCommand(...args: Parameters<typeof SEARCH.parseCommand>) {
+    SEARCH.parseCommand(...args);  
+    args[0].push('NOCONTENT');
+  },
+  transformReply: {
+    2: (reply: SearchRawReply): SearchNoContentReply => {
+      return {
         total: reply[0],
         documents: reply.slice(1)
-    };
-}
+      }
+    },
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
+
+export interface SearchNoContentReply {
+  total: number;
+  documents: Array<string>;
+};
\ No newline at end of file
diff --git a/packages/search/lib/commands/SPELLCHECK.spec.ts b/packages/search/lib/commands/SPELLCHECK.spec.ts
index acabbe8a87c..482deed6a45 100644
--- a/packages/search/lib/commands/SPELLCHECK.spec.ts
+++ b/packages/search/lib/commands/SPELLCHECK.spec.ts
@@ -1,80 +1,81 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { SchemaFieldTypes } from '.';
-import { transformArguments } from './SPELLCHECK';
+import SPELLCHECK from './SPELLCHECK';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+import { DEFAULT_DIALECT } from '../dialect/default';
 
-describe('SPELLCHECK', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query'),
-                ['FT.SPELLCHECK', 'index', 'query']
-            );
-        });
-
-        it('with DISTANCE', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', { DISTANCE: 2 }),
-                ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2']
-            );
-        });
-
-        describe('with TERMS', () => {
-            it('single', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', {
-                        TERMS: {
-                            mode: 'INCLUDE',
-                            dictionary: 'dictionary'
-                        }
-                    }),
-                    ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary']
-                );
-            });
-
-            it('multiple', () => {
-                assert.deepEqual(
-                    transformArguments('index', 'query', {
-                        TERMS: [{
-                            mode: 'INCLUDE',
-                            dictionary: 'include'
-                        }, {
-                            mode: 'EXCLUDE',
-                            dictionary: 'exclude'
-                        }]
-                    }),
-                    ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude']
-                );
-            });
-        });
+describe('FT.SPELLCHECK', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(SPELLCHECK, 'index', 'query'),
+        ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', DEFAULT_DIALECT]
+      );
+    });
 
-        it('with DIALECT', () => {
-            assert.deepEqual(
-                transformArguments('index', 'query', {
-                    DIALECT: 1
-                }),
-                ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1']
-            );
-        });
+    it('with DISTANCE', () => {
+      assert.deepEqual(
+        parseArgs(SPELLCHECK, 'index', 'query', {
+          DISTANCE: 2
+        }),
+        ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2', 'DIALECT', DEFAULT_DIALECT]
+      );
     });
 
-    testUtils.testWithClient('client.ft.spellCheck', async client => {
-        await Promise.all([
-            client.ft.create('index', {
-                field: SchemaFieldTypes.TEXT
-            }),
-            client.hSet('key', 'field', 'query')
-        ]);
+    describe('with TERMS', () => {
+      it('single', () => {
+        assert.deepEqual(
+          parseArgs(SPELLCHECK, 'index', 'query', {
+            TERMS: {
+              mode: 'INCLUDE',
+              dictionary: 'dictionary'
+            }
+          }),
+          ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary', 'DIALECT', DEFAULT_DIALECT]
+        );
+      });
 
+      it('multiple', () => {
         assert.deepEqual(
-            await client.ft.spellCheck('index', 'quer'),
-            [{
-                term: 'quer',
-                suggestions: [{
-                    score: 1,
-                    suggestion: 'query'
-                }]
+          parseArgs(SPELLCHECK, 'index', 'query', {
+            TERMS: [{
+              mode: 'INCLUDE',
+              dictionary: 'include'
+            }, {
+              mode: 'EXCLUDE',
+              dictionary: 'exclude'
             }]
+          }),
+          ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude', 'DIALECT', DEFAULT_DIALECT]
         );
-    }, GLOBAL.SERVERS.OPEN);
+      });
+    });
+
+    it('with DIALECT', () => {
+      assert.deepEqual(
+        parseArgs(SPELLCHECK, 'index', 'query', {
+          DIALECT: 1
+        }),
+        ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1']
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.ft.spellCheck', async client => {
+    const [,, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: 'TEXT'
+      }),
+      client.hSet('key', 'field', 'query'),
+      client.ft.spellCheck('index', 'quer')
+    ]);
+
+    assert.deepEqual(reply, [{
+      term: 'quer',
+      suggestions: [{
+        score: 1,
+        suggestion: 'query'
+      }]
+    }]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts
index c9317a8b4fe..d6d84b19543 100644
--- a/packages/search/lib/commands/SPELLCHECK.ts
+++ b/packages/search/lib/commands/SPELLCHECK.ts
@@ -1,62 +1,83 @@
-interface SpellCheckTerms {
-    mode: 'INCLUDE' | 'EXCLUDE';
-    dictionary: string;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types';
+import { DEFAULT_DIALECT } from '../dialect/default';
+
+export interface Terms {
+  mode: 'INCLUDE' | 'EXCLUDE';
+  dictionary: RedisArgument;
 }
 
-interface SpellCheckOptions {
-    DISTANCE?: number;
-    TERMS?: SpellCheckTerms | Array<SpellCheckTerms>;
-    DIALECT?: number;
+export interface FtSpellCheckOptions {
+  DISTANCE?: number;
+  TERMS?: Terms | Array<Terms>;
+  DIALECT?: number;
 }
 
-export function transformArguments(index: string, query: string, options?: SpellCheckOptions): Array<string> {
-    const args = ['FT.SPELLCHECK', index, query];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Performs spelling correction on a search query.
+   * @param parser - The command parser
+   * @param index - Name of the index to use for spelling corrections
+   * @param query - The search query to check for spelling
+   * @param options - Optional parameters:
+   *   - DISTANCE: Maximum Levenshtein distance for spelling suggestions
+   *   - TERMS: Custom dictionary terms to include/exclude
+   *   - DIALECT: Version of query dialect to use (defaults to 1)
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) {
+    parser.push('FT.SPELLCHECK', index, query);
 
     if (options?.DISTANCE) {
-        args.push('DISTANCE', options.DISTANCE.toString());
+      parser.push('DISTANCE', options.DISTANCE.toString());
     }
 
     if (options?.TERMS) {
-        if (Array.isArray(options.TERMS)) {
-            for (const term of options.TERMS) {
-                pushTerms(args, term);
-            }
-        } else {
-            pushTerms(args, options.TERMS);
+      if (Array.isArray(options.TERMS)) {
+        for (const term of options.TERMS) {
+          parseTerms(parser, term);
         }
+      } else {
+        parseTerms(parser, options.TERMS);
+      }
     }
 
     if (options?.DIALECT) {
-        args.push('DIALECT', options.DIALECT.toString());
+      parser.push('DIALECT', options.DIALECT.toString());
+    } else {
+      parser.push('DIALECT', DEFAULT_DIALECT);
     }
+  },
+  transformReply: {
+    2: (rawReply: SpellCheckRawReply): SpellCheckReply => {
+      return rawReply.map(([, term, suggestions]) => ({
+        term,
+        suggestions: suggestions.map(([score, suggestion]) => ({
+          score: Number(score),
+          suggestion
+        }))
+      }));
+    },
+    3: undefined as unknown as () => ReplyUnion,
+  },
+  unstableResp3: true
+} as const satisfies Command;
 
-    return args;
-}
-
-function pushTerms(args: Array<string>, { mode, dictionary }: SpellCheckTerms): void {
-    args.push('TERMS', mode, dictionary);
+function parseTerms(parser: CommandParser, { mode, dictionary }: Terms) {
+  parser.push('TERMS', mode, dictionary);
 }
 
 type SpellCheckRawReply = Array<[
-    _: string,
-    term: string,
-    suggestions: Array<[score: string, suggestion: string]>
+  _: string,
+  term: string,
+  suggestions: Array<[score: string, suggestion: string]>
 ]>;
 
 type SpellCheckReply = Array<{
-    term: string,
-    suggestions: Array<{
-        score: number,
-        suggestion: string
-    }>
+  term: string,
+  suggestions: Array<{
+    score: number,
+    suggestion: string
+  }>
 }>;
-
-export function transformReply(rawReply: SpellCheckRawReply): SpellCheckReply {
-    return rawReply.map(([, term, suggestions]) => ({
-        term,
-        suggestions: suggestions.map(([score, suggestion]) => ({
-            score: Number(score),
-            suggestion
-        }))
-    }));
-}
diff --git a/packages/search/lib/commands/SUGADD.spec.ts b/packages/search/lib/commands/SUGADD.spec.ts
index 23294eb4abd..2e0ce92edbc 100644
--- a/packages/search/lib/commands/SUGADD.spec.ts
+++ b/packages/search/lib/commands/SUGADD.spec.ts
@@ -1,35 +1,36 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGADD';
+import SUGADD from './SUGADD';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SUGADD', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key', 'string', 1),
-                ['FT.SUGADD', 'key', 'string', '1']
-            );
-        });
+describe('FT.SUGADD', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(SUGADD, 'key', 'string', 1),
+        ['FT.SUGADD', 'key', 'string', '1']
+      );
+    });
 
-        it('with INCR', () => {
-            assert.deepEqual(
-                transformArguments('key', 'string', 1, { INCR: true }),
-                ['FT.SUGADD', 'key', 'string', '1', 'INCR']
-            );
-        });
+    it('with INCR', () => {
+      assert.deepEqual(
+        parseArgs(SUGADD, 'key', 'string', 1, { INCR: true }),
+        ['FT.SUGADD', 'key', 'string', '1', 'INCR']
+      );
+    });
 
-        it('with PAYLOAD', () => {
-            assert.deepEqual(
-                transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }),
-                ['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload']
-            );
-        });
+    it('with PAYLOAD', () => {
+      assert.deepEqual(
+        parseArgs(SUGADD, 'key', 'string', 1, { PAYLOAD: 'payload' }),
+        ['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.sugAdd', async client => {
-        assert.equal(
-            await client.ft.sugAdd('key', 'string', 1),
-            1
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft.sugAdd', async client => {
+    assert.equal(
+      await client.ft.sugAdd('key', 'string', 1),
+      1
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/SUGADD.ts b/packages/search/lib/commands/SUGADD.ts
index d68f0d98841..3fa592a2733 100644
--- a/packages/search/lib/commands/SUGADD.ts
+++ b/packages/search/lib/commands/SUGADD.ts
@@ -1,20 +1,35 @@
-interface SugAddOptions {
-    INCR?: true;
-    PAYLOAD?: string;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+
+export interface FtSugAddOptions {
+  INCR?: boolean;
+  PAYLOAD?: RedisArgument;
 }
 
-export function transformArguments(key: string, string: string, score: number, options?: SugAddOptions): Array<string> {
-    const args = ['FT.SUGADD', key, string, score.toString()];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Adds a suggestion string to an auto-complete suggestion dictionary.
+   * @param parser - The command parser
+   * @param key - The suggestion dictionary key
+   * @param string - The suggestion string to add
+   * @param score - The suggestion score used for sorting
+   * @param options - Optional parameters:
+   *   - INCR: If true, increment the existing entry's score
+   *   - PAYLOAD: Optional payload to associate with the suggestion
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) {
+    parser.push('FT.SUGADD');
+    parser.pushKey(key);
+    parser.push(string, score.toString());
 
     if (options?.INCR) {
-        args.push('INCR');
+      parser.push('INCR');
     }
 
     if (options?.PAYLOAD) {
-        args.push('PAYLOAD', options.PAYLOAD);
+      parser.push('PAYLOAD', options.PAYLOAD);
     }
-
-    return args;
-}
-
-export declare function transformReply(): number;
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SUGDEL.spec.ts b/packages/search/lib/commands/SUGDEL.spec.ts
index 3d89e3b9a72..21677f14213 100644
--- a/packages/search/lib/commands/SUGDEL.spec.ts
+++ b/packages/search/lib/commands/SUGDEL.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGDEL';
+import SUGDEL from './SUGDEL';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SUGDEL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'string'),
-            ['FT.SUGDEL', 'key', 'string']
-        );
-    });
+describe('FT.SUGDEL', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SUGDEL, 'key', 'string'),
+      ['FT.SUGDEL', 'key', 'string']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.sugDel', async client => {
-        assert.equal(
-            await client.ft.sugDel('key', 'string'),
-            false
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft.sugDel', async client => {
+    assert.equal(
+      await client.ft.sugDel('key', 'string'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/SUGDEL.ts b/packages/search/lib/commands/SUGDEL.ts
index b522acdfd47..852b33f5c52 100644
--- a/packages/search/lib/commands/SUGDEL.ts
+++ b/packages/search/lib/commands/SUGDEL.ts
@@ -1,5 +1,18 @@
-export function transformArguments(key: string, string: string): Array<string> {
-    return ['FT.SUGDEL', key, string];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Deletes a string from a suggestion dictionary.
+   * @param parser - The command parser
+   * @param key - The suggestion dictionary key
+   * @param string - The suggestion string to delete
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument) {
+    parser.push('FT.SUGDEL');
+    parser.pushKey(key);
+    parser.push(string);
+  },
+  transformReply: undefined as unknown as () => NumberReply<0 | 1>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SUGGET.spec.ts b/packages/search/lib/commands/SUGGET.spec.ts
index c24c2ff0863..b82ea547782 100644
--- a/packages/search/lib/commands/SUGGET.spec.ts
+++ b/packages/search/lib/commands/SUGGET.spec.ts
@@ -1,46 +1,57 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGGET';
-
-describe('SUGGET', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key', 'prefix'),
-                ['FT.SUGGET', 'key', 'prefix']
-            );
-        });
-
-        it('with FUZZY', () => {
-            assert.deepEqual(
-                transformArguments('key', 'prefix', { FUZZY: true }),
-                ['FT.SUGGET', 'key', 'prefix', 'FUZZY']
-            );
-        });
-
-        it('with MAX', () => {
-            assert.deepEqual(
-                transformArguments('key', 'prefix', { MAX: 10 }),
-                ['FT.SUGGET', 'key', 'prefix', 'MAX', '10']
-            );
-        });
+import SUGGET from './SUGGET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('FT.SUGGET', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(SUGGET, 'key', 'prefix'),
+        ['FT.SUGGET', 'key', 'prefix']
+      );
+    });
+
+    it('with FUZZY', () => {
+      assert.deepEqual(
+        parseArgs(SUGGET, 'key', 'prefix', { FUZZY: true }),
+        ['FT.SUGGET', 'key', 'prefix', 'FUZZY']
+      );
     });
 
-    describe('client.ft.sugGet', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.ft.sugGet('key', 'prefix'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
-
-        testUtils.testWithClient('with suggestions', async client => {
-            await client.ft.sugAdd('key', 'string', 1);
-
-            assert.deepEqual(
-                await client.ft.sugGet('key', 'string'),
-                ['string']
-            );
-        }, GLOBAL.SERVERS.OPEN);
+    it('with MAX', () => {
+      assert.deepEqual(
+        parseArgs(SUGGET, 'key', 'prefix', { MAX: 10 }),
+        ['FT.SUGGET', 'key', 'prefix', 'MAX', '10']
+      );
     });
+  });
+
+  describe('client.ft.sugGet', () => {
+
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => {
+      assert.deepStrictEqual(
+        await client.ft.sugGet('key', 'prefix'),
+        []
+      );
+    }, GLOBAL.SERVERS.OPEN);
+
+
+
+    testUtils.testWithClientIfVersionWithinRange([[6, 2, 0], [7, 4, 0]], 'null', async client => {
+      assert.deepStrictEqual(
+        await client.ft.sugGet('key', 'prefix'),
+        null
+      );
+    }, GLOBAL.SERVERS.OPEN)
+
+    testUtils.testWithClient('with suggestions', async client => {
+      const [, reply] = await Promise.all([
+        client.ft.sugAdd('key', 'string', 1),
+        client.ft.sugGet('key', 's')
+      ]);
+
+      assert.deepEqual(reply, ['string']);
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/search/lib/commands/SUGGET.ts b/packages/search/lib/commands/SUGGET.ts
index 558cedeaa08..6c463a020e2 100644
--- a/packages/search/lib/commands/SUGGET.ts
+++ b/packages/search/lib/commands/SUGGET.ts
@@ -1,22 +1,34 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
 
-export interface SugGetOptions {
-    FUZZY?: true;
-    MAX?: number;
+export interface FtSugGetOptions {
+  FUZZY?: boolean;
+  MAX?: number;
 }
 
-export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
-    const args = ['FT.SUGGET', key, prefix];
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets completion suggestions for a prefix from a suggestion dictionary.
+   * @param parser - The command parser
+   * @param key - The suggestion dictionary key
+   * @param prefix - The prefix to get completion suggestions for
+   * @param options - Optional parameters:
+   *   - FUZZY: Enable fuzzy prefix matching
+   *   - MAX: Maximum number of results to return
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) {
+    parser.push('FT.SUGGET');
+    parser.pushKey(key);
+    parser.push(prefix);
 
     if (options?.FUZZY) {
-        args.push('FUZZY');
+      parser.push('FUZZY');
     }
 
-    if (options?.MAX) {
-        args.push('MAX', options.MAX.toString());
+    if (options?.MAX !== undefined) {
+      parser.push('MAX', options.MAX.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): null | Array<string>;
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<BlobStringReply>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts
index a4a87ebe895..c01b87e2892 100644
--- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts
+++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts
@@ -1,33 +1,43 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGGET_WITHPAYLOADS';
+import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SUGGET WITHPAYLOADS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'prefix'),
-            ['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS']
-        );
-    });
+describe('FT.SUGGET WITHPAYLOADS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SUGGET_WITHPAYLOADS, 'key', 'prefix'),
+      ['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS']
+    );
+  });
 
-    describe('client.ft.sugGetWithPayloads', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.ft.sugGetWithPayloads('key', 'prefix'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => {
+    assert.deepStrictEqual(
+      await client.ft.sugGetWithPayloads('key', 'prefix'),
+      []
+    );
+  }, GLOBAL.SERVERS.OPEN);
 
-        testUtils.testWithClient('with suggestions', async client => {
-            await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' });
+  testUtils.testWithClientIfVersionWithinRange([[6], [7, 4, 0]], 'null', async client => {
+    assert.deepStrictEqual(
+      await client.ft.sugGetWithPayloads('key', 'prefix'),
+      null
+    );
+  }, GLOBAL.SERVERS.OPEN);
 
-            assert.deepEqual(
-                await client.ft.sugGetWithPayloads('key', 'string'),
-                [{
-                    suggestion: 'string',
-                    payload: 'payload'
-                }]
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
+  describe('with suggestions', () => {
+    testUtils.testWithClient('with suggestions', async client => {
+      const [, reply] = await Promise.all([
+        client.ft.sugAdd('key', 'string', 1, {
+          PAYLOAD: 'payload'
+        }),
+        client.ft.sugGetWithPayloads('key', 'string')
+      ]);
+
+      assert.deepEqual(reply, [{
+        suggestion: 'string',
+        payload: 'payload'
+      }]);
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts
index 7eaff4697e1..a83279be0ff 100644
--- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts
+++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts
@@ -1,29 +1,37 @@
-import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
+import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
+import SUGGET from './SUGGET';
 
-export { IS_READ_ONLY } from './SUGGET';
+export default {
+  IS_READ_ONLY: SUGGET.IS_READ_ONLY,
+  /**
+   * Gets completion suggestions with their payloads from a suggestion dictionary.
+   * @param args - Same parameters as FT.SUGGET:
+   *   - parser: The command parser
+   *   - key: The suggestion dictionary key
+   *   - prefix: The prefix to get completion suggestions for
+   *   - options: Optional parameters for fuzzy matching and max results
+   */
+  parseCommand(...args: Parameters<typeof SUGGET.parseCommand>) {
+    SUGGET.parseCommand(...args);
+    args[0].push('WITHPAYLOADS');
+  },
+  transformReply(reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply>>) {
+    if (isNullReply(reply)) return null;
 
-export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
-    return [
-        ...transformSugGetArguments(key, prefix, options),
-        'WITHPAYLOADS'
-    ];
-}
-
-export interface SuggestionWithPayload {
-    suggestion: string;
-    payload: string | null;
-}
-
-export function transformReply(rawReply: Array<string | null> | null): Array<SuggestionWithPayload> | null {
-    if (rawReply === null) return null;
-
-    const transformedReply = [];
-    for (let i = 0; i < rawReply.length; i += 2) {
-        transformedReply.push({
-            suggestion: rawReply[i]!,
-            payload: rawReply[i + 1]
-        });
+    const transformedReply: Array<{
+      suggestion: BlobStringReply;
+      payload: BlobStringReply;
+    }> = new Array(reply.length / 2);
+    let replyIndex = 0,
+      arrIndex = 0;
+    while (replyIndex < reply.length) {
+      transformedReply[arrIndex++] = {
+        suggestion: reply[replyIndex++],
+        payload: reply[replyIndex++]
+      };
     }
 
     return transformedReply;
-}
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts
index e60daa917a9..50db89ffe99 100644
--- a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts
+++ b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts
@@ -1,33 +1,35 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGGET_WITHSCORES';
+import SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SUGGET WITHSCORES', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'prefix'),
-            ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES']
-        );
-    });
+describe('FT.SUGGET WITHSCORES', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SUGGET_WITHSCORES, 'key', 'prefix'),
+      ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES']
+    );
+  });
 
-    describe('client.ft.sugGetWithScores', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.ft.sugGetWithScores('key', 'prefix'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+  describe('client.ft.sugGetWithScores', () => {
 
-        testUtils.testWithClient('with suggestions', async client => {
-            await client.ft.sugAdd('key', 'string', 1);
+    testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'], 'null', async client => {
+      assert.deepStrictEqual(
+        await client.ft.sugGetWithScores('key', 'prefix'),
+        []
+      );
+    }, GLOBAL.SERVERS.OPEN);
 
-            assert.deepEqual(
-                await client.ft.sugGetWithScores('key', 'string'),
-                [{
-                    suggestion: 'string',
-                    score: 2147483648
-                }]
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
+    testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'],'with suggestions', async client => {
+      const [, reply] = await Promise.all([
+        client.ft.sugAdd('key', 'string', 1),
+        client.ft.sugGetWithScores('key', 's')
+      ]);
+
+      assert.ok(Array.isArray(reply));
+      assert.equal(reply.length, 1);
+      assert.equal(reply[0].suggestion, 'string');
+      assert.equal(typeof reply[0].score, 'number');
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.ts
index bad5bff2999..5c0a3fba2a3 100644
--- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts
+++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts
@@ -1,29 +1,56 @@
-import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
+import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
+import SUGGET from './SUGGET';
 
-export { IS_READ_ONLY } from './SUGGET';
-
-export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
-    return [
-        ...transformSugGetArguments(key, prefix, options),
-        'WITHSCORES'
-    ];
+type SuggestScore = {
+  suggestion: BlobStringReply;
+  score: DoubleReply;
 }
 
-export interface SuggestionWithScores {
-    suggestion: string;
-    score: number;
-}
+export default {
+  IS_READ_ONLY: SUGGET.IS_READ_ONLY,
+  /**
+   * Gets completion suggestions with their scores from a suggestion dictionary.
+   * @param args - Same parameters as FT.SUGGET:
+   *   - parser: The command parser
+   *   - key: The suggestion dictionary key
+   *   - prefix: The prefix to get completion suggestions for
+   *   - options: Optional parameters for fuzzy matching and max results
+   */
+  parseCommand(...args: Parameters<typeof SUGGET.parseCommand>) {
+    SUGGET.parseCommand(...args);
+    args[0].push('WITHSCORES');
+  },
+  transformReply: {
+    2: (reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      if (isNullReply(reply)) return null;
 
-export function transformReply(rawReply: Array<string> | null): Array<SuggestionWithScores> | null {
-    if (rawReply === null) return null;
+      const transformedReply: Array<SuggestScore> = new Array(reply.length / 2);
+      let replyIndex = 0,
+        arrIndex = 0;
+      while (replyIndex < reply.length) {
+        transformedReply[arrIndex++] = {
+          suggestion: reply[replyIndex++],
+          score: transformDoubleReply[2](reply[replyIndex++], preserve, typeMapping)
+        };
+      }
 
-    const transformedReply = [];
-    for (let i = 0; i < rawReply.length; i += 2) {
-        transformedReply.push({
-            suggestion: rawReply[i],
-            score: Number(rawReply[i + 1])
-        });
-    }
+      return transformedReply;
+    },
+    3: (reply: UnwrapReply<ArrayReply<BlobStringReply | DoubleReply>>) => {
+      if (isNullReply(reply)) return null;
+      
+      const transformedReply: Array<SuggestScore> = new Array(reply.length / 2);
+      let replyIndex = 0,
+        arrIndex = 0;
+      while (replyIndex < reply.length) {
+        transformedReply[arrIndex++] = {
+          suggestion: reply[replyIndex++] as BlobStringReply,
+          score: reply[replyIndex++] as DoubleReply
+        };
+      }
 
-    return transformedReply;
-}
+      return transformedReply;
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts
index 0900d91b8d9..96eb473159f 100644
--- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts
+++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts
@@ -1,34 +1,37 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGGET_WITHSCORES_WITHPAYLOADS';
+import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SUGGET WITHSCORES WITHPAYLOADS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', 'prefix'),
-            ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS']
-        );
-    });
+describe('FT.SUGGET WITHSCORES WITHPAYLOADS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SUGGET_WITHSCORES_WITHPAYLOADS, 'key', 'prefix'),
+      ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS']
+    );
+  });
 
-    describe('client.ft.sugGetWithScoresWithPayloads', () => {
-        testUtils.testWithClient('null', async client => {
-            assert.equal(
-                await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+  describe('client.ft.sugGetWithScoresWithPayloads', () => {
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => {
+      assert.deepStrictEqual(
+        await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'),
+        []
+      );
+    }, GLOBAL.SERVERS.OPEN);
 
-        testUtils.testWithClient('with suggestions', async client => {
-            await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' });
+    testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'with suggestions', async client => {
+      const [, reply] = await Promise.all([
+        client.ft.sugAdd('key', 'string', 1, {
+          PAYLOAD: 'payload'
+        }),
+        client.ft.sugGetWithScoresWithPayloads('key', 'string')
+      ]);
 
-            assert.deepEqual(
-                await client.ft.sugGetWithScoresWithPayloads('key', 'string'),
-                [{
-                    suggestion: 'string',
-                    score: 2147483648,
-                    payload: 'payload'
-                }]
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
+      assert.ok(Array.isArray(reply));
+      assert.equal(reply.length, 1);
+      assert.equal(reply[0].suggestion, 'string');
+      assert.equal(typeof reply[0].score, 'number');
+      assert.equal(reply[0].payload, 'payload');
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts
index 3b2fe7667b7..b7aa38df3fe 100644
--- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts
+++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts
@@ -1,30 +1,62 @@
-import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
-import { SuggestionWithPayload } from './SUGGET_WITHPAYLOADS';
-import { SuggestionWithScores } from './SUGGET_WITHSCORES';
+import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
+import SUGGET from './SUGGET';
 
-export { IS_READ_ONLY } from './SUGGET';
-
-export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
-    return [
-        ...transformSugGetArguments(key, prefix, options),
-        'WITHSCORES',
-        'WITHPAYLOADS'
-    ];
+type SuggestScoreWithPayload = {
+  suggestion: BlobStringReply;
+  score: DoubleReply;
+  payload: BlobStringReply;
 }
 
-type SuggestionWithScoresAndPayloads = SuggestionWithScores & SuggestionWithPayload;
+export default {
+  IS_READ_ONLY: SUGGET.IS_READ_ONLY,
+  /**
+   * Gets completion suggestions with their scores and payloads from a suggestion dictionary.
+   * @param args - Same parameters as FT.SUGGET:
+   *   - parser: The command parser
+   *   - key: The suggestion dictionary key
+   *   - prefix: The prefix to get completion suggestions for
+   *   - options: Optional parameters for fuzzy matching and max results
+   */
+  parseCommand(...args: Parameters<typeof SUGGET.parseCommand>) {
+    SUGGET.parseCommand(...args);
+    args[0].push(
+      'WITHSCORES',
+      'WITHPAYLOADS'
+    );
+  },
+  transformReply: {
+    2: (reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
+      if (isNullReply(reply)) return null;
 
-export function transformReply(rawReply: Array<string | null> | null): Array<SuggestionWithScoresAndPayloads> | null {
-    if (rawReply === null) return null;
+      const transformedReply: Array<SuggestScoreWithPayload> = new Array(reply.length / 3);
+      let replyIndex = 0,
+        arrIndex = 0;
+      while (replyIndex < reply.length) {
+        transformedReply[arrIndex++] = {
+          suggestion: reply[replyIndex++],
+          score: transformDoubleReply[2](reply[replyIndex++], preserve, typeMapping),
+          payload: reply[replyIndex++]
+        };
+      }
 
-    const transformedReply = [];
-    for (let i = 0; i < rawReply.length; i += 3) {
-        transformedReply.push({
-            suggestion: rawReply[i]!,
-            score: Number(rawReply[i + 1]!),
-            payload: rawReply[i + 2]
-        });
-    }
+      return transformedReply;
+    },
+    3: (reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply | DoubleReply>>) => {
+      if (isNullReply(reply)) return null;
 
-    return transformedReply;
-}
+      const transformedReply: Array<SuggestScoreWithPayload> = new Array(reply.length / 3);
+      let replyIndex = 0,
+        arrIndex = 0;
+      while (replyIndex < reply.length) {
+        transformedReply[arrIndex++] = {
+          suggestion: reply[replyIndex++] as BlobStringReply,
+          score: reply[replyIndex++] as DoubleReply,
+          payload: reply[replyIndex++] as BlobStringReply
+        };
+      }
+
+      return transformedReply;
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SUGLEN.spec.ts b/packages/search/lib/commands/SUGLEN.spec.ts
index 2ea680df953..d738f09042e 100644
--- a/packages/search/lib/commands/SUGLEN.spec.ts
+++ b/packages/search/lib/commands/SUGLEN.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SUGLEN';
+import SUGLEN from './SUGLEN';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SUGLEN', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key'),
-            ['FT.SUGLEN', 'key']
-        );
-    });
+describe('FT.SUGLEN', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SUGLEN, 'key'),
+      ['FT.SUGLEN', 'key']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.sugLen', async client => {
-        assert.equal(
-            await client.ft.sugLen('key'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft.sugLen', async client => {
+    assert.equal(
+      await client.ft.sugLen('key'),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/SUGLEN.ts b/packages/search/lib/commands/SUGLEN.ts
index 15b3da61261..ecc4f4a6fc0 100644
--- a/packages/search/lib/commands/SUGLEN.ts
+++ b/packages/search/lib/commands/SUGLEN.ts
@@ -1,7 +1,15 @@
-export const IS_READ_ONLY = true;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(key: string): Array<string> {
-    return ['FT.SUGLEN', key];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets the size of a suggestion dictionary.
+   * @param parser - The command parser
+   * @param key - The suggestion dictionary key
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument) {
+    parser.push('FT.SUGLEN', key);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SYNDUMP.spec.ts b/packages/search/lib/commands/SYNDUMP.spec.ts
index 472db54bcf8..88bf50cfb54 100644
--- a/packages/search/lib/commands/SYNDUMP.spec.ts
+++ b/packages/search/lib/commands/SYNDUMP.spec.ts
@@ -1,24 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SYNDUMP';
-import { SchemaFieldTypes } from '.';
+import SYNDUMP from './SYNDUMP';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SYNDUMP', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('index'),
-            ['FT.SYNDUMP', 'index']
-        );
-    });
+describe('FT.SYNDUMP', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(SYNDUMP, 'index'),
+      ['FT.SYNDUMP', 'index']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.synDump', async client => {
-        await client.ft.create('index', {
-            field: SchemaFieldTypes.TEXT
-        });
+  testUtils.testWithClient('client.ft.synDump', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.synDump('index')
+    ]);
 
-        assert.deepEqual(
-            await client.ft.synDump('index'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, {});
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts
index 5f1e71aaf78..da3e77b4223 100644
--- a/packages/search/lib/commands/SYNDUMP.ts
+++ b/packages/search/lib/commands/SYNDUMP.ts
@@ -1,5 +1,28 @@
-export function transformArguments(index: string): Array<string> {
-    return ['FT.SYNDUMP', index];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Dumps the contents of a synonym group.
+   * @param parser - The command parser
+   * @param index - Name of the index that contains the synonym group
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument) {
+    parser.push('FT.SYNDUMP', index);
+  },
+  transformReply: {
+    2: (reply: UnwrapReply<ArrayReply<BlobStringReply | ArrayReply<BlobStringReply>>>) => {
+      const result: Record<string, ArrayReply<BlobStringReply>> = {};
+      let i = 0;
+      while (i < reply.length) {
+        const key = (reply[i++] as unknown as UnwrapReply<BlobStringReply>).toString(),
+          value = reply[i++] as unknown as ArrayReply<BlobStringReply>;
+        result[key] = value;
+      }
+      return result;
+    },
+    3: undefined as unknown as () => MapReply<BlobStringReply, ArrayReply<BlobStringReply>>
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/SYNUPDATE.spec.ts b/packages/search/lib/commands/SYNUPDATE.spec.ts
index 19ac9b85e54..f93e0599151 100644
--- a/packages/search/lib/commands/SYNUPDATE.spec.ts
+++ b/packages/search/lib/commands/SYNUPDATE.spec.ts
@@ -1,40 +1,43 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './SYNUPDATE';
-import { SchemaFieldTypes } from '.';
+import SYNUPDATE from './SYNUPDATE';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('SYNUPDATE', () => {
-    describe('transformArguments', () => {
-        it('single term', () => {
-            assert.deepEqual(
-                transformArguments('index', 'groupId', 'term'),
-                ['FT.SYNUPDATE', 'index', 'groupId', 'term']
-            );
-        });
+describe('FT.SYNUPDATE', () => {
+  describe('transformArguments', () => {
+    it('single term', () => {
+      assert.deepEqual(
+        parseArgs(SYNUPDATE, 'index', 'groupId', 'term'),
+        ['FT.SYNUPDATE', 'index', 'groupId', 'term']
+      );
+    });
 
-        it('multiple terms', () => {
-            assert.deepEqual(
-                transformArguments('index', 'groupId', ['1', '2']),
-                ['FT.SYNUPDATE', 'index', 'groupId', '1', '2']
-            );
-        });
+    it('multiple terms', () => {
+      assert.deepEqual(
+        parseArgs(SYNUPDATE, 'index', 'groupId', ['1', '2']),
+        ['FT.SYNUPDATE', 'index', 'groupId', '1', '2']
+      );
+    });
 
-        it('with SKIPINITIALSCAN', () => {
-            assert.deepEqual(
-                transformArguments('index', 'groupId', 'term', { SKIPINITIALSCAN: true }),
-                ['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term']
-            );
-        });
+    it('with SKIPINITIALSCAN', () => {
+      assert.deepEqual(
+        parseArgs(SYNUPDATE, 'index', 'groupId', 'term', {
+          SKIPINITIALSCAN: true
+        }),
+        ['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ft.synUpdate', async client => {
-        await client.ft.create('index', {
-            field: SchemaFieldTypes.TEXT
-        });
+  testUtils.testWithClient('client.ft.synUpdate', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TEXT
+      }),
+      client.ft.synUpdate('index', 'groupId', 'term')
+    ]);
 
-        assert.equal(
-            await client.ft.synUpdate('index', 'groupId', 'term'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/SYNUPDATE.ts b/packages/search/lib/commands/SYNUPDATE.ts
index 3384ea59d94..0fed14f894a 100644
--- a/packages/search/lib/commands/SYNUPDATE.ts
+++ b/packages/search/lib/commands/SYNUPDATE.ts
@@ -1,23 +1,37 @@
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-interface SynUpdateOptions {
-    SKIPINITIALSCAN?: true;
+export interface FtSynUpdateOptions {
+  SKIPINITIALSCAN?: boolean;
 }
 
-export function transformArguments(
-    index: string,
-    groupId: string,
-    terms: string | Array<string>,
-    options?: SynUpdateOptions
-): RedisCommandArguments {
-    const args = ['FT.SYNUPDATE', index, groupId];
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Updates a synonym group with new terms.
+   * @param parser - The command parser
+   * @param index - Name of the index that contains the synonym group
+   * @param groupId - ID of the synonym group to update
+   * @param terms - One or more synonym terms to add to the group
+   * @param options - Optional parameters:
+   *   - SKIPINITIALSCAN: Skip the initial scan for existing documents
+   */
+  parseCommand(
+    parser: CommandParser,
+    index: RedisArgument,
+    groupId: RedisArgument,
+    terms: RedisVariadicArgument,
+    options?: FtSynUpdateOptions
+  ) {
+    parser.push('FT.SYNUPDATE', index, groupId);
 
     if (options?.SKIPINITIALSCAN) {
-        args.push('SKIPINITIALSCAN');
+      parser.push('SKIPINITIALSCAN');
     }
 
-    return pushVerdictArguments(args, terms);
-}
-
-export declare function transformReply(): 'OK';
+    parser.pushVariadic(terms);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/TAGVALS.spec.ts b/packages/search/lib/commands/TAGVALS.spec.ts
index d59bfcfe3ea..f0d83c9f7ad 100644
--- a/packages/search/lib/commands/TAGVALS.spec.ts
+++ b/packages/search/lib/commands/TAGVALS.spec.ts
@@ -1,24 +1,25 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { SchemaFieldTypes } from '.';
-import { transformArguments } from './TAGVALS';
+import TAGVALS from './TAGVALS';
+import { SCHEMA_FIELD_TYPE } from './CREATE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('TAGVALS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('index', '@field'),
-            ['FT.TAGVALS', 'index', '@field']
-        );
-    });
+describe('FT.TAGVALS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(TAGVALS, 'index', '@field'),
+      ['FT.TAGVALS', 'index', '@field']
+    );
+  });
 
-    testUtils.testWithClient('client.ft.tagVals', async client => {
-        await client.ft.create('index', {
-            field: SchemaFieldTypes.TAG
-        });
+  testUtils.testWithClient('client.ft.tagVals', async client => {
+    const [, reply] = await Promise.all([
+      client.ft.create('index', {
+        field: SCHEMA_FIELD_TYPE.TAG
+      }),
+      client.ft.tagVals('index', 'field')
+    ]);
 
-        assert.deepEqual(
-            await client.ft.tagVals('index', 'field'),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, []);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts
index 54342f0c9e5..1c307945f2b 100644
--- a/packages/search/lib/commands/TAGVALS.ts
+++ b/packages/search/lib/commands/TAGVALS.ts
@@ -1,5 +1,20 @@
-export function transformArguments(index: string, fieldName: string): Array<string> {
-    return ['FT.TAGVALS', index, fieldName];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Returns the distinct values in a TAG field.
+   * @param parser - The command parser
+   * @param index - Name of the index
+   * @param fieldName - Name of the TAG field to get values from
+   */
+  parseCommand(parser: CommandParser, index: RedisArgument, fieldName: RedisArgument) {
+    parser.push('FT.TAGVALS', index, fieldName);
+  },
+  transformReply: {
+    2: undefined as unknown as () => ArrayReply<BlobStringReply>,
+    3: undefined as unknown as () => SetReply<BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/_LIST.spec.ts b/packages/search/lib/commands/_LIST.spec.ts
index 602c29975f2..dfe32f2e29d 100644
--- a/packages/search/lib/commands/_LIST.spec.ts
+++ b/packages/search/lib/commands/_LIST.spec.ts
@@ -1,19 +1,20 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './_LIST';
+import _LIST from './_LIST';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
 describe('_LIST', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments(),
-            ['FT._LIST']
-        );
-    });
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(_LIST),
+      ['FT._LIST']
+    );
+  });
 
-    testUtils.testWithClient('client.ft._list', async client => {
-        assert.deepEqual(
-            await client.ft._list(),
-            []
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ft._list', async client => {
+    assert.deepEqual(
+      await client.ft._list(),
+      []
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/search/lib/commands/_LIST.ts b/packages/search/lib/commands/_LIST.ts
index 588ec837c3b..1b30e044e61 100644
--- a/packages/search/lib/commands/_LIST.ts
+++ b/packages/search/lib/commands/_LIST.ts
@@ -1,5 +1,18 @@
-export function transformArguments(): Array<string> {
-    return ['FT._LIST'];
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Lists all existing indexes in the database.
+   * @param parser - The command parser
+   */
+  parseCommand(parser: CommandParser) {
+    parser.push('FT._LIST');
+  },
+  transformReply: {
+    2: undefined as unknown as () => ArrayReply<BlobStringReply>,
+    3: undefined as unknown as () => SetReply<BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/search/lib/commands/index.spec.ts b/packages/search/lib/commands/index.spec.ts
index 4c54a0dfdf2..04808932c59 100644
--- a/packages/search/lib/commands/index.spec.ts
+++ b/packages/search/lib/commands/index.spec.ts
@@ -1,5 +1,6 @@
-import { strict as assert } from 'assert';
-import { pushArgumentsWithLength, pushSortByArguments } from '.';
+import { strict as assert } from 'node:assert';
+
+/* import { pushArgumentsWithLength, pushSortByArguments } from '.';
 
 describe('pushSortByArguments', () => {
     describe('single', () => {
@@ -44,3 +45,4 @@ it('pushArgumentsWithLength', () => {
         ['a', '2', 'b', 'c']
     );
 });
+*/
\ No newline at end of file
diff --git a/packages/search/lib/commands/index.ts b/packages/search/lib/commands/index.ts
index f907e1999e6..7aa3f061bf7 100644
--- a/packages/search/lib/commands/index.ts
+++ b/packages/search/lib/commands/index.ts
@@ -1,690 +1,117 @@
-import * as _LIST from './_LIST';
-import * as ALTER from './ALTER';
-import * as AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
-import * as AGGREGATE from './AGGREGATE';
-import * as ALIASADD from './ALIASADD';
-import * as ALIASDEL from './ALIASDEL';
-import * as ALIASUPDATE from './ALIASUPDATE';
-import * as CONFIG_GET from './CONFIG_GET';
-import * as CONFIG_SET from './CONFIG_SET';
-import * as CREATE from './CREATE';
-import * as CURSOR_DEL from './CURSOR_DEL';
-import * as CURSOR_READ from './CURSOR_READ';
-import * as DICTADD from './DICTADD';
-import * as DICTDEL from './DICTDEL';
-import * as DICTDUMP from './DICTDUMP';
-import * as DROPINDEX from './DROPINDEX';
-import * as EXPLAIN from './EXPLAIN';
-import * as EXPLAINCLI from './EXPLAINCLI';
-import * as INFO from './INFO';
-import * as PROFILESEARCH from './PROFILE_SEARCH';
-import * as PROFILEAGGREGATE from './PROFILE_AGGREGATE';
-import * as SEARCH from './SEARCH';
-import * as SEARCH_NOCONTENT from './SEARCH_NOCONTENT';
-import * as SPELLCHECK from './SPELLCHECK';
-import * as SUGADD from './SUGADD';
-import * as SUGDEL from './SUGDEL';
-import * as SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
-import * as SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
-import * as SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
-import * as SUGGET from './SUGGET';
-import * as SUGLEN from './SUGLEN';
-import * as SYNDUMP from './SYNDUMP';
-import * as SYNUPDATE from './SYNUPDATE';
-import * as TAGVALS from './TAGVALS';
-import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushOptionalVerdictArgument, pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers';
-import { SearchOptions } from './SEARCH';
+import _LIST from './_LIST';
+import ALTER from './ALTER';
+import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
+import AGGREGATE from './AGGREGATE';
+import ALIASADD from './ALIASADD';
+import ALIASDEL from './ALIASDEL';
+import ALIASUPDATE from './ALIASUPDATE';
+import CONFIG_GET from './CONFIG_GET';
+import CONFIG_SET from './CONFIG_SET';
+import CREATE from './CREATE';
+import CURSOR_DEL from './CURSOR_DEL';
+import CURSOR_READ from './CURSOR_READ';
+import DICTADD from './DICTADD';
+import DICTDEL from './DICTDEL';
+import DICTDUMP from './DICTDUMP';
+import DROPINDEX from './DROPINDEX';
+import EXPLAIN from './EXPLAIN';
+import EXPLAINCLI from './EXPLAINCLI';
+import INFO from './INFO';
+import PROFILESEARCH from './PROFILE_SEARCH';
+import PROFILEAGGREGATE from './PROFILE_AGGREGATE';
+import SEARCH_NOCONTENT from './SEARCH_NOCONTENT';
+import SEARCH from './SEARCH';
+import SPELLCHECK from './SPELLCHECK';
+import SUGADD from './SUGADD';
+import SUGDEL from './SUGDEL';
+import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
+import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
+import SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
+import SUGGET from './SUGGET';
+import SUGLEN from './SUGLEN';
+import SYNDUMP from './SYNDUMP';
+import SYNUPDATE from './SYNUPDATE';
+import TAGVALS from './TAGVALS';
 
 export default {
-    _LIST,
-    _list: _LIST,
-    ALTER,
-    alter: ALTER,
-    AGGREGATE_WITHCURSOR,
-    aggregateWithCursor: AGGREGATE_WITHCURSOR,
-    AGGREGATE,
-    aggregate: AGGREGATE,
-    ALIASADD,
-    aliasAdd: ALIASADD,
-    ALIASDEL,
-    aliasDel: ALIASDEL,
-    ALIASUPDATE,
-    aliasUpdate: ALIASUPDATE,
-    CONFIG_GET,
-    configGet: CONFIG_GET,
-    CONFIG_SET,
-    configSet: CONFIG_SET,
-    CREATE,
-    create: CREATE,
-    CURSOR_DEL,
-    cursorDel: CURSOR_DEL,
-    CURSOR_READ,
-    cursorRead: CURSOR_READ,
-    DICTADD,
-    dictAdd: DICTADD,
-    DICTDEL,
-    dictDel: DICTDEL,
-    DICTDUMP,
-    dictDump: DICTDUMP,
-    DROPINDEX,
-    dropIndex: DROPINDEX,
-    EXPLAIN,
-    explain: EXPLAIN,
-    EXPLAINCLI,
-    explainCli: EXPLAINCLI,
-    INFO,
-    info: INFO,
-    PROFILESEARCH,
-    profileSearch: PROFILESEARCH,
-    PROFILEAGGREGATE,
-    profileAggregate: PROFILEAGGREGATE,
-    SEARCH,
-    search: SEARCH,
-    SEARCH_NOCONTENT,
-    searchNoContent: SEARCH_NOCONTENT,
-    SPELLCHECK,
-    spellCheck: SPELLCHECK,
-    SUGADD,
-    sugAdd: SUGADD,
-    SUGDEL,
-    sugDel: SUGDEL,
-    SUGGET_WITHPAYLOADS,
-    sugGetWithPayloads: SUGGET_WITHPAYLOADS,
-    SUGGET_WITHSCORES_WITHPAYLOADS,
-    sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS,
-    SUGGET_WITHSCORES,
-    sugGetWithScores: SUGGET_WITHSCORES,
-    SUGGET,
-    sugGet: SUGGET,
-    SUGLEN,
-    sugLen: SUGLEN,
-    SYNDUMP,
-    synDump: SYNDUMP,
-    SYNUPDATE,
-    synUpdate: SYNUPDATE,
-    TAGVALS,
-    tagVals: TAGVALS
+  _LIST,
+  _list: _LIST,
+  ALTER,
+  alter: ALTER,
+  AGGREGATE_WITHCURSOR,
+  aggregateWithCursor: AGGREGATE_WITHCURSOR,
+  AGGREGATE,
+  aggregate: AGGREGATE,
+  ALIASADD,
+  aliasAdd: ALIASADD,
+  ALIASDEL,
+  aliasDel: ALIASDEL,
+  ALIASUPDATE,
+  aliasUpdate: ALIASUPDATE,
+  /**
+   * @deprecated Redis >=8 uses the standard CONFIG command 
+   */
+  CONFIG_GET,
+  /**
+   * @deprecated Redis >=8 uses the standard CONFIG command 
+   */
+  configGet: CONFIG_GET,
+  /**
+   * @deprecated Redis >=8 uses the standard CONFIG command 
+   */
+  CONFIG_SET,
+  /**
+   * @deprecated Redis >=8 uses the standard CONFIG command 
+   */
+  configSet: CONFIG_SET,
+  CREATE,
+  create: CREATE,
+  CURSOR_DEL,
+  cursorDel: CURSOR_DEL,
+  CURSOR_READ,
+  cursorRead: CURSOR_READ,
+  DICTADD,
+  dictAdd: DICTADD,
+  DICTDEL,
+  dictDel: DICTDEL,
+  DICTDUMP,
+  dictDump: DICTDUMP,
+  DROPINDEX,
+  dropIndex: DROPINDEX,
+  EXPLAIN,
+  explain: EXPLAIN,
+  EXPLAINCLI,
+  explainCli: EXPLAINCLI,
+  INFO,
+  info: INFO,
+  PROFILESEARCH,
+  profileSearch: PROFILESEARCH,
+  PROFILEAGGREGATE,
+  profileAggregate: PROFILEAGGREGATE,
+  SEARCH_NOCONTENT,
+  searchNoContent: SEARCH_NOCONTENT,
+  SEARCH,
+  search: SEARCH,
+  SPELLCHECK,
+  spellCheck: SPELLCHECK,
+  SUGADD,
+  sugAdd: SUGADD,
+  SUGDEL,
+  sugDel: SUGDEL,
+  SUGGET_WITHPAYLOADS,
+  sugGetWithPayloads: SUGGET_WITHPAYLOADS,
+  SUGGET_WITHSCORES_WITHPAYLOADS,
+  sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS,
+  SUGGET_WITHSCORES,
+  sugGetWithScores: SUGGET_WITHSCORES,
+  SUGGET,
+  sugGet: SUGGET,
+  SUGLEN,
+  sugLen: SUGLEN,
+  SYNDUMP,
+  synDump: SYNDUMP,
+  SYNUPDATE,
+  synUpdate: SYNUPDATE,
+  TAGVALS,
+  tagVals: TAGVALS
 };
-
-export enum RedisSearchLanguages {
-    ARABIC = 'Arabic',
-    BASQUE = 'Basque',
-    CATALANA = 'Catalan',
-    DANISH = 'Danish',
-    DUTCH = 'Dutch',
-    ENGLISH = 'English',
-    FINNISH = 'Finnish',
-    FRENCH = 'French',
-    GERMAN = 'German',
-    GREEK = 'Greek',
-    HUNGARIAN = 'Hungarian',
-    INDONESAIN = 'Indonesian',
-    IRISH = 'Irish',
-    ITALIAN = 'Italian',
-    LITHUANIAN = 'Lithuanian',
-    NEPALI = 'Nepali',
-    NORWEIGAN = 'Norwegian',
-    PORTUGUESE = 'Portuguese',
-    ROMANIAN = 'Romanian',
-    RUSSIAN = 'Russian',
-    SPANISH = 'Spanish',
-    SWEDISH = 'Swedish',
-    TAMIL = 'Tamil',
-    TURKISH = 'Turkish',
-    CHINESE = 'Chinese'
-}
-
-export type PropertyName = `${'@' | '$.'}${string}`;
-
-export type SortByProperty = string | {
-    BY: string;
-    DIRECTION?: 'ASC' | 'DESC';
-};
-
-export function pushSortByProperty(args: RedisCommandArguments, sortBy: SortByProperty): void {
-    if (typeof sortBy === 'string') {
-        args.push(sortBy);
-    } else {
-        args.push(sortBy.BY);
-
-        if (sortBy.DIRECTION) {
-            args.push(sortBy.DIRECTION);
-        }
-    }
-}
-
-export function pushSortByArguments(args: RedisCommandArguments, name: string, sortBy: SortByProperty | Array<SortByProperty>): RedisCommandArguments {
-    const lengthBefore = args.push(
-        name,
-        '' // will be overwritten
-    );
-
-    if (Array.isArray(sortBy)) {
-        for (const field of sortBy) {
-            pushSortByProperty(args, field);
-        }
-    } else {
-        pushSortByProperty(args, sortBy);
-    }
-
-    args[lengthBefore - 1] = (args.length - lengthBefore).toString();
-
-    return args;
-}
-
-export function pushArgumentsWithLength(args: RedisCommandArguments, fn: (args: RedisCommandArguments) => void): RedisCommandArguments {
-    const lengthIndex = args.push('') - 1;
-    fn(args);
-    args[lengthIndex] = (args.length - lengthIndex - 1).toString();
-    return args;
-}
-
-export enum SchemaFieldTypes {
-    TEXT = 'TEXT',
-    NUMERIC = 'NUMERIC',
-    GEO = 'GEO',
-    TAG = 'TAG',
-    VECTOR = 'VECTOR',
-    GEOSHAPE = 'GEOSHAPE'
-}
-  
-type CreateSchemaField<
-    T extends SchemaFieldTypes,
-    E = Record<PropertyKey, unknown>
-> = T | ({
-    type: T;
-    AS?: string;
-    INDEXMISSING?: boolean;
-} & E);
-
-type CommonFieldArguments = {
-    SORTABLE?: boolean | 'UNF';
-    NOINDEX?: boolean;
-};
-
-type CreateSchemaCommonField<
-    T extends SchemaFieldTypes,
-    E = Record<PropertyKey, unknown>
-> = CreateSchemaField<
-    T,
-    (CommonFieldArguments & E)
->;
-
-function pushCommonFieldArguments(args: RedisCommandArguments, fieldOptions: CommonFieldArguments) {
-    if (fieldOptions.SORTABLE) {
-        args.push('SORTABLE');
-
-        if (fieldOptions.SORTABLE === 'UNF') {
-            args.push('UNF');
-        }
-    }
-
-    if (fieldOptions.NOINDEX) {
-        args.push('NOINDEX');
-    }
-}
-
-export enum SchemaTextFieldPhonetics {
-    DM_EN = 'dm:en',
-    DM_FR = 'dm:fr',
-    FM_PT = 'dm:pt',
-    DM_ES = 'dm:es'
-}
-
-type CreateSchemaTextField = CreateSchemaCommonField<SchemaFieldTypes.TEXT, {
-    NOSTEM?: true;
-    WEIGHT?: number;
-    PHONETIC?: SchemaTextFieldPhonetics;
-    WITHSUFFIXTRIE?: boolean;
-    INDEXEMPTY?: boolean;
-}>;
-
-type CreateSchemaNumericField = CreateSchemaCommonField<SchemaFieldTypes.NUMERIC>;
-
-type CreateSchemaGeoField = CreateSchemaCommonField<SchemaFieldTypes.GEO>;
-
-type CreateSchemaTagField = CreateSchemaCommonField<SchemaFieldTypes.TAG, {
-    SEPARATOR?: string;
-    CASESENSITIVE?: true;
-    WITHSUFFIXTRIE?: boolean;
-    INDEXEMPTY?: boolean;
-}>;
-
-export enum VectorAlgorithms {
-    FLAT = 'FLAT',
-    HNSW = 'HNSW'
-}
-
-type CreateSchemaVectorField<
-    T extends VectorAlgorithms,
-    A extends Record<string, unknown>
-> = CreateSchemaField<SchemaFieldTypes.VECTOR, {
-    ALGORITHM: T;
-    TYPE: string;
-    DIM: number;
-    DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE';
-    INITIAL_CAP?: number;
-} & A>;
-
-type CreateSchemaFlatVectorField = CreateSchemaVectorField<VectorAlgorithms.FLAT, {
-    BLOCK_SIZE?: number;
-}>;
-
-type CreateSchemaHNSWVectorField = CreateSchemaVectorField<VectorAlgorithms.HNSW, {
-    M?: number;
-    EF_CONSTRUCTION?: number;
-    EF_RUNTIME?: number;
-}>;
-
-export const SCHEMA_GEO_SHAPE_COORD_SYSTEM = {
-    SPHERICAL: 'SPHERICAL',
-    FLAT: 'FLAT'
-} as const;
-
-export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM];
-
-type CreateSchemaGeoShapeField = CreateSchemaCommonField<SchemaFieldTypes.GEOSHAPE, {
-    COORD_SYSTEM?: SchemaGeoShapeFieldCoordSystem;
-}>;
-
-export interface RediSearchSchema {
-    [field: string]:
-        CreateSchemaTextField |
-        CreateSchemaNumericField |
-        CreateSchemaGeoField |
-        CreateSchemaTagField |
-        CreateSchemaFlatVectorField |
-        CreateSchemaHNSWVectorField |
-        CreateSchemaGeoShapeField;
-}
-
-export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema) {
-    for (const [field, fieldOptions] of Object.entries(schema)) {
-        args.push(field);
-
-        if (typeof fieldOptions === 'string') {
-            args.push(fieldOptions);
-            continue;
-        }
-
-        if (fieldOptions.AS) {
-            args.push('AS', fieldOptions.AS);
-        }
-
-        args.push(fieldOptions.type);
-
-        switch (fieldOptions.type) {
-            case SchemaFieldTypes.TEXT:
-                if (fieldOptions.NOSTEM) {
-                    args.push('NOSTEM');
-                }
-
-                if (fieldOptions.WEIGHT) {
-                    args.push('WEIGHT', fieldOptions.WEIGHT.toString());
-                }
-
-                if (fieldOptions.PHONETIC) {
-                    args.push('PHONETIC', fieldOptions.PHONETIC);
-                }
-
-                if (fieldOptions.WITHSUFFIXTRIE) {
-                    args.push('WITHSUFFIXTRIE');
-                }
-
-                pushCommonFieldArguments(args, fieldOptions);
-
-                if (fieldOptions.INDEXEMPTY) {
-                    args.push('INDEXEMPTY');
-                }
-
-                break;
-
-            case SchemaFieldTypes.NUMERIC:
-            case SchemaFieldTypes.GEO:
-                pushCommonFieldArguments(args, fieldOptions);
-                break;
-
-            case SchemaFieldTypes.TAG:
-                if (fieldOptions.SEPARATOR) {
-                    args.push('SEPARATOR', fieldOptions.SEPARATOR);
-                }
-
-                if (fieldOptions.CASESENSITIVE) {
-                    args.push('CASESENSITIVE');
-                }
-
-                if (fieldOptions.WITHSUFFIXTRIE) {
-                    args.push('WITHSUFFIXTRIE');
-                }
-
-                pushCommonFieldArguments(args, fieldOptions);
-
-                if (fieldOptions.INDEXEMPTY) {
-                    args.push('INDEXEMPTY');
-                }
-
-                break;
-
-            case SchemaFieldTypes.VECTOR:
-                args.push(fieldOptions.ALGORITHM);
-
-                pushArgumentsWithLength(args, () => {
-                    args.push(
-                        'TYPE', fieldOptions.TYPE,
-                        'DIM', fieldOptions.DIM.toString(),
-                        'DISTANCE_METRIC', fieldOptions.DISTANCE_METRIC
-                    );
-
-                    if (fieldOptions.INITIAL_CAP) {
-                        args.push('INITIAL_CAP', fieldOptions.INITIAL_CAP.toString());
-                    }
-
-                    switch (fieldOptions.ALGORITHM) {
-                        case VectorAlgorithms.FLAT:
-                            if (fieldOptions.BLOCK_SIZE) {
-                                args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString());
-                            }
-
-                            break;
-
-                        case VectorAlgorithms.HNSW:
-                            if (fieldOptions.M) {
-                                args.push('M', fieldOptions.M.toString());
-                            }
-
-                            if (fieldOptions.EF_CONSTRUCTION) {
-                                args.push('EF_CONSTRUCTION', fieldOptions.EF_CONSTRUCTION.toString());
-                            }
-
-                            if (fieldOptions.EF_RUNTIME) {
-                                args.push('EF_RUNTIME', fieldOptions.EF_RUNTIME.toString());
-                            }
-
-                            break;
-                    }
-                });
-
-                break;
-
-            case SchemaFieldTypes.GEOSHAPE:
-                if (fieldOptions.COORD_SYSTEM !== undefined) {
-                    args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM);
-                }
-
-                pushCommonFieldArguments(args, fieldOptions);
-
-                break;
-        }
-
-        if (fieldOptions.INDEXMISSING) {
-            args.push('INDEXMISSING');
-        }
-    }
-}
-
-export type Params = Record<string, RedisCommandArgument | number>;
-
-export function pushParamsArgs(
-    args: RedisCommandArguments,
-    params?: Params
-): RedisCommandArguments {
-    if (params) {
-        const enrties = Object.entries(params);
-        args.push('PARAMS', (enrties.length * 2).toString());
-        for (const [key, value] of enrties) {
-            args.push(key, typeof value === 'number' ? value.toString() : value);
-        }
-    }
-
-    return args;
-}
-
-export function pushSearchOptions(
-    args: RedisCommandArguments,
-    options?: SearchOptions
-): RedisCommandArguments {
-    if (options?.VERBATIM) {
-        args.push('VERBATIM');
-    }
-
-    if (options?.NOSTOPWORDS) {
-        args.push('NOSTOPWORDS');
-    }
-
-    // if (options?.WITHSCORES) {
-    //     args.push('WITHSCORES');
-    // }
-
-    // if (options?.WITHPAYLOADS) {
-    //     args.push('WITHPAYLOADS');
-    // }
-
-    pushOptionalVerdictArgument(args, 'INKEYS', options?.INKEYS);
-    pushOptionalVerdictArgument(args, 'INFIELDS', options?.INFIELDS);
-    pushOptionalVerdictArgument(args, 'RETURN', options?.RETURN);
-
-    if (options?.SUMMARIZE) {
-        args.push('SUMMARIZE');
-
-        if (typeof options.SUMMARIZE === 'object') {
-            if (options.SUMMARIZE.FIELDS) {
-                args.push('FIELDS');
-                pushVerdictArgument(args, options.SUMMARIZE.FIELDS);
-            }
-
-            if (options.SUMMARIZE.FRAGS) {
-                args.push('FRAGS', options.SUMMARIZE.FRAGS.toString());
-            }
-
-            if (options.SUMMARIZE.LEN) {
-                args.push('LEN', options.SUMMARIZE.LEN.toString());
-            }
-
-            if (options.SUMMARIZE.SEPARATOR) {
-                args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR);
-            }
-        }
-    }
-
-    if (options?.HIGHLIGHT) {
-        args.push('HIGHLIGHT');
-
-        if (typeof options.HIGHLIGHT === 'object') {
-            if (options.HIGHLIGHT.FIELDS) {
-                args.push('FIELDS');
-                pushVerdictArgument(args, options.HIGHLIGHT.FIELDS);
-            }
-
-            if (options.HIGHLIGHT.TAGS) {
-                args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close);
-            }
-        }
-    }
-
-    if (options?.SLOP) {
-        args.push('SLOP', options.SLOP.toString());
-    }
-
-    if (options?.INORDER) {
-        args.push('INORDER');
-    }
-
-    if (options?.LANGUAGE) {
-        args.push('LANGUAGE', options.LANGUAGE);
-    }
-
-    if (options?.EXPANDER) {
-        args.push('EXPANDER', options.EXPANDER);
-    }
-
-    if (options?.SCORER) {
-        args.push('SCORER', options.SCORER);
-    }
-
-    // if (options?.EXPLAINSCORE) {
-    //     args.push('EXPLAINSCORE');
-    // }
-
-    // if (options?.PAYLOAD) {
-    //     args.push('PAYLOAD', options.PAYLOAD);
-    // }
-
-    if (options?.SORTBY) {
-        args.push('SORTBY');
-        pushSortByProperty(args, options.SORTBY);
-    }
-
-    // if (options?.MSORTBY) {
-    //     pushSortByArguments(args, 'MSORTBY', options.MSORTBY);
-    // }
-
-    if (options?.LIMIT) {
-        args.push(
-            'LIMIT',
-            options.LIMIT.from.toString(),
-            options.LIMIT.size.toString()
-        );
-    }
-
-    if (options?.PARAMS) {
-        pushParamsArgs(args, options.PARAMS);
-    }
-
-    if (options?.DIALECT) {
-        args.push('DIALECT', options.DIALECT.toString());
-    }
-
-    if (options?.RETURN?.length === 0) {
-        args.preserve = true;
-    }
-
-    if (options?.TIMEOUT !== undefined) {
-        args.push('TIMEOUT', options.TIMEOUT.toString());
-    }
-
-    return args;
-}
-
-interface SearchDocumentValue {
-    [key: string]: string | number | null | Array<SearchDocumentValue> | SearchDocumentValue;
-}
-
-export interface SearchReply {
-    total: number;
-    documents: Array<{
-        id: string;
-        value: SearchDocumentValue;
-    }>;
-}
-
-export interface ProfileOptions {
-    LIMITED?: true;
-}
-
-export type ProfileRawReply<T> = [
-    results: T,
-    profile: [
-        _: string,
-        TotalProfileTime: string,
-        _: string,
-        ParsingTime: string,
-        _: string,
-        PipelineCreationTime: string,
-        _: string,
-        IteratorsProfile: Array<any>
-    ]
-];
-
-export interface ProfileReply {
-    results: SearchReply | AGGREGATE.AggregateReply;
-    profile: ProfileData;
-}
-
-interface ChildIterator {
-    type?: string,
-    counter?: number,
-    term?: string,
-    size?: number,
-    time?: string,
-    childIterators?: Array<ChildIterator>
-}
-
-interface IteratorsProfile {
-    type?: string,
-    counter?: number,
-    queryType?: string,
-    time?: string,
-    childIterators?: Array<ChildIterator>
-}
-
-interface ProfileData {
-    totalProfileTime: string,
-    parsingTime: string,
-    pipelineCreationTime: string,
-    iteratorsProfile: IteratorsProfile
-}
-
-export function transformProfile(reply: Array<any>): ProfileData{
-    return {
-        totalProfileTime: reply[0][1],
-        parsingTime: reply[1][1],
-        pipelineCreationTime: reply[2][1],
-        iteratorsProfile: transformIterators(reply[3][1])
-    };
-}
-
-function transformIterators(IteratorsProfile: Array<any>): IteratorsProfile {
-    var res: IteratorsProfile = {};
-    for (let i = 0; i < IteratorsProfile.length; i += 2) {
-        const value = IteratorsProfile[i+1];
-        switch (IteratorsProfile[i]) {
-            case 'Type':
-                res.type = value;
-                break;
-            case 'Counter':
-                res.counter = value;
-                break;
-            case 'Time':
-                res.time = value;
-                break;
-            case 'Query type':
-                res.queryType = value;
-                break;
-            case 'Child iterators':
-                res.childIterators = value.map(transformChildIterators);
-                break;
-        }
-    }
-
-    return res;
-}
-
-function transformChildIterators(IteratorsProfile: Array<any>): ChildIterator {
-    var res: ChildIterator = {};
-    for (let i = 1; i < IteratorsProfile.length; i += 2) {
-        const value = IteratorsProfile[i+1];
-        switch (IteratorsProfile[i]) {
-            case 'Type':
-                res.type = value;
-                break;
-            case 'Counter':
-                res.counter = value;
-                break;
-            case 'Time':
-                res.time = value;
-                break;
-            case 'Size':
-                res.size = value;
-                break;
-            case 'Term':
-                res.term = value;
-                break;
-            case 'Child iterators':
-                res.childIterators = value.map(transformChildIterators);
-                break;
-        }
-    }
-
-    return res;
-}
diff --git a/packages/search/lib/dialect/default.ts b/packages/search/lib/dialect/default.ts
new file mode 100644
index 00000000000..54cde05d119
--- /dev/null
+++ b/packages/search/lib/dialect/default.ts
@@ -0,0 +1 @@
+export const DEFAULT_DIALECT = '2';
diff --git a/packages/search/lib/index.ts b/packages/search/lib/index.ts
index 0f84c11466f..9bcfb91b956 100644
--- a/packages/search/lib/index.ts
+++ b/packages/search/lib/index.ts
@@ -1,5 +1,21 @@
-export { default } from './commands';
+export { default } from './commands'
 
-export { RediSearchSchema, RedisSearchLanguages, SchemaFieldTypes, SchemaTextFieldPhonetics, SearchReply, VectorAlgorithms } from './commands';
-export { AggregateGroupByReducers, AggregateSteps } from './commands/AGGREGATE';
-export { SearchOptions } from './commands/SEARCH';
+export { SearchReply } from './commands/SEARCH'
+export { RediSearchSchema } from './commands/CREATE'
+export {
+    REDISEARCH_LANGUAGE,
+    RediSearchLanguage,
+    SCHEMA_FIELD_TYPE,
+    SchemaFieldType,
+    SCHEMA_TEXT_FIELD_PHONETIC,
+    SchemaTextFieldPhonetic,
+    SCHEMA_VECTOR_FIELD_ALGORITHM,
+    SchemaVectorFieldAlgorithm
+} from './commands/CREATE'
+export {
+    FT_AGGREGATE_GROUP_BY_REDUCERS,
+    FtAggregateGroupByReducer,
+    FT_AGGREGATE_STEPS,
+    FtAggregateStep
+} from './commands/AGGREGATE'
+export { FtSearchOptions } from './commands/SEARCH'
diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts
index 9e0af209103..d4d91307b99 100644
--- a/packages/search/lib/test-utils.ts
+++ b/packages/search/lib/test-utils.ts
@@ -1,21 +1,32 @@
 import TestUtils from '@redis/test-utils';
 import RediSearch from '.';
+import { RespVersions } from '@redis/client';
 
-export default new TestUtils({
-    dockerImageName: 'redislabs/redisearch',
-    dockerImageVersionArgument: 'redisearch-version',
-    defaultDockerVersion: '2.4.9'
+export default TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '8.2-rc1'
 });
 
 export const GLOBAL = {
-    SERVERS: {
-        OPEN: {
-            serverArguments: ['--loadmodule /usr/lib/redis/modules/redisearch.so'],
-            clientOptions: {
-                modules: {
-                    ft: RediSearch
-                }
-            }
+  SERVERS: {
+    OPEN: {
+      serverArguments: [],
+      clientOptions: {
+        modules: {
+          ft: RediSearch
         }
+      }
+    },
+    OPEN_3: {
+      serverArguments: [],
+      clientOptions: {
+        RESP: 3 as RespVersions,
+        unstableResp3:true,
+        modules: {
+          ft: RediSearch
+        }
+      }
     }
+  }
 };
diff --git a/packages/search/package.json b/packages/search/package.json
index aaf9bc50f11..5d7fffcfd26 100644
--- a/packages/search/package.json
+++ b/packages/search/package.json
@@ -1,30 +1,26 @@
 {
   "name": "@redis/search",
-  "version": "1.2.0",
+  "version": "5.7.0",
   "license": "MIT",
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
+  "main": "./dist/lib/index.js",
+  "types": "./dist/lib/index.d.ts",
   "files": [
-    "dist/"
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
   ],
   "scripts": {
-    "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
-    "build": "tsc",
-    "documentation": "typedoc"
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
+    "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'",
+    "release": "release-it"
   },
   "peerDependencies": {
-    "@redis/client": "^1.0.0"
+    "@redis/client": "^5.7.0"
   },
   "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
-    "@redis/test-utils": "*",
-    "@types/node": "^20.6.2",
-    "nyc": "^15.1.0",
-    "release-it": "^16.1.5",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typedoc": "^0.25.1",
-    "typescript": "^5.2.2"
+    "@redis/test-utils": "*"
+  },
+  "engines": {
+    "node": ">= 18"
   },
   "repository": {
     "type": "git",
diff --git a/packages/test-utils/lib/cae-client-testing.ts b/packages/test-utils/lib/cae-client-testing.ts
new file mode 100644
index 00000000000..92b846dd37e
--- /dev/null
+++ b/packages/test-utils/lib/cae-client-testing.ts
@@ -0,0 +1,30 @@
+import { readFile } from 'node:fs/promises';
+
+interface RawRedisEndpoint {
+  username?: string;
+  password?: string;
+  tls: boolean;
+  endpoints: string[];
+}
+
+export type RedisEndpointsConfig = Record<string, RawRedisEndpoint>;
+
+export function loadFromJson(jsonString: string): RedisEndpointsConfig {
+  try {
+    return JSON.parse(jsonString) as RedisEndpointsConfig;
+  } catch (error) {
+    throw new Error(`Invalid JSON configuration: ${error}`);
+  }
+}
+
+export async function loadFromFile(path: string): Promise<RedisEndpointsConfig> {
+  try {
+    const configFile = await readFile(path, 'utf-8');
+    return loadFromJson(configFile);
+  } catch (error) {
+    if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
+      throw new Error(`Config file not found at path: ${path}`);
+    }
+    throw error;
+  }
+}
\ No newline at end of file
diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts
index a7e1c610eee..47257964f6a 100644
--- a/packages/test-utils/lib/dockers.ts
+++ b/packages/test-utils/lib/dockers.ts
@@ -1,260 +1,447 @@
-import { createConnection } from 'net';
-import { once } from 'events';
-import RedisClient from '@redis/client/dist/lib/client';
-import { promiseTimeout } from '@redis/client/dist/lib/utils';
-import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS';
-import * as path from 'path';
-import { promisify } from 'util';
-import { exec } from 'child_process';
-const execAsync = promisify(exec);
+import { RedisClusterClientOptions } from '@redis/client/dist/lib/cluster';
+import { createConnection } from 'node:net';
+import { once } from 'node:events';
+import { createClient } from '@redis/client/index';
+import { setTimeout } from 'node:timers/promises';
+// import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS';
+import { execFile as execFileCallback } from 'node:child_process';
+import { promisify } from 'node:util';
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as path from 'node:path';
+
+const execAsync = promisify(execFileCallback);
 
 interface ErrorWithCode extends Error {
-    code: string;
+  code: string;
 }
 
 async function isPortAvailable(port: number): Promise<boolean> {
-    try {
-        const socket = createConnection({ port });
-        await once(socket, 'connect');
-        socket.end();
-    } catch (err) {
-        if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
-            return true;
-        }
+  try {
+    const socket = createConnection({ port });
+    await once(socket, 'connect');
+    socket.end();
+  } catch (err) {
+    if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
+      return true;
     }
+  }
 
-    return false;
+  return false;
 }
 
-const portIterator = (async function*(): AsyncIterableIterator<number> {
-    for (let i = 6379; i < 65535; i++) {
-        if (await isPortAvailable(i)) {
-            yield i;
-        }
+const portIterator = (async function* (): AsyncIterableIterator<number> {
+  for (let i = 6379; i < 65535; i++) {
+    if (await isPortAvailable(i)) {
+      yield i;
     }
+  }
 
-    throw new Error('All ports are in use');
+  throw new Error('All ports are in use');
 })();
 
-export interface RedisServerDockerConfig {
-    image: string;
-    version: string;
+interface RedisServerDockerConfig {
+  image: string;
+  version: string;
 }
 
-export interface RedisServerDocker {
-    port: number;
-    dockerId: string;
+interface SentinelConfig {
+  mode: "sentinel";
+  mounts: Array<string>;
+  port: number;
 }
 
-// ".." cause it'll be in `./dist`
-const DOCKER_FODLER_PATH = path.join(__dirname, '../docker');
-
-async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
-    const port = (await portIterator.next()).value,
-        { stdout, stderr } = await execAsync(
-            'docker run -d --network host $(' +
-                `docker build ${DOCKER_FODLER_PATH} -q ` +
-                `--build-arg IMAGE=${image}:${version} ` +
-                `--build-arg REDIS_ARGUMENTS="--save '' --port ${port.toString()} ${serverArguments.join(' ')}"` +
-            ')'
-        );
+interface ServerConfig {
+  mode: "server";
+}
 
-    if (!stdout) {
-        throw new Error(`docker run error - ${stderr}`);
-    }
+export type RedisServerDockerOptions = RedisServerDockerConfig & (SentinelConfig | ServerConfig)
+
+export interface RedisServerDocker {
+  port: number;
+  dockerId: string;
+}
 
-    while (await isPortAvailable(port)) {
-        await promiseTimeout(50);
+export async function spawnRedisServerDocker(
+options: RedisServerDockerOptions, serverArguments: Array<string>): Promise<RedisServerDocker> {
+  let port;
+  if (options.mode == "sentinel") {
+    port = options.port;
+  } else {
+    port = (await portIterator.next()).value;
+  }
+
+  const portStr = port.toString();
+
+  const dockerArgs = [
+    'run',
+    '--init',
+    '-e', `PORT=${portStr}`
+  ];
+
+  if (options.mode == "sentinel") {
+    options.mounts.forEach(mount => {
+      dockerArgs.push('-v', mount);
+    });
+  }
+
+  dockerArgs.push(
+    '-d',
+    '--network', 'host',
+    `${options.image}:${options.version}`
+  );
+
+  if (serverArguments.length > 0) {
+    for (let i = 0; i < serverArguments.length; i++) {
+      dockerArgs.push(serverArguments[i])
     }
+  }
 
-    return {
-        port,
-        dockerId: stdout.trim()
-    };
-}
+  console.log(`[Docker] Spawning Redis container - Image: ${options.image}:${options.version}, Port: ${port}, Mode: ${options.mode}`);
+
+  const { stdout, stderr } = await execAsync('docker', dockerArgs);
+
+  if (!stdout) {
+    throw new Error(`docker run error - ${stderr}`);
+  }
 
+  while (await isPortAvailable(port)) {
+    await setTimeout(50);
+  }
+
+  return {
+    port,
+    dockerId: stdout.trim()
+  };
+}
 const RUNNING_SERVERS = new Map<Array<string>, ReturnType<typeof spawnRedisServerDocker>>();
 
-export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
-    const runningServer = RUNNING_SERVERS.get(serverArguments);
-    if (runningServer) {
-        return runningServer;
-    }
+export function spawnRedisServer(dockerConfig: RedisServerDockerOptions, serverArguments: Array<string>): Promise<RedisServerDocker> {
+  const runningServer = RUNNING_SERVERS.get(serverArguments);
+  if (runningServer) {
+    return runningServer;
+  }
 
-    const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments);
-    RUNNING_SERVERS.set(serverArguments, dockerPromise);
-    return dockerPromise;
+  const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments);
+  RUNNING_SERVERS.set(serverArguments, dockerPromise);
+  return dockerPromise;
 }
 
 async function dockerRemove(dockerId: string): Promise<void> {
-    const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
-    if (stderr) {
-        throw new Error(`docker rm error - ${stderr}`);
-    }
+  const { stderr } = await execAsync('docker', ['rm', '-f', dockerId]);
+  if (stderr) {
+    throw new Error(`docker rm error - ${stderr}`);
+  }
 }
 
 after(() => {
-    return Promise.all(
-        [...RUNNING_SERVERS.values()].map(async dockerPromise =>
-            await dockerRemove((await dockerPromise).dockerId)
-        )
-    );
+  return Promise.all(
+    [...RUNNING_SERVERS.values()].map(async dockerPromise =>
+      await dockerRemove((await dockerPromise).dockerId)
+    )
+  );
 });
 
-export interface RedisClusterDockersConfig extends RedisServerDockerConfig {
-    numberOfMasters?: number;
-    numberOfReplicas?: number;
+export type RedisClusterDockersConfig = RedisServerDockerOptions & {
+  numberOfMasters?: number;
+  numberOfReplicas?: number;
 }
 
 async function spawnRedisClusterNodeDockers(
-    dockersConfig: RedisClusterDockersConfig,
-    serverArguments: Array<string>,
-    fromSlot: number,
-    toSlot: number
+  dockersConfig: RedisClusterDockersConfig,
+  serverArguments: Array<string>,
+  fromSlot: number,
+  toSlot: number,
+  clientConfig?: Partial<RedisClusterClientOptions>
 ) {
-    const range: Array<number> = [];
-    for (let i = fromSlot; i < toSlot; i++) {
-        range.push(i);
-    }
-
-    const master = await spawnRedisClusterNodeDocker(
-        dockersConfig,
-        serverArguments
-    );
+  const range: Array<number> = [];
+  for (let i = fromSlot; i < toSlot; i++) {
+    range.push(i);
+  }
+
+  const master = await spawnRedisClusterNodeDocker(
+    dockersConfig,
+    serverArguments,
+    clientConfig
+  );
+
+  await master.client.clusterAddSlots(range);
+
+  if (!dockersConfig.numberOfReplicas) return [master];
+
+  const replicasPromises: Array<ReturnType<typeof spawnRedisClusterNodeDocker>> = [];
+  for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) {
+    replicasPromises.push(
+      spawnRedisClusterNodeDocker(dockersConfig, [
+        ...serverArguments,
+        '--cluster-enabled',
+        'yes',
+        '--cluster-node-timeout',
+        '5000'
+      ], clientConfig).then(async replica => {
+
+        const requirePassIndex = serverArguments.findIndex((x) => x === '--requirepass');
+        if (requirePassIndex !== -1) {
+          const password = serverArguments[requirePassIndex + 1];
+          await replica.client.configSet({ 'masterauth': password })
+        }
+        await replica.client.clusterMeet('127.0.0.1', master.docker.port);
 
-    await master.client.clusterAddSlots(range);
+        while ((await replica.client.clusterSlots()).length === 0) {
+          await setTimeout(25);
+        }
 
-    if (!dockersConfig.numberOfReplicas) return [master];
-    
-    const replicasPromises: Array<ReturnType<typeof spawnRedisClusterNodeDocker>> = [];
-    for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) {
-        replicasPromises.push(
-            spawnRedisClusterNodeDocker(dockersConfig, [
-                ...serverArguments,
-                '--cluster-enabled',
-                'yes',
-                '--cluster-node-timeout',
-                '5000'
-            ]).then(async replica => {
-                await replica.client.clusterMeet('127.0.0.1', master.docker.port);
-
-                while ((await replica.client.clusterSlots()).length === 0) {
-                    await promiseTimeout(50);
-                }
-
-                await replica.client.clusterReplicate(
-                    await master.client.clusterMyId()
-                );
-
-                return replica;
-            })
+        await replica.client.clusterReplicate(
+          await master.client.clusterMyId()
         );
-    }
 
-    return [
-        master,
-        ...await Promise.all(replicasPromises)
-    ];
+        return replica;
+      })
+    );
+  }
+
+  return [
+    master,
+    ...await Promise.all(replicasPromises)
+  ];
 }
 
 async function spawnRedisClusterNodeDocker(
-    dockersConfig: RedisClusterDockersConfig,
-    serverArguments: Array<string>
+  dockersConfig: RedisServerDockerOptions,
+  serverArguments: Array<string>,
+  clientConfig?: Partial<RedisClusterClientOptions>
 ) {
-    const docker = await spawnRedisServerDocker(dockersConfig, [
-            ...serverArguments,
-            '--cluster-enabled',
-            'yes',
-            '--cluster-node-timeout',
-            '5000'
-        ]),
-        client = RedisClient.create({
-            socket: {
-                port: docker.port
-            }
-        });
-
-    await client.connect();
-
-    return {
-        docker,
-        client
-    };
+  const docker = await spawnRedisServerDocker(dockersConfig, [
+      ...serverArguments,
+      '--cluster-enabled',
+      'yes',
+      '--cluster-node-timeout',
+      '5000'
+    ]),
+    client = createClient({
+      socket: {
+        port: docker.port
+      },
+      ...clientConfig
+    });
+
+  await client.connect();
+
+  return {
+    docker,
+    client
+  };
 }
 
 const SLOTS = 16384;
 
 async function spawnRedisClusterDockers(
-    dockersConfig: RedisClusterDockersConfig,
-    serverArguments: Array<string>
+  dockersConfig: RedisClusterDockersConfig,
+  serverArguments: Array<string>,
+  clientConfig?: Partial<RedisClusterClientOptions>
 ): Promise<Array<RedisServerDocker>> {
-    const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
-        slotsPerNode = Math.floor(SLOTS / numberOfMasters),
-        spawnPromises: Array<ReturnType<typeof spawnRedisClusterNodeDockers>> = [];
-    for (let i = 0; i < numberOfMasters; i++) {
-        const fromSlot = i * slotsPerNode,
-            toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode;
-        spawnPromises.push(
-            spawnRedisClusterNodeDockers(
-                dockersConfig,
-                serverArguments,
-                fromSlot,
-                toSlot
-            )
-        );
-    }
+  const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
+    slotsPerNode = Math.floor(SLOTS / numberOfMasters),
+    spawnPromises: Array<ReturnType<typeof spawnRedisClusterNodeDockers>> = [];
+  for (let i = 0; i < numberOfMasters; i++) {
+    const fromSlot = i * slotsPerNode,
+      toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode;
+    spawnPromises.push(
+      spawnRedisClusterNodeDockers(
+        dockersConfig,
+        serverArguments,
+        fromSlot,
+        toSlot,
+        clientConfig
+      )
+    );
+  }
 
-    const nodes = (await Promise.all(spawnPromises)).flat(),
-        meetPromises: Array<Promise<unknown>> = [];
-    for (let i = 1; i < nodes.length; i++) {
-        meetPromises.push(
-            nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port)
-        );
-    }
+  const nodes = (await Promise.all(spawnPromises)).flat(),
+    meetPromises: Array<Promise<unknown>> = [];
+  for (let i = 1; i < nodes.length; i++) {
+    meetPromises.push(
+      nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port)
+    );
+  }
 
-    await Promise.all(meetPromises);
+  await Promise.all(meetPromises);
 
-    await Promise.all(
-        nodes.map(async ({ client }) => {
-            while (totalNodes(await client.clusterSlots()) !== nodes.length) {
-                await promiseTimeout(50);
-            }
-        
-            return client.disconnect();
-        })
-    );
+  await Promise.all(
+    nodes.map(async ({ client }) => {
+      while (
+        totalNodes(await client.clusterSlots()) !== nodes.length ||
+        !(await client.sendCommand<string>(['CLUSTER', 'INFO'])).startsWith('cluster_state:ok') // TODO
+        ) {
+        await setTimeout(50);
+      }
 
-    return nodes.map(({ docker }) => docker);
+      client.destroy();
+    })
+  );
+
+  return nodes.map(({ docker }) => docker);
 }
 
-function totalNodes(slots: ClusterSlotsReply) {
-    let total = slots.length;
-    for (const slot of slots) {
-        total += slot.replicas.length;
-    }
+// TODO: type ClusterSlotsReply
+function totalNodes(slots: any) {
+  let total = slots.length;
+  for (const slot of slots) {
+    total += slot.replicas.length;
+  }
 
-    return total;
+  return total;
 }
 
 const RUNNING_CLUSTERS = new Map<Array<string>, ReturnType<typeof spawnRedisClusterDockers>>();
 
-export function spawnRedisCluster(dockersConfig: RedisClusterDockersConfig, serverArguments: Array<string>): Promise<Array<RedisServerDocker>> {
-    const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
-    if (runningCluster) {
-        return runningCluster;
+export function spawnRedisCluster(
+  dockersConfig: RedisClusterDockersConfig,
+  serverArguments: Array<string>,
+  clientConfig?: Partial<RedisClusterClientOptions>): Promise<Array<RedisServerDocker>> {
+
+  const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
+  if (runningCluster) {
+    return runningCluster;
+  }
+
+  const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments, clientConfig);
+
+  RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
+  return dockersPromise;
+}
+
+after(() => {
+  return Promise.all(
+    [...RUNNING_CLUSTERS.values()].map(async dockersPromise => {
+      return Promise.all(
+        (await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId))
+      );
+    })
+  );
+});
+
+
+const RUNNING_NODES = new Map<Array<string>, Array<RedisServerDocker>>();
+const RUNNING_SENTINELS = new Map<Array<string>, Array<RedisServerDocker>>();
+
+export async function spawnRedisSentinel(
+  dockerConfigs: RedisServerDockerOptions,
+  serverArguments: Array<string>,
+): Promise<Array<RedisServerDocker>> {
+    const runningNodes = RUNNING_SENTINELS.get(serverArguments);
+    if (runningNodes) {
+      return runningNodes;
+    }
+    
+    const passIndex = serverArguments.indexOf('--requirepass')+1;
+    let password: string | undefined = undefined;
+    if (passIndex != 0) {
+      password = serverArguments[passIndex];
+    }
+
+    const master = await spawnRedisServerDocker(dockerConfigs, serverArguments);
+    const redisNodes: Array<RedisServerDocker> = [master];
+    const replicaPromises: Array<Promise<RedisServerDocker>> = [];
+    
+    const replicasCount = 2;
+    for (let i = 0; i < replicasCount; i++) {
+      replicaPromises.push((async () => {
+        const replica = await spawnRedisServerDocker(dockerConfigs, serverArguments);
+        const client = createClient({
+          socket: {
+            port: replica.port
+          },
+          password: password
+        });
+
+        await client.connect();
+        await client.replicaOf("127.0.0.1", master.port);
+        await client.close();
+        
+        return replica;
+      })());
+    }
+    
+    const replicas = await Promise.all(replicaPromises);
+    redisNodes.push(...replicas);
+    RUNNING_NODES.set(serverArguments, redisNodes);
+
+    const sentinelPromises: Array<Promise<RedisServerDocker>> = [];
+    const sentinelCount = 3;
+    
+    const appPrefix = 'sentinel-config-dir';
+    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix));
+
+    for (let i = 0; i < sentinelCount; i++) {
+      sentinelPromises.push(
+        spawnSentinelNode(
+          dockerConfigs, 
+          serverArguments, 
+          master.port,
+          "mymaster",
+          path.join(tmpDir, i.toString()),
+          password,
+        ),
+      )
+    }
+    
+    const sentinelNodes = await Promise.all(sentinelPromises);
+    RUNNING_SENTINELS.set(serverArguments, sentinelNodes);
+    
+    if (tmpDir) {
+      fs.rmSync(tmpDir, { recursive: true });
     }
 
-    const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments);
-    RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
-    return dockersPromise;
+    return sentinelNodes;
 }
 
 after(() => {
-    return Promise.all(
-        [...RUNNING_CLUSTERS.values()].map(async dockersPromise => {
-            return Promise.all(
-                (await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId))
-            );
-        })
-    );
+  return Promise.all(
+    [...RUNNING_NODES.values(), ...RUNNING_SENTINELS.values()].map(async dockersPromise => {
+      return Promise.all(
+        dockersPromise.map(({ dockerId }) => dockerRemove(dockerId))
+      );
+    })
+  );
 });
+
+
+export async function spawnSentinelNode(
+  dockerConfigs: RedisServerDockerOptions,
+  serverArguments: Array<string>,
+  masterPort: number, 
+  sentinelName: string,
+  tmpDir: string,
+  password?: string,
+) {
+  const port = (await portIterator.next()).value;
+
+  let sentinelConfig = `port ${port}
+sentinel monitor ${sentinelName} 127.0.0.1 ${masterPort} 2
+sentinel down-after-milliseconds ${sentinelName} 500
+sentinel failover-timeout ${sentinelName} 1000
+`;
+  if (password !== undefined) {
+    sentinelConfig += `requirepass ${password}\n`;
+    sentinelConfig += `sentinel auth-pass ${sentinelName} ${password}\n`;
+  }
+
+  const dir = fs.mkdtempSync(tmpDir);
+  fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => {
+    if (err) {
+      console.error("failed to create temporary config file", err);
+    }
+  });
+
+  return await spawnRedisServerDocker(
+    {
+      image: dockerConfigs.image, 
+      version: dockerConfigs.version, 
+      mode: "sentinel",
+        mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`], 
+        port: port,
+    }, 
+    serverArguments,
+  );
+}
\ No newline at end of file
diff --git a/packages/test-utils/lib/index.spec.ts b/packages/test-utils/lib/index.spec.ts
new file mode 100644
index 00000000000..0f1e7552284
--- /dev/null
+++ b/packages/test-utils/lib/index.spec.ts
@@ -0,0 +1,106 @@
+import { strict as assert } from 'node:assert';
+import TestUtils from './index';
+
+describe('TestUtils', () => {
+  describe('parseVersionNumber', () => {
+    it('should handle special versions', () => {
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('latest'), [Infinity]);
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('edge'), [Infinity]);
+    });
+
+    it('should parse simple version numbers', () => {
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('7.4.0'), [7, 4, 0]);
+    });
+
+    it('should handle versions with multiple dashes and prefixes', () => {
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-7.4.0-v2'), [7, 4, 0]);
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-7.4.0'), [7, 4, 0]);
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('7.4.0-v2'), [7, 4, 0]);
+    });
+
+    it('should handle various version number formats', () => {
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('10.5'), [10, 5]);
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('8.0.0'), [8, 0, 0]);
+      assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-6.2.4-v1'), [6, 2, 4]);
+    });
+
+    it('should throw TypeError for invalid version strings', () => {
+      ['', 'invalid', 'rs-', 'v2', 'rs-invalid-v2'].forEach(version => {
+        assert.throws(
+          () => TestUtils.parseVersionNumber(version),
+          TypeError,
+          `Expected TypeError for version string: ${version}`
+        );
+      });
+    });
+  });
+});
+
+
+
+describe('Version Comparison', () => {
+  it('should correctly compare versions', () => {
+    const tests: [Array<number>, Array<number>, -1 | 0 | 1][] = [
+      [[1, 0, 0], [1, 0, 0], 0],    
+      [[2, 0, 0], [1, 9, 9], 1],   
+      [[1, 9, 9], [2, 0, 0], -1],  
+      [[1, 2, 3], [1, 2], 1],      
+      [[1, 2], [1, 2, 3], -1],      
+      [[1, 2, 0], [1, 2, 1], -1],   
+      [[1], [1, 0, 0], 0],         
+      [[2], [1, 9, 9], 1],          
+    ];
+
+    tests.forEach(([a, b, expected]) => {
+
+      assert.equal(
+        TestUtils.compareVersions(a, b),
+        expected,
+        `Failed comparing ${a.join('.')} with ${b.join('.')}: expected ${expected}`
+      );
+    });
+  });
+
+  it('should correctly compare versions', () => {
+    const tests: [Array<number>, Array<number>, -1 | 0 | 1][] = [
+      [[1, 0, 0], [1, 0, 0], 0],
+      [[2, 0, 0], [1, 9, 9], 1],
+      [[1, 9, 9], [2, 0, 0], -1],
+      [[1, 2, 3], [1, 2], 1],
+      [[1, 2], [1, 2, 3], -1],
+      [[1, 2, 0], [1, 2, 1], -1],
+      [[1], [1, 0, 0], 0],
+      [[2], [1, 9, 9], 1],
+    ];
+
+    tests.forEach(([a, b, expected]) => {
+
+      assert.equal(
+        TestUtils.compareVersions(a, b),
+        expected,
+        `Failed comparing ${a.join('.')} with ${b.join('.')}: expected ${expected}`
+      );
+    });
+  })
+  it('isVersionInRange should work correctly', () => {
+    const tests: [Array<number>, Array<number>, Array<number>, boolean][] = [
+      [[7, 0, 0], [7, 0, 0], [7, 0, 0], true],
+      [[7, 0, 1], [7, 0, 0], [7, 0, 2], true], 
+      [[7, 0, 0], [7, 0, 1], [7, 0, 2], false],
+      [[7, 0, 3], [7, 0, 1], [7, 0, 2], false],
+      [[7], [6, 0, 0], [8, 0, 0], true],
+      [[7, 1, 1], [7, 1, 0], [7, 1, 2], true],
+      [[6, 0, 0], [7, 0, 0], [8, 0, 0], false],
+      [[9, 0, 0], [7, 0, 0], [8, 0, 0], false]
+    ];
+
+    tests.forEach(([version, min, max, expected]) => {
+      const testUtils = new TestUtils({ string: version.join('.'), numbers: version }, "test")
+      assert.equal(
+        testUtils.isVersionInRange(min, max),
+        expected,
+        `Failed checking if ${version.join('.')} is between ${min.join('.')} and ${max.join('.')}: expected ${expected}`
+      );
+    });
+  })
+});
diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts
index b9195c5717a..64b9abc7f48 100644
--- a/packages/test-utils/lib/index.ts
+++ b/packages/test-utils/lib/index.ts
@@ -1,226 +1,630 @@
-import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/lib/commands';
-import RedisClient, { RedisClientOptions, RedisClientType } from '@redis/client/lib/client';
-import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@redis/client/lib/cluster';
-import { RedisSocketCommonOptions } from '@redis/client/lib/client/socket';
-import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
+import {
+  RedisModules,
+  RedisFunctions,
+  RedisScripts,
+  RespVersions,
+  TypeMapping,
+  // CommandPolicies,
+  createClient,
+  createSentinel,
+  RedisClientOptions,
+  RedisClientType,
+  RedisSentinelOptions,
+  RedisSentinelType,
+  RedisPoolOptions,
+  RedisClientPoolType,
+  createClientPool,
+  createCluster,
+  RedisClusterOptions,
+  RedisClusterType
+} from '@redis/client/index';
+import { RedisNode } from '@redis/client/lib/sentinel/types'
+import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions, RedisServerDocker, spawnSentinelNode, spawnRedisServerDocker } from './dockers';
 import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as path from 'node:path';
+import { RedisProxy, getFreePortNumber } from './redis-proxy';
+
 interface TestUtilsConfig {
-    dockerImageName: string;
-    dockerImageVersionArgument: string;
-    defaultDockerVersion?: string;
-}
+  /**
+   * The name of the Docker image to use for spawning Redis test instances.
+   * This should be a valid Docker image name that contains a Redis server.
+   *
+   * @example 'redislabs/client-libs-test'
+   */
+  dockerImageName: string;
+
+  /**
+   * The command-line argument name used to specify the Redis version.
+   * This argument can be passed when running tests / GH actions.
+   *
+   * @example
+   * If set to 'redis-version', you can run tests with:
+   * ```bash
+   * npm test -- --redis-version="6.2"
+   * ```
+   */
+  dockerImageVersionArgument: string;
 
+  /**
+   * The default Redis version to use if no version is specified via command-line arguments.
+   * Can be a specific version number (e.g., '6.2'), 'latest', or 'edge'.
+   * If not provided, defaults to 'latest'.
+   *
+   * @optional
+   * @default 'latest'
+   */
+  defaultDockerVersion?: string;
+}
 interface CommonTestOptions {
-    minimumDockerVersion?: Array<number>;
+  serverArguments: Array<string>;
+  minimumDockerVersion?: Array<number>;
+  skipTest?: boolean;
 }
 
 interface ClientTestOptions<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
 > extends CommonTestOptions {
-    serverArguments: Array<string>;
-    clientOptions?: Partial<Omit<RedisClientOptions<M, F, S>, 'socket'> & { socket: RedisSocketCommonOptions }>;
-    disableClientSetup?: boolean;
+  clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
+  disableClientSetup?: boolean;
+}
+
+interface SentinelTestOptions<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> extends CommonTestOptions {
+  sentinelOptions?: Partial<RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>>;
+  clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
+  scripts?: S;
+  functions?: F;
+  modules?: M;
+  disableClientSetup?: boolean;
+  replicaPoolSize?: number;
+  masterPoolSize?: number;
+  reserveClient?: boolean;
+}
+
+interface ClientPoolTestOptions<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+> extends CommonTestOptions {
+  clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
+  poolOptions?: RedisPoolOptions;
 }
 
 interface ClusterTestOptions<
-    M extends RedisModules,
-    F extends RedisFunctions,
-    S extends RedisScripts
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+  // POLICIES extends CommandPolicies
 > extends CommonTestOptions {
-    serverArguments: Array<string>;
-    clusterConfiguration?: Partial<RedisClusterOptions<M, F, S>>;
-    numberOfMasters?: number;
-    numberOfReplicas?: number;
+  clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>;
+  numberOfMasters?: number;
+  numberOfReplicas?: number;
+}
+
+interface AllTestOptions<
+  M extends RedisModules,
+  F extends RedisFunctions,
+  S extends RedisScripts,
+  RESP extends RespVersions,
+  TYPE_MAPPING extends TypeMapping
+  // POLICIES extends CommandPolicies
+> {
+  client: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>;
+  cluster: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
 }
 
 interface Version {
-    string: string;
-    numbers: Array<number>;
+  string: string;
+  numbers: Array<number>;
 }
 
 export default class TestUtils {
-    static #parseVersionNumber(version: string): Array<number> {
-        if (version === 'latest' || version === 'edge') return [Infinity];
-
-        const dashIndex = version.indexOf('-');
-        return (dashIndex === -1 ? version : version.substring(0, dashIndex))
-            .split('.')
-            .map(x => {
-                const value = Number(x);
-                if (Number.isNaN(value)) {
-                    throw new TypeError(`${version} is not a valid redis version`);
-                }
-
-                return value;
-            });
-    }
+  static parseVersionNumber(version: string): Array<number> {
+    if (version === 'latest' || version === 'edge') return [Infinity];
 
-    static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
-        return yargs(hideBin(process.argv))
-            .option(argumentName, {
-                type: 'string',
-                default: defaultVersion
-            })
-            .coerce(argumentName, (version: string) => {
-                return {
-                    string: version,
-                    numbers: TestUtils.#parseVersionNumber(version)
-                };
-            })
-            .demandOption(argumentName)
-            .parseSync()[argumentName];
+
+    // Match complete version number patterns
+    const versionMatch = version.match(/(^|\-)\d+(\.\d+)*($|\-)/);
+    if (!versionMatch) {
+      throw new TypeError(`${version} is not a valid redis version`);
     }
 
-    readonly #VERSION_NUMBERS: Array<number>;
-    readonly #DOCKER_IMAGE: RedisServerDockerConfig;
+    // Extract just the numbers and dots between first and last dash (or start/end)
+    const versionNumbers = versionMatch[0].replace(/^\-|\-$/g, '');
 
-    constructor(config: TestUtilsConfig) {
-        const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
-        this.#VERSION_NUMBERS = numbers;
-        this.#DOCKER_IMAGE = {
-            image: config.dockerImageName,
-            version: string
+    return versionNumbers.split('.').map(x => {
+      const value = Number(x);
+      if (Number.isNaN(value)) {
+        throw new TypeError(`${version} is not a valid redis version`);
+      }
+      return value;
+    });
+  }
+  static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
+    return yargs(hideBin(process.argv))
+      .option(argumentName, {
+        type: 'string',
+        default: defaultVersion
+      })
+      .coerce(argumentName, (version: string) => {
+        return {
+          string: version,
+          numbers: TestUtils.parseVersionNumber(version)
         };
+      })
+      .demandOption(argumentName)
+      .parseSync()[argumentName];
+  }
+
+  readonly #VERSION_NUMBERS: Array<number>;
+  readonly #DOCKER_IMAGE: RedisServerDockerOptions;
+
+  constructor({ string, numbers }: Version, dockerImageName: string) {
+    this.#VERSION_NUMBERS = numbers;
+    this.#DOCKER_IMAGE = {
+      image: dockerImageName,
+      version: string,
+      mode: "server"
+    };
+  }
+
+  /**
+   * Creates a new TestUtils instance from a configuration object.
+   *
+   * @param config - Configuration object containing Docker image and version settings
+   * @param config.dockerImageName - The name of the Docker image to use for tests
+   * @param config.dockerImageVersionArgument - The command-line argument name for specifying Redis version
+   * @param config.defaultDockerVersion - Optional default Redis version if not specified via arguments
+   * @returns A new TestUtils instance configured with the provided settings
+   */
+  public static createFromConfig(config: TestUtilsConfig) {
+    return new TestUtils(
+      TestUtils.#getVersion(config.dockerImageVersionArgument,
+        config.defaultDockerVersion), config.dockerImageName);
+  }
+
+  isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
+    if (minimumVersion === undefined) return true;
+    return TestUtils.compareVersions(this.#VERSION_NUMBERS, minimumVersion) >= 0;
+  }
+
+  isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
+    const isVersionGreaterThanHook = this.isVersionGreaterThan.bind(this);
+    const versionNumber = this.#VERSION_NUMBERS.join('.');
+    const minimumVersionString = minimumVersion?.join('.');
+    before(function () {
+      if (!isVersionGreaterThanHook(minimumVersion)) {
+        console.warn(`TestUtils: Version ${versionNumber} is less than minimum version ${minimumVersionString}, skipping test`);
+        return this.skip();
+      }
+    });
+  }
+
+  isVersionInRange(minVersion: Array<number>, maxVersion: Array<number>): boolean {
+    return TestUtils.compareVersions(this.#VERSION_NUMBERS, minVersion) >= 0 &&
+      TestUtils.compareVersions(this.#VERSION_NUMBERS, maxVersion) <= 0
+  }
+
+  /**
+   * Compares two semantic version arrays and returns:
+   * -1 if version a is less than version b
+   *  0 if version a equals version b
+   *  1 if version a is greater than version b
+   *
+   * @param a First version array
+   * @param b Second version array
+   * @returns -1 | 0 | 1
+   */
+  static compareVersions(a: Array<number>, b: Array<number>): -1 | 0 | 1 {
+    const maxLength = Math.max(a.length, b.length);
+
+    const paddedA = [...a, ...Array(maxLength - a.length).fill(0)];
+    const paddedB = [...b, ...Array(maxLength - b.length).fill(0)];
+
+    for (let i = 0; i < maxLength; i++) {
+      if (paddedA[i] > paddedB[i]) return 1;
+      if (paddedA[i] < paddedB[i]) return -1;
     }
 
-    isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
-        if (minimumVersion === undefined) return true;
+    return 0;
+  }
 
-        const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1;
-        for (let i = 0; i < lastIndex; i++) {
-            if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) {
-                return true;
-            } else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) {
-                return false;
-            }
+  testWithClient<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(
+    title: string,
+    fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
+    options: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>
+  ): void {
+    let dockerPromise: ReturnType<typeof spawnRedisServer>;
+    if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
+      const dockerImage = this.#DOCKER_IMAGE;
+      before(function () {
+        this.timeout(30000);
+
+        dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
+        return dockerPromise;
+      });
+    }
+
+    it(title, async function () {
+      if (options.skipTest) return this.skip();
+      if (!dockerPromise) return this.skip();
+
+      const client = createClient({
+        ...options.clientOptions,
+        socket: {
+          ...options.clientOptions?.socket,
+          port: (await dockerPromise).port
         }
+      });
+
+      if (options.disableClientSetup) {
+        return fn(client);
+      }
+
+      await client.connect();
+
+      try {
+        await client.flushAll();
+        await fn(client);
+      } finally {
+        if (client.isOpen) {
+          await client.flushAll();
+          client.destroy();
+        }
+      }
+    });
+  }
+  testWithProxiedClient(
+    title: string,
+    fn: (proxiedClient: RedisClientType<any, any, any, any, any>, proxy: RedisProxy) => unknown,
+    options: ClientTestOptions<any, any, any, any, any>
+  ) {
+
+    this.testWithClient(title, async (client) => {
+      const freePort = await getFreePortNumber()
+      const socketOptions = client?.options?.socket;
+      const proxy = new RedisProxy({
+        listenHost: '127.0.0.1',
+        listenPort: freePort,
+        //@ts-ignore
+        targetPort: socketOptions.port,
+        //@ts-ignore
+        targetHost: socketOptions.host,
+        enableLogging: true
+      });
+
 
-        return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
+      await proxy.start();
+      const proxyClient = client.duplicate({
+        socket: {
+          port: proxy.config.listenPort,
+          host: proxy.config.listenHost
+        },
+      });
+
+      await proxyClient.connect();
+
+      try {
+        await fn(proxyClient, proxy);
+      } finally {
+        await proxyClient.destroy();
+        await proxy.stop()
+      }
+    }, options);
+  }
+  testWithClientSentinel<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(
+    title: string,
+    fn: (sentinel: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
+    options: SentinelTestOptions<M, F, S, RESP, TYPE_MAPPING>
+  ): void {
+    let dockerPromises: ReturnType<typeof spawnRedisSentinel>;
+
+    const passIndex = options.serverArguments.indexOf('--requirepass')+1;
+    let password: string | undefined = undefined;
+    if (passIndex != 0) {
+      password = options.serverArguments[passIndex];
     }
 
-    isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
-        const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
-        before(function () {
-            if (!isVersionGreaterThan(minimumVersion)) {
-                return this.skip();
-            }
-        });
+    if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
+      const dockerImage = this.#DOCKER_IMAGE;
+      before(function () {
+        this.timeout(30000);
+        dockerPromises = spawnRedisSentinel(dockerImage, options.serverArguments);
+        return dockerPromises;
+      });
     }
 
-    testWithClient<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(
-        title: string,
-        fn: (client: RedisClientType<M, F, S>) => unknown,
-        options: ClientTestOptions<M, F, S>
-    ): void {
-        let dockerPromise: ReturnType<typeof spawnRedisServer>;
-        if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
-            const dockerImage = this.#DOCKER_IMAGE;
-            before(function () {
-                this.timeout(30000);
-
-                dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
-                return dockerPromise;
-            });
-        }
+    it(title, async function () {
+      this.timeout(30000);
+      if (options.skipTest) return this.skip();
+      if (!dockerPromises) return this.skip();
 
-        it(title, async function() {
-            if (!dockerPromise) return this.skip();
 
-            const client = RedisClient.create({
-                ...options?.clientOptions,
-                socket: {
-                    ...options?.clientOptions?.socket,
-                    port: (await dockerPromise).port
-                }
-            });
+      const promises = await dockerPromises;
+      const rootNodes: Array<RedisNode> = promises.map(promise => ({
+        host: "127.0.0.1",
+        port: promise.port
+      }));
 
-            if (options.disableClientSetup) {
-                return fn(client);
-            }
 
-            await client.connect();
+      const sentinel = createSentinel({
+        name: 'mymaster',
+        sentinelRootNodes: rootNodes,
+        nodeClientOptions: {
+          commandOptions: options.clientOptions?.commandOptions,
+          password: password || undefined,
+        },
+        sentinelClientOptions: {
+          password: password || undefined,
+        },
+        replicaPoolSize: options?.replicaPoolSize || 0,
+        scripts: options?.scripts || {},
+        modules: options?.modules || {},
+        functions: options?.functions || {},
+        masterPoolSize: options?.masterPoolSize || undefined,
+        reserveClient: options?.reserveClient || false,
+        ...options?.sentinelOptions
+      }) as RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>;
 
-            try {
-                await client.flushAll();
-                await fn(client);
-            } finally {
-                if (client.isOpen) {
-                    await client.flushAll();
-                    await client.disconnect();
-                }
-            }
-        });
+      if (options.disableClientSetup) {
+        return fn(sentinel);
+      }
+
+      await sentinel.connect();
+
+      try {
+        await sentinel.flushAll();
+        await fn(sentinel);
+      } finally {
+        if (sentinel.isOpen) {
+          await sentinel.flushAll();
+          sentinel.destroy();
+        }
+      }
+    });
+  }
+
+  testWithClientIfVersionWithinRange<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(
+    range: ([minVersion: Array<number>, maxVersion: Array<number>] | [minVersion: Array<number>, 'LATEST']),
+    title: string,
+    fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
+    options: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>
+  ): void {
+
+    if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) {
+      return this.testWithClient(`${title}  [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options)
+    } else {
+      console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`)
     }
+  }
+
+  testWithClienSentineltIfVersionWithinRange<
+  M extends RedisModules = {},
+  F extends RedisFunctions = {},
+  S extends RedisScripts = {},
+  RESP extends RespVersions = 2,
+  TYPE_MAPPING extends TypeMapping = {}
+  >(
+    range: ([minVersion: Array<number>, maxVersion: Array<number>] | [minVersion: Array<number>, 'LATEST']),
+    title: string,
+    fn: (sentinel: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
+    options: SentinelTestOptions<M, F, S, RESP, TYPE_MAPPING>
+  ): void {
 
-    static async #clusterFlushAll<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(cluster: RedisClusterType<M, F, S>): Promise<unknown> {
-        return Promise.all(
-            cluster.masters.map(async ({ client }) => {
-                if (client) {
-                    await (await client).flushAll();
-                }
-            })
-        );
+    if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) {
+      return this.testWithClientSentinel(`${title}  [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options)
+    } else {
+      console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`)
     }
+  }
+
+  testWithClientPool<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+  >(
+    title: string,
+    fn: (client: RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
+    options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>
+  ): void {
+    let dockerPromise: ReturnType<typeof spawnRedisServer>;
+    if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
+      const dockerImage = this.#DOCKER_IMAGE;
+      before(function () {
+        this.timeout(30000);
+
+        dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
+        return dockerPromise;
+      });
+    }
+
+    it(title, async function () {
+      if (options.skipTest) return this.skip();
+      if (!dockerPromise) return this.skip();
 
-    testWithCluster<
-        M extends RedisModules,
-        F extends RedisFunctions,
-        S extends RedisScripts
-    >(
-        title: string,
-        fn: (cluster: RedisClusterType<M, F, S>) => unknown,
-        options: ClusterTestOptions<M, F, S>
-    ): void {
-        let dockersPromise: ReturnType<typeof spawnRedisCluster>;
-        if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
-            const dockerImage = this.#DOCKER_IMAGE;
-            before(function () {
-                this.timeout(30000);
-
-                dockersPromise = spawnRedisCluster({
-                    ...dockerImage,
-                    numberOfMasters: options?.numberOfMasters,
-                    numberOfReplicas: options?.numberOfReplicas 
-                }, options.serverArguments);
-                return dockersPromise;
-            });
+      const pool = createClientPool({
+        ...options.clientOptions,
+        socket: {
+          ...options.clientOptions?.socket,
+          port: (await dockerPromise).port
         }
+      }, options.poolOptions);
 
-        it(title, async function () {
-            if (!dockersPromise) return this.skip();
-
-            const dockers = await dockersPromise,
-                cluster = RedisCluster.create({
-                    rootNodes: dockers.map(({ port }) => ({
-                        socket: {
-                            port
-                        }
-                    })),
-                    minimizeConnections: true,
-                    ...options.clusterConfiguration
-                });
-
-            await cluster.connect();
-
-            try {
-                await TestUtils.#clusterFlushAll(cluster);
-                await fn(cluster);
-            } finally {
-                await TestUtils.#clusterFlushAll(cluster);
-                await cluster.disconnect();
+      await pool.connect();
+
+      try {
+        await pool.flushAll();
+        await fn(pool);
+      } finally {
+        await pool.flushAll();
+        pool.close();
+      }
+    });
+  }
+
+  static async #clusterFlushAll<
+    M extends RedisModules,
+    F extends RedisFunctions,
+    S extends RedisScripts,
+    RESP extends RespVersions,
+    TYPE_MAPPING extends TypeMapping
+    // POLICIES extends CommandPolicies
+  >(cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>): Promise<unknown> {
+    return Promise.all(
+      cluster.masters.map(async master => {
+        if (master.client) {
+          await (await cluster.nodeClient(master)).flushAll();
+        }
+      })
+    );
+  }
+
+  testWithCluster<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+    // POLICIES extends CommandPolicies = {}
+  >(
+    title: string,
+    fn: (cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
+    options: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>
+  ): void {
+    let dockersPromise: ReturnType<typeof spawnRedisCluster>;
+    if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
+      const dockerImage = this.#DOCKER_IMAGE;
+      before(function () {
+        this.timeout(30000);
+
+        dockersPromise = spawnRedisCluster({
+          ...dockerImage,
+          numberOfMasters: options.numberOfMasters,
+          numberOfReplicas: options.numberOfReplicas
+        }, options.serverArguments,
+          options.clusterConfiguration?.defaults);
+        return dockersPromise;
+      });
+    }
+
+    it(title, async function () {
+      if (!dockersPromise) return this.skip();
+
+      const dockers = await dockersPromise,
+        cluster = createCluster({
+          rootNodes: dockers.map(({ port }) => ({
+            socket: {
+              port
             }
+          })),
+          minimizeConnections: true,
+          ...options.clusterConfiguration
         });
+
+      await cluster.connect();
+
+      try {
+        await TestUtils.#clusterFlushAll(cluster);
+        await fn(cluster);
+      } finally {
+        await TestUtils.#clusterFlushAll(cluster);
+        cluster.destroy();
+      }
+    });
+  }
+
+  testAll<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+    // POLICIES extends CommandPolicies = {}
+  >(
+    title: string,
+    fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING> | RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
+    options: AllTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>
+  ) {
+    this.testWithClient(`client.${title}`, fn, options.client);
+    this.testWithCluster(`cluster.${title}`, fn, options.cluster);
+  }
+
+
+  spawnRedisServer<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+    // POLICIES extends CommandPolicies = {}
+  >(
+    options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>
+  ): Promise<RedisServerDocker> {
+    return spawnRedisServerDocker(this.#DOCKER_IMAGE, options.serverArguments)
+  }
+
+  async spawnRedisSentinels<
+    M extends RedisModules = {},
+    F extends RedisFunctions = {},
+    S extends RedisScripts = {},
+    RESP extends RespVersions = 2,
+    TYPE_MAPPING extends TypeMapping = {}
+    // POLICIES extends CommandPolicies = {}
+  >(
+    options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>,
+    masterPort: number,
+    sentinelName: string,
+    count: number
+  ): Promise<Array<RedisServerDocker>> {
+    const sentinels: Array<RedisServerDocker> = [];
+    for (let i = 0; i < count; i++) {
+      const appPrefix = 'sentinel-config-dir';
+      const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix));
+
+      sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir))
+
+      if (tmpDir) {
+        fs.rmSync(tmpDir, { recursive: true });
+      }
     }
+
+    return sentinels
+  }
 }
diff --git a/packages/test-utils/lib/redis-proxy-spec.ts b/packages/test-utils/lib/redis-proxy-spec.ts
new file mode 100644
index 00000000000..89b3b28c35a
--- /dev/null
+++ b/packages/test-utils/lib/redis-proxy-spec.ts
@@ -0,0 +1,111 @@
+import { strict as assert } from 'node:assert';
+import { Buffer } from 'node:buffer';
+import { testUtils, GLOBAL } from './test-utils';
+import { RedisProxy } from './redis-proxy';
+import type { RedisClientType } from '@redis/client/lib/client/index.js';
+
+describe('RedisSocketProxy', function () {
+  testUtils.testWithClient('basic proxy functionality', async (client: RedisClientType<any, any, any, any, any>) => {
+    const socketOptions = client?.options?.socket;
+    //@ts-ignore
+    assert(socketOptions?.port, 'Test requires a TCP connection to Redis');
+
+    const proxyPort = 50000 + Math.floor(Math.random() * 10000);
+    const proxy = new RedisProxy({
+      listenHost: '127.0.0.1',
+      listenPort: proxyPort,
+      //@ts-ignore
+      targetPort: socketOptions.port,
+      //@ts-ignore
+      targetHost: socketOptions.host || '127.0.0.1',
+      enableLogging: true
+    });
+
+    const proxyEvents = {
+      connections: [] as any[],
+      dataTransfers: [] as any[]
+    };
+
+    proxy.on('connection', (connectionInfo) => {
+      proxyEvents.connections.push(connectionInfo);
+    });
+
+    proxy.on('data', (connectionId, direction, data) => {
+      proxyEvents.dataTransfers.push({ connectionId, direction, dataLength: data.length });
+    });
+
+    try {
+      await proxy.start();
+
+      const proxyClient = client.duplicate({
+        socket: {
+          port: proxyPort,
+          host: '127.0.0.1'
+        },
+      });
+
+      await proxyClient.connect();
+
+      const stats = proxy.getStats();
+      assert.equal(stats.activeConnections, 1, 'Should have one active connection');
+      assert.equal(proxyEvents.connections.length, 1, 'Should have recorded one connection event');
+
+      const pingResult = await proxyClient.ping();
+      assert.equal(pingResult, 'PONG', 'Client should be able to communicate with Redis through the proxy');
+
+      const clientToServerTransfers = proxyEvents.dataTransfers.filter(t => t.direction === 'client->server');
+      const serverToClientTransfers = proxyEvents.dataTransfers.filter(t => t.direction === 'server->client');
+
+      assert(clientToServerTransfers.length > 0, 'Should have client->server data transfers');
+      assert(serverToClientTransfers.length > 0, 'Should have server->client data transfers');
+
+      const testKey = `test:proxy:${Date.now()}`;
+      const testValue = 'proxy-test-value';
+
+      await proxyClient.set(testKey, testValue);
+      const retrievedValue = await proxyClient.get(testKey);
+      assert.equal(retrievedValue, testValue, 'Should be able to set and get values through proxy');
+
+      proxyClient.destroy();
+
+
+    } finally {
+      await proxy.stop();
+    }
+  }, GLOBAL.SERVERS.OPEN_RESP_3);
+
+  testUtils.testWithProxiedClient('custom message injection via proxy client',
+    async (proxiedClient: RedisClientType<any, any, any, any, any>, proxy: RedisProxy) => {
+      const customMessageTransfers: any[] = [];
+
+      proxy.on('data', (connectionId, direction, data) => {
+        if (direction === 'server->client') {
+          customMessageTransfers.push({ connectionId, dataLength: data.length, data });
+        }
+      });
+
+
+      const stats = proxy.getStats();
+      assert.equal(stats.activeConnections, 1, 'Should have one active connection');
+
+      // Send a resp3 push
+      const customMessage = Buffer.from('>4\r\n$6\r\nMOVING\r\n:1\r\n:2\r\n$6\r\nhost:3\r\n');
+
+      const sendResults = proxy.sendToAllClients(customMessage);
+      assert.equal(sendResults.length, 1, 'Should send to one client');
+      assert.equal(sendResults[0].success, true, 'Custom message send should succeed');
+
+
+      const customMessageFound = customMessageTransfers.find(transfer =>
+        transfer.dataLength === customMessage.length
+      );
+      assert(customMessageFound, 'Should have recorded the custom message transfer');
+
+      assert.equal(customMessageFound.dataLength, customMessage.length,
+        'Custom message length should match');
+
+      const pingResult = await proxiedClient.ping();
+      assert.equal(pingResult, 'PONG', 'Client should be able to communicate with Redis through the proxy');
+
+    }, GLOBAL.SERVERS.OPEN_RESP_3)
+});
diff --git a/packages/test-utils/lib/redis-proxy.ts b/packages/test-utils/lib/redis-proxy.ts
new file mode 100644
index 00000000000..217ec528a33
--- /dev/null
+++ b/packages/test-utils/lib/redis-proxy.ts
@@ -0,0 +1,329 @@
+import * as net from 'net';
+import { EventEmitter } from 'events';
+
+interface ProxyConfig {
+  readonly listenPort: number;
+  readonly listenHost?: string;
+  readonly targetHost: string;
+  readonly targetPort: number;
+  readonly timeout?: number;
+  readonly enableLogging?: boolean;
+}
+
+interface ConnectionInfo {
+  readonly id: string;
+  readonly clientAddress: string;
+  readonly clientPort: number;
+  readonly connectedAt: Date;
+}
+
+interface ActiveConnection extends ConnectionInfo {
+  readonly clientSocket: net.Socket;
+  readonly serverSocket: net.Socket;
+}
+
+type SendResult =
+  | { readonly success: true; readonly connectionId: string }
+  | { readonly success: false; readonly error: string; readonly connectionId: string };
+
+type DataDirection = 'client->server' | 'server->client';
+
+interface ProxyStats {
+  readonly activeConnections: number;
+  readonly totalConnections: number;
+  readonly connections: readonly ConnectionInfo[];
+}
+
+interface ProxyEvents {
+  /** Emitted when a new client connects */
+  'connection': (connectionInfo: ConnectionInfo) => void;
+  /** Emitted when a connection is closed */
+  'disconnect': (connectionInfo: ConnectionInfo) => void;
+  /** Emitted when data is transferred */
+  'data': (connectionId: string, direction: DataDirection, data: Buffer) => void;
+  /** Emitted when an error occurs */
+  'error': (error: Error, connectionId?: string) => void;
+  /** Emitted when the proxy server starts */
+  'listening': (host: string, port: number) => void;
+  /** Emitted when the proxy server stops */
+  'close': () => void;
+}
+
+export class RedisProxy extends EventEmitter {
+  private readonly server: net.Server;
+  public readonly config: Required<ProxyConfig>;
+  private readonly connections: Map<string, ActiveConnection>;
+  private isRunning: boolean;
+
+  constructor(config: ProxyConfig) {
+    super();
+
+
+    this.config = {
+      listenHost: '127.0.0.1',
+      timeout: 30000,
+      enableLogging: false,
+      ...config
+    };
+
+    this.connections = new Map();
+    this.isRunning = false;
+    this.server = this.createServer();
+  }
+
+  public async start(): Promise<void> {
+    return new Promise((resolve, reject) => {
+      if (this.isRunning) {
+        reject(new Error('Proxy is already running'));
+        return;
+      }
+
+      this.server.listen(this.config.listenPort, this.config.listenHost, () => {
+        this.isRunning = true;
+        this.log(`Proxy listening on ${this.config.listenHost}:${this.config.listenPort}`);
+        this.log(`Forwarding to Redis server at ${this.config.targetHost}:${this.config.targetPort}`);
+        this.emit('listening', this.config.listenHost, this.config.listenPort);
+        resolve();
+      });
+
+      this.server.on('error', (error) => {
+        this.emit('error', error);
+        reject(error);
+      });
+    });
+  }
+
+  public async stop(): Promise<void> {
+    return new Promise((resolve) => {
+      if (!this.isRunning) {
+        resolve();
+        return;
+      }
+
+      Array.from(this.connections.keys()).forEach((connectionId) => {
+        this.closeConnection(connectionId);
+      });
+
+      this.server.close(() => {
+        this.isRunning = false;
+        this.log('Proxy server stopped');
+        this.emit('close');
+        resolve();
+      });
+    });
+  }
+
+  public getStats(): ProxyStats {
+    const connections = Array.from(this.connections.values());
+
+    return {
+      activeConnections: connections.length,
+      totalConnections: connections.length,
+      connections: connections.map((conn) => ({
+        id: conn.id,
+        clientAddress: conn.clientAddress,
+        clientPort: conn.clientPort,
+        connectedAt: conn.connectedAt,
+      }))
+    };
+  }
+
+  public closeConnection(connectionId: string): boolean {
+    const connection = this.connections.get(connectionId);
+    if (!connection) {
+      return false;
+    }
+
+    connection.clientSocket.destroy();
+    connection.serverSocket.destroy();
+    this.connections.delete(connectionId);
+    this.emit('disconnect', connection);
+    return true;
+  }
+
+  public sendToClient(connectionId: string, data: Buffer): SendResult {
+    const connection = this.connections.get(connectionId);
+    if (!connection) {
+      return {
+        success: false,
+        error: 'Connection not found',
+        connectionId
+      };
+    }
+
+    if (connection.clientSocket.destroyed || !connection.clientSocket.writable) {
+      return {
+        success: false,
+        error: 'Client socket is not writable',
+        connectionId
+      };
+    }
+
+    try {
+      connection.clientSocket.write(data);
+
+      this.log(`Sent ${data.length} bytes to client ${connectionId}`);
+      this.emit('data', connectionId, 'server->client', data);
+
+      return {
+        success: true,
+        connectionId
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+      this.log(`Failed to send data to client ${connectionId}: ${errorMessage}`);
+      return {
+        success: false,
+        error: errorMessage,
+        connectionId
+      };
+    }
+  }
+
+  public sendToAllClients(data: Buffer): readonly SendResult[] {
+    const connectionIds = Array.from(this.connections.keys());
+    const results = connectionIds.map((connectionId) =>
+      this.sendToClient(connectionId, data)
+    );
+
+    const successCount = results.filter((result) => result.success).length;
+    const totalCount = results.length;
+
+    this.log(`Sent ${data.length} bytes to ${successCount}/${totalCount} clients`);
+
+    return results;
+  }
+
+  public sendToClients(connectionIds: readonly string[], data: Buffer): readonly SendResult[] {
+    const results = connectionIds.map((connectionId) =>
+      this.sendToClient(connectionId, data)
+    );
+
+    const successCount = results.filter((result) => result.success).length;
+    const totalCount = results.length;
+
+    this.log(`Sent ${data.length} bytes to ${successCount}/${totalCount} specified clients`);
+
+    return results;
+  }
+
+  public getActiveConnectionIds(): readonly string[] {
+    return Array.from(this.connections.keys());
+  }
+
+  private createServer(): net.Server {
+    return net.createServer((clientSocket) => {
+      this.handleClientConnection(clientSocket);
+    });
+  }
+
+  private handleClientConnection(clientSocket: net.Socket): void {
+    const connectionId = this.generateConnectionId();
+    const serverSocket = net.createConnection({
+      host: this.config.targetHost,
+      port: this.config.targetPort
+    });
+
+    const connectionInfo: ActiveConnection = {
+      id: connectionId,
+      clientAddress: clientSocket.remoteAddress || 'unknown',
+      clientPort: clientSocket.remotePort || 0,
+      connectedAt: new Date(),
+      clientSocket,
+      serverSocket
+    };
+
+    this.connections.set(connectionId, connectionInfo);
+    this.log(`New connection ${connectionId} from ${connectionInfo.clientAddress}:${connectionInfo.clientPort}`);
+
+    clientSocket.setTimeout(this.config.timeout);
+
+    serverSocket.on('connect', () => {
+      this.log(`Connected to Redis server for connection ${connectionId}`);
+      this.emit('connection', connectionInfo);
+    });
+
+    clientSocket.on('data', (data) => {
+      this.emit('data', connectionId, 'client->server', data);
+      serverSocket.write(data);
+    });
+
+    serverSocket.on('data', (data) => {
+      this.emit('data', connectionId, 'server->client', data);
+      clientSocket.write(data);
+    });
+
+    clientSocket.on('close', () => {
+      this.log(`Client disconnected for connection ${connectionId}`);
+      serverSocket.destroy();
+      this.cleanupConnection(connectionId);
+    });
+
+    serverSocket.on('close', () => {
+      this.log(`Server disconnected for connection ${connectionId}`);
+      clientSocket.destroy();
+      this.cleanupConnection(connectionId);
+    });
+
+    clientSocket.on('error', (error) => {
+      this.log(`Client error for connection ${connectionId}: ${error.message}`);
+      this.emit('error', error, connectionId);
+      serverSocket.destroy();
+      this.cleanupConnection(connectionId);
+    });
+
+    serverSocket.on('error', (error) => {
+      this.log(`Server error for connection ${connectionId}: ${error.message}`);
+      this.emit('error', error, connectionId);
+      clientSocket.destroy();
+      this.cleanupConnection(connectionId);
+    });
+
+    clientSocket.on('timeout', () => {
+      this.log(`Connection ${connectionId} timed out`);
+      clientSocket.destroy();
+      serverSocket.destroy();
+      this.cleanupConnection(connectionId);
+    });
+  }
+
+  private cleanupConnection(connectionId: string): void {
+    const connection = this.connections.get(connectionId);
+    if (connection) {
+      this.connections.delete(connectionId);
+      this.emit('disconnect', connection);
+    }
+  }
+
+  private generateConnectionId(): string {
+    return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+  }
+
+  private log(message: string): void {
+    if (this.config.enableLogging) {
+      console.log(`[RedisProxy] ${new Date().toISOString()} - ${message}`);
+    }
+  }
+}
+import { createServer } from 'net';
+
+export function getFreePortNumber(): Promise<number> {
+  return new Promise((resolve, reject) => {
+    const server = createServer();
+
+    server.listen(0, () => {
+      const address = server.address();
+      server.close(() => {
+        if (address && typeof address === 'object') {
+          resolve(address.port);
+        }
+      });
+    });
+
+    server.on('error', reject);
+  });
+}
+
+export { RedisProxy as RedisTransparentProxy };
+export type { ProxyConfig, ConnectionInfo, ProxyEvents, SendResult, DataDirection, ProxyStats };
+
diff --git a/packages/test-utils/lib/test-utils.ts b/packages/test-utils/lib/test-utils.ts
new file mode 100644
index 00000000000..fe27bd93d37
--- /dev/null
+++ b/packages/test-utils/lib/test-utils.ts
@@ -0,0 +1,25 @@
+import TestUtils from './index'
+
+export const testUtils = TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '8.2-M01-pre'
+});
+
+
+
+export const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ?
+  ['--enable-debug-command', 'yes'] :
+  [];
+
+export const GLOBAL = {
+  SERVERS: {
+
+    OPEN_RESP_3: {
+      serverArguments: [...DEBUG_MODE_ARGS],
+      clientOptions: {
+        RESP: 3,
+      }
+    },
+  }
+}
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 2f4e366536e..f7373f6add1 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,24 +1,16 @@
 {
   "name": "@redis/test-utils",
   "private": true,
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
+  "main": "./dist/lib/index.js",
+  "types": "./dist/lib/index.d.ts",
   "scripts": {
-    "build": "tsc"
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'"
   },
   "peerDependencies": {
-    "@redis/client": "^1.0.0"
+    "@redis/client": "*"
   },
   "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
-    "@types/mocha": "^10.0.1",
-    "@types/node": "^20.6.2",
-    "@types/yargs": "^17.0.24",
-    "mocha": "^10.2.0",
-    "nyc": "^15.1.0",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typescript": "^5.2.2",
+    "@types/yargs": "^17.0.32",
     "yargs": "^17.7.2"
   }
 }
diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json
index 14fda1d8711..6bb104668fc 100644
--- a/packages/test-utils/tsconfig.json
+++ b/packages/test-utils/tsconfig.json
@@ -5,5 +5,8 @@
   },
   "include": [
     "./lib/**/*.ts"
-  ]
+  ],
+  "references": [{
+    "path": "../client"
+  }]
 }
diff --git a/packages/time-series/.npmignore b/packages/time-series/.npmignore
deleted file mode 100644
index bbef2b404fb..00000000000
--- a/packages/time-series/.npmignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.nyc_output/
-coverage/
-lib/
-.nycrc.json
-.release-it.json
-tsconfig.json
diff --git a/packages/time-series/.release-it.json b/packages/time-series/.release-it.json
index b5a7c08d24f..0ffec5a0c70 100644
--- a/packages/time-series/.release-it.json
+++ b/packages/time-series/.release-it.json
@@ -1,10 +1,22 @@
 {
+  "npm": {
+    "publish": true,
+    "publishArgs": ["--access", "public"]
+  },
   "git": {
     "tagName": "time-series@${version}",
+    "tagMatch": "time-series@*",
     "commitMessage": "Release ${tagName}",
-    "tagAnnotation": "Release ${tagName}"
+    "tagAnnotation": "Release ${tagName}",
+    "commitArgs": "--all"
   },
-  "npm": {
-    "publishArgs": ["--access", "public"]
+  "plugins": {
+    "@release-it/bumper": {
+      "out": {
+        "file": "package.json",
+        "path": ["peerDependencies.@redis/client"],
+        "versionPrefix": "^"
+      }
+    }
   }
 }
diff --git a/packages/time-series/README.md b/packages/time-series/README.md
index 5923979cd48..ff42bfb6b3d 100644
--- a/packages/time-series/README.md
+++ b/packages/time-series/README.md
@@ -1,8 +1,10 @@
 # @redis/time-series
 
-This package provides support for the [RedisTimeSeries](https://redistimeseries.io) module, which adds a time series data structure to Redis. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RedisTimeSeries commands.
+This package provides support for the [RedisTimeSeries](https://redis.io/docs/data-types/timeseries/) module, which adds a time series data structure to Redis.
 
-To use these extra commands, your Redis server must have the RedisTimeSeries module installed.
+Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis).
+
+:warning: To use these extra commands, your Redis server must have the RedisTimeSeries module installed.
 
 ## Usage
 
@@ -20,20 +22,18 @@ import { createClient } from 'redis';
 import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding, TimeSeriesAggregationType } from '@redis/time-series';
 
 ...
-
- const created = await client.ts.create('temperature', {
-    RETENTION: 86400000, // 1 day in milliseconds
-    ENCODING: TimeSeriesEncoding.UNCOMPRESSED, // No compression - When not specified, the option is set to COMPRESSED
-    DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, // No duplicates - When not specified: set to the global DUPLICATE_POLICY configuration of the database (which by default, is BLOCK).
-  });
-
-    if (created === 'OK') {
-    console.log('Created timeseries.');
-  } else {
-    console.log('Error creating timeseries :(');
-    process.exit(1);
-  }
-
+const created = await client.ts.create('temperature', {
+  RETENTION: 86400000, // 1 day in milliseconds
+  ENCODING: TimeSeriesEncoding.UNCOMPRESSED, // No compression - When not specified, the option is set to COMPRESSED
+  DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, // No duplicates - When not specified: set to the global DUPLICATE_POLICY configuration of the database (which by default, is BLOCK).
+});
+
+if (created === 'OK') {
+  console.log('Created timeseries.');
+} else {
+  console.log('Error creating timeseries :(');
+  process.exit(1);
+}
 ```
 
 ### Adding new value to a Time Series data structure in Redis
@@ -43,33 +43,31 @@ With RedisTimeSeries, we can add a single value to time series data structure us
 ```javascript
 
 let value = Math.floor(Math.random() * 1000) + 1; // Random data point value
-  let currentTimestamp = 1640995200000; // Jan 1 2022 00:00:00
-  let num = 0;
-
-  while (num < 10000) {
-    // Add a new value to the timeseries, providing our own timestamp:
-    // https://redis.io/commands/ts.add/
-    await client.ts.add('temperature', currentTimestamp, value);
-    console.log(`Added timestamp ${currentTimestamp}, value ${value}.`);
-
-    num += 1;
-    value = Math.floor(Math.random() * 1000) + 1; // Get another random value
-    currentTimestamp += 1000; // Move on one second.
-  }
-
-  // Add multiple values to the timeseries in round trip to the server:
-  // https://redis.io/commands/ts.madd/
-  const response = await client.ts.mAdd([{
-    key: 'temperature',
-    timestamp: currentTimestamp + 60000,
-    value: Math.floor(Math.random() * 1000) + 1
-  }, {
-    key: 'temperature',
-    timestamp: currentTimestamp + 120000,
-    value: Math.floor(Math.random() * 1000) + 1
-  }]);
-
-
+let currentTimestamp = 1640995200000; // Jan 1 2022 00:00:00
+let num = 0;
+
+while (num < 10000) {
+  // Add a new value to the timeseries, providing our own timestamp:
+  // https://redis.io/commands/ts.add/
+  await client.ts.add('temperature', currentTimestamp, value);
+  console.log(`Added timestamp ${currentTimestamp}, value ${value}.`);
+
+  num += 1;
+  value = Math.floor(Math.random() * 1000) + 1; // Get another random value
+  currentTimestamp += 1000; // Move on one second.
+}
+
+// Add multiple values to the timeseries in round trip to the server:
+// https://redis.io/commands/ts.madd/
+const response = await client.ts.mAdd([{
+  key: 'temperature',
+  timestamp: currentTimestamp + 60000,
+  value: Math.floor(Math.random() * 1000) + 1
+}, {
+  key: 'temperature',
+  timestamp: currentTimestamp + 120000,
+  value: Math.floor(Math.random() * 1000) + 1
+}]);
 ```
 
 ### Retrieving Time Series data from Redis
@@ -77,31 +75,29 @@ let value = Math.floor(Math.random() * 1000) + 1; // Random data point value
 With RedisTimeSeries, we can retrieve the time series data using the [`TS.RANGE`](https://redis.io/commands/ts.range/) command by passing the criteria as follows:
 
 ```javascript
-
 // Query the timeseries with TS.RANGE:
-  // https://redis.io/commands/ts.range/
-  const fromTimestamp = 1640995200000; // Jan 1 2022 00:00:00
-  const toTimestamp = 1640995260000; // Jan 1 2022 00:01:00
-  const rangeResponse = await client.ts.range('temperature', fromTimestamp, toTimestamp, {
-    // Group into 10 second averages.
-    AGGREGATION: {
-      type: TimeSeriesAggregationType.AVERAGE,
-      timeBucket: 10000
-    }
-  });
-
-  console.log('RANGE RESPONSE:');
-  // rangeResponse looks like:
-  // [
-  //   { timestamp: 1640995200000, value: 356.8 },
-  //   { timestamp: 1640995210000, value: 534.8 },
-  //   { timestamp: 1640995220000, value: 481.3 },
-  //   { timestamp: 1640995230000, value: 437 },
-  //   { timestamp: 1640995240000, value: 507.3 },
-  //   { timestamp: 1640995250000, value: 581.2 },
-  //   { timestamp: 1640995260000, value: 600 }
-  // ]
-
+// https://redis.io/commands/ts.range/
+const fromTimestamp = 1640995200000; // Jan 1 2022 00:00:00
+const toTimestamp = 1640995260000; // Jan 1 2022 00:01:00
+const rangeResponse = await client.ts.range('temperature', fromTimestamp, toTimestamp, {
+  // Group into 10 second averages.
+  AGGREGATION: {
+    type: TimeSeriesAggregationType.AVERAGE,
+    timeBucket: 10000
+  }
+});
+
+console.log('RANGE RESPONSE:');
+// rangeResponse looks like:
+// [
+//   { timestamp: 1640995200000, value: 356.8 },
+//   { timestamp: 1640995210000, value: 534.8 },
+//   { timestamp: 1640995220000, value: 481.3 },
+//   { timestamp: 1640995230000, value: 437 },
+//   { timestamp: 1640995240000, value: 507.3 },
+//   { timestamp: 1640995250000, value: 581.2 },
+//   { timestamp: 1640995260000, value: 600 }
+// ]
 ```
 
 ### Altering Time Series data Stored in Redis
@@ -111,12 +107,10 @@ RedisTimeSeries includes commands that can update values in a time series data s
 Using the [`TS.ALTER`](https://redis.io/commands/ts.alter/) command, we can update time series retention like this:
 
 ```javascript
-
-  // https://redis.io/commands/ts.alter/
-  const alterResponse = await client.ts.alter('temperature', {
-    RETENTION: 0 // Keep the entries forever
-  });
-
+// https://redis.io/commands/ts.alter/
+const alterResponse = await client.ts.alter('temperature', {
+  RETENTION: 0 // Keep the entries forever
+});
 ```
 
 ### Retrieving Information about the timeseries Stored in Redis
@@ -126,26 +120,24 @@ RedisTimeSeries also includes commands that can help to view the information on
 Using the [`TS.INFO`](https://redis.io/commands/ts.info/) command, we can view timeseries information like this:
 
 ```javascript
-
- // Get some information about the state of the timeseries.
-  // https://redis.io/commands/ts.info/
-  const tsInfo = await client.ts.info('temperature');
-
-  // tsInfo looks like this:
-  // {
-  //   totalSamples: 1440,
-  //   memoryUsage: 28904,
-  //   firstTimestamp: 1641508920000,
-  //   lastTimestamp: 1641595320000,
-  //   retentionTime: 86400000,
-  //   chunkCount: 7,
-  //   chunkSize: 4096,
-  //   chunkType: 'uncompressed',
-  //   duplicatePolicy: 'block',
-  //   labels: [],
-  //   sourceKey: null,
-  //   rules: []
-  // }
-
+// Get some information about the state of the timeseries.
+// https://redis.io/commands/ts.info/
+const tsInfo = await client.ts.info('temperature');
+
+// tsInfo looks like this:
+// {
+//   totalSamples: 1440,
+//   memoryUsage: 28904,
+//   firstTimestamp: 1641508920000,
+//   lastTimestamp: 1641595320000,
+//   retentionTime: 86400000,
+//   chunkCount: 7,
+//   chunkSize: 4096,
+//   chunkType: 'uncompressed',
+//   duplicatePolicy: 'block',
+//   labels: [],
+//   sourceKey: null,
+//   rules: []
+// }
 ```
 
diff --git a/packages/time-series/lib/commands/ADD.spec.ts b/packages/time-series/lib/commands/ADD.spec.ts
index 07e67c1adec..d66c85441a1 100644
--- a/packages/time-series/lib/commands/ADD.spec.ts
+++ b/packages/time-series/lib/commands/ADD.spec.ts
@@ -1,90 +1,94 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ADD';
-import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding } from '.';
+import ADD from './ADD';
+import { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from './helpers';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ADD', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1),
-                ['TS.ADD', 'key', '*', '1']
-            );
-        });
+describe('TS.ADD', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1),
+        ['TS.ADD', 'key', '*', '1']
+      );
+    });
 
-        it('with RETENTION', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1, {
-                    RETENTION: 1
-                }),
-                ['TS.ADD', 'key', '*', '1', 'RETENTION', '1']
-            );
-        });
+    it('with RETENTION', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          RETENTION: 1
+        }),
+        ['TS.ADD', 'key', '*', '1', 'RETENTION', '1']
+      );
+    });
 
-        it('with ENCODING', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1, {
-                    ENCODING: TimeSeriesEncoding.UNCOMPRESSED
-                }),
-                ['TS.ADD', 'key', '*', '1', 'ENCODING', 'UNCOMPRESSED']
-            );
-        });
+    it('with ENCODING', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED
+        }),
+        ['TS.ADD', 'key', '*', '1', 'ENCODING', 'UNCOMPRESSED']
+      );
+    });
 
-        it('with CHUNK_SIZE', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1, {
-                    CHUNK_SIZE: 1
-                }),
-                ['TS.ADD', 'key', '*', '1', 'CHUNK_SIZE', '1']
-            );
-        });
+    it('with CHUNK_SIZE', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          CHUNK_SIZE: 1
+        }),
+        ['TS.ADD', 'key', '*', '1', 'CHUNK_SIZE', '1']
+      );
+    });
 
-        it('with ON_DUPLICATE', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1, {
-                    ON_DUPLICATE: TimeSeriesDuplicatePolicies.BLOCK
-                }),
-                ['TS.ADD', 'key', '*', '1', 'ON_DUPLICATE', 'BLOCK']
-            );
-        });
+    it('with ON_DUPLICATE', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          ON_DUPLICATE: TIME_SERIES_DUPLICATE_POLICIES.BLOCK
+        }),
+        ['TS.ADD', 'key', '*', '1', 'ON_DUPLICATE', 'BLOCK']
+      );
+    });
 
-        it('with LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1, {
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.ADD', 'key', '*', '1', 'LABELS', 'label', 'value']
-            );
-        });
+    it('with LABELS', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          LABELS: { label: 'value' }
+        }),
+        ['TS.ADD', 'key', '*', '1', 'LABELS', 'label', 'value']
+      );
+    });
 
-        it('with IGNORE', () => {
-          assert.deepEqual(
-            transformArguments('key', '*', 1, {
-              IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1}
-            }),
-            ['TS.ADD', 'key', '*', '1', 'IGNORE', '1', '1']
-          )
-        });
+    it ('with IGNORE', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          IGNORE: { 
+            maxTimeDiff: 1,
+            maxValDiff: 1
+           }
+        }),
+        ['TS.ADD', 'key', '*', '1', 'IGNORE', '1', '1']
+      )
+    });
 
-        it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS, IGNORE', () => {
-            assert.deepEqual(
-                transformArguments('key', '*', 1, {
-                    RETENTION: 1,
-                    ENCODING: TimeSeriesEncoding.UNCOMPRESSED,
-                    CHUNK_SIZE: 1,
-                    ON_DUPLICATE: TimeSeriesDuplicatePolicies.BLOCK,
-                    LABELS: { label: 'value' },
-                    IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1}
-                }),
-                ['TS.ADD', 'key', '*', '1', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'ON_DUPLICATE', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
-            );
-        });
+    it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS, IGNORE', () => {
+      assert.deepEqual(
+        parseArgs(ADD, 'key', '*', 1, {
+          RETENTION: 1,
+          ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED,
+          CHUNK_SIZE: 1,
+          ON_DUPLICATE: TIME_SERIES_DUPLICATE_POLICIES.BLOCK,
+          LABELS: { label: 'value' },
+          IGNORE: { maxTimeDiff: 1, maxValDiff: 1}
+        }),
+        ['TS.ADD', 'key', '*', '1', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'ON_DUPLICATE', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.add', async client => {
-        assert.equal(
-            await client.ts.add('key', 0, 1),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ts.add', async client => {
+    assert.equal(
+      await client.ts.add('key', 0, 1),
+      0
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts
index 3ed185b9b75..78a9247c41e 100644
--- a/packages/time-series/lib/commands/ADD.ts
+++ b/packages/time-series/lib/commands/ADD.ts
@@ -1,55 +1,66 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
 import {
-    transformTimestampArgument,
-    pushRetentionArgument,
-    TimeSeriesEncoding,
-    pushEncodingArgument,
-    pushChunkSizeArgument,
-    TimeSeriesDuplicatePolicies,
-    Labels,
-    pushLabelsArgument,
-    Timestamp,
-    pushIgnoreArgument,
-} from '.';
+  transformTimestampArgument,
+  parseRetentionArgument,
+  TimeSeriesEncoding,
+  parseEncodingArgument,
+  parseChunkSizeArgument,
+  TimeSeriesDuplicatePolicies,
+  Labels,
+  parseLabelsArgument,
+  Timestamp,
+  parseIgnoreArgument
+} from './helpers';
 
 export interface TsIgnoreOptions {
-  MAX_TIME_DIFF: number;
-  MAX_VAL_DIFF: number;
+  maxTimeDiff: number;
+  maxValDiff: number;
 }
 
-interface AddOptions {
-    RETENTION?: number;
-    ENCODING?: TimeSeriesEncoding;
-    CHUNK_SIZE?: number;
-    ON_DUPLICATE?: TimeSeriesDuplicatePolicies;
-    LABELS?: Labels;
-    IGNORE?: TsIgnoreOptions;
+export interface TsAddOptions {
+  RETENTION?: number;
+  ENCODING?: TimeSeriesEncoding;
+  CHUNK_SIZE?: number;
+  ON_DUPLICATE?: TimeSeriesDuplicatePolicies;
+  LABELS?: Labels;
+  IGNORE?: TsIgnoreOptions;
 }
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Creates or appends a sample to a time series
+   * @param parser - The command parser
+   * @param key - The key name for the time series
+   * @param timestamp - The timestamp of the sample
+   * @param value - The value of the sample
+   * @param options - Optional configuration parameters
+   */
+  parseCommand(
+    parser: CommandParser,
+    key: RedisArgument,
+    timestamp: Timestamp,
+    value: number,
+    options?: TsAddOptions
+  ) {
+    parser.push('TS.ADD');
+    parser.pushKey(key);
+    parser.push(transformTimestampArgument(timestamp), value.toString());
 
-export function transformArguments(key: string, timestamp: Timestamp, value: number, options?: AddOptions): Array<string> {
-    const args = [
-        'TS.ADD',
-        key,
-        transformTimestampArgument(timestamp),
-        value.toString()
-    ];
+    parseRetentionArgument(parser, options?.RETENTION);
 
-    pushRetentionArgument(args, options?.RETENTION);
+    parseEncodingArgument(parser, options?.ENCODING);
 
-    pushEncodingArgument(args, options?.ENCODING);
-
-    pushChunkSizeArgument(args, options?.CHUNK_SIZE);
+    parseChunkSizeArgument(parser, options?.CHUNK_SIZE);
 
     if (options?.ON_DUPLICATE) {
-        args.push('ON_DUPLICATE', options.ON_DUPLICATE);
+      parser.push('ON_DUPLICATE', options.ON_DUPLICATE);
     }
 
-    pushLabelsArgument(args, options?.LABELS);
-
-    pushIgnoreArgument(args, options?.IGNORE);
-
-    return args;
-}
+    parseLabelsArgument(parser, options?.LABELS);
 
-export declare function transformReply(): number;
+    parseIgnoreArgument(parser, options?.IGNORE);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/ALTER.spec.ts b/packages/time-series/lib/commands/ALTER.spec.ts
index 7add3eeec3a..46b94c5863a 100644
--- a/packages/time-series/lib/commands/ALTER.spec.ts
+++ b/packages/time-series/lib/commands/ALTER.spec.ts
@@ -1,82 +1,86 @@
-import { strict as assert } from 'assert';
-import { TimeSeriesDuplicatePolicies } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './ALTER';
+import ALTER from './ALTER';
+import { TIME_SERIES_DUPLICATE_POLICIES } from './helpers';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('ALTER', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['TS.ALTER', 'key']
-            );
-        });
+describe('TS.ALTER', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key'),
+        ['TS.ALTER', 'key']
+      );
+    });
 
-        it('with RETENTION', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    RETENTION: 1
-                }),
-                ['TS.ALTER', 'key', 'RETENTION', '1']
-            );
-        });
+    it('with RETENTION', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key', {
+          RETENTION: 1
+        }),
+        ['TS.ALTER', 'key', 'RETENTION', '1']
+      );
+    });
 
-        it('with CHUNK_SIZE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    CHUNK_SIZE: 1
-                }),
-                ['TS.ALTER', 'key', 'CHUNK_SIZE', '1']
-            );
-        });
+    it('with CHUNK_SIZE', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key', {
+          CHUNK_SIZE: 1
+        }),
+        ['TS.ALTER', 'key', 'CHUNK_SIZE', '1']
+      );
+    });
 
-        it('with DUPLICATE_POLICY', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK
-                }),
-                ['TS.ALTER', 'key', 'DUPLICATE_POLICY', 'BLOCK']
-            );
-        });
+    it('with DUPLICATE_POLICY', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key', {
+          DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK
+        }),
+        ['TS.ALTER', 'key', 'DUPLICATE_POLICY', 'BLOCK']
+      );
+    });
 
-        it('with LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.ALTER', 'key', 'LABELS', 'label', 'value']
-            );
-        });
+    it('with LABELS', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key', {
+          LABELS: { label: 'value' }
+        }),
+        ['TS.ALTER', 'key', 'LABELS', 'label', 'value']
+      );
+    });
 
-        it('with IGNORE with MAX_TIME_DIFF', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1}
-                }),
-                ['TS.ALTER', 'key', 'IGNORE', '1', '1']
-            )
-        });
+    it('with IGNORE with MAX_TIME_DIFF', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key', {
+          IGNORE: { 
+            maxTimeDiff: 1,
+            maxValDiff: 1
+          }
+        }),
+        ['TS.ALTER', 'key', 'IGNORE', '1', '1']
+      )
+    });
 
-        it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    RETENTION: 1,
-                    CHUNK_SIZE: 1,
-                    DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK,
-                    LABELS: { label: 'value' },
-                    IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1}
-                }),
-                ['TS.ALTER', 'key', 'RETENTION', '1', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
-            );
-        });
+    it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => {
+      assert.deepEqual(
+        parseArgs(ALTER, 'key', {
+          RETENTION: 1,
+          CHUNK_SIZE: 1,
+          DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK,
+          LABELS: { label: 'value' },
+          IGNORE: { maxTimeDiff: 1, maxValDiff: 1}
+        }),
+        ['TS.ALTER', 'key', 'RETENTION', '1', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.alter', async client => {
-        await client.ts.create('key');
+  testUtils.testWithClient('client.ts.alter', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.create('key'),
+      client.ts.alter('key')
+    ]);
 
-        assert.equal(
-            await client.ts.alter('key', { RETENTION: 1 }),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts
index 576153a0cca..613539b4861 100644
--- a/packages/time-series/lib/commands/ALTER.ts
+++ b/packages/time-series/lib/commands/ALTER.ts
@@ -1,30 +1,32 @@
-import { pushRetentionArgument, Labels, pushLabelsArgument, TimeSeriesDuplicatePolicies, pushChunkSizeArgument, pushDuplicatePolicy, pushIgnoreArgument } from '.';
-import { TsIgnoreOptions } from './ADD';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { TsCreateOptions } from './CREATE';
+import { parseRetentionArgument, parseChunkSizeArgument, parseDuplicatePolicy, parseLabelsArgument, parseIgnoreArgument } from './helpers';
 
-export const FIRST_KEY_INDEX = 1;
 
-interface AlterOptions {
-    RETENTION?: number;
-    CHUNK_SIZE?: number;
-    DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies;
-    LABELS?: Labels;
-    IGNORE?: TsIgnoreOptions;
-}
+export type TsAlterOptions = Pick<TsCreateOptions, 'RETENTION' | 'CHUNK_SIZE' | 'DUPLICATE_POLICY' | 'LABELS' | 'IGNORE'>;
 
-export function transformArguments(key: string, options?: AlterOptions): Array<string> {
-    const args = ['TS.ALTER', key];
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Alters the configuration of an existing time series
+   * @param parser - The command parser
+   * @param key - The key name for the time series
+   * @param options - Configuration parameters to alter
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: TsAlterOptions) {
+    parser.push('TS.ALTER');
+    parser.pushKey(key);
 
-    pushRetentionArgument(args, options?.RETENTION);
+    parseRetentionArgument(parser, options?.RETENTION);
 
-    pushChunkSizeArgument(args, options?.CHUNK_SIZE);
+    parseChunkSizeArgument(parser, options?.CHUNK_SIZE);
 
-    pushDuplicatePolicy(args, options?.DUPLICATE_POLICY);
+    parseDuplicatePolicy(parser, options?.DUPLICATE_POLICY);
 
-    pushLabelsArgument(args, options?.LABELS);
+    parseLabelsArgument(parser, options?.LABELS);
 
-    pushIgnoreArgument(args, options?.IGNORE);
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+    parseIgnoreArgument(parser, options?.IGNORE);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/CREATE.spec.ts b/packages/time-series/lib/commands/CREATE.spec.ts
index eb7a1c6a637..4fbfabb6858 100644
--- a/packages/time-series/lib/commands/CREATE.spec.ts
+++ b/packages/time-series/lib/commands/CREATE.spec.ts
@@ -1,90 +1,94 @@
-import { strict as assert } from 'assert';
-import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CREATE';
+import CREATE from './CREATE';
+import { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from './helpers';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CREATE', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['TS.CREATE', 'key']
-            );
-        });
+describe('TS.CREATE', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key'),
+        ['TS.CREATE', 'key']
+      );
+    });
+
+    it('with RETENTION', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          RETENTION: 1
+        }),
+        ['TS.CREATE', 'key', 'RETENTION', '1']
+      );
+    });
 
-        it('with RETENTION', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    RETENTION: 1
-                }),
-                ['TS.CREATE', 'key', 'RETENTION', '1']
-            );
-        });
+    it('with ENCODING', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED
+        }),
+        ['TS.CREATE', 'key', 'ENCODING', 'UNCOMPRESSED']
+      );
+    });
 
-        it('with ENCODING', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    ENCODING: TimeSeriesEncoding.UNCOMPRESSED
-                }),
-                ['TS.CREATE', 'key', 'ENCODING', 'UNCOMPRESSED']
-            );
-        });
+    it('with CHUNK_SIZE', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          CHUNK_SIZE: 1
+        }),
+        ['TS.CREATE', 'key', 'CHUNK_SIZE', '1']
+      );
+    });
 
-        it('with CHUNK_SIZE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    CHUNK_SIZE: 1
-                }),
-                ['TS.CREATE', 'key', 'CHUNK_SIZE', '1']
-            );
-        });
+    it('with DUPLICATE_POLICY', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK
+        }),
+        ['TS.CREATE', 'key', 'DUPLICATE_POLICY', 'BLOCK']
+      );
+    });
 
-        it('with DUPLICATE_POLICY', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK
-                }),
-                ['TS.CREATE', 'key', 'DUPLICATE_POLICY', 'BLOCK']
-            );
-        });
+    it('with LABELS', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          LABELS: { label: 'value' }
+        }),
+        ['TS.CREATE', 'key', 'LABELS', 'label', 'value']
+      );
+    });
 
-        it('with LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.CREATE', 'key', 'LABELS', 'label', 'value']
-            );
-        });
-   
-        it('with IGNORE with MAX_TIME_DIFF', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1}
-                }),
-                ['TS.CREATE', 'key', 'IGNORE', '1', '1']
-            )
-        });
+    it('with IGNORE with MAX_TIME_DIFF', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          IGNORE: { 
+            maxTimeDiff: 1,
+            maxValDiff: 1
+          }
+        }),
+        ['TS.CREATE', 'key', 'IGNORE', '1', '1']
+      )
+    });
 
-        it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    RETENTION: 1,
-                    ENCODING: TimeSeriesEncoding.UNCOMPRESSED,
-                    CHUNK_SIZE: 1,
-                    DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK,
-                    LABELS: { label: 'value' },
-                    IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1}
-                }),
-                ['TS.CREATE', 'key', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
-            );
-        });
+    it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => {
+      assert.deepEqual(
+        parseArgs(CREATE, 'key', {
+          RETENTION: 1,
+          ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED,
+          CHUNK_SIZE: 1,
+          DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK,
+          LABELS: { label: 'value' },
+          IGNORE: { maxTimeDiff: 1, maxValDiff: 1}
+        }),
+        ['TS.CREATE', 'key', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.create', async client => {
-        assert.equal(
-            await client.ts.create('key'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ts.create', async client => {
+    assert.equal(
+      await client.ts.create('key'),
+      'OK'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts
index a84d4b5f9fb..e8a6b03773f 100644
--- a/packages/time-series/lib/commands/CREATE.ts
+++ b/packages/time-series/lib/commands/CREATE.ts
@@ -1,43 +1,50 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 import {
-    pushRetentionArgument,
-    TimeSeriesEncoding,
-    pushEncodingArgument,
-    pushChunkSizeArgument,
-    TimeSeriesDuplicatePolicies,
-    Labels,
-    pushLabelsArgument,
-    pushDuplicatePolicy,
-    pushIgnoreArgument
-} from '.';
+  parseRetentionArgument,
+  TimeSeriesEncoding,
+  parseEncodingArgument,
+  parseChunkSizeArgument,
+  TimeSeriesDuplicatePolicies,
+  parseDuplicatePolicy,
+  Labels,
+  parseLabelsArgument,
+  parseIgnoreArgument
+} from './helpers';
 import { TsIgnoreOptions } from './ADD';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface CreateOptions {
-    RETENTION?: number;
-    ENCODING?: TimeSeriesEncoding;
-    CHUNK_SIZE?: number;
-    DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies;
-    LABELS?: Labels;
-    IGNORE?: TsIgnoreOptions;
+export interface TsCreateOptions {
+  RETENTION?: number;
+  ENCODING?: TimeSeriesEncoding;
+  CHUNK_SIZE?: number;
+  DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies;
+  LABELS?: Labels;
+  IGNORE?: TsIgnoreOptions;
 }
 
-export function transformArguments(key: string, options?: CreateOptions): Array<string> {
-    const args = ['TS.CREATE', key];
-
-    pushRetentionArgument(args, options?.RETENTION);
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Creates a new time series
+   * @param parser - The command parser
+   * @param key - The key name for the new time series
+   * @param options - Optional configuration parameters
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: TsCreateOptions) {
+    parser.push('TS.CREATE');
+    parser.pushKey(key);
 
-    pushEncodingArgument(args, options?.ENCODING);
+    parseRetentionArgument(parser, options?.RETENTION);
 
-    pushChunkSizeArgument(args, options?.CHUNK_SIZE);
+    parseEncodingArgument(parser, options?.ENCODING);
 
-    pushDuplicatePolicy(args, options?.DUPLICATE_POLICY);
+    parseChunkSizeArgument(parser, options?.CHUNK_SIZE);
 
-    pushLabelsArgument(args, options?.LABELS);
+    parseDuplicatePolicy(parser, options?.DUPLICATE_POLICY);
 
-    pushIgnoreArgument(args, options?.IGNORE);
-
-    return args;
-}
+    parseLabelsArgument(parser, options?.LABELS);
 
-export declare function transformReply(): 'OK';
+    parseIgnoreArgument(parser, options?.IGNORE);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/CREATERULE.spec.ts b/packages/time-series/lib/commands/CREATERULE.spec.ts
index 65457898181..da26bf458e2 100644
--- a/packages/time-series/lib/commands/CREATERULE.spec.ts
+++ b/packages/time-series/lib/commands/CREATERULE.spec.ts
@@ -1,34 +1,32 @@
-import { strict as assert } from 'assert';
-import { TimeSeriesAggregationType } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './CREATERULE';
+import CREATERULE, { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('CREATERULE', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1),
-                ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1']
-            );
-        });
+describe('TS.CREATERULE', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(CREATERULE, 'source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1),
+        ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1']
+      );
+    });
 
-        it('with alignTimestamp', () => {
-            assert.deepEqual(
-                transformArguments('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1, 1),
-                ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1', '1']
-            );
-        });
+    it('with alignTimestamp', () => {
+      assert.deepEqual(
+        parseArgs(CREATERULE, 'source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1, 1),
+        ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.createRule', async client => {
-        await Promise.all([
-            client.ts.create('source'),
-            client.ts.create('destination')
-        ]);
+  testUtils.testWithClient('client.ts.createRule', async client => {
+    const [, , reply] = await Promise.all([
+      client.ts.create('source'),
+      client.ts.create('destination'),
+      client.ts.createRule('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1)
+    ]);
 
-        assert.equal(
-            await client.ts.createRule('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/CREATERULE.ts b/packages/time-series/lib/commands/CREATERULE.ts
index 87b8579a6ee..71d08009ac2 100644
--- a/packages/time-series/lib/commands/CREATERULE.ts
+++ b/packages/time-series/lib/commands/CREATERULE.ts
@@ -1,28 +1,50 @@
-import { TimeSeriesAggregationType } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
+export const TIME_SERIES_AGGREGATION_TYPE = {
+  AVG: 'AVG',
+  FIRST: 'FIRST',
+  LAST: 'LAST',
+  MIN: 'MIN',
+  MAX: 'MAX',
+  SUM: 'SUM',
+  RANGE: 'RANGE',
+  COUNT: 'COUNT',
+  STD_P: 'STD.P',
+  STD_S: 'STD.S',
+  VAR_P: 'VAR.P',
+  VAR_S: 'VAR.S',
+  TWA: 'TWA'
+} as const;
 
-export function transformArguments(
-    sourceKey: string,
-    destinationKey: string,
+export type TimeSeriesAggregationType = typeof TIME_SERIES_AGGREGATION_TYPE[keyof typeof TIME_SERIES_AGGREGATION_TYPE];
+
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Creates a compaction rule from source time series to destination time series
+   * @param parser - The command parser
+   * @param sourceKey - The source time series key
+   * @param destinationKey - The destination time series key
+   * @param aggregationType - The aggregation type to use
+   * @param bucketDuration - The duration of each bucket in milliseconds
+   * @param alignTimestamp - Optional timestamp for alignment
+   */
+  parseCommand(
+    parser: CommandParser,
+    sourceKey: RedisArgument,
+    destinationKey: RedisArgument,
     aggregationType: TimeSeriesAggregationType,
     bucketDuration: number,
     alignTimestamp?: number
-): Array<string> {
-    const args = [
-        'TS.CREATERULE',
-        sourceKey,
-        destinationKey,
-        'AGGREGATION',
-        aggregationType,
-        bucketDuration.toString()
-    ];
+  ) {
+    parser.push('TS.CREATERULE');
+    parser.pushKeys([sourceKey, destinationKey]);
+    parser.push('AGGREGATION', aggregationType, bucketDuration.toString());
 
-    if (alignTimestamp) {
-        args.push(alignTimestamp.toString());
+    if (alignTimestamp !== undefined) {
+      parser.push(alignTimestamp.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): 'OK';
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/DECRBY.spec.ts b/packages/time-series/lib/commands/DECRBY.spec.ts
index 345e651404b..b272ed1614d 100644
--- a/packages/time-series/lib/commands/DECRBY.spec.ts
+++ b/packages/time-series/lib/commands/DECRBY.spec.ts
@@ -1,81 +1,93 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DECRBY';
+import DECRBY from './DECRBY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DECRBY', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['TS.DECRBY', 'key', '1']
-            );
-        });
+describe('TS.DECRBY', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1),
+        ['TS.DECRBY', 'key', '1']
+      );
+    });
 
-        it('with TIMESTAMP', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    TIMESTAMP: '*'
-                }),
-                ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*']
-            );
-        });
+    it('with TIMESTAMP', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          TIMESTAMP: '*'
+        }),
+        ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*']
+      );
+    });
 
-        it('with RETENTION', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    RETENTION: 1
-                }),
-                ['TS.DECRBY', 'key', '1', 'RETENTION', '1']
-            );
-        });
+    it('with RETENTION', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          RETENTION: 1
+        }),
+        ['TS.DECRBY', 'key', '1', 'RETENTION', '1']
+      );
+    });
 
-        it('with UNCOMPRESSED', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    UNCOMPRESSED: true
-                }),
-                ['TS.DECRBY', 'key', '1', 'UNCOMPRESSED']
-            );
-        });
+    it('with UNCOMPRESSED', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          UNCOMPRESSED: true
+        }),
+        ['TS.DECRBY', 'key', '1', 'UNCOMPRESSED']
+      );
+    });
 
-        it('with CHUNK_SIZE', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    CHUNK_SIZE: 100
-                }),
-                ['TS.DECRBY', 'key', '1', 'CHUNK_SIZE', '100']
-            );
-        });
+    it('with CHUNK_SIZE', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          CHUNK_SIZE: 100
+        }),
+        ['TS.DECRBY', 'key', '1', 'CHUNK_SIZE', '100']
+      );
+    });
 
-        it('with LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.DECRBY', 'key', '1', 'LABELS', 'label', 'value']
-            );
-        });
+    it('with LABELS', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          LABELS: { label: 'value' }
+        }),
+        ['TS.DECRBY', 'key', '1', 'LABELS', 'label', 'value']
+      );
+    });
 
-        it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    TIMESTAMP: '*',
-                    RETENTION: 1,
-                    UNCOMPRESSED: true,
-                    CHUNK_SIZE: 2,
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', 'CHUNK_SIZE', '2', 'LABELS', 'label', 'value']
-            );
-        });
+    it ('with IGNORE', () => {     
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          IGNORE: { 
+            maxTimeDiff: 1,
+            maxValDiff: 1
+          }
+        }),
+        ['TS.DECRBY', 'key', '1', 'IGNORE', '1', '1']
+      )
+    });
+  
+    it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => {
+      assert.deepEqual(
+        parseArgs(DECRBY, 'key', 1, {
+          TIMESTAMP: '*',
+          RETENTION: 1,
+          UNCOMPRESSED: true,
+          CHUNK_SIZE: 2,
+          LABELS: { label: 'value' },
+          IGNORE: { maxTimeDiff: 1, maxValDiff: 1 }
+        }),
+        ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', 'CHUNK_SIZE', '2', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.decrBy', async client => {
-        assert.equal(
-            await client.ts.decrBy('key', 1, {
-                TIMESTAMP: 0
-            }),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ts.decrBy', async client => {
+    assert.equal(
+      typeof await client.ts.decrBy('key', 1),
+      'number'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/DECRBY.ts b/packages/time-series/lib/commands/DECRBY.ts
index 07b5b6f45c0..61b9d94e082 100644
--- a/packages/time-series/lib/commands/DECRBY.ts
+++ b/packages/time-series/lib/commands/DECRBY.ts
@@ -1,10 +1,17 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { IncrDecrOptions, transformIncrDecrArguments } from '.';
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import INCRBY, { parseIncrByArguments } from './INCRBY';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: INCRBY.IS_READ_ONLY,
+  /**
+   * Decreases the value of a time series by a given amount
+   * @param args - Arguments passed to the parseIncrByArguments function
+   */
+  parseCommand(...args: Parameters<typeof parseIncrByArguments>) {
+    const parser = args[0];
 
-export function transformArguments(key: string, value: number, options?: IncrDecrOptions): RedisCommandArguments {
-    return transformIncrDecrArguments('TS.DECRBY', key, value, options);
-}
-
-export declare function transformReply(): number;
+    parser.push('TS.DECRBY');
+    parseIncrByArguments(...args);
+  },
+  transformReply: INCRBY.transformReply
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/DEL.spec.ts b/packages/time-series/lib/commands/DEL.spec.ts
index 0fc4b465807..07d29ca095e 100644
--- a/packages/time-series/lib/commands/DEL.spec.ts
+++ b/packages/time-series/lib/commands/DEL.spec.ts
@@ -1,21 +1,22 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DEL';
+import DEL from './DEL';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DEL', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '-', '+'),
-            ['TS.DEL', 'key', '-', '+']
-        );
-    });
+describe('TS.DEL', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DEL, 'key', '-', '+'),
+      ['TS.DEL', 'key', '-', '+']
+    );
+  });
 
-    testUtils.testWithClient('client.ts.del', async client => {
-        await client.ts.create('key');
+  testUtils.testWithClient('client.ts.del', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.create('key'),
+      client.ts.del('key', '-', '+')
+    ]);
 
-        assert.equal(
-            await client.ts.del('key', '-', '+'),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 0);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts
index 347954c21de..1e0e01164fd 100644
--- a/packages/time-series/lib/commands/DEL.ts
+++ b/packages/time-series/lib/commands/DEL.ts
@@ -1,15 +1,20 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { Timestamp, transformTimestampArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Timestamp, transformTimestampArgument } from './helpers';
+import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRTS_KEY_INDEX = 1;
-
-export function transformArguments(key: string, fromTimestamp: Timestamp, toTimestamp: Timestamp): RedisCommandArguments {
-    return [
-        'TS.DEL',
-        key,
-        transformTimestampArgument(fromTimestamp),
-        transformTimestampArgument(toTimestamp)
-    ];
-}
-
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Deletes samples between two timestamps from a time series
+   * @param parser - The command parser
+   * @param key - The key name of the time series
+   * @param fromTimestamp - Start timestamp to delete from
+   * @param toTimestamp - End timestamp to delete until
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp) {
+    parser.push('TS.DEL');
+    parser.pushKey(key);
+    parser.push(transformTimestampArgument(fromTimestamp), transformTimestampArgument(toTimestamp));
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/DELETERULE.spec.ts b/packages/time-series/lib/commands/DELETERULE.spec.ts
index 9364bea711c..d7a19a8eaa1 100644
--- a/packages/time-series/lib/commands/DELETERULE.spec.ts
+++ b/packages/time-series/lib/commands/DELETERULE.spec.ts
@@ -1,26 +1,25 @@
-import { strict as assert } from 'assert';
-import { TimeSeriesAggregationType } from '.';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './DELETERULE';
+import DELETERULE from './DELETERULE';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('DELETERULE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('source', 'destination'),
-            ['TS.DELETERULE', 'source', 'destination']
-        );
-    });
+describe('TS.DELETERULE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(DELETERULE, 'source', 'destination'),
+      ['TS.DELETERULE', 'source', 'destination']
+    );
+  });
 
-    testUtils.testWithClient('client.ts.deleteRule', async client => {
-        await Promise.all([
-            client.ts.create('source'),
-            client.ts.create('destination'),
-            client.ts.createRule('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1)
-        ]);
+  testUtils.testWithClient('client.ts.deleteRule', async client => {
+    const [, , , reply] = await Promise.all([
+      client.ts.create('source'),
+      client.ts.create('destination'),
+      client.ts.createRule('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1),
+      client.ts.deleteRule('source', 'destination')
+    ]);
 
-        assert.equal(
-            await client.ts.deleteRule('source', 'destination'),
-            'OK'
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.equal(reply, 'OK');
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/DELETERULE.ts b/packages/time-series/lib/commands/DELETERULE.ts
index 7d2cfaeed94..8897f666b7b 100644
--- a/packages/time-series/lib/commands/DELETERULE.ts
+++ b/packages/time-series/lib/commands/DELETERULE.ts
@@ -1,11 +1,17 @@
-export const FIRST_KEY_INDEX = 1;
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export function transformArguments(sourceKey: string, destinationKey: string): Array<string> {
-    return [
-        'TS.DELETERULE',
-        sourceKey,
-        destinationKey
-    ];
-}
-
-export declare function transformReply(): 'OK';
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Deletes a compaction rule between source and destination time series
+   * @param parser - The command parser
+   * @param sourceKey - The source time series key
+   * @param destinationKey - The destination time series key
+   */
+  parseCommand(parser: CommandParser, sourceKey: RedisArgument, destinationKey: RedisArgument) {
+    parser.push('TS.DELETERULE');
+    parser.pushKeys([sourceKey, destinationKey]);
+  },
+  transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/GET.spec.ts b/packages/time-series/lib/commands/GET.spec.ts
index 29634cd775a..836a1b638af 100644
--- a/packages/time-series/lib/commands/GET.spec.ts
+++ b/packages/time-series/lib/commands/GET.spec.ts
@@ -1,46 +1,47 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './GET';
+import GET from './GET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('GET', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key'),
-                ['TS.GET', 'key']
-            );
-        });
+describe('TS.GET', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(GET, 'key'),
+        ['TS.GET', 'key']
+      );
+    });
 
-        it('with LATEST', () => {
-            assert.deepEqual(
-                transformArguments('key', {
-                    LATEST: true
-                }),
-                ['TS.GET', 'key', 'LATEST']
-            );
-        });
+    it('with LATEST', () => {
+      assert.deepEqual(
+        parseArgs(GET, 'key', {
+          LATEST: true
+        }),
+        ['TS.GET', 'key', 'LATEST']
+      );
     });
+  });
 
-    describe('client.ts.get', () => {
-        testUtils.testWithClient('null', async client => {
-            await client.ts.create('key');
+  describe('client.ts.get', () => {
+    testUtils.testWithClient('null', async client => {
+      const [, reply] = await Promise.all([
+        client.ts.create('key'),
+        client.ts.get('key')
+      ]);
 
-            assert.equal(
-                await client.ts.get('key'),
-                null
-            );
-        }, GLOBAL.SERVERS.OPEN);
+      assert.equal(reply, null);
+    }, GLOBAL.SERVERS.OPEN);
 
-        testUtils.testWithClient('with samples', async client => {
-            await client.ts.add('key', 0, 1);
+    testUtils.testWithClient('with sample', async client => {
+      const [, reply] = await Promise.all([
+        client.ts.add('key', 0, 1),
+        client.ts.get('key')
+      ]);
 
-            assert.deepEqual(
-                await client.ts.get('key'),
-                {
-                    timestamp: 0,
-                    value: 1
-                }
-            );
-        }, GLOBAL.SERVERS.OPEN);
-    });
+      assert.deepEqual(reply, {
+        timestamp: 0,
+        value: 1
+      });
+    }, GLOBAL.SERVERS.OPEN);
+  });
 });
diff --git a/packages/time-series/lib/commands/GET.ts b/packages/time-series/lib/commands/GET.ts
index 6d74f97c9cd..9462705c02c 100644
--- a/packages/time-series/lib/commands/GET.ts
+++ b/packages/time-series/lib/commands/GET.ts
@@ -1,20 +1,40 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushLatestArgument, SampleRawReply, SampleReply, transformSampleReply } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, TuplesReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-interface GetOptions {
-    LATEST?: boolean;
+export interface TsGetOptions {
+  LATEST?: boolean;
 }
 
-export function transformArguments(key: string, options?: GetOptions): RedisCommandArguments {
-    return pushLatestArgument(['TS.GET', key], options?.LATEST);
-}
+export type TsGetReply = TuplesReply<[]> | TuplesReply<[NumberReply, DoubleReply]>;
 
-export function transformReply(reply: [] | SampleRawReply): null | SampleReply {
-    if (reply.length === 0) return null;
-
-    return transformSampleReply(reply);
-}
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets the last sample of a time series
+   * @param parser - The command parser
+   * @param key - The key name of the time series
+   * @param options - Optional parameters for the command
+   */
+  parseCommand(parser: CommandParser, key: RedisArgument, options?: TsGetOptions) {
+    parser.push('TS.GET');
+    parser.pushKey(key);
+    
+    if (options?.LATEST) {
+      parser.push('LATEST');
+    }
+  },
+  transformReply: {
+    2(reply: UnwrapReply<Resp2Reply<TsGetReply>>) {
+      return reply.length === 0 ? null : {
+        timestamp: reply[0],
+        value: Number(reply[1])
+      };
+    },
+    3(reply: UnwrapReply<TsGetReply>) {
+      return reply.length === 0 ? null : {
+        timestamp: reply[0],
+        value: reply[1]
+      };
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/INCRBY.spec.ts b/packages/time-series/lib/commands/INCRBY.spec.ts
index acaa4cd3329..5d005952b30 100644
--- a/packages/time-series/lib/commands/INCRBY.spec.ts
+++ b/packages/time-series/lib/commands/INCRBY.spec.ts
@@ -1,91 +1,103 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './INCRBY';
+import INCRBY from './INCRBY';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('INCRBY', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key', 1),
-                ['TS.INCRBY', 'key', '1']
-            );
-        });
+describe('TS.INCRBY', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1),
+        ['TS.INCRBY', 'key', '1']
+      );
+    });
 
-        it('with TIMESTAMP', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    TIMESTAMP: '*'
-                }),
-                ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*']
-            );
-        });
+    it('with TIMESTAMP', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          TIMESTAMP: '*'
+        }),
+        ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*']
+      );
+    });
 
-        it('with RETENTION', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    RETENTION: 1
-                }),
-                ['TS.INCRBY', 'key', '1', 'RETENTION', '1']
-            );
-        });
+    it('with RETENTION', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          RETENTION: 1
+        }),
+        ['TS.INCRBY', 'key', '1', 'RETENTION', '1']
+      );
+    });
 
-        it('with UNCOMPRESSED', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    UNCOMPRESSED: true
-                }),
-                ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED']
-            );
-        });
+    it('with UNCOMPRESSED', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          UNCOMPRESSED: true
+        }),
+        ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED']
+      );
+    });
 
-        it('without UNCOMPRESSED', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    UNCOMPRESSED: false
-                }),
-                ['TS.INCRBY', 'key', '1']
-            );
-        });
+    it('without UNCOMPRESSED', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          UNCOMPRESSED: false
+        }),
+        ['TS.INCRBY', 'key', '1']
+      );
+    });
 
-        it('with CHUNK_SIZE', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    CHUNK_SIZE: 1
-                }),
-                ['TS.INCRBY', 'key', '1', 'CHUNK_SIZE', '1']
-            );
-        });
+    it('with CHUNK_SIZE', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          CHUNK_SIZE: 1
+        }),
+        ['TS.INCRBY', 'key', '1', 'CHUNK_SIZE', '1']
+      );
+    });
 
-        it('with LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.INCRBY', 'key', '1', 'LABELS', 'label', 'value']
-            );
-        });
+    it('with LABELS', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          LABELS: { label: 'value' }
+        }),
+        ['TS.INCRBY', 'key', '1', 'LABELS', 'label', 'value']
+      );
+    });
 
-        it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => {
-            assert.deepEqual(
-                transformArguments('key', 1, {
-                    TIMESTAMP: '*',
-                    RETENTION: 1,
-                    UNCOMPRESSED: true,
-                    CHUNK_SIZE: 1,
-                    LABELS: { label: 'value' }
-                }),
-                ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED',
-                'CHUNK_SIZE', '1', 'LABELS', 'label', 'value']
-            );
-        });
+    it ('with IGNORE', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          IGNORE: { 
+            maxTimeDiff: 1,
+            maxValDiff: 1
+          }
+        }),
+        ['TS.INCRBY', 'key', '1', 'IGNORE', '1', '1']
+      )
+    });
+  
+    it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => {
+      assert.deepEqual(
+        parseArgs(INCRBY, 'key', 1, {
+          TIMESTAMP: '*',
+          RETENTION: 1,
+          UNCOMPRESSED: true,
+          CHUNK_SIZE: 1,
+          LABELS: { label: 'value' },
+          IGNORE: { maxTimeDiff: 1, maxValDiff: 1 }
+        }),
+        ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED',
+          'CHUNK_SIZE', '1', 'LABELS', 'label', 'value', 'IGNORE', '1', '1']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.incrBy', async client => {
-        assert.equal(
-            await client.ts.incrBy('key', 1, {
-                TIMESTAMP: 0
-            }),
-            0
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ts.incrBy', async client => {
+    assert.equal(
+      typeof await client.ts.incrBy('key', 1),
+      'number'
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts
index 1f96801305f..41f11b0d7f5 100644
--- a/packages/time-series/lib/commands/INCRBY.ts
+++ b/packages/time-series/lib/commands/INCRBY.ts
@@ -1,10 +1,61 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { IncrDecrOptions, transformIncrDecrArguments } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { Timestamp, transformTimestampArgument, parseRetentionArgument, parseChunkSizeArgument, Labels, parseLabelsArgument, parseIgnoreArgument } from './helpers';
+import { TsIgnoreOptions } from './ADD';
 
-export const FIRST_KEY_INDEX = 1;
+export interface TsIncrByOptions {
+  TIMESTAMP?: Timestamp;
+  RETENTION?: number;
+  UNCOMPRESSED?: boolean;
+  CHUNK_SIZE?: number;
+  LABELS?: Labels;
+  IGNORE?: TsIgnoreOptions;
+}
+
+/**
+ * Parses arguments for incrementing a time series value
+ * @param parser - The command parser
+ * @param key - The key name of the time series
+ * @param value - The value to increment by
+ * @param options - Optional parameters for the command
+ */
+export function parseIncrByArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  value: number,
+  options?: TsIncrByOptions
+) {
+  parser.pushKey(key);
+  parser.push(value.toString());
+
+  if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) {
+    parser.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP));
+  }
+
+  parseRetentionArgument(parser, options?.RETENTION);
 
-export function transformArguments(key: string, value: number, options?: IncrDecrOptions): RedisCommandArguments {
-    return transformIncrDecrArguments('TS.INCRBY', key, value, options);
+  if (options?.UNCOMPRESSED) {
+    parser.push('UNCOMPRESSED');
+  }
+
+  parseChunkSizeArgument(parser, options?.CHUNK_SIZE);
+
+  parseLabelsArgument(parser, options?.LABELS);
+
+  parseIgnoreArgument(parser, options?.IGNORE);
 }
 
-export declare function transformReply(): number;
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Increases the value of a time series by a given amount
+   * @param args - Arguments passed to the {@link parseIncrByArguments} function
+   */
+  parseCommand(...args: Parameters<typeof parseIncrByArguments>) {
+    const parser = args[0];
+
+    parser.push('TS.INCRBY');
+    parseIncrByArguments(...args);
+  },
+  transformReply: undefined as unknown as () => NumberReply
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/INFO.spec.ts b/packages/time-series/lib/commands/INFO.spec.ts
index c02cdd6da4d..994cb281915 100644
--- a/packages/time-series/lib/commands/INFO.spec.ts
+++ b/packages/time-series/lib/commands/INFO.spec.ts
@@ -1,12 +1,14 @@
-import { strict as assert } from 'assert';
-import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.';
+import { strict as assert } from 'node:assert';
+import { TIME_SERIES_DUPLICATE_POLICIES } from './helpers';
 import testUtils, { GLOBAL } from '../test-utils';
-import { InfoReply, transformArguments } from './INFO';
+import INFO, { InfoReply } from './INFO';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('INFO', () => {
+describe('TS.INFO', () => {
     it('transformArguments', () => {
         assert.deepEqual(
-            transformArguments('key'),
+            parseArgs(INFO, 'key'),
             ['TS.INFO', 'key']
         );
     });
@@ -15,14 +17,14 @@ describe('INFO', () => {
         await Promise.all([
             client.ts.create('key', {
                 LABELS: { id: '1' },
-                DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.LAST
+                DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.LAST
             }),
             client.ts.create('key2'),
-            client.ts.createRule('key', 'key2', TimeSeriesAggregationType.COUNT, 5),
+            client.ts.createRule('key', 'key2', TIME_SERIES_AGGREGATION_TYPE.COUNT, 5),
             client.ts.add('key', 1, 10)
         ]);
 
-        assertInfo(await client.ts.info('key'));
+        assertInfo(await client.ts.info('key') as any);
     }, GLOBAL.SERVERS.OPEN);
 });
 
diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts
index 25ce3ef54ea..2e908a9d32d 100644
--- a/packages/time-series/lib/commands/INFO.ts
+++ b/packages/time-series/lib/commands/INFO.ts
@@ -1,82 +1,134 @@
-import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, BlobStringReply, Command, DoubleReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types";
+import { TimeSeriesDuplicatePolicies } from "./helpers";
+import { TimeSeriesAggregationType } from "./CREATERULE";
+import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const FIRST_KEY_INDEX = 1;
+export type InfoRawReplyTypes = SimpleStringReply | 
+  NumberReply | 
+  TimeSeriesDuplicatePolicies | null |
+  Array<[name: BlobStringReply, value: BlobStringReply]> |
+  BlobStringReply |
+  Array<[key: BlobStringReply, timeBucket: NumberReply, aggregationType: TimeSeriesAggregationType]> |
+  DoubleReply
 
-export const IS_READ_ONLY = true;
+export type InfoRawReply = Array<InfoRawReplyTypes>;
 
-export function transformArguments(key: string): Array<string> {
-    return ['TS.INFO', key];
-}
-
-export type InfoRawReply = [
-    'totalSamples',
-    number,
-    'memoryUsage',
-    number,
-    'firstTimestamp',
-    number,
-    'lastTimestamp',
-    number,
-    'retentionTime',
-    number,
-    'chunkCount',
-    number,
-    'chunkSize',
-    number,
-    'chunkType',
-    string,
-    'duplicatePolicy',
-    TimeSeriesDuplicatePolicies | null,
-    'labels',
-    Array<[name: string, value: string]>,
-    'sourceKey',
-    string | null,
-    'rules',
-    Array<[key: string, timeBucket: number, aggregationType: TimeSeriesAggregationType]>
+export type InfoRawReplyOld = [
+  'totalSamples',
+  NumberReply,
+  'memoryUsage',
+  NumberReply,
+  'firstTimestamp',
+  NumberReply,
+  'lastTimestamp',
+  NumberReply,
+  'retentionTime',
+  NumberReply,
+  'chunkCount',
+  NumberReply,
+  'chunkSize',
+  NumberReply,
+  'chunkType',
+  SimpleStringReply,
+  'duplicatePolicy',
+  TimeSeriesDuplicatePolicies | null,
+  'labels',
+  ArrayReply<[name: BlobStringReply, value: BlobStringReply]>,
+  'sourceKey',
+  BlobStringReply | null,
+  'rules',
+  ArrayReply<[key: BlobStringReply, timeBucket: NumberReply, aggregationType: TimeSeriesAggregationType]>,
+  'ignoreMaxTimeDiff',
+  NumberReply,
+  'ignoreMaxValDiff',
+  DoubleReply,
 ];
 
 export interface InfoReply {
-    totalSamples: number;
-    memoryUsage: number;
-    firstTimestamp: number;
-    lastTimestamp: number;
-    retentionTime: number;
-    chunkCount: number;
-    chunkSize: number;
-    chunkType: string;
-    duplicatePolicy: TimeSeriesDuplicatePolicies | null;
-    labels: Array<{
-        name: string;
-        value: string;
-    }>;
-    sourceKey: string | null;
-    rules: Array<{
-        key: string;
-        timeBucket: number;
-        aggregationType: TimeSeriesAggregationType
-    }>;
+  totalSamples: NumberReply;
+  memoryUsage: NumberReply;
+  firstTimestamp: NumberReply;
+  lastTimestamp: NumberReply;
+  retentionTime: NumberReply;
+  chunkCount: NumberReply;
+  chunkSize: NumberReply;
+  chunkType: SimpleStringReply;
+  duplicatePolicy: TimeSeriesDuplicatePolicies | null;
+  labels: Array<{
+    name: BlobStringReply;
+    value: BlobStringReply;
+  }>;
+  sourceKey: BlobStringReply | null;
+  rules: Array<{
+    key: BlobStringReply;
+    timeBucket: NumberReply;
+    aggregationType: TimeSeriesAggregationType
+  }>;
+  /** Added in 7.4 */
+  ignoreMaxTimeDiff: NumberReply;
+  /** Added in 7.4 */
+  ignoreMaxValDiff: DoubleReply;
 }
 
-export function transformReply(reply: InfoRawReply): InfoReply {
-    return {
-        totalSamples: reply[1],
-        memoryUsage: reply[3],
-        firstTimestamp: reply[5],
-        lastTimestamp: reply[7],
-        retentionTime: reply[9],
-        chunkCount: reply[11],
-        chunkSize: reply[13],
-        chunkType: reply[15],
-        duplicatePolicy: reply[17],
-        labels: reply[19].map(([name, value]) => ({
-            name,
-            value
-        })),
-        sourceKey: reply[21],
-        rules: reply[23].map(([key, timeBucket, aggregationType]) => ({
-            key,
-            timeBucket,
-            aggregationType
-        }))
-    };
-}
+export default {
+    IS_READ_ONLY: true,
+    /**
+     * Gets information about a time series
+     * @param parser - The command parser
+     * @param key - The key name of the time series
+     */
+    parseCommand(parser: CommandParser, key: string) {
+      parser.push('TS.INFO');
+      parser.pushKey(key);
+    },
+    transformReply: {
+      2: (reply: InfoRawReply, _, typeMapping?: TypeMapping): InfoReply => {
+        const ret = {} as any;
+
+        for (let i=0; i < reply.length; i += 2) {
+          const key = (reply[i] as any).toString();
+
+          switch (key) {
+            case 'totalSamples':
+            case 'memoryUsage':
+            case 'firstTimestamp':
+            case 'lastTimestamp':
+            case 'retentionTime':
+            case 'chunkCount':
+            case 'chunkSize':
+            case 'chunkType':
+            case 'duplicatePolicy':
+            case 'sourceKey':
+            case 'ignoreMaxTimeDiff':
+              ret[key] = reply[i+1];
+              break;
+            case 'labels':
+              ret[key] = (reply[i+1] as Array<[name: BlobStringReply, value: BlobStringReply]>).map(
+                ([name, value]) => ({
+                  name,
+                  value
+                })
+              );
+              break;
+            case 'rules':
+              ret[key] = (reply[i+1] as Array<[key: BlobStringReply, timeBucket: NumberReply, aggregationType: TimeSeriesAggregationType]>).map(
+                ([key, timeBucket, aggregationType]) => ({
+                  key,
+                  timeBucket,
+                  aggregationType
+                })
+              );
+              break;
+            case 'ignoreMaxValDiff':
+              ret[key] = transformDoubleReply[2](reply[27] as unknown as BlobStringReply, undefined, typeMapping);
+              break;
+          }
+        }
+
+        return ret;
+      },
+      3: undefined as unknown as () => ReplyUnion
+    },
+    unstableResp3: true
+  } as const satisfies Command;
diff --git a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts
index 666689f5194..ff9d6aa3c72 100644
--- a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts
+++ b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts
@@ -1,30 +1,32 @@
-import { strict as assert } from 'assert';
-import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.';
+import { strict as assert } from 'node:assert';
+import { TIME_SERIES_DUPLICATE_POLICIES } from './helpers';
 import testUtils, { GLOBAL } from '../test-utils';
 import { assertInfo } from './INFO.spec';
-import { transformArguments } from './INFO_DEBUG';
+import INFO_DEBUG from './INFO_DEBUG';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('INFO_DEBUG', () => {
+describe('TS.INFO_DEBUG', () => {
     it('transformArguments', () => {
         assert.deepEqual(
-            transformArguments('key'),
+            parseArgs(INFO_DEBUG, 'key'),
             ['TS.INFO', 'key', 'DEBUG']
         );
     });
 
-    testUtils.testWithClient('client.ts.get', async client => {
+    testUtils.testWithClient('client.ts.infoDebug', async client => {
         await Promise.all([
             client.ts.create('key', {
                 LABELS: { id: '1' },
-                DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.LAST
+                DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.LAST
             }),
             client.ts.create('key2'),
-            client.ts.createRule('key', 'key2', TimeSeriesAggregationType.COUNT, 5),
+            client.ts.createRule('key', 'key2', TIME_SERIES_AGGREGATION_TYPE.COUNT, 5),
             client.ts.add('key', 1, 10)
         ]);
 
         const infoDebug = await client.ts.infoDebug('key');
-        assertInfo(infoDebug);
+        assertInfo(infoDebug as any);
         assert.equal(typeof infoDebug.keySelfName, 'string');
         assert.ok(Array.isArray(infoDebug.chunks));
         for (const chunk of infoDebug.chunks) {
diff --git a/packages/time-series/lib/commands/INFO_DEBUG.ts b/packages/time-series/lib/commands/INFO_DEBUG.ts
index 20d6ff5e242..bbdee4924ff 100644
--- a/packages/time-series/lib/commands/INFO_DEBUG.ts
+++ b/packages/time-series/lib/commands/INFO_DEBUG.ts
@@ -1,57 +1,82 @@
-import {
-    transformArguments as transformInfoArguments,
-    InfoRawReply,
-    InfoReply,
-    transformReply as transformInfoReply
-} from './INFO';
-
-export { IS_READ_ONLY, FIRST_KEY_INDEX } from './INFO';
-
-export function transformArguments(key: string): Array<string> {
-    const args = transformInfoArguments(key);
-    args.push('DEBUG');
-    return args;
-}
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { BlobStringReply, Command, NumberReply, SimpleStringReply, TypeMapping, ReplyUnion } from "@redis/client/dist/lib/RESP/types";
+import INFO, { InfoRawReply, InfoRawReplyTypes, InfoReply } from "./INFO";
+
+type chunkType = Array<[
+  'startTimestamp',
+  NumberReply,
+  'endTimestamp',
+  NumberReply,
+  'samples',
+  NumberReply,
+  'size',
+  NumberReply,
+  'bytesPerSample',
+  SimpleStringReply
+]>;
 
 type InfoDebugRawReply = [
-    ...InfoRawReply,
-    'keySelfName',
-    string,
-    'chunks',
-    Array<[
-        'startTimestamp',
-        number,
-        'endTimestamp',
-        number,
-        'samples',
-        number,
-        'size',
-        number,
-        'bytesPerSample',
-        string
-    ]>
+  ...InfoRawReply,
+  'keySelfName',
+  BlobStringReply,
+  'Chunks',
+  chunkType
 ];
 
-interface InfoDebugReply extends InfoReply {
-    keySelfName: string;
-    chunks: Array<{
-        startTimestamp: number;
-        endTimestamp: number;
-        samples: number;
-        size: number;
-        bytesPerSample: string;
-    }>;
-}
+export type InfoDebugRawReplyType = InfoRawReplyTypes | chunkType
 
-export function transformReply(rawReply: InfoDebugRawReply): InfoDebugReply {
-    const reply = transformInfoReply(rawReply as unknown as InfoRawReply);
-    (reply as InfoDebugReply).keySelfName = rawReply[25];
-    (reply as InfoDebugReply).chunks = rawReply[27].map(chunk => ({
-        startTimestamp: chunk[1],
-        endTimestamp: chunk[3],
-        samples: chunk[5],
-        size: chunk[7],
-        bytesPerSample: chunk[9]
-    }));
-    return reply as InfoDebugReply;
+export interface InfoDebugReply extends InfoReply {
+  keySelfName: BlobStringReply,
+  chunks: Array<{
+    startTimestamp: NumberReply;
+    endTimestamp: NumberReply;
+    samples: NumberReply;
+    size: NumberReply;
+    bytesPerSample: SimpleStringReply;
+  }>;
 }
+
+export default {
+  IS_READ_ONLY: INFO.IS_READ_ONLY,
+  /**
+   * Gets debug information about a time series
+   * @param parser - The command parser
+   * @param key - The key name of the time series
+   */
+  parseCommand(parser: CommandParser, key: string) {
+    INFO.parseCommand(parser, key);
+    parser.push('DEBUG');
+  },
+  transformReply: {
+    2: (reply: InfoDebugRawReply, _, typeMapping?: TypeMapping): InfoDebugReply => {
+      const ret = INFO.transformReply[2](reply as unknown as InfoRawReply, _, typeMapping) as any;
+
+      for (let i=0; i < reply.length; i += 2) {
+        const key = (reply[i] as any).toString();
+
+        switch (key) {
+          case 'keySelfName': {
+            ret[key] = reply[i+1];
+            break;
+          }
+          case 'Chunks': {
+            ret['chunks'] = (reply[i+1] as chunkType).map(
+              chunk => ({
+                startTimestamp: chunk[1],
+                endTimestamp: chunk[3],
+                samples: chunk[5],
+                size: chunk[7],
+                bytesPerSample: chunk[9]
+              })
+            );
+            break;
+          }
+        }
+      }
+
+      return ret;
+    },
+    3: undefined as unknown as () => ReplyUnion
+  },
+  unstableResp3: true
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MADD.spec.ts b/packages/time-series/lib/commands/MADD.spec.ts
index eed014f2b14..8bf8e27fdb3 100644
--- a/packages/time-series/lib/commands/MADD.spec.ts
+++ b/packages/time-series/lib/commands/MADD.spec.ts
@@ -1,39 +1,42 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MADD';
+import MADD from './MADD';
+import { SimpleError } from '@redis/client/lib/errors';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MADD', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments([{
-                key: '1',
-                timestamp: 0,
-                value: 0
-            }, {
-                key: '2',
-                timestamp: 1,
-                value: 1
-            }]),
-            ['TS.MADD', '1', '0', '0', '2', '1', '1']
-        );
-    });
+describe('TS.MADD', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MADD, [{
+        key: '1',
+        timestamp: 0,
+        value: 0
+      }, {
+        key: '2',
+        timestamp: 1,
+        value: 1
+      }]),
+      ['TS.MADD', '1', '0', '0', '2', '1', '1']
+    );
+  });
 
-    // Should we check empty array?
+  testUtils.testWithClient('client.ts.mAdd', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.create('key'),
+      client.ts.mAdd([{
+        key: 'key',
+        timestamp: 0,
+        value: 1
+      }, {
+        key: 'key',
+        timestamp: 0,
+        value: 1
+      }])
+    ]);
 
-    testUtils.testWithClient('client.ts.mAdd', async client => {
-        await client.ts.create('key');
-
-        assert.deepEqual(
-            await client.ts.mAdd([{
-                key: 'key',
-                timestamp: 0,
-                value: 0
-            }, {
-                key: 'key',
-                timestamp: 1,
-                value: 1
-            }]),
-            [0, 1]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.ok(Array.isArray(reply));
+    assert.equal(reply.length, 2);
+    assert.equal(reply[0], 0);
+    assert.ok(reply[1] instanceof SimpleError);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts
index 426eae7e3f3..d0b36ea373f 100644
--- a/packages/time-series/lib/commands/MADD.ts
+++ b/packages/time-series/lib/commands/MADD.ts
@@ -1,25 +1,27 @@
-import { Timestamp, transformTimestampArgument } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Timestamp, transformTimestampArgument } from './helpers';
+import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/dist/lib/RESP/types';
 
-export const FIRST_KEY_INDEX = 1;
-
-interface MAddSample {
-    key: string;
-    timestamp: Timestamp;
-    value: number;
+export interface TsMAddSample {
+  key: string;
+  timestamp: Timestamp;
+  value: number;
 }
 
-export function transformArguments(toAdd: Array<MAddSample>): Array<string> {
-    const args = ['TS.MADD'];
+export default {
+  IS_READ_ONLY: false,
+  /**
+   * Adds multiple samples to multiple time series
+   * @param parser - The command parser
+   * @param toAdd - Array of samples to add to different time series
+   */
+  parseCommand(parser: CommandParser, toAdd: Array<TsMAddSample>) {
+    parser.push('TS.MADD');
 
     for (const { key, timestamp, value } of toAdd) {
-        args.push(
-            key,
-            transformTimestampArgument(timestamp),
-            value.toString()
-        );
+      parser.pushKey(key);
+      parser.push(transformTimestampArgument(timestamp), value.toString());
     }
-
-    return args;
-}
-
-export declare function transformReply(): Array<number>;
+  },
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply | SimpleErrorReply>
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MGET.spec.ts b/packages/time-series/lib/commands/MGET.spec.ts
index 61da3b96383..ba2e571be49 100644
--- a/packages/time-series/lib/commands/MGET.spec.ts
+++ b/packages/time-series/lib/commands/MGET.spec.ts
@@ -1,40 +1,46 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MGET';
+import MGET from './MGET';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MGET', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('label=value'),
-                ['TS.MGET', 'FILTER', 'label=value']
-            );
-        });
+describe('TS.MGET', () => {
+  describe('transformArguments', () => {
+    it('without options', () => {
+      assert.deepEqual(
+        parseArgs(MGET, 'label=value'),
+        ['TS.MGET', 'FILTER', 'label=value']
+      );
+    });
 
-        it('with LATEST', () => {
-            assert.deepEqual(
-                transformArguments('label=value', {
-                    LATEST: true
-                }),
-                ['TS.MGET', 'LATEST', 'FILTER', 'label=value']
-            );
-        });
+    it('with LATEST', () => {
+      assert.deepEqual(
+        parseArgs(MGET, 'label=value', {
+          LATEST: true
+        }),
+        ['TS.MGET', 'LATEST', 'FILTER', 'label=value']
+      );
     });
+  });
 
-    testUtils.testWithClient('client.ts.mGet', async client => {
-        await client.ts.add('key', 0, 0, {
-            LABELS: { label: 'value' }
-        });
+  testUtils.testWithClient('client.ts.mGet', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mGet('label=value')
+    ]);
 
-        assert.deepEqual(
-            await client.ts.mGet('label=value'),
-            [{
-                key: 'key',
-                sample: {
-                    timestamp: 0,
-                    value: 0
-                }
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepStrictEqual(reply, Object.create(null, {
+      key: {
+        configurable: true,
+        enumerable: true,
+        value: {
+          sample: {
+            timestamp: 0,
+            value: 0
+          }
+        }
+      }
+    }));
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts
index 67315722eb6..023c0bda2d4 100644
--- a/packages/time-series/lib/commands/MGET.ts
+++ b/packages/time-series/lib/commands/MGET.ts
@@ -1,31 +1,77 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { Filter, pushFilterArgument, pushLatestArgument, RawLabels, SampleRawReply, SampleReply, transformSampleReply } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { resp2MapToValue, resp3MapToValue, SampleRawReply, transformSampleReply } from './helpers';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const IS_READ_ONLY = true;
+export interface TsMGetOptions {
+  LATEST?: boolean;
+}
 
-export interface MGetOptions {
-    LATEST?: boolean;
+/**
+ * Adds LATEST argument to command if specified
+ * @param parser - The command parser
+ * @param latest - Whether to include the LATEST argument
+ */
+export function parseLatestArgument(parser: CommandParser, latest?: boolean) {
+  if (latest) {
+    parser.push('LATEST');
+  }
 }
 
-export function transformArguments(filter: Filter, options?: MGetOptions): RedisCommandArguments {
-    const args = pushLatestArgument(['TS.MGET'], options?.LATEST);
-    return pushFilterArgument(args, filter);
+/**
+ * Adds FILTER argument to command
+ * @param parser - The command parser
+ * @param filter - Filter to match time series keys
+ */
+export function parseFilterArgument(parser: CommandParser, filter: RedisVariadicArgument) {
+  parser.push('FILTER');
+  parser.pushVariadic(filter);
 }
 
-export type MGetRawReply = Array<[
-    key: string,
-    labels: RawLabels,
-    sample: SampleRawReply
-]>;
+export type MGetRawReply2 = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: never,
+    sample: Resp2Reply<SampleRawReply>
+  ]>
+>;
 
-export interface MGetReply {
-    key: string,
-    sample: SampleReply
-}
+export type MGetRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: never,
+    sample: SampleRawReply
+  ]>
+>;
 
-export function transformReply(reply: MGetRawReply): Array<MGetReply> {
-    return reply.map(([key, _, sample]) => ({
-        key,
-        sample: transformSampleReply(sample)
-    }));
-}
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets the last samples matching a specific filter from multiple time series
+   * @param parser - The command parser
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetOptions) {
+    parser.push('TS.MGET');
+    parseLatestArgument(parser, options?.LATEST);
+    parseFilterArgument(parser, filter);
+  },
+  transformReply: {
+    2(reply: MGetRawReply2, _, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([,, sample]) => {
+        return {
+          sample: transformSampleReply[2](sample)
+        };
+      }, typeMapping);
+    },
+    3(reply: MGetRawReply3) {
+      return resp3MapToValue(reply, ([, sample]) => {
+        return {
+          sample: transformSampleReply[3](sample)
+        };
+      });
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts
new file mode 100644
index 00000000000..d79c463fc7d
--- /dev/null
+++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts
@@ -0,0 +1,47 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MGET_SELECTED_LABELS from './MGET_SELECTED_LABELS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MGET_SELECTED_LABELS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MGET_SELECTED_LABELS, 'label=value', 'label'),
+      ['TS.MGET', 'SELECTED_LABELS', 'label', 'FILTER', 'label=value']
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mGetSelectedLabels', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mGetSelectedLabels('label=value', ['label', 'NX'])
+    ]);
+    
+    assert.deepStrictEqual(reply, Object.create(null, {
+      key: {
+        configurable: true,
+        enumerable: true,
+        value: {
+          labels: Object.create(null, {
+            label: {
+              configurable: true,
+              enumerable: true,
+              value: 'value'
+            },
+            NX: {
+              configurable: true,
+              enumerable: true,
+              value: null
+            }
+          }),
+          sample: {
+            timestamp: 0,
+            value: 0
+          }
+        }
+      }
+    }));
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts
new file mode 100644
index 00000000000..a13fcbeaa56
--- /dev/null
+++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts
@@ -0,0 +1,24 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, BlobStringReply, NullReply } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { TsMGetOptions, parseLatestArgument, parseFilterArgument } from './MGET';
+import { parseSelectedLabelsArguments } from './helpers';
+import { createTransformMGetLabelsReply } from './MGET_WITHLABELS';
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets the last samples matching a specific filter with selected labels
+   * @param parser - The command parser
+   * @param filter - Filter to match time series keys
+   * @param selectedLabels - Labels to include in the output
+   * @param options - Optional parameters for the command
+   */
+  parseCommand(parser: CommandParser, filter: RedisVariadicArgument, selectedLabels: RedisVariadicArgument, options?: TsMGetOptions) {
+    parser.push('TS.MGET');
+    parseLatestArgument(parser, options?.LATEST);
+    parseSelectedLabelsArguments(parser, selectedLabels);
+    parseFilterArgument(parser, filter);
+  },
+  transformReply: createTransformMGetLabelsReply<BlobStringReply | NullReply>(),
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts
index 55fcfde409d..33fc5308444 100644
--- a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts
+++ b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts
@@ -1,39 +1,42 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MGET_WITHLABELS';
+import MGET_WITHLABELS from './MGET_WITHLABELS';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MGET_WITHLABELS', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('label=value'),
-                ['TS.MGET', 'WITHLABELS', 'FILTER', 'label=value']
-            );
-        });
+describe('TS.MGET_WITHLABELS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MGET_WITHLABELS, 'label=value'),
+      ['TS.MGET', 'WITHLABELS', 'FILTER', 'label=value']
+    );
+  });
 
-        it('with SELECTED_LABELS', () => {
-            assert.deepEqual(
-                transformArguments('label=value', { SELECTED_LABELS: 'label' }),
-                ['TS.MGET', 'SELECTED_LABELS', 'label', 'FILTER', 'label=value']
-            );
-        });
-    });
-
-    testUtils.testWithClient('client.ts.mGetWithLabels', async client => {
-        await client.ts.add('key', 0, 0, {
-            LABELS: { label: 'value' }
-        });
-
-        assert.deepEqual(
-            await client.ts.mGetWithLabels('label=value'),
-            [{
-                key: 'key',
-                labels: { label: 'value'},
-                sample: {
-                    timestamp: 0,
-                    value: 0
-                }
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+  testUtils.testWithClient('client.ts.mGetWithLabels', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mGetWithLabels('label=value')
+    ]);
+    
+    assert.deepStrictEqual(reply, Object.create(null, {
+      key: {
+        configurable: true,
+        enumerable: true,
+        value: {
+          labels: Object.create(null, {
+            label: {
+              configurable: true,
+              enumerable: true,
+              value: 'value'
+            }
+          }),
+          sample: {
+            timestamp: 0,
+            value: 0
+          }
+        }
+      }
+    }));
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts
index 232c17a0ada..aa9b5687eec 100644
--- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts
+++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts
@@ -1,37 +1,68 @@
-import {
-    SelectedLabels,
-    pushWithLabelsArgument,
-    Labels,
-    transformLablesReply,
-    transformSampleReply,
-    Filter,
-    pushFilterArgument
-} from '.';
-import { MGetOptions, MGetRawReply, MGetReply } from './MGET';
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { TsMGetOptions, parseLatestArgument, parseFilterArgument } from './MGET';
+import { RawLabelValue, resp2MapToValue, resp3MapToValue, SampleRawReply, transformRESP2Labels, transformSampleReply } from './helpers';
 
-export const IS_READ_ONLY = true;
-
-interface MGetWithLabelsOptions extends MGetOptions {
-    SELECTED_LABELS?: SelectedLabels;
+export interface TsMGetWithLabelsOptions extends TsMGetOptions {
+  SELECTED_LABELS?: RedisVariadicArgument;
 }
 
-export function transformArguments(
-    filter: Filter,
-    options?: MGetWithLabelsOptions
-): RedisCommandArguments {
-    const args = pushWithLabelsArgument(['TS.MGET'], options?.SELECTED_LABELS);
-    return pushFilterArgument(args, filter);
-}
+export type MGetLabelsRawReply2<T extends RawLabelValue> = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: ArrayReply<
+      TuplesReply<[
+        label: BlobStringReply,
+        value: T
+      ]>
+    >,
+    sample: Resp2Reply<SampleRawReply>
+  ]>
+>;
 
-export interface MGetWithLabelsReply extends MGetReply {
-    labels: Labels;
-};
+export type MGetLabelsRawReply3<T extends RawLabelValue> = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: MapReply<BlobStringReply, T>,
+    sample: SampleRawReply
+  ]>
+>;
 
-export function transformReply(reply: MGetRawReply): Array<MGetWithLabelsReply> {
-    return reply.map(([key, labels, sample]) => ({
-        key,
-        labels: transformLablesReply(labels),
-        sample: transformSampleReply(sample)
-    }));
+export function createTransformMGetLabelsReply<T extends RawLabelValue>() {
+  return {
+    2(reply: MGetLabelsRawReply2<T>, _, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([, labels, sample]) => {
+        return {
+          labels: transformRESP2Labels(labels),
+          sample: transformSampleReply[2](sample)
+        };
+      }, typeMapping);
+    },
+    3(reply: MGetLabelsRawReply3<T>) {
+      return resp3MapToValue(reply, ([labels, sample]) => {
+        return {
+          labels,
+          sample: transformSampleReply[3](sample)
+        };
+      });
+    }
+  } satisfies Command['transformReply'];
 }
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets the last samples matching a specific filter with labels
+   * @param parser - The command parser
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetWithLabelsOptions) {
+    parser.push('TS.MGET');
+    parseLatestArgument(parser, options?.LATEST);
+    parser.push('WITHLABELS');
+    parseFilterArgument(parser, filter);
+  },
+  transformReply: createTransformMGetLabelsReply<BlobStringReply>(),
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MRANGE.spec.ts b/packages/time-series/lib/commands/MRANGE.spec.ts
index 4228cc06fb7..94c8e72983a 100644
--- a/packages/time-series/lib/commands/MRANGE.spec.ts
+++ b/packages/time-series/lib/commands/MRANGE.spec.ts
@@ -1,50 +1,63 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MRANGE';
-import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
+import MRANGE from './MRANGE';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MRANGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('-', '+', 'label=value', {
-                FILTER_BY_TS: [0],
-                FILTER_BY_VALUE: {
-                    min: 0,
-                    max: 1
-                },
-                COUNT: 1,
-                ALIGN: '-',
-                AGGREGATION: {
-                    type: TimeSeriesAggregationType.AVERAGE,
-                    timeBucket: 1
-                },
-                GROUPBY: {
-                    label: 'label',
-                    reducer: TimeSeriesReducers.SUM
-                },
-            }),
-            ['TS.MRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
-            'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'FILTER', 'label=value',
-            'GROUPBY', 'label', 'REDUCE', 'SUM']
-        );
-    });
+describe('TS.MRANGE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MRANGE, '-', '+', 'label=value', {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-',
+        'AGGREGATION', 'AVG', '1',
+        'FILTER', 'label=value'
+      ]
+    );
+  });
 
-    testUtils.testWithClient('client.ts.mRange', async client => {
-        await client.ts.add('key', 0, 0, {
-            LABELS: { label: 'value'}
-        });
+  testUtils.testWithClient('client.ts.mRange', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: {
+          label: 'value'
+        }
+      }),
+      client.ts.mRange('-', '+', 'label=value', {
+        COUNT: 1
+      })
+    ]);
 
-        assert.deepEqual(
-            await client.ts.mRange('-', '+', 'label=value', {
-                COUNT: 1
-            }),
-            [{
-                key: 'key',
-                samples: [{
-                    timestamp: 0,
-                    value: 0
-                }]
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        key: {
+          configurable: true,
+          enumerable: true,
+          value: [{
+            timestamp: 0,
+            value: 0
+          }]
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts
index d589ac0332a..8b9ec66e6e3 100644
--- a/packages/time-series/lib/commands/MRANGE.ts
+++ b/packages/time-series/lib/commands/MRANGE.ts
@@ -1,21 +1,73 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers';
+import { TsRangeOptions, parseRangeArguments } from './RANGE';
+import { parseFilterArgument } from './MGET';
 
-export const IS_READ_ONLY = true;
+export type TsMRangeRawReply2 = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: never, // empty array without WITHLABELS or SELECTED_LABELS
+    samples: ArrayReply<Resp2Reply<SampleRawReply>>
+  ]>
+>;
 
-export function transformArguments(
+export type TsMRangeRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: never, // empty hash without WITHLABELS or SELECTED_LABELS
+    metadata: never, // ?!
+    samples: ArrayReply<SampleRawReply>
+  ]>
+>;
+
+/**
+ * Creates a function that parses arguments for multi-range commands
+ * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
+ */
+export function createTransformMRangeArguments(command: RedisArgument) {
+  return (
+    parser: CommandParser,
     fromTimestamp: Timestamp,
     toTimestamp: Timestamp,
-    filters: Filter,
-    options?: MRangeOptions
-): RedisCommandArguments {
-    return pushMRangeArguments(
-        ['TS.MRANGE'],
-        fromTimestamp,
-        toTimestamp,
-        filters,
-        options
+    filter: RedisVariadicArgument,
+    options?: TsRangeOptions
+  ) => {
+    parser.push(command);
+    parseRangeArguments(
+      parser,
+      fromTimestamp,
+      toTimestamp,
+      options
     );
+  
+    parseFilterArgument(parser, filter);
+  };
 }
 
-export { transformMRangeReply as transformReply } from '.';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples for time series matching a specific filter within a time range
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeArguments('TS.MRANGE'),
+  transformReply: {
+    2(reply: TsMRangeRawReply2, _?: any, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([_key, _labels, samples]) => {
+        return transformSamplesReply[2](samples);
+      }, typeMapping);
+    },
+    3(reply: TsMRangeRawReply3) {
+      return resp3MapToValue(reply, ([_labels, _metadata, samples]) => {
+        return transformSamplesReply[3](samples);
+      });
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts
new file mode 100644
index 00000000000..bcdde20fe98
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts
@@ -0,0 +1,67 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MRANGE_GROUPBY, { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MRANGE_GROUPBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MRANGE_GROUPBY, '-', '+', 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      }, {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-', 'AGGREGATION', 'AVG', '1',
+        'FILTER', 'label=value',
+        'GROUPBY', 'label', 'REDUCE', 'AVG'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRangeGroupBy', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRangeGroupBy('-', '+', 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        'label=value': {
+          configurable: true,
+          enumerable: true,
+          value: {  
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts
new file mode 100644
index 00000000000..dc049276127
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts
@@ -0,0 +1,125 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument, TuplesToMapReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers';
+import { TsRangeOptions, parseRangeArguments } from './RANGE';
+import { parseFilterArgument } from './MGET';
+
+export const TIME_SERIES_REDUCERS = {
+  AVG: 'AVG',
+  SUM: 'SUM',
+  MIN: 'MIN',
+  MAX: 'MAX',
+  RANGE: 'RANGE',
+  COUNT: 'COUNT',
+  STD_P: 'STD.P',
+  STD_S: 'STD.S',
+  VAR_P: 'VAR.P',
+  VAR_S: 'VAR.S'
+} as const;
+
+export type TimeSeriesReducer = typeof TIME_SERIES_REDUCERS[keyof typeof TIME_SERIES_REDUCERS];
+
+export interface TsMRangeGroupBy {
+  label: RedisArgument;
+  REDUCE: TimeSeriesReducer;
+}
+
+/**
+ * Adds GROUPBY arguments to command
+ * @param parser - The command parser
+ * @param groupBy - Group by parameters
+ */
+export function parseGroupByArguments(parser: CommandParser, groupBy: TsMRangeGroupBy) {
+  parser.push('GROUPBY', groupBy.label, 'REDUCE', groupBy.REDUCE);
+}
+
+export type TsMRangeGroupByRawReply2 = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: never, // empty array without WITHLABELS or SELECTED_LABELS
+    samples: ArrayReply<Resp2Reply<SampleRawReply>>
+  ]>
+>;
+
+export type TsMRangeGroupByRawMetadataReply3 = TuplesToMapReply<[
+  [BlobStringReply<'sources'>, ArrayReply<BlobStringReply>]
+]>;
+
+export type TsMRangeGroupByRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: never, // empty hash without WITHLABELS or SELECTED_LABELS
+    metadata1: never, // ?!
+    metadata2: TsMRangeGroupByRawMetadataReply3,
+    samples: ArrayReply<SampleRawReply>
+  ]>
+>;
+
+/**
+ * Creates a function that parses arguments for multi-range commands with grouping
+ * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
+ */
+export function createTransformMRangeGroupByArguments(command: RedisArgument) {
+  return (
+    parser: CommandParser,
+    fromTimestamp: Timestamp,
+    toTimestamp: Timestamp,
+    filter: RedisVariadicArgument,
+    groupBy: TsMRangeGroupBy,
+    options?: TsRangeOptions
+  ) => {
+    parser.push(command);
+    parseRangeArguments(parser, fromTimestamp, toTimestamp, options)
+
+    parseFilterArgument(parser, filter);
+
+    parseGroupByArguments(parser, groupBy);
+  };
+}
+
+/**
+ * Extracts source keys from RESP3 metadata reply
+ * @param raw - Raw metadata from RESP3 reply
+ */
+export function extractResp3MRangeSources(raw: TsMRangeGroupByRawMetadataReply3) {
+  const unwrappedMetadata2 = raw as unknown as UnwrapReply<typeof raw>;
+  if (unwrappedMetadata2 instanceof Map) {
+    return unwrappedMetadata2.get('sources')!;
+  } else if (unwrappedMetadata2 instanceof Array) {
+    return unwrappedMetadata2[1];
+  } else {
+    return unwrappedMetadata2.sources;
+  }
+}
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples for time series matching a filter within a time range with grouping
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param groupBy - Group by parameters
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeGroupByArguments('TS.MRANGE'),
+  transformReply: {
+    2(reply: TsMRangeGroupByRawReply2, _?: any, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([_key, _labels, samples]) => {
+        return {
+          samples: transformSamplesReply[2](samples)
+        };
+      }, typeMapping);
+    },
+    3(reply: TsMRangeGroupByRawReply3) {
+      return resp3MapToValue(reply, ([_labels, _metadata1, metadata2, samples]) => {
+        return {
+          sources: extractResp3MRangeSources(metadata2),
+          samples: transformSamplesReply[3](samples)
+        };
+      });
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts
new file mode 100644
index 00000000000..92680dea375
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts
@@ -0,0 +1,73 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MRANGE_SELECTED_LABELS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MRANGE_SELECTED_LABELS, '-', '+', 'label', 'label=value', {
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MRANGE', '-', '+',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-', 'AGGREGATION', 'AVG', '1',
+        'SELECTED_LABELS', 'label',
+        'FILTER', 'label=value'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRangeSelectedLabels', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRangeSelectedLabels('-', '+', ['label', 'NX'], 'label=value', {
+        COUNT: 1
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        key: {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              },
+              NX: {
+                configurable: true,
+                enumerable: true,
+                value: null
+              }
+            }),
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts
new file mode 100644
index 00000000000..c9b737fd290
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts
@@ -0,0 +1,85 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from './helpers';
+import { TsRangeOptions, parseRangeArguments } from './RANGE';
+import { parseFilterArgument } from './MGET';
+
+export type TsMRangeSelectedLabelsRawReply2 = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: ArrayReply<TuplesReply<[
+      label: BlobStringReply,
+      value: BlobStringReply | NullReply
+    ]>>,
+    samples: ArrayReply<Resp2Reply<SampleRawReply>>
+  ]>
+>;
+
+export type TsMRangeSelectedLabelsRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: MapReply<BlobStringReply, BlobStringReply | NullReply>,
+    metadata: never, // ?!
+    samples: ArrayReply<SampleRawReply>
+  ]>
+>;
+
+/**
+ * Creates a function that parses arguments for multi-range commands with selected labels
+ * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
+ */
+export function createTransformMRangeSelectedLabelsArguments(command: RedisArgument) {
+  return (
+    parser: CommandParser,
+    fromTimestamp: Timestamp,
+    toTimestamp: Timestamp,
+    selectedLabels: RedisVariadicArgument,
+    filter: RedisVariadicArgument,
+    options?: TsRangeOptions
+  ) => {
+    parser.push(command);
+    parseRangeArguments(
+      parser,
+      fromTimestamp,
+      toTimestamp,
+      options
+    );
+  
+    parseSelectedLabelsArguments(parser, selectedLabels);
+  
+    parseFilterArgument(parser, filter);
+  };
+}
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples for time series matching a filter with selected labels
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param selectedLabels - Labels to include in the output
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MRANGE'),
+  transformReply: {
+    2(reply: TsMRangeSelectedLabelsRawReply2, _?: any, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([_key, labels, samples]) => {
+        return {
+          labels: transformRESP2Labels(labels, typeMapping),
+          samples: transformSamplesReply[2](samples)
+        };
+      }, typeMapping);
+    },
+    3(reply: TsMRangeSelectedLabelsRawReply3) {
+      return resp3MapToValue(reply, ([_key, labels, samples]) => {
+        return {
+          labels,
+          samples: transformSamplesReply[3](samples)
+        };
+      });
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts
new file mode 100644
index 00000000000..4e5b2b47094
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts
@@ -0,0 +1,81 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MRANGE_SELECTED_LABELS_GROUPBY from './MRANGE_SELECTED_LABELS_GROUPBY';
+import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MRANGE_SELECTED_LABELS_GROUPBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MRANGE_SELECTED_LABELS_GROUPBY, '-', '+', 'label', 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      }, {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-', 'AGGREGATION', 'AVG', '1',
+        'SELECTED_LABELS', 'label',
+        'FILTER', 'label=value',
+        'GROUPBY', 'label', 'REDUCE', 'AVG'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRangeSelectedLabelsGroupBy', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRangeSelectedLabelsGroupBy('-', '+', ['label', 'NX'], 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        'label=value': {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              },
+              NX: {
+                configurable: true,
+                enumerable: true,
+                value: null
+              }
+            }),
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts
new file mode 100644
index 00000000000..d2f94b82bb3
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts
@@ -0,0 +1,77 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgument, NullReply } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { parseSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers';
+import { TsRangeOptions, parseRangeArguments } from './RANGE';
+import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY';
+import { parseFilterArgument } from './MGET';
+import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS';
+
+export type TsMRangeWithLabelsGroupByRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: MapReply<BlobStringReply, BlobStringReply | NullReply>,
+    metadata: never, // ?!
+    metadata2: TsMRangeGroupByRawMetadataReply3,
+    samples: ArrayReply<SampleRawReply>
+  ]>
+>;
+
+/**
+ * Creates a function that parses arguments for multi-range commands with selected labels and grouping
+ * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
+ */
+export function createMRangeSelectedLabelsGroupByTransformArguments(
+  command: RedisArgument
+) {
+  return (
+    parser: CommandParser,
+    fromTimestamp: Timestamp,
+    toTimestamp: Timestamp,
+    selectedLabels: RedisVariadicArgument,
+    filter: RedisVariadicArgument,
+    groupBy: TsMRangeGroupBy,
+    options?: TsRangeOptions
+  ) => {
+    parser.push(command);
+    parseRangeArguments(
+      parser,
+      fromTimestamp,
+      toTimestamp,
+      options
+    );
+  
+    parseSelectedLabelsArguments(parser, selectedLabels);
+  
+    parseFilterArgument(parser, filter);
+  
+    parseGroupByArguments(parser, groupBy);
+  };
+}
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples for time series matching a filter with selected labels and grouping
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param selectedLabels - Labels to include in the output
+   * @param filter - Filter to match time series keys
+   * @param groupBy - Group by parameters
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MRANGE'),
+  transformReply: {
+    2: MRANGE_SELECTED_LABELS.transformReply[2],
+    3(reply: TsMRangeWithLabelsGroupByRawReply3) {
+      return resp3MapToValue(reply, ([labels, _metadata, metadata2, samples]) => {
+        return {
+          labels,
+          sources: extractResp3MRangeSources(metadata2),
+          samples: transformSamplesReply[3](samples)
+        };
+      });
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts
index 983114f840e..eab2e1fadbe 100644
--- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts
+++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts
@@ -1,52 +1,69 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MRANGE_WITHLABELS';
-import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
+import MRANGE_WITHLABELS from './MRANGE_WITHLABELS';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MRANGE_WITHLABELS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('-', '+', 'label=value', {
-                FILTER_BY_TS: [0],
-                FILTER_BY_VALUE: {
-                    min: 0,
-                    max: 1
-                },
-                SELECTED_LABELS: ['label'],
-                COUNT: 1,
-                ALIGN: '-',
-                AGGREGATION: {
-                    type: TimeSeriesAggregationType.AVERAGE,
-                    timeBucket: 1
-                },
-                GROUPBY: {
-                    label: 'label',
-                    reducer: TimeSeriesReducers.SUM
-                },
-            }),
-            ['TS.MRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
-            'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'SELECTED_LABELS', 'label',
-            'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'SUM']
-        );
-    });
+describe('TS.MRANGE_WITHLABELS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MRANGE_WITHLABELS, '-', '+', 'label=value', {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-',
+        'AGGREGATION', 'AVG', '1',
+        'WITHLABELS',
+        'FILTER', 'label=value'
+      ]
+    );
+  });
 
-    testUtils.testWithClient('client.ts.mRangeWithLabels', async client => {
-        await client.ts.add('key', 0, 0, {
-            LABELS: { label: 'value'}
-        });
+  testUtils.testWithClient('client.ts.mRangeWithLabels', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRangeWithLabels('-', '+', 'label=value')
+    ]);
 
-        assert.deepEqual(
-            await client.ts.mRangeWithLabels('-', '+', 'label=value', {
-                COUNT: 1
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        key: {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              }
             }),
-            [{
-                key: 'key',
-                labels: { label: 'value' },
-                samples: [{
-                    timestamp: 0,
-                    value: 0
-                }]
+            samples: [{
+              timestamp: 0,
+              value: 0
             }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts
index 16b7920e82c..01a3634cf4c 100644
--- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts
+++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts
@@ -1,21 +1,93 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers';
+import { TsRangeOptions, parseRangeArguments } from './RANGE';
+import { parseFilterArgument } from './MGET';
 
-export const IS_READ_ONLY = true;
+export type TsMRangeWithLabelsRawReply2 = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: ArrayReply<TuplesReply<[
+      label: BlobStringReply,
+      value: BlobStringReply
+    ]>>,
+    samples: ArrayReply<Resp2Reply<SampleRawReply>>
+  ]>
+>;
 
-export function transformArguments(
+export type TsMRangeWithLabelsRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: MapReply<BlobStringReply, BlobStringReply>,
+    metadata: never, // ?!
+    samples: ArrayReply<SampleRawReply>
+  ]>
+>;
+
+/**
+ * Creates a function that parses arguments for multi-range commands with labels
+ * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
+ */
+export function createTransformMRangeWithLabelsArguments(command: RedisArgument) {
+  return (
+    parser: CommandParser,
     fromTimestamp: Timestamp,
     toTimestamp: Timestamp,
-    filters: string | Array<string>,
-    options?: MRangeWithLabelsOptions
-): RedisCommandArguments {
-    return pushMRangeWithLabelsArguments(
-        ['TS.MRANGE'],
-        fromTimestamp,
-        toTimestamp,
-        filters,
-        options
+    filter: RedisVariadicArgument,
+    options?: TsRangeOptions
+  ) => {
+    parser.push(command);
+    parseRangeArguments(
+      parser,
+      fromTimestamp,
+      toTimestamp,
+      options
     );
+  
+    parser.push('WITHLABELS');
+  
+    parseFilterArgument(parser, filter);
+  };
 }
 
-export { transformMRangeWithLabelsReply as transformReply } from '.';
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples for time series matching a filter with labels
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeWithLabelsArguments('TS.MRANGE'),
+  transformReply: {
+    2(reply: TsMRangeWithLabelsRawReply2, _?: any, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([_key, labels, samples]) => {
+        const unwrappedLabels = labels as unknown as UnwrapReply<typeof labels>;
+        // TODO: use Map type mapping for labels
+        const labelsObject: Record<string, BlobStringReply> = Object.create(null);
+        for (const tuple of unwrappedLabels) {
+          const [key, value] = tuple as unknown as UnwrapReply<typeof tuple>;
+          const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
+          labelsObject[unwrappedKey.toString()] = value;
+        }
+
+        return {
+          labels: labelsObject,
+          samples: transformSamplesReply[2](samples)
+        };
+      }, typeMapping);
+    },
+    3(reply: TsMRangeWithLabelsRawReply3) {
+      return resp3MapToValue(reply, ([labels, _metadata, samples]) => {
+        return {
+          labels,
+          samples: transformSamplesReply[3](samples)
+        };
+      });
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts
new file mode 100644
index 00000000000..4a8b8fe707f
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts
@@ -0,0 +1,78 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MRANGE_WITHLABELS_GROUPBY from './MRANGE_WITHLABELS_GROUPBY';
+import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MRANGE_WITHLABELS_GROUPBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MRANGE_WITHLABELS_GROUPBY, '-', '+', 'label=value', {
+        label: 'label',
+        REDUCE: TIME_SERIES_REDUCERS.AVG
+      }, {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-',
+        'AGGREGATION', 'AVG', '1',
+        'WITHLABELS',
+        'FILTER', 'label=value',
+        'GROUPBY', 'label', 'REDUCE', 'AVG'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRangeWithLabelsGroupBy', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRangeWithLabelsGroupBy('-', '+', 'label=value', {
+        label: 'label',
+        REDUCE: TIME_SERIES_REDUCERS.AVG
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        'label=value': {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              }
+            }),
+            sources: ['key'],
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts
new file mode 100644
index 00000000000..08c70000f70
--- /dev/null
+++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts
@@ -0,0 +1,88 @@
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
+import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2LabelsWithSources, transformSamplesReply } from './helpers';
+import { TsRangeOptions, parseRangeArguments } from './RANGE';
+import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY';
+import { parseFilterArgument } from './MGET';
+
+export type TsMRangeWithLabelsGroupByRawReply2 = ArrayReply<
+  TuplesReply<[
+    key: BlobStringReply,
+    labels: ArrayReply<TuplesReply<[
+      label: BlobStringReply,
+      value: BlobStringReply
+    ]>>,
+    samples: ArrayReply<Resp2Reply<SampleRawReply>>
+  ]>
+>;
+
+export type TsMRangeWithLabelsGroupByRawReply3 = MapReply<
+  BlobStringReply,
+  TuplesReply<[
+    labels: MapReply<BlobStringReply, BlobStringReply>,
+    metadata: never, // ?!
+    metadata2: TsMRangeGroupByRawMetadataReply3,
+    samples: ArrayReply<SampleRawReply>
+  ]>
+>;
+
+export function createMRangeWithLabelsGroupByTransformArguments(command: RedisArgument) {
+  return (
+    parser: CommandParser,
+    fromTimestamp: Timestamp,
+    toTimestamp: Timestamp,
+    filter: RedisVariadicArgument,
+    groupBy: TsMRangeGroupBy,
+    options?: TsRangeOptions
+  ) => {
+    parser.push(command);
+    parseRangeArguments(
+      parser,
+      fromTimestamp,
+      toTimestamp,
+      options
+    );
+  
+    parser.push('WITHLABELS');
+  
+    parseFilterArgument(parser, filter);
+  
+    parseGroupByArguments(parser, groupBy);
+    };
+}
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples for time series matching a filter with labels and grouping
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param groupBy - Group by parameters
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MRANGE'),
+  transformReply: {
+    2(reply: TsMRangeWithLabelsGroupByRawReply2, _?: any, typeMapping?: TypeMapping) {
+      return resp2MapToValue(reply, ([_key, labels, samples]) => {
+        const transformed = transformRESP2LabelsWithSources(labels);
+        return {
+          labels: transformed.labels,
+          sources: transformed.sources,
+          samples: transformSamplesReply[2](samples)
+        };
+      }, typeMapping);
+    },
+    3(reply: TsMRangeWithLabelsGroupByRawReply3) {
+      return resp3MapToValue(reply, ([labels, _metadata, metadata2, samples]) => {
+        return {
+          labels,
+          sources: extractResp3MRangeSources(metadata2),
+          samples: transformSamplesReply[3](samples)
+        };
+      });
+    }
+  },
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MREVRANGE.spec.ts b/packages/time-series/lib/commands/MREVRANGE.spec.ts
index 6e5825d36d6..09051103f8b 100644
--- a/packages/time-series/lib/commands/MREVRANGE.spec.ts
+++ b/packages/time-series/lib/commands/MREVRANGE.spec.ts
@@ -1,50 +1,63 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MREVRANGE';
-import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
+import MREVRANGE from './MREVRANGE';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MREVRANGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('-', '+', 'label=value', {
-                FILTER_BY_TS: [0],
-                FILTER_BY_VALUE: {
-                    min: 0,
-                    max: 1
-                },
-                COUNT: 1,
-                ALIGN: '-',
-                AGGREGATION: {
-                    type: TimeSeriesAggregationType.AVERAGE,
-                    timeBucket: 1
-                },
-                GROUPBY: {
-                    label: 'label',
-                    reducer: TimeSeriesReducers.SUM
-                },
-            }),
-            ['TS.MREVRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
-            'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'FILTER', 'label=value',
-            'GROUPBY', 'label', 'REDUCE', 'SUM']
-        );
-    });
+describe('TS.MREVRANGE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MREVRANGE, '-', '+', 'label=value', {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MREVRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-',
+        'AGGREGATION', 'AVG', '1',
+        'FILTER', 'label=value'
+      ]
+    );
+  });
 
-    testUtils.testWithClient('client.ts.mRevRange', async client => {
-        await client.ts.add('key', 0, 0, {
-            LABELS: { label: 'value'}
-        });
+  testUtils.testWithClient('client.ts.mRevRange', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: {
+          label: 'value'
+        }
+      }),
+      client.ts.mRevRange('-', '+', 'label=value', {
+        COUNT: 1
+      })
+    ]);
 
-        assert.deepEqual(
-            await client.ts.mRevRange('-', '+', 'label=value', {
-                COUNT: 1
-            }),
-            [{
-                key: 'key',
-                samples: [{
-                    timestamp: 0,
-                    value: 0
-                }]
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        key: {
+          configurable: true,
+          enumerable: true,
+          value: [{
+            timestamp: 0,
+            value: 0
+          }]
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts
index 127c052ffe0..54bd5ca9cca 100644
--- a/packages/time-series/lib/commands/MREVRANGE.ts
+++ b/packages/time-series/lib/commands/MREVRANGE.ts
@@ -1,21 +1,17 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.';
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import MRANGE, { createTransformMRangeArguments } from './MRANGE';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    filters: Filter,
-    options?: MRangeOptions
-): RedisCommandArguments {
-    return pushMRangeArguments(
-        ['TS.MREVRANGE'],
-        fromTimestamp,
-        toTimestamp,
-        filters,
-        options
-    );
-}
-
-export { transformMRangeReply as transformReply } from '.';
+export default {
+  NOT_KEYED_COMMAND: MRANGE.NOT_KEYED_COMMAND,
+  IS_READ_ONLY: MRANGE.IS_READ_ONLY,
+  /**
+   * Gets samples for time series matching a specific filter within a time range (in reverse order)
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeArguments('TS.MREVRANGE'),
+  transformReply: MRANGE.transformReply,
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts
new file mode 100644
index 00000000000..d32d675ad0a
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts
@@ -0,0 +1,68 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MREVRANGE_GROUPBY from './MREVRANGE_GROUPBY';
+import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MREVRANGE_GROUPBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MREVRANGE_GROUPBY, '-', '+', 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      }, {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MREVRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-', 'AGGREGATION', 'AVG', '1',
+        'FILTER', 'label=value',
+        'GROUPBY', 'label', 'REDUCE', 'AVG'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRevRangeGroupBy', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRevRangeGroupBy('-', '+', 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        'label=value': {
+          configurable: true,
+          enumerable: true,
+          value: {  
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts
new file mode 100644
index 00000000000..329d9cceb8e
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts
@@ -0,0 +1,17 @@
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_GROUPBY';
+
+export default {
+  IS_READ_ONLY: MRANGE_GROUPBY.IS_READ_ONLY,
+  /**
+   * Gets samples for time series matching a filter within a time range with grouping (in reverse order)
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param groupBy - Group by parameters
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeGroupByArguments('TS.MREVRANGE'),
+  transformReply: MRANGE_GROUPBY.transformReply,
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts
new file mode 100644
index 00000000000..f68e34727c2
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts
@@ -0,0 +1,74 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MREVRANGE_SELECTED_LABELS from './MREVRANGE_SELECTED_LABELS';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MREVRANGE_SELECTED_LABELS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MREVRANGE_SELECTED_LABELS, '-', '+', 'label', 'label=value', {
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MREVRANGE', '-', '+',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-', 'AGGREGATION', 'AVG', '1',
+        'SELECTED_LABELS', 'label',
+        'FILTER', 'label=value'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRevRangeSelectedLabels', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRevRangeSelectedLabels('-', '+', ['label', 'NX'], 'label=value', {
+        COUNT: 1
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        key: {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              },
+              NX: {
+                configurable: true,
+                enumerable: true,
+                value: null
+              }
+            }),
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+          
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts
new file mode 100644
index 00000000000..15dc9d87daa
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts
@@ -0,0 +1,17 @@
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } from './MRANGE_SELECTED_LABELS';
+
+export default {
+  IS_READ_ONLY: MRANGE_SELECTED_LABELS.IS_READ_ONLY,
+  /**
+   * Gets samples for time series matching a filter with selected labels (in reverse order)
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param selectedLabels - Labels to include in the output
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MREVRANGE'),
+  transformReply: MRANGE_SELECTED_LABELS.transformReply,
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts
new file mode 100644
index 00000000000..444bb2f3d24
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts
@@ -0,0 +1,81 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MREVRANGE_SELECTED_LABELS_GROUPBY from './MREVRANGE_SELECTED_LABELS_GROUPBY';
+import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MREVRANGE_SELECTED_LABELS_GROUPBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MREVRANGE_SELECTED_LABELS_GROUPBY, '-', '+', 'label', 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      }, {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MREVRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-', 'AGGREGATION', 'AVG', '1',
+        'SELECTED_LABELS', 'label',
+        'FILTER', 'label=value',
+        'GROUPBY', 'label', 'REDUCE', 'AVG'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRevRangeSelectedLabelsGroupBy', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRevRangeSelectedLabelsGroupBy('-', '+', ['label', 'NX'], 'label=value', {
+        REDUCE: TIME_SERIES_REDUCERS.AVG,
+        label: 'label'
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        'label=value': {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              },
+              NX: {
+                configurable: true,
+                enumerable: true,
+                value: null
+              }
+            }),
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts
new file mode 100644
index 00000000000..c044a9ca064
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts
@@ -0,0 +1,18 @@
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransformArguments } from './MRANGE_SELECTED_LABELS_GROUPBY';
+
+export default {
+  IS_READ_ONLY: MRANGE_SELECTED_LABELS_GROUPBY.IS_READ_ONLY,
+  /**
+   * Gets samples for time series matching a filter with selected labels and grouping (in reverse order)
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param selectedLabels - Labels to include in the output
+   * @param filter - Filter to match time series keys
+   * @param groupBy - Group by parameters
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MREVRANGE'),
+  transformReply: MRANGE_SELECTED_LABELS_GROUPBY.transformReply,
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts
index 7e80e965d4e..da43a715f2e 100644
--- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts
+++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts
@@ -1,52 +1,69 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './MREVRANGE_WITHLABELS';
-import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
+import MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('MREVRANGE_WITHLABELS', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('-', '+', 'label=value', {
-                FILTER_BY_TS: [0],
-                FILTER_BY_VALUE: {
-                    min: 0,
-                    max: 1
-                },
-                SELECTED_LABELS: ['label'],
-                COUNT: 1,
-                ALIGN: '-',
-                AGGREGATION: {
-                    type: TimeSeriesAggregationType.AVERAGE,
-                    timeBucket: 1
-                },
-                GROUPBY: {
-                    label: 'label',
-                    reducer: TimeSeriesReducers.SUM
-                },
-            }),
-            ['TS.MREVRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
-            'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'SELECTED_LABELS', 'label',
-            'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'SUM']
-        );
-    });
+describe('TS.MREVRANGE_WITHLABELS', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MREVRANGE_WITHLABELS, '-', '+', 'label=value', {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MREVRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-',
+        'AGGREGATION', 'AVG', '1',
+        'WITHLABELS',
+        'FILTER', 'label=value'
+      ]
+    );
+  });
 
-    testUtils.testWithClient('client.ts.mRevRangeWithLabels', async client => {
-        await client.ts.add('key', 0, 0, {
-            LABELS: { label: 'value'}
-        });
+  testUtils.testWithClient('client.ts.mRevRangeWithLabels', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRevRangeWithLabels('-', '+', 'label=value')
+    ]);
 
-        assert.deepEqual(
-            await client.ts.mRevRangeWithLabels('-', '+', 'label=value', {
-                COUNT: 1
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        key: {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              }
             }),
-            [{
-                key: 'key',
-                labels: { label: 'value' },
-                samples: [{
-                    timestamp: 0,
-                    value: 0
-                }]
+            samples: [{
+              timestamp: 0,
+              value: 0
             }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts
index 21a0ebc69c3..0a05ab2c985 100644
--- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts
+++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts
@@ -1,21 +1,17 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments, Filter } from '.';
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import MRANGE_WITHLABELS, { createTransformMRangeWithLabelsArguments } from './MRANGE_WITHLABELS';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    filters: Filter,
-    options?: MRangeWithLabelsOptions
-): RedisCommandArguments {
-    return pushMRangeWithLabelsArguments(
-        ['TS.MREVRANGE'],
-        fromTimestamp,
-        toTimestamp,
-        filters,
-        options
-    );
-}
-
-export { transformMRangeWithLabelsReply as transformReply } from '.';
+export default {
+  NOT_KEYED_COMMAND: MRANGE_WITHLABELS.NOT_KEYED_COMMAND,
+  IS_READ_ONLY: MRANGE_WITHLABELS.IS_READ_ONLY,
+  /**
+   * Gets samples for time series matching a filter with labels (in reverse order)
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createTransformMRangeWithLabelsArguments('TS.MREVRANGE'),
+  transformReply: MRANGE_WITHLABELS.transformReply,
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts
new file mode 100644
index 00000000000..f4e6df9f0c6
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts
@@ -0,0 +1,78 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import MREVRANGE_WITHLABELS_GROUPBY from './MREVRANGE_WITHLABELS_GROUPBY';
+import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.MREVRANGE_WITHLABELS_GROUPBY', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(MREVRANGE_WITHLABELS_GROUPBY, '-', '+', 'label=value', {
+        label: 'label',
+        REDUCE: TIME_SERIES_REDUCERS.AVG
+      }, {
+        LATEST: true,
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 0,
+          max: 1
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.MREVRANGE', '-', '+',
+        'LATEST',
+        'FILTER_BY_TS', '0',
+        'FILTER_BY_VALUE', '0', '1',
+        'COUNT', '1',
+        'ALIGN', '-',
+        'AGGREGATION', 'AVG', '1',
+        'WITHLABELS',
+        'FILTER', 'label=value',
+        'GROUPBY', 'label', 'REDUCE', 'AVG'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.mRevRangeWithLabelsGroupBy', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 0, 0, {
+        LABELS: { label: 'value' }
+      }),
+      client.ts.mRevRangeWithLabelsGroupBy('-', '+', 'label=value', {
+        label: 'label',
+        REDUCE: TIME_SERIES_REDUCERS.AVG
+      })
+    ]);
+
+    assert.deepStrictEqual(
+      reply,
+      Object.create(null, {
+        'label=value': {
+          configurable: true,
+          enumerable: true,
+          value: {
+            labels: Object.create(null, {
+              label: {
+                configurable: true,
+                enumerable: true,
+                value: 'value'
+              }
+            }),
+            sources: ['key'],
+            samples: [{
+              timestamp: 0,
+              value: 0
+            }]
+          }
+        }
+      })
+    );
+  }, GLOBAL.SERVERS.OPEN);
+});
diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts
new file mode 100644
index 00000000000..e5c62898951
--- /dev/null
+++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts
@@ -0,0 +1,17 @@
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArguments } from './MRANGE_WITHLABELS_GROUPBY';
+
+export default {
+  IS_READ_ONLY: MRANGE_WITHLABELS_GROUPBY.IS_READ_ONLY,
+  /**
+   * Gets samples for time series matching a filter with labels and grouping (in reverse order)
+   * @param parser - The command parser
+   * @param fromTimestamp - Start timestamp for range
+   * @param toTimestamp - End timestamp for range
+   * @param filter - Filter to match time series keys
+   * @param groupBy - Group by parameters
+   * @param options - Optional parameters for the command
+   */
+  parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MREVRANGE'),
+  transformReply: MRANGE_WITHLABELS_GROUPBY.transformReply,
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/QUERYINDEX.spec.ts b/packages/time-series/lib/commands/QUERYINDEX.spec.ts
index 010c5c8f639..2f3f5617fb3 100644
--- a/packages/time-series/lib/commands/QUERYINDEX.spec.ts
+++ b/packages/time-series/lib/commands/QUERYINDEX.spec.ts
@@ -1,34 +1,35 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './QUERYINDEX';
+import QUERYINDEX from './QUERYINDEX';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('QUERYINDEX', () => {
-    describe('transformArguments', () => {
-        it('single filter', () => {
-            assert.deepEqual(
-                transformArguments('*'),
-                ['TS.QUERYINDEX', '*']
-            );
-        });
-        
-        it('multiple filters', () => {
-            assert.deepEqual(
-                transformArguments(['a=1', 'b=2']),
-                ['TS.QUERYINDEX', 'a=1', 'b=2']
-            );
-        });
+describe('TS.QUERYINDEX', () => {
+  describe('transformArguments', () => {
+    it('single filter', () => {
+      assert.deepEqual(
+        parseArgs(QUERYINDEX, '*'),
+        ['TS.QUERYINDEX', '*']
+      );
     });
 
-    testUtils.testWithClient('client.ts.queryIndex', async client => {
-        await client.ts.create('key', {
-            LABELS: {
-                label: 'value'
-            }
-        });
+    it('multiple filters', () => {
+      assert.deepEqual(
+        parseArgs(QUERYINDEX, ['a=1', 'b=2']),
+        ['TS.QUERYINDEX', 'a=1', 'b=2']
+      );
+    });
+  });
+
+  testUtils.testWithClient('client.ts.queryIndex', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.create('key', {
+        LABELS: {
+          label: 'value'
+        }
+      }),
+      client.ts.queryIndex('label=value')
+    ]);
 
-        assert.deepEqual(
-            await client.ts.queryIndex('label=value'),
-            ['key']
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, ['key']);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts
index 46eb5647040..158a7341c8a 100644
--- a/packages/time-series/lib/commands/QUERYINDEX.ts
+++ b/packages/time-series/lib/commands/QUERYINDEX.ts
@@ -1,11 +1,21 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
-import { Filter } from '.';
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/dist/lib/RESP/types';
+import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(filter: Filter): RedisCommandArguments {
-    return pushVerdictArguments(['TS.QUERYINDEX'], filter);
-}
-
-export declare function transformReply(): Array<string>;
+export default {
+  NOT_KEYED_COMMAND: true,
+  IS_READ_ONLY: true,
+  /**
+   * Queries the index for time series matching a specific filter
+   * @param parser - The command parser
+   * @param filter - Filter to match time series labels
+   */
+  parseCommand(parser: CommandParser, filter: RedisVariadicArgument) {
+    parser.push('TS.QUERYINDEX');
+    parser.pushVariadic(filter);
+  },
+  transformReply: {
+    2: undefined as unknown as () => ArrayReply<BlobStringReply>,
+    3: undefined as unknown as () => SetReply<BlobStringReply>
+  }
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/RANGE.spec.ts b/packages/time-series/lib/commands/RANGE.spec.ts
index 1e6a9958806..2d20b455fc1 100644
--- a/packages/time-series/lib/commands/RANGE.spec.ts
+++ b/packages/time-series/lib/commands/RANGE.spec.ts
@@ -1,38 +1,41 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './RANGE';
-import { TimeSeriesAggregationType } from '.';
+import RANGE from './RANGE';
+import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
 
-describe('RANGE', () => {
-    it('transformArguments', () => {
-        assert.deepEqual(
-            transformArguments('key', '-', '+', {
-                FILTER_BY_TS: [0],
-                FILTER_BY_VALUE: {
-                    min: 1,
-                    max: 2
-                },
-                COUNT: 1,
-                ALIGN: '-',
-                AGGREGATION: {
-                    type: TimeSeriesAggregationType.AVERAGE,
-                    timeBucket: 1
-                }
-            }),
-            ['TS.RANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE',
-            '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1']
-        );
-    });
+describe('TS.RANGE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(RANGE, 'key', '-', '+', {
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 1,
+          max: 2
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.RANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE',
+        '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1'
+      ]
+    );
+  });
 
-    testUtils.testWithClient('client.ts.range', async client => {
-        await client.ts.add('key', 1, 2);
+  testUtils.testWithClient('client.ts.range', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 1, 2),
+      client.ts.range('key', '-', '+')
+    ]);
 
-        assert.deepEqual(
-            await client.ts.range('key', '-', '+'),
-            [{
-                timestamp: 1,
-                value: 2
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+    assert.deepEqual(reply, [{
+      timestamp: 1,
+      value: 2
+    }]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts
index e6ce256bbe6..03d58d012ca 100644
--- a/packages/time-series/lib/commands/RANGE.ts
+++ b/packages/time-series/lib/commands/RANGE.ts
@@ -1,24 +1,122 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { RangeOptions, Timestamp, pushRangeArguments, SampleRawReply, SampleReply, transformRangeReply } from '.';
-
-export const FIRST_KEY_INDEX = 1;
-
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: string,
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    options?: RangeOptions
-): RedisCommandArguments {
-    return pushRangeArguments(
-        ['TS.RANGE', key],
-        fromTimestamp,
-        toTimestamp,
-        options
+import { CommandParser } from '@redis/client/dist/lib/client/parser';
+import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
+import { Timestamp, transformTimestampArgument, SamplesRawReply, transformSamplesReply } from './helpers';
+import { TimeSeriesAggregationType } from './CREATERULE';
+import { Resp2Reply } from '@redis/client/dist/lib/RESP/types';
+
+export const TIME_SERIES_BUCKET_TIMESTAMP = {
+  LOW: '-',
+  MIDDLE: '~',
+  END: '+'
+};
+
+export type TimeSeriesBucketTimestamp = typeof TIME_SERIES_BUCKET_TIMESTAMP[keyof typeof TIME_SERIES_BUCKET_TIMESTAMP];
+
+export interface TsRangeOptions {
+  LATEST?: boolean;
+  FILTER_BY_TS?: Array<Timestamp>;
+  FILTER_BY_VALUE?: {
+    min: number;
+    max: number;
+  };
+  COUNT?: number;
+  ALIGN?: Timestamp;
+  AGGREGATION?: {
+    ALIGN?: Timestamp;
+    type: TimeSeriesAggregationType;
+    timeBucket: Timestamp;
+    BUCKETTIMESTAMP?: TimeSeriesBucketTimestamp;
+    EMPTY?: boolean;
+  };
+}
+
+export function parseRangeArguments(
+  parser: CommandParser,
+  fromTimestamp: Timestamp,
+  toTimestamp: Timestamp,
+  options?: TsRangeOptions
+) {
+  parser.push(
+    transformTimestampArgument(fromTimestamp),
+    transformTimestampArgument(toTimestamp)
+  );
+
+  if (options?.LATEST) {
+    parser.push('LATEST');
+  }
+
+  if (options?.FILTER_BY_TS) {
+    parser.push('FILTER_BY_TS');
+    for (const timestamp of options.FILTER_BY_TS) {
+      parser.push(transformTimestampArgument(timestamp));
+    }
+  }
+
+  if (options?.FILTER_BY_VALUE) {
+    parser.push(
+      'FILTER_BY_VALUE',
+      options.FILTER_BY_VALUE.min.toString(),
+      options.FILTER_BY_VALUE.max.toString()
     );
+  }
+
+  if (options?.COUNT !== undefined) {
+    parser.push('COUNT', options.COUNT.toString());
+  }
+
+  if (options?.AGGREGATION) {
+    if (options?.ALIGN !== undefined) {
+      parser.push('ALIGN', transformTimestampArgument(options.ALIGN));
+    }
+
+    parser.push(
+      'AGGREGATION',
+      options.AGGREGATION.type,
+      transformTimestampArgument(options.AGGREGATION.timeBucket)
+    );
+      
+    if (options.AGGREGATION.BUCKETTIMESTAMP) {
+      parser.push(
+        'BUCKETTIMESTAMP',
+        options.AGGREGATION.BUCKETTIMESTAMP
+      );
+    }
+      
+    if (options.AGGREGATION.EMPTY) {
+      parser.push('EMPTY');
+    }
+  }
 }
 
-export function transformReply(reply: Array<SampleRawReply>): Array<SampleReply> {
-    return transformRangeReply(reply);
+export function transformRangeArguments(
+  parser: CommandParser,
+  key: RedisArgument,
+  fromTimestamp: Timestamp,
+  toTimestamp: Timestamp,
+  options?: TsRangeOptions
+) {
+  parser.pushKey(key);
+  parseRangeArguments(parser, fromTimestamp, toTimestamp, options);
 }
+
+export default {
+  IS_READ_ONLY: true,
+  /**
+   * Gets samples from a time series within a time range
+   * @param args - Arguments passed to the {@link transformRangeArguments} function
+   */
+  parseCommand(...args: Parameters<typeof transformRangeArguments>) {
+    const parser = args[0];
+
+    parser.push('TS.RANGE');
+    transformRangeArguments(...args);
+  },
+  transformReply: {
+    2(reply: Resp2Reply<SamplesRawReply>) {
+      return transformSamplesReply[2](reply);
+    },
+    3(reply: SamplesRawReply) {
+      return transformSamplesReply[3](reply);
+    }
+  }
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/REVRANGE.spec.ts b/packages/time-series/lib/commands/REVRANGE.spec.ts
index ffd90268c81..a4c6aa2c0db 100644
--- a/packages/time-series/lib/commands/REVRANGE.spec.ts
+++ b/packages/time-series/lib/commands/REVRANGE.spec.ts
@@ -1,106 +1,41 @@
-import { strict as assert } from 'assert';
+import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
-import { transformArguments } from './REVRANGE';
-import { TimeSeriesAggregationType } from '.';
-
-describe('REVRANGE', () => {
-    describe('transformArguments', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+'),
-                ['TS.REVRANGE', 'key', '-', '+']
-            );
-        });
-
-        it('with FILTER_BY_TS', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    FILTER_BY_TS: [0]
-                }),
-                ['TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0']
-            );
-        });
-
-        it('with FILTER_BY_VALUE', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    FILTER_BY_VALUE: {
-                        min: 1,
-                        max: 2
-                    }
-                }),
-                ['TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_VALUE', '1', '2']
-            );
-        });
-
-        it('with COUNT', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    COUNT: 1
-                }),
-                ['TS.REVRANGE', 'key', '-', '+', 'COUNT', '1']
-            );
-        });
-
-        it('with ALIGN', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    ALIGN: '-'
-                }),
-                ['TS.REVRANGE', 'key', '-', '+', 'ALIGN', '-']
-            );
-        });
-
-        it('with AGGREGATION', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    AGGREGATION: {
-                        type: TimeSeriesAggregationType.AVERAGE,
-                        timeBucket: 1
-                    }
-                }),
-                ['TS.REVRANGE', 'key', '-', '+', 'AGGREGATION', 'AVG', '1']
-            );
-        });
-
-        it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => {
-            assert.deepEqual(
-                transformArguments('key', '-', '+', {
-                    FILTER_BY_TS: [0],
-                    FILTER_BY_VALUE: {
-                        min: 1,
-                        max: 2
-                    },
-                    COUNT: 1,
-                    ALIGN: '-',
-                    AGGREGATION: {
-                        type: TimeSeriesAggregationType.AVERAGE,
-                        timeBucket: 1
-                    }
-                }),
-                [
-                    'TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE',
-                    '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1'
-                ]
-            );
-        });
-    });
-
-    testUtils.testWithClient('client.ts.revRange', async client => {
-        await Promise.all([
-            client.ts.add('key', 0, 1),
-            client.ts.add('key', 1, 2)
-        ]);
-
-        assert.deepEqual(
-            await client.ts.revRange('key', '-', '+'),
-            [{
-                timestamp: 1,
-                value: 2
-            }, {
-                timestamp: 0,
-                value: 1
-            }]
-        );
-    }, GLOBAL.SERVERS.OPEN);
+import REVRANGE from './REVRANGE';
+import { TIME_SERIES_AGGREGATION_TYPE } from '../index';
+import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
+
+describe('TS.REVRANGE', () => {
+  it('transformArguments', () => {
+    assert.deepEqual(
+      parseArgs(REVRANGE, 'key', '-', '+', {
+        FILTER_BY_TS: [0],
+        FILTER_BY_VALUE: {
+          min: 1,
+          max: 2
+        },
+        COUNT: 1,
+        ALIGN: '-',
+        AGGREGATION: {
+          type: TIME_SERIES_AGGREGATION_TYPE.AVG,
+          timeBucket: 1
+        }
+      }),
+      [
+        'TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE',
+        '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1'
+      ]
+    );
+  });
+
+  testUtils.testWithClient('client.ts.revRange', async client => {
+    const [, reply] = await Promise.all([
+      client.ts.add('key', 1, 2),
+      client.ts.revRange('key', '-', '+')
+    ]);
+
+    assert.deepEqual(reply, [{
+      timestamp: 1,
+      value: 2
+    }]);
+  }, GLOBAL.SERVERS.OPEN);
 });
diff --git a/packages/time-series/lib/commands/REVRANGE.ts b/packages/time-series/lib/commands/REVRANGE.ts
index 9179756b5de..27389b896c3 100644
--- a/packages/time-series/lib/commands/REVRANGE.ts
+++ b/packages/time-series/lib/commands/REVRANGE.ts
@@ -1,24 +1,17 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { RangeOptions, Timestamp, pushRangeArguments, SampleRawReply, SampleReply, transformRangeReply } from '.';
+import { Command } from '@redis/client/dist/lib/RESP/types';
+import RANGE, { transformRangeArguments } from './RANGE';
 
-export const FIRST_KEY_INDEX = 1;
+export default {
+  IS_READ_ONLY: RANGE.IS_READ_ONLY,
+  /**
+   * Gets samples from a time series within a time range (in reverse order)
+   * @param args - Arguments passed to the {@link transformRangeArguments} function
+   */
+  parseCommand(...args: Parameters<typeof transformRangeArguments>) {
+    const parser = args[0];
 
-export const IS_READ_ONLY = true;
-
-export function transformArguments(
-    key: string,
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    options?: RangeOptions
-): RedisCommandArguments {
-    return pushRangeArguments(
-        ['TS.REVRANGE', key],
-        fromTimestamp,
-        toTimestamp,
-        options
-    );
-}
-
-export function transformReply(reply: Array<SampleRawReply>): Array<SampleReply> {
-    return transformRangeReply(reply);
-}
+    parser.push('TS.REVRANGE');
+    transformRangeArguments(...args);
+  },
+  transformReply: RANGE.transformReply
+} as const satisfies Command;
diff --git a/packages/time-series/lib/commands/helpers.ts b/packages/time-series/lib/commands/helpers.ts
new file mode 100644
index 00000000000..3e277d0747d
--- /dev/null
+++ b/packages/time-series/lib/commands/helpers.ts
@@ -0,0 +1,306 @@
+import { CommandParser } from "@redis/client/dist/lib/client/parser";
+import { TsIgnoreOptions } from "./ADD";
+import { ArrayReply, BlobStringReply, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, Resp2Reply, RespType, TuplesReply, TypeMapping, UnwrapReply } from "@redis/client/dist/lib/RESP/types";
+import { RESP_TYPES } from "@redis/client";
+import { RedisVariadicArgument } from "@redis/client/dist/lib/commands/generic-transformers";
+
+export function parseIgnoreArgument(parser: CommandParser, ignore?: TsIgnoreOptions) {
+  if (ignore !== undefined) {
+    parser.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString());
+  }
+}
+
+export function parseRetentionArgument(parser: CommandParser, retention?: number) {
+  if (retention !== undefined) {
+    parser.push('RETENTION', retention.toString());
+  }
+}
+
+export const TIME_SERIES_ENCODING = {
+  COMPRESSED: 'COMPRESSED',
+  UNCOMPRESSED: 'UNCOMPRESSED'
+} as const;
+
+export type TimeSeriesEncoding = typeof TIME_SERIES_ENCODING[keyof typeof TIME_SERIES_ENCODING];
+
+export function parseEncodingArgument(parser: CommandParser, encoding?: TimeSeriesEncoding) {
+  if (encoding !== undefined) {
+    parser.push('ENCODING', encoding);
+  }
+}
+
+export function parseChunkSizeArgument(parser: CommandParser, chunkSize?: number) {
+  if (chunkSize !== undefined) {
+    parser.push('CHUNK_SIZE', chunkSize.toString());
+  }
+}
+
+export const TIME_SERIES_DUPLICATE_POLICIES = {
+  BLOCK: 'BLOCK',
+  FIRST: 'FIRST',
+  LAST: 'LAST',
+  MIN: 'MIN',
+  MAX: 'MAX',
+  SUM: 'SUM'
+} as const;
+
+export type TimeSeriesDuplicatePolicies = typeof TIME_SERIES_DUPLICATE_POLICIES[keyof typeof TIME_SERIES_DUPLICATE_POLICIES];
+
+export function parseDuplicatePolicy(parser: CommandParser, duplicatePolicy?: TimeSeriesDuplicatePolicies) {
+  if (duplicatePolicy !== undefined) {
+    parser.push('DUPLICATE_POLICY', duplicatePolicy);
+  }
+}
+
+export type Timestamp = number | Date | string;
+
+export function transformTimestampArgument(timestamp: Timestamp): string {
+  if (typeof timestamp === 'string') return timestamp;
+
+  return (
+    typeof timestamp === 'number' ?
+      timestamp :
+      timestamp.getTime()
+  ).toString();
+}
+
+export type Labels = {
+  [label: string]: string;
+};
+
+export function parseLabelsArgument(parser: CommandParser, labels?: Labels) {
+  if (labels) {
+    parser.push('LABELS');
+
+    for (const [label, value] of Object.entries(labels)) {
+      parser.push(label, value);
+    }
+  }
+}
+
+export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>;
+
+export const transformSampleReply = {
+  2(reply: Resp2Reply<SampleRawReply>) {
+    const [ timestamp, value ] = reply as unknown as UnwrapReply<typeof reply>;
+    return {
+      timestamp,
+      value: Number(value) // TODO: use double type mapping instead
+    };
+  },
+  3(reply: SampleRawReply) {
+    const [ timestamp, value ] = reply as unknown as UnwrapReply<typeof reply>;
+    return {
+      timestamp,
+      value
+    };
+  }
+};
+
+export type SamplesRawReply = ArrayReply<SampleRawReply>;
+
+export const transformSamplesReply = {
+  2(reply: Resp2Reply<SamplesRawReply>) {
+    return (reply as unknown as UnwrapReply<typeof reply>)
+      .map(sample => transformSampleReply[2](sample));
+  },
+  3(reply: SamplesRawReply) {
+    return (reply as unknown as UnwrapReply<typeof reply>)
+      .map(sample => transformSampleReply[3](sample));
+  }
+};
+
+// TODO: move to @redis/client?
+export function resp2MapToValue<
+  RAW_VALUE extends TuplesReply<[key: BlobStringReply, ...rest: Array<ReplyUnion>]>,
+  TRANSFORMED
+>(
+  wrappedReply: ArrayReply<RAW_VALUE>,
+  parseFunc: (rawValue: UnwrapReply<RAW_VALUE>) => TRANSFORMED,
+  typeMapping?: TypeMapping
+): MapReply<BlobStringReply, TRANSFORMED> {
+  const reply = wrappedReply as unknown as UnwrapReply<typeof wrappedReply>;
+  switch (typeMapping?.[RESP_TYPES.MAP]) {
+    case Map: {
+      const ret = new Map<string, TRANSFORMED>();
+      for (const wrappedTuple of reply) {
+        const tuple = wrappedTuple as unknown as UnwrapReply<typeof wrappedTuple>;
+        const key = tuple[0] as unknown as UnwrapReply<typeof tuple[0]>;
+        ret.set(key.toString(), parseFunc(tuple));
+      }
+      return ret as never;
+    }
+    case Array: {
+      for (const wrappedTuple of reply) {
+        const tuple = wrappedTuple as unknown as UnwrapReply<typeof wrappedTuple>;
+        (tuple[1] as unknown as TRANSFORMED) = parseFunc(tuple);
+      }
+      return reply as never;
+    }
+    default: {
+      const ret: Record<string, TRANSFORMED> = Object.create(null);
+      for (const wrappedTuple of reply) {
+        const tuple = wrappedTuple as unknown as UnwrapReply<typeof wrappedTuple>;
+        const key = tuple[0] as unknown as UnwrapReply<typeof tuple[0]>;
+        ret[key.toString()] = parseFunc(tuple);
+      }
+      return ret as never;
+    }
+  } 
+}
+
+export function resp3MapToValue<
+  RAW_VALUE extends RespType<any, any, any, any>, // TODO: simplify types
+  TRANSFORMED
+>(
+  wrappedReply: MapReply<BlobStringReply, RAW_VALUE>,
+  parseFunc: (rawValue: UnwrapReply<RAW_VALUE>) => TRANSFORMED
+): MapReply<BlobStringReply, TRANSFORMED> {
+  const reply = wrappedReply as unknown as UnwrapReply<typeof wrappedReply>;  
+  if (reply instanceof Array) {
+    for (let i = 1; i < reply.length; i += 2) {
+      (reply[i] as unknown as TRANSFORMED) = parseFunc(reply[i] as unknown as UnwrapReply<RAW_VALUE>);
+    }
+  } else if (reply instanceof Map) {
+    for (const [key, value] of reply.entries()) {
+      (reply as unknown as Map<BlobStringReply, TRANSFORMED>).set(
+        key,
+        parseFunc(value as unknown as UnwrapReply<typeof value>)
+      );
+    }
+  } else {
+    for (const [key, value] of Object.entries(reply)) {
+      (reply[key] as unknown as TRANSFORMED) = parseFunc(value as unknown as UnwrapReply<typeof value>);
+    }
+  }
+  return reply as never;
+}
+
+export function parseSelectedLabelsArguments(
+  parser: CommandParser,
+  selectedLabels: RedisVariadicArgument
+) {
+  parser.push('SELECTED_LABELS');
+  parser.pushVariadic(selectedLabels);
+}
+
+export type RawLabelValue = BlobStringReply | NullReply;
+
+export type RawLabels<T extends RawLabelValue> = ArrayReply<TuplesReply<[
+  label: BlobStringReply,
+  value: T
+]>>;
+
+export function transformRESP2Labels<T extends RawLabelValue>(
+  labels: RawLabels<T>,
+  typeMapping?: TypeMapping
+): MapReply<BlobStringReply, T> {
+  const unwrappedLabels = labels as unknown as UnwrapReply<typeof labels>;
+  switch (typeMapping?.[RESP_TYPES.MAP]) {
+    case Map:
+      const map = new Map<string, T>();
+      for (const tuple of unwrappedLabels) {
+        const [key, value] = tuple as unknown as UnwrapReply<typeof tuple>;
+        const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
+        map.set(unwrappedKey.toString(), value);
+      }
+      return map as never;
+
+    case Array:
+      return unwrappedLabels.flat() as never;
+
+    case Object:
+    default:
+      const labelsObject: Record<string, T> = Object.create(null);
+      for (const tuple of unwrappedLabels) {
+        const [key, value] = tuple as unknown as UnwrapReply<typeof tuple>;
+        const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
+        labelsObject[unwrappedKey.toString()] = value;
+      }
+      return labelsObject as never;
+  }
+}
+
+export function transformRESP2LabelsWithSources<T extends RawLabelValue>(
+  labels: RawLabels<T>,
+  typeMapping?: TypeMapping
+) {
+  const unwrappedLabels = labels as unknown as UnwrapReply<typeof labels>;
+  const to = unwrappedLabels.length - 2; // ignore __reducer__ and __source__
+  let transformedLabels: MapReply<BlobStringReply, T>;
+  switch (typeMapping?.[RESP_TYPES.MAP]) {
+    case Map:
+      const map = new Map<string, T>();
+      for (let i = 0; i < to; i++) {
+        const [key, value] = unwrappedLabels[i] as unknown as UnwrapReply<typeof unwrappedLabels[number]>;
+        const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
+        map.set(unwrappedKey.toString(), value);
+      }
+      transformedLabels = map as never;
+      break;
+
+    case Array:
+      transformedLabels = unwrappedLabels.slice(0, to).flat() as never;
+      break;
+
+    case Object:
+    default:
+      const labelsObject: Record<string, T> = Object.create(null);
+      for (let i = 0; i < to; i++) {
+        const [key, value] = unwrappedLabels[i] as unknown as UnwrapReply<typeof unwrappedLabels[number]>;
+        const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
+        labelsObject[unwrappedKey.toString()] = value;
+      }
+      transformedLabels = labelsObject as never;
+      break;
+  }
+
+  const sourcesTuple = unwrappedLabels[unwrappedLabels.length - 1];
+  const unwrappedSourcesTuple = sourcesTuple as unknown as UnwrapReply<typeof sourcesTuple>;
+  // the __source__ label will never be null
+  const transformedSources = transformRESP2Sources(unwrappedSourcesTuple[1] as BlobStringReply);
+
+  return {
+    labels: transformedLabels,
+    sources: transformedSources
+  };
+}
+
+function transformRESP2Sources(sourcesRaw: BlobStringReply) {
+  // if a label contains "," this function will produce incorrcet results..
+  // there is not much we can do about it, and we assume most users won't be using "," in their labels..
+  
+  const unwrappedSources = sourcesRaw as unknown as UnwrapReply<typeof sourcesRaw>;
+  if (typeof unwrappedSources === 'string') {
+    return unwrappedSources.split(',');
+  }
+
+  const indexOfComma = unwrappedSources.indexOf(',');
+  if (indexOfComma === -1) {
+    return [unwrappedSources];
+  }
+
+  const sourcesArray = [
+    unwrappedSources.subarray(0, indexOfComma)
+  ];
+
+  let previousComma = indexOfComma + 1;
+  while (true) {
+    const indexOf = unwrappedSources.indexOf(',', previousComma);
+    if (indexOf === -1) {
+      sourcesArray.push(
+        unwrappedSources.subarray(previousComma)
+      );
+      break;
+    }
+
+    const source = unwrappedSources.subarray(
+      previousComma,
+      indexOf
+    );
+    sourcesArray.push(source);
+    previousComma = indexOf + 1;
+  }
+
+  return sourcesArray;
+}
\ No newline at end of file
diff --git a/packages/time-series/lib/commands/index.spec.ts b/packages/time-series/lib/commands/index.spec.ts
index a29eefe860a..b565abea476 100644
--- a/packages/time-series/lib/commands/index.spec.ts
+++ b/packages/time-series/lib/commands/index.spec.ts
@@ -1,439 +1,423 @@
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { strict as assert } from 'assert';
-import {
-    transformTimestampArgument,
-    pushRetentionArgument,
-    TimeSeriesEncoding,
-    pushEncodingArgument,
-    pushChunkSizeArgument,
-    pushDuplicatePolicy,
-    pushLabelsArgument,
-    transformIncrDecrArguments,
-    transformSampleReply,
-    TimeSeriesAggregationType,
-    pushRangeArguments,
-    pushMRangeGroupByArguments,
-    TimeSeriesReducers,
-    pushFilterArgument,
-    pushMRangeArguments,
-    pushWithLabelsArgument,
-    pushMRangeWithLabelsArguments,
-    transformRangeReply,
-    transformMRangeReply,
-    transformMRangeWithLabelsReply,
-    TimeSeriesDuplicatePolicies,
-    pushLatestArgument,
-    TimeSeriesBucketTimestamp
-} from '.';
-
-describe('transformTimestampArgument', () => {
-    it('number', () => {
-        assert.equal(
-            transformTimestampArgument(0),
-            '0'
-        );
-    });
-
-    it('Date', () => {
-        assert.equal(
-            transformTimestampArgument(new Date(0)),
-            '0'
-        );
-    });
-
-    it('string', () => {
-        assert.equal(
-            transformTimestampArgument('*'),
-            '*'
-        );
-    });
-});
-
-function testOptionalArgument(fn: (args: RedisCommandArguments) => unknown): void {
-    it('undefined', () => {
-        assert.deepEqual(
-            fn([]),
-            []
-        );
-    });
-}
-
-describe('pushRetentionArgument', () => {
-    testOptionalArgument(pushRetentionArgument);
-
-    it('number', () => {
-        assert.deepEqual(
-            pushRetentionArgument([], 1),
-            ['RETENTION', '1']
-        );
-    });
-});
-
-describe('pushEncodingArgument', () => {
-    testOptionalArgument(pushEncodingArgument);
-
-    it('UNCOMPRESSED', () => {
-        assert.deepEqual(
-            pushEncodingArgument([], TimeSeriesEncoding.UNCOMPRESSED),
-            ['ENCODING', 'UNCOMPRESSED']
-        );
-    });
-});
-
-describe('pushChunkSizeArgument', () => {
-    testOptionalArgument(pushChunkSizeArgument);
-
-    it('number', () => {
-        assert.deepEqual(
-            pushChunkSizeArgument([], 1),
-            ['CHUNK_SIZE', '1']
-        );
-    });
-});
-
-describe('pushDuplicatePolicy', () => {
-    testOptionalArgument(pushDuplicatePolicy);
-
-    it('BLOCK', () => {
-        assert.deepEqual(
-            pushDuplicatePolicy([], TimeSeriesDuplicatePolicies.BLOCK),
-            ['DUPLICATE_POLICY', 'BLOCK']
-        );
-    });
-});
-
-describe('pushLabelsArgument', () => {
-    testOptionalArgument(pushLabelsArgument);
-
-    it("{ label: 'value' }", () => {
-        assert.deepEqual(
-            pushLabelsArgument([], { label: 'value' }),
-            ['LABELS', 'label', 'value']
-        );
-    });
-});
-
-describe('transformIncrDecrArguments', () => {
-    it('without options', () => {
-        assert.deepEqual(
-            transformIncrDecrArguments('TS.INCRBY', 'key', 1),
-            ['TS.INCRBY', 'key', '1']
-        );
-    });
-
-    it('with TIMESTAMP', () => {
-        assert.deepEqual(
-            transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
-                TIMESTAMP: '*'
-            }),
-            ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*']
-        );
-    });
-
-    it('with UNCOMPRESSED', () => {
-        assert.deepEqual(
-            transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
-                UNCOMPRESSED: true
-            }),
-            ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED']
-        );
-    });
-
-    it('with UNCOMPRESSED false', () => {
-        assert.deepEqual(
-            transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
-                UNCOMPRESSED: false
-            }),
-            ['TS.INCRBY', 'key', '1']
-        );
-    });
-});
-
-it('transformSampleReply', () => {
-    assert.deepEqual(
-        transformSampleReply([1, '1.1']),
-        {
-            timestamp: 1,
-            value: 1.1
-        }
-    );
-});
-
-describe('pushRangeArguments', () => {
-    it('without options', () => {
-        assert.deepEqual(
-            pushRangeArguments([], '-', '+'),
-            ['-', '+']
-        );
-    });
-
-    describe('with FILTER_BY_TS', () => {
-        it('string', () => {
-            assert.deepEqual(
-                pushRangeArguments([], '-', '+', {
-                    FILTER_BY_TS: ['ts']
-                }),
-                ['-', '+', 'FILTER_BY_TS', 'ts']
-            );
-        });
-
-        it('Array', () => {
-            assert.deepEqual(
-                pushRangeArguments([], '-', '+', {
-                    FILTER_BY_TS: ['1', '2']
-                }),
-                ['-', '+', 'FILTER_BY_TS', '1', '2']
-            );
-        });
-    });
-
-    it('with FILTER_BY_VALUE', () => {
-        assert.deepEqual(
-            pushRangeArguments([], '-', '+', {
-                FILTER_BY_VALUE: {
-                    min: 1,
-                    max: 2
-                }
-            }),
-            ['-', '+', 'FILTER_BY_VALUE', '1', '2']
-        );
-    });
-
-    it('with COUNT', () => {
-        assert.deepEqual(
-            pushRangeArguments([], '-', '+', {
-                COUNT: 1
-            }),
-            ['-', '+', 'COUNT', '1']
-        );
-    });
-
-    it('with ALIGN', () => {
-        assert.deepEqual(
-            pushRangeArguments([], '-', '+', {
-                ALIGN: 1
-            }),
-            ['-', '+', 'ALIGN', '1']
-        );
-    });
-
-    describe('with AGGREGATION', () => {
-        it('without options', () => {
-            assert.deepEqual(
-                pushRangeArguments([], '-', '+', {
-                    AGGREGATION: {
-                        type: TimeSeriesAggregationType.FIRST,
-                        timeBucket: 1
-                    }
-                }),
-                ['-', '+', 'AGGREGATION', 'FIRST', '1']
-            );
-        });
-
-        it('with BUCKETTIMESTAMP', () => {
-            assert.deepEqual(
-                pushRangeArguments([], '-', '+', {
-                    AGGREGATION: {
-                        type: TimeSeriesAggregationType.FIRST,
-                        timeBucket: 1,
-                        BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW
-                    }
-                }),
-                ['-', '+', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-']
-            );
-        });
-
-        it('with BUCKETTIMESTAMP', () => {
-            assert.deepEqual(
-                pushRangeArguments([], '-', '+', {
-                    AGGREGATION: {
-                        type: TimeSeriesAggregationType.FIRST,
-                        timeBucket: 1,
-                        EMPTY: true
-                    }
-                }),
-                ['-', '+', 'AGGREGATION', 'FIRST', '1', 'EMPTY']
-            );
-        });
-    });
-
-    it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => {
-        assert.deepEqual(
-            pushRangeArguments([], '-', '+', {
-                FILTER_BY_TS: ['ts'],
-                FILTER_BY_VALUE: {
-                    min: 1,
-                    max: 2
-                },
-                COUNT: 1,
-                ALIGN: 1,
-                AGGREGATION: {
-                    type: TimeSeriesAggregationType.FIRST,
-                    timeBucket: 1,
-                    BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW,
-                    EMPTY: true
-                }
-            }),
-            ['-', '+', 'FILTER_BY_TS', 'ts', 'FILTER_BY_VALUE', '1', '2',
-            'COUNT', '1', 'ALIGN', '1', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-', 'EMPTY']
-        );
-    });
-});
-
-describe('pushMRangeGroupByArguments', () => {
-    it('undefined', () => {
-        assert.deepEqual(
-            pushMRangeGroupByArguments([]),
-            []
-        );
-    });
-
-    it('with GROUPBY', () => {
-        assert.deepEqual(
-            pushMRangeGroupByArguments([], {
-                label: 'label',
-                reducer: TimeSeriesReducers.MAXIMUM
-            }),
-            ['GROUPBY', 'label', 'REDUCE', 'MAX']
-        );
-    });
-});
-
-describe('pushFilterArgument', () => {
-    it('string', () => {
-        assert.deepEqual(
-            pushFilterArgument([], 'label=value'),
-            ['FILTER', 'label=value']
-        );
-    });
-
-    it('Array', () => {
-        assert.deepEqual(
-            pushFilterArgument([], ['1=1', '2=2']),
-            ['FILTER', '1=1', '2=2']
-        );
-    });
-});
-
-describe('pushMRangeArguments', () => {
-    it('without options', () => {
-        assert.deepEqual(
-            pushMRangeArguments([], '-', '+', 'label=value'),
-            ['-', '+', 'FILTER', 'label=value']
-        );
-    });
-
-    it('with GROUPBY', () => {
-        assert.deepEqual(
-            pushMRangeArguments([], '-', '+', 'label=value', {
-                GROUPBY: {
-                    label: 'label',
-                    reducer: TimeSeriesReducers.MAXIMUM
-                }
-            }),
-            ['-', '+', 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'MAX']
-        );
-    });
-});
-
-describe('pushWithLabelsArgument', () => {
-    it('without selected labels', () => {
-        assert.deepEqual(
-            pushWithLabelsArgument([]),
-            ['WITHLABELS']
-        );
-    });
-
-    it('with selected labels', () => {
-        assert.deepEqual(
-            pushWithLabelsArgument([], ['label']),
-            ['SELECTED_LABELS', 'label']
-        );
-    });
-});
-
-it('pushMRangeWithLabelsArguments', () => {
-    assert.deepEqual(
-        pushMRangeWithLabelsArguments([], '-', '+', 'label=value'),
-        ['-', '+', 'WITHLABELS', 'FILTER', 'label=value']
-    );
-});
-
-it('transformRangeReply', () => {
-    assert.deepEqual(
-        transformRangeReply([[1, '1.1'], [2, '2.2']]),
-        [{
-            timestamp: 1,
-            value: 1.1
-        }, {
-            timestamp: 2,
-            value: 2.2
-        }]
-    );
-});
-
-describe('transformMRangeReply', () => {
-    assert.deepEqual(
-        transformMRangeReply([[
-            'key',
-            [],
-            [[1, '1.1'], [2, '2.2']]
-        ]]),
-        [{
-            key: 'key',
-            samples: [{
-                timestamp: 1,
-                value: 1.1
-            }, {
-                timestamp: 2,
-                value: 2.2
-            }]
-        }]
-    );
-});
-
-describe('transformMRangeWithLabelsReply', () => {
-    assert.deepEqual(
-        transformMRangeWithLabelsReply([[
-            'key',
-            [['label', 'value']],
-            [[1, '1.1'], [2, '2.2']]
-        ]]),
-        [{
-            key: 'key',
-            labels: {
-                label: 'value'
-            },
-            samples: [{
-                timestamp: 1,
-                value: 1.1
-            }, {
-                timestamp: 2,
-                value: 2.2
-            }]
-        }]
-    );
-});
-
-describe('pushLatestArgument', () => {
-    it('undefined', () => {
-        assert.deepEqual(
-            pushLatestArgument([]),
-            []
-        );
-    });
-
-    it('false', () => {
-        assert.deepEqual(
-            pushLatestArgument([], false),
-            []
-        );
-    });
-
-    it('true', () => {
-        assert.deepEqual(
-            pushLatestArgument([], true),
-            ['LATEST']
-        );
-    });
-})
+// import { RedisCommandArguments } from '@redis/client/lib/commands';
+// import { strict as assert } from 'node:assert';
+// import {
+//     transformTimestampArgument,
+//     pushRetentionArgument,
+//     TimeSeriesEncoding,
+//     pushEncodingArgument,
+//     pushChunkSizeArgument,
+//     pushDuplicatePolicy,
+//     pushLabelsArgument,
+//     transformIncrDecrArguments,
+//     transformSampleReply,
+//     TimeSeriesAggregationType,
+//     pushRangeArguments,
+//     pushMRangeGroupByArguments,
+//     TimeSeriesReducers,
+//     pushFilterArgument,
+//     pushMRangeArguments,
+//     pushWithLabelsArgument,
+//     pushMRangeWithLabelsArguments,
+//     transformRangeReply,
+//     transformMRangeReply,
+//     transformMRangeWithLabelsReply,
+//     TimeSeriesDuplicatePolicies,
+//     pushLatestArgument,
+//     TimeSeriesBucketTimestamp
+// } from './helpers';
+
+// describe('transformTimestampArgument', () => {
+//     it('number', () => {
+//         assert.equal(
+//             transformTimestampArgument(0),
+//             '0'
+//         );
+//     });
+
+//     it('Date', () => {
+//         assert.equal(
+//             transformTimestampArgument(new Date(0)),
+//             '0'
+//         );
+//     });
+
+//     it('string', () => {
+//         assert.equal(
+//             transformTimestampArgument('*'),
+//             '*'
+//         );
+//     });
+// });
+
+// function testOptionalArgument(fn: (args: RedisCommandArguments) => unknown): void {
+//     it('undefined', () => {
+//         assert.deepEqual(
+//             fn([]),
+//             []
+//         );
+//     });
+// }
+
+// describe('pushRetentionArgument', () => {
+//     testOptionalArgument(pushRetentionArgument);
+
+//     it('number', () => {
+//         assert.deepEqual(
+//             pushRetentionArgument([], 1),
+//             ['RETENTION', '1']
+//         );
+//     });
+// });
+
+// describe('pushEncodingArgument', () => {
+//     testOptionalArgument(pushEncodingArgument);
+
+//     it('UNCOMPRESSED', () => {
+//         assert.deepEqual(
+//             pushEncodingArgument([], TimeSeriesEncoding.UNCOMPRESSED),
+//             ['ENCODING', 'UNCOMPRESSED']
+//         );
+//     });
+// });
+
+// describe('pushChunkSizeArgument', () => {
+//     testOptionalArgument(pushChunkSizeArgument);
+
+//     it('number', () => {
+//         assert.deepEqual(
+//             pushChunkSizeArgument([], 1),
+//             ['CHUNK_SIZE', '1']
+//         );
+//     });
+// });
+
+// describe('pushDuplicatePolicy', () => {
+//     testOptionalArgument(pushDuplicatePolicy);
+
+//     it('BLOCK', () => {
+//         assert.deepEqual(
+//             pushDuplicatePolicy([], TimeSeriesDuplicatePolicies.BLOCK),
+//             ['DUPLICATE_POLICY', 'BLOCK']
+//         );
+//     });
+// });
+
+// describe('pushLabelsArgument', () => {
+//     testOptionalArgument(pushLabelsArgument);
+
+//     it("{ label: 'value' }", () => {
+//         assert.deepEqual(
+//             pushLabelsArgument([], { label: 'value' }),
+//             ['LABELS', 'label', 'value']
+//         );
+//     });
+// });
+
+// describe('transformIncrDecrArguments', () => {
+//     it('without options', () => {
+//         assert.deepEqual(
+//             transformIncrDecrArguments('TS.INCRBY', 'key', 1),
+//             ['TS.INCRBY', 'key', '1']
+//         );
+//     });
+
+//     it('with TIMESTAMP', () => {
+//         assert.deepEqual(
+//             transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
+//                 TIMESTAMP: '*'
+//             }),
+//             ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*']
+//         );
+//     });
+
+//     it('with UNCOMPRESSED', () => {
+//         assert.deepEqual(
+//             transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
+//                 UNCOMPRESSED: true
+//             }),
+//             ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED']
+//         );
+//     });
+
+//     it('with UNCOMPRESSED false', () => {
+//         assert.deepEqual(
+//             transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
+//                 UNCOMPRESSED: false
+//             }),
+//             ['TS.INCRBY', 'key', '1']
+//         );
+//     });
+// });
+
+// it('transformSampleReply', () => {
+//     assert.deepEqual(
+//         transformSampleReply([1, '1.1']),
+//         {
+//             timestamp: 1,
+//             value: 1.1
+//         }
+//     );
+// });
+
+// describe('pushRangeArguments', () => {
+//     it('without options', () => {
+//         assert.deepEqual(
+//             pushRangeArguments([], '-', '+'),
+//             ['-', '+']
+//         );
+//     });
+
+//     describe('with FILTER_BY_TS', () => {
+//         it('string', () => {
+//             assert.deepEqual(
+//                 pushRangeArguments([], '-', '+', {
+//                     FILTER_BY_TS: ['ts']
+//                 }),
+//                 ['-', '+', 'FILTER_BY_TS', 'ts']
+//             );
+//         });
+
+//         it('Array', () => {
+//             assert.deepEqual(
+//                 pushRangeArguments([], '-', '+', {
+//                     FILTER_BY_TS: ['1', '2']
+//                 }),
+//                 ['-', '+', 'FILTER_BY_TS', '1', '2']
+//             );
+//         });
+//     });
+
+//     it('with FILTER_BY_VALUE', () => {
+//         assert.deepEqual(
+//             pushRangeArguments([], '-', '+', {
+//                 FILTER_BY_VALUE: {
+//                     min: 1,
+//                     max: 2
+//                 }
+//             }),
+//             ['-', '+', 'FILTER_BY_VALUE', '1', '2']
+//         );
+//     });
+
+//     it('with COUNT', () => {
+//         assert.deepEqual(
+//             pushRangeArguments([], '-', '+', {
+//                 COUNT: 1
+//             }),
+//             ['-', '+', 'COUNT', '1']
+//         );
+//     });
+
+//     it('with ALIGN', () => {
+//         assert.deepEqual(
+//             pushRangeArguments([], '-', '+', {
+//                 ALIGN: 1
+//             }),
+//             ['-', '+', 'ALIGN', '1']
+//         );
+//     });
+
+//     describe('with AGGREGATION', () => {
+//         it('without options', () => {
+//             assert.deepEqual(
+//                 pushRangeArguments([], '-', '+', {
+//                     AGGREGATION: {
+//                         type: TimeSeriesAggregationType.FIRST,
+//                         timeBucket: 1
+//                     }
+//                 }),
+//                 ['-', '+', 'AGGREGATION', 'FIRST', '1']
+//             );
+//         });
+
+//         it('with BUCKETTIMESTAMP', () => {
+//             assert.deepEqual(
+//                 pushRangeArguments([], '-', '+', {
+//                     AGGREGATION: {
+//                         type: TimeSeriesAggregationType.FIRST,
+//                         timeBucket: 1,
+//                         BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW
+//                     }
+//                 }),
+//                 ['-', '+', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-']
+//             );
+//         });
+
+//         it('with BUCKETTIMESTAMP', () => {
+//             assert.deepEqual(
+//                 pushRangeArguments([], '-', '+', {
+//                     AGGREGATION: {
+//                         type: TimeSeriesAggregationType.FIRST,
+//                         timeBucket: 1,
+//                         EMPTY: true
+//                     }
+//                 }),
+//                 ['-', '+', 'AGGREGATION', 'FIRST', '1', 'EMPTY']
+//             );
+//         });
+//     });
+
+//     it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => {
+//         assert.deepEqual(
+//             pushRangeArguments([], '-', '+', {
+//                 FILTER_BY_TS: ['ts'],
+//                 FILTER_BY_VALUE: {
+//                     min: 1,
+//                     max: 2
+//                 },
+//                 COUNT: 1,
+//                 ALIGN: 1,
+//                 AGGREGATION: {
+//                     type: TimeSeriesAggregationType.FIRST,
+//                     timeBucket: 1,
+//                     BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW,
+//                     EMPTY: true
+//                 }
+//             }),
+//             ['-', '+', 'FILTER_BY_TS', 'ts', 'FILTER_BY_VALUE', '1', '2',
+//             'COUNT', '1', 'ALIGN', '1', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-', 'EMPTY']
+//         );
+//     });
+// });
+
+// describe('pushMRangeGroupByArguments', () => {
+//     it('undefined', () => {
+//         assert.deepEqual(
+//             pushMRangeGroupByArguments([]),
+//             []
+//         );
+//     });
+
+//     it('with GROUPBY', () => {
+//         assert.deepEqual(
+//             pushMRangeGroupByArguments([], {
+//                 label: 'label',
+//                 reducer: TimeSeriesReducers.MAXIMUM
+//             }),
+//             ['GROUPBY', 'label', 'REDUCE', 'MAX']
+//         );
+//     });
+// });
+
+// describe('pushFilterArgument', () => {
+//     it('string', () => {
+//         assert.deepEqual(
+//             pushFilterArgument([], 'label=value'),
+//             ['FILTER', 'label=value']
+//         );
+//     });
+
+//     it('Array', () => {
+//         assert.deepEqual(
+//             pushFilterArgument([], ['1=1', '2=2']),
+//             ['FILTER', '1=1', '2=2']
+//         );
+//     });
+// });
+
+// describe('pushMRangeArguments', () => {
+//     it('without options', () => {
+//         assert.deepEqual(
+//             pushMRangeArguments([], '-', '+', 'label=value'),
+//             ['-', '+', 'FILTER', 'label=value']
+//         );
+//     });
+
+//     it('with GROUPBY', () => {
+//         assert.deepEqual(
+//             pushMRangeArguments([], '-', '+', 'label=value', {
+//                 GROUPBY: {
+//                     label: 'label',
+//                     reducer: TimeSeriesReducers.MAXIMUM
+//                 }
+//             }),
+//             ['-', '+', 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'MAX']
+//         );
+//     });
+// });
+
+// it('pushMRangeWithLabelsArguments', () => {
+//     assert.deepEqual(
+//         pushMRangeWithLabelsArguments([], '-', '+', 'label=value'),
+//         ['-', '+', 'WITHLABELS', 'FILTER', 'label=value']
+//     );
+// });
+
+// it('transformRangeReply', () => {
+//     assert.deepEqual(
+//         transformRangeReply([[1, '1.1'], [2, '2.2']]),
+//         [{
+//             timestamp: 1,
+//             value: 1.1
+//         }, {
+//             timestamp: 2,
+//             value: 2.2
+//         }]
+//     );
+// });
+
+// describe('transformMRangeReply', () => {
+//     assert.deepEqual(
+//         transformMRangeReply([[
+//             'key',
+//             [],
+//             [[1, '1.1'], [2, '2.2']]
+//         ]]),
+//         [{
+//             key: 'key',
+//             samples: [{
+//                 timestamp: 1,
+//                 value: 1.1
+//             }, {
+//                 timestamp: 2,
+//                 value: 2.2
+//             }]
+//         }]
+//     );
+// });
+
+// describe('transformMRangeWithLabelsReply', () => {
+//     assert.deepEqual(
+//         transformMRangeWithLabelsReply([[
+//             'key',
+//             [['label', 'value']],
+//             [[1, '1.1'], [2, '2.2']]
+//         ]]),
+//         [{
+//             key: 'key',
+//             labels: {
+//                 label: 'value'
+//             },
+//             samples: [{
+//                 timestamp: 1,
+//                 value: 1.1
+//             }, {
+//                 timestamp: 2,
+//                 value: 2.2
+//             }]
+//         }]
+//     );
+// });
+
+// describe('pushLatestArgument', () => {
+//     it('undefined', () => {
+//         assert.deepEqual(
+//             pushLatestArgument([]),
+//             []
+//         );
+//     });
+
+//     it('false', () => {
+//         assert.deepEqual(
+//             pushLatestArgument([], false),
+//             []
+//         );
+//     });
+
+//     it('true', () => {
+//         assert.deepEqual(
+//             pushLatestArgument([], true),
+//             ['LATEST']
+//         );
+//     });
+// })
diff --git a/packages/time-series/lib/commands/index.ts b/packages/time-series/lib/commands/index.ts
index ca382498060..43bde4767bf 100644
--- a/packages/time-series/lib/commands/index.ts
+++ b/packages/time-series/lib/commands/index.ts
@@ -1,473 +1,97 @@
-import * as ADD from './ADD';
-import * as ALTER from './ALTER';
-import * as CREATE from './CREATE';
-import * as CREATERULE from './CREATERULE';
-import * as DECRBY from './DECRBY';
-import * as DEL from './DEL';
-import * as DELETERULE from './DELETERULE';
-import * as GET from './GET';
-import * as INCRBY from './INCRBY';
-import * as INFO_DEBUG from './INFO_DEBUG';
-import * as INFO from './INFO';
-import * as MADD from './MADD';
-import * as MGET from './MGET';
-import * as MGET_WITHLABELS from './MGET_WITHLABELS';
-import * as QUERYINDEX from './QUERYINDEX';
-import * as RANGE from './RANGE';
-import * as REVRANGE from './REVRANGE';
-import * as MRANGE from './MRANGE';
-import * as MRANGE_WITHLABELS from './MRANGE_WITHLABELS';
-import * as MREVRANGE from './MREVRANGE';
-import * as MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS';
-import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
-import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
+import ADD from './ADD';
+import ALTER from './ALTER';
+import CREATE from './CREATE';
+import CREATERULE from './CREATERULE';
+import DECRBY from './DECRBY';
+import DEL from './DEL';
+import DELETERULE from './DELETERULE';
+import GET from './GET';
+import INCRBY from './INCRBY';
+import INFO_DEBUG from './INFO_DEBUG';
+import INFO from './INFO';
+import MADD from './MADD';
+import MGET_SELECTED_LABELS from './MGET_SELECTED_LABELS';
+import MGET_WITHLABELS from './MGET_WITHLABELS';
+import MGET from './MGET';
+import MRANGE_GROUPBY from './MRANGE_GROUPBY';
+import MRANGE_SELECTED_LABELS_GROUPBY from './MRANGE_SELECTED_LABELS_GROUPBY';
+import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS';
+import MRANGE_WITHLABELS_GROUPBY from './MRANGE_WITHLABELS_GROUPBY';
+import MRANGE_WITHLABELS from './MRANGE_WITHLABELS';
+import MRANGE from './MRANGE';
+import MREVRANGE_GROUPBY from './MREVRANGE_GROUPBY';
+import MREVRANGE_SELECTED_LABELS_GROUPBY from './MREVRANGE_SELECTED_LABELS_GROUPBY';
+import MREVRANGE_SELECTED_LABELS from './MREVRANGE_SELECTED_LABELS';
+import MREVRANGE_WITHLABELS_GROUPBY from './MREVRANGE_WITHLABELS_GROUPBY';
+import MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS';
+import MREVRANGE from './MREVRANGE';
+import QUERYINDEX from './QUERYINDEX';
+import RANGE from './RANGE';
+import REVRANGE from './REVRANGE';
+import { RedisCommands } from '@redis/client/dist/lib/RESP/types';
+
+export * from './helpers';
 
 export default {
-    ADD,
-    add: ADD,
-    ALTER,
-    alter: ALTER,
-    CREATE,
-    create: CREATE,
-    CREATERULE,
-    createRule: CREATERULE,
-    DECRBY,
-    decrBy: DECRBY,
-    DEL,
-    del: DEL,
-    DELETERULE,
-    deleteRule: DELETERULE,
-    GET,
-    get: GET,
-    INCRBY,
-    incrBy: INCRBY,
-    INFO_DEBUG,
-    infoDebug: INFO_DEBUG,
-    INFO,
-    info: INFO,
-    MADD,
-    mAdd: MADD,
-    MGET,
-    mGet: MGET,
-    MGET_WITHLABELS,
-    mGetWithLabels: MGET_WITHLABELS,
-    QUERYINDEX,
-    queryIndex: QUERYINDEX,
-    RANGE,
-    range: RANGE,
-    REVRANGE,
-    revRange: REVRANGE,
-    MRANGE,
-    mRange: MRANGE,
-    MRANGE_WITHLABELS,
-    mRangeWithLabels: MRANGE_WITHLABELS,
-    MREVRANGE,
-    mRevRange: MREVRANGE,
-    MREVRANGE_WITHLABELS,
-    mRevRangeWithLabels: MREVRANGE_WITHLABELS
-};
+  ADD,
+  add: ADD,
+  ALTER,
+  alter: ALTER,
+  CREATE,
+  create: CREATE,
+  CREATERULE,
+  createRule: CREATERULE,
+  DECRBY,
+  decrBy: DECRBY,
+  DEL,
+  del: DEL,
+  DELETERULE,
+  deleteRule: DELETERULE,
+  GET,
+  get: GET,
+  INCRBY,
+  incrBy: INCRBY,
+  INFO_DEBUG,
+  infoDebug: INFO_DEBUG,
+  INFO,
+  info: INFO,
+  MADD,
+  mAdd: MADD,
+  MGET_SELECTED_LABELS,
+  mGetSelectedLabels: MGET_SELECTED_LABELS,
+  MGET_WITHLABELS,
+  mGetWithLabels: MGET_WITHLABELS,
+  MGET,
+  mGet: MGET,
+  MRANGE_GROUPBY,
+  mRangeGroupBy: MRANGE_GROUPBY,
+  MRANGE_SELECTED_LABELS_GROUPBY,
+  mRangeSelectedLabelsGroupBy: MRANGE_SELECTED_LABELS_GROUPBY,
+  MRANGE_SELECTED_LABELS,
+  mRangeSelectedLabels: MRANGE_SELECTED_LABELS,
+  MRANGE_WITHLABELS_GROUPBY,
+  mRangeWithLabelsGroupBy: MRANGE_WITHLABELS_GROUPBY,
+  MRANGE_WITHLABELS,
+  mRangeWithLabels: MRANGE_WITHLABELS,
+  MRANGE,
+  mRange: MRANGE,
+  MREVRANGE_GROUPBY,
+  mRevRangeGroupBy: MREVRANGE_GROUPBY,
+  MREVRANGE_SELECTED_LABELS_GROUPBY,
+  mRevRangeSelectedLabelsGroupBy: MREVRANGE_SELECTED_LABELS_GROUPBY,
+  MREVRANGE_SELECTED_LABELS,
+  mRevRangeSelectedLabels: MREVRANGE_SELECTED_LABELS,
+  MREVRANGE_WITHLABELS_GROUPBY,
+  mRevRangeWithLabelsGroupBy: MREVRANGE_WITHLABELS_GROUPBY,
+  MREVRANGE_WITHLABELS,
+  mRevRangeWithLabels: MREVRANGE_WITHLABELS,
+  MREVRANGE,
+  mRevRange: MREVRANGE,
+  QUERYINDEX,
+  queryIndex: QUERYINDEX,
+  RANGE,
+  range: RANGE,
+  REVRANGE,
+  revRange: REVRANGE
+} as const satisfies RedisCommands;
 
-export enum TimeSeriesAggregationType {
-    AVG = 'AVG',
-    // @deprecated
-    AVERAGE = 'AVG',
-    FIRST = 'FIRST',
-    LAST = 'LAST',
-    MIN = 'MIN',
-    // @deprecated
-    MINIMUM = 'MIN',
-    MAX = 'MAX',
-    // @deprecated
-    MAXIMUM = 'MAX',
-    SUM = 'SUM',
-    RANGE = 'RANGE',
-    COUNT = 'COUNT',
-    STD_P = 'STD.P',
-    STD_S = 'STD.S',
-    VAR_P = 'VAR.P',
-    VAR_S = 'VAR.S',
-    TWA = 'TWA'
-}
-
-export enum TimeSeriesDuplicatePolicies {
-    BLOCK = 'BLOCK',
-    FIRST = 'FIRST',
-    LAST = 'LAST',
-    MIN = 'MIN',
-    MAX = 'MAX',
-    SUM = 'SUM'
-}
-
-export enum TimeSeriesReducers {
-    AVG = 'AVG',
-    SUM = 'SUM',
-    MIN = 'MIN',
-    // @deprecated
-    MINIMUM = 'MIN',
-    MAX = 'MAX',
-    // @deprecated
-    MAXIMUM = 'MAX',
-    RANGE = 'range',
-    COUNT = 'COUNT',
-    STD_P = 'STD.P',
-    STD_S = 'STD.S',
-    VAR_P = 'VAR.P',
-    VAR_S = 'VAR.S',
-}
-
-export type Timestamp = number | Date | string;
-
-export function transformTimestampArgument(timestamp: Timestamp): string {
-    if (typeof timestamp === 'string') return timestamp;
-
-    return (
-        typeof timestamp === 'number' ?
-            timestamp :
-            timestamp.getTime()
-    ).toString();
-}
-
-export function pushIgnoreArgument(args: RedisCommandArguments, ignore?: ADD.TsIgnoreOptions) {
-  if (ignore !== undefined) {
-    args.push('IGNORE', ignore.MAX_TIME_DIFF.toString(), ignore.MAX_VAL_DIFF.toString());
-  }
-}
-
-export function pushRetentionArgument(args: RedisCommandArguments, retention?: number): RedisCommandArguments {
-    if (retention !== undefined) {
-        args.push(
-            'RETENTION',
-            retention.toString()
-        );
-    }
-
-    return args;
-}
-
-export enum TimeSeriesEncoding {
-    COMPRESSED = 'COMPRESSED',
-    UNCOMPRESSED = 'UNCOMPRESSED'
-}
-
-export function pushEncodingArgument(args: RedisCommandArguments, encoding?: TimeSeriesEncoding): RedisCommandArguments {
-    if (encoding !== undefined) {
-        args.push(
-            'ENCODING',
-            encoding
-        );
-    }
-
-    return args;
-}
-
-export function pushChunkSizeArgument(args: RedisCommandArguments, chunkSize?: number): RedisCommandArguments {
-    if (chunkSize !== undefined) {
-        args.push(
-            'CHUNK_SIZE',
-            chunkSize.toString()
-        );
-    }
-
-    return args;
-}
-
-export function pushDuplicatePolicy(args: RedisCommandArguments, duplicatePolicy?: TimeSeriesDuplicatePolicies): RedisCommandArguments {
-    if (duplicatePolicy !== undefined) {
-        args.push(
-            'DUPLICATE_POLICY',
-            duplicatePolicy
-        );
-    }
-
-    return args;
-}
-
-export type RawLabels = Array<[label: string, value: string]>;
-
-export type Labels = {
-    [label: string]: string;
-};
-
-export function transformLablesReply(reply: RawLabels): Labels {
-    const labels: Labels = {};
-
-    for (const [key, value] of reply) {
-        labels[key] = value;
-    }
-
-    return labels
-}
-
-export function pushLabelsArgument(args: RedisCommandArguments, labels?: Labels): RedisCommandArguments {
-    if (labels) {
-        args.push('LABELS');
-
-        for (const [label, value] of Object.entries(labels)) {
-            args.push(label, value);
-        }
-    }
-
-    return args;
-}
-
-export interface IncrDecrOptions {
-    TIMESTAMP?: Timestamp;
-    RETENTION?: number;
-    UNCOMPRESSED?: boolean;
-    CHUNK_SIZE?: number;
-    LABELS?: Labels;
-}
-
-export function transformIncrDecrArguments(
-    command: 'TS.INCRBY' | 'TS.DECRBY',
-    key: string,
-    value: number,
-    options?: IncrDecrOptions
-): RedisCommandArguments {
-    const args = [
-        command,
-        key,
-        value.toString()
-    ];
-
-    if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) {
-        args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP));
-    }
-
-    pushRetentionArgument(args, options?.RETENTION);
-
-    if (options?.UNCOMPRESSED) {
-        args.push('UNCOMPRESSED');
-    }
-
-    pushChunkSizeArgument(args, options?.CHUNK_SIZE);
-
-    pushLabelsArgument(args, options?.LABELS);
-
-    return args;
-}
-
-export type SampleRawReply = [timestamp: number, value: string];
-
-export interface SampleReply {
-    timestamp: number;
-    value: number;
-}
-
-export function transformSampleReply(reply: SampleRawReply): SampleReply {
-    return {
-        timestamp: reply[0],
-        value: Number(reply[1])
-    };
-}
-
-export enum TimeSeriesBucketTimestamp {
-    LOW = '-',
-    HIGH = '+',
-    MID = '~'
-}
-
-export interface RangeOptions {
-    LATEST?: boolean;
-    FILTER_BY_TS?: Array<Timestamp>;
-    FILTER_BY_VALUE?: {
-        min: number;
-        max: number;
-    };
-    COUNT?: number;
-    ALIGN?: Timestamp;
-    AGGREGATION?: {
-        type: TimeSeriesAggregationType;
-        timeBucket: Timestamp;
-        BUCKETTIMESTAMP?: TimeSeriesBucketTimestamp;
-        EMPTY?: boolean;
-    };
-}
-
-export function pushRangeArguments(
-    args: RedisCommandArguments,
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    options?: RangeOptions
-): RedisCommandArguments {
-    args.push(
-        transformTimestampArgument(fromTimestamp),
-        transformTimestampArgument(toTimestamp)
-    );
-
-    pushLatestArgument(args, options?.LATEST);
-
-    if (options?.FILTER_BY_TS) {
-        args.push('FILTER_BY_TS');
-        for (const ts of options.FILTER_BY_TS) {
-            args.push(transformTimestampArgument(ts));
-        }
-    }
-
-    if (options?.FILTER_BY_VALUE) {
-        args.push(
-            'FILTER_BY_VALUE',
-            options.FILTER_BY_VALUE.min.toString(),
-            options.FILTER_BY_VALUE.max.toString()
-        );
-    }
-
-    if (options?.COUNT) {
-        args.push(
-            'COUNT',
-            options.COUNT.toString()
-        );
-    }
-
-    if (options?.ALIGN) {
-        args.push(
-            'ALIGN',
-            transformTimestampArgument(options.ALIGN)
-        );
-    }
-
-    if (options?.AGGREGATION) {
-        args.push(
-            'AGGREGATION',
-            options.AGGREGATION.type,
-            transformTimestampArgument(options.AGGREGATION.timeBucket)
-        );
-
-        if (options.AGGREGATION.BUCKETTIMESTAMP) {
-            args.push(
-                'BUCKETTIMESTAMP',
-                options.AGGREGATION.BUCKETTIMESTAMP
-            );
-        }
-
-        if (options.AGGREGATION.EMPTY) {
-            args.push('EMPTY');
-        }
-    }
-
-    return args;
-}
-
-interface MRangeGroupBy {
-    label: string;
-    reducer: TimeSeriesReducers;
-}
-
-export function pushMRangeGroupByArguments(args: RedisCommandArguments, groupBy?: MRangeGroupBy): RedisCommandArguments {
-    if (groupBy) {
-        args.push(
-            'GROUPBY',
-            groupBy.label,
-            'REDUCE',
-            groupBy.reducer
-        );
-    }
-
-    return args;
-}
-
-export type Filter = string | Array<string>;
-
-export function pushFilterArgument(args: RedisCommandArguments, filter: string | Array<string>): RedisCommandArguments {
-    args.push('FILTER');
-    return pushVerdictArguments(args, filter);
-}
-
-export interface MRangeOptions extends RangeOptions {
-    GROUPBY?: MRangeGroupBy;
-}
-
-export function pushMRangeArguments(
-    args: RedisCommandArguments,
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    filter: Filter,
-    options?: MRangeOptions
-): RedisCommandArguments {
-    args = pushRangeArguments(args, fromTimestamp, toTimestamp, options);
-    args = pushFilterArgument(args, filter);
-    return pushMRangeGroupByArguments(args, options?.GROUPBY);
-}
-
-export type SelectedLabels = string | Array<string>;
-
-export function pushWithLabelsArgument(args: RedisCommandArguments, selectedLabels?: SelectedLabels): RedisCommandArguments {
-    if (!selectedLabels) {
-        args.push('WITHLABELS');
-    } else {
-        args.push('SELECTED_LABELS');
-        args = pushVerdictArguments(args, selectedLabels);
-    }
-
-    return args;
-}
-
-export interface MRangeWithLabelsOptions extends MRangeOptions {
-    SELECTED_LABELS?: SelectedLabels;
-}
-
-export function pushMRangeWithLabelsArguments(
-    args: RedisCommandArguments,
-    fromTimestamp: Timestamp,
-    toTimestamp: Timestamp,
-    filter: Filter,
-    options?: MRangeWithLabelsOptions
-): RedisCommandArguments {
-    args = pushRangeArguments(args, fromTimestamp, toTimestamp, options);
-    args = pushWithLabelsArgument(args, options?.SELECTED_LABELS);
-    args = pushFilterArgument(args, filter);
-    return pushMRangeGroupByArguments(args, options?.GROUPBY);
-}
-
-export function transformRangeReply(reply: Array<SampleRawReply>): Array<SampleReply> {
-    return reply.map(transformSampleReply);
-}
-
-type MRangeRawReply = Array<[
-    key: string,
-    labels: RawLabels,
-    samples: Array<SampleRawReply>
-]>;
-
-interface MRangeReplyItem {
-    key: string;
-    samples: Array<SampleReply>;
-}
-
-export function transformMRangeReply(reply: MRangeRawReply): Array<MRangeReplyItem> {
-    const args = [];
-
-    for (const [key, _, sample] of reply) {
-        args.push({
-            key,
-            samples: sample.map(transformSampleReply)
-        });
-    }
-
-    return args;
-}
-export interface MRangeWithLabelsReplyItem extends MRangeReplyItem {
-    labels: Labels;
-}
-
-export function transformMRangeWithLabelsReply(reply: MRangeRawReply): Array<MRangeWithLabelsReplyItem> {
-    const args = [];
-
-    for (const [key, labels, samples] of reply) {
-        args.push({
-            key,
-            labels: transformLablesReply(labels),
-            samples: samples.map(transformSampleReply)
-        });
-    }
-
-    return args;
-}
-
-export function pushLatestArgument(args: RedisCommandArguments, latest?: boolean): RedisCommandArguments {
-    if (latest) {
-        args.push('LATEST');
-    }
-
-    return args;
-}
diff --git a/packages/time-series/lib/index.ts b/packages/time-series/lib/index.ts
index 6002556ca1a..52422bf1b5a 100644
--- a/packages/time-series/lib/index.ts
+++ b/packages/time-series/lib/index.ts
@@ -1,9 +1,8 @@
-export { default } from './commands';
-
 export {
-  TimeSeriesDuplicatePolicies,
-  TimeSeriesEncoding,
-  TimeSeriesAggregationType,
-  TimeSeriesReducers,
-  TimeSeriesBucketTimestamp
+  default,
+  TIME_SERIES_ENCODING, TimeSeriesEncoding,
+  TIME_SERIES_DUPLICATE_POLICIES, TimeSeriesDuplicatePolicies
 } from './commands';
+export { TIME_SERIES_AGGREGATION_TYPE, TimeSeriesAggregationType } from './commands/CREATERULE';
+export { TIME_SERIES_BUCKET_TIMESTAMP, TimeSeriesBucketTimestamp } from './commands/RANGE';
+export { TIME_SERIES_REDUCERS, TimeSeriesReducer } from './commands/MRANGE_GROUPBY';
diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts
index 6d534ccccef..9c59918e705 100644
--- a/packages/time-series/lib/test-utils.ts
+++ b/packages/time-series/lib/test-utils.ts
@@ -1,20 +1,21 @@
 import TestUtils from '@redis/test-utils';
 import TimeSeries from '.';
 
-export default new TestUtils({
-    dockerImageName: 'redislabs/redistimeseries',
-    dockerImageVersionArgument: 'timeseries-version'
+export default TestUtils.createFromConfig({
+  dockerImageName: 'redislabs/client-libs-test',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '8.2-rc1'
 });
 
 export const GLOBAL = {
-    SERVERS: {
-        OPEN: {
-            serverArguments: ['--loadmodule /usr/lib/redis/modules/redistimeseries.so'],
-            clientOptions: {
-                modules: {
-                    ts: TimeSeries
-                }
-            }
+  SERVERS: {
+    OPEN: {
+      serverArguments: [],
+      clientOptions: {
+        modules: {
+          ts: TimeSeries
         }
+      }
     }
+  }
 };
diff --git a/packages/time-series/package.json b/packages/time-series/package.json
index 65ee1e99c23..44fca5643f3 100644
--- a/packages/time-series/package.json
+++ b/packages/time-series/package.json
@@ -1,30 +1,25 @@
 {
   "name": "@redis/time-series",
-  "version": "1.1.0",
+  "version": "5.7.0",
   "license": "MIT",
-  "main": "./dist/index.js",
-  "types": "./dist/index.d.ts",
+  "main": "./dist/lib/index.js",
+  "types": "./dist/lib/index.d.ts",
   "files": [
-    "dist/"
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
   ],
   "scripts": {
-    "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
-    "build": "tsc",
-    "documentation": "typedoc"
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
+    "release": "release-it"
   },
   "peerDependencies": {
-    "@redis/client": "^1.0.0"
+    "@redis/client": "^5.7.0"
   },
   "devDependencies": {
-    "@istanbuljs/nyc-config-typescript": "^1.0.2",
-    "@redis/test-utils": "*",
-    "@types/node": "^20.6.2",
-    "nyc": "^15.1.0",
-    "release-it": "^16.1.5",
-    "source-map-support": "^0.5.21",
-    "ts-node": "^10.9.1",
-    "typedoc": "^0.25.1",
-    "typescript": "^5.2.2"
+    "@redis/test-utils": "*"
+  },
+  "engines": {
+    "node": ">= 18"
   },
   "repository": {
     "type": "git",
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 1157be947b9..d4a631fc008 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1,13 +1,21 @@
 {
-  "extends": "@tsconfig/node14/tsconfig.json",
   "compilerOptions": {
+    "lib": ["ES2023"],
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "target": "ES2022",
+
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noUnusedLocals": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    
+    "composite": true,
+    "sourceMap": true,
     "declaration": true,
+    "declarationMap": true,
     "allowJs": true,
-    "useDefineForClassFields": true,
-    "esModuleInterop": false,
     "resolveJsonModule": true
-  },
-  "ts-node": {
-    "files": true
   }
 }
diff --git a/tsconfig.json b/tsconfig.json
index 285b7ff0a97..180b3fc2ba7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,29 @@
 {
-  "extends": "./tsconfig.base.json",
-  "compilerOptions": {
-    "outDir": "./dist"
-  },
-  "include": [
-    "./index.ts"
+  "files": [],
+  "references": [
+    {
+      "path": "./packages/client"
+    },
+    {
+      "path": "./packages/test-utils"
+    },
+    {
+      "path": "./packages/bloom"
+    },
+    {
+      "path": "./packages/json"
+    },
+    {
+      "path": "./packages/search"
+    },
+    {
+      "path": "./packages/time-series"
+    },
+    {
+      "path": "./packages/entraid"
+    },
+    {
+      "path": "./packages/redis"
+    }
   ]
 }
