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/dist/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SforaConfig {
|
|
2
|
+
url?: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
org?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const CONFIG_PATH: string;
|
|
7
|
+
export declare const DEFAULT_URL = "http://localhost:2222";
|
|
8
|
+
export declare function readConfig(): Promise<SforaConfig>;
|
|
9
|
+
export declare function writeConfig(cfg: SforaConfig): Promise<string>;
|
|
10
|
+
export interface ResolvedSettings {
|
|
11
|
+
url: string;
|
|
12
|
+
apiKey: string | undefined;
|
|
13
|
+
org: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolveSettings(flags: {
|
|
16
|
+
url?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
org?: string;
|
|
19
|
+
}, cfg: SforaConfig): ResolvedSettings;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted CLI config at ~/.sfora/config.json so you don't pass
|
|
3
|
+
* SFORA_API_KEY / SFORA_URL on every invocation. Resolution precedence:
|
|
4
|
+
* explicit flags > environment > config file > built-in default.
|
|
5
|
+
*/
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
9
|
+
const CONFIG_DIR = join(homedir(), ".sfora");
|
|
10
|
+
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
11
|
+
export const DEFAULT_URL = "http://localhost:2222";
|
|
12
|
+
export async function readConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function writeConfig(cfg) {
|
|
23
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
24
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
|
|
25
|
+
// The file holds an API key — keep it owner-readable only.
|
|
26
|
+
await chmod(CONFIG_PATH, 0o600).catch(() => { });
|
|
27
|
+
return CONFIG_PATH;
|
|
28
|
+
}
|
|
29
|
+
// First non-empty value (treats "" / undefined as unset).
|
|
30
|
+
function pick(...vals) {
|
|
31
|
+
return vals.find((v) => v != null && v !== "");
|
|
32
|
+
}
|
|
33
|
+
export function resolveSettings(flags, cfg) {
|
|
34
|
+
return {
|
|
35
|
+
url: pick(flags.url, process.env.SFORA_URL, cfg.url) ?? DEFAULT_URL,
|
|
36
|
+
apiKey: pick(flags.apiKey, process.env.SFORA_API_KEY, cfg.apiKey),
|
|
37
|
+
org: pick(flags.org, process.env.SFORA_ORG, cfg.org),
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sfora — a real bash shell mounted on sfora posts.
|
|
3
|
+
*
|
|
4
|
+
* `createSforaShell` wires the `/v1/fs` HTTP API into a {@link SforaFs} and
|
|
5
|
+
* hands it to a just-bash {@link Bash} instance, so `ls`/`cat`/`grep`/`echo >`
|
|
6
|
+
* all work against your workspace's posts.
|
|
7
|
+
*/
|
|
8
|
+
import { Bash } from "just-bash";
|
|
9
|
+
import { SforaFs } from "./SforaFs.js";
|
|
10
|
+
export interface CreateSforaShellOptions {
|
|
11
|
+
/** Base URL of the sfora deployment, e.g. `http://localhost:2222`. */
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
/** Agent API key (`sk_…`). The key is org-scoped server-side. */
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/**
|
|
16
|
+
* Workspace/org slug. The API key already determines the org, so this is used
|
|
17
|
+
* for display (banner/prompt) and to document intent rather than routing.
|
|
18
|
+
*/
|
|
19
|
+
org: string;
|
|
20
|
+
/** Initial working directory inside the virtual fs. Defaults to `/`. */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SforaShell {
|
|
24
|
+
bash: Bash;
|
|
25
|
+
fs: SforaFs;
|
|
26
|
+
}
|
|
27
|
+
export declare function createSforaShell(options: CreateSforaShellOptions): SforaShell;
|
|
28
|
+
export { SforaFs } from "./SforaFs.js";
|
|
29
|
+
export { SforaApiClient, SforaApiError, type SforaApiConfig, type Project, type Entry, type PostKind, type WriteResult, } from "./api-client.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sfora — a real bash shell mounted on sfora posts.
|
|
3
|
+
*
|
|
4
|
+
* `createSforaShell` wires the `/v1/fs` HTTP API into a {@link SforaFs} and
|
|
5
|
+
* hands it to a just-bash {@link Bash} instance, so `ls`/`cat`/`grep`/`echo >`
|
|
6
|
+
* all work against your workspace's posts.
|
|
7
|
+
*/
|
|
8
|
+
import { Bash } from "just-bash";
|
|
9
|
+
import { SforaApiClient } from "./api-client.js";
|
|
10
|
+
import { SforaFs } from "./SforaFs.js";
|
|
11
|
+
export function createSforaShell(options) {
|
|
12
|
+
const client = new SforaApiClient({
|
|
13
|
+
baseUrl: options.baseUrl,
|
|
14
|
+
apiKey: options.apiKey,
|
|
15
|
+
});
|
|
16
|
+
const fs = new SforaFs(client);
|
|
17
|
+
// Disable just-bash's in-process defense-in-depth sandbox. It defaults on and
|
|
18
|
+
// hardens against *untrusted* scripts by blocking globals like `WeakRef` —
|
|
19
|
+
// which undici's `fetch` uses internally, so it would break every network-backed
|
|
20
|
+
// fs call. sfora runs the user's / agent's own commands against their own
|
|
21
|
+
// workspace over HTTPS, so this hardening is unnecessary here.
|
|
22
|
+
const bash = new Bash({ fs, cwd: options.cwd ?? "/", defenseInDepth: false });
|
|
23
|
+
return { bash, fs };
|
|
24
|
+
}
|
|
25
|
+
export { SforaFs } from "./SforaFs.js";
|
|
26
|
+
export { SforaApiClient, SforaApiError, } from "./api-client.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio server exposing a single `bash` tool. Each tool call runs the
|
|
3
|
+
* command through one persistent {@link createSforaShell} instance, with cwd/env
|
|
4
|
+
* carried across calls (just-bash doesn't persist them itself), so an agent's
|
|
5
|
+
* `cd` and `export` survive between tool invocations.
|
|
6
|
+
*
|
|
7
|
+
* stdout is the JSON-RPC channel — all human-facing logging goes to stderr.
|
|
8
|
+
*/
|
|
9
|
+
export interface RunMcpServerOptions {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
org: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function runMcpServer(options: RunMcpServerOptions): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio server exposing a single `bash` tool. Each tool call runs the
|
|
3
|
+
* command through one persistent {@link createSforaShell} instance, with cwd/env
|
|
4
|
+
* carried across calls (just-bash doesn't persist them itself), so an agent's
|
|
5
|
+
* `cd` and `export` survive between tool invocations.
|
|
6
|
+
*
|
|
7
|
+
* stdout is the JSON-RPC channel — all human-facing logging goes to stderr.
|
|
8
|
+
*/
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { createSforaShell } from "./index.js";
|
|
13
|
+
const TOOL_DESCRIPTION = `Run a bash command against the sfora workspace — a Unix-style view where every post, task, and doc is a markdown file:
|
|
14
|
+
- /projects/<slug>/posts/<file>.md published posts
|
|
15
|
+
- /projects/<slug>/drafts/<file>.md your drafts
|
|
16
|
+
- /projects/<slug>/board/<NN-col>/<NNNN>.md tasks (kanban cards), by column
|
|
17
|
+
- /projects/<slug>/docs/<file>.md docs / notes
|
|
18
|
+
- /inbox/mentions.md unread mentions
|
|
19
|
+
- /me/api-key your identity
|
|
20
|
+
Examples: 'ls /projects', 'cat /projects/web/board/01-todo/*.md', 'grep -ri TODO /projects', 'echo "# Fix login\\nstatus: active" > /projects/web/board/01-todo/fix.md'.
|
|
21
|
+
Write a file to create or update the entity (frontmatter sets fields like status/priority/assignees/due). cwd and environment persist across calls.`;
|
|
22
|
+
export async function runMcpServer(options) {
|
|
23
|
+
const { bash } = createSforaShell({
|
|
24
|
+
baseUrl: options.baseUrl,
|
|
25
|
+
apiKey: options.apiKey,
|
|
26
|
+
org: options.org,
|
|
27
|
+
});
|
|
28
|
+
// Persistent shell state across tool calls.
|
|
29
|
+
let cwd = "/";
|
|
30
|
+
let env;
|
|
31
|
+
const server = new Server({ name: "sfora", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
33
|
+
tools: [
|
|
34
|
+
{
|
|
35
|
+
name: "bash",
|
|
36
|
+
description: TOOL_DESCRIPTION,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
command: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "The bash command line to execute.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["command"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}));
|
|
50
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
51
|
+
if (request.params.name !== "bash") {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{ type: "text", text: `Unknown tool: ${request.params.name}` },
|
|
55
|
+
],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const rawArgs = request.params.arguments;
|
|
60
|
+
const command = typeof rawArgs?.command === "string" ? rawArgs.command : "";
|
|
61
|
+
if (!command.trim()) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: "Missing required 'command' argument." }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const res = await bash.exec(command, { cwd, env });
|
|
69
|
+
env = res.env;
|
|
70
|
+
cwd = res.env?.PWD ?? cwd;
|
|
71
|
+
const text = res.stderr ? `${res.stdout}${res.stderr}` : res.stdout;
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: text.length > 0 ? text : "(no output)" }],
|
|
74
|
+
isError: res.exitCode !== 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: message }],
|
|
81
|
+
isError: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const transport = new StdioServerTransport();
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
process.stderr.write(`sfora MCP server ready (org: ${options.org || "—"}, ${options.baseUrl})\n`);
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sfora-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A real bash shell + MCP server over your sfora workspace — every post, task, and doc is a markdown file your agents can ls/cat/grep/write.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sfora",
|
|
8
|
+
"mcp",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"agents",
|
|
11
|
+
"ai",
|
|
12
|
+
"cli",
|
|
13
|
+
"markdown",
|
|
14
|
+
"project-management"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/wavyrai/sfora/tree/master/packages/sfora#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/wavyrai/sfora.git",
|
|
21
|
+
"directory": "packages/sfora"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/wavyrai/sfora/issues"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"sfora": "./dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p .",
|
|
40
|
+
"dev": "tsx src/cli.ts",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"just-bash": "^3.0.1",
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.0",
|
|
49
|
+
"tsx": "^4.20.3",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20"
|
|
54
|
+
}
|
|
55
|
+
}
|