Skip to content
← Public packages

@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):

  1. From Kody: mint with createUpload({ filename, contentType, confirm: true }).
  2. On the VM: fetch(upload.url, { method: 'PUT', headers: upload.headers, body }) (or curl with the returned headers). Use exactly upload.headers.
  3. From Kody execute: fetch(downloadUrl) to read bytes, or pass downloadUrl as sourceUrl to @kentcdodds/cloudflare/r2-put-object for 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. Use create-upload → client PUT. ./upload auto-rejects above ~400 KB decoded.
  • Upload URL TTL defaults to 3600s (expiresInSeconds); clamp is 60…604800. PUT promptly with the minted headers (SigV4 x-amz-date freshness).
  • Objects are short-lived (~7 day lifecycle). Do not treat downloadUrl as 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 at api.cloudflare.com / dash.cloudflare.com — do not invent tokens.
  • Do not modify @kentcdodds/home-maintenance or @kentcdodds/stash while working here.

Storage / infra

ItemValue
Cloudflare accounta41d50ecaf0ae0f86dd1824ef6729cb2
Bucketagent-files
Secretuser cloudflareApiToken
packageStorager2ParentAccessKeyId, r2PublicDomain, r2LifecycleApplied