Skip to content

feat: add retry, health command, output controls, and 429 exit code (DIS-136)#39

Closed
ckorhonen wants to merge 4 commits intomainfrom
devin/DIS-136-1772640406
Closed

feat: add retry, health command, output controls, and 429 exit code (DIS-136)#39
ckorhonen wants to merge 4 commits intomainfrom
devin/DIS-136-1772640406

Conversation

@ckorhonen
Copy link
Collaborator

@ckorhonen ckorhonen commented Mar 4, 2026

feat: add retry, health command, output controls, and 429 exit code (DIS-136)

Summary

Implements the four subtasks from DIS-136:

  1. Retry logic (src/client.ts): Refactors get() and post() to share a private fetchWithRetry() method with exponential backoff + jitter for 429/5xx errors. Configurable via --retries (default 3). Respects Retry-After header. Logs retry attempts in verbose mode.

  2. Health command (src/commands/health.ts): New opensea health command that hits GET /api/v2/collections?limit=1 and reports JSON with status, latency_ms, and error details. Exits 3 on 429, exits 1 on other failures.

  3. Output controls (src/output.ts + all command files): New --fields <fields> (comma-separated field selection) and --max-items <n> (array truncation) global options. Filtering is applied at the data level before formatting, so it works with json, table, and toon formats. All 9 command files updated to accept and pass through a getFilters thunk.

  4. 429 exit code: process.exit(3) for rate-limit errors vs process.exit(1) for other API errors, consistent across both cli.ts error handler and health command.

Confidence: 🟡 MEDIUM — All tests pass (172/172), lint and type-check clean, but the retry logic refactors the core HTTP path and was only verified via unit tests, not against the live API.

Review & Testing Checklist for Human

  • Retry logic is now the sole HTTP code path: fetchWithRetry replaces the previous inline fetch in both get() and post(). Verify the refactor didn't change behavior for non-retry cases (especially AbortSignal.timeout and verbose logging). The throw lastError! non-null assertion at the end of the retry loop is logically unreachable but triggers a Biome warning.
  • End-to-end smoke test: Run the commands below to verify the new features work against the live API. Unit tests alone don't cover real network behavior.
  • Exit code 3 convention: Not yet documented in .agents/rules.md (which only lists codes 0, 1, 2). Consider updating rules.md after merge.

Recommended Test Plan

# Install locally
npm run build && npm link

# Test health command
opensea health --api-key $OPENSEA_API_KEY

# Test output filters
opensea collections list --limit 5 --fields name,slug --format json
opensea collections list --limit 10 --max-items 3 --format table

# Test retry (may require rate-limiting or mocking)
# Expected: retries on 429/500 errors, gives up after 3 retries

Notes

  • Test mock change (test/mocks.ts): mockResolvedValuemockImplementation ensures each retry attempt gets a fresh Response object (bodies can only be read once). This is correct but changes behavior for all existing tests—all 172 still pass.
  • filterData shallow-clones the input object before truncating nested arrays to avoid mutating caller data.
  • Empty --fields tokens (e.g. --fields "" or trailing commas) are filtered out via .filter(Boolean).

Updates since last revision

  • Fixed mutation hazard: filterData now shallow-clones the object before truncating nested arrays (src/output.ts:23).
  • Fixed empty field tokens: getFilters() now filters empty strings from --fields splits (src/cli.ts:93).
  • Consistent 429 exit code: health command now exits 3 on 429 errors, matching cli.ts behavior (src/commands/health.ts:29).
  • Removed dead code: getBaseUrl() and getApiKey() public methods on OpenSeaClient were removed.

Link to Devin Session: https://app.devin.ai/sessions/96ff332df20a42459b338655d36d8447
Requested by: @ckorhonen


Open with Devin

…DIS-136)

- Add exponential backoff retry logic with jitter for 429/5xx errors
- Add --retries option (default 3) with Retry-After header support
- Add health command for API connectivity diagnostics
- Add --fields and --max-items output control options
- Use exit code 3 for 429 rate limit errors (vs 1 for other API errors)
- Add comprehensive tests for all new functionality

Co-Authored-By: Chris K <ckorhonen@gmail.com>
@devin-ai-integration
Copy link
Contributor

Original prompt from Chris K
Please work on ticket "opensea-cli: Add retry, diagnostics, and output control" ([DIS-136](https://linear.app/opensea/issue/DIS-136/opensea-cli-add-retry-diagnostics-and-output-control))

PLAYBOOK_md:
# Ticket to PR

## Overview

This playbook guides the process of taking a Linear ticket from initial scoping through implementation to final review. The workflow ensures proper context gathering, quality implementation, and thorough code review before delivery. The agent uses the Linear MCP to manage ticket status and communication throughout.

## What's Needed From User

- Linear ticket URL or ticket ID (e.g., `ENG-123` or `https://linear.app/team/issue/ENG-123/...`)
- Repository access for the codebase where changes will be made

<phase name="Disambiguation" id="1">
## Disambiguation Phase

Think about the full user intent. Tickets are sometimes sparse. Make sure you disambiguate to the full scope that the user intended.

1. Fetch the ticket details using the Linear MCP `get_issue` tool with the ticket ID
2. Before diving into code: use the devin MCP to get a high-level understanding of the relevant systems and architecture. Use `ask_question` to learn about the relevant systems – send queries for multiple repos that could be relevant to get the full picture. Use `read_wiki_contents` to then get a better understanding how different parts of the codebase connect to each other.
3. Gather additional context to understand what the ticket means and refers to:
   - Look at past tickets in the same project and from the same author to understand patterns and terminology
   - Search for related commits and PRs (by author and content) that may provide context on the affected systems
   - Check any linked documents, designs, or parent tickets
   - Investigate the actual code
4. Identify any ambiguity in what the ticket refers to or asks for, including jargon or project-specific terms and use all means necessary to answer this yourself
5. Consult your smart friend: pass in the r... (8109 chars truncated...)

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration bot and others added 2 commits March 4, 2026 16:17
Co-Authored-By: Chris K <ckorhonen@gmail.com>
…mpty fields, consistent 429 exit code in health

Co-Authored-By: Chris K <ckorhonen@gmail.com>
@devin-ai-integration devin-ai-integration bot marked this pull request as ready for review March 4, 2026 16:24
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 7 additional findings.

Open in Devin Review

…mand mocks)

Co-Authored-By: Chris K <ckorhonen@gmail.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

import { Command } from "commander"
import { OpenSeaAPIError, type OpenSeaClient } from "../client.js"

export function healthCommand(getClient: () => OpenSeaClient): Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Health command violates mandatory command file pattern from rules.md

The repository's .agents/rules.md (lines 92-118) explicitly states: "Every command file in src/commands/ follows this pattern" — the pattern requires the factory function to receive both getClient and getFormat thunks, and route all output through formatOutput() to console.log(). The healthCommand factory only receives getClient, never calls formatOutput(), and instead constructs JSON directly with JSON.stringify(). This also means --format table and --format toon are silently ignored for the health command, producing inconsistent behavior compared to all other commands.

Prompt for agents
In src/commands/health.ts, update the healthCommand factory function to accept getFormat (and optionally getFilters) thunks matching the pattern of all other command files. Use formatOutput() for the success output at line 13 instead of raw JSON.stringify. For the error paths (lines 17-42), those write to stderr which is acceptable per the rules (stderr for errors). Also update src/cli.ts line 113 to pass getFormat (and getFilters) when wiring the health command: program.addCommand(healthCommand(getClient, getFormat, getFilters)).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +7 to +43
.action(async () => {
const client = getClient()
const start = performance.now()
try {
await client.get("/api/v2/collections", { limit: 1 })
const ms = Math.round(performance.now() - start)
console.log(JSON.stringify({ status: "ok", latency_ms: ms }, null, 2))
} catch (error) {
const ms = Math.round(performance.now() - start)
if (error instanceof OpenSeaAPIError) {
console.error(
JSON.stringify(
{
status: "error",
latency_ms: ms,
http_status: error.statusCode,
message: error.responseBody,
},
null,
2,
),
)
process.exit(error.statusCode === 429 ? 3 : 1)
}
console.error(
JSON.stringify(
{
status: "error",
latency_ms: ms,
message: (error as Error).message,
},
null,
2,
),
)
process.exit(1)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Health command violates Design Rule #3: 'Commands are thin wrappers'

.agents/rules.md line 171 states: "Commands are thin wrappers — CLI commands should only parse arguments, call the client, and format output. No business logic in command files." The health command at src/commands/health.ts contains substantial business logic: latency measurement (performance.now() at lines 9, 12, 15), custom error handling with process.exit() calls (lines 29, 42), and structured error JSON construction (lines 17-28, 31-40). Other commands let errors propagate to the global handler in src/cli.ts:118-148. This means the health command's error output has a different JSON structure (http_status, latency_ms) than the global handler's format (status, path), creating an inconsistency for script consumers.

Prompt for agents
Refactor src/commands/health.ts so the command action is a thin wrapper: it should call client.get(), format the success result through formatOutput(), and let errors propagate to the global error handler in src/cli.ts. Latency measurement could be moved to the client layer or done externally. The process.exit() calls for 429/error should be removed from the command file — the global handler in cli.ts already handles OpenSeaAPIError with exit code 3 for 429 and exit code 1 for other errors.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@ckorhonen
Copy link
Collaborator Author

Closing this PR — its contents are now fully covered by the individual PRs that were merged (#34, #35, #36, #38) plus #37 (retry logic). This omnibus PR is redundant.

@ckorhonen ckorhonen closed this Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants