@meterapp/car-image-sdk 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -16
- package/dist/client.d.ts +54 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +171 -6
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +283 -131
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +343 -44
- package/dist/mcp/index.js.map +1 -1
- package/dist/params.d.ts +24 -2
- package/dist/params.d.ts.map +1 -1
- package/dist/params.js +92 -25
- package/dist/params.js.map +1 -1
- package/dist/types.d.ts +302 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @meterapp/car-image-sdk
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for the **Car Image API** by Meter — studio-quality, transparent-background renders of any vehicle in the open [`@meterapp/vehicle-db`](https://github.com/MeterApp/vehicle-db) catalog (1,599 makes, 44,254 models, model years 1990–2027).
|
|
3
|
+
TypeScript SDK for the **Car Image API** by Meter — studio-quality, transparent-background renders of any vehicle in the open [`@meterapp/vehicle-db`](https://github.com/MeterApp/vehicle-db) catalog (1,599 makes, 44,254 models, model years 1990–2027), in any paint color; plus stable vehicle ids, free VIN decoding and textured 3D models.
|
|
4
4
|
|
|
5
5
|
- Zero runtime dependencies. Works in Node 20+, Vercel/Cloudflare edge runtimes, Deno, Bun and browsers.
|
|
6
6
|
- Typed parameters and responses for every endpoint.
|
|
7
7
|
- `CarImageError` carries `status`, `title`, `detail`, `requestId` and the raw RFC 9457 problem.
|
|
8
8
|
- Automatic retries with full jitter on `429`/`503` (honoring `Retry-After`); never retries `402`.
|
|
9
|
-
- Signed-URL creation
|
|
9
|
+
- Signed-URL and 3D-model creation carry an `Idempotency-Key`, so a retried `POST` replays instead of billing twice.
|
|
10
10
|
- Shared MCP tool definitions (`@meterapp/car-image-sdk/mcp`) used by both the stdio and the remote MCP servers.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
@@ -15,7 +15,7 @@ npm install @meterapp/car-image-sdk
|
|
|
15
15
|
|
|
16
16
|
## Pricing in one line
|
|
17
17
|
|
|
18
|
-
Every delivered image costs **1 credit**, cold or CDN hit. **$1 = 1,000 credits**, every account starts with **100 free credits**, there is no subscription. Signed delivery URLs cost 1 credit when created; loading them is free.
|
|
18
|
+
Every delivered image costs **1 credit**, cold or CDN hit. **$1 = 1,000 credits**, every account starts with **100 free credits**, there is no subscription. Signed delivery URLs cost 1 credit when created; loading them is free. A 3D model costs **1,000 credits** ($1.00) when requested; VIN decoding, catalog lookups, polling and downloads are free.
|
|
19
19
|
|
|
20
20
|
Get a key at <https://carimage.dev/dashboard?ref=npm-sdk> or run `npx @meterapp/car-image login`.
|
|
21
21
|
|
|
@@ -32,7 +32,7 @@ const image = await client.getImage({
|
|
|
32
32
|
model: "911",
|
|
33
33
|
year: 2024,
|
|
34
34
|
view: "front-3-4", // front | front-3-4 | side | side-right | rear | rear-3-4
|
|
35
|
-
color: "red", // 15 presets
|
|
35
|
+
color: "red", // 15 presets (white black gray silver blue red green brown beige tan orange yellow gold burgundy purple) or any hex: "#1a2b3c"
|
|
36
36
|
size: "medium", // thumb=256 small=512 medium=768 large=1024 — or width/height, 1–1024 px each
|
|
37
37
|
format: "png", // png | webp | jpg | auto (auto negotiates WebP or PNG from the Accept header)
|
|
38
38
|
});
|
|
@@ -43,9 +43,71 @@ console.log(image.source, image.creditsCharged, image.creditsRemaining, image.re
|
|
|
43
43
|
|
|
44
44
|
`apiKey` and `baseUrl` fall back to `CAR_IMAGE_API_KEY` / `CAR_IMAGE_API_URL` when running on a server.
|
|
45
45
|
|
|
46
|
+
## Any paint color
|
|
47
|
+
|
|
48
|
+
`color` is one of the fifteen presets or any hex paint — `#1a2b3c`, `1a2b3c`, `#abc` or `abc`. Every image call takes it (`getImage`, `getImageUrl`, `createImageUrls`, `feedback`) and so does `create3dModel`. The SDK spells a hex bare in query strings (`color=1a2b3c`) and sends it as written in JSON bodies; responses echo it as `#1a2b3c`. A hex that matches a preset's own swatch is that preset. The `Color` type keeps the preset names in autocomplete while accepting any string; `isColor(value)` tells the two apart from anything else, and `COLORS` is still the list of presets.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await client.getImage({ make: "Porsche", model: "911", year: 2024, color: "#1a2b3c" });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Stable vehicle ids
|
|
55
|
+
|
|
56
|
+
Every catalog vehicle (one make, model and year) has a stable id such as `veh_395yw8tn73ff8`. It never changes across catalog releases, so it is what to store instead of three strings. Every catalog response carries them (`searchVehicles` hits get `id` for the year the query named and `ids` for every year; `vehicles({ year, makeId })` models get `id`; `resolve()` params get `vehicle_id`; `decodeVin()` names one), and every image or 3D call accepts `{ vehicle }` in place of `{ make, model, year }`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const { data } = await client.searchVehicles("2024 porsche 911");
|
|
60
|
+
const id = data.results[0].id; // "veh_395yw8tn73ff8"
|
|
61
|
+
await client.getImage({ vehicle: id, view: "side" }); // same image as make/model/year
|
|
62
|
+
await client.vehicle(id); // { make, model, year, years, image_path, images } — public, no key
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`ImageParams` is `{ make, model, year } | { vehicle }` plus the options, so TypeScript refuses both at once; at runtime whichever you pass is sent and the server answers `400` to both. Responses echo the id as `vehicle.vehicle_id` (null when the catalog cannot name one).
|
|
66
|
+
|
|
67
|
+
## VIN decoding
|
|
68
|
+
|
|
69
|
+
`decodeVin(vin, { year? })` decodes a full 17-character VIN, or a partial one of at least 5 characters with `*` for unknown positions, with NHTSA vPIC data — free, key required. `data.vehicle` is the catalog vehicle it maps to, with the id that renders it, or null when the catalog does not carry the car.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const { data } = await client.decodeVin("1HGCM82633A004352");
|
|
73
|
+
data.year, data.make, data.model, data.trim; // 2003, "Honda", "Accord", "EX-V6"
|
|
74
|
+
data.engine, data.transmission, data.plant; // typed sub-objects; unknown values are null
|
|
75
|
+
data.attributes; // every decoded vPIC variable, verbatim
|
|
76
|
+
if (data.vehicle) await client.getImage({ vehicle: data.vehicle.id });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`valid`, `errors` and `suggested_vin` carry the decoder's diagnostics (a bad check digit, a model-year mismatch with the `year` hint). A VIN NHTSA has no record of is a `404`; when neither decoder answers it is a `502`.
|
|
80
|
+
|
|
81
|
+
## 3D models
|
|
82
|
+
|
|
83
|
+
`create3dModel` builds a textured 3D model — GLB, USDZ, FBX and a PNG thumbnail — of any catalog vehicle in any paint color, from the same renders the images use. It costs 1,000 credits at creation whether the model is cached, in progress or new (a failed model is refunded); polling and downloads are free. A model takes one to five minutes.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const { data: model, billing } = await client.create3dModel(
|
|
87
|
+
{ make: "Toyota", model: "Camry", year: 2025 }, // or { vehicle: "veh_…" }
|
|
88
|
+
{ color: "#1a2b3c", webhookUrl: "https://example.com/hooks/3d", webhookSecret: process.env.HOOK_SECRET }
|
|
89
|
+
);
|
|
90
|
+
model.status; // "queued" | "processing" | "ready" (cached) | "failed"
|
|
91
|
+
|
|
92
|
+
const ready = await client.wait3dModel(model.id, { // polls GET /api/v1/3d/{id}
|
|
93
|
+
intervalMs: 5_000, // default 5 s
|
|
94
|
+
timeoutMs: 600_000, // default 10 min; throws a CarImageError with status 408
|
|
95
|
+
onProgress: (state) => console.log(state.status, state.progress, state.stage, state.estimated_seconds_remaining),
|
|
96
|
+
});
|
|
97
|
+
if (ready.status === "ready") {
|
|
98
|
+
const glb = await client.download3dModel(ready.id, "glb"); // { bytes, contentType } — also "usdz" | "fbx" | "thumbnail"
|
|
99
|
+
await writeFile("camry.glb", glb.bytes);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await client.get3dModel(model.id); // one poll
|
|
103
|
+
await client.list3dModels({ limit: 20 }); // your recent requests + pricing.credits_per_model
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`ready.files.<kind>.url` are API URLs that answer `302` to a signed download valid for an hour; `download3dModel` follows that redirect itself and never sends the API key to the storage host (`fetch` only attaches it to the client's own origin). With `webhookUrl` (https, public host) the server POSTs `{ event: "3d_model.ready" | "3d_model.failed", data, sent_at }` once the model settles, signed as `X-CarImage-Signature: sha256=<hex HMAC of the raw body>` when `webhookSecret` is set. `create3dModel` sends an `Idempotency-Key` like `createImageUrls` (pass `idempotencyKey` to make retries across processes safe). A `503` means 3D models are switched off or at the day's capacity; nothing is charged and `retryAfterSeconds` says when to try again.
|
|
107
|
+
|
|
46
108
|
## Sizing, backgrounds and trimming
|
|
47
109
|
|
|
48
|
-
Six views,
|
|
110
|
+
Six views, any paint color, PNG/WebP/JPG up to 1024 px — delivered at whatever box your layout needs.
|
|
49
111
|
|
|
50
112
|
```ts
|
|
51
113
|
await client.getImage({
|
|
@@ -61,6 +123,7 @@ await client.getImage({
|
|
|
61
123
|
|
|
62
124
|
| Parameter | Values | What it does |
|
|
63
125
|
| --- | --- | --- |
|
|
126
|
+
| `color` | one of `COLORS` · hex as `#rrggbb`, `rrggbb`, `#rgb` or `rgb` | The paint. Defaults to `silver`; echoed back as the preset name or `#rrggbb`. |
|
|
64
127
|
| `size` | `thumb` 256 · `small` 512 · `medium` 768 · `large` 1024 | Square presets; shorthand for `width`. |
|
|
65
128
|
| `width`, `height` | 1–1024 px each | One dimension keeps the aspect ratio. Both together give exactly `width`×`height` (see `fit`). Nothing is upscaled past 1024. |
|
|
66
129
|
| `fit` | `contain` (default) · `cover` · `inside` | Only matters with both dimensions. `contain` returns exactly the box with the whole car and transparent (or `background`) padding; `cover` fills the box and centre-crops; `inside` keeps the car within the box and may return a smaller image — the old behaviour. |
|
|
@@ -69,7 +132,7 @@ await client.getImage({
|
|
|
69
132
|
| `padding` | 0–50 | Margin around a trimmed car, as a percentage of its longer side. Only with `trim`. |
|
|
70
133
|
| `format` | `png` (default) · `webp` · `jpg` · `auto` | `auto` picks WebP for clients whose `Accept` header lists it and PNG otherwise (`Vary: Accept`); a signed URL created with `auto` negotiates on every load. |
|
|
71
134
|
|
|
72
|
-
The same parameters apply to `getImageUrl` and to every entry of `createImageUrls`; the returned `vehicle` echoes `fit`, `background`, `trim` and `padding` back. `assertImageParams` (and therefore every call) rejects a `padding` without `trim
|
|
135
|
+
The same parameters apply to `getImageUrl` and to every entry of `createImageUrls`; the returned `vehicle` echoes `vehicle_id`, `color`, `fit`, `background`, `trim` and `padding` back. `assertImageParams` (and therefore every call) rejects a `padding` without `trim`, dimensions outside 1–1024, a color that is neither a preset nor a hex, and a request that names neither a vehicle id nor all of make, model and year, before anything is sent.
|
|
73
136
|
|
|
74
137
|
## Next.js route handler
|
|
75
138
|
|
|
@@ -122,13 +185,17 @@ await client.createImageUrls(images, { ttlSeconds: 604_800, idempotencyKey: `ord
|
|
|
122
185
|
|
|
123
186
|
```ts
|
|
124
187
|
await client.getImageUrl(params); // 1 credit, returns a signed URL instead of bytes
|
|
125
|
-
await client.options(); // views, colors, sizes, fits, backgrounds, trim limits, formats, pricing, catalog coverage
|
|
126
|
-
await client.resolve("red 2024 porsche 911 side view"); // free text -> { make, model, year, view, color } + candidates + confidence high|medium|low
|
|
188
|
+
await client.options(); // views, colors (and how hex paint is spelled), sizes, fits, backgrounds, trim limits, formats, pricing, catalog coverage, vehicle ids
|
|
189
|
+
await client.resolve("red 2024 porsche 911 side view"); // free text -> { vehicle_id, make, model, year, view, color } + candidates + confidence high|medium|low
|
|
127
190
|
await client.vehicles(); // years
|
|
128
191
|
await client.vehicles({ year: 2024 }); // makes for a year
|
|
129
|
-
await client.vehicles({ year: 2024, makeId: 7 }); // models for a year + make
|
|
130
|
-
await client.
|
|
131
|
-
await client.
|
|
192
|
+
await client.vehicles({ year: 2024, makeId: 7 }); // models for a year + make, each with its vehicle id
|
|
193
|
+
await client.vehicles({ make: "porsche", model: "911" }); // a model's years, each with its vehicle id
|
|
194
|
+
await client.vehicle("veh_395yw8tn73ff8"); // one vehicle by stable id (public)
|
|
195
|
+
await client.searchVehicles("porshe 911", { limit: 5 }); // fuzzy catalog search; model hits carry ids
|
|
196
|
+
await client.decodeVin("1HGCM82633A004352"); // free VIN decode + the catalog vehicle it maps to
|
|
197
|
+
await client.create3dModel({ vehicle: id }, { color }); // 1,000 credits; then wait3dModel, get3dModel, list3dModels, download3dModel
|
|
198
|
+
await client.feedback({ requestId, verdict: "good" }); // or { make, model, year, rating: 4, reason } or { vehicle, verdict }
|
|
132
199
|
await client.listRequests({ sort: "top" }); // vehicle and feature requests; see "Requests & feedback"
|
|
133
200
|
await client.account(); // credits, auto-reload, usage_30d, key scopes
|
|
134
201
|
await client.createCheckout(5000); // hosted Stripe Checkout URL (scope billing:write)
|
|
@@ -173,12 +240,12 @@ try {
|
|
|
173
240
|
await client.getImage({ make: "Porsche", model: "911", year: 2024 });
|
|
174
241
|
} catch (error) {
|
|
175
242
|
if (error instanceof CarImageError) {
|
|
176
|
-
error.status; // 400 | 401 | 402 | 404 | 409 | 422 | 429 | 502 | 0 (network)
|
|
243
|
+
error.status; // 400 | 401 | 402 | 404 | 409 | 422 | 429 | 502 | 503 | 408 (wait3dModel timeout) | 0 (network)
|
|
177
244
|
error.title; // "Insufficient credits"
|
|
178
245
|
error.detail; // "This request needs 1 credit; balance is 0"
|
|
179
246
|
error.requestId; // "0d1e…" — quote this when contacting support
|
|
180
247
|
error.problem; // the raw application/problem+json body (402 includes balance, required_credits)
|
|
181
|
-
error.retryAfterSeconds; // set on 429,
|
|
248
|
+
error.retryAfterSeconds; // set on 429, on a 409 (Idempotency-Key conflict, or a 3D file requested before the model is ready) and on a 503
|
|
182
249
|
if (error.isInsufficientCredits) {
|
|
183
250
|
// Send a human to https://carimage.dev/dashboard?ref=npm-sdk#billing — never buy credits automatically.
|
|
184
251
|
}
|
|
@@ -199,13 +266,13 @@ import {
|
|
|
199
266
|
} from "@meterapp/car-image-sdk/mcp";
|
|
200
267
|
```
|
|
201
268
|
|
|
202
|
-
Tools come in two sets. `MCP_TOOLSETS.core` (`DEFAULT_MCP_TOOLSET`, `"core"`) is the
|
|
269
|
+
Tools come in two sets. `MCP_TOOLSETS.core` (`DEFAULT_MCP_TOOLSET`, `"core"`) is the eleven an agent needs to find, render, embed and model a car: `get_car_image`, `create_car_image_urls`, `search_vehicles`, `resolve_vehicle`, `decode_vin`, `list_image_options`, `get_account`, `rate_image`, `create_3d_model`, `get_3d_model`, `describe_api`. `MCP_TOOLSETS.all` adds the request board and the two "about you" tools: `list_requests`, `request_vehicle`, `request_feature`, `get_request`, `upvote_request`, `comment_on_request`, `share_building`, `share_referral`. The hosted server exposes `core` at `https://carimage.dev/api/mcp` and `all` at `https://carimage.dev/api/mcp?toolset=all`; the stdio server takes `car-image mcp --toolset all`. `mcpInstructions(toolset)` returns the matching server instructions (`MCP_INSTRUCTIONS` is the `all` text) and `isMcpToolset(value)` validates a name from a URL or a flag.
|
|
203
270
|
|
|
204
|
-
`get_car_image
|
|
271
|
+
`get_car_image`, each `create_car_image_urls` entry, `rate_image` and `create_3d_model` name the vehicle by make, model and year or by `vehicle` (a stable id) and take `color` as a preset or a hex; the image tools also take `fit`, `background`, `trim` and `padding` alongside view, size and format (`auto` included; `inlineImageFormat` is what both servers deliver an inline image as for it: png), and `create_car_image_urls` honors `renew` and `renew_days` and takes `idempotency_key` (`IDEMPOTENCY_KEY_PATTERN`), which the stdio server forwards as the `Idempotency-Key` header. `create_3d_model` costs 1,000 credits, and its description and the server instructions tell the agent to confirm with the human first. Every tool but `describe_api`, whose result is the reference prose itself, advertises an `outputSchema` and returns `structuredContent` that matches it, so a host can hand the model typed data instead of re-parsing the text block. The schemas allow unknown fields: the payloads mirror the REST API and gain fields as it grows.
|
|
205
272
|
|
|
206
273
|
## Types
|
|
207
274
|
|
|
208
|
-
`View`, `Color
|
|
275
|
+
`View`, `Color` (a `PresetColor` or any hex string), `Size`, `Format`, `RequestFormat`, `Fit`, `VehicleId`, `VehicleSelector`, `ImageParams`, `ImageOptions`, `ImageResult`, `CreateImageUrlsOptions`, `CreateImageUrlsResponse`, `OptionsResponse`, `FitOption`, `ResolveResponse`, `ResolveConfidence` (`"high" | "medium" | "low"`), `VehicleSearchResponse`, `VehicleResponse`, `VinResult`, `VinResponse`, `Model3d`, `Model3dFileKind`, `Model3dStatus`, `CreateModel3dOptions`, `CreateModel3dResponse`, `Model3dListResponse`, `Model3dFileResult`, `WaitModel3dOptions`, `AccountResponse`, `RequestRecord`, `RequestComment`, `CreateRequestInput`, `ReferralSource`, … plus the constant arrays `VIEWS`, `COLORS` (the presets), `SIZES`, `FORMATS`, `REQUEST_FORMATS` (the formats plus `auto`), `FITS`, `MODEL_3D_FILE_KINDS`, `REQUEST_STATUSES`, `REFERRAL_SOURCES`, the patterns `HEX_COLOR_PATTERN` and `VEHICLE_ID_PATTERN`, the helpers `isColor`, `isPresetColor`, `isHexColor`, `colorParam` and `vehicleRequestBody`, and the limits `MAX_DIMENSION` (1024), `MAX_PADDING_PERCENT` (50) and `CREDITS_PER_3D_MODEL` (1000).
|
|
209
276
|
|
|
210
277
|
## License
|
|
211
278
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AccountResponse, type BuildingResponse, type CheckoutResponse, type CreateImageUrlsOptions, type CreateImageUrlsResponse, type CreateRequestInput, type CreateRequestResponse, type DeviceCodeResponse, type DeviceTokenResult, type FeedbackInput, type FeedbackResponse, type FetchLike, type HealthResponse, type ImageParams, type ImageResult, type ImageUrlResponse, type ListRequestsParams, type OpenApiDocument, type OptionsResponse, type PortalResponse, type ReferralResponse, type ReferralSource, type RequestCommentResponse, type RequestCommentsResponse, type RequestDetailResponse, type RequestListResponse, type RequestResponse, type ResolveResponse, type TelemetryEvent, type VehicleMakesResponse, type VehicleModelsResponse, type VehicleSearchResponse, type VehicleYearsResponse } from "./types.js";
|
|
1
|
+
import { type AccountResponse, type BuildingResponse, type CheckoutResponse, type CreateImageUrlsOptions, type CreateImageUrlsResponse, type CreateModel3dOptions, type CreateModel3dResponse, type CreateRequestInput, type CreateRequestResponse, type DecodeVinOptions, type DeviceCodeResponse, type DeviceTokenResult, type FeedbackInput, type FeedbackResponse, type FetchLike, type HealthResponse, type ImageParams, type ImageResult, type ImageUrlResponse, type ListRequestsParams, type Model3d, type Model3dFileKind, type Model3dFileResult, type Model3dListResponse, type Model3dResponse, type OpenApiDocument, type OptionsResponse, type PortalResponse, type ReferralResponse, type ReferralSource, type RequestCommentResponse, type RequestCommentsResponse, type RequestDetailResponse, type RequestListResponse, type RequestResponse, type ResolveResponse, type TelemetryEvent, type VehicleMakeModelsResponse, type VehicleMakesResponse, type VehicleModelLookupResponse, type VehicleModelsResponse, type VehicleResponse, type VehicleSearchResponse, type VehicleSelector, type VehicleYearsResponse, type VinResponse, type WaitModel3dOptions } from "./types.js";
|
|
2
2
|
export interface CarImageClientOptions {
|
|
3
3
|
/** `cimg_…` key. Falls back to `process.env.CAR_IMAGE_API_KEY` when running on a server. */
|
|
4
4
|
apiKey?: string;
|
|
@@ -62,7 +62,11 @@ export declare class CarImageClient {
|
|
|
62
62
|
options(options?: RequestOptions): Promise<OptionsResponse>;
|
|
63
63
|
/** POST /api/v1/images/resolve — free; turns "red 2024 porsche 911 side view" into parameters. */
|
|
64
64
|
resolve(query: string, options?: RequestOptions): Promise<ResolveResponse>;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* GET /api/v1/vehicles — public. No filter: years; `year`: makes; `year`+`makeId`:
|
|
67
|
+
* models (each with its vehicle id); `make` slug: that make's models; `make`+`model`
|
|
68
|
+
* slugs: the model's years, each with its vehicle id.
|
|
69
|
+
*/
|
|
66
70
|
vehicles(filter?: Record<string, never>, options?: RequestOptions): Promise<VehicleYearsResponse>;
|
|
67
71
|
vehicles(filter: {
|
|
68
72
|
year: number;
|
|
@@ -71,12 +75,59 @@ export declare class CarImageClient {
|
|
|
71
75
|
year: number;
|
|
72
76
|
makeId: number;
|
|
73
77
|
}, options?: RequestOptions): Promise<VehicleModelsResponse>;
|
|
78
|
+
vehicles(filter: {
|
|
79
|
+
make: string;
|
|
80
|
+
year?: number;
|
|
81
|
+
}, options?: RequestOptions): Promise<VehicleMakeModelsResponse>;
|
|
82
|
+
vehicles(filter: {
|
|
83
|
+
make: string;
|
|
84
|
+
model: string;
|
|
85
|
+
year?: number;
|
|
86
|
+
}, options?: RequestOptions): Promise<VehicleModelLookupResponse>;
|
|
87
|
+
/** GET /api/v1/vehicles/{id} — public; the vehicle a stable id names, its years and the image URLs that render it. */
|
|
88
|
+
vehicle(id: string, options?: RequestOptions): Promise<VehicleResponse>;
|
|
89
|
+
/**
|
|
90
|
+
* GET /api/v1/vin/{vin} — free; decodes a full 17-character VIN, or a partial
|
|
91
|
+
* one of at least 5 characters with `*` for unknown positions, and names the
|
|
92
|
+
* catalog vehicle it maps to (`data.vehicle`, with the id that renders it).
|
|
93
|
+
*/
|
|
94
|
+
decodeVin(vin: string, options?: DecodeVinOptions & RequestOptions): Promise<VinResponse>;
|
|
95
|
+
/**
|
|
96
|
+
* POST /api/v1/3d — a textured 3D model (GLB, USDZ, FBX and a thumbnail) of one
|
|
97
|
+
* vehicle in one paint color. Costs `credits_per_model` credits (1,000 at
|
|
98
|
+
* launch) at creation, whether the model is cached, in progress or new; a
|
|
99
|
+
* failed model is refunded, and polling and downloads are free. Resolves at
|
|
100
|
+
* once with `status` queued or processing (202), or ready when it was cached
|
|
101
|
+
* (200); `wait3dModel` polls until it settles. Every call carries an
|
|
102
|
+
* `Idempotency-Key`, like `createImageUrls`.
|
|
103
|
+
*/
|
|
104
|
+
create3dModel(vehicle: VehicleSelector, options?: CreateModel3dOptions & RequestOptions): Promise<CreateModel3dResponse>;
|
|
105
|
+
/** GET /api/v1/3d/{id} — free; status, progress and, once ready, the files of one 3D request. */
|
|
106
|
+
get3dModel(id: string, options?: RequestOptions): Promise<Model3dResponse>;
|
|
107
|
+
/** GET /api/v1/3d — free; the caller's recent 3D requests, newest first (`limit` 1–50, default 20). */
|
|
108
|
+
list3dModels(options?: {
|
|
109
|
+
limit?: number;
|
|
110
|
+
} & RequestOptions): Promise<Model3dListResponse>;
|
|
111
|
+
/**
|
|
112
|
+
* GET /api/v1/3d/{id}/files/{kind} — free; the bytes of one file of a ready
|
|
113
|
+
* model. The route answers 302 to a short-lived signed URL on the storage
|
|
114
|
+
* host, which is followed here by hand so the API key never travels there:
|
|
115
|
+
* `fetch` attaches the key only to the client's own origin.
|
|
116
|
+
*/
|
|
117
|
+
download3dModel(id: string, kind: Model3dFileKind, options?: RequestOptions): Promise<Model3dFileResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Polls GET /api/v1/3d/{id} until the model is `ready` or `failed` and
|
|
120
|
+
* returns it either way (check `status`; a failed model carries `error` and
|
|
121
|
+
* was refunded). Throws a `CarImageError` with status 408 when `timeoutMs`
|
|
122
|
+
* (default 10 minutes) passes first; `intervalMs` defaults to 5 seconds.
|
|
123
|
+
*/
|
|
124
|
+
wait3dModel(id: string, options?: WaitModel3dOptions & RequestOptions): Promise<Model3d>;
|
|
74
125
|
/** GET /api/v1/vehicles?q= — public fuzzy search across makes and models. */
|
|
75
126
|
searchVehicles(query: string, options?: {
|
|
76
127
|
limit?: number;
|
|
77
128
|
year?: number;
|
|
78
129
|
} & RequestOptions): Promise<VehicleSearchResponse>;
|
|
79
|
-
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle. */
|
|
130
|
+
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle (make, model and year, or a vehicle id). */
|
|
80
131
|
feedback(input: FeedbackInput, options?: RequestOptions): Promise<FeedbackResponse>;
|
|
81
132
|
/** GET /api/v1/requests — public board of vehicle and feature requests; `viewer.voted` is filled in when a key is sent. */
|
|
82
133
|
listRequests(params?: ListRequestsParams, options?: RequestOptions): Promise<RequestListResponse>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CarImageError } from "./errors.js";
|
|
2
|
-
import { imageRequestBody, imageSearchParams, normalizeYear } from "./params.js";
|
|
2
|
+
import { imageRequestBody, imageSearchParams, isColor, normalizeYear, vehicleRequestBody } from "./params.js";
|
|
3
3
|
import { RETRYABLE_STATUSES, backoffDelayMs, parseRetryAfter } from "./retry.js";
|
|
4
|
-
import { DEFAULT_BASE_URL, REFERRAL_SOURCES, } from "./types.js";
|
|
4
|
+
import { COLORS, DEFAULT_BASE_URL, MODEL_3D_FILE_KINDS, REFERRAL_SOURCES, } from "./types.js";
|
|
5
5
|
import { SDK_VERSION } from "./version.js";
|
|
6
6
|
function readEnv(name) {
|
|
7
7
|
const proc = globalThis.process;
|
|
@@ -144,7 +144,12 @@ export class CarImageClient {
|
|
|
144
144
|
method: "POST",
|
|
145
145
|
body: JSON.stringify(body),
|
|
146
146
|
});
|
|
147
|
-
|
|
147
|
+
const result = await this.json(response);
|
|
148
|
+
// A replay carries `Idempotent-Replayed: true`; surface it so a caller
|
|
149
|
+
// (a batch script, the stdio MCP server) can tell a retry from a mint.
|
|
150
|
+
if (response.headers.get("idempotent-replayed") === "true")
|
|
151
|
+
result.idempotent_replayed = true;
|
|
152
|
+
return result;
|
|
148
153
|
}
|
|
149
154
|
/** GET /api/v1/images/options — public; views, colors, sizes, formats, pricing, catalog. */
|
|
150
155
|
async options(options = {}) {
|
|
@@ -168,10 +173,169 @@ export class CarImageClient {
|
|
|
168
173
|
search.set("year", String(normalizeYear(filter.year)));
|
|
169
174
|
if (filter.makeId !== undefined)
|
|
170
175
|
search.set("makeId", String(filter.makeId));
|
|
176
|
+
if (filter.make !== undefined)
|
|
177
|
+
search.set("make", filter.make.trim());
|
|
178
|
+
if (filter.model !== undefined)
|
|
179
|
+
search.set("model", filter.model.trim());
|
|
171
180
|
const query = search.size ? `?${search}` : "";
|
|
172
181
|
const response = await this.send(`/api/v1/vehicles${query}`, { ...options, auth: "optional" });
|
|
173
182
|
return this.json(response);
|
|
174
183
|
}
|
|
184
|
+
/** GET /api/v1/vehicles/{id} — public; the vehicle a stable id names, its years and the image URLs that render it. */
|
|
185
|
+
async vehicle(id, options = {}) {
|
|
186
|
+
if (typeof id !== "string" || !id.trim())
|
|
187
|
+
throw new TypeError("vehicle id is required");
|
|
188
|
+
const response = await this.send(`/api/v1/vehicles/${encodeURIComponent(id.trim())}`, { ...options, auth: "optional" });
|
|
189
|
+
return this.json(response);
|
|
190
|
+
}
|
|
191
|
+
// ------------------------------------------------------------------ VIN
|
|
192
|
+
/**
|
|
193
|
+
* GET /api/v1/vin/{vin} — free; decodes a full 17-character VIN, or a partial
|
|
194
|
+
* one of at least 5 characters with `*` for unknown positions, and names the
|
|
195
|
+
* catalog vehicle it maps to (`data.vehicle`, with the id that renders it).
|
|
196
|
+
*/
|
|
197
|
+
async decodeVin(vin, options = {}) {
|
|
198
|
+
if (typeof vin !== "string" || !vin.trim())
|
|
199
|
+
throw new TypeError("vin is required");
|
|
200
|
+
const { year, ...request } = options;
|
|
201
|
+
const search = new URLSearchParams();
|
|
202
|
+
if (year !== undefined)
|
|
203
|
+
search.set("year", String(normalizeYear(year)));
|
|
204
|
+
const query = search.size ? `?${search}` : "";
|
|
205
|
+
const response = await this.send(`/api/v1/vin/${encodeURIComponent(vin.trim())}${query}`, request);
|
|
206
|
+
return this.json(response);
|
|
207
|
+
}
|
|
208
|
+
// ------------------------------------------------------------ 3D models
|
|
209
|
+
/**
|
|
210
|
+
* POST /api/v1/3d — a textured 3D model (GLB, USDZ, FBX and a thumbnail) of one
|
|
211
|
+
* vehicle in one paint color. Costs `credits_per_model` credits (1,000 at
|
|
212
|
+
* launch) at creation, whether the model is cached, in progress or new; a
|
|
213
|
+
* failed model is refunded, and polling and downloads are free. Resolves at
|
|
214
|
+
* once with `status` queued or processing (202), or ready when it was cached
|
|
215
|
+
* (200); `wait3dModel` polls until it settles. Every call carries an
|
|
216
|
+
* `Idempotency-Key`, like `createImageUrls`.
|
|
217
|
+
*/
|
|
218
|
+
async create3dModel(vehicle, options = {}) {
|
|
219
|
+
const { color, webhookUrl, webhookSecret, idempotencyKey, ...request } = options;
|
|
220
|
+
const body = vehicleRequestBody(vehicle);
|
|
221
|
+
if (color !== undefined) {
|
|
222
|
+
if (!isColor(color))
|
|
223
|
+
throw new TypeError(`Invalid color "${String(color)}": expected one of ${COLORS.join(", ")}, or a hex paint such as #1a2b3c`);
|
|
224
|
+
body.color = String(color).trim();
|
|
225
|
+
}
|
|
226
|
+
if (webhookUrl !== undefined)
|
|
227
|
+
body.webhook_url = webhookUrl;
|
|
228
|
+
if (webhookSecret !== undefined)
|
|
229
|
+
body.webhook_secret = webhookSecret;
|
|
230
|
+
const key = idempotencyKey ?? generateIdempotencyKey();
|
|
231
|
+
const headers = { ...request.headers };
|
|
232
|
+
if (key)
|
|
233
|
+
headers["Idempotency-Key"] = key;
|
|
234
|
+
const response = await this.send("/api/v1/3d", {
|
|
235
|
+
...request,
|
|
236
|
+
headers,
|
|
237
|
+
method: "POST",
|
|
238
|
+
body: JSON.stringify(body),
|
|
239
|
+
});
|
|
240
|
+
return this.json(response);
|
|
241
|
+
}
|
|
242
|
+
/** GET /api/v1/3d/{id} — free; status, progress and, once ready, the files of one 3D request. */
|
|
243
|
+
async get3dModel(id, options = {}) {
|
|
244
|
+
const response = await this.send(`/api/v1/3d/${requestPath(id)}`, options);
|
|
245
|
+
return this.json(response);
|
|
246
|
+
}
|
|
247
|
+
/** GET /api/v1/3d — free; the caller's recent 3D requests, newest first (`limit` 1–50, default 20). */
|
|
248
|
+
async list3dModels(options = {}) {
|
|
249
|
+
const { limit, ...request } = options;
|
|
250
|
+
const search = new URLSearchParams();
|
|
251
|
+
if (limit !== undefined)
|
|
252
|
+
search.set("limit", String(limit));
|
|
253
|
+
const query = search.size ? `?${search}` : "";
|
|
254
|
+
const response = await this.send(`/api/v1/3d${query}`, request);
|
|
255
|
+
return this.json(response);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* GET /api/v1/3d/{id}/files/{kind} — free; the bytes of one file of a ready
|
|
259
|
+
* model. The route answers 302 to a short-lived signed URL on the storage
|
|
260
|
+
* host, which is followed here by hand so the API key never travels there:
|
|
261
|
+
* `fetch` attaches the key only to the client's own origin.
|
|
262
|
+
*/
|
|
263
|
+
async download3dModel(id, kind, options = {}) {
|
|
264
|
+
if (!MODEL_3D_FILE_KINDS.includes(kind)) {
|
|
265
|
+
throw new RangeError(`kind must be one of: ${MODEL_3D_FILE_KINDS.join(", ")}`);
|
|
266
|
+
}
|
|
267
|
+
const redirect = await this.rawSend(`/api/v1/3d/${requestPath(id)}/files/${kind}`, { ...options, accept: "*/*", redirect: "manual" });
|
|
268
|
+
const requestId = redirect.headers.get("x-request-id");
|
|
269
|
+
const location = redirect.headers.get("location");
|
|
270
|
+
if (redirect.status >= 300 && redirect.status < 400 && location) {
|
|
271
|
+
try {
|
|
272
|
+
await redirect.body?.cancel();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Nothing to release.
|
|
276
|
+
}
|
|
277
|
+
const target = new URL(location, this.baseUrl).toString();
|
|
278
|
+
const file = await this.fetch(target, { headers: { Accept: "*/*" }, signal: this.signalFor(options) });
|
|
279
|
+
if (!file.ok) {
|
|
280
|
+
throw new CarImageError({
|
|
281
|
+
status: file.status,
|
|
282
|
+
title: "3D file download failed",
|
|
283
|
+
detail: `The storage host answered HTTP ${file.status} for the ${kind} file. Ask for the model again to get a fresh link.`,
|
|
284
|
+
requestId,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
kind,
|
|
289
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
290
|
+
contentType: file.headers.get("content-type") ?? redirect.headers.get("x-file-content-type") ?? "application/octet-stream",
|
|
291
|
+
requestId,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (redirect.ok) {
|
|
295
|
+
return {
|
|
296
|
+
kind,
|
|
297
|
+
bytes: new Uint8Array(await redirect.arrayBuffer()),
|
|
298
|
+
contentType: redirect.headers.get("content-type") ?? "application/octet-stream",
|
|
299
|
+
requestId,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (redirect.status === 0 || redirect.type === "opaqueredirect") {
|
|
303
|
+
throw new CarImageError({
|
|
304
|
+
status: 0,
|
|
305
|
+
title: "Redirect hidden by the runtime",
|
|
306
|
+
detail: `This runtime does not expose redirects; fetch model.files.${kind}.url yourself and follow it without the API key.`,
|
|
307
|
+
requestId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
throw await CarImageError.fromResponse(redirect);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Polls GET /api/v1/3d/{id} until the model is `ready` or `failed` and
|
|
314
|
+
* returns it either way (check `status`; a failed model carries `error` and
|
|
315
|
+
* was refunded). Throws a `CarImageError` with status 408 when `timeoutMs`
|
|
316
|
+
* (default 10 minutes) passes first; `intervalMs` defaults to 5 seconds.
|
|
317
|
+
*/
|
|
318
|
+
async wait3dModel(id, options = {}) {
|
|
319
|
+
const { intervalMs = 5_000, timeoutMs = 600_000, onProgress, ...request } = options;
|
|
320
|
+
const deadline = Date.now() + timeoutMs;
|
|
321
|
+
for (;;) {
|
|
322
|
+
if (request.signal?.aborted)
|
|
323
|
+
throw request.signal.reason ?? new Error("wait3dModel aborted");
|
|
324
|
+
const { data } = await this.get3dModel(id, request);
|
|
325
|
+
onProgress?.(data);
|
|
326
|
+
if (data.status === "ready" || data.status === "failed")
|
|
327
|
+
return data;
|
|
328
|
+
const remaining = deadline - Date.now();
|
|
329
|
+
if (remaining <= 0) {
|
|
330
|
+
throw new CarImageError({
|
|
331
|
+
status: 408,
|
|
332
|
+
title: "Timed out waiting for the 3D model",
|
|
333
|
+
detail: `Model ${id} is still ${data.status} (${data.progress}%) after ${Math.round(timeoutMs / 1000)} s. Keep polling get3dModel, or pass a webhookUrl when creating one.`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
await this.sleep(Math.min(Math.max(1, intervalMs), remaining));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
175
339
|
/** GET /api/v1/vehicles?q= — public fuzzy search across makes and models. */
|
|
176
340
|
async searchVehicles(query, options = {}) {
|
|
177
341
|
if (typeof query !== "string" || !query.trim())
|
|
@@ -186,7 +350,7 @@ export class CarImageClient {
|
|
|
186
350
|
return this.json(response);
|
|
187
351
|
}
|
|
188
352
|
// ------------------------------------------------------------- feedback
|
|
189
|
-
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle. */
|
|
353
|
+
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle (make, model and year, or a vehicle id). */
|
|
190
354
|
async feedback(input, options = {}) {
|
|
191
355
|
if (input.rating === undefined && input.verdict === undefined) {
|
|
192
356
|
throw new TypeError("feedback requires a rating (1-5) or a verdict (good|bad)");
|
|
@@ -195,14 +359,14 @@ export class CarImageClient {
|
|
|
195
359
|
if ("requestId" in input && input.requestId) {
|
|
196
360
|
body.request_id = input.requestId;
|
|
197
361
|
}
|
|
198
|
-
else if ("make" in input) {
|
|
362
|
+
else if ("make" in input || "vehicle" in input) {
|
|
199
363
|
Object.assign(body, imageRequestBody(input));
|
|
200
364
|
delete body.width;
|
|
201
365
|
delete body.height;
|
|
202
366
|
delete body.format;
|
|
203
367
|
}
|
|
204
368
|
else {
|
|
205
|
-
throw new TypeError("feedback requires requestId or make/model/year");
|
|
369
|
+
throw new TypeError("feedback requires requestId, a vehicle id, or make/model/year");
|
|
206
370
|
}
|
|
207
371
|
if (input.rating !== undefined)
|
|
208
372
|
body.rating = input.rating;
|
|
@@ -500,6 +664,7 @@ export class CarImageClient {
|
|
|
500
664
|
headers,
|
|
501
665
|
body: options.body,
|
|
502
666
|
signal: this.signalFor(options),
|
|
667
|
+
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
503
668
|
});
|
|
504
669
|
}
|
|
505
670
|
catch (error) {
|