sfora-cli 0.1.0 → 0.2.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 +14 -9
- package/dist/cli.js +140 -22
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
# sfora
|
|
1
|
+
# sfora-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
post, task, and doc is a markdown file
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
**Your sfora workspace as a markdown filesystem** — a Linear for your agents.
|
|
4
|
+
Every post, task, and doc is a markdown file under `/projects/<slug>/…`, so you
|
|
5
|
+
(and your agents) can `post`, `task`, `doc`, `ls`, `cat`, and `grep` your work —
|
|
6
|
+
or `echo >` straight into it. Ships first-class verbs, an interactive shell, and
|
|
7
|
+
an **MCP server** (`--mcp`) so agents in Claude / Cursor operate sfora natively.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
```bash
|
|
10
|
+
sfora post plan.md --project acme # push a markdown plan as a post
|
|
11
|
+
sfora task spec.md --project acme # …or a task on the board
|
|
12
|
+
sfora cat /projects/acme/board/01-todo/0042-fix-login.md
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Under the hood it maps a Unix view onto sfora's `/v1/fs` HTTP API (a sandboxed
|
|
16
|
+
shell interpreter backs the interactive mode).
|
|
12
17
|
|
|
13
18
|
```
|
|
14
19
|
/
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as readline from "node:readline";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
+
import { readFile as readLocalFile } from "node:fs/promises";
|
|
12
|
+
import { basename } from "node:path";
|
|
11
13
|
import { createSforaShell } from "./index.js";
|
|
12
14
|
import { runMcpServer } from "./mcp-server.js";
|
|
13
15
|
import { readConfig, writeConfig, resolveSettings, DEFAULT_URL, } from "./config.js";
|
|
@@ -22,7 +24,7 @@ const colors = {
|
|
|
22
24
|
red: "\x1b[31m",
|
|
23
25
|
};
|
|
24
26
|
function parseArgs(argv) {
|
|
25
|
-
const args = { cwd: "/", mcp: false, help: false };
|
|
27
|
+
const args = { rest: [], cwd: "/", mcp: false, help: false, draft: false };
|
|
26
28
|
for (let i = 0; i < argv.length; i++) {
|
|
27
29
|
const a = argv[i];
|
|
28
30
|
if (a === "--mcp")
|
|
@@ -53,32 +55,48 @@ function parseArgs(argv) {
|
|
|
53
55
|
args.web = argv[++i];
|
|
54
56
|
else if (a.startsWith("--web="))
|
|
55
57
|
args.web = a.slice("--web=".length);
|
|
56
|
-
else if (
|
|
57
|
-
args.
|
|
58
|
+
else if (a === "--project")
|
|
59
|
+
args.project = argv[++i];
|
|
60
|
+
else if (a.startsWith("--project="))
|
|
61
|
+
args.project = a.slice("--project=".length);
|
|
62
|
+
else if (a === "--column")
|
|
63
|
+
args.column = argv[++i];
|
|
64
|
+
else if (a.startsWith("--column="))
|
|
65
|
+
args.column = a.slice("--column=".length);
|
|
66
|
+
else if (a === "--draft")
|
|
67
|
+
args.draft = true;
|
|
68
|
+
else if (!a.startsWith("-")) {
|
|
69
|
+
if (!args.command)
|
|
70
|
+
args.command = a;
|
|
71
|
+
else
|
|
72
|
+
args.rest.push(a);
|
|
73
|
+
}
|
|
58
74
|
}
|
|
59
75
|
return args;
|
|
60
76
|
}
|
|
61
|
-
const HELP = `sfora —
|
|
77
|
+
const HELP = `sfora — the CLI for your sfora workspace
|
|
62
78
|
|
|
63
|
-
|
|
79
|
+
Get started:
|
|
64
80
|
sfora login [--agent <name>] Authorize via the browser (saves a key)
|
|
65
|
-
sfora init Save URL + API key + org manually
|
|
66
|
-
sfora [--org <slug>] [--cwd <path>] Interactive shell
|
|
67
|
-
sfora --mcp [--org <slug>] MCP stdio server (for agents)
|
|
68
|
-
sfora mcp-config Print an MCP config snippet to paste
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
Push markdown straight to sfora:
|
|
83
|
+
sfora post <file.md> [--project <slug>] [--draft] Publish (or draft) a post
|
|
84
|
+
sfora task <file.md> [--project <slug>] [--column <name>] Create a task
|
|
85
|
+
sfora doc <file.md> [--project <slug>] Create a doc
|
|
86
|
+
|
|
87
|
+
Browse:
|
|
88
|
+
sfora projects List your projects
|
|
89
|
+
sfora ls [path] List a path (default /projects)
|
|
90
|
+
sfora cat <path> Print a file's markdown
|
|
91
|
+
sfora Open the interactive shell
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
--org <slug>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
Agents & config:
|
|
94
|
+
sfora --mcp [--org <slug>] Run as an MCP server (for agents)
|
|
95
|
+
sfora mcp-config Print an MCP config snippet to paste
|
|
96
|
+
sfora init Save URL + API key + org manually
|
|
97
|
+
|
|
98
|
+
Config resolution: flags > env (SFORA_API_KEY / SFORA_URL / SFORA_ORG) >
|
|
99
|
+
~/.sfora/config.json > default (URL ${DEFAULT_URL}).
|
|
82
100
|
`;
|
|
83
101
|
// `sfora init` — persist url/apiKey/org to ~/.sfora/config.json. Uses flags
|
|
84
102
|
// when given, otherwise prompts (on a TTY), defaulting to any existing config.
|
|
@@ -202,6 +220,90 @@ function printMcpConfig(s) {
|
|
|
202
220
|
};
|
|
203
221
|
process.stdout.write(`${JSON.stringify(snippet, null, 2)}\n`);
|
|
204
222
|
}
|
|
223
|
+
// ─── Structured verbs (post / task / doc / ls / cat / projects) ──
|
|
224
|
+
// These talk to the workspace over the same /v1/fs the app + agents use, so the
|
|
225
|
+
// CLI is a real product interface — not a generic shell.
|
|
226
|
+
function frontmatterValue(md, key) {
|
|
227
|
+
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
228
|
+
if (!m)
|
|
229
|
+
return undefined;
|
|
230
|
+
const line = m[1]
|
|
231
|
+
.split("\n")
|
|
232
|
+
.find((l) => l.trim().toLowerCase().startsWith(`${key}:`));
|
|
233
|
+
if (!line)
|
|
234
|
+
return undefined;
|
|
235
|
+
return line
|
|
236
|
+
.slice(line.indexOf(":") + 1)
|
|
237
|
+
.trim()
|
|
238
|
+
.replace(/^["']|["']$/g, "");
|
|
239
|
+
}
|
|
240
|
+
async function resolveProject(fs, flag, md) {
|
|
241
|
+
const chosen = flag ?? frontmatterValue(md, "project");
|
|
242
|
+
if (chosen)
|
|
243
|
+
return chosen;
|
|
244
|
+
const slugs = await fs.readdir("/projects").catch(() => []);
|
|
245
|
+
if (slugs.length === 1)
|
|
246
|
+
return slugs[0];
|
|
247
|
+
throw new Error(`specify --project <slug>${slugs.length ? ` (yours: ${slugs.join(", ")})` : ""}`);
|
|
248
|
+
}
|
|
249
|
+
const VERBS = new Set(["projects", "ls", "cat", "post", "task", "doc"]);
|
|
250
|
+
async function runVerb(args, fs) {
|
|
251
|
+
const ok = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
|
252
|
+
if (args.command === "projects") {
|
|
253
|
+
const slugs = await fs.readdir("/projects");
|
|
254
|
+
console.log(slugs.length ? slugs.join("\n") : "(no projects)");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (args.command === "ls") {
|
|
258
|
+
const entries = await fs.readdir(args.rest[0] ?? "/projects");
|
|
259
|
+
console.log(entries.join("\n"));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (args.command === "cat") {
|
|
263
|
+
if (!args.rest[0])
|
|
264
|
+
throw new Error("usage: sfora cat <path>");
|
|
265
|
+
process.stdout.write(await fs.readFile(args.rest[0], "utf8"));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// post / task / doc — push a local markdown file into the workspace.
|
|
269
|
+
const file = args.rest[0];
|
|
270
|
+
if (!file) {
|
|
271
|
+
throw new Error(`usage: sfora ${args.command} <file.md> [--project <slug>]`);
|
|
272
|
+
}
|
|
273
|
+
const md = await readLocalFile(file, "utf8");
|
|
274
|
+
const project = await resolveProject(fs, args.project, md);
|
|
275
|
+
const base = basename(file);
|
|
276
|
+
const name = base.endsWith(".md") ? base : `${base}.md`;
|
|
277
|
+
if (args.command === "post") {
|
|
278
|
+
const dir = args.draft ? "drafts" : "posts";
|
|
279
|
+
await fs.writeFile(`/projects/${project}/${dir}/${name}`, md);
|
|
280
|
+
ok(`${args.draft ? "Drafted" : "Posted"} to ${project} · ${name}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (args.command === "doc") {
|
|
284
|
+
await fs.writeFile(`/projects/${project}/docs/${name}`, md);
|
|
285
|
+
ok(`Doc saved to ${project} · ${name}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (args.command === "task") {
|
|
289
|
+
const cols = await fs
|
|
290
|
+
.readdir(`/projects/${project}/board`)
|
|
291
|
+
.catch(() => []);
|
|
292
|
+
if (cols.length === 0)
|
|
293
|
+
throw new Error(`no board columns in ${project}`);
|
|
294
|
+
let col = cols[0];
|
|
295
|
+
if (args.column) {
|
|
296
|
+
const want = args.column
|
|
297
|
+
.toLowerCase()
|
|
298
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
299
|
+
.replace(/^-|-$/g, "");
|
|
300
|
+
col = cols.find((c) => c.replace(/^\d+-/, "") === want) ?? col;
|
|
301
|
+
}
|
|
302
|
+
await fs.writeFile(`/projects/${project}/board/${col}/${name}`, md);
|
|
303
|
+
ok(`Task created in ${project} / ${col} · ${name}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
205
307
|
async function main() {
|
|
206
308
|
const args = parseArgs(process.argv.slice(2));
|
|
207
309
|
if (args.help) {
|
|
@@ -225,10 +327,26 @@ async function main() {
|
|
|
225
327
|
}
|
|
226
328
|
if (!apiKey) {
|
|
227
329
|
process.stderr.write(`${colors.red}error:${colors.reset} no API key found.\n` +
|
|
228
|
-
`Run \`sfora
|
|
330
|
+
`Run \`sfora login\` to authorize, or \`sfora init\` to set one.\n`);
|
|
229
331
|
process.exitCode = 1;
|
|
230
332
|
return;
|
|
231
333
|
}
|
|
334
|
+
// Structured verbs — the workspace as a markdown filesystem.
|
|
335
|
+
if (args.command && VERBS.has(args.command)) {
|
|
336
|
+
const { fs } = createSforaShell({
|
|
337
|
+
baseUrl,
|
|
338
|
+
apiKey,
|
|
339
|
+
org: settings.org ?? "",
|
|
340
|
+
});
|
|
341
|
+
try {
|
|
342
|
+
await runVerb(args, fs);
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
process.stderr.write(`${colors.red}error:${colors.reset} ${e instanceof Error ? e.message : String(e)}\n`);
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
232
350
|
// MCP mode: stdout is the protocol channel — never write logs there.
|
|
233
351
|
if (args.mcp) {
|
|
234
352
|
await runMcpServer({ baseUrl, apiKey, org: settings.org ?? "" });
|
|
@@ -253,7 +371,7 @@ async function main() {
|
|
|
253
371
|
catch {
|
|
254
372
|
identity = "";
|
|
255
373
|
}
|
|
256
|
-
console.log(`${colors.cyan}${colors.bold}sfora${colors.reset} ${colors.dim}—
|
|
374
|
+
console.log(`${colors.cyan}${colors.bold}sfora${colors.reset} ${colors.dim}— your workspace as a markdown filesystem · ${baseUrl} (org: ${org})${colors.reset}`);
|
|
257
375
|
if (identity) {
|
|
258
376
|
const who = identity
|
|
259
377
|
.split("\n")
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sfora —
|
|
2
|
+
* sfora — your workspace as a markdown filesystem.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every post, task, and doc is a markdown file under `/projects/<slug>/…`.
|
|
5
|
+
* `createSforaShell` wires the `/v1/fs` HTTP API into a {@link SforaFs} and a
|
|
6
|
+
* sandboxed shell interpreter, so `ls`/`cat`/`grep`/`echo >` work against your
|
|
7
|
+
* workspace. The CLI also exposes first-class verbs (post/task/doc) and an MCP
|
|
8
|
+
* server so agents operate sfora natively.
|
|
7
9
|
*/
|
|
8
10
|
import { Bash } from "just-bash";
|
|
9
11
|
import { SforaFs } from "./SforaFs.js";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sfora —
|
|
2
|
+
* sfora — your workspace as a markdown filesystem.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every post, task, and doc is a markdown file under `/projects/<slug>/…`.
|
|
5
|
+
* `createSforaShell` wires the `/v1/fs` HTTP API into a {@link SforaFs} and a
|
|
6
|
+
* sandboxed shell interpreter, so `ls`/`cat`/`grep`/`echo >` work against your
|
|
7
|
+
* workspace. The CLI also exposes first-class verbs (post/task/doc) and an MCP
|
|
8
|
+
* server so agents operate sfora natively.
|
|
7
9
|
*/
|
|
8
10
|
import { Bash } from "just-bash";
|
|
9
11
|
import { SforaApiClient } from "./api-client.js";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfora-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "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.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"sfora",
|
|
8
8
|
"mcp",
|