# @drawcall/market

Typed client, dependency resolver, and CLI for the [Drawcall Market](https://market.drawcall.ai) — an asset marketplace for 3D models, textures, animations, audio, environments, flipbooks, and templates.

This package is the single source of truth for the Market API surface: the oRPC contract, Zod schemas, and TypeScript types. It ships both a programmatic API and the `market` CLI.

## Install

```bash
npm install @drawcall/market
```

## Usage

`v1.createClient` is the one API. It returns a fully-typed [oRPC](https://orpc.dev) client for the `/api/v1` REST surface — every operation (`search`, `exact`, `manifest`, `files`, `uploadZip`, `generate`, `installMetadata`) is type-checked end-to-end against the routed contract, and the generated OpenAPI document lives at `https://market.drawcall.ai/api/v1/openapi.json`.

```ts
import { v1, downloadAssetZipBytes } from '@drawcall/market'

const client = v1.createClient()
// v1.createClient({ baseUrl, fetch, authToken }) to override the API host,
// supply a custom fetch, or authenticate reads/writes.

const asset = await client.asset.exact({ name: 'my-model', includeUnapproved: false })
// The manifest: metadata + every file's content-plane URL + previewUrl + zipUrl:
const manifest = await client.asset.manifest({ name: 'my-model', version: asset.latestVersion })
// Or the whole version as one zip:
const zip = await downloadAssetZipBytes({ name: 'my-model', version: asset.latestVersion })
```

Reads (`search`, `exact`, `manifest`, `files`, `installMetadata`) are public. `uploadZip` and `generate` require an `authToken`.

### Searching

`client.asset.search` takes a query plus paging and returns a relevance-ranked paginated list:

```ts
const page = await client.asset.search({
  query: 'robot',
  type: 'model', // optional AssetType; omit to search every type
  page: 1,
  limit: 12,
  includeUnapproved: false,
})
```

#### Output format

`search` resolves to a `PaginatedList<AssetSearchResult>`:

```ts
{
  items: AssetSearchResult[]
  total: number        // total matches across all pages
  page: number
  limit: number
  totalPages: number
}
```

Each `AssetSearchResult` is:

```ts
{
  id: string
  name: string // globally unique, install by this name
  type: string // AssetType, e.g. 'model'
  description: string | null
  ownerId: string
  createdAt: Date
  updatedAt: Date
  latestVersion: string // semver of the latest published version
  approved: boolean
  npmDependencies: Record<string, string>
  assetDependencies: AssetDependencies // range string or { version, alias } per entry
  skillDependencies: Record<string, string>
  previewUrl: string | null // image URL for an <img>, or null
}
```

### Previews

`previewUrl` is the fetch-ready image URL for a result, or `null` for types without a preview (only `model`, `humanoid-model`, `texture`, `environment`, and `flipbook` have one). Drop it straight into an `<img src>`; private and unapproved results already carry the capability when the caller is entitled to see them:

```ts
import { v1 } from '@drawcall/market'

const client = v1.createClient()
const { items } = await client.asset.search({
  query: 'robot',
  type: 'model',
  page: 1,
  limit: 12,
  includeUnapproved: false,
})

for (const item of items) {
  if (!item.previewUrl) continue
  // <img src={item.previewUrl} loading="lazy">
}
```

The runnable Market frontend renders results immediately and lets the browser load previews from these URLs.

## `resolve` — dependency resolution

Resolve a set of assets (and their transitive asset/npm/skill dependencies) to a concrete, installable plan. The CLI and any server-side caller share this resolver:

```ts
import { v1, resolve } from '@drawcall/market'

const client = v1.createClient()
const plan = await resolve(client.asset, [{ name: 'my-model', range: '^1.0.0' }])

plan.assets // resolved name@version per asset (with its type)
plan.npmDependencies // merged npm ranges
plan.skillDependencies // merged skill sources
```

The Node-only filesystem APIs are available from `@drawcall/market/install`. `install` downloads and
writes resolved assets; `listInstalledAssets` reads the declared `assetDependencies` (name, range,
aliases) from the nearest `package.json`; `getCliClient` uses `DRAWCALL_AUTH_TOKEN` or the saved
Market login when private assets must resolve.

```ts
import { getCliClient, listInstalledAssets } from '@drawcall/market/install'

const { assets } = await listInstalledAssets(process.cwd())
const { client } = await getCliClient()
const plan = await resolve(
  client.asset,
  assets.map(({ name, range }) => ({ name, range })),
)
const index = await client.asset.files({
  name: plan.assets[0].name,
  version: plan.assets[0].version,
})
await fetch(index.files[0].url)
```

## CLI

```bash
npx @drawcall/market install my-model        # resolve + install by name
npx @drawcall/market types                   # current types, generation support, search guidance
npx @drawcall/market list                    # list locally installed assets
npx @drawcall/market list --files            # include installed file paths
npx @drawcall/market search robot --type model
npx @drawcall/market preview my-model@1.2.0  # save that version's preview image
npx @drawcall/market urls my-model@1.2.0     # remote file base URL, subpaths, and preview URL
npx @drawcall/market pack ./my-model.zip --out ./my-model.packed.zip
npx @drawcall/market upload my-model ./my-model.zip "A robot" --type model
npx @drawcall/market login                   # device-authorization sign-in
```

Market credentials are managed by `@drawcall/auth` and shared with other Drawcall CLIs. Existing
credentials in `drawcall-market/config.json` migrate automatically.

Reads (`types`, `install`, `list`, `search`, `preview`, `urls`) work without auth; `pack` is offline; `upload` and `generate` require `market login`. After an install, the CLI prints provider-owned usage once per installed asset type.

`urls` performs no filesystem writes. Its output follows the same compact asset-and-files layout as `install`; the `base` plus each indented file subpath forms a fetch-ready asset file URL:

```text
URLs:
- my-model@1.2.0
  base: https://market.drawcallcontent.com/public/my-model@1.2.0/
  files:
    public/models/my-model.glb
  preview: https://market.drawcallcontent.com/public/previews/my-model@1.2.0.webp
```

Use `--key <key>` for a shared private asset and `--unapproved` when explicitly resolving an unapproved version. Private capability-bearing URLs are secrets.

`pack` creates the same asset zip that `upload` sends. It runs offline, infers template packing from a root `package.json`, and accepts `--type` only when you need to override that inference. `upload` calls the shared pack step internally, then publishes.

`list` is an offline local inventory command. It reads `assetDependencies` from the nearest `package.json` and prints each declared asset with its range and file aliases.

Run `npx @drawcall/market skill` to print the agent workflow guidance, or `npx @drawcall/market --help` for the full command list.

The Commander implementation is available for aggregate CLIs:

```ts
import { createMarketCommand } from '@drawcall/market'

program.addCommand(await createMarketCommand())
```

The MCP tools follow the same concepts as `types`, `search`, `preview`, `generate`, and `agent`,
without exposing CLI-only filesystem operations such as install or pack:

```ts
import { registerMarketTools, v1 } from '@drawcall/market'
import { McpServer } from '@modelcontextprotocol/server'

const client = v1.createClient({ authToken: process.env.DRAWCALL_AUTH_TOKEN })
const server = new McpServer({ name: 'drawcall-mcp', version: '1.0.0' })

registerMarketTools(server, client, {
  capabilities: { generate: true, agent: true },
})
```

The host owns server identity, authentication, transport, and capability selection. The MCP SDK is
an optional peer dependency and is required only when mounting these tools.
