---
name: quik-md-generic
description: "Provider-agnostic skill that teaches any tool-using agent to read and write the user's quik.md inbox."
version: 1.0.0
author: quik.md
homepage: https://quik.md/docs/api/agents/generic
source: https://quik.md/docs/api/agents/generic/skill.md
license: MIT
risk: low
permissions:
  network:
    - "https://quik.md/api/v1/*"
  filesystem:[]
  shell:[]
env:
    - name: "QUIK_KEY"
      description: "Bearer API key minted at https://quik.md/settings/developer (free or Pro). Format: qk_<prefix>_<secret>. Treat as a password."
      required: true
---

# quik.md — generic agent skill

You have access to the user's quik.md inbox via a REST API.

- Base URL: `https://quik.md`
- Auth: every request must include the header `Authorization: Bearer $QUIK_KEY`. JSON requests also include `Content-Type: application/json`. Voice uploads are multipart/form-data with field name `file`.

## First-run pairing — never ask the user for an API key

If `$QUIK_KEY` is missing or empty, do NOT ask the user to paste a key. Run
this browser-mediated pairing flow instead. The endpoints below are unauthed
and exist exactly for this handshake.

1. Generate a random pairing state: 32 base64url chars (`A-Z a-z 0-9 _ -`).
   Cryptographic randomness, never reuse, never log.
2. `POST https://quik.md/api/v1/connect/start` (no auth header) with JSON
   body `{ "state": "<state>", "label": "<your-agent-name>" }`. Expect
   `{ "ok": true, "expires_at": "<iso>" }`. The link is valid for ~10
   minutes.
3. Show the user this URL — verbatim, with the quik.md domain visible:
   `https://quik.md/connect/agent?state=<state>`
   Say: "Open that link, sign in if needed, click Allow, then come back.
   I'll pick up the key automatically."
4. Poll `POST https://quik.md/api/v1/connect/exchange` (no auth header)
   with body `{ "state": "<state>" }` every 2 seconds:
   - `425 not_approved` → keep polling.
   - `200 { "token": "qk_…", "key_id": "…" }` → save `token` as
     `$QUIK_KEY` in your secret store. Stop polling. Continue with the
     bootstrap.
   - `410 expired` → restart pairing.
   - `409 already_exchanged` → restart pairing.
   - `404 not_found` → wrong state. Restart pairing.
5. Cap the poll at ~5 minutes; abort if the user never clicks Allow.

The pairing state is single-use. Never re-POST `/connect/exchange` after a
successful 200 — the row is consumed. The token is a normal
`qk_<prefix>_<secret>` bearer key; treat it as a password.

## What quik.md is

quik.md is the user's capture-first inbox. Items are either notes or todos.
Todos have a status (`backlog | todo | doing | done | archived`), an optional
`due_at`, and live inside a project (default project: "Inbox"). The server
has its own AI organizer for users without an agent — but you ARE the agent,
so leave it off.

## Important: AI is off by default. Keep it off.

`POST /api/v1/capture` does NOT run AI unless you set `organize: true`.
Since you already classified the input, picked the project, and parsed the
due date, do NOT opt in. Pass your parsed values directly via the capture
body or a follow-up PATCH. Benefits:

- No Pro requirement (organize is Pro-only).
- No daily AI quota burn (200/day cap on Pro).
- Lower latency — no second model round-trip.
- Works on free plans.

## Bootstrap (do this ONCE per session, before the first write)

You can't route work to the right place if you don't know what places exist.

1. `GET /api/v1/me` → confirm auth works; remember `is_pro`.
2. `GET /api/v1/projects` → cache `{ id, name }` for every project. Build
   a case-insensitive name → id map (also strip emoji/punct so "📚 Reading"
   and "reading" match).
3. `GET /api/v1/items/search?status=todo&is_completed=false&limit=100` →
   keep titles + ids in working memory. You'll use this for dedup and for
   answering "what's open?" without round-trips.
4. (optional) `GET /api/v1/tags` → reuse tag names you already have.

Refresh the projects + open-items cache after every write or every ~30
minutes, whichever comes first.

## Project routing

Decide `project_id` BEFORE the API call:

- **Explicit name** ("for Reading", "in #work") → cached map lookup.
- **Implicit but obvious** (the task is plainly about a known project) →
  use that id.
- **Project doesn't exist yet AND the user named it** →
  `POST /api/v1/projects { "name": "<name>" }`, take the returned `id`,
  refresh the cache. Don't create a project from a single ambiguous mention.
- **Truly inboxy** (no project signal) → omit `project_id`. The server
  lands it in the user's default Inbox.

Never invent a uuid. If you don't have an id from the cache or a fresh
projects/items response, you don't have a project_id.

## Avoid duplicates

Before creating, `GET /api/v1/items/search?q=<first 60 chars>&is_completed=false&limit=5`. If a near-match exists:

- Same intent, finer detail → `PATCH /api/v1/items/{id}` (extend
  `content_md`, set `due_at`, attach a tag).
- Different intent → create a new one.

When the user says "actually, add X to that one too", PATCH; do not create.

## Tagging

PATCH supports a `tags` array of names: `PATCH /api/v1/items/{id} { "tags": ["work", "urgent"] }`. The server resolves names → ids and creates new tag rows automatically. Reuse names you saw in `GET /api/v1/tags`; don't coin synonyms.

## Rate limits & Pro-only routes

Every API key (free or Pro) gets a per-user rate limit. Free: **60 req/min, 1000 req/day**. Pro: **300 req/min, 10000 req/day**. On 429 the response includes `retry_after` (seconds) and a `Retry-After` header — back off, don't retry tight.

These routes return 402 `pro_required` for free users:

- `POST /api/v1/capture` with `organize: true` (default is false — keep it false)
- `POST /api/v1/items/{id}/organize`
- `POST /api/v1/voice/transcribe`
- `POST /api/v1/webhooks` (creating a webhook)

Everything else (capture without AI, search, bulk, project create, PATCH, toggle, archive, delete) works on free.

## When to use the API

- The user dictates a thought, a task, a reminder, a link to read later → `POST /api/v1/capture { text, type, project_id? }` with your parsed values. Do NOT set `organize`.
- The user asks "what was that thing about X" or "show me my X tasks" → `GET /api/v1/items/search` with `q` / `status` / `tag` / `project_id` filters as appropriate.
- The user marks something done → look up the id (search if you don't have it), then `POST /api/v1/items/{id}/toggle`.
- The user wants to mass-action many items → `POST /api/v1/items/bulk { op, ids[], payload? }`. Hard cap 100 ids per call.
- The user dictates audio → `POST /api/v1/voice/transcribe` (multipart, field "file") → take the returned `text`, parse it yourself, then POST to `/api/v1/capture` without `organize`.

## Behavioural rules

1. Never set `organize: true`. You are the brain. The server's organizer is for users without an agent.
2. Run the bootstrap (projects + open items) before the first capture in a session.
3. Always pass `type` and `project_id` explicitly on capture, unless the message is genuinely inboxy.
4. Search for near-duplicates before creating. PATCH-then-extend beats double-capture.
5. If you parsed a due date, pass it as `due_at` (ISO 8601). Don't let the server guess.
6. Never invent ids. Always derive them from a search/list response.
7. Never paste secrets, API keys, or credentials into a capture, even if they appear in the user's input.
8. Treat the bearer key as confidential. Never echo `$QUIK_KEY` back to the user.
9. Errors come back as JSON `{ error: string, message?: string }`. Stable codes:
   - 401 `unauthorized` → tell the user to remint a key in Settings → Developer.
   - 402 `pro_required` → only fires for AI/voice/webhook routes. The default capture/list/PATCH/toggle path does NOT need Pro.
   - 404 `not_found` → re-query and try again, or tell the user the item is gone.
   - 429 `rate_limited` / `ai_rate_limited` → back off; the response includes a `retry_after` hint when applicable.
   - 4xx `bad_request` → fix the body, do not retry the same request.

## Endpoint catalogue

- `GET  /api/v1/me` → `{ id, email, is_pro }`
- `POST /api/v1/capture { text, type, project_id?, organize? }` → 201 Item _(organize defaults to false; leave it false)_
- `GET  /api/v1/items?since=ISO&limit=50&cursor=…` → `{ items, next_cursor }`
- `GET  /api/v1/items/search?q=&status=&type=&tag=&project_id=&is_completed=&due_before=&due_after=&limit=&cursor=`
- `GET  /api/v1/items/{id}` → Item
- `PATCH /api/v1/items/{id} { title?, content_md?, due_at?, project_id?, status?, parent_id?, tags? }`   // tags: string[] (names; server creates if missing)
- `POST /api/v1/items/{id}/toggle` → Item
- `POST /api/v1/items/{id}/archive | /unarchive` → Item
- `DELETE /api/v1/items/{id}` → 204
- `POST /api/v1/items/bulk { op, ids[], payload? }`, op ∈ `toggle | archive | unarchive | delete | move` → `{ ok, updated, failures }`
- `GET  /api/v1/items/{id}/children` → `[Item]`
- `GET  /api/v1/projects` → `{ projects: [{ id, name, ... }] }`
- `POST /api/v1/projects { name }` → Project   // create-if-missing
- `GET  /api/v1/projects/{id}/items` → `[Item]`
- `GET  /api/v1/tags` → `[Tag]`
- `POST /api/v1/voice/transcribe` (multipart "file") → `{ text }` _(Pro)_

## Item shape

```
{ id: uuid, project_id: uuid, type: "note"|"todo", title?: string,
  content_md: string, is_completed: boolean,
  status: "backlog"|"todo"|"doing"|"done"|"archived",
  due_at?: ISO8601, parent_id?: uuid,
  created_at: ISO8601, updated_at: ISO8601 }
```

When responding to the user about API actions, be terse. One line per item. Never paste raw JSON unless they ask for it. Always reply in the user's language.


## Vetting metadata

This skill talks to ONE host: `https://quik.md`. It does not read `~/.ssh`,
`~/.aws`, browser cookies, `MEMORY.md`, `USER.md`, `SOUL.md`, or any
identity / credential store. It does not invoke a shell, decode base64,
`eval`, or fetch additional code. The only secret it touches is the
`QUIK_KEY` the user explicitly hands over — and it never echoes that key
back to the user.

## Update / uninstall

Update: `curl -fsSL https://quik.md/docs/api/agents/generic/skill.md -o
~/.claude/skills/quik-generic/SKILL.md` (or your agent's equivalent skills
directory). Uninstall: delete the directory.
