---
name: argus
description: Work with the Argus media catalog API and MCP tools (argus.build). Use this skill whenever the user mentions Argus, media catalog, asset uploads, image analysis, AI-generated tags/descriptions/mood/palette, or references argus.build. Also trigger when the user asks to upload images to a catalog, search media by meaning, manage workspaces or API keys, check credit usage, or build workflows involving media asset management — even if they don't say "Argus" by name. Two CLI front doors: `argus-media-cli sync <folder>` PUSHES a folder of files into Argus; `argus-media-cli init <project>` PULLS an existing project's brand context into a fresh/empty folder ("onboard this project WITH the <brand> assets"). If the user has the Argus MCP server connected, this skill provides the context needed to use those tools effectively.
---

# Argus

AI-powered media catalog. Upload images → get AI analysis (description, tags, mood, colors, use-cases) → search by meaning.

- **Website:** https://argus.build
- **Docs:** https://argus.build/docs
- **Machine-readable spec:** https://argus.build/agents.txt
- **MCP server:** hosted at `https://argus.build/mcp` (no install)

> For the full API reference, fetch `https://argus.build/agents.txt`. This skill covers the operational patterns you need to use Argus well.

-----

## Visual judgment — what Argus actually reads

Argus is the **visual judgment layer for AI agents**. It is a media catalog, yes — but its
value is that it reads what's inside each image, so an agent can *choose* and *advise* rather
than merely retrieve. Frame every Argus interaction this way: your job is not to find "a file,"
it's to pick the right image and argue why it fits.

Beyond object detection, every analyzed asset carries the aesthetic signal an agent needs to
make a creative call:

- **Mood** — `analysis.mood`: the emotional register of the frame (`"warm, celebratory, abundant"`).
- **Composition** — how the frame is arranged and what it was shot to do; lives in `analysis.richDescription` and `analysis.useCases`.
- **Palette** — `analysis.dominantColors`: the named hex colors, the basis for on-brand [palette-fit ranking](#palette-fit-search-bro-862).
- **Fitness for purpose** — `analysis.useCases` (hero, texture, editorial), which tells you what the asset is *for*, not just what it depicts.
- **Cohesion** — how consistently an asset set reads together, so an agent can flag the outlier that breaks the campaign's visual thread.

**Search with the vocabulary of judgment, not filenames.** Because the AI-written fields are
natural language, `q` matches how an image *reads*. Query the way a human creative director
talks:

- `q="warm, celebratory hero"` → surfaces the hero shot by meaning, not by `IMG_1467`.
- `GET /assets?q=...&paletteFit=<project>&minFit=0.6` → re-ranks hits by on-brand color fit and drops the off-palette rows.

**When you need a recommendation** ("which of these fits this campaign?"): pull the full asset
(`get_asset`, or `detail:"full"` on search), read `analysis.mood` and `analysis.useCases`, and
score color fit with `paletteFit`. Treat `fitScore` as a filter, not a verdict — open the asset
`url` and look at the image before you let an agent's pick ship.

Argus is deliberately **not** a DAM/token manager. A DAM answers "where is it stored?" Argus
carries the judgment those files hold — mood, composition, palette, cohesion, fitness — so the
agent on the other end of an MCP call makes a creative decision with eyes, not a filename.

-----

## Core Concepts

### Analysis is async

Uploads return immediately with `status: "pending"`. AI analysis takes 5–15 seconds. Poll `GET /assets/:id` every 2–3 seconds until status is `"ready"` or `"error"`. There are no webhooks.

### Search is keyword-over-AI-fields, not vector

The `q` param on `GET /assets` does case-insensitive substring matching across AI-generated fields (description, richDescription, mood, tags, useCases) plus filename. It *feels* semantic because the fields are AI-written — a sunset photo matches "warm golden hour" if the AI used those words. Use descriptive, natural-language terms for best results.

### Credits = analyses

1 credit = 1 AI image analysis. Uploads auto-consume a credit. `POST /assets/:id/analyze` re-analyzes and costs another credit.

**When to reach for each tool (these are cheap or free — don't under-use them):**

- `get_usage` / `GET /usage` — free. Call it *before* any bulk upload or generative step, and again the moment a run looks like it may exceed the budget you last checked. Check it rather than guessing at remaining credits.
- `analyze_asset` / `POST /assets/:id/analyze` — reach for it *when* an asset comes back `status: "error"`, or *when* its analysis is stale because you re-uploaded/replaced the image. Not for a `ready` asset you simply haven't inspected yet — read that one with `get_asset` first; re-analysis costs a credit.
- `recover_asset` — the moment you realize a delete was wrong. The window is 30 days, but reach for it immediately rather than deferring.
- `search_assets` vs `get_asset` — search when you're *finding* assets by meaning; `get_asset` when you already hold an id and need the full analysis (e.g. polling to `ready`).

### Soft delete

`DELETE /assets/:id` is a soft delete — asset is hidden but recoverable for 30 days via `POST /assets/:id/recover`. Pass `status=deleted` on `GET /assets` to list deleted assets. After 30 days, permanent purge.

-----

## Auth

Two auth modes:

- **API keys** (for agents/scripts): `Authorization: Bearer ak_...` — create via Settings in the web UI or via `POST /keys` with session auth.
- **Session cookies** (for the web UI): Magic link sign-in via `POST /auth/magic` → verify with `GET /auth/magic/verify?token=...` or `POST /auth/magic/verify` with `{ email, code }`.

### Named workspaces — never ask the user to paste a key (BRO-1212)

**When the user names a workspace ("use the brookfield blooms workspace", "upload these to gizzi"), NEVER ask them for an API key.** Keys pasted into a chat are exposed in the transcript. Resolve the name against the registry instead:

```bash
argus-media-cli workspace list --json      # names + active state; never prints keys
```

Match the user's phrasing against the registered names (case-insensitive, and treat "brookfield blooms" as matching `brookfield-blooms`). Then either target it for a single command, or switch the active workspace:

```bash
argus-media-cli <command> --workspace brookfield-blooms   # one command only
argus-media-cli workspace use brookfield-blooms           # persists as active
```

`--workspace <name>` outranks the ambient `ARGUS_API_KEY` env var, so it is always the safe way to target a specific workspace from an agent environment.

**If no registered workspace matches, say exactly this and stop — do not ask them to paste a key into the chat:**

> I don't have a key for that workspace. Run `argus-media-cli workspace add <name> <key>` to register it.

`workspace add` stores the key in the OS keychain (macOS Keychain / Linux `secret-tool`, falling back to a `0600` file) — never in `~/.argus/config.json` and never in the transcript. The user runs it themselves in their own terminal.

### Agent-native invite claim (no browser needed)

```
POST /invite/:token/claim         → { email } → sends verification code
POST /invite/:token/claim/verify  → { email, code } → returns { apiKey, workspaceId, workspaceName, role }
```

This flow lets an agent join a workspace and get an API key without cookies or a browser session.

-----

## Response Shapes & Envelopes

Every endpoint wraps its payload in a JSON envelope — the object below is the
**asset object**, but you never receive it bare. A single asset arrives under an
`asset` key; a list arrives under `assets` with pagination fields; the library
overview is its own top-level object. Read [Response envelopes](#response-envelopes)
before parsing a response so you reach for the right key on the first try.

### The asset object

```json
{
  "id": "uuid",
  "filename": "photo.jpg",
  "mimeType": "image/jpeg",
  "url": "https://...",
  "status": "ready",
  "tags": ["outdoor", "nature"],
  "analysis": {
    "description": "Short description",
    "richDescription": "Detailed paragraph with mood, composition, suggested uses...",
    "mood": "serene",
    "dominantColors": [{ "hex": "#f5c842", "name": "golden yellow" }],
    "useCases": ["hero image", "seasonal campaign"],
    "tags": ["wildflowers", "golden hour", "landscape"]
  }
}
```

- `tags` — merged user-supplied + AI-generated (deduplicated)
- `analysis.tags` — AI-generated only

### Response envelopes

The asset object above is never returned bare. Which key holds it depends on the
endpoint — get this wrong and you burn a round-trip re-fetching:

| Endpoint | Envelope | Reach for |
|----------|----------|-----------|
| `GET /assets/:id`, `POST /assets/upload`, `POST /assets/upload-from-url`, `PATCH /assets/:id`, `POST /assets/:id/analyze` | `{ "asset": { …asset } }` | `resp.asset` |
| `GET /assets` (list, search, or palette-fit) | `{ "assets": [ …asset ], "total", "limit", "offset", "hasMore", … }` | `resp.assets` |
| `GET /library/overview` | bare overview object (no wrapper) | `resp.totalAssets`, `resp.byProject`, … |
| `DELETE /assets/:id` | bare `{ "id", "status", "deletedAt", "recoversAt" }` | see [Delete response](#delete-response) |

**Single asset** — `{ "asset": {...} }`. The asset object sits one level down under
`asset`; there is no top-level `id`. This is the envelope every single-asset write
(upload, patch, analyze) returns too.

```json
{ "asset": { "id": "uuid", "filename": "photo.jpg", "status": "ready", "analysis": { … } } }
```

**List / search** — `{ "assets": [...], "total", "limit", "offset", "hasMore" }`. The
paginated envelope. **Check `hasMore` and page with `offset`** rather than assuming
the first page is the whole result set. A plain semantic search (`?q=` with no
paging) returns only `{ "assets", "total" }`.

```json
{ "assets": [ { "id": "uuid", … }, … ], "total": 128, "limit": 50, "offset": 0, "hasMore": true }
```

When `paletteFit` is set (see [palette-fit search](#palette-fit-search-bro-862)) each
asset gains a numeric `fitScore` (0–1) and the envelope adds `paletteSource` and
`scanned`:

```json
{
  "assets": [ { "id": "uuid", "fitScore": 0.94, … }, … ],
  "total": 12, "limit": 50, "offset": 0, "hasMore": false,
  "paletteSource": "project:Argus-site", "scanned": 131
}
```

**Library overview** — the whole object *is* the payload, no wrapper. Read
`totalAssets`, `totalBytes`, `byProject`, `byMediaType`, `byStatus`, `byApproval`,
`topTags`, `recentUploads` directly off the top level.

```json
{
  "totalAssets": 131, "totalBytes": 48210233,
  "byProject": [ { "project": "Argus-site", "count": 42 }, … ],
  "byMediaType": [ { "mediaType": "image/png", "count": 88 }, … ],
  "byStatus": [ { "status": "ready", "count": 129 }, … ],
  "byApproval": [ { "approval": "approved", "count": 40 }, … ],
  "topTags": [ { "tag": "logo", "count": 17 }, … ],
  "recentUploads": [ { "id": "uuid", "filename": "og.png", "mimeType": "image/png", "createdAt": "…" }, … ]
}
```

> The **MCP** `search_assets` / `get_library_overview` tools return the same
> envelopes; `search_assets` defaults to compact `summary` assets (pass
> `detail:"full"` for the vision `analysis` blob and SVG `vector` block).

### Delete response

```json
{
  "id": "uuid",
  "status": "deleted",
  "deletedAt": "2026-03-30T00:00:00.000Z",
  "recoversAt": "2026-04-29T00:00:00.000Z"
}
```

-----

## REST API Quick Reference

### Assets (Bearer API key)

| Method | Path | Notes |
|--------|------|-------|
| POST | /assets/upload | Multipart, field: `file`, max 20 MB (images). Video (`video/*`) also accepted for accounts with video uploads enabled, capped at 10 MB. Optional: `project`, `tags`, `uploadedBy`, `version_of` ([versioning](#versioning-this-is-a-new-version-of-x)) |
| POST | /assets/upload-from-url | Body: `{ url, filename?, project?, tags?, uploadedBy?, versionOf? }` |
| GET | /assets/:id/versions | Version history → `{ assetId, currentVersion, versions[] }`, each with `reason`, `userLinked`, `autoLinked`, `distance?` |
| POST | /assets/:id/analyze | Re-trigger analysis (1 credit) |
| GET | /library/overview | One-call read model of the whole workspace: totals, counts by project/media-type/status/approval, top tags, recent uploads. Query: `top_tags`, `recent` |
| GET | /assets | Search/list → `{ assets, total, limit, offset, hasMore }`. Query: `q`, `project`, `status`, `tags`, `sort`, `limit`, `offset`, `paletteFit`, `minFit` |
| GET | /assets/:id | Full asset + analysis |
| PATCH | /assets/:id | Update metadata. `{ tags?, project?, uploadedBy? }` — tags **replace**, not merge |
| DELETE | /assets/:id | Soft-delete (30-day recovery window) |
| POST | /assets/:id/recover | Recover soft-deleted asset |

### Versioning: "this is a new version of X"

An asset id is stable across re-uploads. When the user hands you a revised file
for something already in the library, **attach it as a version instead of
uploading a second asset** — the id, and every reference to it, keeps resolving
to the new bytes, and the old bytes stay in history.

Set `versionOf` to the existing asset id (`version_of` on the multipart form;
`?versionOf=` also works on both endpoints):

```bash
curl -X POST https://argus.build/assets/upload-from-url \
  -H "Authorization: Bearer $ARGUS_API_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hero-v2.png","versionOf":"'"$ASSET_ID"'"}'
# 201 → { asset: {…same id…}, version: { number: 2, linkedTo, reason: "manual" } }
```

- **`403 Required role: editor`** — versioning repoints an existing asset, so it
  needs `editor`, not the `uploader` that covers ordinary uploads. Upload without
  `versionOf` to file the bytes as their own asset, or ask an editor.
- **`404 No such asset to version`** — the id is missing, soft-deleted, or in
  another workspace. Confirm the id with `GET /assets/:id` first; don't retry.
- **`400`** — you sent `versionOf` *and* `allowDuplicate`/`?force=1`. They ask for
  opposite outcomes. Send exactly one.
- **`409`** — the bytes are byte-identical to what's already stored, so there is
  no new version to record. Upload the revised file, not the same one.
- No perceptual hash is computed on this path, so it is **faster** than an
  ordinary upload and it works for JP2/AVIF, which can never auto-link.

Without `versionOf`, Argus may still *guess* a version link from image
similarity (`reason: "auto_near_dup"`, with a `distance`). That is a suggestion;
`versionOf` is an assertion. `allowDuplicate: true` opts out of both.

**Over MCP, pass `version_of`** on either upload tool — `upload_from_url` or
`argus_upload` (the latter is how you version a file that is on your disk, with
no URL to hand). Same validator, same errors as above; the tools forward the
target, they do not re-implement it.

```jsonc
// upload_from_url
{ "url": "https://example.com/hero-v2.png", "version_of": "<asset id>" }
// → { …asset, "version": { "number": 2, "linkedTo": "<asset id>", "reason": "manual" } }
```

`reason` tells you which claim you got back: **`manual`** means the link is the
one you asked for, **`auto_near_dup`** (with a `distance`) means you named no
target and Argus guessed. Don't read a guess as a confirmation.

Two MCP-specific things worth knowing before you plan a re-upload:

- **`version_of` needs `editor`;** the upload tools themselves only need
  `uploader`. An `at_` token carries its owner's workspace role, so a token that
  uploads fine can still 403 here — that is the role, not the token.
- **`version_of` + `allow_duplicate` is a 400** on hosted `upload_from_url`. If a
  duplicate 409 comes back while a version target is set, the fix is to upload
  the *revised* file, not to retry with `allow_duplicate`.

### Derivatives: "this was made FROM that"

A *version* is a new take on the same asset. A *derivative* is a different
asset made from another one — a crop, a plate split, an extraction, a re-edit.
Argus records derivatives as a directed graph, and **it never infers them**: if
you don't say it, nobody ever knows it.

**Say it in the same breath as the upload.** You are the only actor who knows
which source your crops came from, and you know it for about one tool call.
Upload the derivatives, then immediately:

```
report_derivative(
  source_asset_id = "<the original>",
  derivative_asset_ids = ["<crop-1>", "…", "<crop-20>"],   # 1→N in ONE call
  relation_type = "extracted_from",
  evidence_note = "crop region 3 of 20, top-left quadrant"  # optional
)
```

- `derivative_asset_id` (singular) for the 1→1 case. Sending both forms is an
  error, not a merge.
- The batch is **all-or-nothing**: 20 edges land or none do, so a failed split
  can never leave some crops attributed and the rest silently orphaned.
- **Re-asserting is safe.** An existing edge comes back under `existing`, not as
  a failure — a retried batch is not a duplicate. Max 100 per call; split larger
  sets with overlap if you like.
- `relation_type`: `extracted_from` when the derivative came from the source's
  own pixels; `reported` when you are reporting a derivative produced *outside*
  Argus. Both assets must already exist in the workspace — upload first.
- Who asserted it and when are recorded **server-side from your credential**.
  There is no parameter for either; you cannot backdate or reattribute a claim.
- Requires `uploader` or above — deliberately the same role that uploads, so
  recording provenance never needs a role escalation.

**Reading it back.** `get_asset` returns `derivativeLinks`:

```json
{ "derivativeLinks": {
    "parents":  [ { "assetId": "...", "filename": "plate-07.tif",
                    "relationType": "extracted_from",
                    "assertedBy": "...", "assertedAt": "..." } ],
    "children": [ /* … */ ],
    "parentsTruncated": false, "childrenTruncated": true } }
```

- `parents` = what this asset was made **from**. `children` = what was made
  **from** it.
- Each direction is capped at 20. When `childrenTruncated` is `true` there are
  more — page them with `list_asset_derivatives`, and do **not** report the
  capped list as the whole set. A plate split into its crops exceeds the cap by
  design, so this is the normal case, not an edge one.

```
list_asset_derivatives(asset_id = "<the plate>", direction = "children",
                       limit = 50, offset = 0)
# → { total, limit, offset, derivatives: [ … ] }
# keep paging while offset + derivatives.length < total
```

- **Multiple hops back** — "this crop came from that scan, which came from that
  plate" — is `get_asset_lineage`:

```
get_asset_lineage(asset_id = "<the crop>", depth = 5)
# → { depth, truncated, ancestors: [ { assetId, depth, viaAssetId, … } ] }
```

  `depth` defaults to 5 and is **clamped** to 20 rather than rejected. Each
  ancestor carries `viaAssetId` — the asset that hop was reached *from* — so a
  branching ancestry reassembles unambiguously. `truncated: true` means the walk
  stopped at a bound, **not** that the chain ended; don't report it as complete.
- The REST twins are `GET /assets/:id/derivatives?direction=&limit=&offset=` and
  `GET /assets/:id/lineage?depth=N`, same shapes.

> **`derivativeLinks` is not `derivedFrom`.** `derivedFrom` is an unrelated
> legacy scalar — a single source id set only by Argus's own SVG palette
> adaptation. An asset can have a full `derivativeLinks.parents` and no
> `derivedFrom` at all; that is the normal case. Don't read one for the other.

### Billing (Bearer API key)

| Method | Path | Notes |
|--------|------|-------|
| GET | /usage | Credits remaining, asset count, tier info |

An agent key **cannot** change a workspace's plan or grant credits — billing is
capability-locked by design, and `POST /checkout/session` / `POST /checkout/credits`
are retired (`410 checkout_endpoint_retired`) even for a session cookie; they were
never reachable with an agent key to begin with. If a user needs a plan or credit
change, say so plainly and point them at **Settings → Billing & usage** in the
Argus web app (owner-only). Don't describe a checkout flow, dashboard steps, or
UI you haven't verified — if you don't know the exact path, say that and stop
there rather than inventing one.

### Workspaces & Keys (session cookie)

See `agents.txt` for full workspace/member/invite management and key CRUD endpoints.

### HTTP clients

Note: raw Python stdlib `urllib` requests are blocked at the edge (Cloudflare 403, error 1010 — banned UA signature). Set any custom `User-Agent` header (e.g. `my-agent/1.0`), or use `python-requests`/curl/the Argus CLI/MCP, which all pass.

-----

## CLI — two front doors

`npm i -g argus-media-cli`, then `argus-media-cli login` (or set `ARGUS_API_KEY`).
The two onboarding verbs run in opposite directions:

| Verb | Direction | When |
|------|-----------|------|
| `argus-media-cli sync <folder>` | **push** — folder of files → Argus | You have a directory of brand/asset files to ingest. Recursive walk, filter, dedup, sequential upload. See the `argus-brand-onboarding` skill. |
| `argus-media-cli init <project>` | **pull** — Argus → this folder | The assets **already live in Argus** and you're in a fresh/empty folder. *"Onboard this project **with** the `<brand>` assets."* Zero credits — read-only. |

`init` resolves the project from the library, writes the project's style guide to
`.argus/DESIGN-CONTEXT.md`, and drops `.argus/AGENTS.md` + a root `AGENTS.md`
pointer telling a fresh agent how to pull assets (`search_assets` scoped to the
project, `get_asset` for inline SVG logos). Both verbs accept `--json` and
`--no-agent-setup`. Route by direction: *files in hand →* `sync`; *empty folder +
a project name →* `init`.

### Curator Mode — control what agents retrieve (BRO-959)

Every asset carries an `agentRetrievable` flag. Retrievable assets (the default)
are visible to agent-facing MCP retrieval; **hidden** assets stay on the human/UI
path but are invisible to `search_assets` / `get_asset` / `get_library_overview`.
Toggling is an **editor+** action (viewer keys are agent-shaped and can never see
hidden assets by any means); the summary is viewer-readable.

| Command | What it does |
|---------|--------------|
| `argus-media-cli curator status` | Counts: total, agent-retrievable, hidden, hidden-without-note |
| `argus-media-cli curator list [--hidden\|--approved] [--no-note]` | List assets by curator state (`--hidden` is editor+ only) |
| `argus-media-cli curator approve <id\|glob> [--note "…"]` | Make asset(s) agent-retrievable |
| `argus-media-cli curator hide <id\|glob> [--note "…"]` | Hide asset(s) from agent retrieval |
| `argus-media-cli sync … --curator-default hidden\|visible` | Per-run override of the workspace default for that run's uploads only |

Globs resolve client-side against local filenames, then look up ids via
`GET /assets` (filename + `--project`). Renamed or deduped files may not resolve —
pass explicit asset ids for those. Notes are capped at 280 chars. REST equivalents:
`PATCH /assets/:id/curator`, `POST /assets/curator/bulk`, `GET /assets/curator/summary`,
`GET /assets/:id/curator/history`, and `?agentRetrievable=true|false` on `GET /assets`
(editor+ only for `false`). Admins set the workspace default via
`PUT /workspaces/:id/curator-default` (`opt-out` = new uploads retrievable, `opt-in`
= hidden); changing it never touches existing assets.

-----

## MCP Tools

Exposed by the hosted server at `https://argus.build/mcp` (see [MCP config](#mcp-config)), except where a row says **stdio package only** — those tools do local-filesystem work before calling the API, which a Worker cannot do, so they exist only in the `argus-media-mcp` npm package. Calling one over the hosted transport returns "unknown tool". The stdio package accepts `ARGUS_API_KEY` (required) and `ARGUS_BASE_URL` (optional, defaults to `https://argus.build`).

| Tool | Params | What it does |
|------|--------|--------------|
| `get_library_overview` | top_tags?, recent? | Whole-workspace read model in one call: totals, facet counts, top tags |
| `search_assets` | query?, project?, status?, tags?, brand_id?, brand_role?, pinned_view?, rights?, include_expired?, approved_only?, palette_fit?, min_fit?, min_score?, detail?, limit?, offset? | Keyword search across AI fields; `palette_fit` ranks by brand-palette fit; `detail:"full"` for the analysis blob. Expired-rights assets are excluded unless `include_expired`; `brand_id` / `brand_role` scope to one brand and cannot combine with `pinned_view` |
| `get_asset` | asset_id, allow_expired? | Full asset details + analysis, plus `derivativeLinks` — see [Derivatives](#derivatives-this-was-made-from-that) |
| `upload_from_url` | url, filename?, project?, tags?, uploaded_by?, version_of?, allow_duplicate? | Fetch URL → store → auto-analyze. `version_of` attaches the bytes to an existing asset instead — see [Versioning](#versioning-this-is-a-new-version-of-x). `allow_duplicate` stores byte-identical bytes anyway; the two are mutually exclusive |
| `argus_upload` | file_path, project?, tags?, uploaded_by?, version_of? | **stdio package only** — upload local file → auto-analyze. Reads `file_path` off your disk, so the hosted server cannot offer it; over the hosted transport use `upload_from_url`, or POST the bytes to `/assets/upload` |
| `analyze_asset` | asset_id | Re-run AI analysis (1 credit) |
| `report_derivative` | source_asset_id, derivative_asset_id? \| derivative_asset_ids?, relation_type, evidence_note?, evidence_hash? | Record that assets were made FROM a source asset. Single or 1→N batch — see [Derivatives](#derivatives-this-was-made-from-that) |
| `list_asset_derivatives` | asset_id, direction?, limit?, offset? | Page one direction of the derivative graph past `get_asset`'s 20-link cap |
| `get_asset_lineage` | asset_id, depth? | Walk ancestors multiple hops back — "A came from B came from C" |
| `delete_asset` | asset_id | Soft-delete |
| `recover_asset` | asset_id | Restore within 30-day window |
| `build_moodboard` | asset_ids, palette_id? | Build a moodboard from assets already in the library — imagery, palette and type pairing as one shareable, login-free page (1 credit, `editor`). Returns `viewUrl` plus the `direction` it derived, so you can quote the palette back without fetching the page. `palette_id` pins it to a stored brand palette |
| `remove_background` | asset_id | Segment a raster asset into a new RGBA cutout plus a distinct alpha-matte asset (`editor`, free). Use the returned `matteAssetId` as `mask.sourceAssetId` in a composition. Sources must be 20 MB or smaller. |
| `render_composition` | composition_id, version? | Flatten a composition version at full canvas resolution, store the PNG, and attach it as that version's `renderedAssetId` (`editor`, 1 credit). Omit `version` to render the current graph. |
| `get_usage` | *(none)* | Credits, asset count, tier |
| `create_api_key` | name | Returns full key once — store it |
| `get_curator_status` | *(none)* | Curator summary: total / retrievable / hidden / hidden-without-note (admin only) |
| `list_pinned_views` | *(none)* | List saved workspace views with live retrievable-asset counts |
| `set_asset_rights` | asset_id, license?, rights_holder?, license_url?, rights_expires_at?, rights_notes? | Set or clear an asset's rights and license metadata |
| `get_asset_usage` | asset_id, limit? | Read the asset's reuse audit trail |
| `link_asset_to_project` | asset_id, project | Add an asset to another project without changing its origin project |
| `unlink_asset_from_project` | asset_id, project | Remove an additional project link from an asset |
| `adapt_svg_to_palette` | asset_id, palette_fit, save? | Recolour an SVG to a style guide or project palette, optionally saving the result |
| `list_workspaces` | *(none)* | List workspaces available to this credential and identify the bound workspace |
| `moderate_asset` | asset_id | Run the brand-policy moderation check and persist its verdict |
| `set_asset_approval` | asset_id, status, note? | Move an asset through the approval state machine (editor+) |
| `create_brand_project` | name, business_description?, audience?, tone_words?, competitors?, style?, taste_notes?, phase?, linked_asset_ids?, style_guide_id? | Create a persisted brand project and creative brief |
| `get_brand_project` | brand_project_id | Read a brand project, its brief, linked assets, and phase |
| `update_brief` | brand_project_id, name?, business_description?, audience?, tone_words?, competitors?, style?, phase?, linked_asset_ids?, style_guide_id? | Update a brand project's persisted creative brief |
| `get_brand_dna` | workspace_id?, brand? | Read the latest per-brand Study Mode palette, type, cohesion, and composition principles |
| `generate_directions` | brand_project_id, count?, asset_ids?, purpose?, audience?, tone?, style? | Generate and persist 2–3 contrasting creative directions |
| `list_directions` | brand_project_id, status? | List a project's creative directions, optionally by status |
| `set_direction_status` | direction_id, status | Change a direction's review status |
| `lock_direction` | brand_project_id, direction_id, remix_notes?, feedback? | Lock the chosen direction as the project's generation ground |
| `unlock_direction` | brand_project_id, feedback? | Unlock the current direction so another can be selected |
| `get_lock_history` | brand_project_id | Read the project's direction lock/unlock audit history |
| `export_brand_kit` | brand_project_id, include_hidden_assets? | Export a versioned brand-kit snapshot from the locked direction/style guide |
| `get_brand_kit` | brand_project_id | Read the current assembled brand kit for generation grounding |
| `get_brand_marks` | brand, role?, variant?, ground? | Deterministically fetch declared marks for one brand |
| `set_brand_role` | asset_id, brand, role, variant?, ground?, replace_existing? | Declare an asset's brand-mark role and variant |
| `suggest_brand_roles` | brand, project?, limit? | Suggest mark roles for unclassified brand assets |
| `list_brand_kits` | brand_project_id | List exported brand-kit versions newest first |
| `export_design_tokens` | guide_id | Export a style guide as CSS custom properties and structured JSON tokens |
| `generate_creative_direction` | asset_id?, asset_ids?, brand_project_id?, purpose?, audience?, tone?, style? | Generate a creative direction from assets and optional persisted brief context |
| `suggest_typography` | asset_id?, purpose?, brand_project_id? | Suggest type pairings grounded in an asset or brand project |
| `generate_color_palette` | asset_id?, brand_project_id? | Generate a colour palette grounded in an asset or brand project |
| `compare_assets_cohesion` | asset_ids, purpose?, brand_name? | Compare a set of assets for visual cohesion |
| `generate_style_guide` | asset_ids, name, guide_id?, brand_project_id? | Generate or revise a persisted style guide from assets |
| `export_design_context` | guide_id, off_brand_tags? | Export a style guide as agent-readable design context |
| `get_generation_job` | job_id | Read status and result for an asynchronous generation job |
| `generate_mockups` | brand_project_id, templates? | Render locked brand tokens into persisted application mockups |
| `list_mockups` | brand_project_id | List persisted application mockups for a brand project |
| `create_composition` | width, height, title?, background?, brand_project_id?, layers? | Create a composition — a layer-graph document describing an image built from assets in this library (not pixels; nothing is rendered or billed here). layers[0] is the back of the stack; pass the id to the layer-editing tools below |
| `get_composition` | composition_id, version?, include_history? | Read a composition's current layer-graph, or one historical version. Return its `version` to detect concurrent edits |
| `list_compositions` | brand_project_id?, limit? | List this workspace's compositions newest first, optionally scoped to a brand project |
| `add_layer` | composition_id, layer_id, asset_id, opacity?, blend?, index?, clip_to?, x?, y?, scale?, rotate?, expected_version? | Add an existing asset as a new layer (references assets already in this workspace; does not upload) |
| `remove_layer` | composition_id, layer_id, expected_version? | Remove a layer from a composition — appends a new version, so undo by reading the prior one. Layers clipped to it are un-clipped, not deleted |
| `reorder_layer` | composition_id, layer_id, to_index, expected_version? | Move a layer to an absolute paint-order position; 0 = furthest back, index is clamped |
| `set_layer_source` | composition_id, layer_id, asset_id, expected_version? | Repoint a layer at a different asset, or a different version of the same one |
| `set_opacity` | composition_id, layer_id, opacity, expected_version? | Set one layer's opacity (0 = invisible, 1 = opaque); appends a new composition version |
| `apply_blend` | composition_id, layer_id, blend, expected_version? | Set one layer's blend mode (CSS mix-blend-mode keywords = the server compositor's formulas); appends a new version |
| `adjust` | composition_id, layer_id, op, replace?, channel?, points?, in_black?, in_white?, gamma?, out_black?, out_white?, hue?, saturation?, lightness?, l?, a?, b?, expected_version? | Add a colour adjustment to a layer's stack — curves, levels, HSL or LAB. Adjustments stack in order; `replace: true` clears first |
| `list_catalog_design_resources` | category?, query? | List published catalog.design resources, optionally filtered by category or a case-insensitive text query |
| `get_catalog_design_resource` | id | Get one published catalog.design resource by id |
| `list_design_skills` | *(none)* | List Argus's branded-collateral design-skill recipes |
| `get_design_skill` | id | Read one complete design-skill manual by id |
| `record_preference_signal` | subject_type, subject_id, checkpoint, signal, revision?, facets?, note? | Record or replace a human verdict on a generated artefact |
| `list_preference_signals` | subject_type?, subject_id?, limit? | List recorded preference signals for the workspace |

Both upload tools take `version_of` — you do not have to drop to REST to attach a
revised file to an existing asset. It needs the `editor` role, unlike an ordinary
upload; see [Versioning](#versioning-this-is-a-new-version-of-x) for the error
table.

### MCP config

Hosted transport — nothing to install:

```bash
claude mcp add --transport http argus https://argus.build/mcp --header "Authorization: Bearer at_YOUR_TOKEN"
```

Get the `at_` token from the Argus library toolbar → **Connect agent**. It is
short-lived and workspace-scoped, and it is the only Argus credential safe to
paste into a chat transcript (`ak_` keys are durable — env vars and CI only).
Verify it without restarting the session:

```bash
curl -s https://argus.build/auth/me -H "Authorization: Bearer at_YOUR_TOKEN"
```

Equivalent client config:

```json
{
  "mcpServers": {
    "argus": {
      "type": "http",
      "url": "https://argus.build/mcp",
      "headers": { "Authorization": "Bearer at_YOUR_TOKEN" }
    }
  }
}
```

The stdio package (`npx argus-media-mcp`, env `ARGUS_API_KEY`) stays available
for one job the hosted server cannot do: uploading a file from the local disk
via `argus_upload`.

### Streamable HTTP (Claude.ai / remote)

```json
{
  "mcpServers": {
    "argus": {
      "url": "https://argus.build/mcp"
    }
  }
}
```

-----

## Common Workflows

### Upload + wait for analysis

1. `POST /assets/upload` (or use `argus_upload` / `upload_from_url` MCP tool)
2. Poll `GET /assets/:id` every 2–3s
3. When `status === "ready"`, the `analysis` object is populated

### Handling pending assets — best-effort policy

When a workspace has both `ready` and `pending` assets, **do not wait or poll for pending ones unless the user explicitly asks you to**. The correct behavior:

1. Filter to `status: ready` assets only (`GET /assets?status=ready` or filter the list client-side).
2. Proceed with ready assets immediately.
3. Add a one-line coverage note in your output: "X of Y assets were still analyzing and are not reflected."

Do **not** poll pending assets one-by-one — this burns time and requires user interruption to stop. If all assets need to be ready before you can proceed (e.g. a final synthesis), tell the user and ask them to wait rather than polling silently.

### Bulk upload (no batch API)

Loop uploads one at a time. Check `GET /usage` first to confirm credit budget. Fire uploads sequentially via MCP `argus_upload` or REST, collecting IDs, then poll each for completion.

### Reading a library — overview → filter → search

When you land in an unfamiliar or large workspace, work **top-down** — never page
the whole catalog to find your bearings:

1. **Overview first.** `GET /library/overview` (or `get_library_overview`) returns
   the whole-workspace shape in one call: total assets + storage, counts by
   project / media-type / status / approval, and the top tags. This is your map,
   and it's cheap — one aggregate query, no per-asset scan. A synced folder also
   carries a cached snapshot of this in its `.argus/AGENTS.md`, so an agent often
   starts already knowing the shape at zero API cost.
2. **Facet-filter to narrow.** Use a real project or tag from the overview to
   scope down *before* searching: `GET /assets?project=<p>` or `?tags=<t>`.
3. **Semantic search within that slice.** `GET /assets?q=...&project=<p>` finds
   the specific asset by meaning inside the narrowed set.

Paging the full library (looping `?limit=&offset=` over everything) is the
anti-pattern — it's slow, blows the context budget, and the overview + facet
filters make it unnecessary even at 10k+ assets.

### Search then use

1. `GET /assets?q=warm+floral+bouquet` (or `search_assets` MCP tool)
2. Results ranked by term-match count across AI fields
3. Use `analysis.useCases` to pick the right asset for the job

### Palette-fit search (BRO-862)

Keyword search finds assets by *meaning*; it can't tell an on-brand asset from an
off-brand one that happens to match the words (a purple/yellow wordmark surfacing
on an Argus query). `paletteFit` re-ranks results by how closely each asset's
dominant colours sit on a target brand palette, so on-palette assets rise and
cross-brand noise sinks.

- **Target** — pass a **style-guide id** *or* a **project name** whose analyzed
  assets define the palette (`paletteFit=Argus-site` or `paletteFit=<style-guide-id>`).
- **Score** — every returned asset gains a `fitScore` in `[0,1]` (1 = dead-on
  palette); off-palette assets sort last. The envelope also carries `paletteSource`
  and `scanned`.
- **Threshold** — add `minFit=0.6` to drop anything below the cutoff instead of
  merely sorting it down.
- **Composes** with `q`, `project`, `tags`, and `status`. With `q` present it
  re-ranks the semantic hits; without `q` it re-ranks the browse list.

| Surface | How |
|---------|-----|
| REST | `GET /assets?paletteFit=Argus-site&minFit=0.6` (optionally `&q=…&project=…`) |
| MCP | `search_assets` with `palette_fit` (+ `min_fit`) |
| CLI | `argus-media-cli search --palette-fit Argus-site --min-fit 0.6` |

### Recipe: find reusable on-brand assets for a project

When you need to reuse existing catalog assets that *match a brand* — logos,
textures, hero images to drop into that project's collateral — don't eyeball a raw
search. Rank by palette fit, then confirm with your eyes:

1. **Overview → get your bearings.** `get_library_overview` (or
   `GET /library/overview`). Confirm the project exists and note its asset count
   and top tags so you know what's in play and what to scope to. Cheap, one call.
2. **Palette-fit search → rank the candidates.** Search scoped to the look you
   want and rank by fit against the brand:
   `search_assets` with `palette_fit:"<project>"` (+ `query`/`tags` to narrow,
   `min_fit` to drop off-palette rows). On-brand assets come back first, each with
   a `fitScore`. This replaces guessing which of a keyword search's hits are
   actually on-brand.
3. **Download + visually verify the shortlist.** `fitScore` ranks colour fit, not
   subject or quality — it is a filter, not a verdict. For the top few, pull the
   full asset (`get_asset`, or `detail:"full"` on the search) and **open the `url`
   to look at each image** before committing. Read `analysis.useCases` to confirm
   the asset actually fits the job. Only then reuse them.

The shortlist is small and human-checked, so a bad colour match or an
off-subject asset gets caught before it ships.

### Tag management

`PATCH /assets/:id` with `{ tags: [...] }` — this **replaces** the entire tag array, it does not merge. Always read current tags first if you need to append.

### `/argus brief <workspace>` — shareable brand brief in one shot

**Trigger phrases:** "give me a brief for [workspace]", "share the brand assets", "create a design brief", "brand brief for [workspace]", "jlnk the brand brief".

**Target time: under 3 minutes** for a typical workspace (25–100 assets).

#### Step 1 — Resolve workspace

If the user named a workspace, resolve it against the registry — see [Named workspaces](#named-workspaces--never-ask-the-user-to-paste-a-key-bro-1212). Do **not** assume the ambient `ARGUS_API_KEY` is the workspace they meant:

```bash
argus-media-cli workspace list --json
argus-media-cli brief --workspace <name> --output brief.html
```

If the user named no workspace, the ambient `ARGUS_API_KEY` (or the active workspace) is the right default and no resolution is needed. If they named one that isn't registered, give them the `workspace add` line and stop.

#### Step 2 — Overview (free, one call)

```
GET /library/overview?top_tags=20&recent=10
```

or `get_library_overview`. Read from the response:
- `totalAssets` and `byStatus` → know how many are `ready` vs `pending`
- `byProject` → project breakdown for the stats section
- `byMediaType` → media-type breakdown
- `topTags` → pre-computed tag cloud

#### Step 3 — Fetch ready assets

```
GET /assets?status=ready&limit=100
```

or `search_assets` with `status: "ready"` and `detail: "full"`.

- Filter client-side to `status === "ready"` only — skip `pending` entirely.
- If `pendingTotal > 0`, note: `"X of Y assets were still analyzing and are not reflected."`

#### Step 4 — Mine brand signals (from the asset list)

| Signal | Source | How |
|--------|--------|-----|
| Palette | `analysis.dominantColors[].hex` | Count hex frequency; take top 6–8 by frequency |
| Mood | `analysis.mood` | Frequency-count; take top 3 moods |
| Use cases | `analysis.useCases[]` | Union across all assets; deduplicate; take top 10 |
| Top tags | `analysis.tags[]` | Frequency-count across all assets; take top 20 |
| SVG logos | `mimeType === "image/svg+xml"` | Fetch `get_asset` (or `GET /assets/:id`) for each SVG to get `vector.inlineSvg` |

#### Step 5 — Generate the HTML brief

Use CDN URLs (`displayUrl` or `url` field) for raster image thumbnails — **do not base64-embed rasters**; that blows the 5 MB jlnk limit. Inline SVGs are fine (usually tiny).

Sections in order:
1. **Header** — workspace name + asset count
2. **Coverage note** — if any assets are pending
3. **Brand palette** — color swatches with hex + name
4. **Logos / vectors** — inline SVG renders (max 8)
5. **Asset gallery** — thumbnail grid using CDN URLs (max 16)
6. **Top tags** — tag chips
7. **Stats** — by-project, by-media-type, by-status tables

**Security:** never embed the API key or session token in the HTML — the brief is public once jlnked.

#### Step 6 — Publish to jlnk

```
mcp__jlnk__create_link(htmlContent, "72h")
```

Return the URL to the user.

#### CLI shortcut

```bash
argus-media-cli brief [workspace] --output brief.html
```

Generates the HTML locally without publishing. The agent can then read the file and call jlnk separately, or the user can inspect it first.

Named workspace support (BRO-1212): use `--workspace <name>` to target a specific workspace without touching the active selection:

```bash
argus-media-cli brief --workspace gizzi-productions --output gizzi.html
```

-----

## Pricing

| Tier | Assets | Credits/mo | Price |
|------|--------|------------|-------|
| Free | 50 | 50 | $0 |
| Starter | Unlimited | 500 | $19/mo |
| Pro | Unlimited | 5,000 | $79/mo |
| Enterprise | Unlimited | Unlimited | Custom |

Credit packs (one-time, don't expire): 100/$2, 500/$8, 2,500/$30.

-----

## Supported Formats

- **AI-analyzed:** JPEG, PNG, GIF, WebP, SVG
- **Stored but not analyzed:** Any other file type (PDFs, docs, etc.)
- **Video:** `video/*` (MP4, MOV, WebM, etc.) accepted only for accounts with the video-uploads flag enabled (default off); not analyzed
- **Max upload:** 20 MB (images); 10 MB (video)
- **Storage:** Cloudflare R2

-----

## Limitations — When NOT to Use Argus

- Video is flag-gated per account (default off) and capped at 10 MB; still no AI analysis
- No real-time/synchronous analysis (always async)
- Non-image files are stored but never analyzed
- No BYO-bucket (R2 only)
- No batch import API — one upload at a time

-----

## Errors

All errors return `{ "error": "..." }`. Status codes: 400, 401, 404, 429 (tier limit — includes `"upgrade": true`), 500. On 429, check `GET /usage` for current consumption.
