redis/extend-commands-api
Add or extend Redis commands in the Jedis client API — a new core command, a family of new commands, an extension to an existing command's options, or a module command (Search/TimeSeries/JSON/Bloom). Gathers evidence (Redis server PR, HLD document), plans the full implementation matrix in plan mode, then implements with unit and integration tests following Jedis maintainer conventions.
npx skills add https://github.com/redis/jedis --skill extend-commands-api
Implement a new Redis command (or extend an existing one) in Jedis, following the
conventions Jedis maintainers enforce in review.
Do all of the following before writing any plan or code:
markdown file containing the High-Level Design for the command(s) (or confirm
that no HLD exists). If a path is given, read it fully — it is the primary
source for syntax, semantics, reply shape, and edge cases.
redis/redis GitHub repo. First verifythat gh works in the current (sandboxed) environment:
gh auth status
Sandboxes often block access to credential files (~/.netrc, gh config), so
gh may report unauthenticated here even though it works on the user's
machine. If gh auth status fails or reports no authentication, **ask the
user for permission to run the gh commands outside the sandbox** (e.g. with
the sandbox override for these specific read-only commands), explaining that
gh cannot reach its credentials from inside the sandbox. Only if the user
declines (or gh is genuinely not logged in anywhere) fall back to the
unauthenticated GitHub REST API or fetching
https://github.com/redis/redis/pulls?q=<command>.
Then search for the PR that adds/extends the command on the server:
gh search prs --repo redis/redis "<COMMAND NAME>" --limit 10
gh pr view <num> --repo redis/redis
gh pr diff <num> --repo redis/redis # look at src/commands/*.json for exact syntax
From the server PR,
extract: exact wire syntax (argument order and optionality), reply type per
RESP2/RESP3, error conditions, time complexity, and — critically — the **first
server version carrying the feature** (RC builds like 8.7.225 = Redis 8.8 RC
are used for test gating). Note whether the server marks the feature
*experimental / in preview*.
integration environment using the latest .env.vX.XX file under
src/test/resources/env/ (pick the highest version numerically, not
lexicographically — e.g. 8.10 > 8.8):
ls src/test/resources/env/.env.v* | sort -V | tail -1 # → e.g. .env.v8.10
make start version=8.10
Then probe the standalone0 endpoint. Read its connection details from
src/test/resources/endpoints.json (currently redis://localhost:6379,
password foobared) and check the target command via redis-cli:
redis-cli -u redis://localhost:6379 -a foobared INFO server | grep redis_version
redis-cli -u redis://localhost:6379 -a foobared COMMAND INFO <COMMAND> # non-empty → command exists
redis-cli -u redis://localhost:6379 -a foobared COMMAND DOCS <COMMAND> # arity/args — compare with the server PR
For a NEW command, COMMAND INFO must return a non-empty reply. For an
EXTENDED command, additionally invoke it with the new syntax against a
scratch key and confirm the server accepts it (no ERR syntax error /
ERR unknown argument).
If the command/option is missing on the latest env version, the image tag
pinned in the .env.vX.XX file is too old for the target change. Interactively
ask the user for a redislabs/client-libs-test image tag that contains it
(e.g. an RC/edge/milestone build — tags are listed at
https://hub.docker.com/r/redislabs/client-libs-test/tags; you may check them
yourself and suggest candidates). Then restart the environment with that tag
and re-run the probes:
make stop
make start CLIENT_LIBS_TEST_IMAGE_TAG=<tag>
If the user has no tag to offer (or the probe still fails), report it and
proceed anyway: the implementation can continue, but integration tests will
be gated/skipped until a redislabs/client-libs-test image ships the change.
Keep the environment running for later test runs, or make stop if you
won't need it soon.
the running environment, derive a small set of redis-cli scenarios from the
HLD and the server PR and run them against standalone0. These serve two
purposes at once:
server — happy path, each new option/keyword, reply shape (run with
redis-cli -3 too if RESP3 replies differ), edge cases and error
conditions called out in the HLD (empty/missing key, out-of-range args,
conflicting options), so the Java implementation is built against observed
replies, not assumptions.
WHY the command/option exists — what a user could not do (or could only do
awkwardly) before, and how the new API covers it. Prefer realistic data
over foo/bar (e.g. rate-limit counters for a bounded INCR, sensor
readings for a time-series aggregator).
Save the scenarios as a commented script in a scratch file (one block per
use-case: a one-line "what this demonstrates" comment, the redis-cli
commands, and the observed reply pasted back as comments). Carry this
material forward: it feeds the Phase 1 plan (as the explanation of the
feature and the source of expected values for tests), the Java test
assertions, and the PR description. If the command could not be made
available on any image (see step 3), still write the scenarios from the
HLD/PR as *expected* transcripts and mark them unverified.
docs/integration-testing.md in the Jedis repo — it defines the testenvironment, endpoint discovery, *Test vs *IT naming, and how to run tests.
command group, similar reply shape) so the plan mirrors real code, not
guesswork.
@since version: mvn help:evaluate -Dexpression=project.version -q -DforceStdout
Strip -SNAPSHOT (e.g. 8.0.0-SNAPSHOT → @since 8.0).
Enter plan mode. Using the evidence, classify the change with the decision
tree below, enumerate the exact file-by-file touch list, the test matrix, and the
gating annotations. Open the plan with a short "what this feature enables"
section built from the redis-cli showcase scenarios (Phase 0 step 4), including
one or two representative command/reply transcripts, so the user sees the
use-case before the file list. Present the plan to the user and **explicitly ask
permission to execute in auto-accept mode** before implementing. Do not start
editing files until the user approves.
A. Extension of an existing command that fits an existing params class
(e.g. new enum value / new option token):
CommandObjects, UnifiedJedis, PipeliningBase, or Jedis — the existing
IParams.addParams(CommandArguments) delegation carries the new option through
automatically, for both String and binary surfaces.
Rawable(bytes come from SafeEncoder.encode(name()) — self-wiring).
this, extendaddParams(), and update equals/hashCode in sync.
class backward-compatibly — keep old constructors/getters working, add new
accessors, update the builder in the relevant *BuilderFactory to branch on
reply size. Jedis does not defensively copy response collections (stated
maintainer convention).
B. New core command(s) — the FULL matrix, every layer:
Protocol.java — new Command enum constant(s); new Keyword constants forsub-tokens, grouped under a // <FEATURE> keywords comment, alphabetical.
**Never add a Keyword that duplicates a token already carried by a dedicated
Rawable enum** (dead keywords get flagged in review).
commands/: <Group>Commands, <Group>BinaryCommands,<Group>PipelineCommands, <Group>PipelineBinaryCommands. For a whole new
command *family*, create four new <Family>*Commands interfaces and add one
extends entry to JedisCommands, JedisBinaryCommands, PipelineCommands,
PipelineBinaryCommands.
CommandObjects — String and byte[] factory methods side by side under a// <Feature> commands section. ClusterCommandObjects needs overrides ONLY
for multi-key commands requiring slot checks; single-key commands need nothing.
UnifiedJedis — one-line @Override delegating toexecuteCommand(commandObjects.xxx(...)).
PipeliningBase — appendCommand(commandObjects.xxx(...)) returningResponse<T>.
Jedis (legacy) — checkIsInMultiOrPipeline(); connection.executeCommand(commandObjects.xxx(...)).pom.xml — add every new file to the formatter-maven-plugin includes list(this repo format-enforces an allowlist; new files must be registered).
C. Extension needing new overloads / a new params class (hybrid): new methods
go through the full matrix of B; the option plumbing follows the params
conventions below.
D. Module command (search / timeseries / json / bloom):
*BinaryCommands variantsfor modules; never create them.
(TimeSeriesProtocol.TimeSeriesCommand/TimeSeriesKeyword,
SearchProtocol.SearchCommand/SearchKeyword, …), each an enum implementing
ProtocolCommand/Rawable with SafeEncoder.encode()-cached bytes.
interface methods — PRs #4504 and #4534 shipped whole features with ZERO
changes to interfaces, CommandObjects, UnifiedJedis, or PipeliningBase.
Reducer subclasses) build a List<Object> viagetOwnArgs(); the parent computes narg automatically. Canonicalize clause
order at serialization time regardless of builder call order.
(TimeSeriesBuilderFactory, SearchBuilderFactory); search aggregation results
are loosely typed and often absorb new reply content with no parsing changes.
interfaces, paired CommandObjects methods, and all three clients. Reviewers
explicitly request binary integration tests too.
byte[] overloads. Numeric/structuralargs (long index, ranges, booleans, enums) stay identical.
surfaces; if a param carries text, give the params class both foo(String) and
foo(byte[]) setters (see ArgrepParams).
String→byte[],List<String>→List<byte[]>); count/index/model results stay shared.
params/)IParams with addParams(CommandArguments args). Fluent settersreturn this; provide a static factory named after the class
(increxParams(), rangeParams()).
options but differ in a typed field (long vs double), create a self-typed
abstract base: BaseFooParams<T extends BaseFooParams<T>> with a private
self() cast, plus two concrete subclasses — compile-time type safety over a
polymorphic single class.
equals/hashCode (needed for Mockito matching in mocked tests)and keep them in sync with every new field. toString is not required on
params classes.
IllegalArgumentException /IllegalStateException messages, consistent wording across sibling classes
(e.g. "Aggregators must be non-null and non-empty"). Required-config checks may
run at serialization time. No client-side server-version checks — an old server
returning an error is acceptable and should be noted in the PR description.
SetParams).BuilderFactory / resps/)(LONG_LIST, DOUBLE_LIST, STRING, ENCODED_OBJECT_MAP, …). Maintainers
actively reject bespoke response classes for 2-element arrays and the like.
redis.clients.jedis.util.KeyValue<K,V> for pair replies.resps/: public String constants for reply field names, a
Map<String,Object>-taking constructor, typed getters, plus a raw-map accessor
for forward compatibility; subclass for FULL/extended variants.
Long — never Optional/OptionalLongin the command API; consistency with existing signatures (e.g. zrank) wins.
into distinctly-named typed Java methods (aropAggregate/aropBitwise/aropCount)
rather than one polymorphic method. But mode selected by argument type uses
overloads of one name (increx(key, long, …) / increx(key, double, …)),
with the mode keyword (BYINT/BYFLOAT) implied by the overload.
SafeEncoder.encode() — never String.getBytes().Protocol.toByteArray().args/ and implement Rawable withraw = SafeEncoder.encode(name()) cached in the constructor.
@since, @ExperimentalUnifiedJedis,Jedis, PipeliningBase, CommandObjects) carry none.
Command</a></b>, prose semantics, Time complexity: O(...), @param`,
@return, @since <version>. Binary variants get short javadoc with
@see to the String variant. Pipeline variants: one line — "Pipeline variant
of {@link ...}" + @since.
@since on every new public method and class, computed from pom.xml.(interfaces, methods, params, args, resps) with
redis.clients.jedis.annots.Experimental and label the PR experimental.
Do NOT use @Experimental for stable GA features.
Follow docs/integration-testing.md for environment, layout, and naming. The
established per-command test layers (write all that apply):
params/FooParamsTest, plain *Test, no Redis):@Nested groups (ValidationTests, AddParamsTests, overload-equivalence),
asserting exact wire args and order with
redis.clients.jedis.util.CommandArgumentsMatchers
(hasArgumentCount, hasArguments) and RawableFactory.from(...); also test
the equals/hashCode contract.
@Mock CommandObject<T>fields to MockedCommandObjectsTestBase (name them by TYPE, e.g.
listLongCommandObject — reuse existing ones when the type matches), then
when/verify tests in mocked/unified/UnifiedJedis<Group>CommandsTest and
mocked/pipeline/PipeliningBase<Group>CommandsTest.
commands/unified/<Group>CommandsTestBase when one exists; for a new family,
create <Family>CommandsTestBase extends UnifiedJedisCommandsTestBase plus
thin per-topology runners — standalone
(RedisClientCommandsTestHelper.getClient(protocol)) and cluster
(ClusterCommandsTestHelper.getCleanCluster(protocol)). **New concrete
integration classes MUST be named *IT** — never *IntegrationTest, never
@Tag("integration") on new classes.
hash-tagged keys (s1{:}) so keys share a slot; tests semantically
incompatible with cluster get @Test @Override + @Disabled("<reason>")
empty bodies.
Jedis coverage: extend commands/jedis/<Group>*CommandsTest(which extends JedisCommandsTestBase) — can be minimal (smoke/missing-key)
when the unified base covers behavior fully. Binary variants get their own
integration tests (reviewers ask for these explicitly).
Cross-cutting test conventions:
@ParameterizedClass +@MethodSource("redis.clients.jedis.commands.CommandsTestsParameters#respVersions")
(or #jedisRespVersions for legacy) — no per-test work; just make sure new
classes inherit the right base.
@SinceRedisVersion("<RC build>")(e.g. "8.7.225" for 8.8) at the shared base-class level ONCE — do not repeat
it on subclasses (maintainers remove redundant ones). Command not yet in any
GA server → @EnabledOnCommand("<COMMAND>") (capability probe via COMMAND
INFO). Module presence as precondition → assumeTrue(hasCommand(...)) probes.
Environment exclusions → `@ConditionalOnEnv(value = TestEnvUtil.ENV_...,
enabled = false)` (e.g. skip Redis Enterprise for brand-new features).
with/without optional args, standalone + cluster), asserting real semantics,
not just "no error". Avoid brittle assertions (no hard-coded hashCode()
literals).
Jedis pins the CI JDK (Java 8) for the test build — check
.github/workflows/ for the exact version and use a matching local JDK. Verify
with java -version before running anything; a newer JDK may fail the build or
silently produce the wrong bytecode target.
mvn -B test, or mvn -Dtest=FooParamsTest test.verify lifecycle: make start version=8.8 # pick the version that carries the new command
mvn -B verify # or: mvn -Dtest=FooIT verify
make stop
Never run integration coverage via mvn test/surefire:test — Failsafe owns
*IT classes. If a brand-new command isn't in any published
redislabs/client-libs-test tag yet, say so: the integration tests will be
skipped/gated (that is expected and acceptable — @EnabledOnCommand /
@SinceRedisVersion handle it), but they must still be written and compile.
must propagate across all 4 interfaces × 3 implementations × all test layers).
pom.xml formatter-plugin includes.@since (and @Experimental if preview) on all new public API.Protocol.Keyword constants.equals/hashCode on params updated and unit-tested.docs/ updated if user-facing behavior/configuration changed; migrationguide entry if anything breaks.
layers intentionally did NOT change and why, and behavior against older
servers.
Take redis/extend-commands-api from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.