Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt

Use this file to discover all available pages before exploring further.

Mirage ships BoxResource in two runtimes:
  • @struktoai/mirage-node, uses client_id + client_secret + refresh token (rotated on each use)
  • @struktoai/mirage-browser, supports the same refresh token via PKCE (no client secret in the bundle)
Both runtimes hit the same Box v2 API endpoints (/folders/{id}/items, /files/{id}/content, /search). Credentials are obtained the same way in both runtimes, see Box Credentials.

Quick start: developer token

For exploration, skip the OAuth flow and use a developer token (one-button-click in the Box app console, 60-minute lifetime). The BoxResource accepts an accessToken field that short-circuits the refresh logic:
import { BoxResource, MountMode, Workspace } from '@struktoai/mirage-node'

const box = new BoxResource({
  accessToken: process.env.BOX_DEVELOPER_TOKEN!,
})
const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
await ws.execute('ls /box/')
When the token expires (Box 401s with invalid_token), regenerate it in the console and re-run. See Box Credentials -> Quick Start.

Node (server-side, long-running)

pnpm add @struktoai/mirage-node
import { BoxResource, MountMode, Workspace } from '@struktoai/mirage-node'

const box = new BoxResource({
  clientId: process.env.BOX_CLIENT_ID!,
  clientSecret: process.env.BOX_CLIENT_SECRET!,
  refreshToken: process.env.BOX_REFRESH_TOKEN!,
  // Box rotates the refresh token; persist the new one if you want to survive restarts.
  onRefreshTokenRotated: async (next) => {
    await fs.writeFile('.box-refresh', next, 'utf-8')
  },
})

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
const res = await ws.execute('ls /box/')
console.log(res.stdoutText)
The BoxTokenManager caches the access token in memory (5-minute safety buffer before expiry) and rotates the refresh token on every refresh. Without onRefreshTokenRotated, the rotation is in-memory only and a process restart needs a fresh refresh token.

Browser (PKCE, no client secret)

pnpm add @struktoai/mirage-browser
import { BoxResource, MountMode, Workspace } from '@struktoai/mirage-browser'

// Refresh token obtained via the PKCE flow, see examples/typescript/browser/src/box_pkce.ts.
const box = new BoxResource({
  clientId: import.meta.env.VITE_BOX_CLIENT_ID,
  refreshToken: localStorage.getItem('box-refresh')!,
  onRefreshTokenRotated: (next) => localStorage.setItem('box-refresh', next),
})

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
await ws.execute('ls /box/')
Box requires the origin to be allowlisted on the app’s Allowed Origins config (see setup). The bundled examples/typescript/browser/src/box_pkce.ts runs the full PKCE dance and persists the rotated refresh token to localStorage.

VFS mode (patchNodeFs)

import { createRequire } from 'node:module'
import { BoxResource, MountMode, patchNodeFs, Workspace } from '@struktoai/mirage-node'

const require = createRequire(import.meta.url)
const fs = require('fs') as typeof import('fs')

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
const restore = patchNodeFs(ws)

const entries = await fs.promises.readdir('/box/')
const stat = await fs.promises.stat('/box/Documents')
const bytes = await fs.promises.readFile('/box/Documents/example.json')

restore()
Only fs.promises.* is patched, sync forms aren’t supported.

FUSE mode

import { BoxResource, FuseManager, MountMode, Workspace } from '@struktoai/mirage-node'

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
const fm = new FuseManager()
const mp = await fm.setup(ws)
// ${mp}/box/ is now a real filesystem path:
//   ls ${mp}/box/
//   find ${mp}/box -type f
//   cat ${mp}/box/example.json | jq .
await fm.close(ws)
Requires macFUSE on macOS or libfuse on Linux.

Path → ID resolution

Box uses numeric folder/file IDs internally (root folder is id 0). Mirage caches the path → ID mapping in the RAMIndexCacheStore attached to the resource. The first time you ls /box/foo/bar/, it walks the tree top-down (one API call per level) and caches each entry’s ID. Subsequent reads of /box/foo/bar/anyfile.json hit the cache instead of re-walking. The cache TTL defaults to 24h. To force re-resolution, recreate the BoxResource.

Special file types

Five Box-specific file types get a .json suffix appended in the vfs and return clean, agent-friendly JSON when you cat them. The Box file ID is unchanged, the suffix is purely a vfs hint that cat returns JSON, mirroring Google Drive’s .gdoc.json style. In ls /box/:
hihi.gdoc.json           # Box's Google Doc (backing bytes are .docx)
gsheet.gsheet.json       # Box's Google Sheet (.xlsx)
hihihi.gslides.json      # Box's Google Slides (.pptx)
test.boxnote.json        # Box Note
test canvas.boxcanvas.json

.boxnote.json, Box Notes

{
  "id": "...",
  "body_text": "paragraphs joined by \n",
  "paragraphs": [{ "text": "...", "authors": ["..."] }],
  "authors": { "<id>": "Author Name" },
  "last_edit_at": "..."
}
cat /box/foo.boxnote.json | jq -r .body_text   # plain text
cat /box/foo.boxnote.json | jq .authors        # who wrote it

.boxcanvas.json, Box Canvas (whiteboards)

{
  "id": "...",
  "widget_count": 11,
  "widgets_by_type": { "shape": 6, "link": 5 },
  "body_text": "shape labels joined by \n",
  "widgets": [{ "id": "...", "type": "shape", "text": "..." }],
  "authors": ["..."]
}
cat "/box/foo.boxcanvas.json" | jq -r .body_text
cat "/box/foo.boxcanvas.json" | jq .widgets_by_type

.gdoc.json / .gsheet.json / .gslides.json, Box’s V2 Google files

Box’s V2 G Suite integration stores Google-format files as Office Open XML zips (.docx / .xlsx / .pptx). On cat, Mirage uses Box’s representations API with X-Rep-Hints: [extracted_text] to fetch Box’s auto-extracted plain-text version, then returns a JSON envelope:
{
  "id": "...",
  "name": "hihi.gdoc.json",
  "format": "docx",
  "size": 8921,
  "modified_at": "...",
  "body_text": "auto-extracted plain text"
}
cat /box/hihi.gdoc.json | jq -r .body_text
cat /box/gsheet.gsheet.json | jq .format       # "xlsx"
This is different from Google Drive’s .gdoc.json, Box uses its own API + extracted-text representation; Google Drive uses Google’s full Documents API and returns the rich Document structure (paragraphs, named styles, lists, etc.). For real edits to the underlying Google Doc, mount Google Drive separately with Google credentials and use the GDocs / GSheets / GSlides resources, gws-docs-* commands won’t work on Box because the file IDs are in different namespaces.

Available commands

BoxResource ships the same shell command set as GDriveResource and DropboxResource:
  • Filesystem: ls, cat, head, tail, nl, wc, stat, find, tree, du, file, realpath, basename, dirname
  • Search/text: grep, rg, awk, sed, sort, uniq, cut, diff, cmp, jq
  • Encoding: base64
  • Format-aware: cat_parquet, cat_feather, cat_hdf5, plus head_*/grep_*/ls_*/ stat_*/tail_*/wc_*/file_*/cut_* variants for .parquet, .feather, .hdf5/.h5, .orc files

Examples

End-to-end runnable scripts are in examples/typescript/box/:
  • box.ts, Workspace.execute('ls/stat/cat/tree …') shell demo
  • box_parquet.ts, parquet preview through cat
  • box_boxnote.ts, .boxnote.json decoded into clean JSON
  • box_office.ts, .gdoc.json / .gsheet.json / .gslides.json via Box’s extracted_text representation
  • box_vfs.ts, patchNodeFs + native fs.promises.* calls
  • box_fuse.ts, FUSE mount with shell access in another terminal