From 4c796abd77ed9d6865748fcd86282641e9bbdd4f Mon Sep 17 00:00:00 2001 From: Blocks Task Runner Date: Thu, 1 Oct 2026 21:31:04 +0000 Subject: [PATCH 1/4] Add blocks-api skill and npx skills install scaffolding Co-Authored-By: Claude Fable 5.1 --- README.md | 51 ++++++ install.sh | 10 ++ skills/blocks-api/SKILL.md | 103 ++++++++++++ skills/blocks-api/references/sessions.md | 206 +++++++++++++++++++++++ 4 files changed, 370 insertions(+) create mode 100644 README.md create mode 100755 install.sh create mode 100644 skills/blocks-api/SKILL.md create mode 100644 skills/blocks-api/references/sessions.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b7339e --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Blocks Agent Skills + +Installable `SKILL.md` skills for working with the [Blocks](https://blocks.team) REST API from AI coding agents. + +## Install + +List the skills in this repo: + +```bash +npx skills add https://github.com/BlocksOrg/skills --list +``` + +Install every skill globally: + +```bash +npx skills add https://github.com/BlocksOrg/skills --all --global +``` + +Install a single skill: + +```bash +npx skills add https://github.com/BlocksOrg/skills --skill blocks-api --global +``` + +Omit `--global` to install into the current project instead of your home directory. + +One-liner alternative: + +```bash +curl -fsSL https://raw.githubusercontent.com/BlocksOrg/skills/main/install.sh | sh +``` + +## Skills + +| Skill | What it does | +| --- | --- | +| [`blocks-api`](skills/blocks-api/SKILL.md) | Fetch an existing Blocks session by ID, read its plan and transcript as an overview, and drill into tool calls when more detail is needed. Requires `BLOCKS_API_KEY`. | + +## Verify + +After installing, ask your agent: + +> Fetch the transcript for Blocks session `` and summarize what the agent did. + +## Source of truth + +The skills follow the public REST API docs: https://docs.blocks.team/rest-api/quick-start + +## License + +MIT diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..90d1815 --- /dev/null +++ b/install.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +if ! command -v npx >/dev/null 2>&1; then + echo "Error: npx is required but was not found on your PATH." >&2 + echo "npx ships with Node.js (npm). Install Node.js from https://nodejs.org/ (or via your package manager), then re-run this script." >&2 + exit 1 +fi + +npx skills add --all --global --yes https://github.com/BlocksOrg/skills diff --git a/skills/blocks-api/SKILL.md b/skills/blocks-api/SKILL.md new file mode 100644 index 0000000..d5b1e2e --- /dev/null +++ b/skills/blocks-api/SKILL.md @@ -0,0 +1,103 @@ +--- +name: blocks-api +description: > + Use when working with the Blocks REST API to inspect existing agent sessions: + fetch a session by ID, read its transcript (user prompts and assistant replies), + and drill into tool calls when more detail is needed. Trigger on "Blocks session", + "Blocks API", "fetch session transcript", "session ID", or any request to read + what a Blocks agent did in a session. Requires BLOCKS_API_KEY. +license: MIT +metadata: + version: "0.1.0" +--- + +## Setup + +```bash +BASE_URL="${BLOCKS_API_BASE_URL:-https://api.blocks.team}" +AUTH="Authorization: ApiKey $BLOCKS_API_KEY" # Settings > API Keys in the Blocks dashboard +``` + +All endpoints live under `/rest/v1`, return JSON, and are rate limited to 100 requests/minute per key. + +## Getting up to speed on a session + +Do these in order and stop as soon as you have enough: + +1. Fetch the session for metadata and `plan_artifact_ids`. +2. If there is a plan, read it first. It holds most of the useful information. +3. Read the transcript overview (one call). +4. Pull tool calls only if you still need to know how something was done. + +## 1. Fetch a session + +```bash +curl "$BASE_URL/rest/v1/sessions/$SESSION_ID" -H "$AUTH" +``` + +Useful fields: `title`, `pull_requests`, `source_url`, `is_archived`, +`plan_artifact_ids`, `session_html_url`, `_links.messages`. +`thread_id` and `_links.final_message` are always `null` on this endpoint. + +## 2. Read the plan, if there is one + +`plan_artifact_ids` has one entry per plan the agent wrote, already pointing +at that plan's latest version, oldest plan first. Take the last entry. + +```bash +PLAN_ARTIFACT_ID=$(curl -s "$BASE_URL/rest/v1/sessions/$SESSION_ID" -H "$AUTH" | jq -r '.plan_artifact_ids[-1]') +PLAN_URL=$(curl -s "$BASE_URL/rest/v1/sessions/$SESSION_ID/artifacts/$PLAN_ARTIFACT_ID" -H "$AUTH" | jq -r '.url') +curl -s "$PLAN_URL" # presigned, valid 6 days, NO Authorization header +``` + +The artifact response includes `kind` (`plan` or `file`), `file_name`, +`mime_type`, `plan_id`, `created_at`. Confirm `kind == "plan"`. +Skip this step when `plan_artifact_ids` is empty. + +## 3. Transcript overview + +```bash +curl "$BASE_URL/rest/v1/sessions/$SESSION_ID/messages?type=message&role=user&role=assistant" -H "$AUTH" +``` + +- Returns user prompts and assistant messages, oldest first. Do not pass `direction`. +- Each thread (`chat_thread_id`) is one user turn. The last assistant `message` + in a finished thread is its final reply. +- Skip items with `chat_thread_id: null` (system notices). +- A thread whose last item is the user prompt is still running. + +Why `type=message` and not the API default (`message,final_message`): +the last `message` of a finished thread is identical to its `final_message`, +so the default doubles the final text. + +**Fallback if `role=user&role=assistant` returns 422** (older deployments): +make two calls, `?type=message&role=user` and `?type=message`, merge by `ts`. + +## 4. More detail, only when needed + +- **Tool calls:** add `type=tool_call`, e.g. `?type=message&type=tool_call&role=user&role=assistant`. + `message` is a JSON string `{"__name__": "", "input": ""}`. Invocation only, no result. +- **Outcomes only:** `?type=final_message` gives one completed reply per thread. +- **One thread:** `?thread_id=` or `/sessions/{id}/threads/{thread_id}/messages`. + +## Pagination and polling + +`page`, `limit` (1-100, default 50), `meta.total_pages`. +`_links.new_messages.href` carries the `gts` cursor for incremental reads, but only the cursor. +Both `_links` drop your filters. Rebuild pagination and polling URLs yourself. + +## Errors + +| Status | Meaning | +| --- | --- | +| 401 | Missing or malformed `Authorization` header | +| 403 | Bad key, or key for a different environment | +| 404 | Session not in your workspace, or artifact not attached to it | +| 422 | Bad `limit`, `page`, `type`, `role`, `direction`, non-UUID id, comma lists | + +Array params repeat the key (`type=a&type=b`). Unknown thread id returns 200 with empty `items`. + +## Reference + +`references/sessions.md` has full field tables, Python recipes, and verified quirks. +Docs: https://docs.blocks.team/rest-api/sessions/get, .../sessions/messages, .../sessions/artifacts diff --git a/skills/blocks-api/references/sessions.md b/skills/blocks-api/references/sessions.md new file mode 100644 index 0000000..5e6d185 --- /dev/null +++ b/skills/blocks-api/references/sessions.md @@ -0,0 +1,206 @@ +# Sessions reference + +Detail for the `blocks-api` skill: query parameters, response fields, Python +recipes, and quirks verified against the live API. One file per API area; +this one covers sessions, messages, and artifacts. + +All requests: `Authorization: ApiKey $BLOCKS_API_KEY`, base URL +`https://api.blocks.team`, paths under `/rest/v1`. + +## Endpoints + +| Method | Path | Purpose | +| --- | --- | --- | +| GET | `/sessions/{session_id}` | Session metadata and `plan_artifact_ids` | +| GET | `/sessions/{session_id}/messages` | All messages on a session | +| GET | `/sessions/{session_id}/threads/{thread_id}/messages` | Messages on one thread | +| GET | `/sessions/{session_id}/artifacts/{artifact_id}` | Download link for a plan or file | + +## Query parameters for `/messages` + +Both message endpoints accept the same parameters. Array parameters repeat the +key: `type=message&type=tool_call`. Comma lists (`type=a,b`) return 422 and the +bracket form (`type[]=a`) is silently ignored. + +| Param | Type | Default | Allowed | Notes | +| --- | --- | --- | --- | --- | +| `type` | string[] | `message`, `final_message` | `message`, `final_message`, `tool_call` | `tool_call` must be requested explicitly | +| `role` | string[] | `assistant` | `user`, `assistant` | Repeatable. Older deployments accept one value only and return 422 when repeated | +| `thread_id` | uuid | none | | Ignored on the threaded URL form. Unknown id returns 200 with empty `items` | +| `gts` | number | none | epoch seconds | Returns messages with `ts` strictly greater than the cursor | +| `page` | number | `1` | >= 1 | 1-indexed | +| `limit` | number | `50` | 1 to 100 | | +| `direction` | string | `asc` | `asc`, `desc` | Sorted by `ts`. Omit it for transcript reads | + +## Session object + +`GET /sessions/{session_id}` + +| Field | Type | Notes | +| --- | --- | --- | +| `id` | uuid | | +| `title` | string | | +| `pull_requests` | object[] | PRs the session opened, each with `url` and `status` | +| `source_url` | string or null | Where the session was started from, if any | +| `is_archived` | boolean | | +| `is_private` | boolean | | +| `plan_artifact_ids` | uuid[] | Latest artifact for each plan the agent wrote, oldest plan first. Empty when no plan | +| `session_group_id` | uuid or null | Present, not yet documented publicly | +| `session_html_url` | string | Link to the session in the Blocks dashboard | +| `thread_id` | null | Always `null` on this endpoint | +| `created_at`, `updated_at` | ISO 8601 | | +| `_links.self` | object | `href` to this session | +| `_links.messages` | object | `href` to the session's messages | +| `_links.thread`, `_links.final_message` | null | Always `null` on this endpoint | + +## Message object + +Items in `/messages` responses. + +| Field | Type | Notes | +| --- | --- | --- | +| `id` | uuid | | +| `chat_id` | uuid | The session | +| `chat_thread_id` | uuid or null | One thread per user turn. `null` for system notices, which you can skip | +| `task_id` | uuid | Agent invocation that produced the message | +| `role` | string | `user` or `assistant` | +| `type` | string | `message`, `final_message`, or `tool_call` | +| `message` | string | Body. For `tool_call` it is a JSON-encoded string `{"__name__": "", "input": ""}` with no result | +| `ts` | number or null | Epoch seconds, may be fractional. Use as the `gts` cursor | +| `created_at`, `updated_at` | ISO 8601 | | + +Response envelope: `items`, `meta` (`total`, `page`, `limit`, `total_pages`), +`_links.self` and `_links.new_messages` (the latter carries the `gts` cursor only). + +## Artifact object + +`GET /sessions/{session_id}/artifacts/{artifact_id}` + +| Field | Type | Notes | +| --- | --- | --- | +| `id` | uuid | | +| `url` | string | Presigned download link, valid 6 days. Fetch without the `Authorization` header | +| `file_name` | string | e.g. `plan.md` | +| `file_path` | string | Path inside the agent's sandbox when captured | +| `file_size_in_kb` | number | | +| `mime_type` | string | e.g. `text/markdown` | +| `kind` | string | `plan` or `file` | +| `plan_id` | uuid or null | Groups iterations of one plan. `null` when `kind` is `file` | +| `created_at` | ISO 8601 | | + +There is no list endpoint. Artifact IDs come from the session's +`plan_artifact_ids`. Older iterations of a plan stay downloadable by their own ID. + +## Python recipes + +```python +import json, os, requests + +BASE_URL = os.environ.get("BLOCKS_API_BASE_URL", "https://api.blocks.team") +HEADERS = {"Authorization": f"ApiKey {os.environ['BLOCKS_API_KEY']}"} + + +def get(path, **params): + r = requests.get(f"{BASE_URL}/rest/v1{path}", headers=HEADERS, params=params) + r.raise_for_status() + return r.json() + + +def all_items(path, **params): + """Follow page/total_pages. Pass list values to repeat a key.""" + page = 1 + while True: + body = get(path, page=page, limit=100, **params) + yield from body["items"] + if page >= body["meta"]["total_pages"]: + return + page += 1 +``` + +### Fetch both roles, with the fallback for older deployments + +```python +def messages(session_id, types): + """User and assistant items of the given type(s), oldest first.""" + path = f"/sessions/{session_id}/messages" + try: + return list(all_items(path, type=types, role=["user", "assistant"])) + except requests.HTTPError as e: + if e.response.status_code != 422: + raise + # Older deployments: role is single-valued. Two calls, merge by ts. + users = all_items(path, type=types, role="user") + assistants = all_items(path, type=types) + return sorted([*users, *assistants], key=lambda m: m["ts"] or 0) +``` + +### Get up to speed on a session + +```python +def catch_up(session_id): + session = get(f"/sessions/{session_id}") + + plan = None + if session.get("plan_artifact_ids"): + artifact = get(f"/sessions/{session_id}/artifacts/{session['plan_artifact_ids'][-1]}") + if artifact["kind"] == "plan": + plan = requests.get(artifact["url"]).text # presigned, no auth header + + transcript = [m for m in messages(session_id, "message") if m["chat_thread_id"] is not None] + return session, plan, transcript +``` + +### Full transcript with tool calls decoded + +```python +def full_transcript(session_id): + for m in messages(session_id, ["message", "tool_call"]): + if m["type"] == "tool_call": + call = json.loads(m["message"]) + name = call.pop("__name__", "?") + yield m["chat_thread_id"], "tool", f"{name} {json.dumps(call)[:200]}" + else: + yield m["chat_thread_id"], m["role"], m["message"] +``` + +### Poll for new messages + +```python +import time +from urllib.parse import parse_qs, urlparse + + +def poll(session_id, thread_id=None, interval=5, timeout=600): + """Yield new final_message items as they arrive. Returns on timeout.""" + params = {"type": "final_message"} + if thread_id: + params["thread_id"] = thread_id + deadline = time.time() + timeout + while time.time() < deadline: + body = get(f"/sessions/{session_id}/messages", **params) + yield from body["items"] + # new_messages.href carries only the gts cursor, so keep your own filters. + href = body["_links"]["new_messages"]["href"] + params["gts"] = parse_qs(urlparse(href).query)["gts"][0] + time.sleep(interval) +``` + +## Verified quirks + +Observed on live sessions produced by two different agents. + +- Validation failures return 422, not 400: bad `limit`, `page`, `type`, `role`, + `direction`, non-UUID ids, and comma-separated lists. +- Default sort is `ts` ascending when `direction` is omitted. `direction=desc` flips it. +- `_links.self.href` drops the query parameters you sent, and `_links.new_messages.href` + keeps only `gts`. Rebuild pagination and polling URLs with your own filters. +- In every finished thread the last `message` equals the `final_message` exactly, + so the default `type` filter returns the final text twice. +- Each thread has exactly one `role=user` item, of type `message`. +- Items with `chat_thread_id: null` are system notices (for example sandbox + lifecycle warnings), not part of the conversation. +- An unknown `thread_id` filter returns 200 with empty `items`, not 404. +- `tool_call.message` holds only the invocation; tool results are not exposed. +- No rate-limit headers are returned. The limit is 100 requests per minute per key. +- Missing `Authorization` header returns 401; a wrong key, or a key for a + different environment, returns 403. From 2ad44b23a380588ea11c885b59cfcc147971a266 Mon Sep 17 00:00:00 2001 From: Blocks Task Runner Date: Thu, 1 Oct 2026 21:39:38 +0000 Subject: [PATCH 2/4] Restructure blocks-api: SKILL.md as index, walkthrough in references/sessions-get-overview.md Drop references/sessions.md, which duplicated the public REST docs. Co-Authored-By: Claude Fable 5.1 --- skills/blocks-api/SKILL.md | 77 +------ .../references/sessions-get-overview.md | 74 +++++++ skills/blocks-api/references/sessions.md | 206 ------------------ 3 files changed, 81 insertions(+), 276 deletions(-) create mode 100644 skills/blocks-api/references/sessions-get-overview.md delete mode 100644 skills/blocks-api/references/sessions.md diff --git a/skills/blocks-api/SKILL.md b/skills/blocks-api/SKILL.md index d5b1e2e..cc6c5e2 100644 --- a/skills/blocks-api/SKILL.md +++ b/skills/blocks-api/SKILL.md @@ -19,72 +19,16 @@ AUTH="Authorization: ApiKey $BLOCKS_API_KEY" # Settings > API Keys in the Bloc ``` All endpoints live under `/rest/v1`, return JSON, and are rate limited to 100 requests/minute per key. +Array query params repeat the key (`type=a&type=b`); comma lists are rejected. +Endpoint reference: https://docs.blocks.team/rest-api/quick-start -## Getting up to speed on a session +## Skills -Do these in order and stop as soon as you have enough: +Read the file for the task at hand. Each one is a complete, ordered walkthrough. -1. Fetch the session for metadata and `plan_artifact_ids`. -2. If there is a plan, read it first. It holds most of the useful information. -3. Read the transcript overview (one call). -4. Pull tool calls only if you still need to know how something was done. - -## 1. Fetch a session - -```bash -curl "$BASE_URL/rest/v1/sessions/$SESSION_ID" -H "$AUTH" -``` - -Useful fields: `title`, `pull_requests`, `source_url`, `is_archived`, -`plan_artifact_ids`, `session_html_url`, `_links.messages`. -`thread_id` and `_links.final_message` are always `null` on this endpoint. - -## 2. Read the plan, if there is one - -`plan_artifact_ids` has one entry per plan the agent wrote, already pointing -at that plan's latest version, oldest plan first. Take the last entry. - -```bash -PLAN_ARTIFACT_ID=$(curl -s "$BASE_URL/rest/v1/sessions/$SESSION_ID" -H "$AUTH" | jq -r '.plan_artifact_ids[-1]') -PLAN_URL=$(curl -s "$BASE_URL/rest/v1/sessions/$SESSION_ID/artifacts/$PLAN_ARTIFACT_ID" -H "$AUTH" | jq -r '.url') -curl -s "$PLAN_URL" # presigned, valid 6 days, NO Authorization header -``` - -The artifact response includes `kind` (`plan` or `file`), `file_name`, -`mime_type`, `plan_id`, `created_at`. Confirm `kind == "plan"`. -Skip this step when `plan_artifact_ids` is empty. - -## 3. Transcript overview - -```bash -curl "$BASE_URL/rest/v1/sessions/$SESSION_ID/messages?type=message&role=user&role=assistant" -H "$AUTH" -``` - -- Returns user prompts and assistant messages, oldest first. Do not pass `direction`. -- Each thread (`chat_thread_id`) is one user turn. The last assistant `message` - in a finished thread is its final reply. -- Skip items with `chat_thread_id: null` (system notices). -- A thread whose last item is the user prompt is still running. - -Why `type=message` and not the API default (`message,final_message`): -the last `message` of a finished thread is identical to its `final_message`, -so the default doubles the final text. - -**Fallback if `role=user&role=assistant` returns 422** (older deployments): -make two calls, `?type=message&role=user` and `?type=message`, merge by `ts`. - -## 4. More detail, only when needed - -- **Tool calls:** add `type=tool_call`, e.g. `?type=message&type=tool_call&role=user&role=assistant`. - `message` is a JSON string `{"__name__": "", "input": ""}`. Invocation only, no result. -- **Outcomes only:** `?type=final_message` gives one completed reply per thread. -- **One thread:** `?thread_id=` or `/sessions/{id}/threads/{thread_id}/messages`. - -## Pagination and polling - -`page`, `limit` (1-100, default 50), `meta.total_pages`. -`_links.new_messages.href` carries the `gts` cursor for incremental reads, but only the cursor. -Both `_links` drop your filters. Rebuild pagination and polling URLs yourself. +| Skill | What it covers | +| --- | --- | +| [`references/sessions-get-overview.md`](references/sessions-get-overview.md) | Get up to speed on an existing session by ID. Fetch its metadata, download the latest plan the agent wrote, read the transcript as a compact overview of user prompts and assistant replies, then pull tool calls, single threads, or new messages only when you need more detail. | ## Errors @@ -94,10 +38,3 @@ Both `_links` drop your filters. Rebuild pagination and polling URLs yourself. | 403 | Bad key, or key for a different environment | | 404 | Session not in your workspace, or artifact not attached to it | | 422 | Bad `limit`, `page`, `type`, `role`, `direction`, non-UUID id, comma lists | - -Array params repeat the key (`type=a&type=b`). Unknown thread id returns 200 with empty `items`. - -## Reference - -`references/sessions.md` has full field tables, Python recipes, and verified quirks. -Docs: https://docs.blocks.team/rest-api/sessions/get, .../sessions/messages, .../sessions/artifacts diff --git a/skills/blocks-api/references/sessions-get-overview.md b/skills/blocks-api/references/sessions-get-overview.md new file mode 100644 index 0000000..b659ae3 --- /dev/null +++ b/skills/blocks-api/references/sessions-get-overview.md @@ -0,0 +1,74 @@ +# Getting up to speed on a session + +Assumes `BASE_URL` and `AUTH` from the Setup section of `SKILL.md`. + +Do these in order and stop as soon as you have enough: + +1. Fetch the session for metadata and `plan_artifact_ids`. +2. If there is a plan, read it first. It holds most of the useful information. +3. Read the transcript overview (one call). +4. Pull tool calls only if you still need to know how something was done. + +## 1. Fetch a session + +```bash +curl "$BASE_URL/rest/v1/sessions/$SESSION_ID" -H "$AUTH" +``` + +Useful fields: `title`, `pull_requests`, `source_url`, `is_archived`, +`plan_artifact_ids`, `session_html_url`, `_links.messages`. +`thread_id` and `_links.final_message` are always `null` on this endpoint. + +## 2. Read the plan, if there is one + +`plan_artifact_ids` has one entry per plan the agent wrote, already pointing +at that plan's latest version, oldest plan first. Take the last entry. + +```bash +PLAN_ARTIFACT_ID=$(curl -s "$BASE_URL/rest/v1/sessions/$SESSION_ID" -H "$AUTH" | jq -r '.plan_artifact_ids[-1]') +PLAN_URL=$(curl -s "$BASE_URL/rest/v1/sessions/$SESSION_ID/artifacts/$PLAN_ARTIFACT_ID" -H "$AUTH" | jq -r '.url') +curl -s "$PLAN_URL" # presigned, valid 6 days, NO Authorization header +``` + +The artifact response includes `kind` (`plan` or `file`), `file_name`, +`mime_type`, `plan_id`, `created_at`. Confirm `kind == "plan"`. +Skip this step when `plan_artifact_ids` is empty. + +## 3. Transcript overview + +```bash +curl "$BASE_URL/rest/v1/sessions/$SESSION_ID/messages?type=message&role=user&role=assistant" -H "$AUTH" +``` + +- Returns user prompts and assistant messages, oldest first. Do not pass `direction`. +- Each thread (`chat_thread_id`) is one user turn. The last assistant `message` + in a finished thread is its final reply. +- Skip items with `chat_thread_id: null` (system notices). +- A thread whose last item is the user prompt is still running. + +Why `type=message` and not the API default (`message,final_message`): +the last `message` of a finished thread is identical to its `final_message`, +so the default doubles the final text. + +**Fallback if `role=user&role=assistant` returns 422** (older deployments): +make two calls, `?type=message&role=user` and `?type=message`, merge by `ts`. + +## 4. More detail, only when needed + +- **Tool calls:** add `type=tool_call`, e.g. `?type=message&type=tool_call&role=user&role=assistant`. + `message` is a JSON string `{"__name__": "", "input": ""}`. Invocation only, no result. +- **Outcomes only:** `?type=final_message` gives one completed reply per thread. +- **One thread:** `?thread_id=` or `/sessions/{id}/threads/{thread_id}/messages`. + +## Pagination and polling + +`page`, `limit` (1-100, default 50), `meta.total_pages`. +`_links.new_messages.href` carries the `gts` cursor for incremental reads, but only the cursor. +Both `_links` drop your filters. Rebuild pagination and polling URLs yourself. +Unknown thread id returns 200 with empty `items`. + +## Docs + +https://docs.blocks.team/rest-api/sessions/get +https://docs.blocks.team/rest-api/sessions/messages +https://docs.blocks.team/rest-api/sessions/artifacts diff --git a/skills/blocks-api/references/sessions.md b/skills/blocks-api/references/sessions.md deleted file mode 100644 index 5e6d185..0000000 --- a/skills/blocks-api/references/sessions.md +++ /dev/null @@ -1,206 +0,0 @@ -# Sessions reference - -Detail for the `blocks-api` skill: query parameters, response fields, Python -recipes, and quirks verified against the live API. One file per API area; -this one covers sessions, messages, and artifacts. - -All requests: `Authorization: ApiKey $BLOCKS_API_KEY`, base URL -`https://api.blocks.team`, paths under `/rest/v1`. - -## Endpoints - -| Method | Path | Purpose | -| --- | --- | --- | -| GET | `/sessions/{session_id}` | Session metadata and `plan_artifact_ids` | -| GET | `/sessions/{session_id}/messages` | All messages on a session | -| GET | `/sessions/{session_id}/threads/{thread_id}/messages` | Messages on one thread | -| GET | `/sessions/{session_id}/artifacts/{artifact_id}` | Download link for a plan or file | - -## Query parameters for `/messages` - -Both message endpoints accept the same parameters. Array parameters repeat the -key: `type=message&type=tool_call`. Comma lists (`type=a,b`) return 422 and the -bracket form (`type[]=a`) is silently ignored. - -| Param | Type | Default | Allowed | Notes | -| --- | --- | --- | --- | --- | -| `type` | string[] | `message`, `final_message` | `message`, `final_message`, `tool_call` | `tool_call` must be requested explicitly | -| `role` | string[] | `assistant` | `user`, `assistant` | Repeatable. Older deployments accept one value only and return 422 when repeated | -| `thread_id` | uuid | none | | Ignored on the threaded URL form. Unknown id returns 200 with empty `items` | -| `gts` | number | none | epoch seconds | Returns messages with `ts` strictly greater than the cursor | -| `page` | number | `1` | >= 1 | 1-indexed | -| `limit` | number | `50` | 1 to 100 | | -| `direction` | string | `asc` | `asc`, `desc` | Sorted by `ts`. Omit it for transcript reads | - -## Session object - -`GET /sessions/{session_id}` - -| Field | Type | Notes | -| --- | --- | --- | -| `id` | uuid | | -| `title` | string | | -| `pull_requests` | object[] | PRs the session opened, each with `url` and `status` | -| `source_url` | string or null | Where the session was started from, if any | -| `is_archived` | boolean | | -| `is_private` | boolean | | -| `plan_artifact_ids` | uuid[] | Latest artifact for each plan the agent wrote, oldest plan first. Empty when no plan | -| `session_group_id` | uuid or null | Present, not yet documented publicly | -| `session_html_url` | string | Link to the session in the Blocks dashboard | -| `thread_id` | null | Always `null` on this endpoint | -| `created_at`, `updated_at` | ISO 8601 | | -| `_links.self` | object | `href` to this session | -| `_links.messages` | object | `href` to the session's messages | -| `_links.thread`, `_links.final_message` | null | Always `null` on this endpoint | - -## Message object - -Items in `/messages` responses. - -| Field | Type | Notes | -| --- | --- | --- | -| `id` | uuid | | -| `chat_id` | uuid | The session | -| `chat_thread_id` | uuid or null | One thread per user turn. `null` for system notices, which you can skip | -| `task_id` | uuid | Agent invocation that produced the message | -| `role` | string | `user` or `assistant` | -| `type` | string | `message`, `final_message`, or `tool_call` | -| `message` | string | Body. For `tool_call` it is a JSON-encoded string `{"__name__": "", "input": ""}` with no result | -| `ts` | number or null | Epoch seconds, may be fractional. Use as the `gts` cursor | -| `created_at`, `updated_at` | ISO 8601 | | - -Response envelope: `items`, `meta` (`total`, `page`, `limit`, `total_pages`), -`_links.self` and `_links.new_messages` (the latter carries the `gts` cursor only). - -## Artifact object - -`GET /sessions/{session_id}/artifacts/{artifact_id}` - -| Field | Type | Notes | -| --- | --- | --- | -| `id` | uuid | | -| `url` | string | Presigned download link, valid 6 days. Fetch without the `Authorization` header | -| `file_name` | string | e.g. `plan.md` | -| `file_path` | string | Path inside the agent's sandbox when captured | -| `file_size_in_kb` | number | | -| `mime_type` | string | e.g. `text/markdown` | -| `kind` | string | `plan` or `file` | -| `plan_id` | uuid or null | Groups iterations of one plan. `null` when `kind` is `file` | -| `created_at` | ISO 8601 | | - -There is no list endpoint. Artifact IDs come from the session's -`plan_artifact_ids`. Older iterations of a plan stay downloadable by their own ID. - -## Python recipes - -```python -import json, os, requests - -BASE_URL = os.environ.get("BLOCKS_API_BASE_URL", "https://api.blocks.team") -HEADERS = {"Authorization": f"ApiKey {os.environ['BLOCKS_API_KEY']}"} - - -def get(path, **params): - r = requests.get(f"{BASE_URL}/rest/v1{path}", headers=HEADERS, params=params) - r.raise_for_status() - return r.json() - - -def all_items(path, **params): - """Follow page/total_pages. Pass list values to repeat a key.""" - page = 1 - while True: - body = get(path, page=page, limit=100, **params) - yield from body["items"] - if page >= body["meta"]["total_pages"]: - return - page += 1 -``` - -### Fetch both roles, with the fallback for older deployments - -```python -def messages(session_id, types): - """User and assistant items of the given type(s), oldest first.""" - path = f"/sessions/{session_id}/messages" - try: - return list(all_items(path, type=types, role=["user", "assistant"])) - except requests.HTTPError as e: - if e.response.status_code != 422: - raise - # Older deployments: role is single-valued. Two calls, merge by ts. - users = all_items(path, type=types, role="user") - assistants = all_items(path, type=types) - return sorted([*users, *assistants], key=lambda m: m["ts"] or 0) -``` - -### Get up to speed on a session - -```python -def catch_up(session_id): - session = get(f"/sessions/{session_id}") - - plan = None - if session.get("plan_artifact_ids"): - artifact = get(f"/sessions/{session_id}/artifacts/{session['plan_artifact_ids'][-1]}") - if artifact["kind"] == "plan": - plan = requests.get(artifact["url"]).text # presigned, no auth header - - transcript = [m for m in messages(session_id, "message") if m["chat_thread_id"] is not None] - return session, plan, transcript -``` - -### Full transcript with tool calls decoded - -```python -def full_transcript(session_id): - for m in messages(session_id, ["message", "tool_call"]): - if m["type"] == "tool_call": - call = json.loads(m["message"]) - name = call.pop("__name__", "?") - yield m["chat_thread_id"], "tool", f"{name} {json.dumps(call)[:200]}" - else: - yield m["chat_thread_id"], m["role"], m["message"] -``` - -### Poll for new messages - -```python -import time -from urllib.parse import parse_qs, urlparse - - -def poll(session_id, thread_id=None, interval=5, timeout=600): - """Yield new final_message items as they arrive. Returns on timeout.""" - params = {"type": "final_message"} - if thread_id: - params["thread_id"] = thread_id - deadline = time.time() + timeout - while time.time() < deadline: - body = get(f"/sessions/{session_id}/messages", **params) - yield from body["items"] - # new_messages.href carries only the gts cursor, so keep your own filters. - href = body["_links"]["new_messages"]["href"] - params["gts"] = parse_qs(urlparse(href).query)["gts"][0] - time.sleep(interval) -``` - -## Verified quirks - -Observed on live sessions produced by two different agents. - -- Validation failures return 422, not 400: bad `limit`, `page`, `type`, `role`, - `direction`, non-UUID ids, and comma-separated lists. -- Default sort is `ts` ascending when `direction` is omitted. `direction=desc` flips it. -- `_links.self.href` drops the query parameters you sent, and `_links.new_messages.href` - keeps only `gts`. Rebuild pagination and polling URLs with your own filters. -- In every finished thread the last `message` equals the `final_message` exactly, - so the default `type` filter returns the final text twice. -- Each thread has exactly one `role=user` item, of type `message`. -- Items with `chat_thread_id: null` are system notices (for example sandbox - lifecycle warnings), not part of the conversation. -- An unknown `thread_id` filter returns 200 with empty `items`, not 404. -- `tool_call.message` holds only the invocation; tool results are not exposed. -- No rate-limit headers are returned. The limit is 100 requests per minute per key. -- Missing `Authorization` header returns 401; a wrong key, or a key for a - different environment, returns 403. From 2e36fc5c00a0620eca4568e019a8e92654b97166 Mon Sep 17 00:00:00 2001 From: Blocks Task Runner Date: Thu, 1 Oct 2026 21:44:27 +0000 Subject: [PATCH 3/4] Tighten blocks-api trigger description Co-Authored-By: Claude Fable 5.1 --- skills/blocks-api/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/blocks-api/SKILL.md b/skills/blocks-api/SKILL.md index cc6c5e2..bb222f2 100644 --- a/skills/blocks-api/SKILL.md +++ b/skills/blocks-api/SKILL.md @@ -1,11 +1,11 @@ --- name: blocks-api description: > - Use when working with the Blocks REST API to inspect existing agent sessions: + Use when working with the `Blocks` to inspect existing agent sessions: fetch a session by ID, read its transcript (user prompts and assistant replies), and drill into tool calls when more detail is needed. Trigger on "Blocks session", - "Blocks API", "fetch session transcript", "session ID", or any request to read - what a Blocks agent did in a session. Requires BLOCKS_API_KEY. + "Blocks API", "fetch blocks session transcript" or any request to read what a + Blocks agent did in a session. Requires BLOCKS_API_KEY. license: MIT metadata: version: "0.1.0" From e98336304c37cdf0e4444d21e6294cb9c03fcd68 Mon Sep 17 00:00:00 2001 From: "cubeorgdev[bot]" <194556374+cubeorgdev[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:11:25 -0700 Subject: [PATCH 4/4] ci: add CI, CodeQL, issue and PR templates, SECURITY.md, and README header (#2) Mirrors the repository scaffolding of blocks-ci-sessions. The CI job names are the required status checks on main. Co-authored-by: Blocks Task Runner Co-authored-by: Claude Fable 5.1 --- .github/ISSUE_TEMPLATE/bug_report.yml | 37 +++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 27 +++++++ .github/ISSUE_TEMPLATE/question.yml | 14 ++++ .github/assets/blocks-logo-dark.svg | 10 +++ .github/assets/blocks-logo-light.svg | 10 +++ .github/pull_request_template.md | 20 +++++ .github/workflows/ci.yml | 90 ++++++++++++++++++++++ .github/workflows/codeql.yml | 37 +++++++++ README.md | 12 +++ SECURITY.md | 26 +++++++ 11 files changed, 291 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/assets/blocks-logo-dark.svg create mode 100644 .github/assets/blocks-logo-light.svg create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..95cb3a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,37 @@ +name: Bug report +description: A skill gives wrong instructions or an example does not work +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Search existing issues first. Keep it short and specific. + - type: input + id: skill + attributes: + label: Skill + description: The skill directory, e.g. `blocks-api`, and the reference file if relevant. + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened + description: What went wrong, and what did you expect instead? + validations: + required: true + - type: textarea + id: request + attributes: + label: Request and response + description: The API call you made and what came back. Reference the key, never paste it. + render: text + validations: + required: false + - type: input + id: agent + attributes: + label: Agent and installer + description: e.g. `Claude Code`, installed with `npx skills add --global`. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..fd74836 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Blocks documentation + url: https://docs.blocks.team + about: Platform docs, including the REST API these skills call. + - name: Report a security vulnerability + url: https://github.com/BlocksOrg/skills/security/policy + about: Report vulnerabilities privately — do not open a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..b5a7058 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,27 @@ +name: Feature request +description: Suggest a new skill or an addition to an existing one +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem does this solve? + description: What are you trying to get an agent to do with Blocks that the skills make hard or impossible today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: A new skill, a new reference playbook, a changed recommendation — whatever you have in mind. + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Before submitting + options: + - label: I searched existing issues and this isn't a duplicate. + required: true + - label: I checked the existing skills and the Blocks docs and this isn't already covered. + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..15991c6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,14 @@ +name: Question +description: Ask a question about using the skills +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Check the [README](https://github.com/BlocksOrg/skills#readme) and the [Blocks docs](https://docs.blocks.team) first. For security reports, see SECURITY.md — do not open a public issue. + - type: textarea + id: question + attributes: + label: Your question + validations: + required: true diff --git a/.github/assets/blocks-logo-dark.svg b/.github/assets/blocks-logo-dark.svg new file mode 100644 index 0000000..e698bb8 --- /dev/null +++ b/.github/assets/blocks-logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.github/assets/blocks-logo-light.svg b/.github/assets/blocks-logo-light.svg new file mode 100644 index 0000000..2d691d3 --- /dev/null +++ b/.github/assets/blocks-logo-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..aeb74cb --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Summary + + + +## Linked issue + +Fixes # + +## How I verified + + + +## Checklist + +- [ ] `npx skills add . --list` lists every skill under `skills/` +- [ ] Frontmatter `name` matches the skill directory and `description` says when to trigger +- [ ] Examples use `https://api.blocks.team` and `$BLOCKS_API_KEY`; no real keys or internal hosts +- [ ] Docs links resolve and the skill does not restate what the public docs already cover +- [ ] PR title follows Conventional Commits (`type(scope): summary`) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e9d4d34 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + # Lints the workflow files themselves: unknown runner labels, bad expressions, + # shellcheck over every `run:` block. A workflow that fails to parse posts no + # check-run at all, so nothing else in this file would ever catch it. + actionlint: + name: Lint workflows + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + ACTIONLINT_VERSION: 1.7.12 + # sha256 of actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz, from the + # release's published checksums file. + ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - name: Install actionlint + run: | + tarball="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + curl -fsSL -o "$tarball" \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${tarball}" + echo "${ACTIONLINT_SHA256} ${tarball}" | sha256sum --check --strict - + tar -xzf "$tarball" actionlint + - run: ./actionlint -color + + # Every skill must be discoverable by `npx skills add`, its frontmatter name + # must match its directory, and nothing in the repo may leak a real key or an + # internal host. The install script must at least parse. + validate: + name: Validate skills + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + SKILLS_CLI_VERSION: 1.7.0 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: 24 + - name: Frontmatter name matches directory + run: | + set -euo pipefail + status=0 + for file in skills/*/SKILL.md; do + dir=$(basename "$(dirname "$file")") + name=$(sed -n 's/^name:[[:space:]]*//p' "$file" | head -n1 | tr -d '"'"'"'') + if [ "$name" != "$dir" ]; then + echo "::error file=$file::frontmatter name '$name' does not match directory '$dir'" + status=1 + fi + if ! grep -q '^description:' "$file"; then + echo "::error file=$file::frontmatter is missing a description" + status=1 + fi + done + exit "$status" + - name: skills CLI discovers every skill + run: | + set -euo pipefail + listing=$(npx --yes "skills@${SKILLS_CLI_VERSION}" add . --list 2>&1 | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g') + echo "$listing" + status=0 + for dir in skills/*/; do + name=$(basename "$dir") + if ! printf '%s\n' "$listing" | grep -qE "^[^A-Za-z0-9_-]*${name}[[:space:]]*$"; then + echo "::error::skills CLI did not list '$name'" + status=1 + fi + done + exit "$status" + - name: No real keys or internal hosts + run: | + set -euo pipefail + if grep -rnE 'ApiKey [A-Za-z0-9_-]{30,}|api\.dev\.blocks\.team|appdev\.blocks\.team' \ + --exclude-dir=.git --exclude-dir=.github .; then + echo "::error::found what looks like a real API key or an internal host" + exit 1 + fi + - name: install.sh parses + run: sh -n install.sh diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4d5aa23 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,37 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: '23 5 * * 1' + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze: + # The check-run is always named "CodeQL", whether it runs or is skipped, so + # it can be a required status check. + name: CodeQL + # Code scanning on a private or internal repository needs GitHub Advanced + # Security. Skip until the repo is public: a skipped job still posts a + # check-run, which counts as passing for a required status check. + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 + with: + # This repo ships markdown and workflow files; only the workflows are code. + languages: actions + - uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 diff --git a/README.md b/README.md index 9b7339e..f52727e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,17 @@ +

+ + + + Blocks + + +

+ # Blocks Agent Skills +[![CI](https://github.com/BlocksOrg/skills/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/BlocksOrg/skills/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) + Installable `SKILL.md` skills for working with the [Blocks](https://blocks.team) REST API from AI coding agents. ## Install diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e477215 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security + +## What this repository handles + +- This repository ships markdown skill files and a shell installer. Nothing here runs on + its own; an AI coding agent reads the skills and decides what to do. +- **`BLOCKS_API_KEY`** is the only secret the skills refer to. It stays in your environment + and is sent only as an `Authorization: ApiKey` header to `https://api.blocks.team`. The + skills never ask an agent to print it, write it to disk, or send it anywhere else. +- Presigned artifact URLs returned by the API are fetched without the `Authorization` + header, as the skills state, so the key is never sent to a storage host. +- `install.sh` runs `npx skills add` against this repository and nothing else. CI checks + that every example uses the public host and a placeholder key. + +## Trust boundaries + +- `main` requires review and passing checks, so what a user installs is what was reviewed. +- What an agent does with a session it reads is governed by the agent, its configuration, + and the credentials it holds. These skills only describe how to call the API. + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately via +[GitHub's private vulnerability reporting](https://github.com/BlocksOrg/skills/security/advisories/new) +rather than opening a public issue. Include a description, reproduction steps, and impact. +We aim to acknowledge reports within a few business days.