agentleFS
dougborgdougborg/frontapp-openapi-client/CLAUDE.md

frontapp-openapi-client

CLAUDE.md0 starsChanged 7 months agoMITArchived repositoryView on GitHub

Guidance for Claude Code working with this repository. NEVER CANCEL long-running commands - they may appear to hang but are processing. For routine factual questions about the generated API surface, read docs/api-facts.yaml before grepping the api/ tree. It is generated by scripts/generate_api_facts.py and validated in CI by uv run poe facts-check, so it cannot drift from the generated code. The file pre-computes the most common queries: - "Does list_X return field_results, a raw array, or a single object?" → summary.list_endpoints_with_field_results…

  • Reads credentials
  • Installs packages
  • Commits and pushes

The file

Show its rules and text

We split this CLAUDE.md into the rules it gives an agent

14 rules. About 0 are common, 6 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    docs/api-facts.yaml: before grepping the api/ tree.

    line 33
  2. Very specific:

    When the answer isn't in the facts file (rare — check summary.* first), fall back to direct inspection of …

    line 50
  3. Very specific:

    Always run the appropriate validation tier before considering work complete.

    line 72
  4. Distinctive:

    Fix a tricky bug?: Add the root cause to Known Pitfalls.

    line 82
  5. Distinctive:

    Discover a new anti-pattern?: Add it to Anti-Patterns to Avoid.

    line 83
  6. Distinctive:

    Find a command or workflow that's missing?: Add it to Essential Commands or Detailed Documentation.

    line 84
  7. Distinctive:

    Hit a confusing API behavior?: Document it so the next session doesn't waste time rediscovering it.

    line 86
  8. Distinctive:

    This is a living document: When you discover a new recurring mistake, surprising API behavior, or gotcha during development, add it here so …

    line 94
  9. Very specific:

    If LSP <op> returns No LSP server available for file type: .py (or .ts), the plugin isn't enabled or the binary isn't reachable — verify …

    line 182
  10. Very specific:

    If the question is "where is the string foo in this repo?" — Grep.

    line 222
  11. Very specific:

    If hover returns Unknown for a _project-external_ import (e.g., pydantic, fastmcp), the langserver is stale — flag to the user that Claude …

    line 233
  12. Very specific:

    Use ! for breaking changes: feat(client)!: breaking change

    line 299
  13. Very specific:

    Use the helper utilities in frontapp_public_api_client/utils.py for consistent response handling:

    line 303
  14. Distinctive:

    Discover on-demand: - read these when working on specific areas:

    line 413

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

Guidance for Claude Code working with this repository.

Quick Start

uv sync --all-extras         # Install dependencies
uv run pre-commit install    # Setup hooks
cp .env.example .env         # Add FRONTAPP_API_KEY

Essential Commands

CommandTimeWhen to Use
uv run poe quick-check~5-10sDuring development
uv run poe agent-check~8-12sBefore committing
uv run poe check~30sBefore opening PR
uv run poe full-check~40sBefore requesting review
uv run poe fix~5sAuto-fix lint issues
uv run poe test~16sRun tests (4 workers)
uv run python scripts/vendor_spec.py~3sRefresh spec from upstream Front repo
uv run poe regenerate-client~1-2minRegenerate Python client from spec
uv run poe facts~3sRegenerate docs/api-facts.yaml
pnpm --filter frontapp-client generate~5sRegenerate TypeScript client

NEVER CANCEL long-running commands - they may appear to hang but are processing.

API Facts Index — Read this first

For routine factual questions about the generated API surface, read docs/api-facts.yaml before grepping the api/ tree. It is generated by scripts/generate_api_facts.py and validated in CI by uv run poe facts-check, so it cannot drift from the generated code.

The file pre-computes the most common queries:

  • "Does list_X return field_results, a raw array, or a single object?" → summary.list_endpoints_with_field_results / summary.list_endpoints_returning_raw_array
  • "Which endpoints are mutations (need two-step confirm)?" → summary.mutations
  • "Which generated module names have name quirks (_a_ infix, removes_ prefix)?" → summary.module_name_quirks
  • "Is there already a helper / domain projection for resource X?" → tags.<resource>.helper.built / tags.<resource>.domain.built
  • "What's the response model class name for list_X?" → tags.<resource>.endpoints[].response_type

When the answer isn't in the facts file (rare — check summary.* first), fall back to direct inspection of frontapp_public_api_client/api/<tag>/. Always prefer the facts file for context efficiency: one Read returns answers that would otherwise require dozens of file inspections.

CRITICAL: Zero Tolerance for Ignoring Errors

FIX ALL ISSUES. NO EXCEPTIONS.

  • NO noqa, type: ignore, exclusions, or skips
  • NO "pre-existing issues" or "unrelated to my changes" excuses
  • NO --no-verify commits
  • ASK for help if blocked - don't work around errors

Proper fixes:

  • Too many parameters? → Create a dataclass
  • Name shadows built-in? → Rename it
  • Circular import? → Use TYPE_CHECKING block

Verify Your Work

Always run the appropriate validation tier before considering work complete. See the Essential Commands table above - use quick-check during development, agent-check before committing, and check before opening a PR. Don't trust that code works just because it looks right.

Continuous Improvement

This file is meant to evolve. Update it when you learn something that would help future sessions:

  • Fix a tricky bug? Add the root cause to Known Pitfalls.
  • Discover a new anti-pattern? Add it to Anti-Patterns to Avoid.
  • Find a command or workflow that's missing? Add it to Essential Commands or Detailed Documentation.
  • Hit a confusing API behavior? Document it so the next session doesn't waste time rediscovering it.

The same applies to all project documentation - if instructions are wrong, incomplete, or misleading, fix them as part of your current work rather than leaving them for later.

Known Pitfalls

This is a living document. When you discover a new recurring mistake, surprising API behavior, or gotcha during development, add it here so future sessions don't repeat it.

Common mistakes to avoid:

  • Editing generated files — api/**/*.py, models/**/*.py, and client.py are generated. Run uv run poe regenerate-client instead of editing them directly.
  • Editing the spec in-place — docs/frontapp-openapi.yaml is vendored and sanitized. To pull upstream changes run uv run python scripts/vendor_spec.py, not a direct edit. If upstream adds a quirk that breaks codegen, patch the sanitization rules in scripts/vendor_spec.py rather than hand-editing the YAML.
  • UNSET vs None confusion — attrs model fields that are unset use a sentinel value, not None. Use unwrap_unset(field, default) from frontapp_public_api_client.domain.converters, not isinstance or hasattr checks.
  • Manual status code checks — Don't write if response.status_code == 200. Use unwrap_as(), unwrap_data(), or is_success() from frontapp_public_api_client.utils.
  • Pagination: cursor tokens — Front uses cursor-based pagination. List responses include _pagination.next as a full URL; pass the page_token query parameter back to the list endpoint to fetch the next page. limit caps page size (max 100, default 50). No total count is returned.
  • Front search syntax (q= parameter) — Conversation list/search endpoints accept Front's query DSL: status:open tag:urgent assignee:me before:2024-12-31. Not every filter is a separate query param — they're all in the q= string.
  • Generated list-response field names — openapi-python-client prefixes fields with field_ when they'd otherwise collide with Python (leading-underscore names). So _results becomes field_results, _pagination becomes field_pagination. Always use getattr(parsed, "field_results", None) or [] when unwrapping list responses. Which list endpoints use field_results vs return raw arrays is enumerated in docs/api-facts.yaml under summary.list_endpoints_with_field_results and summary.list_endpoints_returning_raw_array. Front's spec is currently all-field_results (the raw-array set is empty); the facts file is the authoritative source if that ever changes.
  • Front error responses have no single schema — Front doesn't define a canonical ErrorResponse type; each endpoint models errors inline. utils.unwrap() dispatches on HTTP status code, not on parsed-type isinstance checks. Don't try to import an ErrorResponse model from models/; there isn't one.
  • None-to-UNSET conversion — When building attrs API request models from optional fields, use to_unset(value) from frontapp_public_api_client.domain.converters instead of value if value is not None else UNSET.
  • Help resource drift — frontapp_mcp_server/.../resources/help.py contains hardcoded tool documentation. When adding or modifying tool parameters, also update the help resource content to stay in sync.
  • typecheck skips generated code deliberately — ty check only runs over hand-written paths (helpers/, domain/, frontapp_client.py, utils.py). The generated api/ and models/ have ~9 false positives from openapi-python-client's from_dict patterns that ty (pre-1.0) doesn't narrow correctly. When adding new hand-written modules, extend the typecheck task path list in pyproject.toml to include them. Revisit the exclusion when ty hits 1.0 — tracked in issue #8.
  • git push -u origin <branch> can land on main — when a feature branch is created via git checkout -b <name> origin/main, its upstream is origin/main, so a bare git push -u origin <name> resolves to main and bypasses PR review. The scripts/pre-push-guard.sh pre-commit hook blocks this. The right form for first-time pushes is git push -u origin HEAD:refs/heads/<branch-name>. --no-verify is forbidden — fix the push command instead.
  • uv.lock drift you didn't cause — when you start a branch off an older main and a sibling-package release (e.g. mcp-vX.Y.Z) lands on main while you work, your uv.lock will show modifications you didn't make once you sync or run uv sync. Stage and commit it (git add uv.lock) bundled with your other work — don't try to revert it. The lockfile is authoritative and CI re-resolves from pyproject.toml anyway, so a stale lockfile causes spurious CI diffs.
  • Generator/script edits without committing the regen — when you edit scripts/vendor_spec.py, scripts/regenerate_client.py, or scripts/generate_api_facts.py, you must also run the corresponding regenerate task and commit the regenerated output. block-generated-edits.sh blocks direct edits to generated files but doesn't catch this case (the generator changes look unrelated). CI will fail on facts-check or generated-client diff if you forget.
  • Live Front API data must not enter the repo — when testing against a real Front workspace, never commit raw response fixtures, names, emails, or conversation bodies from real customers. Sanitize to placeholders (cnv_TEST, customer@example.com, generic message bodies) before saving as a test fixture or example. Front workspaces contain customer PII and the published client is a public package.

Using the LSP tool

Both Python (pyright) and TypeScript (typescript-language-server) LSPs are wired through Claude Code plugins (pyright-lsp@claude-plugins-official and typescript-lsp@claude-plugins-official, enabled in .claude/settings.json → enabledPlugins). The plugins only register the language servers — the binaries themselves must be installed and on PATH:

uv tool install pyright                              # pyright + pyright-langserver → ~/.local/bin
npm install --prefix ~/.local typescript-language-server typescript
# then symlink both binaries into a directory already on PATH (e.g. ~/bin/)

If LSP <op> returns No LSP server available for file type: .py (or .ts), the plugin isn't enabled or the binary isn't reachable — verify which pyright-langserver / which typescript-language-server and restart Claude Code after enabling the plugin.

Prefer LSP operations over Read + Grep for type and call-graph questions — they are faster, more accurate, and cross-reference the real type system (including third-party libraries in .venv).

When you need to…Use
Understand a symbol's type/signature/docstringLSP hover
Jump to where a function/class is defined (project code)LSP goToDefinition
Find every caller of a function before changing its signatureLSP findReferences
List all symbols in a file (skim without reading all of it)LSP documentSymbol
Trace callers of a function (who calls X?)LSP incomingCalls
Trace callees of a function (what does X call?)LSP outgoingCalls

Project root must match workspace root. The pyright config lives at pyrightconfig.json (relative venvPath: "."). CLI pyright uses it automatically (npx pyright or uv run pyright); the langserver reads it on startup.

Reach for LSP first in these situations

It's easy to fall back on Grep reflexively — these are the situations where doing so is genuinely worse than the LSP:

  • Before extracting a helper, renaming a symbol, or changing a function signature — LSP findReferences returns the precise call graph. Grepping the symbol name misses re-exports, dynamic dispatch, and string-only matches in unrelated docs; LSP doesn't.
  • Before designing a class that mirrors an existing one — LSP documentSymbol on the canonical file (e.g. helpers/contacts.py) gives you the full method list in one hop; Read walks the whole file.
  • When debugging a 'X' object has no attribute 'y' style error — LSP hover on the access tells you whether the type is Unset, None, a domain model, or a third-party class. Grepping for the field misses the type relationship.
  • Refactors that touch >10 files — get the call graph from LSP findReferences, then mechanically rewrite. Lesson from PR #71 (confirm_or_preview rollout): a grep-driven refactor missed several formatting variants the regex didn't catch and needed a hand-edit pass to clean up.

If the question is "where is the string foo in this repo?" — Grep. If the question is "what calls / extends / overrides / shadows foo?" — LSP.

LSP known limitations

  • workspaceSymbol returns nothing in this tooling — pyright only indexes open files, and the LSP tool doesn't expose the query parameter. Use Grep for project-wide symbol search instead (e.g., Grep "def format_md_table").
  • goToImplementation is not implemented by pyright — use goToDefinition instead.
  • goToDefinition on external-library imports returns "no definition found" — use hover instead, which gives you the class signature + docstring.
  • If hover returns Unknown for a project-external import (e.g., pydantic, fastmcp), the langserver is stale — flag to the user that Claude Code needs a restart to re-read pyrightconfig.json. All project-internal imports should always resolve.

Architecture Overview

Monorepo with 3 packages:

  • frontapp_public_api_client/ — Python client with transport-layer resilience and hand-written domain helpers (client.conversations.… + more as verticals ship)
  • frontapp_mcp_server/ — FastMCP server exposing Front operations as AI tools
  • packages/frontapp-client/ — TypeScript client (hey-api generated + transport middleware for retries and pagination)

API scope: Front's Core API — 139 paths, 233 generated operations across conversations, messages, contacts, teammates, tags, inboxes, channels, rules, analytics, etc. Bearer auth on every endpoint.

Key patterns:

  • Transport-layer resilience — retries, rate-limit awareness live in the httpx transport stack, not on individual methods. Every endpoint inherits them automatically via FrontappClient.
  • Two-layer API surface — direct from frontapp_public_api_client.api.<tag> import <operation> access to every generated endpoint, plus hand-written client.<resource>.… facades that return Pydantic domain models for the most-used resources.
  • Two-step confirm on MCP mutations — tools like update_conversation and create_draft_reply take a confirm: bool = False parameter; confirm=False returns a preview, confirm=True both executes and elicits explicit user approval via ctx.elicit. Outbound replies always go through the drafts vertical (create_draft_reply, create_draft_on_channel, etc.) — there is no direct-send tool. See ADR-0016 → "Drafts-first outbound".
  • Spec sanitization at vendor time — Front's upstream OpenAPI spec has a few quirks that break openapi-python-client (example values on required path params, allOf-inherited defaults, binary */* attachment downloads). scripts/vendor_spec.py downloads and patches before writing docs/frontapp-openapi.yaml.

File Rules

CategoryFilesAction
EDITABLEfrontapp_public_api_client/{frontapp_client.py, helpers/**, domain/**, utils.py}, frontapp_mcp_server/src/**, packages/frontapp-client/src/**, tests/**, frontapp_mcp_server/tests/**, scripts/**, docs/** (except the YAMLs below), .claude/**, .github/**Can modify
GENERATED — DO NOT EDITfrontapp_public_api_client/api/**/*.py, frontapp_public_api_client/models/**/*.py, frontapp_public_api_client/client.py, frontapp_public_api_client/client_types.py, frontapp_public_api_client/errors.py, docs/frontapp-openapi.yaml (vendored from upstream), docs/api-facts.yaml (generated from the api/ tree), packages/frontapp-client/src/generated/** (TS)DO NOT EDIT

The .claude/hooks/block-generated-edits.sh PreToolUse hook enforces the generated-file boundary; an attempted Edit/Write/MultiEdit on any file in the "DO NOT EDIT" row exits 2 with regen-flow guidance. The canonical glob list lives in .claude/hooks/_lib/generated-paths.sh and is shared with format-on-edit.sh — adding a new generated path means changing it once.

RegenerateCommandTime
Python clientuv run poe regenerate-client~1-2min
Vendored OpenAPI specuv run python scripts/vendor_spec.py~3s
API facts indexuv run poe facts~3s
Full pipeline (all three)uv run poe regenerate-all~1-2min

Commit Standards

feat(client): add feature    # Client MINOR release
fix(mcp): fix bug            # MCP PATCH release
docs: update README          # No release

Use ! for breaking changes: feat(client)!: breaking change

API Response Handling Best Practices

Use the helper utilities in frontapp_public_api_client/utils.py for consistent response handling:

Response Unwrapping

from frontapp_public_api_client.utils import unwrap, unwrap_as, unwrap_data, is_success
from frontapp_public_api_client.domain.converters import unwrap_unset
from frontapp_public_api_client.models.conversation_response import ConversationResponse

# Single-object responses (200 OK with parsed model)
conv = unwrap_as(response, ConversationResponse)  # Type-safe with validation

# Paginated list responses — Front wraps results in _results (→ field_results)
parsed = unwrap(response)
results = getattr(parsed, "field_results", None) or []

# For success-only responses (202 Accepted, 204 No Content)
if is_success(response):
    # Handle success case

# For attrs model fields that may be UNSET
tags = unwrap_unset(conv.tags, [])  # Returns [] if UNSET

When to Use Each Pattern

ScenarioPatternExample
Single object (200)unwrap_as(response, Type)Get/update by id
Paginated list (200)unwrap(response) + getattr(…, "field_results")Most list endpoints (see docs/api-facts.yaml summary block)
Raw-array list (200)unwrap(response) (parsed is a list directly)A handful of conversation sub-resource lists; see facts file
Create (201)is_success(response)POST with no body
Accepted (202) / Deleted (204)is_success(response)Replies, archives
attrs UNSET fieldunwrap_unset(field, default)Optional API fields

For the authoritative per-endpoint shape (which list returns field_results vs a raw array vs a single object), consult docs/api-facts.yaml — summary.list_endpoints_with_field_results, summary.list_endpoints_returning_raw_array, and tags.<resource>.endpoints[].list_shape are CI-validated and cannot drift.

Anti-Patterns to Avoid

# ❌ DON'T: Manual status code checks
if response.status_code == 200:
    result = response.parsed
# ✅ DO: Use helpers
result = unwrap_as(response, ExpectedType)

# ❌ DON'T: isinstance with UNSET
if not isinstance(value, type(UNSET)):
    use(value)
# ✅ DO: Use unwrap_unset
use(unwrap_unset(value, default))

# ❌ DON'T: hasattr for attrs-defined fields
if hasattr(conv, "status"):
    status = conv.status
# ✅ DO: Use unwrap_unset (attrs fields always exist, may be UNSET)
status = unwrap_unset(conv.status, None)

Exception Hierarchy

unwrap() and unwrap_as() raise typed exceptions:

  • AuthenticationError - 401 Unauthorized
  • ValidationError - 422 Unprocessable Entity
  • RateLimitError - 429 Too Many Requests
  • ServerError - 5xx server errors
  • APIError - Other errors (400, 403, 404, etc.)

Claude Code Commands

Project slash commands in .claude/commands/ and skills in .claude/skills/:

CommandPurpose
/new-verticalScaffold a new resource vertical (helper + domain + MCP tools + tests + docs)
/vendor-and-regenRefresh the vendored Front OpenAPI spec from upstream and regenerate the client safely
/review-prAddress PR review comments — fetch, fix, validate, push, reply
/open-prOpen PR with self-review, CI wait, feedback
/reviewStructured code review of current branch
/techdebtScan for tech debt and anti-patterns
/write-testsWrite comprehensive tests for target code
/generate-docsGenerate or update documentation
/verifySkeptically validate implementation quality
/pre-commitQuick pre-flight check before committing

Claude Code Agents

Sub-agents in .claude/agents/ that can be spawned for delegated work during complex tasks:

AgentPurpose
vertical-plannerPlan a new resource vertical (helper + domain + MCP tools) BEFORE any code is written. Sonnet, Read+Bash+Grep+Glob.
domain-advisorRead-only oracle for Front API factual questions (response shape, module names, helper status). Haiku, Read+Grep+Glob (no Bash).
code-modernizerSimplify code using repo-specific patterns (UNSET helpers, response unwrap, etc.)
pr-preparerValidate branch readiness for pull request
spec-auditorAudit local OpenAPI spec against upstream for drift

The vertical-planner and domain-advisor both consult docs/api-facts.yaml as their primary knowledge source. When you have a one-off factual question about the API surface mid-task, prefer domain-advisor over grep+ls.

Detailed Documentation

Discover on-demand - read these when working on specific areas:

TopicFile
Agent workflowsAGENT_WORKFLOW.md
Client guidefrontapp_public_api_client/docs/guide.md
MCP docsfrontapp_mcp_server/docs/README.md
TypeScript clientpackages/frontapp-client/README.md
ADRsdocs/adr/README.md

Validation tiers, commit standards, and file rules are documented inline in this file — see "Essential Commands", "Commit Standards", and "File Rules" above.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.