Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ jobs:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm format:check
- run: pnpm typecheck
- run: pnpm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dist/

# Local agent / tool artifacts
CLAUDE.md
.claude/
node-compile-cache/
update-check/
playwright_chromiumdev_profile-*/
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist/
docs/
plans/
pnpm-lock.yaml
src/docs/data.ts
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @auto.dev/sdk

SDK, MCP server and CLI over the auto.dev API. One client (`src/core/client.ts`), one endpoint table (`src/core/endpoints.ts`) that the SDK, the MCP tools and the CLI all read; an endpoint's `description` is the MCP tool description and the CLI help line.

## Types

- `src/core/types.generated.ts` is written by `pnpm generate` from `https://api.auto.dev/openapi`. Never edit it; change the converter in `scripts/openapi-to-ts.ts`.
- That spec documents one 200 per route. Anything the API answers besides that (the `/build` 207, `missing[]`) is hand-kept in `src/core/types.build.ts`, against the published contract in the docs.auto.dev repo, `src/v2.openapi.json`.

## Checks and release

- `pnpm format:check && pnpm typecheck && pnpm test` is CI. `pnpm test` also typechecks `test/**/*.test.ts`, so `expectTypeOf` assertions count.
- `pnpm release` bumps, tags, pushes and publishes. It runs `prepublishOnly` (docs + build) by itself. Needs `npm login` as a package owner.
88 changes: 45 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ Settings are stored in `~/.auto-dev/config.json`.

Auto.dev exposes its full vehicle data API as native [Model Context Protocol](https://modelcontextprotocol.io) tools. Two paths — pick one:

| Path | Best for | Install |
|------|----------|---------|
| **Remote** — `https://mcp.auto.dev/mcp` | Hosted clients (Claude Desktop, ChatGPT), zero setup | None — just add the URL |
| **Local stdio** — this SDK | CLI users, low-latency, offline-friendly | `npm i -g @auto.dev/sdk` |
| Path | Best for | Install |
| --------------------------------------- | ---------------------------------------------------- | ------------------------ |
| **Remote** — `https://mcp.auto.dev/mcp` | Hosted clients (Claude Desktop, ChatGPT), zero setup | None — just add the URL |
| **Local stdio** — this SDK | CLI users, low-latency, offline-friendly | `npm i -g @auto.dev/sdk` |

Both expose the same tools. Remote authenticates via OAuth (one click). Stdio uses `auto login` or `AUTODEV_API_KEY`.

Expand All @@ -88,13 +88,15 @@ Auto-configures Claude Code, Claude Desktop, and Cursor with the stdio MCP serve
#### Claude Code

**Remote:**

```bash
claude mcp add -s user -t http auto-dev https://mcp.auto.dev/mcp
```

Then run `/mcp` and select **Authenticate** for `auto-dev`.

**Stdio:**

```bash
npx @auto.dev/sdk mcp install
```
Expand Down Expand Up @@ -161,23 +163,23 @@ Requires VS Code 1.99+ with Copilot Agent mode.

### Available MCP Tools

| Tool | Description | Plan |
|------|-------------|------|
| `auto_decode` | Decode a VIN | Free |
| `auto_photos` | Vehicle photos | Free |
| `auto_listings` | Search listings with filters | Free |
| `auto_specs` | Vehicle specifications | Growth |
| `auto_build` | OEM build data | Growth |
| `auto_recalls` | Safety recalls | Growth |
| `auto_payments` | Payment calculations | Growth |
| `auto_apr` | Interest rates | Growth |
| `auto_tco` | Total cost of ownership | Growth |
| `auto_open_recalls` | Open/unresolved recalls | Scale |
| `auto_plate` | License plate to VIN | Scale |
| `auto_taxes` | Taxes and fees | Scale |
| `auto_docs` | Search bundled API documentation | — |
| `auto_config_set` | Set a config value (e.g. `raw: true`) | — |
| `auto_config_get` | Get a config value or list all settings | — |
| Tool | Description | Plan |
| ------------------- | --------------------------------------- | ------ |
| `auto_decode` | Decode a VIN | Free |
| `auto_photos` | Vehicle photos | Free |
| `auto_listings` | Search listings with filters | Free |
| `auto_specs` | Vehicle specifications | Growth |
| `auto_build` | OEM build data | Growth |
| `auto_recalls` | Safety recalls | Growth |
| `auto_payments` | Payment calculations | Growth |
| `auto_apr` | Interest rates | Growth |
| `auto_tco` | Total cost of ownership | Growth |
| `auto_open_recalls` | Open/unresolved recalls | Scale |
| `auto_plate` | License plate to VIN | Scale |
| `auto_taxes` | Taxes and fees | Scale |
| `auto_docs` | Search bundled API documentation | — |
| `auto_config_set` | Set a config value (e.g. `raw: true`) | — |
| `auto_config_get` | Get a config value or list all settings | — |

## SDK

Expand Down Expand Up @@ -231,37 +233,37 @@ All methods return:

### SDK Methods

| Method | Tier | Description |
|--------|------|-------------|
| `decode(vin)` | Free | Decode VIN — make, model, year, trim |
| `photos(vin)` | Free | Vehicle photos |
| `listings(filters?)` | Free | Search listings with filters |
| `specs(vin)` | Growth | Detailed vehicle specifications |
| `build(vin)` | Growth | OEM build and trim data; a 207 partial answer (`build: null`, `vehicle`, `catalog`, `reason`) when no record builds the VIN, see `isPartialBuild` |
| `recalls(vin)` | Growth | Safety recalls |
| `payments(vin, opts?)` | Growth | Monthly payment calculations |
| `apr(vin, opts?)` | Growth | Interest rates by credit profile |
| `tco(vin, opts?)` | Growth | Total cost of ownership |
| `openRecalls(vin)` | Scale | Open/unresolved recalls |
| `plate(state, plate)` | Scale | License plate to VIN |
| `taxes(vin, opts?)` | Scale | Taxes and fees |
| `usage()` | — | Account usage statistics |
| Method | Tier | Description |
| ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decode(vin)` | Free | Decode VIN — make, model, year, trim |
| `photos(vin)` | Free | Vehicle photos |
| `listings(filters?)` | Free | Search listings with filters |
| `specs(vin)` | Growth | Detailed vehicle specifications |
| `build(vin)` | Growth | OEM build and trim data; a 207 partial answer (`build: null`, `vehicle`, `catalog`, `reason`) when no record builds the VIN, see `isPartialBuild` |
| `recalls(vin)` | Growth | Safety recalls |
| `payments(vin, opts?)` | Growth | Monthly payment calculations |
| `apr(vin, opts?)` | Growth | Interest rates by credit profile |
| `tco(vin, opts?)` | Growth | Total cost of ownership |
| `openRecalls(vin)` | Scale | Open/unresolved recalls |
| `plate(state, plate)` | Scale | License plate to VIN |
| `taxes(vin, opts?)` | Scale | Taxes and fees |
| `usage()` | — | Account usage statistics |

### Subpath Exports

```typescript
import { AutoDev } from '@auto.dev/sdk' // SDK client
import { createMcpServer } from '@auto.dev/sdk/mcp' // MCP server
import { AutoDevClient } from '@auto.dev/sdk/core' // low-level client
import { AutoDev } from '@auto.dev/sdk' // SDK client
import { createMcpServer } from '@auto.dev/sdk/mcp' // MCP server
import { AutoDevClient } from '@auto.dev/sdk/core' // low-level client
```

## Plans & Pricing

| Plan | Monthly | Endpoints |
|------|---------|-----------|
| **Free** | $0, no card | Decode, Listings, Photos |
| Plan | Monthly | Endpoints |
| ---------- | ------------------- | ------------------------------------------- |
| **Free** | $0, no card | Decode, Listings, Photos |
| **Growth** | $299/mo + data fees | + Specs, Build, Recalls, Payments, APR, TCO |
| **Scale** | $599/mo + data fees | + Open Recalls, Plate-to-VIN, Taxes & Fees |
| **Scale** | $599/mo + data fees | + Open Recalls, Plate-to-VIN, Taxes & Fees |

Free includes 1,000 API calls/month as a hard cap — requests stop at the limit and there are no
data fees. Growth and Scale bill all API calls. See [auto.dev/pricing](https://auto.dev/pricing)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check .",
"release": "pnpm version patch && git push --follow-tags && pnpm publish --access public",
"prepublishOnly": "pnpm build:docs && pnpm build:docs-data && pnpm build"
},
"keywords": [
Expand Down
5 changes: 4 additions & 1 deletion scripts/build-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ function convertMdxToMarkdown(content: string): string {
// Single line version
md = md.replace(/<ClickableCodeBlock\s+code='([^']+)'\s+href='[^']+'\s+lang='([^']+)'\s*\/>/g, '\n```$2\n$1\n```\n')
// Multi-line version
md = md.replace(/<ClickableCodeBlock\s*\n\s*code='([^']+)'\s*\n\s*href='[^']+'\s*\n\s*lang='([^']+)'\s*\n\s*\/>/g, '\n```$2\n$1\n```\n')
md = md.replace(
/<ClickableCodeBlock\s*\n\s*code='([^']+)'\s*\n\s*href='[^']+'\s*\n\s*lang='([^']+)'\s*\n\s*\/>/g,
'\n```$2\n$1\n```\n',
)

// Remove any remaining JSX self-closing tags
md = md.replace(/<[A-Z]\w+[^>]*\/>/g, '')
Expand Down
8 changes: 6 additions & 2 deletions src/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function getValidToken(clientType: ClientType = 'cli'): Promise<str
const tokenData = await storage.getTokenData()
if (!tokenData?.accessToken) return null

const needsRefresh = tokenData.expiresAt && (tokenData.expiresAt - Date.now() < REFRESH_BUFFER_MS)
const needsRefresh = tokenData.expiresAt && tokenData.expiresAt - Date.now() < REFRESH_BUFFER_MS

if (!needsRefresh) return tokenData.accessToken

Expand All @@ -50,7 +50,11 @@ export async function getValidToken(clientType: ClientType = 'cli'): Promise<str
/**
* Save token data after device flow login.
*/
export async function saveTokenData(data: { access_token: string; refresh_token?: string; expires_in?: number }): Promise<void> {
export async function saveTokenData(data: {
access_token: string
refresh_token?: string
expires_in?: number
}): Promise<void> {
await storage.setTokenData({
accessToken: data.access_token,
refreshToken: data.refresh_token,
Expand Down
5 changes: 1 addition & 4 deletions src/auth/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { AutoDevError } from '../errors'
import type { ClientType } from '../core/types'

export async function resolveAuth(options?: { apiKey?: string; clientType?: ClientType }): Promise<string> {
const apiKey =
options?.apiKey ??
process.env.AUTODEV_API_KEY ??
await getValidToken(options?.clientType ?? 'cli')
const apiKey = options?.apiKey ?? process.env.AUTODEV_API_KEY ?? (await getValidToken(options?.clientType ?? 'cli'))

if (!apiKey) {
throw new AutoDevError(401, 'UNAUTHORIZED', 'No API key found', 'Set AUTODEV_API_KEY or run: auto login')
Expand Down
76 changes: 43 additions & 33 deletions src/cli/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { exec } from 'node:child_process'
import { platform } from 'node:os'
import { Command } from 'commander'
import { DEFAULT_AUTH_CONFIG } from '../auth/config'
import { authorizeDevice, clearCredentials, getValidToken, pollForTokens, saveApiKey, saveTokenData } from '../auth/oauth'
import {
authorizeDevice,
clearCredentials,
getValidToken,
pollForTokens,
saveApiKey,
saveTokenData,
} from '../auth/oauth'
import { buildAuthHeaders } from '../core/user-agent'
import { brand, formatError, formatSuccess, header, hint, kv, value } from './colors'
import { createSpinner } from './spinner'
Expand Down Expand Up @@ -30,15 +37,22 @@ export function buildLoginCommand(): Command {
const deviceCode = await authorizeDevice(DEFAULT_AUTH_CONFIG.clientId, CLI_HEADERS)
deviceSpinner.stop()

const verifyUrl = deviceCode.verification_uri_complete || `${deviceCode.verification_uri}?user_code=${deviceCode.user_code}`
const verifyUrl =
deviceCode.verification_uri_complete || `${deviceCode.verification_uri}?user_code=${deviceCode.user_code}`
console.log(`\n${brand('auto.dev')} ${hint('authentication')}`)
console.log(`\nOpening browser to: ${value(verifyUrl)}`)
console.log(`If it doesn't open, visit: ${value(deviceCode.verification_uri)}`)
console.log(`Enter code: ${brand(deviceCode.user_code)}\n`)
openBrowser(verifyUrl)
console.log(hint('Waiting for authorization...'))

const token = await pollForTokens(DEFAULT_AUTH_CONFIG.clientId, deviceCode.device_code, deviceCode.interval, deviceCode.expires_in, CLI_HEADERS)
const token = await pollForTokens(
DEFAULT_AUTH_CONFIG.clientId,
deviceCode.device_code,
deviceCode.interval,
deviceCode.expires_in,
CLI_HEADERS,
)

await saveTokenData(token)
console.log(formatSuccess('Login successful!'))
Expand All @@ -52,41 +66,37 @@ export function buildLoginCommand(): Command {
}

export function buildLogoutCommand(): Command {
const cmd = new Command('logout')
.description('Log out and clear stored credentials')
.action(async () => {
await clearCredentials('cli')
console.log(formatSuccess('Logged out. Credentials cleared.'))
})
const cmd = new Command('logout').description('Log out and clear stored credentials').action(async () => {
await clearCredentials('cli')
console.log(formatSuccess('Logged out. Credentials cleared.'))
})

return cmd
}

export function buildWhoamiCommand(): Command {
const cmd = new Command('whoami')
.description('Show current user and active org')
.action(async () => {
const token = await getValidToken('cli')
if (!token) {
console.log(formatError('Not logged in', 'Run: auto login'))
return
}
const { getUser } = await import('../auth/oauth')
const whoamiSpinner = createSpinner()
const { user } = await getUser(token, CLI_HEADERS)
whoamiSpinner.stop()
if (user) {
console.log(`\n${header('auto.dev')}`)
console.log()
console.log(kv('Email', user.email ?? 'unknown', 2))
console.log(kv('Name ', user.name ?? 'unknown', 2))
console.log(kv('ID ', user.id, 2))
if (user.organizationId) console.log(kv('Org ', user.organizationId, 2))
console.log()
} else {
console.log(hint('Authenticated (could not fetch user info)'))
}
})
const cmd = new Command('whoami').description('Show current user and active org').action(async () => {
const token = await getValidToken('cli')
if (!token) {
console.log(formatError('Not logged in', 'Run: auto login'))
return
}
const { getUser } = await import('../auth/oauth')
const whoamiSpinner = createSpinner()
const { user } = await getUser(token, CLI_HEADERS)
whoamiSpinner.stop()
if (user) {
console.log(`\n${header('auto.dev')}`)
console.log()
console.log(kv('Email', user.email ?? 'unknown', 2))
console.log(kv('Name ', user.name ?? 'unknown', 2))
console.log(kv('ID ', user.id, 2))
if (user.organizationId) console.log(kv('Org ', user.organizationId, 2))
console.log()
} else {
console.log(hint('Authenticated (could not fetch user info)'))
}
})

return cmd
}
34 changes: 17 additions & 17 deletions src/cli/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@ function colorsEnabled(): boolean {
}

function ansi(code: string) {
return (text: string) => colorsEnabled() ? `${code}${text}\x1b[0m` : text
return (text: string) => (colorsEnabled() ? `${code}${text}\x1b[0m` : text)
}

// Core palette — matches auto.dev website
export const purple = ansi('\x1b[35m') // brand accent
export const boldPurple = ansi('\x1b[1;35m') // headers, command names
export const cyan = ansi('\x1b[36m') // values, data
export const boldCyan = ansi('\x1b[1;36m') // important values
export const dim = ansi('\x1b[2m') // secondary info
export const bold = ansi('\x1b[1m') // emphasis
export const green = ansi('\x1b[32m') // success
export const boldGreen = ansi('\x1b[1;32m') // success headers
export const limeGreen = ansi('\x1b[33m') // links/URLs
export const brown = ansi('\x1b[38;5;180m') // string values
export const yellowNum = ansi('\x1b[92m') // numbers and booleans (bright green)
export const red = ansi('\x1b[31m') // errors
export const boldRed = ansi('\x1b[1;31m') // error headers
export const yellow = ansi('\x1b[33m') // warnings
export const white = ansi('\x1b[37m') // neutral
export const boldWhite = ansi('\x1b[1;37m') // labels
export const purple = ansi('\x1b[35m') // brand accent
export const boldPurple = ansi('\x1b[1;35m') // headers, command names
export const cyan = ansi('\x1b[36m') // values, data
export const boldCyan = ansi('\x1b[1;36m') // important values
export const dim = ansi('\x1b[2m') // secondary info
export const bold = ansi('\x1b[1m') // emphasis
export const green = ansi('\x1b[32m') // success
export const boldGreen = ansi('\x1b[1;32m') // success headers
export const limeGreen = ansi('\x1b[33m') // links/URLs
export const brown = ansi('\x1b[38;5;180m') // string values
export const yellowNum = ansi('\x1b[92m') // numbers and booleans (bright green)
export const red = ansi('\x1b[31m') // errors
export const boldRed = ansi('\x1b[1;31m') // error headers
export const yellow = ansi('\x1b[33m') // warnings
export const white = ansi('\x1b[37m') // neutral
export const boldWhite = ansi('\x1b[1;37m') // labels

// Semantic aliases
export const brand = boldPurple
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Command } from 'commander'
import { ENDPOINTS } from '../core/endpoints'
import { type CommandDef, makeCommand } from './factory'

// prettier-ignore
const commands: CommandDef[] = [
// Simple VIN commands
{ name: 'decode', description: 'Decode a VIN — returns make, model, year, trim, engine, drivetrain' },
Expand Down
3 changes: 1 addition & 2 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { loadConfig, saveConfig } from '../core/config'
import { brand, formatSuccess, hint, label, value } from './colors'

export function buildConfigCommand(): Command {
const config = new Command('config')
.description('Manage auto.dev CLI configuration')
const config = new Command('config').description('Manage auto.dev CLI configuration')

config
.command('set')
Expand Down
Loading
Loading