Skip to main content
ClaudeWave
Skill341 repo starsupdated today

attachments

The attachments skill orchestrates file transfers between Gini's upload space, external HTTPS endpoints, and workspace disk files. Use it when chat attachments need routing to Linear or GitHub, when pasted URLs require ingestion, when code execution produces artifacts to send elsewhere, or when API responses include signed URLs demanding byte movement and processing.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/Lilac-Labs/gini-agent /tmp/attachments && cp -r /tmp/attachments/skills/attachments ~/.claude/skills/attachments
Then start a new Claude Code session; the skill loads automatically.

SKILL.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`).

### `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})`. The response carries the signed URL, headers to send verbatim, and an asset URL for the finalize step.
2. **PUT bytes** — `skill_run({skill: "attachments", script: "signed-upload", args: {uploadId, url: <prepared.url>, headers: <prepared.headers>}})`.
3. **Finalize** — call the provider's finalize tool. Linear: `create_attachment_from_upload({issue, assetUrl, title})`.

`uploadId` is in the user message marker: `Attached image uploads (in order): - <id> (<mime>, <bytes> bytes)`. Read the mime and size from the same marker so the prepare call doesn't fail with `EntityTooLarge` / `EntityTooSmall`.

Targets without a public upload API (e.g. GitHub issues) don't use this recipe — `materialize` the upload to disk instead (recipe below) and follow that integration skill's own attach flow.

### Reading a screensho