sfora-cli 0.2.0 → 0.4.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 +7 -2
- package/dist/api-client.d.ts +5 -0
- package/dist/api-client.js +5 -0
- package/dist/cli.js +56 -20
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ or `echo >` straight into it. Ships first-class verbs, an interactive shell, and
|
|
|
7
7
|
an **MCP server** (`--mcp`) so agents in Claude / Cursor operate sfora natively.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
+
sfora new "Acme" # create a project
|
|
10
11
|
sfora post plan.md --project acme # push a markdown plan as a post
|
|
11
12
|
sfora task spec.md --project acme # …or a task on the board
|
|
12
13
|
sfora cat /projects/acme/board/01-todo/0042-fix-login.md
|
|
@@ -30,10 +31,14 @@ shell interpreter backs the interactive mode).
|
|
|
30
31
|
```bash
|
|
31
32
|
npm i -g sfora-cli # installs the `sfora` command; or: npx sfora-cli …
|
|
32
33
|
|
|
33
|
-
sfora login # authorize
|
|
34
|
-
sfora --
|
|
34
|
+
sfora login # authorize as you (browser; saves your key)
|
|
35
|
+
sfora login --bot ci # …or as a named bot (its own key)
|
|
36
|
+
sfora # interactive shell — explore /projects, cat, grep
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
One CLI for everyone: the default identity is **you**; add `--bot <name>` to any
|
|
40
|
+
command to run as that bot.
|
|
41
|
+
|
|
37
42
|
`sfora init` writes `~/.sfora/config.json` (chmod 600). Resolution precedence is
|
|
38
43
|
**flags > env (`SFORA_API_KEY` / `SFORA_URL` / `SFORA_ORG`) > config > default**,
|
|
39
44
|
so a saved config means no env vars on every run.
|
package/dist/api-client.d.ts
CHANGED
|
@@ -70,6 +70,11 @@ export declare class SforaApiClient {
|
|
|
70
70
|
constructor(config: SforaApiConfig);
|
|
71
71
|
/** `GET /v1/fs/projects` */
|
|
72
72
|
listProjects(): Promise<Project[]>;
|
|
73
|
+
/** `POST /v1/fs/projects` — create a project; the caller becomes its lead. */
|
|
74
|
+
createProject(name: string): Promise<{
|
|
75
|
+
slug: string;
|
|
76
|
+
name: string;
|
|
77
|
+
}>;
|
|
73
78
|
/**
|
|
74
79
|
* `GET …/posts` or `…/drafts`. `scheduled` lists drafts that have a
|
|
75
80
|
* (future) `scheduledFor` set.
|
package/dist/api-client.js
CHANGED
|
@@ -85,6 +85,11 @@ export class SforaApiClient {
|
|
|
85
85
|
const data = await this.#json("/v1/fs/projects");
|
|
86
86
|
return data.projects;
|
|
87
87
|
}
|
|
88
|
+
/** `POST /v1/fs/projects` — create a project; the caller becomes its lead. */
|
|
89
|
+
async createProject(name) {
|
|
90
|
+
const res = await this.#request("POST", "/v1/fs/projects", JSON.stringify({ name }));
|
|
91
|
+
return (await res.json());
|
|
92
|
+
}
|
|
88
93
|
/**
|
|
89
94
|
* `GET …/posts` or `…/drafts`. `scheduled` lists drafts that have a
|
|
90
95
|
* (future) `scheduledFor` set.
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as readline from "node:readline";
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { readFile as readLocalFile } from "node:fs/promises";
|
|
12
12
|
import { basename } from "node:path";
|
|
13
|
-
import { createSforaShell } from "./index.js";
|
|
13
|
+
import { createSforaShell, SforaApiClient } from "./index.js";
|
|
14
14
|
import { runMcpServer } from "./mcp-server.js";
|
|
15
15
|
import { readConfig, writeConfig, resolveSettings, DEFAULT_URL, } from "./config.js";
|
|
16
16
|
const colors = {
|
|
@@ -47,10 +47,12 @@ function parseArgs(argv) {
|
|
|
47
47
|
args.key = argv[++i];
|
|
48
48
|
else if (a.startsWith("--key="))
|
|
49
49
|
args.key = a.slice("--key=".length);
|
|
50
|
-
else if (a === "--agent")
|
|
51
|
-
args.
|
|
50
|
+
else if (a === "--bot" || a === "--agent")
|
|
51
|
+
args.bot = argv[++i];
|
|
52
|
+
else if (a.startsWith("--bot="))
|
|
53
|
+
args.bot = a.slice("--bot=".length);
|
|
52
54
|
else if (a.startsWith("--agent="))
|
|
53
|
-
args.
|
|
55
|
+
args.bot = a.slice("--agent=".length);
|
|
54
56
|
else if (a === "--web")
|
|
55
57
|
args.web = argv[++i];
|
|
56
58
|
else if (a.startsWith("--web="))
|
|
@@ -77,15 +79,21 @@ function parseArgs(argv) {
|
|
|
77
79
|
const HELP = `sfora — the CLI for your sfora workspace
|
|
78
80
|
|
|
79
81
|
Get started:
|
|
80
|
-
sfora login
|
|
82
|
+
sfora login Authorize as you (saves your key)
|
|
83
|
+
sfora login --bot <name> Authorize as a named bot (its own key)
|
|
84
|
+
|
|
85
|
+
Run as a bot: add --bot <name> to any command (default identity is you).
|
|
81
86
|
|
|
82
87
|
Push markdown straight to sfora:
|
|
83
88
|
sfora post <file.md> [--project <slug>] [--draft] Publish (or draft) a post
|
|
84
89
|
sfora task <file.md> [--project <slug>] [--column <name>] Create a task
|
|
85
90
|
sfora doc <file.md> [--project <slug>] Create a doc
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
Projects:
|
|
93
|
+
sfora new "<name>" Create a project
|
|
88
94
|
sfora projects List your projects
|
|
95
|
+
|
|
96
|
+
Browse:
|
|
89
97
|
sfora ls [path] List a path (default /projects)
|
|
90
98
|
sfora cat <path> Print a file's markdown
|
|
91
99
|
sfora Open the interactive shell
|
|
@@ -149,7 +157,7 @@ async function runLogin(args, baseUrl) {
|
|
|
149
157
|
const res = await fetch(`${base}/v1/cli/start`, {
|
|
150
158
|
method: "POST",
|
|
151
159
|
headers: { "content-type": "application/json" },
|
|
152
|
-
body: JSON.stringify({ agent: args.
|
|
160
|
+
body: JSON.stringify({ agent: args.bot }),
|
|
153
161
|
});
|
|
154
162
|
if (!res.ok)
|
|
155
163
|
throw new Error(`start failed (${res.status})`);
|
|
@@ -165,7 +173,7 @@ async function runLogin(args, baseUrl) {
|
|
|
165
173
|
? `${webOverride.replace(/\/+$/, "")}/cli?code=${encodeURIComponent(start.userCode)}`
|
|
166
174
|
: start.verifyUrl;
|
|
167
175
|
console.log(`\n${colors.bold}Authorize the sfora CLI${colors.reset}` +
|
|
168
|
-
(args.
|
|
176
|
+
(args.bot ? ` ${colors.dim}(bot: ${args.bot})${colors.reset}` : "") +
|
|
169
177
|
`\n ${colors.cyan}${verifyUrl}${colors.reset}\n ${colors.dim}code: ${start.userCode}${colors.reset}\n`);
|
|
170
178
|
openBrowser(verifyUrl);
|
|
171
179
|
process.stdout.write(`${colors.dim}Waiting for approval…${colors.reset}`);
|
|
@@ -186,12 +194,21 @@ async function runLogin(args, baseUrl) {
|
|
|
186
194
|
continue;
|
|
187
195
|
}
|
|
188
196
|
if (data.status === "approved" && data.apiKey) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
const cfg = await readConfig();
|
|
198
|
+
const next = { ...cfg, url: base };
|
|
199
|
+
if (args.bot) {
|
|
200
|
+
// Save under the bot's slot; keep the user's personal key intact.
|
|
201
|
+
next.bots = {
|
|
202
|
+
...cfg.bots,
|
|
203
|
+
[args.bot]: { apiKey: data.apiKey, org: data.orgSlug ?? undefined },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
next.apiKey = data.apiKey;
|
|
208
|
+
next.org = data.orgSlug ?? cfg.org;
|
|
209
|
+
}
|
|
210
|
+
const path = await writeConfig(next);
|
|
211
|
+
console.log(`\n${colors.green}✓${colors.reset} Logged in as ${data.name ?? "you"}${data.orgSlug ? ` ${colors.dim}(${data.orgSlug})${colors.reset}` : ""}${args.bot ? ` ${colors.dim}[bot: ${args.bot}]${colors.reset}` : ""} — saved ${path}`);
|
|
195
212
|
return;
|
|
196
213
|
}
|
|
197
214
|
if (data.status === "expired" || data.status === "unknown") {
|
|
@@ -246,14 +263,30 @@ async function resolveProject(fs, flag, md) {
|
|
|
246
263
|
return slugs[0];
|
|
247
264
|
throw new Error(`specify --project <slug>${slugs.length ? ` (yours: ${slugs.join(", ")})` : ""}`);
|
|
248
265
|
}
|
|
249
|
-
const VERBS = new Set([
|
|
250
|
-
|
|
266
|
+
const VERBS = new Set([
|
|
267
|
+
"projects",
|
|
268
|
+
"new",
|
|
269
|
+
"ls",
|
|
270
|
+
"cat",
|
|
271
|
+
"post",
|
|
272
|
+
"task",
|
|
273
|
+
"doc",
|
|
274
|
+
]);
|
|
275
|
+
async function runVerb(args, fs, client) {
|
|
251
276
|
const ok = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
|
252
277
|
if (args.command === "projects") {
|
|
253
278
|
const slugs = await fs.readdir("/projects");
|
|
254
279
|
console.log(slugs.length ? slugs.join("\n") : "(no projects)");
|
|
255
280
|
return;
|
|
256
281
|
}
|
|
282
|
+
if (args.command === "new") {
|
|
283
|
+
const name = args.rest.join(" ").trim();
|
|
284
|
+
if (!name)
|
|
285
|
+
throw new Error('usage: sfora new "<project name>"');
|
|
286
|
+
const res = await client.createProject(name);
|
|
287
|
+
ok(`Created project ${res.name} · /projects/${res.slug}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
257
290
|
if (args.command === "ls") {
|
|
258
291
|
const entries = await fs.readdir(args.rest[0] ?? "/projects");
|
|
259
292
|
console.log(entries.join("\n"));
|
|
@@ -315,7 +348,7 @@ async function main() {
|
|
|
315
348
|
return;
|
|
316
349
|
}
|
|
317
350
|
const cfg = await readConfig();
|
|
318
|
-
const settings = resolveSettings({ url: args.url, apiKey: args.key, org: args.org }, cfg);
|
|
351
|
+
const settings = resolveSettings({ url: args.url, apiKey: args.key, org: args.org, bot: args.bot }, cfg);
|
|
319
352
|
const { url: baseUrl, apiKey } = settings;
|
|
320
353
|
if (args.command === "mcp-config") {
|
|
321
354
|
printMcpConfig(settings);
|
|
@@ -326,8 +359,10 @@ async function main() {
|
|
|
326
359
|
return;
|
|
327
360
|
}
|
|
328
361
|
if (!apiKey) {
|
|
329
|
-
process.stderr.write(
|
|
330
|
-
|
|
362
|
+
process.stderr.write(args.bot
|
|
363
|
+
? `${colors.red}error:${colors.reset} no key for bot '${args.bot}'. Run \`sfora login --bot ${args.bot}\`.\n`
|
|
364
|
+
: `${colors.red}error:${colors.reset} no API key found.\n` +
|
|
365
|
+
`Run \`sfora login\` to authorize, or \`sfora init\` to set one.\n`);
|
|
331
366
|
process.exitCode = 1;
|
|
332
367
|
return;
|
|
333
368
|
}
|
|
@@ -338,8 +373,9 @@ async function main() {
|
|
|
338
373
|
apiKey,
|
|
339
374
|
org: settings.org ?? "",
|
|
340
375
|
});
|
|
376
|
+
const client = new SforaApiClient({ baseUrl, apiKey });
|
|
341
377
|
try {
|
|
342
|
-
await runVerb(args, fs);
|
|
378
|
+
await runVerb(args, fs, client);
|
|
343
379
|
}
|
|
344
380
|
catch (e) {
|
|
345
381
|
process.stderr.write(`${colors.red}error:${colors.reset} ${e instanceof Error ? e.message : String(e)}\n`);
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ export interface SforaConfig {
|
|
|
2
2
|
url?: string;
|
|
3
3
|
apiKey?: string;
|
|
4
4
|
org?: string;
|
|
5
|
+
bots?: Record<string, {
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
org?: string;
|
|
8
|
+
}>;
|
|
5
9
|
}
|
|
6
10
|
export declare const CONFIG_PATH: string;
|
|
7
11
|
export declare const DEFAULT_URL = "http://localhost:2222";
|
|
@@ -16,4 +20,5 @@ export declare function resolveSettings(flags: {
|
|
|
16
20
|
url?: string;
|
|
17
21
|
apiKey?: string;
|
|
18
22
|
org?: string;
|
|
23
|
+
bot?: string;
|
|
19
24
|
}, cfg: SforaConfig): ResolvedSettings;
|
package/dist/config.js
CHANGED
|
@@ -31,8 +31,19 @@ function pick(...vals) {
|
|
|
31
31
|
return vals.find((v) => v != null && v !== "");
|
|
32
32
|
}
|
|
33
33
|
export function resolveSettings(flags, cfg) {
|
|
34
|
+
const url = pick(flags.url, process.env.SFORA_URL, cfg.url) ?? DEFAULT_URL;
|
|
35
|
+
// `--bot <name>`: act as that saved bot. Don't fall back to the user's key /
|
|
36
|
+
// SFORA_API_KEY env, so the identity is never silently the wrong one.
|
|
37
|
+
if (flags.bot) {
|
|
38
|
+
const bot = cfg.bots?.[flags.bot];
|
|
39
|
+
return {
|
|
40
|
+
url,
|
|
41
|
+
apiKey: pick(flags.apiKey, bot?.apiKey),
|
|
42
|
+
org: pick(flags.org, bot?.org, cfg.org),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
34
45
|
return {
|
|
35
|
-
url
|
|
46
|
+
url,
|
|
36
47
|
apiKey: pick(flags.apiKey, process.env.SFORA_API_KEY, cfg.apiKey),
|
|
37
48
|
org: pick(flags.org, process.env.SFORA_ORG, cfg.org),
|
|
38
49
|
};
|
package/package.json
CHANGED