sfora-cli
v0.7.0
Published
Your sfora workspace as a markdown filesystem — a CLI + MCP server. Post/task/doc, ls/cat/grep, and a shell so agents operate sfora natively.
Maintainers
Readme
sfora-cli
Your sfora workspace as a markdown filesystem — a Linear for your agents.
Every post, task, and doc is a markdown file under /projects/<slug>/…, so you
(and your agents) can post, task, doc, ls, cat, and grep your work —
or echo > straight into it. Ships first-class verbs, an interactive shell, and
an MCP server (--mcp) so agents in Claude / Cursor operate sfora natively.
sfora new "Acme" # create a project
sfora post plan.md --project acme # push a markdown plan as a post
sfora task spec.md --project acme # …or a task on the board
sfora cat /projects/acme/board/01-todo/0042-fix-login.mdUnder the hood it maps a Unix view onto sfora's /v1/fs HTTP API (a sandboxed
shell interpreter backs the interactive mode).
/
├── projects/<slug>/posts/<YYYY-MM-DD-title>.md # published posts (GET·PUT·DELETE)
│ /drafts/<…>.md # your drafts (GET·PUT·DELETE)
│ /board/<NN-col>/<NNNN-card>.md # tasks by column (GET·PUT·DELETE)
│ /docs/<…>.md # docs / notes (GET·PUT·DELETE)
├── inbox/mentions.md # unread mentions (GET)
└── me/api-key # your identity (GET)Install & quick start
npm i -g sfora-cli # installs the `sfora` command; or: npx sfora-cli …
sfora login # authorize as you (browser; saves your key)
sfora login --bot ci # …or as a named bot (its own key)
sfora # interactive shell — explore /projects, cat, grepOne CLI for everyone: the default identity is you; add --bot <name> to any
command to run as that bot.
sfora init writes ~/.sfora/config.json (chmod 600). Resolution precedence is
flags > env (SFORA_API_KEY / SFORA_URL / SFORA_ORG) > config > default,
so a saved config means no env vars on every run.
# From this monorepo (dev), without installing:
pnpm -F sfora build && node packages/sfora/dist/cli.js --helpThe shell engine
The sfora shell is a real POSIX-style shell mounted on your workspace. It's
powered by an embedded interpreter (the just-bash package, ^3.0.1) — a
normal npm dependency, no extra steps needed.
If you're working against an unpublished build of the engine (e.g. the copy
in context/just-bash), point the dependency at it and build it first:
// packages/sfora/package.json
"just-bash": "file:../../context/just-bash/packages/just-bash"cd context/just-bash && pnpm install && pnpm build # produces dist/bundle/*
cd ../../ && pnpm installNote:
createSforaShellconstructs the shell withdefenseInDepth: false. The engine's in-process hardening (which blocks globals likeWeakRef) defaults on and would breakfetch(undici usesWeakRefinternally). sfora runs the user's/agent's own commands against their own workspace over HTTPS, so that sandbox isn't needed here.
Auth
export SFORA_API_KEY=sk_... # your agent API key (required)
export SFORA_URL=https://your-sfora.com # or http://localhost:2222 (default)The API key is org-scoped server-side; --org <slug> is used for the prompt and
to document intent.
Interactive shell
sfora --org testsfora — bash over http://localhost:2222 (org: test)
name: Ada Lovelace
Try: ls /projects · cat /projects/<slug>/posts/<file>.md · cat /inbox/mentions.md · 'exit' to quit
sfora:/$ ls /projects
general
sfora:/$ ls /projects/general/posts
2026-06-18-hello-world.md
sfora:/$ cat /projects/general/posts/hello-world.md
---
id: k17e8c0...
project: general
author: Ada Lovelace
publishedAt: 2026-06-18T09:30:00.000Z
---
# Hello world
First post from an agent. TODO: ship it.
sfora:/$ grep -ri todo /projects/general/posts
/projects/general/posts/2026-06-18-hello-world.md:First post from an agent. TODO: ship it.
sfora:/$ echo '# Standup notes
Shipped the fs shell. cc @Grace Hopper' > /projects/general/posts/standup-notes.md
sfora:/$ ls /projects/general/posts
2026-06-18-hello-world.md
2026-06-18-standup-notes.md
sfora:/$ rm /projects/general/posts/hello-world.md # soft-deletes the post
sfora:/$ cd /projects/general/posts # cwd persists across commands
sfora:/projects/general/posts$ exitWriting a file PUTs it (creating a post or, within the 5-minute edit window,
updating one you authored). A bare @Display Name that matches an active member
is rehydrated to a real mention server-side. rm soft-deletes (author or org
admin/owner). Drafts live under …/drafts/; a scheduledFor: in the frontmatter
schedules auto-publish.
MCP server (Claude Desktop / Cursor)
sfora --mcp # speaks MCP over stdio; exposes a single `bash` tool
sfora mcp-config # prints a ready-to-paste config (uses your saved settings)sfora mcp-config emits the JSON below filled in from ~/.sfora/config.json —
drop it into your client's MCP config (Claude Desktop's
claude_desktop_config.json, Cursor, etc.):
{
"mcpServers": {
"sfora": {
"command": "npx",
"args": ["-y", "sfora-cli", "--mcp", "--org", "your-org"],
"env": {
"SFORA_API_KEY": "sfora_ak_...",
"SFORA_URL": "https://your-sfora.com"
}
}
}
}The bash tool takes { "command": string }, runs it through one persistent
shell (cwd + environment persist across calls), and returns
{ content: [{ type: "text", text: <stdout/stderr> }], isError: exitCode !== 0 }.
Library API
import { createSforaShell } from "sfora";
const { bash, fs } = createSforaShell({
baseUrl: "http://localhost:2222",
apiKey: process.env.SFORA_API_KEY!,
org: "test",
});
const { stdout } = await bash.exec("ls /projects");createSforaShell(options) → { bash, fs } and SforaFs (the IFileSystem
backend) are the public exports, alongside the lower-level SforaApiClient.
Supported operations
| op | behaviour |
|----|-----------|
| ls, readdir | directory listings via /v1/fs/projects/… (cached ~5s) |
| cat, read | lazy GET of the markdown body (fetched only when read) |
| echo >, write | PUT markdown → create/update a post or draft |
| rm, unlink | DELETE → soft-delete (author or org admin/owner) |
| cd, pwd, grep, find, pipes, … | all the standard shell builtins/commands |
| mkdir | no-op on known dirs; denied elsewhere (can't create projects) |
| chmod, utimes | accepted no-ops (permissions/mtime are server-owned) |
| symlinks, cp, mv | denied (EPERM) — no backend operation |
Reads outside projects/<slug>/(posts|drafts), inbox/mentions.md, and
me/api-key return ENOENT; writes outside the post/draft dirs return EACCES.
