Skip to main content
ClaudeWave
Skill1.8k repo starsupdated 1mo ago

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

Install in Claude Code
Copy
git clone --depth 1 https://github.com/Open-Curiosity/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`).

**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 `![image](gini-upload://<id>)` (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})`.