From e44ae4e8f99a29bc6cfc4ec3a0ae482380efe76c Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 12 Aug 2026 18:17:44 +0300 Subject: [PATCH 1/2] DX-2866: add --user-agent to start-redis Adds an optional --user-agent flag that is sent as the User-Agent header on the start-redis request, so agents can identify themselves (claude-code, cursor, codex, opencode, ...). When the flag is omitted the CLI sends upstash/cli. The README tells agents to pass the flag on every call; the default is left undocumented. --- README.md | 6 ++++++ src/commands/start-redis.ts | 16 ++++++++++++++-- tests/unit/start-redis.test.ts | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ebf46ee..4da8779 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ upstash start-redis This prints markdown with credentials and a quickstart. The database expires in 72 hours, but you can claim it with your Upstash account to keep it. Pass `--id ` to re-fetch the credentials of a database you created earlier. Every command that touches your Upstash account needs authentication — only `start-redis` and the `login`/`logout` credential helpers do not. +If you are an agent, identify yourself with `--user-agent` on every `start-redis` call, so we can see which agents are creating databases: + +```bash +upstash start-redis --user-agent claude-code # or cursor, codex, opencode, ... +``` + ## Authentication Grab a Developer API key from the [Upstash Console](https://console.upstash.com/account/api), then save it once per machine: diff --git a/src/commands/start-redis.ts b/src/commands/start-redis.ts index f5cc2b4..b48b91c 100644 --- a/src/commands/start-redis.ts +++ b/src/commands/start-redis.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import { plainError } from "../output.js"; const START_REDIS_URL = "https://upstash.com/start-redis"; +const DEFAULT_USER_AGENT = "upstash/cli"; export function registerStartRedis(program: Command): void { program @@ -10,7 +11,18 @@ export function registerStartRedis(program: Command): void { "Get a free temporary Redis database — no account or API key needed. Prints markdown with credentials and a quickstart. Expires in 72 hours; claim it with an Upstash account to keep it.", ) .option("--id ", "Re-fetch the credentials of a database created earlier") - .action(async (flags: { id?: string }) => { + .option( + "--user-agent ", + "Identify the caller. If you are an agent, pass your own name (e.g. claude-code, cursor, codex, opencode)", + ) + .action(async (flags: { id?: string; userAgent?: string }) => { + const headers: Record = { + "User-Agent": flags.userAgent || DEFAULT_USER_AGENT, + }; + if (flags.id) { + headers["Idempotency-Key"] = flags.id; + } + // This command's output is markdown, not JSON, so network failures are // reported as plain text too rather than through the JSON error path. let response: Response; @@ -18,7 +30,7 @@ export function registerStartRedis(program: Command): void { try { response = await fetch(START_REDIS_URL, { method: "POST", - headers: flags.id ? { "Idempotency-Key": flags.id } : undefined, + headers, }); text = await response.text(); } catch (err) { diff --git a/tests/unit/start-redis.test.ts b/tests/unit/start-redis.test.ts index a18ea56..cc0cb7b 100644 --- a/tests/unit/start-redis.test.ts +++ b/tests/unit/start-redis.test.ts @@ -38,7 +38,7 @@ describe("start-redis", () => { const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe("https://upstash.com/start-redis"); expect(init?.method).toBe("POST"); - expect(init?.headers).toBeUndefined(); + expect(init?.headers).toEqual({ "User-Agent": "upstash/cli" }); expect(output).toEqual([MARKDOWN.trimEnd()]); }); @@ -50,7 +50,35 @@ describe("start-redis", () => { await run(["start-redis", "--id", "db-123"]); const [, init] = fetchMock.mock.calls[0]!; - expect(init?.headers).toEqual({ "Idempotency-Key": "db-123" }); + expect(init?.headers).toEqual({ + "Idempotency-Key": "db-123", + "User-Agent": "upstash/cli", + }); + }); + + it("sends the caller name as a user agent when --user-agent is given", async () => { + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response(MARKDOWN, { status: 200 })); + + await run(["start-redis", "--user-agent", "claude-code"]); + + const [, init] = fetchMock.mock.calls[0]!; + expect(init?.headers).toEqual({ "User-Agent": "claude-code" }); + }); + + it("sends both headers when --id and --user-agent are given", async () => { + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response(MARKDOWN, { status: 200 })); + + await run(["start-redis", "--id", "db-123", "--user-agent", "cursor"]); + + const [, init] = fetchMock.mock.calls[0]!; + expect(init?.headers).toEqual({ + "Idempotency-Key": "db-123", + "User-Agent": "cursor", + }); }); it("throws a plain error when the network is unreachable", async () => { From 57d3b0495635a5861586008b07350793a94313aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:29:53 +0000 Subject: [PATCH 2/2] test: avoid flaky rename-back in integration tests Co-authored-by: CahidArda <57228345+CahidArda@users.noreply.github.com> --- tests/integration/redis.test.ts | 4 ---- tests/integration/search.test.ts | 3 --- tests/integration/vector.test.ts | 3 --- 3 files changed, 10 deletions(-) diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts index 12b2a62..13d9220 100644 --- a/tests/integration/redis.test.ts +++ b/tests/integration/redis.test.ts @@ -61,10 +61,6 @@ describe("redis rename", () => { const db = await runCommand(p2, ["redis", "get", "--db-id", dbId!]) as Database; expect(db.database_name).toBe(newName); - - // rename back so subsequent tests aren't affected - const p3 = await createRedisProgram(); - await runCommand(p3, ["redis", "rename", "--db-id", dbId!, "--name", TEST_NAME]); }); }); diff --git a/tests/integration/search.test.ts b/tests/integration/search.test.ts index ccecb71..e31036d 100644 --- a/tests/integration/search.test.ts +++ b/tests/integration/search.test.ts @@ -51,9 +51,6 @@ describe("search rename", () => { const p2 = await createSearchProgram(); const idx = await runCommand(p2, ["search", "get", "--index-id", indexId!]) as SearchIndex; expect(idx.name).toBe(newName); - - const p3 = await createSearchProgram(); - await runCommand(p3, ["search", "rename", "--index-id", indexId!, "--name", TEST_NAME]); }); }); diff --git a/tests/integration/vector.test.ts b/tests/integration/vector.test.ts index 8e4e981..d07ce87 100644 --- a/tests/integration/vector.test.ts +++ b/tests/integration/vector.test.ts @@ -54,9 +54,6 @@ describe("vector rename", () => { const p2 = await createVectorProgram(); const idx = await runCommand(p2, ["vector", "get", "--index-id", indexId!]) as VectorIndex; expect(idx.name).toBe(newName); - - const p3 = await createVectorProgram(); - await runCommand(p3, ["vector", "rename", "--index-id", indexId!, "--name", TEST_NAME]); }); });