upstash-box-cli

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
box
operates on a remote container, not this machine. Your own file and shell tools act locally; anything that must happen inside the box goes through
box
.
box
操作的是 远程容器,而不是本机。你自己的文件和 shell 工具在本地运行;任何必须在 box 内进行的操作都通过
box
完成。

Install

安装

bash
npm i -g @upstash/box-cli
bash
npm i -g @upstash/box-cli

Authentication

身份验证

Every command needs an API key, or it fails with "API token required". Set it once, or pass
--token
on any single command. Create one at https://console.upstash.com/box.
bash
export UPSTASH_BOX_API_KEY=box_...
每条命令都需要 API 密钥,否则会以 "API token required" 失败。设置一次,或在单条命令上传递
--token
。在 https://console.upstash.com/box 创建一个。
bash
export UPSTASH_BOX_API_KEY=box_...

Selecting a box

选择一个 box

Resolution order is
--box <id>
, then
$BOX_ID
, then the nearest
.box
file (searched upward). Create one and pin it to the working directory:
bash
box create --no-repl --runtime node                    # prints the id, writes .box
box create --no-repl --runtime node --clone-repo https://github.com/org/repo
box list                                               # find an existing box
box use <box-id>                                       # pin one to this directory
box status                                             # id, where it came from, state
--keep-alive
,
--browser
,
--env
and
--size
can only be chosen at create time; to change any of them you make a new box. There is no resize.
Default to a plain
box create --no-repl
. A plain box pauses when it goes idle and resumes on the next command, which is what almost all work wants:
bash
box create --no-repl --browser             # provision a headless Chromium
box create --no-repl --env KEY=VAL         # env for this box (repeatable)
box create --no-repl --size medium         # small (default), medium, large
Add
--keep-alive
only when something has to survive an idle gap: a detached server you are about to reach over a preview URL, or a job that keeps running between commands. It stops the box pausing, so the box keeps costing money until you pause or delete it.
--init-command
is rejected without it:
bash
box create --no-repl --keep-alive                            # stays up when idle
box create --no-repl --keep-alive --init-command "npm ci"    # startup script
The rest of the create-time options, all equally unchangeable afterwards:
bash
box create --no-repl --skill upstash/skills/redis      # repeatable
box create --no-repl --mcp docs=@org/mcp-server        # or name=https://url
box create --no-repl --mcp-file servers.json           # for args and headers
box create --no-repl --network-policy deny-all         # or allow-all, or custom
box create --no-repl --network-policy custom --allow-domain api.example.com
box create --no-repl --attach-headers-file headers.json
--attach-headers-file
holds a JSON object keyed by host pattern (
{"api.stripe.com": {"Authorization": "Bearer ..."}}
), and those headers are injected into matching outbound requests from the box. There is an
--attach-header host:Name=value
form too, but the value lands in
ps
and in shell history, so prefer the file for anything secret.
A skill id has three parts,
owner/repo/skill-name
. A malformed one is only warned about server-side, so the box comes up with the skill silently absent.
--env
is per-box.
box env set
is account-level: it is merged into every box created afterwards, never into one that already exists. A per-box
--env
wins for the same key, so account-level values only fill in what the box did not set. Account-level skills and MCP servers are merged the same way.
paused
is not an error; the next command resumes the box.
A
.box
file is found by walking up from the working directory, so
cd
-ing into another project can silently pick up a pin left there earlier and run against the wrong box. In any session touching more than one box, pass
--box
explicitly;
box status
says which box it resolved and where that came from.
Clean up when the work is done. Boxes cost money while they exist:
bash
box pause                   # keeps the workspace, resumes on the next command
box resume                  # rarely needed; any command resumes a paused box
box delete --yes            # irreversible; --yes is required without a terminal
Never run
box create
or
box connect
without
--no-repl
: they open an interactive REPL and will hang.
box from-snapshot
takes
--no-repl
too.
bash
box snapshot                                # snapshot this box, prints the id
box snapshot list
box from-snapshot <snapshot-id> --no-repl   # restore into a new box, pinned
box snapshot delete <snapshot-id>
解析顺序是
--box <id>
,然后是
$BOX_ID
,然后是最近的
.box
文件(向上查找)。创建一个并将其固定到当前工作目录:
bash
box create --no-repl --runtime node                    # prints the id, writes .box
box create --no-repl --runtime node --clone-repo https://github.com/org/repo
box list                                               # find an existing box
box use <box-id>                                       # pin one to this directory
box status                                             # id, where it came from, state
--keep-alive
、
--browser
、
--env
和
--size
只能在创建时选择;要更改其中任何一项,需要新建一个 box。没有 resize 功能。
默认使用普通的
box create --no-repl
。普通 box 空闲时会暂停,并在下一条命令时恢复,这正是大多数工作所需要的:
bash
box create --no-repl --browser             # provision a headless Chromium
box create --no-repl --env KEY=VAL         # env for this box (repeatable)
box create --no-repl --size medium         # small (default), medium, large
仅当某些内容需要在空闲间隙中保持存活时才添加
--keep-alive
:例如你即将通过预览 URL 访问的分离服务器,或在命令之间持续运行的作业。它会阻止 box 暂停,因此 box 会持续计费,直到你暂停或删除它。没有它时
--init-command
会被拒绝:
bash
box create --no-repl --keep-alive                            # stays up when idle
box create --no-repl --keep-alive --init-command "npm ci"    # startup script
其余创建时选项,之后同样都不可更改:
bash
box create --no-repl --skill upstash/skills/redis      # repeatable
box create --no-repl --mcp docs=@org/mcp-server        # or name=https://url
box create --no-repl --mcp-file servers.json           # for args and headers
box create --no-repl --network-policy deny-all         # or allow-all, or custom
box create --no-repl --network-policy custom --allow-domain api.example.com
box create --no-repl --attach-headers-file headers.json
--attach-headers-file
保存一个以主机模式为键的 JSON 对象(
{"api.stripe.com": {"Authorization": "Bearer ..."}}
),这些请求头会被注入到来自 box 的匹配出站请求中。也有
--attach-header host:Name=value
形式,但该值会出现在
ps
和 shell 历史中,所以任何机密信息请优先使用文件。
skill id 由三部分组成:
owner/repo/skill-name
。格式错误的 id 只会在服务端给出警告,因此 box 启动时该 skill 会静默缺失。
--env
是 per-box 的。
box env set
是账户级别的:它会合并到之后创建的每个 box 中,而不会合并到已存在的 box。对于相同键,per-box 的
--env
优先,因此账户级值只填充 box 未设置的部分。账户级 skills 和 MCP servers 也以同样方式合并。
paused
不是错误;下一条命令会恢复 box。
.box
文件是从工作目录向上查找的,因此
cd
到另一个项目可能会静默拾取之前留在那里的固定配置,并针对错误的 box 运行。在任何涉及多个 box 的会话中,请显式传递
--box
;
box status
会显示解析到了哪个 box 以及来自哪里。
工作完成后请清理。box 存在期间会持续计费:
bash
box pause                   # keeps the workspace, resumes on the next command
box resume                  # rarely needed; any command resumes a paused box
box delete --yes            # irreversible; --yes is required without a terminal
永远不要在没有
--no-repl
的情况下运行
box create
或
box connect
:它们会打开交互式 REPL 并挂起。
box from-snapshot
也需要
--no-repl
。
bash
box snapshot                                # snapshot this box, prints the id
box snapshot list
box from-snapshot <snapshot-id> --no-repl   # restore into a new box, pinned
box snapshot delete <snapshot-id>

Running commands

运行命令

Put the remote command after
--
, or its flags are parsed as
box
's own.
bash
box exec -- npm install
box exec -C repo -- npm test
box exec --json -- node -e 'console.log(1)'   # {stdout, stderr, exit_code}
The remote shell is
sh
, not bash. A heredoc inside
box exec
fails with
Syntax error: redirection unexpected
; write the file with
box files write - 
instead, or wrap the command in
bash -c
when the box has bash.
For an interactive shell, ssh straight in. The box id is the user and the Box API key is the password:
bash
ssh <box-id>@us-east-1.box.upstash.com
Commands run as
boxuser
, so a global npm install needs sudo, which is passwordless:
bash
box exec -- 'sudo npm install -g @upstash/docs7'   # EACCES without sudo
One argument is a shell expression, sent as written, so pipes and redirection work. Several arguments are argv and are quoted individually, so an argument containing spaces stays one argument.
The remote command's exit code is passed through, so
box exec -- npm test && ...
chains normally. Exit code 125 means the CLI itself failed (bad box, bad flags), never a status the remote command returned.
A background server dies with the command that started it. Detach it:
bash
box exec -- '( npm run dev > dev.log 2>&1 & )'
box public-url 3000                               # prints the public URL
box public-url list
box public-url delete 3000
Inline code, when a shell one-liner would be worse than a program:
bash
box code - --lang python < script.py
box code 'console.log(1 + 1)' --lang js
将远程命令放在
--
之后,否则其标志会被解析为
box
自己的标志。
bash
box exec -- npm install
box exec -C repo -- npm test
box exec --json -- node -e 'console.log(1)'   # {stdout, stderr, exit_code}
远程 shell 是
sh
,不是 bash。在
box exec
中使用 heredoc 会以
Syntax error: redirection unexpected
失败;请改用
box files write - 
写入文件,或者在 box 有 bash 时用
bash -c
包裹命令。
如需交互式 shell,直接 ssh 进入。box id 是用户名,Box API 密钥是密码:
bash
ssh <box-id>@us-east-1.box.upstash.com
命令以
boxuser
身份运行,因此全局 npm install 需要 sudo,且 sudo 免密:
bash
box exec -- 'sudo npm install -g @upstash/docs7'   # EACCES without sudo
单个参数是 shell 表达式,按原样发送,因此管道和重定向有效。多个参数是 argv,会逐个加引号,因此包含空格的参数仍是一个参数。
远程命令的退出码会被透传,因此
box exec -- npm test && ...
可以正常串联。退出码 125 表示 CLI 自身失败(box 或标志错误),绝不是远程命令返回的状态。
后台服务器会随启动它的命令一起退出。要分离它:
bash
box exec -- '( npm run dev > dev.log 2>&1 & )'
box public-url 3000                               # prints the public URL
box public-url list
box public-url delete 3000
当 shell 一行命令不如一段程序时,可以使用内联代码:
bash
box code - --lang python < script.py
box code 'console.log(1 + 1)' --lang js

Building something and handing back a link

构建东西并交回链接

"Make me a snake game, use Upstash Box" is a request to build it in a box, run it there, and reply with a URL the user can open. Do the whole thing; do not stop at writing the file.
bash
box create --no-repl --runtime node --keep-alive   # writes .box; stays up for the URL

box files write index.html - <<'HTML'
<!doctype html><meta charset="utf-8"><title>Snake</title>
<canvas id="c" width="400" height="400"></canvas>
<script>/* the game */</script>
HTML

box files write server.js - <<'JS'
const http = require("http"), fs = require("fs");
http.createServer((_, res) => {
  res.writeHead(200, { "Content-Type": "text/html" });
  res.end(fs.readFileSync("index.html"));
}).listen(3000, "0.0.0.0");   // the default binds ::, which the check below misses
JS

box exec -- '( node server.js > server.log 2>&1 & )'          # detached, or it dies
box exec -- 'sleep 1; ss -ltn | grep -q "0.0.0.0:3000" && echo up'
box public-url 3000                                           # the link to reply with
Node's own
http
module rather than a package: no install, no network fetch, and it works on a bare
node
runtime.
Check the port before publishing it, and check what it is bound to, not just that it answers. A server on
127.0.0.1
replies to a curl from inside the box and still cannot be published: the proxy reaches the container by address, so
box public-url
returns 502. That is why the check above greps for
0.0.0.0
rather than curling localhost, which passes in exactly the case that fails.
Most dev servers need telling:
--host 0.0.0.0
for Vite and many others,
-H 0.0.0.0
for some, and a few cannot be moved off loopback at all.
--keep-alive
is what keeps the link working. Without it the box pauses when idle, the detached server dies with it, and the URL you handed over starts answering errors some minutes later.
Reply with the URL itself, not just "it is running". Say that the box keeps costing money until
box delete --yes
, and that the URL is public to anyone who has it —
box public-url 3000 --basic-auth
puts credentials in front of it.
“用 Upstash Box 给我做个贪吃蛇游戏”是要求在一个 box 中构建、运行,并回复一个用户可以打开的 URL。请完成整个过程;不要停留在写完文件。
bash
box create --no-repl --runtime node --keep-alive   # writes .box; stays up for the URL

box files write index.html - <<'HTML'
<!doctype html><meta charset="utf-8"><title>Snake</title>
<canvas id="c" width="400" height="400"></canvas>
<script>/* the game */</script>
HTML

box files write server.js - <<'JS'
const http = require("http"), fs = require("fs");
http.createServer((_, res) => {
  res.writeHead(200, { "Content-Type": "text/html" });
  res.end(fs.readFileSync("index.html"));
}).listen(3000, "0.0.0.0");   // the default binds ::, which the check below misses
JS

box exec -- '( node server.js > server.log 2>&1 & )'          # detached, or it dies
box exec -- 'sleep 1; ss -ltn | grep -q "0.0.0.0:3000" && echo up'
box public-url 3000                                           # the link to reply with
使用 Node 自带的
http
模块而不是包:无需安装、无需网络获取,并且可以在裸
node
运行时上工作。
在发布端口前检查它,并检查它绑定到什么地址,而不仅仅是它能否响应。绑定在
127.0.0.1
的服务器可以响应 box 内部的 curl,但仍然无法发布:代理通过地址访问容器,因此
box public-url
会返回 502。这就是上面检查 grep
0.0.0.0
而不是 curl localhost 的原因——curl localhost 恰好会在失败的情况下通过。
大多数开发服务器需要明确指定:Vite 和许多其他服务器用
--host 0.0.0.0
,有些用
-H 0.0.0.0
,少数完全无法移出 loopback。
--keep-alive
是保持链接可用的关键。没有它,box 会在空闲时暂停,分离的服务器也会随之退出,你交出去的 URL 几分钟后就会开始返回错误。
回复时给出 URL 本身,而不仅仅是“它正在运行”。要说明 box 会持续计费直到
box delete --yes
,并且该 URL 对任何拥有它的人都是公开的——
box public-url 3000 --basic-auth
可以为它加上凭据保护。

Files

文件

Paths are relative to
/workspace/home
.
bash
box files list src
box files read src/index.ts
box files write src/app.ts -    < local.ts    # - reads stdin: use this for code
box files write notes.txt "short text"
box files stat src/index.ts
box files mkdir -p a/b/c
box files rename old.ts new.ts
box files remove build -r                      # a directory needs -r
box files upload ./local.zip /workspace/home/local.zip
box files download repo                        # a folder lands in ./repo
box files download logs/app.log -o ./app.log   # a file; -o names the destination
Write code with
-
and stdin. Passing source as an argument mangles it in the shell.
To search, use the box's own tools:
box exec -- grep -rn TODO src
.
路径相对于
/workspace/home
。
bash
box files list src
box files read src/index.ts
box files write src/app.ts -    < local.ts    # - reads stdin: use this for code
box files write notes.txt "short text"
box files stat src/index.ts
box files mkdir -p a/b/c
box files rename old.ts new.ts
box files remove build -r                      # a directory needs -r
box files upload ./local.zip /workspace/home/local.zip
box files download repo                        # a folder lands in ./repo
box files download logs/app.log -o ./app.log   # a file; -o names the destination
使用
-
和 stdin 编写代码。将源码作为参数传递会在 shell 中导致内容损坏。
要搜索,请使用 box 自己的工具:
box exec -- grep -rn TODO src
。

Git

Git

A clone lands in a directory named after the repo, and every git verb except
clone
needs that directory via
-C
. Without it git runs at the workspace root, which is not a repository.
bash
box git clone https://github.com/org/repo
box git clone https://github.com/org/repo -C my-app   # -C is the destination here
box git status -C repo
box git diff -C repo
box git config -C repo --name "Bot" --email bot@example.com
box git checkout -C repo feature/x             # creates the branch if missing
box git exec -C repo -- add -A
box git commit -C repo -m "message"
box git push -C repo                           # pushes the checked-out branch
box git create-pr -C repo --title "Fix the thing" --base main
box git create-pr -C repo --title "Fix the thing" --body-file notes.md
box git create-issue -C repo --title "Search returns nothing"
Use
--body-file
for anything longer than a sentence: a body worth writing does not survive shell quoting.
-
reads stdin.
box git exec
takes git's arguments without the leading
git
, and passes git's exit code through.
Private repos and PRs need a token at creation:
box create --no-repl --git-token $GITHUB_TOKEN
.
clone 会落在一个以仓库命名的目录中,除
clone
外的每个 git 动词都需要通过
-C
指定该目录。否则 git 会在工作区根目录运行,而那里不是仓库。
bash
box git clone https://github.com/org/repo
box git clone https://github.com/org/repo -C my-app   # -C is the destination here
box git status -C repo
box git diff -C repo
box git config -C repo --name "Bot" --email bot@example.com
box git checkout -C repo feature/x             # creates the branch if missing
box git exec -C repo -- add -A
box git commit -C repo -m "message"
box git push -C repo                           # pushes the checked-out branch
box git create-pr -C repo --title "Fix the thing" --base main
box git create-pr -C repo --title "Fix the thing" --body-file notes.md
box git create-issue -C repo --title "Search returns nothing"
对于超过一句话的内容,请使用
--body-file
:值得写的正文经不起 shell 引用的折腾。
-
从 stdin 读取。
box git exec
接受不带
git
前缀的 git 参数,并透传 git 的退出码。
私有仓库和 PR 在创建时需要 token:
box create --no-repl --git-token $GITHUB_TOKEN
。

Attaching a screenshot to a pull request or issue

将截图附加到 pull request 或 issue

--attach
uploads an image or video to the new pull request or issue, and repeats for several. Alt text for an image goes after a
#
. A video renders as a player and takes no alt text.
The path is read inside the box, relative to
-C
. A browser screenshot is written to the machine running the CLI, not into the box, so it has to be uploaded first. That upload is the step people miss:
bash
box browser screenshot -o /tmp/shot.png          # lands here, not in the box
box files upload /tmp/shot.png repo/shot.png     # now it is in the repository
box git create-issue -C repo \
  --title "Search returns nothing" \
  --body 'Reproduced on staging.

![what I saw](./shot.png)' \
  --attach 'shot.png#the empty result list'
A
![alt](./shot.png)
reference in the body is rewritten to point at the uploaded asset, so the image renders in the issue instead of pointing at a path that exists only inside the box.
Four rules are enforced, each a 400 before anything is created: the extension must be png, jpg, jpeg, gif, webp, mp4, mov or webm; at most 50 files; the path must stay inside the
-C
directory; and a video cannot carry alt text.
When some attachments upload and others fail, the item is still created and its URL is still returned, with a
warning
alongside it. Text output prints the warning on its own line, and
--json
carries it as the
warning
field. Check it before reporting the issue as filed with its evidence attached.
--attach
将图片或视频上传到新的 pull request 或 issue,并且可以重复多次。图片的替代文本放在
#
后面。视频会渲染为播放器,不需要替代文本。
路径在 box 内读取,相对于
-C
。浏览器截图会写入运行 CLI 的机器,而不是 box 内,因此必须先上传。这一步是大家容易遗漏的:
bash
box browser screenshot -o /tmp/shot.png          # lands here, not in the box
box files upload /tmp/shot.png repo/shot.png     # now it is in the repository
box git create-issue -C repo \
  --title "Search returns nothing" \
  --body 'Reproduced on staging.

![what I saw](./shot.png)' \
  --attach 'shot.png#the empty result list'
正文中的
![alt](./shot.png)
引用会被重写为指向已上传的资产,因此图片会在 issue 中渲染,而不是指向仅存在于 box 内的路径。
有四个规则会被强制检查,任何内容创建前都会以 400 拒绝:扩展名必须是 png、jpg、jpeg、gif、webp、mp4、mov 或 webm;最多 50 个文件;路径必须保持在
-C
目录内;视频不能携带替代文本。
当部分附件上传成功、部分失败时,条目仍会被创建,其 URL 仍会返回,并附带
warning
。文本输出会在单独一行打印该警告,
--json
会将其作为
warning
字段携带。在报告 issue 已提交并附有证据之前,请检查它。

Agent

Agent

If the box was created with an agent, hand it a task:
bash
box create --no-repl --agent-harness claude-code --agent-model anthropic/claude-sonnet-5
box run "Fix the failing test in src/auth.test.ts"
box run - < prompt.txt
Text goes to stdout, tool calls to stderr. Prefer doing the work yourself with the commands above;
box run
is for delegating a whole task to the box's own agent.
如果 box 是带 agent 创建的,可以交给它一个任务:
bash
box create --no-repl --agent-harness claude-code --agent-model anthropic/claude-sonnet-5
box run "Fix the failing test in src/auth.test.ts"
box run - < prompt.txt
文本输出到 stdout,工具调用输出到 stderr。优先用上面的命令自己做;
box run
用于将整个任务委托给 box 自己的 agent。

Watching and stopping work

查看和停止工作

bash
box status runs                    # id, type, status, duration, cost
box status logs --limit 50
box cancel <run-id>                # ids come from status runs
A run started by another process cannot be stopped any other way:
box cancel
takes the id, so a long agent run or build is interruptible from a fresh shell.
bash
box status runs                    # id, type, status, duration, cost
box status logs --limit 50
box cancel <run-id>                # ids come from status runs
由另一个进程启动的 run 无法用其他方式停止:
box cancel
需要 id,因此长时间的 agent run 或构建可以从新 shell 中断。

Browser

浏览器

Only on a box created with
--browser
. Chromium runs inside the box, so it reaches your app on
http://localhost:3000
with no public URL involved. What lives outside is only the control path: these commands reach Chromium through the API, so there is no
box exec
spelling of them. A script running in the box can still talk to Chromium directly over CDP, which is the escape hatch at the end of this section.
bash
box browser open https://example.com    # prints the tab id
box browser tabs
box browser content                     # title, url, text, links
box browser screenshot -o page.png
box browser goto https://example.com/login
box browser act "click the login button"
box browser close
box browser cdp-url                     # drive it with Playwright instead
box browser observe "what can I click here?"
box browser live-url                    # a URL for a human to watch the tab
Every
box browser act
is metered: it takes an instruction in words and needs a model to read the page. The SDK can replay an
observe()
result for free, but the CLI takes only the string form, so a loop of
act
calls costs a model call each time.
content
,
goto
,
screenshot
and
close
are not metered.
Recordings, when you need to show what happened rather than describe it:
bash
box browser recordings start --max-seconds 120
box browser recordings stop
box browser recordings list
box browser recordings get <recording-id>
box browser recordings download <recording-id> -o session.mp4
Chromium starts on first use, so the very first
box browser open
is slower than the rest, and anything talking to CDP directly fails until it has run once.
--tab <id>
is optional while one tab is open and required once there are several.
screenshot
writes to a file because stdout carries text, and that file lands on this machine rather than in the box. To put a screenshot on a pull request or issue, see "Attaching a screenshot to a pull request or issue".
Pull structured data off the page with a flat JSON Schema file:
bash
echo '{"type":"object","properties":{"price":{"type":"string"}},"required":["price"]}' > s.json
box browser extract "the listed price" --schema s.json
A property not named in
required
is optional. Nested objects are refused.
仅在用
--browser
创建的 box 上可用。Chromium 运行在 box 内部,因此它可以通过
http://localhost:3000
访问你的应用,无需公共 URL。外部只有控制路径:这些命令通过 API 访问 Chromium,因此没有对应的
box exec
形式。在 box 内运行的脚本仍然可以通过 CDP 直接与 Chromium 通信,这是本节末尾的逃生通道。
bash
box browser open https://example.com    # prints the tab id
box browser tabs
box browser content                     # title, url, text, links
box browser screenshot -o page.png
box browser goto https://example.com/login
box browser act "click the login button"
box browser close
box browser cdp-url                     # drive it with Playwright instead
box browser observe "what can I click here?"
box browser live-url                    # a URL for a human to watch the tab
每次
box browser act
都按量计费:它接收文字指令,需要模型来读取页面。SDK 可以免费重放
observe()
的结果,但 CLI 只接受字符串形式,因此循环调用
act
每次都会消耗一次模型调用。
content
、
goto
、
screenshot
和
close
不按量计费。
当你需要展示而不是描述发生了什么时,可以使用录制:
bash
box browser recordings start --max-seconds 120
box browser recordings stop
box browser recordings list
box browser recordings get <recording-id>
box browser recordings download <recording-id> -o session.mp4
Chromium 在首次使用时启动,因此第一次
box browser open
会比后续慢,并且任何直接与 CDP 通信的操作在它运行过一次之前都会失败。
在只有一个标签页打开时
--tab <id>
是可选的,有多个标签页时则是必需的。
screenshot
写入文件是因为 stdout 用于文本,而且该文件会落在本机而不是 box 内。要将截图放到 pull request 或 issue 上,请参阅“将截图附加到 pull request 或 issue”。
使用扁平的 JSON Schema 文件从页面提取结构化数据:
bash
echo '{"type":"object","properties":{"price":{"type":"string"}},"required":["price"]}' > s.json
box browser extract "the listed price" --schema s.json
未在
required
中命名的属性是可选的。嵌套对象会被拒绝。

Capturing straight into the box

直接捕获到 box 内

box browser screenshot --full-page -o page.png
is the short way, and it is enough whenever the image can live on this machine. It writes to the machine running the CLI, though, so getting the image into the box costs an upload.
Chromium's CDP is open on
127.0.0.1:9222
from inside the box with no token, so a script running there captures and writes in one step, and can clip to a single element, which the CLI does not expose:
Write the script with
box files write
rather than inlining it: the remote shell is
sh
, and quoting a program through
box exec
is where this goes wrong.
bash
box files write shot.mjs - <<'JS'
const targets = await (await fetch("http://127.0.0.1:9222/json")).json();
const page = targets.find((t) => t.type === "page");
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((r) => (ws.onopen = r));

let id = 0;
const pending = new Map();
ws.onmessage = (m) => {
  const msg = JSON.parse(m.data);
  pending.get(msg.id)?.(msg.result);
  pending.delete(msg.id);
};
const send = (method, params = {}) =>
  new Promise((resolve) => {
    const callId = ++id;
    pending.set(callId, resolve);
    ws.send(JSON.stringify({ id: callId, method, params }));
  });

// captureBeyondViewport only permits capture outside the viewport; the clip is
// what makes it the whole page. cssContentSize is in CSS pixels, which is what
// clip expects.
const metrics = await send("Page.getLayoutMetrics");
const size = metrics.cssContentSize ?? metrics.contentSize;
const { data } = await send("Page.captureScreenshot", {
  format: "png",
  captureBeyondViewport: true,
  clip: { x: 0, y: 0, width: size.width, height: size.height, scale: 1 },
});

const fs = await import("node:fs");
fs.writeFileSync("shot.png", Buffer.from(data, "base64"));
ws.close();
JS

box exec -- 'node shot.mjs'      # shot.png is now in the box
The
clip
is what makes this a full-page capture rather than a viewport one;
--full-page
does the same thing. An element-clipped capture is the same call with that element's box as the clip, and that one has no CLI flag. Node's global
fetch
and
WebSocket
are enough, so nothing has to be installed, but Chromium must have been started once by a
box browser
command first.
This is only worth it when the image should stay in the box or you need a capture the CLI cannot make. Otherwise
screenshot -o
then
files upload
is shorter.
box browser screenshot --full-page -o page.png
是简便方法,只要图片可以放在本机上就足够了。但它会写入运行 CLI 的机器,因此将图片放入 box 需要一次上传。
Chromium 的 CDP 在 box 内部 的
127.0.0.1:9222
上开放,无需 token,因此在其中运行的脚本可以一步完成捕获和写入,并且可以裁剪到单个元素,而 CLI 不提供此功能:
使用
box files write
编写脚本,而不是内联:远程 shell 是
sh
,通过
box exec
给程序加引号正是容易出错的地方。
bash
box files write shot.mjs - <<'JS'
const targets = await (await fetch("http://127.0.0.1:9222/json")).json();
const page = targets.find((t) => t.type === "page");
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((r) => (ws.onopen = r));

let id = 0;
const pending = new Map();
ws.onmessage = (m) => {
  const msg = JSON.parse(m.data);
  pending.get(msg.id)?.(msg.result);
  pending.delete(msg.id);
};
const send = (method, params = {}) =>
  new Promise((resolve) => {
    const callId = ++id;
    pending.set(callId, resolve);
    ws.send(JSON.stringify({ id: callId, method, params }));
  });

// captureBeyondViewport only permits capture outside the viewport; the clip is
// what makes it the whole page. cssContentSize is in CSS pixels, which is what
// clip expects.
const metrics = await send("Page.getLayoutMetrics");
const size = metrics.cssContentSize ?? metrics.contentSize;
const { data } = await send("Page.captureScreenshot", {
  format: "png",
  captureBeyondViewport: true,
  clip: { x: 0, y: 0, width: size.width, height: size.height, scale: 1 },
});

const fs = await import("node:fs");
fs.writeFileSync("shot.png", Buffer.from(data, "base64"));
ws.close();
JS

box exec -- 'node shot.mjs'      # shot.png is now in the box
clip
是让这次捕获成为整页捕获而非视口捕获的关键;
--full-page
做同样的事情。元素裁剪捕获是相同的调用,只是把该元素的 box 作为 clip,而且这个操作没有 CLI 标志。Node 全局的
fetch
和
WebSocket
就足够了,因此无需安装任何东西,但 Chromium 必须首先由
box browser
命令启动过一次。
只有当图片应该留在 box 内,或者你需要 CLI 无法生成的捕获时,这才值得。否则
screenshot -o
然后
files upload
更简短。

Schedules

定时任务

Cron on the box, in UTC. Nothing inside the container can register one.
bash
box schedule exec --cron '0 9 * * *' -- npm run backup
box schedule agent --cron '@daily' "summarise yesterday's errors"
box schedule list
box schedule get <schedule-id>            # includes run and failure counts
box schedule pause <schedule-id>
box schedule resume <schedule-id>
box schedule update <schedule-id> --cron '0 10 * * *'
box schedule delete <schedule-id>
update
changes only what you name, so setting the cron leaves the command alone.
在 box 上运行 cron,使用 UTC 时间。容器内的任何东西都无法注册 cron。
bash
box schedule exec --cron '0 9 * * *' -- npm run backup
box schedule agent --cron '@daily' "summarise yesterday's errors"
box schedule list
box schedule get <schedule-id>            # includes run and failure counts
box schedule pause <schedule-id>
box schedule resume <schedule-id>
box schedule update <schedule-id> --cron '0 10 * * *'
box schedule delete <schedule-id>
update
只更改你指定的内容,因此设置 cron 不会改动命令。

Box configuration

Box 配置

bash
box skills add upstash-redis-js        # skills available to the box's agent
box skills list
box skills remove upstash-redis-js
box config model anthropic/claude-sonnet-5
box config init-command set "npm ci"   # keep-alive boxes only; runs on start
box config init-command get
box config init-command delete
box config network deny-all            # or allow-all, or custom
box config network custom --allow-domain api.example.com
box config harness --command my-agent  # a custom agent harness
Account-level settings, which apply to boxes you create later rather than to this one:
bash
box env set KEY VAL                    # applies to boxes created after this
box env list
box env delete KEY
box env set-all A=1 B=2                # replaces every var, does not merge
box labels add staging                 # then: box list --label staging
box labels list
box labels remove staging
Both
box env set
and
box create --env
take the value as an argument, so a secret passed either way is visible in
ps
and lands in shell history. Neither is a secrets mechanism; keep real credentials out of both and use a token the box fetches for itself.
bash
box skills add upstash-redis-js        # skills available to the box's agent
box skills list
box skills remove upstash-redis-js
box config model anthropic/claude-sonnet-5
box config init-command set "npm ci"   # keep-alive boxes only; runs on start
box config init-command get
box config init-command delete
box config network deny-all            # or allow-all, or custom
box config network custom --allow-domain api.example.com
box config harness --command my-agent  # a custom agent harness
账户级设置,适用于你之后创建的 box,而不是当前这个:
bash
box env set KEY VAL                    # applies to boxes created after this
box env list
box env delete KEY
box env set-all A=1 B=2                # replaces every var, does not merge
box labels add staging                 # then: box list --label staging
box labels list
box labels remove staging
box env set
和
box create --env
都将值作为参数接收,因此无论哪种方式传递的机密都会在
ps
中可见并进入 shell 历史。两者都不是机密机制;请将真实凭据远离这两者,使用 box 自行获取的 token。

Flag reference

标志参考

The flags the walkthroughs above do not reach. Every command also takes the global
--box
,
--json
and
--token
.
bash
box create --no-repl --git-user-name N --git-user-email E   # commit identity
box create --no-repl --agent-api-key stored                 # key saved in the console
box create --no-repl --no-use                               # do not write .box
box init-demo --directory my-demo                           # scaffold elsewhere

box exec -C /srv/app -- npm test         # -C/--cwd: working directory
box run --timeout 600 -q "..."           # -q/--quiet: no tool-call logs on stderr
box code --timeout 120 "..."             # both take --timeout in seconds

box files read --offset 0 --length 65536 big.log   # a slice; 8 MiB per read
box files read --encoding base64 logo.png          # binary out
box files write --encoding base64 logo.png -       # binary in
box files remove -r build/                         # -r required for a directory
box files mkdir -p a/b/c                           # -p creates missing parents
box files stat --follow link                       # resolve a final symlink

box git clone --branch main --depth 1 <url>        # shallow, single branch
box git clone --github-token $TOKEN <url>          # private repository
box git commit -m "msg" --author-name N --author-email E
box git push --branch feature/x                    # names the branch to push

box public-url 3000 --bearer-token       # or --basic-auth; both generate credentials
box use --unset                          # drop this directory's .box, never a parent's

box schedule agent --cron "0 9 * * *" --model <m> --timeout 300 --webhook-url <url> "..."
box schedule update <id> --timeout 0     # 0 clears the timeout; --prompt, --cron, --model too

box config network custom --allow-domain a.test --allow-cidr 10.0.0.0/8 --deny-cidr 10.1.0.0/16
box config harness --command my-agent --arg --verbose   # --arg repeatable, sent before the prompt
-C
means the working directory on
box exec
(
--cwd
) and the repository directory on every
box git
and
box schedule
subcommand (
--folder
).
上面教程未涉及的标志。每条命令也接受全局的
--box
、
--json
和
--token
。
bash
box create --no-repl --git-user-name N --git-user-email E   # commit identity
box create --no-repl --agent-api-key stored                 # key saved in the console
box create --no-repl --no-use                               # do not write .box
box init-demo --directory my-demo                           # scaffold elsewhere

box exec -C /srv/app -- npm test         # -C/--cwd: working directory
box run --timeout 600 -q "..."           # -q/--quiet: no tool-call logs on stderr
box code --timeout 120 "..."             # both take --timeout in seconds

box files read --offset 0 --length 65536 big.log   # a slice; 8 MiB per read
box files read --encoding base64 logo.png          # binary out
box files write --encoding base64 logo.png -       # binary in
box files remove -r build/                         # -r required for a directory
box files mkdir -p a/b/c                           # -p creates missing parents
box files stat --follow link                       # resolve a final symlink

box git clone --branch main --depth 1 <url>        # shallow, single branch
box git clone --github-token $TOKEN <url>          # private repository
box git commit -m "msg" --author-name N --author-email E
box git push --branch feature/x                    # names the branch to push

box public-url 3000 --bearer-token       # or --basic-auth; both generate credentials
box use --unset                          # drop this directory's .box, never a parent's

box schedule agent --cron "0 9 * * *" --model <m> --timeout 300 --webhook-url <url> "..."
box schedule update <id> --timeout 0     # 0 clears the timeout; --prompt, --cron, --model too

box config network custom --allow-domain a.test --allow-cidr 10.0.0.0/8 --deny-cidr 10.1.0.0/16
box config harness --command my-agent --arg --verbose   # --arg repeatable, sent before the prompt
-C
在
box exec
上表示工作目录(
--cwd
),在每个
box git
和
box schedule
子命令上表示仓库目录(
--folder
)。

Output

输出

Data goes to stdout, diagnostics to stderr, so piping is safe.
--json
prints the result as JSON with no wrapper, on every command that returns data. The ones that open a REPL or print a shell script (
connect
,
init-demo
,
completion
) reject it rather than answering an automation caller with a prompt:
bash
box files list --json | jq -r '.[].name'
box get "$(cat .box)" --json
box init-demo
and
box completion
exist but are for people, not agents: one scaffolds a local demo project, the other prints a shell completion script.
数据输出到 stdout,诊断信息输出到 stderr,因此管道是安全的。
--json
在每条返回数据的命令上都会以 JSON 形式打印结果,不包含包装。那些打开 REPL 或打印 shell 脚本的命令(
connect
、
init-demo
、
completion
)会拒绝它,而不是用提示符回应自动化调用者:
bash
box files list --json | jq -r '.[].name'
box get "$(cat .box)" --json
box init-demo
和
box completion
存在但面向人类,而非 agent:一个搭建本地演示项目,另一个打印 shell 补全脚本。