uploads-cli

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Uploading files to uploads.sh and embedding in GitHub

将文件上传至uploads.sh并嵌入GitHub

What this does and why

功能与用途

GitHub's native image hosting (
github.com/user-attachments/…
) is only reachable through an authenticated browser session — there is no
gh
CLI or REST endpoint for it. So any image URL you put in a PR/issue body written with
gh … --body-file
must already point at something publicly hosted.
This skill covers both transports: the
uploads
CLI
(local files, git, localhost) and the hosted MCP at
https://agents.uploads.sh/mcp
(bytes you already have, no checkout). Both PUT to the uploads.sh API and return a stable public URL plus ready-to-paste markdown. For PRs and issues the managed attachments comment is available on both — CLI via local
gh
as a fallback, hosted MCP bot-only.
GitHub原生图片托管服务(
github.com/user-attachments/…
)仅支持通过已认证的浏览器会话访问——没有对应的
gh
CLI或REST端点。因此,使用
gh … --body-file
编写PR/issue正文时,其中的图片URL必须指向已公开托管的资源。
本技能涵盖两种传输方式:
uploads
CLI
(适用于本地文件、git仓库、localhost资源),以及托管在
https://agents.uploads.sh/mcp
的MCP服务(适用于已获取字节数据、无需代码检出的场景)。两者均会向uploads.sh API执行PUT请求,并返回稳定的公共URL及可直接粘贴的Markdown代码。针对PR和issue,两种方式均支持托管附件评论功能——CLI会以本地
gh
作为备选方案,托管MCP则仅通过机器人实现。

MCP vs CLI

MCP与CLI对比

Same product, two transports. Skills do not install a binary.
NeedUseWhy
Bytes already in context (ChatGPT attachment, base64)Hosted MCP
put
files: [{ filename, contentBase64 }]
. Pass
repo
+ (
pr
|
branch
). No git inference.
List, find, metadata, comment, promoteEitherHosted:
list
,
find_files
,
get_metadata
/
set_metadata
,
comment
,
promote
. CLI:
uploads list
/
find
/
meta
/
comment
/
attach --promote
.
Local path or current-branch attachCLIHosted server has no filesystem and no
attach
tool. Use
put
instead.
localhost
/ private-network screenshot
CLI
uploads screenshot --via local
Remote render cannot reach your machine.
Selector annotate on a live pageCLI
uploads screenshot --annotate --via local
Remote backend rejects selector-bearing specs.
Neither transportStopDo not treat
npm install -g
as the ChatGPT path. OAuth on
https://agents.uploads.sh/mcp
is the published remote path.
CLI examples in the rest of this skill assume a checkout and the
uploads
binary. Hosted tool contracts live under Notes and cautions (the MCP bullet) below.
For the common case, use
uploads attach <file...>
. It infers the current branch's PR, uploads every file under stable attachment keys (in parallel), and maintains the comment by default. One bad file does not block the rest — JSON includes
uploads
and
failures
(exit
1
when any failed):
bash
uploads attach ./before.png ./after.png
uploads attach ./shot.png --issue 45 --repo buildinternet/uploads
Pass
--no-comment
when only stable URLs are wanted. Use
put
for lower-level naming and output control.
Stage as you go, before a PR exists.
uploads attach ./shot.png --branch [name]
stages files under
gh/<owner>/<repo>/branch/<branch>/<filename>
instead of a PR/issue number — same upload path, no target flags, no comment (there's nothing to comment on yet). With no value,
--branch
resolves the current git branch;
/
in the name sanitizes to
-
. Attach this way at every visual milestone during the work, not just once at the end. Staged files carry
gh.status=staged
until promotion flips them to
promoted
, so
uploads find gh.status=staged
(add
gh.branch=<name>
to narrow) lists what's still in flight. The server also stamps
gh.uploader
/
gh.uploader-id
(from the token's minting user) on gh.*-tagged uploads, so
uploads find gh.status=staged gh.uploader=<login>
narrows to one contributor's in-flight files.
Check what's staged:
uploads staged
.
A dedicated read-only view — "what's staged for this branch, and will it auto-attach?" — instead of hand-building the
find
/
list
query above:
bash
uploads staged                                  # current branch, repo from gh/git remote
uploads staged --branch feature/thing --repo owner/name
uploads staged --format json
Same branch/repo resolution as
attach --branch
(current git branch by default, worktree-safe). Human mode prints one compact line per staged file (filename, size,
gh.staged-at
, public URL), then a
binding:
line and
once the PR exists: uploads attach --promote
(the promote line is omitted for
binding: other
— promoting from a non-owning workspace would be rejected by the cross-tenant gate). Nothing staged prints a single zero-state line.
--format json
(or global
--json
) always emits a valid document —
{ repo, branch, files, binding }
— even with zero files;
files
is
[]
, never empty stdout.
binding
folds in the same repo↔workspace check the stage-time warning uses (see "Repo binding" below), so you don't have to separately reason about it:
binding.state
binding.autoAttach
Meaning
self
true
Repo is bound to this workspace — staged files auto-attach on PR open.
none
false
Repo isn't linked yet — link it (
uploads github link
) or nothing auto-attaches.
other
false
Repo is linked to a different workspace — these files won't auto-attach from here.
unknown
false
Binding check failed (offline, or an older server without the route) — advisory only, never blocks the view.
The
none
/
other
wording is the exact same advisory text as the
attach --branch
stage-time warning (issue #398) — one source of truth, so the two surfaces never drift.
Local stdio MCP mirrors this as the
staged
tool (
branch
/
repo
args, same
{ repo, branch, files, binding }
shape). The hosted MCP has no dedicated
staged
tool (no git defaults) — list/find_files recipes and hosted
put
/
promote
with explicit
repo
/
branch
are under Notes and cautions (the MCP bullet) below.
Getting those files into the PR's attachments comment needs no extra step once a PR exists for that branch:
  • GitHub App installed on the repo: a webhook auto-promotes staged files into the PR's attachment prefix and creates/updates the managed comment the moment the PR opens, reopens, or gets a new commit.
  • No GitHub App: the next
    uploads attach
    targeting that PR auto-promotes those staged files into the PR's attachment prefix before the comment refresh. If that first attach has nothing new to upload, run
    uploads attach --promote
    (zero file arguments) to promote and refresh the comment on its own; it exits
    0
    even when nothing was staged. Skip auto-promotion on a given call with
    --no-promote
    .
Promotion only applies to PRs, never issues, and both paths degrade silently (no error) if the workspace's server doesn't support promotion yet.
Promotion needs the repo already bound to the workspace. Both the webhook and the CLI-triggered path above rely on the same repo↔workspace binding used by the managed comment (see "Repo binding" below) — any earlier successful
attach
/
comment
/promote call against that repo binds it implicitly, or
uploads github link
claims it explicitly. A repo that has never been bound and is only ever staged with
--branch
sees no error and no comment — promotion is a silent no-op at PR-open time. If you can't confirm the repo is already bound, don't promise auto-attach; the zero-setup fallback that works regardless of binding history is running
uploads attach --promote
(or any targeted
uploads attach
) once the PR exists.
Comment missing? First, give it a moment — if the App is installed and subscribed to
issue_comment
, a deleted or mangled bot comment self-heals on the next webhook delivery; don't panic-repost. If it's still missing, check the repo↔workspace binding —
uploads github link --status
(read-only, shows the binding without claiming it). See "Repo binding" below.
The killer feature for GitHub:
--pr
/
--issue
produce hash-free, stable keys (
gh/<owner>/<repo>/pull/<num>/<name>
), so re-uploading the same filename overwrites in place and the URL never changes. There is no confirmation prompt — hot-swap is intentional for agents and re-runs. Human mode prints
>> replaced existing object (same URL)
after overwrite; JSON has
"replaced": true|false
. Use
--dry-run
to preview: it prints
>> would replace existing object (same URL)
when the key already exists, without writing.
Every other key is strict (issue #174): an explicit
--key
, or the default
put
path with no
--pr
/
--issue
, refuses to overwrite an existing object — the CLI error names the existing object's URL and tells you to add
--replace
(MCP:
replace: true
). Set
UPLOADS_OVERWRITE=1
to restore always-overwrite for those paths.
--dry-run
previews the refusal too:
>> would refuse: key already exists
. This never applies to
--pr
/
--issue
keys, which always hot-swap regardless.
Responses include two public URLs when the shared dual-host setup applies:
FieldHost (default)Use for
url
storage.uploads.sh
Durable link, click-through, non-GitHub embeds
embedUrl
embed.uploads.sh
GitHub PR/issue markdown (
<img src>
/
![]()
)
embedUrl
is the same object with badge-style no-cache headers so GitHub Camo revalidates after an overwrite. CLI/MCP
markdown
and the managed attachments comment already prefer
embedUrl
. Override with
UPLOADS_EMBED_PUBLIC_BASE_URL
(empty disables; self-host set your no-cache CDN base).
两者属于同一产品的不同传输方式,技能无需安装二进制文件。
需求场景使用方式原因说明
已获取字节数据(如ChatGPT附件、base64编码)托管MCP的
put
命令
传入
files: [{ filename, contentBase64 }]
,同时指定
repo
+ (
pr
|
branch
)。无需自动推断git信息。
列表查询、文件查找、元数据操作、评论管理、文件升级任意一种方式托管MCP:
list
find_files
get_metadata
/
set_metadata
comment
promote
。CLI:
uploads list
/
find
/
meta
/
comment
/
attach --promote
本地路径或当前分支附件上传CLI托管服务器无文件系统,也无
attach
工具,请使用
put
替代。
localhost
/ 私有网络截图
CLI的
uploads screenshot --via local
命令
远程渲染无法访问你的本地机器。
对实时页面进行选择器标注CLI的
uploads screenshot --annotate --via local
命令
远程后端会拒绝包含选择器的请求。
无需上述两种传输方式停止操作不要将
npm install -g
视为ChatGPT的使用路径。
https://agents.uploads.sh/mcp
上的OAuth认证是官方推荐的远程使用路径。
本技能后续的CLI示例均假设已完成代码检出并安装了
uploads
二进制文件。托管工具的契约说明请查看下方注意事项中的MCP部分。
常见场景下,使用
uploads attach <file...>
命令即可。它会自动推断当前分支对应的PR,并行上传所有文件至稳定的附件密钥下,并默认维护评论。单个文件上传失败不会阻塞其他文件——JSON输出会包含
uploads
failures
字段(若有失败则退出码为
1
):
bash
uploads attach ./before.png ./after.png
uploads attach ./shot.png --issue 45 --repo buildinternet/uploads
若仅需稳定URL,可传递
--no-comment
参数。如需更底层的命名和输出控制,请使用
put
命令。
在PR创建前逐步上传文件。 使用
uploads attach ./shot.png --branch [name]
命令可将文件暂存至
gh/<owner>/<repo>/branch/<branch>/<filename>
路径下,而非PR/issue编号路径——上传路径相同,但无需指定目标标志,也不会生成评论(此时尚无可用的评论对象)。若未指定分支名称,
--branch
会自动解析当前git分支;分支名称中的
/
会被替换为
-
。在工作过程中的每个可视化里程碑阶段都采用这种方式上传,而非仅在最终阶段一次性上传。暂存文件会携带
gh.status=staged
元数据,直至执行升级操作后变为
promoted
,因此使用
uploads find gh.status=staged
(可添加
gh.branch=<name>
缩小范围)即可列出所有待处理文件。服务器还会为
gh.*
标签的上传文件添加
gh.uploader
/
gh.uploader-id
元数据(来自令牌的创建用户),因此使用
uploads find gh.status=staged gh.uploader=<login>
可筛选出指定贡献者的待处理文件。
查看暂存文件:
uploads staged
这是一个只读的专用视图——显示“当前分支的暂存文件,以及是否会自动附加至PR”——无需手动构建上述
find
/
list
查询:
bash
uploads staged                                  # 查看当前分支的暂存文件,仓库信息来自gh/git远程仓库
uploads staged --branch feature/thing --repo owner/name
uploads staged --format json
分支/仓库解析逻辑与
attach --branch
一致(默认使用当前git分支,支持工作树安全)。人类可读模式会为每个暂存文件打印一行简洁信息(文件名、大小、
gh.staged-at
、公共URL),随后打印
binding:
行和
once the PR exists: uploads attach --promote
提示(若
binding: other
则省略升级提示——从非所属工作区执行升级会被跨租户网关拒绝)。若无暂存文件,则打印一行空状态信息。
--format json
(或全局
--json
参数)始终会输出有效的JSON文档——
{ repo, branch, files, binding }
——即使没有文件;
files
字段为
[]
,不会输出空内容。
binding
字段整合了暂存时警告所使用的仓库↔工作区绑定检查逻辑(见下方“仓库绑定”),因此无需单独判断:
binding.state
binding.autoAttach
含义
self
true
仓库已绑定至当前工作区——暂存文件会在PR创建时自动附加。
none
false
仓库尚未关联——执行
uploads github link
关联仓库,否则不会自动附加文件。
other
false
仓库已关联至其他工作区——这些文件无法从当前工作区自动附加。
unknown
false
绑定检查失败(离线,或服务器版本过旧不支持该路由)——仅为提示信息,不会阻塞视图。
none
/
other
的提示文本与
attach --branch
暂存时的警告完全一致(issue #398)——单一数据源确保两处提示不会出现差异。
本地标准输入输出MCP也提供了对应的
staged
工具(支持
branch
/
repo
参数,返回相同的
{ repo, branch, files, binding }
结构)。托管MCP没有专用的
staged
工具(无git默认值)——列表/文件查找方法以及托管
put
/
promote
命令的
repo
/
branch
显式参数说明,请查看下方注意事项中的MCP部分。
PR创建后,无需额外步骤即可将这些文件添加至PR的附件评论:
  • 仓库已安装GitHub App:PR创建、重新打开或收到新提交时,Webhook会自动将暂存文件升级至PR的附件前缀路径,并创建/更新托管评论。
  • 未安装GitHub App:下一次针对该PR执行
    uploads attach
    命令时,会自动升级暂存文件至PR的附件前缀路径,然后刷新评论。若首次执行
    attach
    命令时无新文件可上传,可运行
    uploads attach --promote
    (无需传入文件参数)单独执行升级和评论刷新;即使没有暂存文件,该命令也会返回退出码
    0
    。若需在某次调用中跳过自动升级,可使用
    --no-promote
    参数。
升级操作仅适用于PR,不适用于issue;若工作区服务器暂不支持升级功能,两种方式均会静默降级(无错误提示)。
升级操作需要仓库已绑定至工作区。 上述Webhook和CLI触发的升级路径均依赖托管评论所使用的仓库↔工作区绑定逻辑(见下方“仓库绑定”)——此前针对该仓库执行的任何成功
attach
/
comment
/promote操作都会隐式绑定仓库,或通过
uploads github link
显式绑定仓库。若仓库从未绑定,仅通过
--branch
暂存文件,则不会出现错误提示,也不会生成评论——PR创建时升级操作会静默失效。若无法确认仓库是否已绑定,请不要承诺自动附加功能;无论绑定历史如何,最稳妥的零配置备选方案是PR创建后执行
uploads attach --promote
(或任何指定PR的
uploads attach
命令)。
评论缺失? 首先稍作等待——若已安装App并订阅
issue_comment
事件,被删除或损坏的机器人评论会在下次Webhook触发时自动修复;不要急于重新发布。若评论仍缺失,请检查仓库↔工作区绑定情况——运行
uploads github link --status
(只读,仅显示绑定状态,不会执行绑定操作)。详情见下方“仓库绑定”。
针对GitHub的核心特性:
--pr
/
--issue
参数会生成无哈希值的稳定密钥
gh/<owner>/<repo>/pull/<num>/<name>
),因此重新上传相同文件名的文件会直接覆盖原有内容,URL保持不变。该操作无确认提示——这是为代理和重复执行场景设计的热替换机制。人类可读模式会在覆盖后打印
>> replaced existing object (same URL)
;JSON输出会包含
"replaced": true|false
字段。使用
--dry-run
参数可预览操作:若密钥已存在,会打印
>> would replace existing object (same URL)
,但不会执行写入操作。
其他所有密钥均为严格模式(issue #174):使用显式
--key
参数,或未指定
--pr
/
--issue
的默认
put
路径,均不允许覆盖现有对象——CLI会输出错误信息,包含现有对象的URL,并提示添加
--replace
参数(MCP对应参数为
replace: true
)。设置
UPLOADS_OVERWRITE=1
可恢复这些路径的自动覆盖行为。
--dry-run
参数也会预览拒绝操作:
>> would refuse: key already exists
。此规则不适用于
--pr
/
--issue
密钥,这类密钥始终允许热替换。
当启用双宿主共享配置时,响应会包含两个公共URL:
字段默认宿主用途
url
storage.uploads.sh
持久链接、直接访问、非GitHub嵌入场景
embedUrl
embed.uploads.sh
GitHub PR/issue Markdown
<img src>
/
![]()
embedUrl
指向同一对象,但带有徽章式无缓存头,因此GitHub Camo会在覆盖后重新验证缓存。CLI/MCP的
markdown
输出以及托管附件评论均默认使用
embedUrl
。可通过
UPLOADS_EMBED_PUBLIC_BASE_URL
参数覆盖(空值禁用;自托管场景请设置无缓存CDN基础URL)。

Prerequisites

前置条件

  • No shell / ChatGPT? Skip this section. Use the hosted MCP (
    https://agents.uploads.sh/mcp
    ) and the table above. Do not install the CLI.
  • Node.js ≥ 22.
  • The CLI. Install globally for repeated agent use, or run it once with
    npx
    :
    bash
    npm install --global @buildinternet/uploads
    npx @buildinternet/uploads --help
    uploads --version
    Every example in this skill uses the global
    uploads …
    binary (as after install). Inside the uploads monorepo only,
    pnpm uploads …
    builds from local source first — do not write product/PR examples that way. Prefer
    --json
    or
    --quiet
    for scripted steps (keeps stderr clean and skips optional update-available hints).
  • A configured token (one-time — see below). Check with
    uploads doctor
    .
  • gh
    CLI, authenticated
    — only for the
    --comment
    /
    comment
    features that write to a PR/issue. Plain uploads don't need it.
  • 无Shell环境 / 使用ChatGPT? 跳过本节。使用托管MCP服务(
    https://agents.uploads.sh/mcp
    )及上述表格。无需安装CLI。
  • Node.js ≥ 22版本。
  • CLI工具。 如需重复使用,可全局安装;或通过
    npx
    一次性运行:
    bash
    npm install --global @buildinternet/uploads
    npx @buildinternet/uploads --help
    uploads --version
    本技能中的所有示例均使用全局
    uploads …
    二进制文件(安装后即可使用)。仅在uploads单仓库内部,
    pnpm uploads …
    会先从本地源码构建——请勿以此方式编写产品/PR示例。 脚本化步骤中建议使用
    --json
    --quiet
    参数(保持stderr干净,跳过可选的更新提示)。
  • 已配置令牌(仅需一次——见下文)。可通过
    uploads doctor
    检查配置。
  • 已认证的
    gh
    CLI
    ——仅适用于向PR/issue写入内容的
    --comment
    /
    comment
    功能。普通上传无需此工具。

One-time setup

一次性配置

Config lives in a user-owned file so it survives skill reinstalls:
~/.config/buildinternet/config        # or $XDG_CONFIG_HOME/buildinternet/config
Resolution is per key, first match wins: CLI flags (
--api-url
,
--token
,
--workspace
) →
UPLOADS_*
environment vars →
--env-file <path>
$BUILDINTERNET_CONFIG
→ the shared config file. For a one-off against a different API or workspace, just export the var or pass
--env-file
.
The fastest path is
uploads login
. Have a workspace admin invite your email to a workspace first, then run it once, interactively, to sign in:
bash
uploads login          # opens a browser to approve sign-in, saves config, runs doctor
uploads login --workspace acme   # only needed if your account can access more than one
If the account has no workspace yet,
login
prompts for a name and offers one derived from your GitHub login as a bracketed default — press Enter to take it, or type your own. Nothing is prefilled when no valid, unclaimed name can be derived.
--workspace <name> --create
skips the prompt entirely, which is the form to use in scripts.
That's a one-time, human-in-the-loop step (device sign-in needs a browser); once the config file is written, every later
uploads
invocation — including from a non-interactive agent — just reads the saved token. Routine agents never need
ADMIN_TOKEN
.
Inviting a teammate (workspace admin/owner only): open the people tab under
/account/workspaces/<name>/people
in the browser (invite, revoke pending invites, promote members to admin), or:
bash
uploads invite create --email teammate@example.com --workspace acme
Device login as you (not
ADMIN_TOKEN
/ not a workspace token). The CLI prints an accept URL to share if email isn’t configured. Invitee accepts, then
uploads login
. Workspace admins can promote existing members to admin on that people tab; only the workspace owner can demote or remove other admins.
For headless machines with no browser at all, an operator can mint a token directly (
/admin/tokens
,
ADMIN_TOKEN
-gated — see
docs/admin-tokens.md
) and hand it to the agent as
UPLOADS_TOKEN
, or an enrollment code (
upe_…
, an alternative invite-link/code path — useful when you don't have the recipient's email) can be exchanged with
uploads login --code
. Neither is the normal path for new setups.
The resulting token defaults to 90 days and
files:read
plus
files:write
; it cannot delete files unless an administrator explicitly grants
files:delete
. Verify or inspect setup at any time:
bash
uploads setup                                  # shows effective configuration
uploads doctor                                 # version + health + auth + workspace
uploads doctor --json
Tokens encode their workspace (
up_<workspace>_…
), so the CLI infers
--workspace
when you don't set it. Legacy administrator-minted tokens remain valid. See "Config commands" for setting put defaults (default repo, prefix, image width) once instead of per-command.
配置信息存储在用户自有文件中,因此技能重装后配置不会丢失:
~/.config/buildinternet/config        # 或$XDG_CONFIG_HOME/buildinternet/config
配置解析优先级为按键匹配,首次匹配生效:CLI标志(
--api-url
--token
--workspace
)→
UPLOADS_*
环境变量→
--env-file <path>
$BUILDINTERNET_CONFIG
→ 共享配置文件。若需针对不同API或工作区执行一次性操作,只需导出环境变量或传递
--env-file
参数即可。
最快的配置方式是运行
uploads login
。请先让工作区管理员将你的邮箱邀请至工作区,然后运行一次该命令,通过交互式方式登录:
bash
uploads login          # 打开浏览器进行登录授权,保存配置并运行诊断
uploads login --workspace acme   # 仅当你的账户可访问多个工作区时需要指定
若账户尚未关联工作区,
login
会提示输入工作区名称,并提供基于你的GitHub登录名生成的默认名称(方括号标注)——按Enter键使用默认名称,或输入自定义名称。若无法生成有效且未被占用的名称,则不会预填充内容。
--workspace <name> --create
参数可跳过提示,适用于脚本场景。
这是一次性的人工交互步骤(设备登录需要浏览器);配置文件写入后,后续所有
uploads
调用——包括非交互式代理调用——都会直接读取已保存的令牌。常规代理无需使用
ADMIN_TOKEN
邀请团队成员(仅工作区管理员/所有者可用):在浏览器中打开
/account/workspaces/<name>/people
页面(邀请成员、撤销待处理邀请、将成员提升为管理员),或运行以下命令:
bash
uploads invite create --email teammate@example.com --workspace acme
以你的身份进行设备登录(非
ADMIN_TOKEN
/ 非工作区令牌)。若未配置邮箱,CLI会打印接受链接供你分享。被邀请者接受邀请后,运行
uploads login
即可登录。工作区管理员可在上述人员页面将现有成员提升为管理员;仅工作区所有者可降级或移除其他管理员。
对于完全无浏览器的无头机器,管理员可直接生成令牌(
/admin/tokens
,需
ADMIN_TOKEN
权限——见
docs/admin-tokens.md
),并通过
UPLOADS_TOKEN
环境变量传递给代理;或使用注册码(
upe_…
,替代邀请链接/码的路径,适用于无法获取收件人邮箱的场景),通过
uploads login --code
完成登录。这两种方式均非新配置的常规路径。
生成的令牌默认有效期为90天,拥有
files:read
files:write
权限;除非管理员明确授予
files:delete
权限,否则无法删除文件。可随时验证或查看配置:
bash
uploads setup                                  # 显示有效配置
uploads doctor                                 # 版本 + 健康状态 + 认证信息 + 工作区信息
uploads doctor --json
令牌中包含工作区信息(
up_<workspace>_…
),因此若未指定
--workspace
参数,CLI会自动推断工作区。旧版管理员生成的令牌仍然有效。如需设置
put
命令的默认值(默认仓库、前缀、图片宽度),请查看“配置命令”。

Core workflow:
uploads put

核心工作流:
uploads put

Upload one or more files and get back URL(s) plus ready-to-paste markdown. Multiple paths upload in parallel; multi-file JSON is
{ uploads, failures }
(exit
1
when any failed). Single-file JSON stays a flat object.
bash
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New live feed cards" --width 700
uploads put ./before.png ./after.png
Human output goes to stderr; the URL and markdown to stdout, so you can pipe or capture them. Use
-
as the file to read from stdin.
Key options (
uploads put --help
for all):
FlagPurpose
--alt <text>
Alt text for the markdown (default: filename). Always write meaningful alt text.
--width <px>
Emit sized
<img width=…>
HTML instead of
![]()
(markdown can't size images).
--repo <owner/repo>
Repo segment of the auto key (default: git remote, or
UPLOADS_DEFAULT_REPO
).
--ref <id>
PR/issue/branch/date segment (default: today, or
UPLOADS_DEFAULT_REF
).
--destination <id>
Typed root:
screenshots
|
gh
|
f
(sets key prefix).
--prefix <path>
Key prefix (default:
screenshots
, or
UPLOADS_DEFAULT_PREFIX
).
--key <key>
Set the object key explicitly; skips the auto-naming below.
--name <leaf>
Clean filename for the key's leaf + default alt (no
/
); keeps the
--pr
/default path. Not with
--key
.
--replace
Allow overwriting an existing object on a strict key (
--key
/default path). No effect on
--pr
/
--issue
(or
UPLOADS_OVERWRITE=1
).
--dry-run
Resolve + print the key and final public URL without uploading; reports if the key would replace (or, on a strict key, be refused). Not with
--gallery
; skips the managed comment sync even with
--pr
/
--issue
.
--content-type <mime>
Override the content type (else inferred from extension; ignored when optimize rewrites the body).
--frame <id>
Opt-in chrome before optimize:
phone
,
browser
,
iphone-16-pro
.
--frame-url <url>
Address bar text for
--frame browser
.
--frame-fit cover|contain
How the shot fills the screen (default:
cover
).
--no-optimize
Skip client-side image optimization (default: still images → WebP). Or
UPLOADS_NO_OPTIMIZE=1
.
--optimize-max-edge <px>
Max long edge when optimizing (default: 2400).
--optimize-quality <1-100>
WebP quality when optimizing (default: 85).
--keep-exif
Keep EXIF/XMP/ICC when optimizing (default: strip for privacy). Or
UPLOADS_KEEP_EXIF=1
.
--no-git
Don't derive
--repo
from the git remote (or
UPLOADS_NO_GIT=1
).
--format human|url|markdown|json
Control stdout.
--json
(global) forces json.
-w, --workspace <name>
Override workspace (wins over env and token inference).
Image optimization (default on): PNG/JPEG and similar still images are re-encoded to WebP (long edge capped at 2400px, quality 85) before upload so PR/issue embeds stay lean. The object key/filename extension follows the output (e.g.
shot.png
…/shot.webp
). EXIF/XMP is stripped by default (public URLs + privacy); pass
--keep-exif
when the discussion needs the embedded image metadata. Animated GIF, SVG, video, and non-images are left alone; if the optimized payload is not smaller, the original is uploaded. Use
--no-optimize
when you need lossless originals.
Frames (opt-in):
--frame phone
(generic bezel),
--frame browser
, or
--frame iphone-16-pro
(community device art, cached under
~/.cache/uploads/frames
). Default is no frame.
How keys work — three paths, no extra naming modes:
IntentCommand
Just upload it, give me a URL
uploads put ./file.png
Explicit typed destination
uploads put ./file.png --destination screenshots
Stable GitHub embed I might re-upload
uploads put ./file.png --pr <num>
Stable
--pr
path but a clean leaf
uploads put ./capture-2026-…Z.png --pr <num> --name hero.png
I know exactly where it goes
uploads put ./file.png --key screenshots/…/x.png
Timestamped captures break stable
--pr
keys — pass
--name hero.webp
to keep a clean leaf. Use
--dry-run
to preview the exact public URL before uploading.
Default
put
is the fast path; you don't need
--key
,
--prefix
, or
--repo
. Inside a git repo, on a non-default branch, a bare
put
now stages automatically (issue #403)
— same key/metadata as
attach --branch
(
gh/<owner>/<repo>/branch/<branch>/<filename>
), so it auto-attaches to that branch's PR when one opens. This fires whenever none of
--pr
/
--issue
/
--key
/
--ref
/
--prefix
/
--destination
is set and
--no-git
isn't passed; any of those flags (or the default branch, detached HEAD, not being in a git repo, or
--no-git
) falls back to the classic dated layout:
<prefix>/<repo-name>/<ref-or-date>/<basename>-<shorthash>.<ext>
— the short hash prevents collisions without random names or a separate "preserve name" flag. Prefer
--destination screenshots
(or
gh
with
--pr
/
--issue
) over inventing roots — workspaces may allowlist only those destinations. Override with
--key
only when you have a reason, and keep the key under an allowed root. Pass
--ref
/
--prefix
/
--destination
explicitly for a plain dated upload on a branch (the opt-out).
Output formats — pick what you'll consume:
bash
uploads put ./shot.png --format url        # just the URL, for scripting
uploads put ./shot.png --format markdown   # just the ![]()/<img> snippet
uploads put ./shot.png --json              # {workspace,key,url,size,markdown}
The bare-put staging note (issue #403). Since a bare
put
on a non-default branch now stages by default (see above), it prints a one-line note confirming that instead of nudging you to do it yourself — human mode writes it to stderr,
--format json
adds it as an additive optional
hint
field on the same response:
text
note: staged for branch fix-header — auto-comments to pull request when opened
(or run: uploads attach --promote once it exists). Use --ref/--prefix for a
plain dated upload.
If the same call also trips the stage-time binding warning (issue #398/#400 — the repo isn't bound to this workspace), that warning takes the
hint
slot instead (it's the more actionable of the two); both still print on stderr in human mode. Suppress the note (not the staging itself) with
--quiet
,
UPLOADS_NO_NUDGE=1
(env), or
UPLOADS_NO_NUDGE=1
in the config file (
uploads config set UPLOADS_NO_NUDGE 1
).
The old "rerun with --pr" nudge (issue #393) still fires, unchanged, for the narrower case a bare put still lands on the dated layout with a detectable PR — in practice, an explicit
--ref
/
--prefix
opting out of staging while a PR is open for that branch:
text
note: on branch fix-header (PR #142 open) — rerun with --pr 142 for a stable
key plus a managed comment that collects this PR's media, or stage pre-PR
files with: uploads attach <file> --branch
It's best-effort (a quick
gh pr view
lookup, bounded to 3s) — no open PR just widens the wording to a generic
--pr <num>
. Same suppression as above.
上传一个或多个文件,并返回URL及可直接粘贴的Markdown代码。多文件上传会并行执行;多文件JSON输出为
{ uploads, failures }
(若有失败则退出码为
1
)。单文件JSON输出为扁平对象。
bash
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New live feed cards" --width 700
uploads put ./before.png ./after.png
人类可读输出会发送至stderr;URL和Markdown代码会发送至stdout,因此可通过管道或捕获方式获取。使用
-
作为文件名可从stdin读取内容。
关键参数(所有参数请查看
uploads put --help
):
参数用途
--alt <text>
Markdown的替代文本(默认值:文件名)。请始终填写有意义的替代文本。
--width <px>
生成带尺寸的
<img width=…>
HTML代码,而非
![]()
(Markdown无法设置图片尺寸)。
--repo <owner/repo>
自动生成密钥的仓库段(默认值:git远程仓库,或
UPLOADS_DEFAULT_REPO
)。
--ref <id>
PR/issue/分支/日期段(默认值:今日日期,或
UPLOADS_DEFAULT_REF
)。
--destination <id>
类型化根路径:
screenshots
|
gh
|
f
(设置密钥前缀)。
--prefix <path>
密钥前缀(默认值:
screenshots
,或
UPLOADS_DEFAULT_PREFIX
)。
--key <key>
显式设置对象密钥;跳过自动命名逻辑。
--name <leaf>
密钥叶子节点的简洁文件名(无
/
)+ 默认替代文本;保留
--pr
/默认路径。不可与
--key
同时使用。
--replace
允许覆盖严格密钥(
--key
/默认路径)下的现有对象。对
--pr
/
--issue
密钥无影响(或设置
UPLOADS_OVERWRITE=1
)。
--dry-run
解析并打印密钥和最终公共URL,但不执行上传;报告密钥是否会被替换(或在严格密钥下被拒绝)。不可与
--gallery
同时使用;即使指定
--pr
/
--issue
,也会跳过托管评论同步。
--content-type <mime>
覆盖内容类型(否则从文件扩展名推断;优化重写内容时会忽略该参数)。
--frame <id>
优化前选择添加边框:
phone
browser
iphone-16-pro
--frame-url <url>
--frame browser
模式下的地址栏文本。
--frame-fit cover|contain
截图填充屏幕的方式(默认值:
cover
)。
--no-optimize
跳过客户端图片优化(默认:静态图片会转换为WebP格式)。或设置
UPLOADS_NO_OPTIMIZE=1
--optimize-max-edge <px>
优化时的最大长边尺寸(默认值:2400)。
--optimize-quality <1-100>
WebP格式的优化质量(默认值:85)。
--keep-exif
优化时保留EXIF/XMP/ICC元数据(默认:移除以保护隐私)。或设置
UPLOADS_KEEP_EXIF=1
--no-git
不从git远程仓库推断
--repo
参数(或设置
UPLOADS_NO_GIT=1
)。
--format human|url|markdown|json
控制stdout输出格式。
--json
(全局参数)强制输出JSON格式。
-w, --workspace <name>
覆盖工作区设置(优先级高于环境变量和令牌推断)。
图片优化(默认启用):PNG/JPEG等静态图片会在上传前重新编码为WebP格式(长边限制为2400px,质量为85),以确保PR/issue嵌入内容保持轻量化。对象密钥/文件扩展名会随输出格式变化(例如
shot.png
…/shot.webp
)。默认会移除EXIF/XMP元数据(公共URL + 隐私保护);若讨论需要图片嵌入的元数据,请传递
--keep-exif
参数。动态GIF、SVG、视频和非图片文件不会被修改;若优化后的文件体积未减小,则会上传原始文件。如需无损原始文件,请使用
--no-optimize
参数。
边框(可选启用)
--frame phone
(通用边框)、
--frame browser
--frame iphone-16-pro
(社区设备素材,缓存于
~/.cache/uploads/frames
)。默认无边框
密钥工作逻辑——三种路径,无额外命名模式:
意图命令
仅上传文件并获取URL
uploads put ./file.png
显式指定类型化目标路径
uploads put ./file.png --destination screenshots
生成可重新上传的稳定GitHub嵌入URL
uploads put ./file.png --pr <num>
使用稳定
--pr
路径但保留简洁文件名
uploads put ./capture-2026-…Z.png --pr <num> --name hero.png
明确指定文件存储路径
uploads put ./file.png --key screenshots/…/x.png
带时间戳的捕获文件会破坏稳定的
--pr
密钥——传递
--name hero.webp
可保留简洁文件名。使用
--dry-run
参数可预览上传前的精确公共URL。
默认
put
命令是快速路径;无需使用
--key
--prefix
--repo
参数。在git仓库内的非默认分支上执行裸
put
命令,会自动暂存文件(issue #403)
——密钥/元数据与
attach --branch
命令相同(
gh/<owner>/<repo>/branch/<branch>/<filename>
),因此当该分支的PR创建时,文件会自动附加至PR。当未设置
--pr
/
--issue
/
--key
/
--ref
/
--prefix
/
--destination
参数,且未传递
--no-git
时,会触发自动暂存;若设置了上述任一参数(或处于默认分支、分离HEAD状态、不在git仓库内、传递了
--no-git
),则会回退至经典的日期化路径:
<prefix>/<repo-name>/<ref-or-date>/<basename>-<shorthash>.<ext>
——短哈希值可避免冲突,无需随机名称或单独的“保留名称”标志。建议使用
--destination screenshots
(或结合
--pr
/
--issue
使用
gh
),而非自定义根路径——工作区可能仅允许这些目标路径。仅当有明确需求时才使用
--key
参数覆盖,并确保密钥位于允许的根路径下。如需在分支上执行普通日期化上传,请显式传递
--ref
/
--prefix
/
--destination
参数(选择退出自动暂存)。
输出格式——选择适合你的格式:
bash
uploads put ./shot.png --format url        # 仅输出URL,适用于脚本
uploads put ./shot.png --format markdown   # 仅输出![]()/<img>代码片段
uploads put ./shot.png --json              # {workspace,key,url,size,markdown}
put
命令的暂存提示(issue #403)
。由于非默认分支上的裸
put
命令现在默认会自动暂存(见上文),因此会打印一行提示信息确认该操作,而非提示手动执行——人类可读模式会将提示写入stderr,
--format json
会在响应中添加可选的
hint
字段:
text
note: staged for branch fix-header — auto-comments to pull request when opened
(or run: uploads attach --promote once it exists). Use --ref/--prefix for a
plain dated upload.
若同一调用同时触发了暂存时的绑定警告(issue #398/#400——仓库未绑定至当前工作区),则警告信息会替代
hint
字段(更具可操作性);两种信息仍会在人类可读模式下打印至stderr。可通过
--quiet
UPLOADS_NO_NUDGE=1
(环境变量)或配置文件中的
UPLOADS_NO_NUDGE=1
uploads config set UPLOADS_NO_NUDGE 1
)抑制提示信息(不会影响暂存操作本身)。
**旧版“使用--pr重新运行”提示(issue #393)**仍会在特定场景触发,未做修改:裸
put
命令仍会生成日期化路径,且可检测到PR——实际上是显式设置
--ref
/
--prefix
选择退出暂存,但该分支的PR已打开:
text
note: on branch fix-header (PR #142 open) — rerun with --pr 142 for a stable
key plus a managed comment that collects this PR's media, or stage pre-PR
files with: uploads attach <file> --branch
这是尽力而为的检测(快速执行
gh pr view
查询,超时时间为3秒)——若无打开的PR,提示文本会泛化为
--pr <num>
。抑制方式同上。

Capturing a screenshot:
uploads screenshot

截图捕获:
uploads screenshot

Capture a URL or a local
.html
file and host it — no separate screenshot tool needed, and no browser install required for the default path:
bash
uploads screenshot https://uploads.sh --pr 128 --comment
uploads screenshot ./card.html --out ./card.png
uploads screenshot ./card.html --no-upload --out ./card.png
After capture, a screenshot shares the exact
put
upload pipeline described above: optional
--frame
, optimize-by-default,
--pr
/
--issue
attachment +
--comment
,
--gallery
,
--meta
, and the same output formats. It also ships as an MCP tool (
screenshot
) alongside the CLI command.
Two capture backends, selected with
--via
:
BackendWhat it isNeeds
local
Drives an already-installed Chrome/Chromium via
playwright-core
A discoverable browser on disk, or
--cdp
remote
Renders server-side via the uploads.sh render endpointNothing local; counts against the workspace's monthly upload budget
--via auto
(the default) prefers local when a usable browser is found, else falls back to remote. Set a persistent default with
UPLOADS_SCREENSHOT_VIA=auto|local|remote
(env,
--env-file
, or the user config file — see "Config commands"); the
--via
flag always wins.
localhost/private-network targets are local-only. With
--via remote
(or
auto
falling back to remote) these fail fast with a clear error instead of sending a request that could never work. Local
.html
files work on both backends — the remote backend receives the file's contents inline (≤ 2 MiB), so anything the page references via
file://
or relative paths only resolves with
--via local
. A numeric
--wait <ms>
(fixed settle delay after load) is also local-only; use
--wait load|domcontentloaded|networkidle
for a backend-agnostic wait.
Use
--cdp <endpoint>
to attach to a Chrome that's already running (
http://host:port
or
ws://…
) instead of launching a new one — handy when an agent already has a Playwright MCP or
agent-browser
session open.
--browser <path>
(or
UPLOADS_CHROME_PATH
/
CHROME_PATH
) points at an explicit executable.
Key options (
uploads screenshot --help
for all):
FlagPurpose
--via auto|local|remote
Capture backend (default:
auto
, or
UPLOADS_SCREENSHOT_VIA
).
--browser <path>
Explicit local browser executable (or
UPLOADS_CHROME_PATH
/
CHROME_PATH
).
--cdp <endpoint>
Attach to a running Chrome via CDP instead of launching one (local backend only).
--viewport <WxH[@Sx]>
Size + device scale factor (default:
1280x800@2
).
--selector <css>
Capture one element instead of the viewport.
--full-page
Capture the full scrollable page.
--max-height <px>
Cap on
--full-page
capture height in CSS px (default:
5000
;
0
= uncapped). A page over the cap is clipped, with a note to stderr and a
--format json
hint
. Requires
--full-page
; applies on both
--via local
and
--via remote
.
--dark
/
--light
Emulate
prefers-color-scheme
(full media-query emulation on
--via local
only —
--via remote
only sets the CSS
color-scheme
property, so a page's own
prefers-color-scheme
queries won't flip).
--wait <load|domcontentloaded|networkidle|ms>
Settle strategy (default:
load
); a millisecond count is local-only.
--out <file>
Also write the PNG to a local file, plus a sidecar manifest (
<file>.uploads.json
) with this capture's derived metadata (
path
/
url
/
env
/
viewport
, plus
--state
if given) and a content hash. A later
put
/
attach
of that exact file picks the metadata back up automatically — explicit
--meta
/
--state
still win. See
--no-sidecar
.
--no-sidecar
Don't write the
<file>.uploads.json
sidecar alongside
--out
.
--no-upload
Skip hosting; requires
--out
(local file only).
--key
/
--pr
/
--issue
/
--comment
Same destination and attachment options as
put
(see above);
--pr
/
--issue
also give a stable, hash-free key.
--branch [name]
Stage against a branch, pre-PR — same key as
attach --branch
(see below); this is also what a bare
screenshot
on a non-default branch does automatically (issue #469).
--frame
/
--no-optimize
/
--gallery
/
--meta
Same as
put
— reused from the shared upload pipeline.
Inside a git repo, on a non-default branch, a bare
screenshot
now stages automatically too (issue #469, mirroring
put
's issue #403 default)
— same key/metadata as
--branch
/
attach --branch
(
gh/<owner>/<repo>/branch/<branch>/<filename>
), carrying every derived fact (
path
/
url
/
env
/
viewport
, plus
--state
) through to the PR once it opens. This is what closes the gap a coding agent hits capturing before the PR exists: capture early with a plain
uploads screenshot <url> --out shot.png
, and the metadata rides along instead of being re-stated (or lost) at
attach --pr <num>
time. Fires whenever none of
--pr
/
--issue
/
--branch
/
--key
/
--ref
/
--prefix
/
--destination
is set and
--no-git
isn't passed; the same set of flags (or the default branch, detached HEAD, not being in a git repo, or
--no-git
) falls back to the classic dated
screenshots/<repo>/<date>/...
layout. Prints the same staging note as bare
put
(see above) — same stderr wording, same JSON
hint
field, same
--quiet
/
UPLOADS_NO_NUDGE
suppression.
Errors and hints: a local capture with no usable browser fails with
BROWSER_NOT_FOUND
(exit
2
) — hint: try
--via remote
, or install a browser (
npx playwright install chromium
). A remote render that the server can't complete returns
RENDER_FAILED
. A burst rate limit on the render endpoint returns
RATE_LIMITED
(exit
4
) — hint: wait ~60s and retry. A remote render over the workspace's monthly upload budget surfaces the usual
UPLOAD_BUDGET
code and hint (
uploads usage
, then delete objects or raise limits) — renders and puts share one monthly counter.
uploads doctor
reports which local browser (if any) was detected and which backend
--via auto
would currently pick.
Key derivation and
--state
.
Whenever the object's filename is auto-derived from the captured URL (host + path) — the default dated layout, or the
--pr
/
--issue
leaf name — passing
--state
folds it into that derived filename stem —
localhost-docs-mcp.webp
becomes
localhost-docs-mcp-before.webp
/
localhost-docs-mcp-after.webp
— so capturing the same URL twice with
--state before
then
--state after
produces two distinct objects instead of the second silently overwriting the first. Re-capturing the same URL with the same state still replaces the existing object in place (idempotent re-capture). An explicit
--key
is unaffected by
--state
folding. On overwrite, human mode prints
>> replaced existing object (same URL)
to stderr (same wording as
put
's hot-swap note, above) and
--format json
adds
"replaced": true
, plus a
hint
field when a
--state
capture replaced an existing object.
捕获URL或本地
.html
文件并托管——无需单独的截图工具,默认路径无需安装浏览器:
bash
uploads screenshot https://uploads.sh --pr 128 --comment
uploads screenshot ./card.html --out ./card.png
uploads screenshot ./card.html --no-upload --out ./card.png
捕获完成后,截图会使用上述
put
命令的上传流程:可选
--frame
参数、默认优化、
--pr
/
--issue
附件+
--comment
--gallery
--meta
参数,以及相同的输出格式。该功能也作为MCP工具(
screenshot
)与CLI命令并行提供。
两种捕获后端,通过
--via
参数选择:
后端说明依赖条件
local
通过
playwright-core
驱动已安装的Chrome/Chromium浏览器
磁盘上存在可检测到的浏览器,或使用
--cdp
参数
remote
通过uploads.sh渲染端点进行服务器端渲染无需本地依赖;会占用工作区的月度上传配额
--via auto
(默认值)优先使用本地浏览器(若找到可用浏览器),否则回退至远程渲染。可通过
UPLOADS_SCREENSHOT_VIA=auto|local|remote
(环境变量、
--env-file
或用户配置文件——见“配置命令”)设置持久默认值;
--via
参数优先级最高。
localhost/私有网络目标仅支持本地后端。使用
--via remote
(或
auto
回退至远程)时,此类目标会快速失败并返回清晰错误,而非发送无法成功的请求。本地
.html
文件在两种后端均可用——远程后端会接收文件内容(≤2MiB),因此页面通过
file://
或相对路径引用的资源仅在
--via local
模式下可解析。数值型
--wait <ms>
(加载后的固定等待延迟)也仅支持本地后端;如需跨后端的等待策略,请使用
--wait load|domcontentloaded|networkidle
使用
--cdp <endpoint>
可连接至已运行的Chrome浏览器(
http://host:port
ws://…
),而非启动新浏览器——当代理已打开Playwright MCP或
agent-browser
会话时非常实用。
--browser <path>
(或
UPLOADS_CHROME_PATH
/
CHROME_PATH
)指定浏览器可执行文件的路径。
关键参数(所有参数请查看
uploads screenshot --help
):
参数用途
--via auto|local|remote
捕获后端(默认值:
auto
,或
UPLOADS_SCREENSHOT_VIA
)。
--browser <path>
本地浏览器可执行文件的路径(或
UPLOADS_CHROME_PATH
/
CHROME_PATH
)。
--cdp <endpoint>
通过CDP连接至已运行的Chrome浏览器,而非启动新浏览器(仅本地后端支持)。
--viewport <WxH[@Sx]>
尺寸 + 设备缩放因子(默认值:
1280x800@2
)。
--selector <css>
捕获单个元素,而非整个视口。
--full-page
捕获可滚动的完整页面。
--max-height <px>
--full-page
模式下的捕获高度上限(CSS px,默认值:
5000
0
表示无上限)。若页面超过该上限,会被裁剪,并在stderr打印提示信息,
--format json
会添加
hint
字段。需配合
--full-page
使用;适用于
--via local
--via remote
模式。
--dark
/
--light
模拟
prefers-color-scheme
(仅
--via local
模式支持完整媒体查询模拟——
--via remote
仅设置CSS
color-scheme
属性,因此页面自身的
prefers-color-scheme
查询不会生效)。
--wait <load|domcontentloaded|networkidle|ms>
等待策略(默认值:
load
);毫秒数仅支持本地后端。
--out <file>
同时将PNG文件写入本地,并生成附带清单文件(
<file>.uploads.json
),包含捕获的派生元数据(
path
/
url
/
env
/
viewport
,以及
--state
参数)和内容哈希值。后续对该文件执行
put
/
attach
命令时会自动恢复元数据——显式
--meta
/
--state
参数优先级更高。见
--no-sidecar
参数。
--no-sidecar
执行
--out
时不生成
<file>.uploads.json
附带清单文件。
--no-upload
跳过托管;需配合
--out
使用(仅保存至本地文件)。
--key
/
--pr
/
--issue
/
--comment
put
命令相同的目标和附件参数(见上文);
--pr
/
--issue
也会生成稳定的无哈希值密钥。
--branch [name]
针对分支暂存文件(PR创建前)——密钥与
attach --branch
相同(见下文);非默认分支上的裸
screenshot
命令也会自动执行该操作(issue #469)。
--frame
/
--no-optimize
/
--gallery
/
--meta
put
命令相同——复用共享上传流程。
在git仓库内的非默认分支上执行裸
screenshot
命令,会自动暂存文件(issue #469,镜像
put
命令的issue #403默认行为)
——密钥/元数据与
--branch
/
attach --branch
相同(
gh/<owner>/<repo>/branch/<branch>/<filename>
),并将所有派生信息(
path
/
url
/
env
/
viewport
,以及
--state
)传递至PR。这解决了编码代理在PR创建前捕获截图的痛点:现在执行
uploads screenshot <url> --out shot.png
即可捕获截图,元数据会随文件保留,无需在
attach --pr <num>
时重新指定(或丢失)。当未设置
--pr
/
--issue
/
--branch
/
--key
/
--ref
/
--prefix
/
--destination
参数,且未传递
--no-git
时,会触发自动暂存;若设置了上述任一参数(或处于默认分支、分离HEAD状态、不在git仓库内、传递了
--no-git
),则会回退至经典的日期化
screenshots/<repo>/<date>/...
路径。打印的暂存提示与裸
put
命令相同(见上文)——stderr文本一致,JSON
hint
字段一致,
--quiet
/
UPLOADS_NO_NUDGE
抑制方式一致。
错误与提示:本地捕获时若未找到可用浏览器,会返回
BROWSER_NOT_FOUND
错误(退出码
2
)——提示:尝试
--via remote
,或安装浏览器(
npx playwright install chromium
)。远程渲染失败会返回
RENDER_FAILED
错误。渲染端点触发突发速率限制会返回
RATE_LIMITED
错误(退出码
4
)——提示:等待约60秒后重试。远程渲染超过工作区月度上传配额会返回常规
UPLOAD_BUDGET
错误码和提示(
uploads usage
,然后删除对象或提升配额)——渲染和上传共享同一月度计数器。
uploads doctor
会报告检测到的本地浏览器(若有),以及当前
--via auto
会选择的后端。
密钥推导与
--state
参数
。当对象文件名从捕获URL(主机+路径)自动推导时——默认日期化路径,或
--pr
/
--issue
的叶子名称——传递
--state
参数会将其整合至推导的文件名中——
localhost-docs-mcp.webp
会变为
localhost-docs-mcp-before.webp
/
localhost-docs-mcp-after.webp
——因此针对同一URL分别使用
--state before
--state after
捕获时,会生成两个不同的对象,而非第二个覆盖第一个。针对同一URL使用相同
state
参数重新捕获时,仍会覆盖现有对象(幂等重新捕获)。显式
--key
参数不受
--state
参数影响。覆盖时,人类可读模式会在stderr打印
>> replaced existing object (same URL)
(与
put
命令的热替换提示文本相同),
--format json
会添加
"replaced": true
字段,若
--state
捕获替换了现有对象,还会添加
hint
字段。

Baking in callouts:
--annotate

添加标注:
--annotate

--annotate <file|->
bakes hand-drawn boxes, arrows, labels, freeform strokes, and redactions onto the capture before it's uploaded (JSON spec, a file path or
-
for stdin). Selectors resolve against the live page — the local backend only in v1, so a selector-bearing spec on
--via remote
is rejected up front:
bash
uploads screenshot http://localhost:3000 --via local --annotate ./callouts.json
For the spec format and an existing-image equivalent (
uploads annotate <image> --spec <file|->
, pixel-only, no selectors), see the annotate-screenshots skill.
--annotate <file|->
可在上传前将手绘框、箭头、标签、自由线条和遮罩添加至截图(JSON格式,支持文件路径或
-
表示stdin)。选择器会针对实时页面解析——v1版本仅支持本地后端,因此
--via remote
模式下包含选择器的请求会被提前拒绝:
bash
uploads screenshot http://localhost:3000 --via local --annotate ./callouts.json
关于规范格式和现有图片的等效命令(
uploads annotate <image> --spec <file|->
,仅基于像素,无选择器),请查看annotate-screenshots技能。

Custom metadata & search

自定义元数据与搜索

Every object can carry queryable key-value metadata (distinct from optimize/frame provenance) — tag uploads at put time, then find them later.
每个对象都可携带可查询的键值对元数据(与优化/边框来源不同)——上传时标记文件,以便后续查找。

The canonical vocabulary

标准词汇表

Metadata is only useful if it is spelled the same way every time. These ten keys are the agreed vocabulary; most are derived for you, so the main job is not to fight them by inventing a different spelling.
KeySourceExample
url
auto — screenshot target
https://app.example/settings
path
auto — pathname
/settings
env
auto —
local
only
local
theme
auto — only when forced
dark
viewport
auto — capture opts / EXIF
1280x800@2x
device
auto — image EXIF
Apple iPhone 16 Pro
software
auto — image EXIF
Figma
captured
auto — image EXIF
2026-07-20T20:35:39
state
you
--state
before
|
after
app
you
--app
web
|
ios
path
and
state
are the two highest-value keys — pass both, every time, as a habit.
path
is the key most worth getting right (the one agents most often misspell as
route
,
page
, or
screen
— a near-miss warns on stderr and suggests the canonical spelling, but is never rewritten for you, so fix it at the source) and
state
captures the before/after pattern that dominates PR screenshots — nothing can infer either one from the image alone.
state
is a closed set:
before
,
after
,
empty
,
error
,
loading
(a near-miss like
--state post
fails fast and suggests
after
).
uploads screenshot
derives
path
automatically from the captured URL, so you only need to add
--state
.
uploads put
/
uploads attach
of an already-existing file have nothing to derive
path
from, so pass it explicitly with
--meta path=/route
— and
attach
/
put --pr
/
put --issue
print a
tip: add --meta path=/route so this shot is findable by page
on stderr (plus a JSON
hint
field) when an image lands with no
path
meta, as a reminder (issue #469 lever 3; respects
--quiet
).
bash
uploads screenshot https://app.example/settings --state before
元数据只有在拼写一致时才有用。以下十个键是约定的词汇表;大部分会自动推导,因此主要工作是不要自定义拼写,避免破坏一致性。
来源示例
url
自动推导——截图目标URL
https://app.example/settings
path
自动推导——路径名
/settings
env
自动推导——仅本地模式
local
theme
自动推导——仅强制设置时生效
dark
viewport
自动推导——捕获选项/EXIF
1280x800@2x
device
自动推导——图片EXIF
Apple iPhone 16 Pro
software
自动推导——图片EXIF
Figma
captured
自动推导——图片EXIF
2026-07-20T20:35:39
state
手动设置——
--state
参数
before
|
after
app
手动设置——
--app
参数
web
|
ios
path
state
是两个最高价值的键——请养成习惯,每次都传递这两个参数。
path
是最需要确保正确的键(代理经常误写为
route
page
screen
——拼写接近时会在stderr打印警告并建议标准拼写,但不会自动修改,因此需要从源头修正),
state
则捕获了PR截图中常见的前后对比模式——无法从图片本身推断这两个键。
state
是固定集合:
before
after
empty
error
loading
(拼写接近的值如
--state post
会快速失败并建议使用
after
)。
uploads screenshot
会从捕获URL自动推导
path
,因此只需添加
--state
参数即可。
uploads put
/
uploads attach
上传已有文件时,无法从文件推导
path
,因此需通过
--meta path=/route
显式传递——
attach
/
put --pr
/
put --issue
命令在图片无
path
元数据时,会在stderr打印
tip: add --meta path=/route so this shot is findable by page
提示(JSON输出会添加
hint
字段),作为提醒(issue #469 lever 3;受
--quiet
参数影响)。
bash
uploads screenshot https://app.example/settings --state before

→ stamps url, path=/settings, viewport, state=before

→ 标记url、path=/settings、viewport、state=before

uploads put ./after.png --pr 123 --meta path=/settings --state after --app web uploads find path=/settings state=after # what it was all for
undefined
uploads put ./after.png --pr 123 --meta path=/settings --state after --app web uploads find path=/settings state=after # 这就是元数据的用途
undefined

What is derived, and when

自动推导规则与时机

  • uploads screenshot
    knows its own target, so it stamps
    url
    ,
    path
    (query stripped),
    viewport
    ,
    env=local
    for a local target, and
    theme
    when
    --dark
    /
    --light
    forced one.
  • uploads put
    /
    attach
    read the image's own EXIF before the optimizer strips it, promoting an allowlist:
    viewport
    (from pixel dimensions and DPI),
    device
    ,
    software
    ,
    captured
    .
  • uploads put
    /
    attach
    also read a sidecar manifest
    left by a prior
    screenshot --out
    of that exact file (
    <file>.uploads.json
    , content-hash guarded — a regenerated/edited file silently loses it) and merge in its derived metadata. This closes the capture-then-attach gap where a shot is taken before a PR exists:
    uploads screenshot ... --out shot.png --state after
    now,
    uploads attach shot.png --pr 123
    later, still gets
    path
    /
    url
    /
    env
    /
    viewport
    /
    state
    on the PR-keyed object. Disable writing it with
    --no-sidecar
    .
env
is only ever
local
. It is never set to
prod
— inferring that from "not localhost" would mislabel every staging and preview URL, and wrong metadata is worse than none.
Never promoted from EXIF, regardless of
--keep-exif
:
all GPS tags, body and lens serial numbers,
Artist
/
Copyright
/
OwnerName
, and free-form user comments. Note the flip side:
device
and
software
were previously discarded and now become queryable metadata that renders on the public
/f/
page.
Precedence: explicit
--meta
/
--state
/
--app
> screenshot capture facts > sidecar manifest > EXIF > unset. Derived keys are also dropped first if the 24-key cap is reached — your own keys are never dropped, and a full key budget never fails an upload.
Turn the whole derived tier off with
--no-auto
or
UPLOADS_NO_AUTO_META=1
.
  • uploads screenshot
    知晓目标URL,因此会标记
    url
    path
    (移除查询参数)、
    viewport
    ,本地目标会标记
    env=local
    ,强制设置
    --dark
    /
    --light
    时会标记
    theme
  • uploads put
    /
    attach
    会在优化器移除EXIF前读取图片的EXIF元数据,提取允许的字段:
    viewport
    (从像素尺寸和DPI推导)、
    device
    software
    captured
  • uploads put
    /
    attach
    还会读取之前
    screenshot --out
    生成的附带清单文件
    <file>.uploads.json
    ,由内容哈希值保护——重新生成/编辑的文件会静默丢失该清单),并合并派生元数据。这解决了PR创建前捕获然后上传的痛点:现在执行
    uploads screenshot ... --out shot.png --state after
    ,后续执行
    uploads attach shot.png --pr 123
    时,PR密钥的对象仍会携带
    path
    /
    url
    /
    env
    /
    viewport
    /
    state
    元数据。可通过
    --no-sidecar
    参数禁止生成清单文件。
env
仅会被设置为
local
。永远不会设置为
prod
——从“非localhost”推断为prod会错误标记所有 staging 和预览URL,错误的元数据比没有元数据更糟。
无论是否设置
--keep-exif
,均不会从EXIF提取的字段
:所有GPS标签、机身和镜头序列号、
Artist
/
Copyright
/
OwnerName
,以及自由格式用户评论。请注意另一面:
device
software
之前会被丢弃,现在会变为可查询的元数据,并显示在公共
/f/
页面上。
优先级:显式
--meta
/
--state
/
--app
参数 > 截图捕获信息 > 附带清单文件 > EXIF元数据 > 未设置。若达到24个键的上限,会优先移除自动推导的键——手动设置的键永远不会被移除,键数量达到上限也不会导致上传失败。
可通过
--no-auto
UPLOADS_NO_AUTO_META=1
禁用所有自动推导的元数据。

Rules and reserved keys

规则与保留键

Validated client-side, fail-fast, before uploading: key
^[a-z][a-z0-9._-]{0,63}$
(lowercase, dot-namespacing allowed, e.g.
gh.repo
); value 1–512 printable ASCII characters;
--meta k=v
may repeat up to 24 times per request; a value may itself contain
=
(only the first
=
splits key from value).
content-sha256
and
visibility
are reserved (server-computed / the real R2 visibility gate, respectively).
uploads attach
writes its own
gh.*
reserved-by-convention keys automatically — see below.
meta set
on a
gh/…
-keyed object also refreshes the managed PR/issue comment whenever the write touches a rendered key (
path
/
state
) — best-effort, after the metadata write already lands. On success it prints
refreshed the managed comment on <repo>#<num>
to stderr; if the bot endpoint is unavailable it prints
tip: run \
uploads comment --pr <num>` to refresh the PR comment` instead, and either way the metadata write itself never fails.
bash
uploads meta get screenshots/myapp/42/settings.webp
uploads meta set screenshots/myapp/42/settings.webp path=/onboarding --delete url
uploads meta set screenshots/myapp/42/settings.webp --meta path=/onboarding  # same thing
uploads list --meta app=web --meta path=/settings   # ANDed, repeatable
uploads find app=web path=/settings                 # same filter, positional pairs
uploads find --meta app=web                         # --meta works here too
uploads find hero                                   # bare name = filename substring
uploads find --name hero --meta app=web             # name + meta, either order
uploads meta keys                                   # which meta keys exist here
uploads meta values app                             # values (with counts) for one key
meta set
and
find
accept pairs in either spelling: positional
k=v
, or the repeatable
--meta k=v
that
put
,
attach
,
screenshot
, and
list
use. Both forms can appear in one call.
find
also takes a case-insensitive filename substring (
--name <term>
, or a bare positional without
=
). When you don't know which keys exist, start with
meta keys
/
meta values <key>
(or the MCP
list_metadata_keys
tool) — keys are user/agent-defined, not a fixed schema.
On the default
screenshots/…
path,
put
also auto-derives GitHub context and stamps
gh.repo
/
gh.kind
/
gh.number
/
gh.ref
from the current branch's PR (or a numeric
--ref
), so the file's
/f/
page shows an "Attached to" link. This is on by default and best-effort; disable it with
--no-auto
,
--no-git
, or
UPLOADS_NO_AUTO_META=1
. On this auto path an explicit
--meta gh.*
overrides the auto-derived value — the opposite of the
--pr
/
--issue
precedence below, where the target's own
gh.*
always wins. Both paths also stamp
gh.title
with the resolved PR/issue title when local
gh
can resolve one — best-effort, never blocks the upload if it can't.
Re-upload semantics: re-uploading to an existing key with metadata replaces that file's entire metadata set (delete-then-set, not a merge); re-uploading with no metadata at all preserves the existing metadata untouched. Derived keys count as metadata here, so a re-upload that derives anything replaces the set — pass
--no-auto
when re-uploading a key whose metadata you curated with
uploads meta set
.
Non-
gh.*
metadata values supplied via
--meta
(CLI) or
metadata
(MCP) render on the object's public
/f/<key>
file page. Treat them like the URL itself: don't put internal notes, secrets, tokens, IDs, or private paths in them.
客户端会验证元数据,快速失败,不会上传:键名需符合
^[a-z][a-z0-9._-]{0,63}$
(小写,允许点号命名空间,例如
gh.repo
);值为1–512个可打印ASCII字符;
--meta k=v
参数每个请求最多重复24次;值中可包含
=
(仅第一个
=
用于分隔键和值)。
content-sha256
visibility
为保留键(由服务器计算 / 实际R2可见性控制)。
uploads attach
会自动写入自己的
gh.*
约定保留键——见下文。
针对
gh/…
密钥的对象执行
meta set
操作时,若写入操作涉及可渲染的键(
path
/
state
),还会刷新托管PR/issue评论——尽力而为,元数据写入完成后执行。成功时会在stderr打印
refreshed the managed comment on <repo>#<num>
;若机器人端点不可用,会打印
tip: run \
uploads comment --pr <num>` to refresh the PR comment`提示;无论哪种情况,元数据写入本身都不会失败。
bash
uploads meta get screenshots/myapp/42/settings.webp
uploads meta set screenshots/myapp/42/settings.webp path=/onboarding --delete url
uploads meta set screenshots/myapp/42/settings.webp --meta path=/onboarding  # 效果相同
uploads list --meta app=web --meta path=/settings   # 多条件AND,可重复
uploads find app=web path=/settings                 # 相同筛选条件,位置参数对
uploads find --meta app=web                         # --meta参数也可用于此处
uploads find hero                                   # 裸名称 = 文件名子串
uploads find --name hero --meta app=web             # 文件名子串 + 可选元数据,顺序任意
uploads meta keys                          # 查看工作区存在的元数据键
uploads meta values app                             # 查看某个键的所有值(含计数)
meta set
find
命令接受两种拼写格式:位置参数
k=v
,或
put
attach
screenshot
list
命令使用的可重复
--meta k=v
参数。两种格式可在同一调用中混合使用。
find
命令还接受不区分大小写的文件名子串(
--name <term>
,或不带
=
的位置参数)。若不知道存在哪些键,可先使用
meta keys
/
meta values <key>
(或MCP的
list_metadata_keys
工具)——键由用户/代理定义,并非固定 schema。
在默认
screenshots/…
路径下,
put
命令还会自动推导GitHub上下文,并从当前分支的PR(或数值型
--ref
)标记
gh.repo
/
gh.kind
/
gh.number
/
gh.ref
元数据,因此文件的
/f/
页面会显示“Attached to”链接。默认启用,尽力而为;可通过
--no-auto
--no-git
UPLOADS_NO_AUTO_META=1
禁用。在该自动路径下,显式
--meta gh.*
参数会覆盖自动推导的值——与下文
--pr
/
--issue
的优先级相反,后者的
gh.*
值始终优先。两种路径还会在本地
gh
可解析时标记
gh.title
为PR/issue的实际标题——尽力而为,若
gh
无法解析则不会阻塞上传。
重新上传语义:重新上传至已有密钥且携带元数据时,会替换该文件的整个元数据集(先删除再设置,而非合并);重新上传无元数据时,会保留现有元数据不变。自动推导的键属于元数据,因此重新上传时若有自动推导的键,会替换元数据集——若已通过
uploads meta set
手动整理元数据,重新上传时请传递
--no-auto
参数。
通过
--meta
(CLI)或
metadata
(MCP)设置的非
gh.*
元数据值会显示在对象的公共
/f/<key>
页面上。请像对待URL一样处理这些值:不要在其中添加内部注释、密钥、令牌、ID或私有路径。

Public media galleries

公共媒体画廊

Use galleries when several existing public uploads should be shared as one ordered collection. A gallery has an opaque, API-returned public URL; do not derive one in scripts. Anyone who knows the URL can view the gallery and its media. GitHub repository visibility does not make it private, and a gallery does not pin objects against retention.
bash
uploads gallery create --title "Settings redesign"
uploads gallery add gal_example screenshots/app/settings-before.webp screenshots/app/settings-after.webp
uploads put ./after.png --gallery gal_example --alt "Updated settings page"
uploads gallery show gal_example
uploads gallery link gal_example --github buildinternet/uploads#58
uploads gallery list --github https://github.com/buildinternet/uploads/pull/58
gallery add
processes keys sequentially so it obtains a current optimistic version before each mutation. With
--json
, its stable
added
and
failures
arrays make partial failures safe for agents to inspect. A workspace may have up to 100 active galleries; each gallery permits up to 100 items and 20 linked external references. Deleting a gallery removes only its gallery record—not the objects.
Optionally link a gallery to a GitHub issue or PR with
uploads gallery link <gallery-id> --github <owner/repo#number>
. The CLI also accepts strict
https://github.com/<owner>/<repo>/issues|pull/<number>
URLs. Use
uploads gallery list --github <coordinate-or-url>
for the authenticated reverse lookup. This is metadata only: it does not make a gallery private or change its opaque identity.
当多个现有公共上传文件需要作为有序集合共享时,可使用画廊功能。画廊拥有API返回的不透明公共URL;请勿在脚本中自行推导URL。知晓URL的任何人都可查看画廊及其媒体。GitHub仓库可见性不会使画廊私有,画廊也不会固定对象以防止被删除。
bash
uploads gallery create --title "Settings redesign"
uploads gallery add gal_example screenshots/app/settings-before.webp screenshots/app/settings-after.webp
uploads put ./after.png --gallery gal_example --alt "Updated settings page"
uploads gallery show gal_example
uploads gallery link gal_example --github buildinternet/uploads#58
uploads gallery list --github https://github.com/buildinternet/uploads/pull/58
gallery add
会按顺序处理密钥,因此每次修改前都会获取当前乐观版本。使用
--json
参数时,稳定的
added
failures
数组可确保部分失败时代理可安全检查。每个工作区最多可拥有100个活跃画廊;每个画廊最多允许100个项目和20个外部链接引用。删除画廊仅会移除画廊记录,不会删除对象。
可通过
uploads gallery link <gallery-id> --github <owner/repo#number>
将画廊链接至GitHub issue或PR。CLI也接受严格的
https://github.com/<owner>/<repo>/issues|pull/<number>
URL。使用
uploads gallery list --github <coordinate-or-url>
可进行已认证的反向查询。这仅为元数据操作:不会使画廊私有,也不会改变其不透明身份。

Embedding in a GitHub PR or issue

嵌入GitHub PR或issue

Two ways, depending on whether you want a durable URL, a managed comment, or both.
两种方式,取决于是否需要持久URL、托管评论,或两者都需要。

Option A — stable attachment URL (
--pr
/
--issue
)

选项A——稳定附件URL(
--pr
/
--issue

Gives the file a hash-free, stable key so re-uploads overwrite in place and the URL is safe to hard-code in a PR body you'll edit later:
bash
uploads put ./after.png --pr 123 --alt "Dashboard after"
为文件生成无哈希值的稳定密钥,因此重新上传会直接覆盖原有内容,URL可安全硬编码至后续编辑的PR正文中:
bash
uploads put ./after.png --pr 123 --alt "Dashboard after"

key: gh/<owner>/<repo>/pull/123/after.webp → stable public URL (PNG optimized to WebP)

密钥: gh/<owner>/<repo>/pull/123/after.webp → 稳定公共URL(PNG优化为WebP)


`--issue <num>` does the same under `.../issues/<num>/`. The `<owner>/<repo>` comes
from `--repo` or the git remote. `--pr`/`--issue` can't be combined with `--key`,
`--ref`, or `--prefix` (the key layout is fixed), and are mutually exclusive.

These keys are deliberately predictable: they include the owner, repository, PR or
issue number, and filename. uploads.sh does not check GitHub visibility, so a private
or internal repository does **not** make the uploaded file private. Before using this
mode, confirm the media is safe for a public, guessable URL; otherwise redact it or do
not upload it.

If the uploads GitHub App can see that the target repo is private, this key layout
changes automatically: the key becomes `gh/private/<id>/...`, where `<id>` is a random
id minted per branch (or per repo, for issues), instead of the derivable
`gh/<owner>/<repo>/...`. No flag needed — public repos, and repos the App can't see,
keep the derivable layout. The URL is still unauthenticated and durable, not
access-controlled — anyone who obtains it can read it until you rotate the id with
`uploads github rotate-prefix --branch <branch>` (or `--repo-level` for issues/ingested
assets). See `docs/private-attachments.md` for the full threat model.

Then reference the **embed** URL in the PR/issue markdown you write with `gh`
(CLI `--format markdown` / MCP `markdown` already do this):

```markdown
<img width="700" alt="Dashboard after" src="https://embed.uploads.sh/default/gh/myorg/myapp/pull/123/after.webp">
Keep
url
(storage host) when you need a durable share link outside GitHub.
put --pr
/
--issue
(and
uploads attach
, below) writes
gh.repo
/
gh.kind
/
gh.number
/
gh.ref
as queryable metadata automatically, so
uploads find gh.ref=myorg/myapp#123
or
uploads list --meta gh.repo=myorg/myapp
finds everything attached to that PR/issue without needing the
gh/...
prefix. Add
--meta k=v
extras for your own pairs on top — a
--meta gh.*
override loses to the target's own
gh.*
values. It also stamps
gh.title
with the real PR/issue title when resolvable via local
gh
(best-effort; omitted rather than failing the upload if
gh
can't resolve one).

`--issue <num>`会生成`.../issues/<num>/`路径下的密钥。`<owner>/<repo>`来自`--repo`参数或git远程仓库。`--pr`/`--issue`不可与`--key`、`--ref`或`--prefix`同时使用(密钥结构固定),且两者互斥。

这些密钥是可预测的:包含所有者、仓库、PR或issue编号,以及文件名。uploads.sh不会检查GitHub可见性,因此私有或内部仓库的上传文件仍为公共文件。使用此模式前,请确认媒体内容适合公开、可猜测的URL;否则请裁剪/遮敏感内容,或不要上传。

若uploads GitHub App可检测到目标仓库为私有,密钥结构会自动变化:密钥变为`gh/private/<id>/...`,其中`<id>`为每个分支(或每个仓库,针对issue)生成的随机ID,而非可推导的`gh/<owner>/<repo>/...`。无需额外标志——公共仓库和App无法检测的仓库仍使用可推导结构。URL仍为未认证的持久链接,无访问控制——任何人获取URL后均可读取,直到通过`uploads github rotate-prefix --branch <branch>`(或针对issue/导入资源使用`--repo-level`)旋转ID。完整威胁模型请查看`docs/private-attachments.md`。

然后在使用`gh`编写的PR/issue Markdown中引用**embed** URL(CLI`--format markdown`/MCP`markdown`已自动处理):

```markdown
<img width="700" alt="Dashboard after" src="https://embed.uploads.sh/default/gh/myorg/myapp/pull/123/after.webp">
若需在GitHub外部使用持久共享链接,请使用
url
(存储宿主)。
put --pr
/
--issue
(以及
uploads attach
,见下文)会自动写入
gh.repo
/
gh.kind
/
gh.number
/
gh.ref
作为可查询元数据,因此使用
uploads find gh.ref=myorg/myapp#123
uploads list --meta gh.repo=myorg/myapp
即可找到所有附加至该PR/issue的文件,无需
gh/...
前缀。可在此基础上添加自定义
--meta k=v
参数——显式
--meta gh.*
参数会被目标的
gh.*
值覆盖。当本地
gh
可解析时,还会标记
gh.title
为PR/issue的实际标题——尽力而为,若
gh
无法解析则不会阻塞上传。

Option B — managed attachments comment (default with
--pr
/
--issue
, or
comment
)

选项B——托管附件评论(
--pr
/
--issue
默认启用,或
comment
命令)

put --pr
/
--issue
(like
attach
) uploads and creates/updates a single marker-owned comment on the PR/issue by default — no separate flag needed. It keeps loose
gh/...
attachments and every public gallery linked to that PR/issue in clearly separate sections, with up to three available gallery images inline. It finds its own prior comment via a hidden marker and edits it in place — it never touches the description or other comments:
bash
uploads put ./after.png --pr 123
Pass
--no-comment
to skip the sync (upload only), matching
attach --no-comment
.
--comment
is still accepted on
put
as a no-op — it's redundant now that the sync is the default, kept only for scripts written before this changed (#537).
The upload is authoritative; the comment is best-effort — if
gh
is missing or unauthenticated, the upload still succeeds and you get a warning. To (re)sync the comment without uploading anything (e.g. after several
--pr
uploads or gallery links), use the standalone command:
bash
uploads comment --pr 123
uploads comment --issue 45 --repo buildinternet/uploads
Removed the wrong screenshots?
delete
the object(s) and re-run
comment
to re-sync. When the last attachment and gallery are gone, the managed comment is rewritten in place to a neutral empty state (
No attachments are currently associated with this pull request.
) — it is never deleted (a later upload repopulates it) and never created just to say it's empty:
bash
uploads delete gh/owner/name/pull/123/after.png   # remove the asset
uploads comment --pr 123                           # comment now shows the empty state
Past 16 inline images, the comment collapses the rest into a
<details>
link list so a heavily-screenshotted PR stays readable. Each workspace gets its own managed comment on a shared repo (namespaced under the hood) instead of clobbering another workspace's — use
uploads github link
(below) to see or set which workspace a repo is bound to.
put --pr
/
--issue
(与
attach
相同)默认会上传文件在PR/issue上创建/更新单个机器人拥有的评论——无需额外标志。它会将松散的
gh/...
附件和所有链接至该PR/issue的公共画廊分为清晰的部分,最多显示三个画廊图片。它会通过隐藏标记找到之前的评论并原地编辑——永远不会修改描述或其他评论:
bash
uploads put ./after.png --pr 123
传递
--no-comment
参数可跳过同步(仅上传),与
attach --no-comment
一致。
put
命令仍接受
--comment
参数,但为无操作——自默认同步功能变更后(#537),该参数已冗余,仅为兼容旧脚本保留。
上传操作是权威的;评论操作是尽力而为的——若
gh
缺失或未认证,上传仍会成功,并打印警告。若需在不重新上传的情况下(重新)同步评论(例如多次
--pr
上传或画廊链接后),可使用独立命令:
bash
uploads comment --pr 123
uploads comment --issue 45 --repo buildinternet/uploads
误删了截图?删除对象后重新运行
comment
命令即可同步。当最后一个附件和画廊被移除后,托管评论会原地重写为中性空状态(
No attachments are currently associated with this pull request.
)——永远不会被删除(后续上传会重新填充),也不会仅为显示空状态而创建:
bash
uploads delete gh/owner/name/pull/123/after.png   # 删除资源
uploads comment --pr 123                           # 评论现在显示空状态
当内联图片超过16张时,评论会将剩余图片折叠为
<details>
链接列表,以便截图较多的PR保持可读性。每个工作区在共享仓库上拥有自己的托管评论(内部命名空间),不会覆盖其他工作区的评论——使用
uploads github link
(见下文)可查看或设置仓库绑定的工作区。

Repo binding (
uploads github link
/
unlink
/
doctor
)

仓库绑定(
uploads github link
/
unlink
/
doctor

The managed comment and webhook auto-promotion use a first-claim-wins binding between a repo and a workspace, normally created implicitly by your first
comment
/
put --comment
/promote call. Inspect, claim, or release it:
bash
uploads github link                       # claim the current repo for this workspace
uploads github link --repo owner/name     # claim a specific repo
uploads github link --status              # read-only: show the current binding, don't claim
uploads github unlink --repo owner/name   # release a binding this workspace owns
uploads github doctor                     # check the App itself (config + webhook events)
Claiming an already-bound repo never steals it — the command reports the existing owner instead.
unlink
only releases a binding this workspace owns; it 403s if another workspace owns it (an operator can reassign or remove it from the admin panel instead). On an older/self-hosted server without these routes they fail with a clear "server does not support repo bindings/GitHub App health check yet" message.
Claiming an unbound repo is authorized, not just first-come (issue #297): the server only lets a workspace make that first claim when its linked GitHub account has push (or higher) access to the repo, checked live against GitHub via the App's installation token. A token with no linked GitHub identity — a legacy/enrollment/shared token, including
default
's — can never claim a new repo, though it keeps working normally on any repo already bound to it. Claiming reports
claimed: false, reason: "not_authorized"
when this check fails; link a GitHub account with push access to the repo, or ask an operator to bind it explicitly from the admin panel.
not_authorized
on
comment
/
attach --comment
means either the repo is bound to a different workspace, or it's unbound and this workspace couldn't be verified as entitled to claim it (see above). Either way it's a hard decline, not a degrade — the CLI does not fall back to posting via local
gh
in this case, unlike other bot-post failures. Run
uploads github link --status
to see the current binding (if any), switch to a workspace with a linked GitHub account that has access, or ask an operator to bind the repo explicitly.
uploads github doctor
checks the App's own configuration and webhook event subscriptions (needs
issues
+
pull_request
;
issue_comment
is recommended so a deleted/mangled bot comment self-heals instead of waiting for the next PR push) — useful when webhook-driven behavior (auto-promotion, title updates, self-healing) seems to be silently doing nothing.
托管评论和Webhook自动升级功能使用仓库与工作区的先到先得绑定逻辑,通常由首次
comment
/
put --comment
/promote调用隐式创建。可查看、声明或解除绑定:
bash
uploads github link                       # 将当前仓库绑定至当前工作区
uploads github link --repo owner/name     # 将指定仓库绑定至当前工作区
uploads github link --status              # 只读:查看当前绑定状态,不执行绑定
uploads github unlink --repo owner/name   # 解除当前工作区拥有的仓库绑定
uploads github doctor                     # 检查App本身(配置 + Webhook事件)
声明已绑定的仓库不会抢占绑定——命令会报告现有所有者。
unlink
仅能解除当前工作区拥有的绑定;若绑定属于其他工作区,会返回403错误(管理员可从控制面板重新分配或移除绑定)。在不支持这些路由的旧版/自托管服务器上,命令会失败并显示清晰的“server does not support repo bindings/GitHub App health check yet”信息。
声明未绑定仓库需要授权,并非先到先得(issue #297):仅当工作区关联的GitHub账户拥有仓库推送(或更高)权限时,服务器才允许工作区进行首次绑定,该检查会通过App的安装令牌实时向GitHub验证。无关联GitHub身份的令牌——旧版/注册/共享令牌,包括
default
令牌——永远无法声明新仓库,但在已绑定的仓库上仍可正常工作。当验证失败时,声明会返回
claimed: false, reason: "not_authorized"
;请关联拥有仓库推送权限的GitHub账户,或请管理员从控制面板显式绑定仓库。
comment
/
attach --comment
返回
not_authorized
意味着仓库已绑定至其他工作区,或未绑定且当前工作区无权限声明(见上文)。无论哪种情况,都会直接拒绝,不会降级——CLI在这种情况下不会
回退至本地
gh
发布,与其他机器人发布失败不同。运行
uploads github link --status
查看当前绑定状态(若有),切换至关联拥有权限的GitHub账户的工作区,或请管理员显式绑定仓库。
uploads github doctor
会检查App的配置和Webhook事件订阅(需要
issues
+
pull_request
;建议订阅
issue_comment
,以便被删除/损坏的机器人评论自动修复,无需等待下一次PR推送)——当Webhook驱动的行为(自动升级、标题更新、自动修复)似乎静默无响应时,该命令非常有用。

Mirroring GitHub-native attachments (
uploads ingest
)

镜像GitHub原生附件(
uploads ingest

Images someone drops straight into a PR/issue via
github.com/user-attachments/…
only exist behind GitHub's own authenticated hosting — they're never public URLs.
uploads ingest --pr <n>
(or
--issue <n>
) scans the description and comments for that media, mirrors any new ones into the workspace (indexed, not added to the managed comment), and detaches ones no longer referenced — a reattached one un-detaches without a re-fetch:
bash
uploads ingest --pr 123
uploads ingest --issue 45 --repo owner/name --json
Requires the repo be linked to the workspace (
uploads github link
) and the GitHub App installed — otherwise it fails with a clear error rather than guessing. This is the manual/backfill entry point; the
.uploads.yml
ingestGithubAttachments
knob only gates the automatic webhook path and has no effect on running
ingest
directly.
直接通过
github.com/user-attachments/…
上传至PR/issue的图片仅存在于GitHub的认证托管服务后——并非公共URL。
uploads ingest --pr <n>
(或
--issue <n>
)会扫描描述和评论中的媒体内容,将新内容镜像至工作区(建立索引,不会添加至托管评论),并分离不再被引用的内容——重新引用的内容会重新关联,无需重新获取:
bash
uploads ingest --pr 123
uploads ingest --issue 45 --repo owner/name --json
需要仓库已链接至工作区(
uploads github link
)且已安装GitHub App——否则会失败并显示清晰错误,而非猜测。这是手动/回填入口;
.uploads.yml
中的
ingestGithubAttachments
开关仅控制自动Webhook路径,对直接运行
ingest
命令无影响。

Embedding best practices

嵌入最佳实践

  • Meaningful alt text, always — it's what readers with images off and search see.
  • Constrain width on large shots with
    --width
    so they don't dominate the page.
  • Before/after reads best side by side in a table:
    markdown
    | Before                               | After                               |
    | ------------------------------------ | ----------------------------------- |
    | <img width="380" src="…/before.png"> | <img width="380" src="…/after.png"> |
  • Prefer writing the body to a file and using
    gh pr edit --body-file
    /
    gh issue comment --body-file
    over inline HEREDOCs.
  • The host is agnostic — the same URLs work in issues, PR comments, discussions, and plain markdown docs.
  • Prefer short, compressed clips (or an animated capture) over large raw video files when attaching to PRs. Per-file and workspace storage caps apply underneath, and a multi-minute uncompressed recording is a poor PR embed regardless — trim to the relevant seconds and compress before uploading.
  • 始终添加有意义的替代文本——这是关闭图片的读者和搜索工具看到的内容。
  • 使用
    --width
    限制大截图的宽度
    ,避免页面被截图占据。
  • 前后对比内容并排显示效果最佳,可使用表格:
    markdown
    | Before                               | After                               |
    | ------------------------------------ | ----------------------------------- |
    | <img width="380" src="…/before.png"> | <img width="380" src="…/after.png"> |
  • 建议将正文写入文件,使用
    gh pr edit --body-file
    /
    gh issue comment --body-file
    ,而非内联HEREDOC。
  • 宿主无关——相同URL可用于issue、PR评论、讨论和普通Markdown文档。
  • PR附件优先选择短时长压缩剪辑(或动态捕获),而非大型原始视频文件。底层会应用单文件和工作区存储上限,多分钟未压缩录屏作为PR嵌入体验不佳——裁剪至相关片段并压缩后再上传。

Managing uploads

管理上传文件

bash
uploads list --prefix screenshots/        # list objects (key + url)
uploads list --pr 123                      # everything attached to a PR
uploads list --meta app=myapp              # filter by metadata (repeatable, ANDed)
uploads list --name hero --meta app=web    # filename substring (+ optional meta)
uploads find app=myapp path=/settings      # same filter, human-friendly positional pairs
uploads find hero                          # filename substring alone
uploads list --all --json                  # paginate fully, machine-readable
uploads meta get <key>                     # show an object's metadata
uploads meta set <key> k=v [k=v…] [--delete k]# merge-set / delete metadata pairs
uploads meta set <key> --meta k=v                  # same, in put/list's flag spelling
uploads meta keys                          # discover workspace metadata keys
uploads meta values <meta-key>             # distinct values for one key
uploads delete <key>                       # remove an object
uploads delete <key> --dry-run             # show what would be deleted
uploads usage                              # storage / monthly upload counters (+ limits)
uploads reconcile                          # rebuild ledger from storage
uploads purge-expired                      # delete past retentionDays (if set)
uploads health                             # API liveness (no auth)
uploads doctor                             # version + health + auth + workspace + usage
uploads --version
doctor
is the first thing to run when something's off — it reports the installed CLI version, distinguishes a down API / bad token / workspace mismatch / local-vs-prod URL, and prints targeted hints.
Destructive preview:
delete
supports
--dry-run
.
purge-expired
does not yet (#78); preview via
list
/
usage
and retention settings.
bash
uploads list --prefix screenshots/        # 列出对象(密钥 + URL)
uploads list --pr 123                      # 列出附加至指定PR的所有文件
uploads list --meta app=myapp              # 按元数据筛选(可重复,多条件AND)
uploads list --name hero --meta app=web    # 文件名子串(+ 可选元数据)
uploads find app=myapp path=/settings      # 相同筛选条件,更友好的位置参数对
uploads find hero                          # 仅按文件名子串筛选
uploads list --all --json                  # 完整分页,机器可读
uploads meta get <key>                     # 查看对象的元数据
uploads meta set <key> k=v [k=v…] [--delete k]# 合并设置 / 删除元数据对
uploads meta set <key> --meta k=v                  # 相同操作,使用put/list的标志拼写
uploads meta keys                          # 发现工作区的元数据键
uploads meta values <meta-key>             # 查看某个键的所有不同值(含计数)
uploads delete <key>                       # 删除对象
uploads delete <key> --dry-run             # 预览会删除的内容
uploads usage                              # 存储 / 月度上传计数器(+ 限制)
uploads reconcile                          # 从存储重建分类账
uploads purge-expired                      # 删除超过retentionDays的文件(若设置)
uploads health                             # API存活状态(无需认证)
uploads doctor                             # 版本 + 健康状态 + 认证信息 + 工作区 + 使用情况
uploads --version
出现问题时,首先运行
doctor
命令——它会报告已安装的CLI版本,区分API宕机/令牌错误/工作区不匹配/本地与生产URL差异,并打印针对性提示。
破坏性操作预览
delete
命令支持
--dry-run
参数。
purge-expired
暂不支持(#78);可通过
list
/
usage
和保留设置预览。

Config commands

配置命令

Set shared defaults once instead of passing flags every time:
bash
uploads config show                              # effective settings (token redacted)
uploads config path                              # resolved config file path
uploads config set UPLOADS_DEFAULT_REPO myorg/myapp
uploads config set UPLOADS_DEFAULT_WIDTH 700
uploads config init --api-url http://localhost:8787 --workspace acme --token up_acme_…
init
writes only the keys you pass. With no flags it seeds
UPLOADS_API_URL
alone and deliberately sets no workspace: a
UPLOADS_WORKSPACE
in the config file outranks the workspace encoded in your token, so seeding one would pin every later
uploads login
to it. Pass
--workspace
when you want it fixed.
Recognized keys:
UPLOADS_API_URL
,
UPLOADS_WORKSPACE
,
UPLOADS_TOKEN
,
UPLOADS_DEFAULT_PREFIX
,
UPLOADS_DEFAULT_REPO
,
UPLOADS_DEFAULT_REF
,
UPLOADS_DEFAULT_WIDTH
,
UPLOADS_NO_GIT
,
UPLOADS_NO_OPTIMIZE
,
UPLOADS_KEEP_EXIF
,
UPLOADS_NO_AUTO_META
,
UPLOADS_SCREENSHOT_VIA
,
UPLOADS_NO_NUDGE
. Also read (env only, not config-file keys):
UPLOADS_EMBED_PUBLIC_BASE_URL
,
UPLOADS_OVERWRITE
.
一次性设置共享默认值,无需每次传递标志:
bash
uploads config show                              # 显示有效设置(令牌已脱敏)
uploads config path                              # 显示解析后的配置文件路径
uploads config set UPLOADS_DEFAULT_REPO myorg/myapp
uploads config set UPLOADS_DEFAULT_WIDTH 700
uploads config init --api-url http://localhost:8787 --workspace acme --token up_acme_…
init
仅写入传递的键。无标志时仅初始化
UPLOADS_API_URL
,且故意不设置工作区:配置文件中的
UPLOADS_WORKSPACE
优先级高于令牌中的工作区信息,因此初始化时设置工作区会固定后续所有
uploads login
的工作区。若需固定工作区,请传递
--workspace
参数。
支持的键:
UPLOADS_API_URL
UPLOADS_WORKSPACE
UPLOADS_TOKEN
UPLOADS_DEFAULT_PREFIX
UPLOADS_DEFAULT_REPO
UPLOADS_DEFAULT_REF
UPLOADS_DEFAULT_WIDTH
UPLOADS_NO_GIT
UPLOADS_NO_OPTIMIZE
UPLOADS_KEEP_EXIF
UPLOADS_NO_AUTO_META
UPLOADS_SCREENSHOT_VIA
UPLOADS_NO_NUDGE
。还支持读取(仅环境变量,非配置文件键):
UPLOADS_EMBED_PUBLIC_BASE_URL
UPLOADS_OVERWRITE

Local development

本地开发

Point at a locally running API (
pnpm dev
serves it on
:8787
). Tokens minted with
workspace:add --local
only work against localhost; prod tokens need
UPLOADS_API_URL=https://api.uploads.sh
.
doctor
flags this mismatch for you.
bash
uploads --api-url http://localhost:8787 doctor
指向本地运行的API(
pnpm dev
会在
:8787
端口启动)。使用
workspace:add --local
生成的令牌仅对localhost有效;生产环境令牌需要设置
UPLOADS_API_URL=https://api.uploads.sh
doctor
会标记这种不匹配。
bash
uploads --api-url http://localhost:8787 doctor

Notes and cautions

注意事项

  • Uploads are public and effectively permanent until deleted. GitHub repository visibility is not an access control: private/internal PR and issue attachments remain public, and
    gh/<owner>/<repo>/pull|issues/<num>/<filename>
    keys are predictable. Never upload secrets, tokens, internal dashboards with sensitive data, or customer PII visible in a shot — crop/redact first.
  • Edge cache / dual host: stable
    url
    responses carry
    Cache-Control: max-age=60
    . For GitHub, use
    embedUrl
    (no-cache host) so overwrites propagate through Camo. Prefer CLI/MCP
    markdown
    rather than hand-building storage URLs into PR bodies. See repo
    docs/ops.md
    (dual public hosts).
  • Exit codes:
    2
    usage/token/file,
    3
    auth/policy,
    4
    network,
    1
    other.
    --json
    emits
    {error,code,status}
    — branch on
    code
    . Scripted formats (
    json|url|markdown
    ) also print failures on stdout. Usage errors:
    hint: uploads <cmd> --help
    .
  • Errors stay short (stderr), so trimming output never hides them. A missing argument prints one
    error:
    line, a runnable example (
    uploads put ./shot.png --pr 123
    ), and that hint — not the command's help. In
    --json
    the example rides along as
    example
    . A mistyped command prints the error, a
    did you mean: uploads <cmd>
    line, and the help pointers; with
    --json
    it returns
    {error,code:"USAGE",didYouMean}
    on stdout. Read the first line; run
    uploads <cmd> --help
    for the full help.
  • Update hints (stderr): successful human runs may note a newer npm release (daily). Silence with
    --quiet
    /
    --json
    /
    UPLOADS_NO_UPDATE=1
    .
  • Telemetry: anonymous command-name pings (no paths/tokens). Opt out with
    UPLOADS_TELEMETRY_DISABLED=1
    ,
    DO_NOT_TRACK=1
    , or
    uploads telemetry disable
    .
  • Reports: only when the user asks —
    uploads report "what broke"
    or
    --file ./trace.log
    . Never auto-send logs. MCP tool:
    report
    .
  • MCP:
    uploads mcp
    (stdio) mirrors CLI tools; hosted MCP at
    https://agents.uploads.sh/mcp
    — the one to reach for when an agent has no local filesystem or git checkout to shell out from (send base64 content directly). Metadata:
    get_metadata
    /
    set_metadata
    /
    find_files
    /
    list_metadata_keys
    (same as
    meta get
    /
    meta set
    /
    find
    /
    meta keys
    |
    meta values
    ).
    find_files
    accepts optional
    name
    (filename substring) with or without
    filters
    . Both support multi-file
    put
    in one call — stdio takes
    files
    as paths, hosted takes
    files: [{ filename, contentBase64, alt? }]
    (max 20/call; per-item
    alt
    overrides the top-level one) — returning
    { uploads, failures }
    with per-item results.
    uploads install
    sets up this skill + hosted MCP + Grok/Cursor hooks (short progress;
    --verbose
    /
    --dry-run
    available). Claude and Codex ship the same pre-PR reminder via their plugins (
    uploads hook pre-pr-screenshot
    ).
    Hosted MCP
    put
    comment parity (issue #392).
    The hosted
    put
    tool accepts
    pr
    /
    issue
    (mutually exclusive, mirroring the CLI's
    --pr
    /
    --issue
    ) plus a required
    repo
    (
    owner/name
    — the hosted server has no git context to infer it from) to get the same stable
    gh/…
    key the CLI produces. With
    pr
    /
    issue
    the managed
    uploads-sh[bot]
    attachments comment is posted/updated by default, same as CLI
    put --pr
    (#537) — pass
    comment: false
    to skip it. Bot-only on this server, no local-
    gh
    fallback; the default sync needs the
    files:read
    scope and is silently skipped on a write-only token (explicit
    comment: true
    errors instead). Prefer
    pr
    /
    issue
    over just returning a raw
    url
    /
    embedUrl
    whenever the caller is going to paste the result into a PR/issue: it gets the stable overwrite-in-place key and lands straight in the collected attachments comment instead of a one-off link the caller has to hand-embed. A comment failure never fails the upload — it's returned honestly in the result's
    comment
    field as one of
    not_installed
    (App not installed on the repo),
    not_authorized
    (repo bound to a different workspace, or unbound and this workspace isn't entitled to claim it), or
    forbidden
    (App installed but Issues/PR write access not yet approved — includes a
    fixUrl
    to the org's permission-review page), never as a thrown tool error; an unexpected error surfaces separately as
    commentError
    .
    Hosted MCP standalone
    comment
    tool.
    The hosted server also has a
    comment
    tool (
    { repo, pr | issue }
    ,
    repo
    required for the same no-git-context reason) that refreshes the managed comment without re-uploading — the hosted equivalent of CLI
    uploads comment
    . Use it to re-sync after deleting an asset:
    delete
    the
    gh/…
    key, then call
    comment
    . When the last attachment and gallery are gone the comment is rewritten in place to a neutral empty state (never deleted, never created empty). It is bot-only with the same honest declines as
    put
    's comment field. Both this tool and
    put
    with
    pr
    /
    issue
    honor the target repo's
    .uploads.yml
    (same as the bot path — no separate MCP config; see https://uploads.sh/docs/comment-config).
    Hosted MCP: branch staging + promote. There is still no
    attach
    tool on the hosted server (no filesystem paths) — use
    put
    instead:
    text
    # Stage pre-PR (CLI attach --branch parity). repo + branch required.
    put  { contentBase64, filename, repo: "owner/name", branch: "feature/x", state: "after" }
    # → key gh/owner/name/branch/feature-x/<filename>, gh.status=staged
    
    # Promote staged files into a PR once it exists (CLI attach --promote).
    promote  { repo: "owner/name", pr: 123, branch: "feature/x" }
    # optional comment: false to skip the managed comment refresh (default on)
    
    # Or attach a new file to the PR and promote that branch in one call:
    put  { contentBase64, filename, repo: "owner/name", pr: 123, branch: "feature/x" }
    Hosted MCP: checking what's staged (issue #405). There's no dedicated
    staged
    tool on the hosted server — it has no local git context to default
    branch
    from, so it always needs the caller's own
    repo
    /
    branch
    . Answer "what's staged?" with the existing tools instead:
    text
    list  { prefix: "gh/<owner>/<repo>/branch/<branch>/" }
    # or
    find_files  { filters: { "gh.branch": "<branch>" } }
    find_files
    returns each match's metadata inline, so
    gh.staged-at
    gives recency without a second call; add
    "gh.repo": "<owner>/<repo>"
    to the filters when branch names aren't unique across repos you're working in. For the binding question ("will these auto-attach?"), use the hosted
    repo_link_status
    tool (issue #422):
    text
    repo_link_status  { repo: "<owner>/<repo>" }
    It returns
    { binding: "self" | "other" | "none" }
    :
    "self"
    means this repo is bound to this workspace and staged files will auto-attach,
    "other"
    means it's bound to a different workspace and they won't — deliberately without ever naming that workspace — and
    "none"
    means the repo is unbound.
  • Agents on the Worker side: the package also exports
    createUploadsWorkerFileTools()
    from
    @buildinternet/uploads/agent
    for exposing upload/list/delete as AI-SDK tools inside a Worker — only relevant if you're building agent tooling that runs on the server, not for everyday PR embeds.
  • 上传文件为公共文件,且实际上是永久的,直到被删除。GitHub仓库可见性并非访问控制:私有/内部PR和issue的附件仍为公共文件,
    gh/<owner>/<repo>/pull|issues/<num>/<filename>
    密钥是可预测的。永远不要上传密钥、令牌、包含敏感数据的内部仪表板,或截图中可见的客户PII——请先裁剪/遮敏感内容。
  • 边缘缓存 / 双宿主:稳定
    url
    响应携带
    Cache-Control: max-age=60
    头。针对GitHub,请使用
    embedUrl
    (无缓存宿主),以便覆盖操作可通过Camo传播。建议使用CLI/MCP的
    markdown
    输出,而非手动构建存储URL至PR正文。详情见仓库
    docs/ops.md
    (双公共宿主)。
  • 退出码
    2
    表示使用/令牌/文件错误,
    3
    表示认证/策略错误,
    4
    表示网络错误,
    1
    表示其他错误。
    --json
    会输出
    {error,code,status}
    ——可根据
    code
    分支处理。脚本化格式(
    json|url|markdown
    )也会在stdout打印失败信息。使用错误会提示:
    hint: uploads <cmd> --help
  • 错误信息简短(stderr),因此裁剪输出不会隐藏错误。缺少参数会打印一行
    error:
    信息、一个可运行示例(
    uploads put ./shot.png --pr 123
    ),以及提示——不会打印命令帮助。
    --json
    输出会包含
    example
    字段。命令拼写错误会打印错误信息、
    did you mean: uploads <cmd>
    行,以及帮助指针;
    --json
    会在stdout返回
    {error,code:"USAGE",didYouMean}
    。请查看第一行错误信息;运行
    uploads <cmd> --help
    获取完整帮助。
  • 更新提示(stderr):成功的人类可读模式运行可能会提示npm新版本(每日检查)。可通过
    --quiet
    /
    --json
    /
    UPLOADS_NO_UPDATE=1
    抑制。
  • 遥测:匿名命令名称 ping(无路径/令牌)。可通过
    UPLOADS_TELEMETRY_DISABLED=1
    DO_NOT_TRACK=1
    uploads telemetry disable
    选择退出。
  • 报告:仅当用户请求时生成——
    uploads report "what broke"
    --file ./trace.log
    。永远不会自动发送日志。MCP工具:
    report
  • MCP
    uploads mcp
    (标准输入输出)镜像CLI工具;托管MCP位于
    https://agents.uploads.sh/mcp
    ——当代理无本地文件系统或git检出权限时,可使用该服务(直接发送base64内容)。元数据操作:
    get_metadata
    /
    set_metadata
    /
    find_files
    /
    list_metadata_keys
    (与
    meta get
    /
    meta set
    /
    find
    /
    meta keys
    |
    meta values
    相同)。
    find_files
    接受可选的
    name
    (文件名子串),可配合或不配合
    filters
    使用。两者均支持单次调用上传多个文件——标准输入输出接受
    files
    为路径,托管服务接受
    files: [{ filename, contentBase64, alt? }]
    (每次调用最多20个文件;单个文件的
    alt
    会覆盖顶层
    alt
    )——返回
    { uploads, failures }
    ,包含每个文件的结果。
    uploads install
    会设置该技能+托管MCP+Grok/Cursor钩子(进度简短;支持
    --verbose
    /
    --dry-run
    参数)。Claude和Codex通过其插件提供相同的PR前提醒(
    uploads hook pre-pr-screenshot
    )。
    托管MCP
    put
    命令的评论一致性(issue #392)
    。托管
    put
    工具接受
    pr
    /
    issue
    (互斥,镜像CLI的
    --pr
    /
    --issue
    ),以及必填的
    repo
    owner/name
    ——托管服务器无git上下文可推断),以生成与CLI相同的稳定
    gh/…
    密钥。指定
    pr
    /
    issue
    时,默认会发布/更新托管的
    uploads-sh[bot]
    附件评论,与CLI
    put --pr
    相同(#537)——传递
    comment: false
    可跳过。该服务器仅支持机器人发布,无本地
    gh
    备选方案;默认同步需要
    files:read
    权限,若使用仅写令牌会静默跳过同步(显式
    comment: true
    会返回错误)。当调用者要将结果粘贴至PR/issue时,建议使用
    pr
    /
    issue
    参数,而非仅返回原始
    url
    /
    embedUrl
    :这样可获得稳定的原地覆盖密钥,并直接添加至收集的附件评论,而非需要手动嵌入的一次性链接。评论失败不会导致上传失败——会在结果的
    comment
    字段中如实返回
    not_installed
    (仓库未安装App)、
    not_authorized
    (仓库已绑定至其他工作区,或未绑定且当前工作区无权限声明)或
    forbidden
    (已安装App,但尚未批准Issues/PR写入权限——包含
    fixUrl
    指向组织权限审核页面),不会抛出工具错误;意外错误会单独显示为
    commentError
    托管MCP独立
    comment
    工具
    。托管服务器还提供
    comment
    工具(
    { repo, pr | issue }
    repo
    必填,原因同上无git上下文),用于刷新托管评论无需重新上传——相当于CLI的
    uploads comment
    命令。删除资源后可使用该命令重新同步:删除
    gh/…
    密钥,然后调用
    comment
    。当最后一个附件和画廊被移除后,评论会原地重写为中性空状态(永远不会删除,不会为空状态创建)。仅支持机器人发布,与
    put
    命令的评论字段有相同的如实拒绝逻辑。该工具和指定
    pr
    /
    issue
    put
    命令均遵循目标仓库的
    .uploads.yml
    配置(与机器人路径相同——无单独MCP配置;见https://uploads.sh/docs/comment-config)。
    托管MCP:分支暂存 + 升级。托管服务器仍无
    attach
    工具(无文件系统路径)——请使用
    put
    替代:
    text
    # PR创建前暂存(与CLI attach --branch一致)。必填repo + branch。
    put  { contentBase64, filename, repo: "owner/name", branch: "feature/x", state: "after" }
    # → 密钥gh/owner/name/branch/feature-x/<filename>, gh.status=staged
    
    # PR创建后升级暂存文件(与CLI attach --promote一致)。
    promote  { repo: "owner/name", pr: 123, branch: "feature/x" }
    # 可选comment: false跳过托管评论刷新(默认启用)
    
    # 或在一次调用中上传新文件至PR并升级该分支:
    put  { contentBase64, filename, repo: "owner/name", pr: 123, branch: "feature/x" }
    托管MCP:查看暂存文件(issue #405)。托管服务器无专用
    staged
    工具——无本地git上下文可默认
    branch
    ,因此始终需要调用者指定
    repo
    /
    branch
    。可使用现有工具回答“哪些文件已暂存?”:
    text
    list  { prefix: "gh/<owner>/<repo>/branch/<branch>/" }
    # 或
    find_files  { filters: { "gh.branch": "<branch>" } }
    find_files
    会在结果中包含每个匹配项的元数据,因此
    gh.staged-at
    可显示暂存时间,无需二次调用;当分支名称在多个仓库中不唯一时,可在筛选器中添加
    "gh.repo": "<owner>/<repo>"
    。关于绑定问题(“这些文件会自动附加吗?”),可使用托管
    repo_link_status
    工具(issue #422):
    text
    repo_link_status  { repo: "<owner>/<repo>" }
    返回
    { binding: "self" | "other" | "none" }
    "self"
    表示仓库已绑定至当前工作区,暂存文件会自动附加;
    "other"
    表示仓库已绑定至其他工作区,不会自动附加——故意不显示其他工作区名称;
    "none"
    表示仓库未绑定。
  • Worker端代理:包还从
    @buildinternet/uploads/agent
    导出
    createUploadsWorkerFileTools()
    ,用于在Worker内部将上传/列表/删除功能作为AI-SDK工具暴露——仅在构建服务器端代理工具时相关,日常PR嵌入无需关注。