# Shell Reference

Complete reference for SLICC's shell capabilities, including supplemental commands, .jsh scripts, and binary handling.

---

## Overview

SLICC uses `just-bash` (WASM Bash interpreter v2.14.3) as its core shell runtime. This provides 78+ standard Unix commands plus 17+ custom supplemental commands and auto-discovered `.jsh` script commands.

**Entry point**: Via the `bash` agent tool. All shell features available to agents.

---

## Supplemental Commands

Custom commands implemented in TypeScript and registered in just-bash.

| Command                                     | File                       | Description                                                                                                                                                                                                                                                 | Key Arguments                                                                                                                               |
| ------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **commands**                                | `help-command.ts`          | List all available commands (built-ins + .jsh)                                                                                                                                                                                                              | None                                                                                                                                        |
| **which**                                   | `which-command.ts`         | Resolve a command path                                                                                                                                                                                                                                      | `<command>` — returns `/usr/bin/<name>` or VFS path                                                                                         |
| **uname**                                   | `uname-command.ts`         | Print the current browser user agent                                                                                                                                                                                                                        | None                                                                                                                                        |
| **host**                                    | `host-command.ts`          | Print the current leader tray status plus `launch_url` and `join_url` (`launch_url` is `https://.../tray/<id>` when this runtime is leader)                                                                                                                 | None                                                                                                                                        |
| **oauth-token**                             | `oauth-token-command.ts`   | Get an OAuth access token for a provider. Returns the **masked** Bearer token (in both CLI and extension modes). The proxy/SW unmasks at the network boundary.                                                                                              | `<providerId>`, `--provider <id>`, `--list`, no args = selected provider; auto-triggers login if needed                                     |
| **oauth-domain**                            | `oauth-domain-command.ts`  | Manage per-provider extra allowed domains for OAuth-issued tokens. Provider hardcoded `oauthTokenDomains` stay immutable; entries here layer on top. Stored in `localStorage` (`slicc_oauth_extra_domains`); also editable from the extension options page. | `list [<providerId>]`, `add <providerId> <domain>`, `remove <providerId> <domain>`, `clear <providerId>`                                    |
| **local-llm**                               | `local-llm-command.ts`     | Inspect / configure the Local LLM provider (Ollama, LM Studio, llama.cpp, vLLM, mlx, Jan, LocalAI)                                                                                                                                                          | `local-llm` or `local-llm status` — verify connection; `local-llm discover` — probe `/v1/models` and save the list to Settings              |
| **serve**                                   | `serve-command.ts`         | Open a VFS app directory in a browser tab                                                                                                                                                                                                                   | `[--entry <relative-path>] <directory>` — defaults to `index.html`; rejects absolute/traversal entry paths                                  |
| **open**                                    | `open-command.ts`          | Open URL or VFS file in browser tab                                                                                                                                                                                                                         | `<url\|path>` — serves VFS files via preview SW; `--download` / `-d` forces download; `--view` / `-v` returns image inline for agent vision |
| **imgcat**                                  | `imgcat-command.ts`        | Display image inline in terminal                                                                                                                                                                                                                            | `<path>` — base64 + ansi escape codes                                                                                                       |
| **zip**                                     | `zip-command.ts`           | Create ZIP archive                                                                                                                                                                                                                                          | `<archive.zip> <file1> [file2...]`                                                                                                          |
| **unzip**                                   | `unzip-command.ts`         | Extract ZIP archive                                                                                                                                                                                                                                         | `<archive.zip> [-d output-dir]`                                                                                                             |
| **sqlite3**                                 | `sqlite-command.ts`        | Execute SQLite queries                                                                                                                                                                                                                                      | `-c "SELECT * FROM table" db.sqlite`                                                                                                        |
| **node**                                    | `node-command.ts`          | Execute JavaScript code                                                                                                                                                                                                                                     | `-e "console.log(1+1)"` with fs bridge                                                                                                      |
| **python3 / python**                        | `python-command.ts`        | Execute Python code                                                                                                                                                                                                                                         | `-c "print([i**2 for i in range(5)])"` with Pyodide                                                                                         |
| **webhook**                                 | `webhook-command.ts`       | Manage webhooks for event-driven licks                                                                                                                                                                                                                      | `webhook create <endpoint>`, `webhook list`, `webhook delete <id>`                                                                          |
| **websocat**                                | `websocat-command.ts`      | Minimal WebSocket client (netcat/curl for ws://). Sends stdin lines as messages, prints received messages. Client-only — server mode and advanced specifiers (`exec:`, `tcp:`, `broadcast:`, `ws-l:`) are not supported.                                    | `websocat ws://URL`, `-1` one-shot, `-b` binary, `--jsonrpc`/`--jsonrpc-omit-jsonrpc`, `--base64`, `--protocol`, `--max-messages`           |
| **crontask**                                | `crontask-command.ts`      | Schedule cron jobs that dispatch licks                                                                                                                                                                                                                      | `crontask add <name> "0 9 * * *" scoop-name "instructions..."`                                                                              |
| **pdftk / pdf**                             | `pdftk-command.ts`         | PDF manipulation                                                                                                                                                                                                                                            | `pdf burst input.pdf`, `pdf cat input.pdf output output.pdf`                                                                                |
| **convert / magick**                        | `convert-command.ts`       | Image conversion (ImageMagick style)                                                                                                                                                                                                                        | `convert -resize 800x600 input.jpg output.jpg`                                                                                              |
| **playwright-cli / playwright / puppeteer** | `playwright-command.ts`    | Browser automation shell CLI                                                                                                                                                                                                                                | `snapshot`, `click <ref>`, `cookie-set`, `tab-list`                                                                                         |
| **screencapture**                           | `screencapture-command.ts` | Capture user's screen via browser screen sharing API                                                                                                                                                                                                        | `<output.png>`, `-c` (clipboard), `-v` / `--view` (agent vision)                                                                            |
| **upskill**                                 | `upskill-command.ts`       | Install skills from GitHub, the Tessl registry, or browse.sh; suggest skills for open browser tabs                                                                                                                                                          | `upskill owner/repo`, `upskill tessl:<name>`, `upskill browse:<host>/<task>`, `upskill search "query"`, `upskill tabs [--json]`             |
| **sprinkle**                                | `sprinkle-command.ts`      | Manage `.shtml` sprinkle panels and inline chat UI                                                                                                                                                                                                          | `sprinkle list`, `sprinkle open <name>`, `sprinkle chat '<html>'`                                                                           |
| **cost**                                    | `cost-command.ts`          | Show session cost breakdown per scoop/cone                                                                                                                                                                                                                  | `--json`, `-h`                                                                                                                              |
| **models**                                  | `models-command.ts`        | List available LLM models with pricing and benchmarks                                                                                                                                                                                                       | `--all`, `--json`, `--provider <id>`, `--refresh`                                                                                           |
| **secret**                                  | `secret-command.ts`        | Manage secrets (API keys, tokens) with domain-scoped injection. CLI: prints `~/.slicc/secrets.env` / Keychain edit instructions. Extension: writes to `chrome.storage.local` directly, or open the Options page (form UI) via `secret edit`.                | `list`, `set <name> [<value>] --domain <patterns>`, `delete <name>`, `test <name> <url>`, `edit` (extension only — opens Options page)      |
| **mount**                                   | (MountCommands class)      | Mount local directories or remote storage (S3 / S3-compatible / DA) into the VFS                                                                                                                                                                            | `mount [--source <url>] [--profile <name>] <path>`, `mount unmount [--clear-cache] <path>`, `mount list`, `mount refresh [--bodies] <path>` |
| **git**                                     | (isomorphic-git)           | Full git support                                                                                                                                                                                                                                            | `git clone`, `git commit`, `git push`, etc.                                                                                                 |

**Example usage**:

```bash
# List all available commands
commands

# Resolve a command path
which node
# Output: /usr/bin/node

# Print the current browser user agent
uname

# Show the current leader tray status, launch URL, and join URL
host

# In a leader runtime, launch_url is the tray URL itself
# In non-leader/error runtimes with a saved session, it stays the local app launch URL
# join_url exposes the tray join capability directly when a session exists

# Open a URL in a browser tab
open https://example.com

# Serve a VFS app directory (defaults to index.html)
serve /workspace/app

# Serve the same app with a custom entry file
serve --entry pages/home.html /workspace/app

# Open a VFS file in a browser tab (served via preview service worker)
open /workspace/app/index.html

# Force download instead of opening in tab
open --download /workspace/report.pdf

# View an image (agent can see it in the response)
open --view /workspace/screenshot.png

# Execute JavaScript
node -e "console.log('Hello from Node')"

# Execute Python
python3 -c "print(sum(range(10)))"

# Create ZIP archive
zip archive.zip file1.txt file2.txt

# Query SQLite
sqlite3 -c "SELECT COUNT(*) FROM users" database.db

# Browse with playwright-cli
playwright-cli open https://example.com
playwright-cli snapshot

# Capture user's screen (prompts user to select screen/window/tab)
screencapture desktop.png
screencapture --view screen.png   # Capture and return for agent vision
screencapture -c                   # Capture to clipboard

# Display image
imgcat screenshot.png

# Schedule a cron job
crontask add "daily-backup" "0 2 * * *" backup-scoop "Backup all files"
```

---

## playwright-cli

Browser automation is also exposed as shell commands: `playwright-cli`, `playwright`, and `puppeteer`.

- **Shared state across aliases**: all three names operate on the same current tab, snapshot cache, cookies/storage context, and `/.playwright/session.md` history.
- **Default targeting**: `open` / `tab-new` open in the background by default, but if there is no current browser target yet, the first opened tab becomes current so `snapshot` works immediately.
- **Fresh refs required**: `click`, `fill`, `goto`, `go-back`, `go-forward`, `reload`, and similar state-changing commands invalidate prior snapshot refs. After history navigation or reload, run `snapshot` again before using refs.
- **Cookie convenience forms**: `cookie-set <name> <value>` and `cookie-delete <name>` use the current page URL when `--domain` and `--path` are omitted.
- **Teleport restores auth state**: arm it explicitly with `playwright teleport --start=<regex> --return=<regex>` or implicitly with `--teleport-start` / `--teleport-return` on `open`, `tab-new`, or `goto` / `navigate`. When the leader hits `--start`, the intercepted auth URL opens on a follower for the human to finish login; when the follower hits `--return`, teleport restores both cookies and page storage (`localStorage` + `sessionStorage`) back to the leader. For cross-origin SSO flows, teleport hydrates the captured app origin first, then lands on the best matching app URL.
- **Unexpected dialogs**: attached pages auto-dismiss unexpected JavaScript dialogs so a stray `alert()` or similar modal does not stall automation indefinitely.
- **Link-header discovery**: `playwright-cli fetch <url>` always emits JSON with parsed RFC 8288 `links[]` and any SLICC handoff match; pass `--discover` to also fetch P0 capability docs (`api-catalog`, `service-desc`, `llms.txt`, …) and to populate `discovery.browseShSkills[]` with any browse.sh catalog entries whose hostname matches the destination URL (cold-cache call triggers one lazy fetch per shell). The same `--discover` flag on `goto` / `navigate` / `open` / `tab-new` performs an auxiliary proxied fetch and switches output to the same JSON payload. See [link-discovery.md](link-discovery.md) for the full module map.

### Common flow

```bash
playwright-cli open https://example.com
playwright-cli snapshot
playwright-cli click e5
playwright-cli snapshot
playwright-cli cookie-set theme dark
```

### Session files

- `/.playwright/session.md` — chronological command log
- `/.playwright/snapshots/` — saved accessibility snapshots for state-changing commands that auto-snapshot
- `/.playwright/screenshots/` — saved screenshots

Use the skill doc at `packages/vfs-root/workspace/skills/playwright-cli/SKILL.md` for the full command list and operating guidance.

---

## upskill

Skill package manager. Installs into `/workspace/skills/<name>/` from three registries:

| Install ref                        | Registry                                                                                                                                                                                     |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `upskill <owner>/<repo>[@branch]`  | GitHub. Supports `--skill <name>` (repeatable), `--all`, `--path <subfolder>`, `--branch/-b`, `--list`, `--force`.                                                                           |
| `upskill tessl:<name>`             | Tessl registry (resolves to a GitHub source under the hood).                                                                                                                                 |
| `upskill browse:<hostname>/<task>` | [browse.sh](https://browse.sh) site-specific skills. Equivalent URL form: `upskill https://browse.sh/skills/<hostname>/<task>`. Installs into `/workspace/skills/browse-<hostname>-<name>/`. |

`upskill search "<query>"` round-robin interleaves results from Tessl and the browse.sh catalog (first hit from each source, then second from each, …) so both registries get visibility in the top page. `upskill recommendations` matches your profile; add `--install` to write the matches.

### browse.sh: SLICC adapter preamble

Installed browse.sh `SKILL.md` files get a fixed preamble inserted **immediately below the upstream YAML frontmatter** (the frontmatter remains the first thing in the file; the upstream body round-trips byte-identical below the preamble). The preamble is the same for every browse.sh skill regardless of `recommendedMethod`:

```markdown
> [!NOTE] **Imported from browse.sh** — original slug: `<hostname>/<task>`
>
> **SLICC adaptation:** use `playwright-cli` — you are running inside the user's real browser session, so the bot-detection workarounds the upstream skill assumes are usually unnecessary.
>
> Source: <https://browse.sh/skills/<hostname>/<task>> · updated <date>
```

### `upskill tabs [--json]`

Suggests skills for each open browser tab. For every tab `upskill tabs` lists:

- **Origin-advertised upskill rels** — for each tab URL, fetches it through the same proxied fetch the rest of the shell uses, parses the response's `Link` header (same `parseLinkHeader` helper as `discover` / PR #602), and surfaces any `Link: <…>; rel="https://www.sliccy.ai/rel/upskill"` the site emits. Distinct from `discoverLinks`, which follows P0 capability rels (`api-catalog`, `service-desc`, `llms-txt`, …) — `upskill tabs` only looks at the `upskill` rel.
- **Browse.sh catalog matches** — hostname-exact after stripping leading `www.` (so `https://www.weather.gov/` matches `weather.gov` but `https://forecast.weather.gov/` does not). Each match prints `installHint` and a `✓` marker for skills already installed under `/workspace/skills/browse-<host>-<name>/`.

`--json` emits the same data as a `{ tabs: TabUpskillResult[] }` envelope (one entry per tab with `targetId`, `url`, `hostname`, `active`, `origin[]`, `catalog[]`, `failures[]`). Per-tab discovery failures are collected non-fatally; a catalog fetch failure becomes a stderr warning but the command still exits 0. Without a browser API attached the command prints `browser APIs unavailable in this environment` and exits 1.

---

## mount

Bridges local directories and remote object storage into the VirtualFS so that file tools (`read_file`, `write_file`, `edit_file`, `bash`) operate on remote content the same way they do on browser-local files. Three peer backends share a `MountBackend` interface: a local FS Access backend (uses the `showDirectoryPicker()` flow), an S3 / S3-compatible backend (AWS, Cloudflare R2, MinIO via custom endpoints), and a DA backend (Adobe da.live, authenticated via the existing Adobe IMS provider).

Implementation lives outside `supplemental-commands/`: `packages/webapp/src/fs/mount-commands.ts` is the dispatcher, registered via the `MountCommands` class consumed by `wasm-shell.ts`. Backends are under `packages/webapp/src/fs/mount/`.

### Subcommands

| Form                                               | Behavior                                                                                                                                                                                                                                                         |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mount <path>`                                     | Local FS Access mount. Opens a directory picker (cone-only — fails fast in scoops, which have no UI gesture).                                                                                                                                                    |
| `mount --source s3://<bucket>[/<prefix>] <path>`   | S3 / S3-compatible mount. Reads creds from `s3.<profile>.*` secrets (`--profile` selects the namespace; defaults to `default`). Allowed in scoops.                                                                                                               |
| `mount --source da://<org>/<repo>[/<path>] <path>` | Adobe da.live mount. Reuses the existing Adobe provider's IMS bearer token; `--profile` is accepted for symmetry but has a single global identity in v1. Allowed in scoops.                                                                                      |
| `mount list`                                       | Show all active mounts with their kind, source, and profile (where applicable).                                                                                                                                                                                  |
| `mount unmount [--clear-cache] <path>`             | Tear down a mount. `--clear-cache` also drops cached listings + bodies for that mount; without it, cache entries persist until TTL or the next session.                                                                                                          |
| `mount refresh [--bodies] <path>`                  | Re-walk the source and diff against the cache. Prints `Refreshed <path>: +<added> -<removed> ~<changed> (<unchanged> unchanged, <errors> errors)`. Without `--bodies` only the listing is rechecked; with `--bodies` changed files are conditionally re-fetched. |

### Mount-time flags

| Flag                | Applies to    | Effect                                                                                                                                                                                 |
| ------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--source <url>`    | mount         | Selects a remote backend by URL scheme (`s3://`, `da://`). Without `--source`, the local picker is used.                                                                               |
| `--profile <name>`  | mount         | Profile name resolved against `s3.<profile>.*` secrets (S3) or used as a label (DA). Defaults to `default`.                                                                            |
| `--no-probe`        | mount         | Skip the mount-time `HEAD` bucket / `GET /list` round-trip. Use when latency matters and you trust the source URL is well-formed and accessible.                                       |
| `--max-body-mb <n>` | mount         | Override the per-mount maximum body size for read/write. Defaults: S3 25 MB, DA 5 MB. Files exceeding the threshold throw `EFBIG` before any body bytes flow.                          |
| `--clear-cache`     | mount unmount | Drop the `RemoteMountCache` entries (listings + bodies) for this mount.                                                                                                                |
| `--bodies`          | mount refresh | After the listing diff, conditionally re-fetch bodies for paths whose ETag changed. Without this flag a refresh is one paginated list (or one DA recursive walk) plus zero body bytes. |

### Caching and conflict semantics

Remote backends share a `RemoteMountCache` (TTL + ETag, IDB-backed under `slicc-mount-cache`). Default TTL is 30 s.

- **Reads**: cache-fresh → zero RTT; cache-stale → conditional `GET` with `If-None-Match` (304 keeps cached body, 200 replaces it); cache-miss → unconditional `GET`.
- **Writes**: existing files use `If-Match: <etag>`; new files use `If-None-Match: *` to refuse silent overwrite. A 412 from a fresh first-attempt PUT surfaces as `FsError('EBUSY', …)` so the agent's edit loop can re-read and retry. (412 inside a bounded retry window of an in-flight PUT is silently reconciled — that case means "we already won this PUT" rather than a conflict.)
- **Auth**: 401/403 triggers a one-time profile re-resolution (covers credential rotation and IMS token refresh) before bubbling `EACCES`.
- **Recovery**: mount descriptors persist across sessions. On reload, local mounts may need a user gesture to re-grant the FS Access handle; remote mounts auto-restore as long as profiles resolve and IMS hasn't expired. Failures surface via a `session-reload` lick that the cone renders as an actionable retry prompt.

### Credentials

S3 secrets follow the `s3.<profile>.*` namespace. DA reuses the Adobe IMS token from the existing provider — no DA-specific secret to set. See [docs/secrets.md](secrets.md#mount-backend-secrets) for the full key list and example setup.

### Examples

```bash
# Local picker (cone only — runs in the panel/UI context with a user gesture)
mount /mnt/local

# S3 (AWS) — first store creds, then mount
secret set s3.aws.access_key_id      # follow printed instructions
secret set s3.aws.secret_access_key  # follow printed instructions
mount --source s3://my-bucket/site --profile aws /mnt/aws

# Cloudflare R2 (S3-compatible — uses --source s3:// with a custom endpoint in the profile)
secret set s3.r2.access_key_id
secret set s3.r2.secret_access_key
secret set s3.r2.endpoint            # https://<account>.r2.cloudflarestorage.com
mount --source s3://my-r2-bucket/path --profile r2 /mnt/r2

# Adobe da.live — uses the Adobe provider's existing IMS identity
mount --source da://my-org/my-repo /mnt/da

# Inspect, refresh, unmount
mount list
mount refresh /mnt/r2                # listing-only diff
mount refresh --bodies /mnt/r2       # also revalidates changed bodies
mount unmount --clear-cache /mnt/r2  # drops cache as well
```

### Approval flow

Only **local** mounts render an approval card. The card is not a consent gate — it's the click that satisfies Chrome's user-gesture rule for the File System Access API, since `showDirectoryPicker()` must be invoked from inside a user event handler. In the extension, the click also routes through a popup window because Chrome crashes if the picker is invoked from side-panel context for system directories.

**S3** and **DA** mounts have no approval card. The trust boundary lives at the credential profile resolver in node-server (`/api/s3-sign-and-forward`, `/api/da-sign-and-forward`) or the SW signing path in extension mode — not in the chat. The probe at mount time will fail with an actionable error if the profile is misconfigured.

Local mounts are gated to cone context only because the directory picker requires a real user gesture. S3 and DA mounts are allowed from scoops since their credentials come from the secret store and no UI gesture is required.

---

## .jsh Script Commands

JavaScript shell scripts auto-discovered anywhere on the VirtualFS. Executable like any shell command.

**Discovery**: `jsh-discovery.ts` scans VFS with priority roots:

```
Priority: /workspace/skills/
Then: / (full filesystem scan)

Rule: First basename wins (no conflicts)
```

`script-catalog.ts` is the shared lookup layer used by `WasmShell`, `which`, and browser-script matching. When an `FsWatcher` is present it caches discovery results and clears them on filesystem changes; mounted directories bypass the cache because external edits inside File System Access mounts are not observable through the watcher.

**Execution**: Via `jsh-executor.ts` (dual-mode):

- CLI: `AsyncFunction` constructor with Node-like globals
- Extension: Sandbox iframe (CSP-compliant), VFS via postMessage

### Globals API

#### process

```typescript
process.argv: string[]                       // ['node', 'script.jsh', ...args]
process.env: object                          // Environment variables
process.cwd(): string                        // Current working directory
process.exit(code?: number)                  // Exit with code (0 default)
process.stdout.write(s)                      // Write to stdout
process.stderr.write(s)                      // Write to stderr
process.stdin.read(): string | null          // Buffered piped stdin; null after EOF
process.stdin.isTTY: false                   // Always false in this environment
process.stdin[Symbol.asyncIterator]()        // Yields the buffered string once
String(process.stdin)                        // Non-consuming view of the buffer
```

#### stdin (via `process.stdin`)

Stdin from upstream pipelines is buffered fully before the script runs — there is **no streaming**. `read()` drains the buffer with Node-like EOF semantics:

```typescript
// echo "a,b,c" | parse-csv
const data = process.stdin.read(); // 'a,b,c\n'
const again = process.stdin.read(); // null — buffer was drained
```

The async iterator shares that consumed state with `read()`, so re-iterating yields nothing after the first pass (and yields nothing at all if you called `read()` first):

```typescript
let total = '';
for await (const chunk of process.stdin) total += chunk;
```

For a non-consuming view, use `String(process.stdin)` or `process.stdin.toString()`. If no input is piped, the first `read()` returns `''` and subsequent calls return `null`.

Stdin is intentionally NOT exposed as a top-level identifier — user scripts are free to declare their own `const stdin = …` without colliding with the runtime.

#### console

```typescript
console.log(...args); // stdout (space-separated)
console.info(...args); // stdout
console.warn(...args); // stderr
console.error(...args); // stderr
```

#### fs (VirtualFS bridge)

All paths are resolved relative to `process.cwd()`.

```typescript
fs.readFile(path): Promise<string>
fs.readFileBinary(path): Promise<Uint8Array>
fs.writeFile(path, content: string): Promise<void>
fs.writeFileBinary(path, bytes: Uint8Array): Promise<void>
fs.readDir(path): Promise<string[]>
fs.exists(path): Promise<boolean>
fs.stat(path): Promise<{ isDirectory, isFile, size }>
fs.mkdir(path): Promise<void>
fs.rm(path): Promise<void> // Recursive delete
fs.fetchToFile(url, path): Promise<number> // Download and save, returns byte count
```

#### exec (shell command bridge)

Run any shell command through just-bash and get the result. Works in both CLI and extension mode.

```typescript
exec(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>

// Example: get an OAuth token
const r = await exec('oauth-token adobe');
const token = r.stdout.trim();

// Example: list files
const ls = await exec('ls -la /workspace');
console.log(ls.stdout);
```

#### require / module / exports

Scripts can import npm packages via `require('package-name')`. This fetches from esm.sh CDN and caches for the session. Version pinning is supported: `require('lodash@4')`.

```typescript
const _ = require('lodash');
const { marked } = require('marked');
const chalk = require('chalk@5');
module.exports: {}        // Available for ES module pattern
exports: module.exports   // Alias
```

### Example .jsh Script

```javascript
// /workspace/skills/my-tool/process-csv.jsh
const args = process.argv.slice(2);

if (args.length < 1) {
  console.error('Usage: process-csv <input.csv>');
  process.exit(1);
}

const inputFile = args[0];
const outputFile = args[1] || inputFile.replace(/\.csv$/, '.json');

(async () => {
  try {
    const csv = await fs.readFile(inputFile);
    const lines = csv.split('\n').filter((l) => l.trim());
    const header = lines[0].split(',').map((s) => s.trim());

    const rows = lines.slice(1).map((line) => {
      const values = line.split(',').map((s) => s.trim());
      return Object.fromEntries(header.map((h, i) => [h, values[i]]));
    });

    const json = JSON.stringify(rows, null, 2);
    await fs.writeFile(outputFile, json);

    console.log(`Converted: ${inputFile} → ${outputFile}`);
    console.log(`Records: ${rows.length}`);
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
})();
```

**Usage**:

```bash
# Call by basename (from any directory)
process-csv input.csv output.json
```

### Error Handling

```javascript
try {
  const data = await fs.readFile('/nonexistent.json');
} catch (err) {
  // err.message: "ENOENT: /nonexistent.json not found"
  console.error(err.message);
  process.exit(1);
}
```

---

## Argument Parsing

Shell arguments support quotes, escapes, and whitespace.

**Parser**: `parse-shell-args.ts`

### Rules

| Pattern         | Result                              |
| --------------- | ----------------------------------- |
| `word`          | Single word token                   |
| `"hello world"` | Single token: `hello world`         |
| `'hello world'` | Single token: `hello world`         |
| `hello\ world`  | Single token: `hello world`         |
| `a "b c" d`     | Three tokens: `a`, `b c`, `d`       |
| `"a\"b"`        | Single token: `a"b` (escaped quote) |

### Examples

```bash
# Multiple words in quotes
node -e "console.log('Hello, World')"
# Parsed as: ['node', '-e', "console.log('Hello, World')"]

# Path with spaces
open "/path/to/my file.html"
# Parsed as: ['open', '/path/to/my file.html']

# Escaped characters
echo "Line 1\nLine 2"
# Parsed as: ['echo', 'Line 1\nLine 2']
```

---

## Command Discovery

### Priority Roots

Scan order (first wins):

1. `/workspace/skills/` — Skill scripts, highest priority
2. `/` — Full filesystem walk

### Basename Rule

When multiple `.jsh` files have the same basename:

```
/workspace/skills/my-skill/build.jsh     ← Chosen
/tools/scripts/build.jsh                 ← Ignored (same basename)
```

First occurrence by priority root wins.

### Dynamic Registration

The `commands` command lists all available commands:

```bash
$ commands
Available commands:
  Built-in: ls, cat, grep, find, ... (78+ commands)
  Custom: convert, sqlite3, webhook, crontask, ...
  Scripts: process-csv, backup-db, deploy-site, ...
```

The agent can dynamically discover new scripts via `commands`, then invoke them by name.

---

## Binary Handling

SLICC's shell supports binary data (images, PDFs, archives) via careful encoding.

**Binary cache**: `binary-cache.ts`

### Flow

1. **VFS read**: `fs.readFileBinary(path)` returns `Uint8Array`
2. **just-bash limitations**: Bash strings are Unicode; binary data must be encoded
3. **Latin-1 encoding**: Binary bytes preserved via `String.fromCharCode(byte)` mapping
4. **VFS write**: `fs.writeFile(path, encodedString)` is detected as binary (stored in cache) and decoded back to `Uint8Array`

### API

```typescript
// Read binary
const bytes: Uint8Array = await fs.readFileBinary('/image.png');

// Write binary
const newBytes = new Uint8Array([0xFF, 0xD8, ...]);
await fs.writeFile('/output.jpg', newBytes);
```

### Tools Supporting Binary

- **playwright-cli**: `screenshot --filename=<path>` saves PNGs directly to the VFS
- **node** / **.jsh**: `fs.readFileBinary()`, `fs.writeFileBinary()` available
- **bash**: Limited binary support (command output truncated at 100KB)

---

## Proxied Fetch

Network requests are proxied to handle CORS and cross-origin restrictions.

### CLI Mode

Express server provides `/api/fetch-proxy`:

```bash
curl -X POST /api/fetch-proxy \
  -H "X-Target-URL: https://api.example.com/data" \
  -H "Content-Type: application/json" \
  -d '{"key": "value"}'
```

All `fetch()` and `curl` calls route through proxy (CLI: `/api/fetch-proxy`, extension: `fetch-proxy.fetch` SW Port handler). Both modes now provide full secret-injection coverage.

### Extension Mode

Extension mode routes through the service worker `fetch-proxy.fetch` Port handler. The handler unmasks secrets at the network boundary and uses `host_permissions` for CORS bypass:

```json
"host_permissions": [
  "https://*/*",
  "http://*/*"
]
```

### Behavior

| Runtime           | Fetch Type         | Route                     |
| ----------------- | ------------------ | ------------------------- |
| CLI Node          | Any                | `/api/fetch-proxy`        |
| CLI browser page  | Anthropic API      | Direct (whitelist)        |
| CLI browser page  | Other cross-origin | `/api/fetch-proxy`        |
| Extension         | Anthropic API      | Direct (whitelist)        |
| Extension         | Other              | Direct (host_permissions) |
| Extension sandbox | Any                | postMessage to parent     |

---

## Common Patterns

### Chain Commands

```bash
cat input.txt | grep "pattern" | sort | uniq
```

### Conditional Execution

```bash
mkdir -p output && cp file.txt output/ || echo "Failed"
```

### Variable Expansion

```bash
MYVAR="hello"
echo $MYVAR
```

### Function Definition

```bash
greet() {
  echo "Hello, $1"
}
greet "World"
```

### Here Document

```bash
cat > file.txt << EOF
Line 1
Line 2
EOF
```

### Command Substitution

```bash
DATE=$(date)
echo "Today is $DATE"
```

---

## Performance

- **Command startup**: <100ms (just-bash WASM initialization)
- **Script execution**: O(script complexity), typically <500ms
- **File I/O**: IndexedDB operations, <100ms per file
- **Binary operations**: LightningFS encoding/decoding, <50ms for typical images

For large-scale processing (1000+ files), batch operations and `.jsh` scripts are faster than shell loops.

---

## CDN-backed require()

`node -e`, `.jsh`, and `.bsh` scripts can import npm packages at runtime via `require()`:

```js
const _ = require('lodash');
const { marked } = require('marked');
const chalk = require('chalk@5');
```

Packages are fetched from [esm.sh](https://esm.sh) and cached for the session. Version pinning via `@version` syntax is supported.

**Note:** require() is synchronous. Modules referenced with string literals are automatically pre-fetched before script execution. For dynamic specifiers, use `await import('https://esm.sh/' + name)` directly.

### Node Built-in Modules

Some Node.js built-in modules are available via `require()`:

| Module                                                  | Status                                                                |
| ------------------------------------------------------- | --------------------------------------------------------------------- |
| `fs`                                                    | ✅ VFS bridge (readFile, writeFile, readDir, exists, stat, mkdir, rm) |
| `process`                                               | ✅ Shim (argv, env, cwd, exit, stdout, stderr)                        |
| `buffer`                                                | ✅ Browser polyfill                                                   |
| `path`                                                  | ✅ Via esm.sh (browser polyfill)                                      |
| `url`, `querystring`, `util`, `events`, `assert`        | ✅ Via esm.sh                                                         |
| `http`, `https`, `crypto`, `net`, `child_process`, etc. | ❌ Not available in browser                                           |

The `node:` prefix is supported: `require('node:path')` works the same as `require('path')`.

---

## Limitations

- **Binary output in bash**: Commands producing binary output are limited to 100KB (just-bash constraint)
- **Symlinks**: Not supported by LightningFS
- **Large files**: Reading >100MB files in bash is slow; use `node -e` or `.jsh` scripts instead
- **Network timeout**: curl/fetch timeout at 30 seconds (default)

---

## Dual-Mode Notes

### CLI Mode

- Full bash capabilities
- Shell state persisted across commands
- `node -e` uses `AsyncFunction` constructor
- Fetch requests routed through Express `/api/fetch-proxy`

### Extension Mode

- Full bash capabilities (same as CLI)
- Shell state persisted across commands
- `node -e` and `.jsh` scripts run in sandbox iframe (CSP-compliant)
- Fetch requests via `host_permissions` (no proxy needed)

Both modes share the same VirtualFS and command interface.

---

## Useful Commands

```bash
# Find files
find /workspace -name "*.js" -type f

# Search text
rg "TODO" /src --type js

# Process JSON
curl https://api.example.com/data | jq '.items[] | select(.status == "active")'

# Probe a WebSocket echo server (send one message, receive one, exit)
echo hello | websocat -1 wss://ws.vi-server.org/mirror

# Drive a Chrome DevTools target via JSON-RPC over WebSocket
echo 'Page.navigate {"url":"https://example.com"}' \
  | websocat -1 --jsonrpc --jsonrpc-omit-jsonrpc \
      ws://127.0.0.1:9222/devtools/page/<id>

# Batch rename
for file in *.txt; do mv "$file" "${file%.txt}.md"; done

# ZIP archive
zip -r backup.zip /workspace -x "*.node_modules/*" "*.git/*"

# Git workflow
git status
git add .
git commit -m "Feature: add new tool"
git push origin main

# Python data processing
python3 -c "
import json
data = json.load(open('data.json'))
result = [x for x in data if x['count'] > 10]
print(json.dumps(result, indent=2))
"

# Node scripting
node -e "
const fs = require('fs');
const files = fs.readdirSync('.');
console.log(files);
"

# Schedule a task
crontask add "cleanup" "0 3 * * 0" cleaner-scoop "Remove old files from /tmp"

# List configured secrets (names + domains, never values)
secret list

# Check if a secret would be injected for a URL
secret test GITHUB_TOKEN https://api.github.com/repos/foo/bar

# Show instructions for adding a new secret
secret set API_KEY
```

---

## References

- **just-bash**: https://github.com/jotaen/just-bash
- **Supplemental commands**: `packages/webapp/src/shell/supplemental-commands/`
- **JSH executor**: `packages/webapp/src/shell/jsh-executor.ts`
- **Binary cache**: `packages/webapp/src/shell/binary-cache.ts`
- **Argument parser**: `packages/webapp/src/shell/parse-shell-args.ts`
- **Discovery**: `packages/webapp/src/shell/script-catalog.ts`, `packages/webapp/src/shell/jsh-discovery.ts`
