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 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 {};