branch:
README.md
6958 bytesRaw
# `@cloudflare/shell`

> **Experimental.** API surface is still settling — expect breaking changes.

Sandboxed JavaScript execution and filesystem runtime for Cloudflare Workers agents.

Instead of parsing shell syntax, `@cloudflare/shell` runs JavaScript inside an isolated Worker and exposes a typed `state` object for operating on a filesystem backend. It is designed for agent workflows that need structured state operations, predictable semantics, and coarse host-side filesystem primitives.

## What it is

- A runtime-neutral `StateBackend` interface for filesystem/state operations
- A `FileSystem` interface with two implementations: `InMemoryFs` (ephemeral) and `WorkspaceFileSystem` (durable)
- `FileSystemStateBackend` — a single adapter wrapping any `FileSystem` into a `StateBackend`
- `Workspace` — durable file storage backed by SQLite + optional R2
- `stateTools(workspace)` — a `ToolProvider` for `@cloudflare/codemode` that exposes `state.*` in sandboxed executions
- A prebuilt `state` stdlib with type declarations for LLM prompts

## What it is not

This is **not** a bash interpreter. It does not parse shell syntax, expose pipes, or emulate POSIX shell behavior. It executes JavaScript.

## Example — in-memory state

```ts
import { createMemoryStateBackend } from "@cloudflare/shell";
import { stateToolsFromBackend } from "@cloudflare/shell/workers";
import { DynamicWorkerExecutor, resolveProvider } from "@cloudflare/codemode";

const backend = createMemoryStateBackend({
  files: {
    "/src/app.ts": 'export const answer = "foo";\n'
  }
});

const executor = new DynamicWorkerExecutor({ loader: env.LOADER });

const result = await executor.execute(
  `async () => {
    const text = await state.readFile("/src/app.ts");
    await state.writeFile("/src/app.ts", text.replace("foo", "bar"));
    return await state.readFile("/src/app.ts");
  }`,
  [resolveProvider(stateToolsFromBackend(backend))]
);
```

## Example — durable Workspace

```ts
import { Agent } from "agents";
import { Workspace } from "@cloudflare/shell";
import { stateTools } from "@cloudflare/shell/workers";
import { DynamicWorkerExecutor, resolveProvider } from "@cloudflare/codemode";

class MyAgent extends Agent<Env> {
  workspace = new Workspace(this, { r2: this.env.MY_BUCKET });

  async run(code: string) {
    const executor = new DynamicWorkerExecutor({ loader: this.env.LOADER });
    return executor.execute(code, [
      resolveProvider(stateTools(this.workspace))
    ]);
  }
}
```

## Design goals

- Structured state operations instead of shell parsing
- Coarse host-side operations like `glob()` and `diff()` to avoid chatty RPC
- Compatibility with both ephemeral in-memory state and durable `Workspace`
- Secure execution with isolate-level timeouts and outbound network blocking by default

## `state` object API

The `state` object is available inside every isolate and exposes:

### Primitive filesystem

`readFile`, `writeFile`, `appendFile`, `readFileBytes`, `writeFileBytes`, `mkdir`, `rm`, `cp`, `mv`, `symlink`, `readlink`, `realpath`, `readdir`, `glob`, `stat`, `lstat`, `exists`, `diff`, `diffContent`

### JSON helpers

`readJson(path)`, `writeJson(path, value, { spaces? })`, `queryJson(path, query)`, `updateJson(path, operations)`

### Search and replace

`searchText(path, query, options?)`, `searchFiles(glob, query, options?)`, `replaceInFile(path, search, replacement, options?)`, `replaceInFiles(glob, search, replacement, { dryRun?, rollbackOnError?, ...options })`

### Filesystem queries

`find(path, options?)`, `walkTree(path, { maxDepth? })`, `summarizeTree(path, { maxDepth? })`

### Archive and compression

`createArchive(path, sources)`, `listArchive(path)`, `extractArchive(path, destination)`, `compressFile(path, destination?)`, `decompressFile(path, destination?)`, `hashFile(path, { algorithm? })`, `detectFile(path)`

### Structured edit planning

`planEdits(instructions)`, `applyEditPlan(plan, { dryRun?, rollbackOnError? })`, `applyEdits(edits, { dryRun?, rollbackOnError? })`

## Multi-file workflow

```ts
// Preview changes without applying
const preview = await state.replaceInFiles("src/**/*.ts", "foo", "bar", {
  dryRun: true
});

// Plan structured edits with intent
const plan = await state.planEdits([
  { kind: "replace", path: "/src/app.ts", search: "foo", replacement: "bar" },
  { kind: "writeJson", path: "/src/config.json", value: { enabled: true } }
]);
await state.applyEditPlan(plan);

// Apply raw edits transactionally
await state.applyEdits([
  { path: "/src/generated.ts", content: "export const generated = true;\n" }
]);
```

Batch writes roll back by default if any write fails. Set `rollbackOnError: false` to allow partial progress.

## Rough command translation

| Shell command        | `state` equivalent                                                         |
| -------------------- | -------------------------------------------------------------------------- |
| `cat`                | `state.readFile()`                                                         |
| `echo x > file`      | `state.writeFile()`                                                        |
| `mkdir`              | `state.mkdir()`                                                            |
| `ls` / `find`        | `state.readdir()` / `state.glob()`                                         |
| `find` with filters  | `state.find()`                                                             |
| `tree` / `du`        | `state.walkTree()` / `state.summarizeTree()`                               |
| `cp` / `mv` / `rm`   | `state.cp()` / `state.mv()` / `state.rm()`                                 |
| `diff`               | `state.diff()` / `state.diffContent()`                                     |
| `grep`               | `state.searchText()` / `state.searchFiles()`                               |
| `sed`                | `state.replaceInFile()` / `state.replaceInFiles()`                         |
| `jq`                 | `state.readJson()` / `state.queryJson()` / `state.updateJson()`            |
| `tar`                | `state.createArchive()` / `state.listArchive()` / `state.extractArchive()` |
| `gzip` / `gunzip`    | `state.compressFile()` / `state.decompressFile()`                          |
| `sha256sum` / `file` | `state.hashFile()` / `state.detectFile()`                                  |

## Maybe later

- Stream-oriented transforms inspired by `sort`, `uniq`, `comm`, `cut`, `paste`, `tr`
- Full `rg` CLI parity — file types, ignore controls, multiple roots
- Richer JSON query semantics closer to `jq` filters
- Structured patch helpers covering more of diff / codemod workflows
- A smaller "command" library built on top of `state.*`

## Relationship to other packages

- `@cloudflare/codemode`: executes sandboxed JavaScript that orchestrates tools
- `@cloudflare/shell`: provides filesystem backends and `stateTools()` ToolProvider for codemode