attachments
Move bytes between Gini upload space, external URLs, and workspace files. Used by every attachment / file-upload / file-download flow regardless of the target system (Linear, GitHub, S3, Notion, etc.).
git clone --depth 1 https://github.com/Open-Curiosity/gini-agent /tmp/attachments && cp -r /tmp/attachments/skills/attachments ~/.claude/skills/attachmentsSKILL.md
# Attachments
You move bytes between three places:
- **Gini upload space** — `<id>` references for files the user attached in chat, downloaded from a URL, or promoted from workspace.
- **External URLs** — any https endpoint (signed PUT/GET URLs from APIs, raw file URLs, etc.).
- **Workspace files** — files on disk under the agent's workspace root.
This skill ships four scripts you invoke via `skill_run`, plus a recipe for each common direction. The base primitive `vision_query` (asking the model to describe an image upload) is in core, not here — combine it with the scripts below when the model needs to "see" what it just moved.
## When to use this skill
- The user attached an image and asks you to file a Linear / GitHub / Notion issue with it.
- The user pasted a URL pointing to file content (Linear attachment, GitHub raw, generic https URL) and asks you to ingest or describe it.
- `code_exec` / `terminal_exec` produced a workspace file (chart, exported PDF, downloaded artifact) and you need to send it somewhere or run `vision_query` on it.
- An MCP server returned an `assetUrl` / signed URL pointing to file content and you want to do something with the bytes.
## The four scripts
### `signed-upload` — chat-attached upload → external URL
PUT bytes from a Gini upload (chat attachment, downloaded file, promoted workspace file) to a signed URL the model obtained from an API's prepare step. Used in 3-step attachment flows: prepare via the API → `signed-upload` → finalize via the API.
```
skill_run({
skill: "attachments",
script: "signed-upload",
args: {
uploadId: "abc-123-...", // from the user message marker, signed-download, or promote-file
url: "https://uploads.linear.app/...?X-Goog-...",
headers: { // pass through whatever the prepare step returned, verbatim
"content-type": "image/png",
"x-goog-content-length-range": "36116,36116"
}
}
})
// → { ok: true, status: 200, bytesSent: 36116 }
// or { ok: false, status, error: "..." }
```
Only https URLs are accepted. The script never fabricates headers — pass through what the prepare step gave you. Signed URLs typically expire in 60 seconds; move immediately from prepare → PUT.
### `signed-download` — external URL → Gini upload
GET bytes from a URL and store them as a Gini upload. Used to ingest content (Linear attachment URLs, GitHub raw files, user-pasted URLs, S3 presigned downloads) into the upload-addressable space so `vision_query` or `signed-upload` can consume them.
```
skill_run({
skill: "attachments",
script: "signed-download",
args: {
url: "https://uploads.linear.app/asset/abc.png",
headers: { authorization: "Bearer ..." }, // optional, depends on the URL
filename: "screenshot.png" // optional, defaults to URL basename
}
})
// → { ok: true, uploadId: "xyz-456-...", mimeType: "image/png", size: 36116 }
```
Only https URLs accepted. Body capped at 50MB. Inferred mime comes from the `content-type` response header; falls back to `application/octet-stream`.
### `promote-file` — workspace file → Gini upload
Register a workspace-relative file as a Gini upload. Used when `code_exec` / `terminal_exec` left a file on disk that you want to attach somewhere or run `vision_query` on.
```
skill_run({
skill: "attachments",
script: "promote-file",
args: {
path: ".charts/sales-q4.png",
mimeType: "image/png" // optional, sniffed from extension when omitted
}
})
// → { ok: true, uploadId: "ghi-789-...", mimeType: "image/png", size: 24512 }
```
Path is workspace-relative and escape-protected (same guard as `file_read`).
**When the user asked you to send / show them a file** (a screenshot, a chart, a generated picture, a PDF, a CSV), `promote-file` is all you need. Its result includes an `attachmentMarkdown` field — a ready-to-paste markdown tag like `` (for images) or `[name](gini-upload://<id>)` (for other files). **Paste that tag verbatim into your reply at the spot where the attachment should appear** — it renders inline there (an image shows as a picture, any other file as a download chip), so you can place a screenshot mid-sentence, right where you're describing it. Do **not** paste the raw `uploadId` on its own — use the provided `attachmentMarkdown` tag. The bare `uploadId` is only an argument to a *follow-up* tool (`signed-upload`, `vision_query`, `materialize`), never user-facing text.
### `materialize` — Gini upload → workspace file
The inverse of `promote-file`: write a Gini upload's bytes to a workspace file. Used when you need a chat-attached (or downloaded / promoted) upload on disk so `terminal_exec`, `code_exec`, or a git flow can read the actual file — e.g. committing an image to an asset branch.
```
skill_run({
skill: "attachments",
script: "materialize",
args: {
uploadId: "abc-123-...", // from the user message marker, signed-download, or promote-file
path: "assets/diagram.png" // optional, workspace-relative; defaults to the manifest filename
}
})
// → { ok: true, path: "assets/diagram.png", absPath: "/abs/.../assets/diagram.png",
// mimeType: "image/png", size: 36116, filename: "diagram.png" }
```
Destination is workspace-relative and escape-protected (same guard as `promote-file`). When `path` is omitted it defaults to the upload's original filename (basename, sanitized) at the workspace root, or `<uploadId>.<ext>` when the manifest has none. `absPath` is the absolute on-disk path — hand it to commands that need an absolute path (e.g. `git hash-object -w <absPath>`).
## Recipe patterns
### Filing an issue with a chat-attached screenshot (providers with a signed-upload API)
Applies to providers that expose a public prepare → PUT → finalize upload API (Linear, S3 / any presigned-URL backend):
1. **Prepare** — call the provider's prepare-upload tool via `mcp_call`. Linear: `prepare_attachment_upload({issue, filename, contentType, size})`.How to dogfood and verify a Gini behavior change by driving a real chat turn as a real user would. Use when verifying that the agent reaches for a tool or path on its own — a behavioral steer, a new tool, an INSTRUCTIONS.md change, or a dispatch/provider/memory/skill change — or before claiming a steer "works". Enforces bare, uncoached prompts so the test measures the default, not instruction-following.
Delegate coding work to Claude Code CLI for repository edits, reviews, and multi-turn implementation sessions.
Delegate coding work to the OpenAI Codex CLI for repository changes, reviews, and focused fixes.
Gini's self-knowledge: how Gini configures, extends, and operates on its own state via /api/* and registered tools. Load when the user asks Gini about its own capabilities or asks Gini to modify its own configuration.
Manage Apple Notes via memo CLI: create, search, edit.
Apple Reminders via remindctl: add, list, complete.
File a locally-captured, already-redacted Gini crash report as a GitHub issue, with the user's consent. Reads the pending crash queue and delegates the actual filing to the github-issues skill.
Create, search, triage, label, assign, comment on, and close GitHub issues using the gh CLI, with a curl REST fallback.