@kentcdodds/agent-files
Mint short-lived R2 PUT URLs for agent file handoffs; public download via managed r2.dev.
AGENTS.md
97 lines · 3.6 KB · Markdown@kentcdodds/agent-files — agent notes
Human intent and setup live in README.md. This file is the
agent runbook.
Imports
import createUpload from 'kody:@kentcdodds/agent-files/create-upload'
import get from 'kody:@kentcdodds/agent-files/get'
import deleteFile from 'kody:@kentcdodds/agent-files/delete'
import upload from 'kody:@kentcdodds/agent-files/upload'
// root "." also resolves to create-upload
import createUploadRoot from 'kody:@kentcdodds/agent-files'Secrets: refer to cloudflareApiToken by name only (user scope via
kody.secretMounts). Never print, commit, or invent token values. In prose,
mention placeholders as <secret:cloudflareApiToken> — never a live
double-curly form that would resolve if the text were fetched.
Positive playbook — machine ↔ Kody
Typical VM → Kody path (Cursor cloud agent has the bytes; Kody has the token):
- From Kody: mint with
createUpload({ filename, contentType, confirm: true }). - On the VM:
fetch(upload.url, { method: 'PUT', headers: upload.headers, body })(or curl with the returned headers). Use exactlyupload.headers. - From Kody
execute:fetch(downloadUrl)to read bytes, or passdownloadUrlassourceUrlto@kentcdodds/cloudflare/r2-put-objectfor a durable R2 copy on Kent's Cloudflare account.
Kody → machine: mint, PUT from execute/./upload (small only), hand
downloadUrl to the consumer to GET.
Object key shape: {prefix}/{yyyy-mm-dd}/{shortId}-{safeName} with default
prefix: "handoff".
Auth path matches home-maintenance / stash: Cloudflare
temp-access-credentials + SigV4 signed PUT headers. Do not invent a different
signing scheme.
Smoke snippets
import createUpload from 'kody:@kentcdodds/agent-files/create-upload'
import get from 'kody:@kentcdodds/agent-files/get'
import deleteFile from 'kody:@kentcdodds/agent-files/delete'
const preview = await createUpload({ filename: 'smoke-1x1.png', dryRun: true })
const target = await createUpload({
filename: 'smoke-1x1.png',
contentType: 'image/png',
confirm: true,
})
// PUT body to target.upload.url with target.upload.headers (from the VM)
const meta = await get({ key: target.key })
await deleteFile({ key: target.key, confirm: true })Tiny MCP upload (escape hatch only):
import upload from 'kody:@kentcdodds/agent-files/upload'
await upload({
filename: 'hi.txt',
contentType: 'text/plain',
bytesBase64: btoa('hi'),
confirm: true,
})Edge cases
- Never shove multi‑MB files through MCP
bytesBase64. Usecreate-upload→ client PUT../uploadauto-rejects above ~400 KB decoded. - Upload URL TTL defaults to 3600s (
expiresInSeconds); clamp is 60…604800. PUT promptly with the minted headers (SigV4x-amz-datefreshness). - Objects are short-lived (~7 day lifecycle). Do not treat
downloadUrlas durable storage; copy into a long-lived bucket when needed. - Public GET uses managed r2.dev (v1 — no signed GET /
create-download). - Bucket + public domain + ~7-day lifecycle are ensured on first write. If lifecycle PUT fails, README documents the manual dashboard rule; uploads still work.
- Missing / denied
cloudflareApiToken: errors name the secret and point atapi.cloudflare.com/ dash.cloudflare.com — do not invent tokens. - Do not modify
@kentcdodds/home-maintenanceor@kentcdodds/stashwhile working here.
Storage / infra
| Item | Value |
|---|---|
| Cloudflare account | a41d50ecaf0ae0f86dd1824ef6729cb2 |
| Bucket | agent-files |
| Secret | user cloudflareApiToken |
| packageStorage | r2ParentAccessKeyId, r2PublicDomain, r2LifecycleApplied |