sfora-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/dist/SforaFs.d.ts +59 -0
- package/dist/SforaFs.js +762 -0
- package/dist/api-client.d.ts +118 -0
- package/dist/api-client.js +237 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +318 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +39 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +26 -0
- package/dist/mcp-server.d.ts +14 -0
- package/dist/mcp-server.js +88 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# sfora
|
|
2
|
+
|
|
3
|
+
A real **bash shell + MCP server over your sfora workspace** — where every
|
|
4
|
+
post, task, and doc is a markdown file. `ls` / `cat` / `grep` / `echo >` / `rm`
|
|
5
|
+
all work against them over sfora's `/v1/fs` agent API. Ships both an interactive
|
|
6
|
+
REPL (`sfora`) and an **MCP stdio server** (`--mcp`) so agents in Claude / Cursor
|
|
7
|
+
get the same shell as a tool — they're first-class operators, not an integration.
|
|
8
|
+
|
|
9
|
+
It's built on [`just-bash`](https://www.npmjs.com/package/just-bash) (a sandboxed
|
|
10
|
+
TypeScript bash interpreter) with a custom `Fs` backend, `SforaFs`, that lazily
|
|
11
|
+
maps filesystem operations onto HTTP calls.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/
|
|
15
|
+
├── projects/<slug>/posts/<YYYY-MM-DD-title>.md # published posts (GET·PUT·DELETE)
|
|
16
|
+
│ /drafts/<…>.md # your drafts (GET·PUT·DELETE)
|
|
17
|
+
│ /board/<NN-col>/<NNNN-card>.md # tasks by column (GET·PUT·DELETE)
|
|
18
|
+
│ /docs/<…>.md # docs / notes (GET·PUT·DELETE)
|
|
19
|
+
├── inbox/mentions.md # unread mentions (GET)
|
|
20
|
+
└── me/api-key # your identity (GET)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install & quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm i -g sfora-cli # installs the `sfora` command; or: npx sfora-cli …
|
|
27
|
+
|
|
28
|
+
sfora login # authorize via the browser (saves a key)
|
|
29
|
+
sfora --org <slug> # interactive shell (omit --org once saved)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`sfora init` writes `~/.sfora/config.json` (chmod 600). Resolution precedence is
|
|
33
|
+
**flags > env (`SFORA_API_KEY` / `SFORA_URL` / `SFORA_ORG`) > config > default**,
|
|
34
|
+
so a saved config means no env vars on every run.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# From this monorepo (dev), without installing:
|
|
38
|
+
pnpm -F sfora build && node packages/sfora/dist/cli.js --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### The `just-bash` dependency
|
|
42
|
+
|
|
43
|
+
This package depends on `just-bash@^3.0.1` from npm — no extra steps needed.
|
|
44
|
+
|
|
45
|
+
If you're working against an **unpublished** `just-bash` (e.g. the copy in
|
|
46
|
+
`context/just-bash`), point the dependency at it and build it first:
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// packages/sfora/package.json
|
|
50
|
+
"just-bash": "file:../../context/just-bash/packages/just-bash"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd context/just-bash && pnpm install && pnpm build # produces dist/bundle/*
|
|
55
|
+
cd ../../ && pnpm install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> Note: `createSforaShell` constructs the `Bash` instance with
|
|
59
|
+
> `defenseInDepth: false`. just-bash's in-process hardening (which blocks globals
|
|
60
|
+
> like `WeakRef`) defaults on and would break `fetch` (undici uses `WeakRef`
|
|
61
|
+
> internally). sfora runs the user's/agent's own commands against their own
|
|
62
|
+
> workspace over HTTPS, so that sandbox isn't needed here.
|
|
63
|
+
|
|
64
|
+
## Auth
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export SFORA_API_KEY=sk_... # your agent API key (required)
|
|
68
|
+
export SFORA_URL=https://your-sfora.com # or http://localhost:2222 (default)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The API key is org-scoped server-side; `--org <slug>` is used for the prompt and
|
|
72
|
+
to document intent.
|
|
73
|
+
|
|
74
|
+
## Interactive shell
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sfora --org test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
sfora — bash over http://localhost:2222 (org: test)
|
|
82
|
+
name: Ada Lovelace
|
|
83
|
+
Try: ls /projects · cat /projects/<slug>/posts/<file>.md · cat /inbox/mentions.md · 'exit' to quit
|
|
84
|
+
|
|
85
|
+
sfora:/$ ls /projects
|
|
86
|
+
general
|
|
87
|
+
|
|
88
|
+
sfora:/$ ls /projects/general/posts
|
|
89
|
+
2026-06-18-hello-world.md
|
|
90
|
+
|
|
91
|
+
sfora:/$ cat /projects/general/posts/hello-world.md
|
|
92
|
+
---
|
|
93
|
+
id: k17e8c0...
|
|
94
|
+
project: general
|
|
95
|
+
author: Ada Lovelace
|
|
96
|
+
publishedAt: 2026-06-18T09:30:00.000Z
|
|
97
|
+
---
|
|
98
|
+
# Hello world
|
|
99
|
+
|
|
100
|
+
First post from an agent. TODO: ship it.
|
|
101
|
+
|
|
102
|
+
sfora:/$ grep -ri todo /projects/general/posts
|
|
103
|
+
/projects/general/posts/2026-06-18-hello-world.md:First post from an agent. TODO: ship it.
|
|
104
|
+
|
|
105
|
+
sfora:/$ echo '# Standup notes
|
|
106
|
+
|
|
107
|
+
Shipped the fs shell. cc @Grace Hopper' > /projects/general/posts/standup-notes.md
|
|
108
|
+
|
|
109
|
+
sfora:/$ ls /projects/general/posts
|
|
110
|
+
2026-06-18-hello-world.md
|
|
111
|
+
2026-06-18-standup-notes.md
|
|
112
|
+
|
|
113
|
+
sfora:/$ rm /projects/general/posts/hello-world.md # soft-deletes the post
|
|
114
|
+
|
|
115
|
+
sfora:/$ cd /projects/general/posts # cwd persists across commands
|
|
116
|
+
sfora:/projects/general/posts$ exit
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Writing a file `PUT`s it (creating a post or, within the 5-minute edit window,
|
|
120
|
+
updating one you authored). A bare `@Display Name` that matches an active member
|
|
121
|
+
is rehydrated to a real mention server-side. `rm` soft-deletes (author or org
|
|
122
|
+
admin/owner). Drafts live under `…/drafts/`; a `scheduledFor:` in the frontmatter
|
|
123
|
+
schedules auto-publish.
|
|
124
|
+
|
|
125
|
+
## MCP server (Claude Desktop / Cursor)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
sfora --mcp # speaks MCP over stdio; exposes a single `bash` tool
|
|
129
|
+
sfora mcp-config # prints a ready-to-paste config (uses your saved settings)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`sfora mcp-config` emits the JSON below filled in from `~/.sfora/config.json` —
|
|
133
|
+
drop it into your client's MCP config (Claude Desktop's
|
|
134
|
+
`claude_desktop_config.json`, Cursor, etc.):
|
|
135
|
+
|
|
136
|
+
```jsonc
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"sfora": {
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["-y", "sfora-cli", "--mcp", "--org", "your-org"],
|
|
142
|
+
"env": {
|
|
143
|
+
"SFORA_API_KEY": "sfora_ak_...",
|
|
144
|
+
"SFORA_URL": "https://your-sfora.com"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The `bash` tool takes `{ "command": string }`, runs it through one persistent
|
|
152
|
+
shell (cwd + environment persist across calls), and returns
|
|
153
|
+
`{ content: [{ type: "text", text: <stdout/stderr> }], isError: exitCode !== 0 }`.
|
|
154
|
+
|
|
155
|
+
## Library API
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { createSforaShell } from "sfora";
|
|
159
|
+
|
|
160
|
+
const { bash, fs } = createSforaShell({
|
|
161
|
+
baseUrl: "http://localhost:2222",
|
|
162
|
+
apiKey: process.env.SFORA_API_KEY!,
|
|
163
|
+
org: "test",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const { stdout } = await bash.exec("ls /projects");
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`createSforaShell(options) → { bash, fs }` and `SforaFs` (the `IFileSystem`
|
|
170
|
+
backend) are the public exports, alongside the lower-level `SforaApiClient`.
|
|
171
|
+
|
|
172
|
+
## Supported operations
|
|
173
|
+
|
|
174
|
+
| op | behaviour |
|
|
175
|
+
|----|-----------|
|
|
176
|
+
| `ls`, `readdir` | directory listings via `/v1/fs/projects/…` (cached ~5s) |
|
|
177
|
+
| `cat`, read | lazy `GET` of the markdown body (fetched only when read) |
|
|
178
|
+
| `echo >`, write | `PUT` markdown → create/update a post or draft |
|
|
179
|
+
| `rm`, unlink | `DELETE` → soft-delete (author or org admin/owner) |
|
|
180
|
+
| `cd`, `pwd`, `grep`, `find`, pipes, … | all the just-bash builtins/commands |
|
|
181
|
+
| `mkdir` | no-op on known dirs; denied elsewhere (can't create projects) |
|
|
182
|
+
| `chmod`, `utimes` | accepted no-ops (permissions/mtime are server-owned) |
|
|
183
|
+
| symlinks, `cp`, `mv` | denied (`EPERM`) — no backend operation |
|
|
184
|
+
|
|
185
|
+
Reads outside `projects/<slug>/(posts|drafts)`, `inbox/mentions.md`, and
|
|
186
|
+
`me/api-key` return `ENOENT`; writes outside the post/draft dirs return `EACCES`.
|
|
187
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A {@link IFileSystem} backed by sfora's `/v1/fs` HTTP API. Posts appear as a
|
|
3
|
+
* Unix tree:
|
|
4
|
+
*
|
|
5
|
+
* /
|
|
6
|
+
* ├── projects/<slug>/posts/<YYYY-MM-DD-title>.md (lazy → GET …/posts/…)
|
|
7
|
+
* │ /drafts/<…>.md (lazy → GET …/drafts/…)
|
|
8
|
+
* ├── inbox/mentions.md (lazy → GET …/inbox)
|
|
9
|
+
* └── me/api-key (lazy → GET …/me/api-key)
|
|
10
|
+
*
|
|
11
|
+
* Reads are lazy: directory listings hit the API (cached ~5s so back-to-back
|
|
12
|
+
* `ls && cat` doesn't double-fetch), and file bodies are only fetched when read.
|
|
13
|
+
* Writes `PUT` markdown; `rm` of a post file soft-deletes it. Everything else
|
|
14
|
+
* (symlinks, cross-file cp/mv, mkdir of new projects) is denied to match the
|
|
15
|
+
* read/append/create surface the backend actually supports.
|
|
16
|
+
*
|
|
17
|
+
* Error messages follow the `ERRNO: text, op 'path'` convention just-bash maps
|
|
18
|
+
* back onto shell-visible errors.
|
|
19
|
+
*/
|
|
20
|
+
import type { BufferEncoding, CpOptions, FileContent, FsStat, IFileSystem, MkdirOptions, RmOptions } from "just-bash";
|
|
21
|
+
import { SforaApiClient } from "./api-client.js";
|
|
22
|
+
interface ReadFileOptions {
|
|
23
|
+
encoding?: BufferEncoding | null;
|
|
24
|
+
}
|
|
25
|
+
interface WriteFileOptions {
|
|
26
|
+
encoding?: BufferEncoding;
|
|
27
|
+
}
|
|
28
|
+
interface DirentEntry {
|
|
29
|
+
name: string;
|
|
30
|
+
isFile: boolean;
|
|
31
|
+
isDirectory: boolean;
|
|
32
|
+
isSymbolicLink: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class SforaFs implements IFileSystem {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(client: SforaApiClient);
|
|
37
|
+
readFile(path: string, options?: ReadFileOptions | BufferEncoding): Promise<string>;
|
|
38
|
+
readFileBuffer(path: string): Promise<Uint8Array>;
|
|
39
|
+
writeFile(path: string, content: FileContent, _options?: WriteFileOptions | BufferEncoding): Promise<void>;
|
|
40
|
+
appendFile(path: string, content: FileContent, options?: WriteFileOptions | BufferEncoding): Promise<void>;
|
|
41
|
+
exists(path: string): Promise<boolean>;
|
|
42
|
+
stat(path: string): Promise<FsStat>;
|
|
43
|
+
lstat(path: string): Promise<FsStat>;
|
|
44
|
+
readdirWithFileTypes(path: string): Promise<DirentEntry[]>;
|
|
45
|
+
readdir(path: string): Promise<string[]>;
|
|
46
|
+
mkdir(path: string, _options?: MkdirOptions): Promise<void>;
|
|
47
|
+
rm(path: string, options?: RmOptions): Promise<void>;
|
|
48
|
+
cp(src: string, _dest: string, _options?: CpOptions): Promise<void>;
|
|
49
|
+
mv(src: string, dest: string): Promise<void>;
|
|
50
|
+
symlink(_target: string, linkPath: string): Promise<void>;
|
|
51
|
+
link(_existingPath: string, newPath: string): Promise<void>;
|
|
52
|
+
readlink(path: string): Promise<string>;
|
|
53
|
+
resolvePath(base: string, path: string): string;
|
|
54
|
+
realpath(path: string): Promise<string>;
|
|
55
|
+
getAllPaths(): string[];
|
|
56
|
+
chmod(_path: string, _mode: number): Promise<void>;
|
|
57
|
+
utimes(_path: string, _atime: Date, _mtime: Date): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export {};
|