Skip to content

reply-cli: profile management, team context & raw api passthrough - #2

Merged
vigubikReply merged 8 commits into
mainfrom
dev/REPLY-51354-profile-management
Jul 22, 2026
Merged

reply-cli: profile management, team context & raw api passthrough#2
vigubikReply merged 8 commits into
mainfrom
dev/REPLY-51354-profile-management

Conversation

@vigubikReply

Copy link
Copy Markdown
Contributor

Summary

Builds out the reply CLI beyond bare auth: full profile management, team context + a team command, and a raw api passthrough that already reaches the entire v3 API. Covers REPLY-51354, REPLY-51325, REPLY-51257, REPLY-51258, REPLY-51259.

What's included

REPLY-51354 — profile management

  • profile rename <old> <new> (moves the stored credential with the def), profile delete <name> (rm; confirm-by-default, --yes to skip), profile show [name] (redacted; authorization shown in precedence order), profile unset <name> <field> (clear authority/api_base/team-id).
  • URL validation on --authority/--api-base (http/https only).

REPLY-51325 — client identification

  • Sends User-Agent: reply-cli/<version> on API requests (captured by APM as user_agent.original), so API-key traffic is identifiable even without an OAuth client_id.

REPLY-51257 — team/acting-user context

  • Profile api_base is the host (no /v3); the version prefix lives in the request path. X-TEAM-ID precedence --team-id > REPLY_TEAM_ID > profile team_id; org-key acting user via --user-id / --user-email.

REPLY-51258 — team command + conflict guidance

  • team list / current (pinned + effective-from-whoami) / use <id> (verified) / clear, editing the current profile's team_id (no separate team state).
  • Discovery via /v3/whoami/team-users, resilient to the TEAM_REQUIRED 403 (same teams[]); falls back to the single whoami team when the endpoint is organization-only.
  • On a workload team/user-resolution conflict, prints a tailored fix + team list to stderr (aligns with backend contract in reply-team/replyapp#1704).

REPLY-51259 — reply api raw passthrough

  • reply api <path> [--method] [--body]{code, data}; GET by default, POST when --body (inline / @file / - stdin). Request URL is literally api_base + path (slash-safe join) — no version-specific code.
  • --verbose prints a curl-style request/response trace to stderr with Authorization redacted; stdout stays clean JSON (pipe-safe).

Docs & help

  • Worked Examples: blocks + clarity notes on every non-trivial command; README updated (intro surfaces reply api, Teams/Profiles/Raw-API sections, env-var table).

Testing

  • 234 unit/integration tests pass (npm test); fully offline (mocked fetch, temp config/credential store).
  • Verified against dev end-to-end: api /v3/whoami (and --verbose, redacted) → 200; team list/current/use/clear for a personal account; profile rename/delete/show/unset.
  • The team-conflict guidance path is unit-tested now; full integration depends on reply-team/replyapp#1704 reaching dev.

Notes

  • No breaking changes to existing auth commands.
  • Backend dependency: the TEAM_* / USER_* problem+json contract ships in reply-team/replyapp#1704.

🤖 Generated with Claude Code

vigubikReply and others added 7 commits July 22, 2026 12:28
…Y-51354)

Profile-management completeness for the reply CLI:

- profile rename <old> <new>: renames the config def and moves the stored
  credential to the new key (store is keyed by profile name); refuses if the
  target name or credential already exists; repoints current_profile.
- profile delete <name> (alias rm): removes the def and stored credential;
  confirm-by-default, --yes skips, non-interactive without --yes refused;
  resets current to default.
- profile show [name]: shows backend URLs (marks inherited), pinned team, and
  authorization in priority order (--api-key > REPLY_API_KEY > stored) with no
  secrets. Defaults to the current profile.
- profile unset <name> <field>: clears authority | api_base | team-id (never
  name or credentials); idempotent; allowed on the built-in default.
- add/set: validate --authority/--api-base as http(s) URLs.
- Lock name-on-create behavior with a test.

profile.ts stays config-only/sync/store-agnostic; credential-touching logic
lives in commands/profile.ts as exported, offline-testable handlers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…EPLY-51325)

Identifies CLI traffic for telemetry (captured by Elastic APM/OTel as
user_agent.original) — including the API-key path, where there is no OAuth
client_id. Identification only; authorization stays scope-based.

- config.ts: add cli_version() (memoized package.json read, '0.0.0' fallback)
  and user_agent() = `${APP_NAME}-cli/<version>`, derived from APP_NAME to keep
  the single build identity.
- utils/client.ts: set User-Agent on the shared request() transport (covers
  both OAuth and API-key credentials; every current and future API call).
- index.ts: use cli_version() for --version, dropping the duplicate local
  read_version() and its now-unused fs/path imports.

Scope: API requests only. The OAuth /connect/token endpoint and the browser
authorize step are intentionally left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guidance (REPLY-51258, REPLY-51259)

Two intertwined pieces landing together (api is the workload vehicle for the
team-conflict guidance; both share request_raw and teams.ts):

REPLY-51258 — team discovery + conflict guidance:
- src/teams.ts: resolve_my_teams() (resilient — /whoami/team-users on 200, or the
  same teams[] from a TEAM_REQUIRED/TEAM_NOT_ACCESSIBLE 403), parse_teams()
  (dedupe, case-insensitive), team_error_guidance() (TEAM_REQUIRED/
  TEAM_NOT_ACCESSIBLE/USER_REQUIRED/USER_NOT_FOUND → tailored fix + team list).
- src/commands/team.ts: `team list` / `current` (pinned + effective-from-whoami,
  graceful) / `use <id>` (verified against your teams) / `clear`. Reads & writes
  the current profile's team_id — no new state.
- Guidance is surfaced ONLY on the workload `api` command; team/auth commands
  consume the team list silently.

REPLY-51259 — reply api raw passthrough:
- src/commands/api.ts: `reply api <path> [--method] [--body]` → prints {code,data}
  for any status; GET by default, POST when --body; --body takes inline JSON,
  @file, or - (stdin). Query lives in the path. Exits 1 on HTTP >= 400 and, on a
  team/user-resolution conflict, prints the tailored guidance to stderr.

Shared plumbing:
- src/utils/client.ts: request_raw() returns {status, data} for any HTTP status
  (never throws on status; still retries transient, throws only on network).
- src/commands/authed.ts: shared credential + team/acting-user header resolution.

Docs: index.ts help + README gain team and api sections. Tests fully offline
(mock fetch, temp config/store).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-only team fallback (REPLY-51257, REPLY-51258, REPLY-51259)

REPLY-51259 — reply api examples + URL model:
- Advertise the API reference (docs.reply.io/api-reference/introduction) and use
  only route-verified endpoints (/v3/whoami, /v3/sequences, /v3/contacts, …) in
  help + README.
- The request URL is now literally api_base + path — no /v3 special-casing.
  Removed ensure_v3 (path) and strip_api_version (base). The caller types the
  docs path (/v3/…); the CLI just joins.

REPLY-51257 — profile base convention:
- api_base is the HOST only (embedded default https://api.reply.io); the /v3
  version prefix lives in the request path, not the profile. Internal calls use
  literal /v3/whoami and /v3/whoami/team-users against the host base.

REPLY-51258 — team discovery on non-org accounts:
- /whoami/team-users is organization-only; on 403 workspace.organizationRequired
  fall back to the single team from whoami instead of erroring.

client: slash-safe base+path join (collapses //, inserts a missing /) shared by
request and request_raw.

Verified against dev: api /v3/whoami and api v3/whoami both 200; team list/current/
use/clear work for a personal account.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Y-51259)

--verbose (global flag, honored by the workload `api` command):
- Prints a curl -v-style request/response trace to stderr while {code,data}
  stays on stdout (pipe-safe). Authorization is redacted at the source
  (request_raw returns a pre-redacted request view; the raw token never leaves),
  and cookie response headers are redacted on print.
- request_raw now returns {status, data, response_headers, request}.

Help polish:
- Added worked Examples blocks to the non-trivial commands that lacked them
  (auth status; profile add/set/use/show/rename/delete/unset; team list/current/
  use), plus clarity notes (auth status runs offline; team list/current call the
  API; profile delete confirm-vs--yes). Trivial argument-less commands
  (auth logout, profile list/current, team clear) intentionally left without.

Verified on dev: `api /v3/whoami --verbose` shows the redacted trace + 200;
`... --verbose 2>/dev/null | jq` yields clean JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n (REPLY-51257, REPLY-51259)

- Environment-variables table was missing REPLY_TEAM_ID (referenced in Teams,
  supported by the CLI) — added the row.
- Raw API section documents --body/--pretty/exit codes but not the new
  --verbose; added an example line and a one-sentence note (stderr trace,
  credentials redacted, stdout stays JSON).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…REPLY-51259)

The intro implied only auth/identity works today with resource commands "on the
way", underselling that `reply api` already reaches the entire v3 API. Add that
to the opening pitch (one sentence, no restructuring); the Raw API section
already carries real runnable examples (/v3/whoami, /v3/sequences, /v3/contacts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olor-stable (REPLY-51258)

CI failed on `team list` — the `*` marker is pc.green('*'), and under FORCE_COLOR
(set in CI) the reset code lands between `*` and the id, so /\*\s*1045/ no longer
matched. Passed locally only because color was off. Strip ANSI in the test's
capture helper; verified the full suite green with FORCE_COLOR=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vigubikReply
vigubikReply enabled auto-merge July 22, 2026 22:46

@ArtemKosolap ArtemKosolap left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review — profile management, team context & raw api passthrough

Overview

Substantial, well-structured feature PR (+1886/−44, 18 files) extending the reply CLI with:

  • Profile management: profile rename/delete/show/unset with URL validation.
  • team command: list/current/use/clear, editing the current profile's team_id, with resilient team discovery (/v3/whoami/team-usersTEAM_REQUIRED body → single-whoami fallback).
  • reply api raw passthrough: literal api_base + path, method inference, @file/-/inline body, --verbose trace with credential redaction, {code,data} output.
  • Client identification: User-Agent: reply-cli/<version> on all requests.
  • A repositioning of api_base to be host-only (the /v3 prefix moves into request paths).

Verification: tsc --noEmit clean; vitest run231 passed / 3 skipped. New code is well-covered by focused unit tests with injected deps (stdin, confirm prompt, credential store).

Strengths

  • Clean separation of concerns: config-only mutations in profile.ts (rename/delete/unset_profile_def) vs. credential-store orchestration in the command layer. The handle_rename ordering (guard target → read → rename def → move credential, "orphan not destroy" on partial failure) is exactly the right instinct for a two-store operation without a transaction.
  • Secret hygiene is careful and tested: request_raw pre-redacts Authorization in its request view so the raw token never leaves the transport; --verbose additionally redacts cookies; profile show reuses describe_status (no refresh, no write) and has explicit "never leaks token/key" tests.
  • Consistent conventions: matches the codebase's snake_case locals, UsageError/RuntimeError with code/hint, stdout-data/stderr-status split, and injectable-deps testing style.
  • /v3 path migration is applied consistently across auth, teams, team, and api (no leftover un-prefixed endpoints).

Issues & risks (by severity)

1. api_base semantics change is a breaking change for existing custom profiles (Medium).
EMBEDDED.api_base moved from https://api.reply.io/v3https://api.reply.io, and all paths now carry /v3. Any user who followed the previous docs and stored api_base: "https://api.dev.reply.io/v3" will now hit …/v3/v3/whoami → 404, since join_url just concatenates. There is no migration or normalization, and the prod default is safe, but the PR body's "No breaking changes to existing auth commands" doesn't hold for hand-authored dev/staging profiles (the old help text and tests confirm /v3-in-api_base was the documented convention).

  • Suggestion: strip a trailing /v3 (or /v[0-9]+) from api_base in resolve_profile, or detect /v3/v3 in join_url, or at minimum add a CHANGELOG/upgrade note. profile show marking inherited-vs-explicit helps users self-diagnose, but silent 404s are a rough landing.

2. request_raw retries non-idempotent methods on transient statuses (Medium-Low).
request_raw retries [429,500,502,503,504] up to 3× regardless of method. reply api … --body @c.json is a POST, and the docs actively promote it. A POST that created a resource but returned 500 would be retried and can double-create. (429/502/503/504 are generally safe to retry; 500 after a mutation is the dangerous case.)

  • Suggestion: restrict body-bearing/POST/PATCH retries to clearly-safe statuses (e.g. 429/503), or only retry idempotent methods, or document that mutations may be retried. request had the same behavior but was only ever used for GET /whoami, so request_raw is where this first becomes a real exposure.

3. handle_team_current collapses all errors into "failed to retrieve" (Low).
The catch treats a 401/invalid-credential the same as an offline network blip. A logged-out user sees failed to retrieve (…) rather than a clear "not logged in." Acceptable for a best-effort read, but a 401 could be special-cased for a better hint.

4. read_all_stdin via fs.readFileSync(0) (Low).
Correctly wrapped in try/catch (→ '' → clean UsageError). One rough edge: reply api /x --body - run interactively (no pipe) on a TTY will block on the read rather than erroring fast. Minor; worth a note that - expects piped input.

Style / maintainability (nits)

  • Boilerplate duplication: Global_opts, read_globals, wants_json, print_opts are now copy-pasted across api.ts, team.ts, and profile.ts (with slightly different field sets). Consider a shared helper to prevent drift as more commands land.
  • Double validation of clearable fields: CLEARABLE_INPUT/map_clearable in the command layer and CLEARABLE_FIELDS in unset_profile_field. Fine as defense-in-depth, but the command-layer map already guarantees a valid Clearable_field, so the core check is effectively unreachable — worth a comment saying so.
  • Consistency: handle_team_current uses create_client().get('/v3/whoami') while the rest of the new code uses request_raw. Harmless, but two ways to reach /whoami.

Test coverage

Strong and idiomatic: slash-safe join, request_raw (2xx/4xx/retry/network), redaction, method inference, body parsing (@file/-/inline/invalid), rename/delete/unset edge cases (default-slot refusal, current-repointing, idempotent no-op), team resolution incl. the org-only fallback, and team_error_guidance per code. Gaps worth adding: a test asserting request_raw retry behavior for a POST (documents the intended idempotency decision from #2), and an integration-style test that an existing /v3 api_base config doesn't double up (would pin down #1).

Security

No concerns beyond the above. Credential redaction is enforced at the transport boundary and unit-tested against a literal secret; --user-id/--user-email remain flag-only; URL validation rejects non-http(s) schemes (file://, ftp:// tested). --verbose prints request headers + response headers only (not the response body) to stderr, keeping stdout pipe-clean.

Verdict: High-quality, ships clean (types + tests green). Recommend treating #1 as clarify-before-merge (add normalization/migration or correct the "no breaking changes" claim) and considering #2 given the raw-api POST guidance; the rest are good follow-ups.

🤖 Generated with Claude Code

@vigubikReply
vigubikReply merged commit ac4ff2e into main Jul 22, 2026
2 checks passed
@vigubikReply
vigubikReply deleted the dev/REPLY-51354-profile-management branch July 22, 2026 22:53
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