Workflow for editing space ""
The loop
- Read.
state.glob("**/*"),state.readFile,state.searchText. - Plan. Multi-file edits:
state.planEdits+state.applyEdits. - Edit.
state.writeFile,state.replaceInFile. - Commit.
git.add({ filepath: ".", dir: "/" })+git.commit({ message, author, dir: "/" }). - Build.
host.build()(insiderun_code). Fix warnings. - Preview (optional). Branch off
main(git.branchthengit.checkout), commit,host.deploy()→ preview URL (read it from the deploy response). - Ship. Back on
main(git.checkout({ ref: "main" })), merge the feature branch (git.merge({ theirs })) or commit directly, thenhost.deploy().
Which branch you're on decides the deploy target — check with
git.branch({ list: true }), which returns { branches, current }.
Refusals:
DIRTY_TREE— uncommitted changes. Stage + commit before building.NO_COMMITS— empty repo. Make an initial commit.DETACHED_HEAD— checkout a branch first.
Elaborate example: stateful counter
This builds a tiny stateful Worker that persists a counter across requests
using the App DO's KV storage (see dynamic-worker
for capabilities). The whole flow happens through one run_code call.
async () => {
// 1. Write the worker source. Named `App` export — required by the bundler
// (see bundling: unfolder://docs/bundling).
await state.writeFile(
"/src/index.ts",
`import { DurableObject } from "cloudflare:workers";
export class App extends DurableObject {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/reset") {
this.ctx.storage.kv.put("counter", 0);
return new Response("reset");
}
let counter = this.ctx.storage.kv.get("counter") ?? 0;
counter++;
this.ctx.storage.kv.put("counter", counter);
return new Response("count=" + counter + " path=" + url.pathname);
}
}
`,
);
// 2. Commit on main.
await git.add({ filepath: ".", dir: "/" });
const { oid } = await git.commit({
message: "feat: counter app",
author: { name: "Agent", email: "agent@unfolder.space" },
dir: "/",
});
return { committed: oid };
};
Then, in run_code (build is optional — deploy always builds first):
async () => host.build() // → { commitSha, mainModule, moduleCount, ... }
async () => host.deploy() // → { target: "production", url: ".../" }
A request to https://<your-space-slug>.unfolder.space/hello returns count=1 path=/hello,
and the second request returns count=2. Production facets preserve the App
DO's SQLite across redeploys, so the counter survives redeploys of main.
Reading and exploring
state.* (exposed inside run_code) is the full file API. drop into run_code and
call the workspace primitives directly. To preserve template literals
(e.g. ${name}) verbatim in a string you're writing, use write_files
instead — those bytes are not JS-evaluated.
// Walk the workspace (nested tree, max depth 3)
async () => state.walkTree("/", { maxDepth: 3 })
// Find files by pattern
async () => state.glob("src/**/*.{ts,tsx}")
// One-level listing
async () => state.readdirWithFileTypes("/src")
// Read a text file
async () => state.readFile("/src/index.ts")
// Read a binary file as base64 (MCP transport is JSON; pick an encoding)
async () => {
const bytes = await state.readFileBytes("/photo.png");
return { encoding: "base64", contents: btoa(String.fromCharCode(...bytes)) };
}
// Search content across files
async () => state.searchText("/", "TODO")
// Aggregate counters — also exposed on the `unfolder://spaces/<your-space-slug>`
// resource as `workspaceInfo` (no tool call needed).
async () => state.summarizeTree("/")
Preview branch flow
async () => {
await git.branch({ name: "feat-x", dir: "/" });
await git.checkout({ ref: "feat-x", dir: "/" });
await state.replaceInFile("/src/index.ts", "count=", "preview-count=");
await git.add({ filepath: ".", dir: "/" });
return await git.commit({
message: "preview tweak",
author: { name: "Agent", email: "agent@unfolder.space" },
dir: "/",
});
};
// then host.deploy() — read the preview URL from the deploy response:
// https://<your-space-slug>--{previewId}--preview.unfolder.space (previewId is a
// generated id, NOT the branch name — don't construct the URL yourself).
Preview facets reset SQLite per redeploy, so don't rely on persisted state
inside a preview when iterating. To promote a previewed change to production,
check out main and git.merge({ theirs: "feat-x" }), then deploy.