sfora-cli 0.2.0 → 0.3.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 CHANGED
@@ -30,10 +30,14 @@ shell interpreter backs the interactive mode).
30
30
  ```bash
31
31
  npm i -g sfora-cli # installs the `sfora` command; or: npx sfora-cli …
32
32
 
33
- sfora login # authorize via the browser (saves a key)
34
- sfora --org <slug> # interactive shell (omit --org once saved)
33
+ sfora login # authorize as you (browser; saves your key)
34
+ sfora login --bot ci # …or as a named bot (its own key)
35
+ sfora # interactive shell — explore /projects, cat, grep
35
36
  ```
36
37
 
38
+ One CLI for everyone: the default identity is **you**; add `--bot <name>` to any
39
+ command to run as that bot.
40
+
37
41
  `sfora init` writes `~/.sfora/config.json` (chmod 600). Resolution precedence is
38
42
  **flags > env (`SFORA_API_KEY` / `SFORA_URL` / `SFORA_ORG`) > config > default**,
39
43
  so a saved config means no env vars on every run.
package/dist/cli.js CHANGED
@@ -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.agent = argv[++i];
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.agent = a.slice("--agent=".length);
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,7 +79,10 @@ function parseArgs(argv) {
77
79
  const HELP = `sfora — the CLI for your sfora workspace
78
80
 
79
81
  Get started:
80
- sfora login [--agent <name>] Authorize via the browser (saves a key)
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
@@ -149,7 +154,7 @@ async function runLogin(args, baseUrl) {
149
154
  const res = await fetch(`${base}/v1/cli/start`, {
150
155
  method: "POST",
151
156
  headers: { "content-type": "application/json" },
152
- body: JSON.stringify({ agent: args.agent }),
157
+ body: JSON.stringify({ agent: args.bot }),
153
158
  });
154
159
  if (!res.ok)
155
160
  throw new Error(`start failed (${res.status})`);
@@ -165,7 +170,7 @@ async function runLogin(args, baseUrl) {
165
170
  ? `${webOverride.replace(/\/+$/, "")}/cli?code=${encodeURIComponent(start.userCode)}`
166
171
  : start.verifyUrl;
167
172
  console.log(`\n${colors.bold}Authorize the sfora CLI${colors.reset}` +
168
- (args.agent ? ` ${colors.dim}(agent: ${args.agent})${colors.reset}` : "") +
173
+ (args.bot ? ` ${colors.dim}(bot: ${args.bot})${colors.reset}` : "") +
169
174
  `\n ${colors.cyan}${verifyUrl}${colors.reset}\n ${colors.dim}code: ${start.userCode}${colors.reset}\n`);
170
175
  openBrowser(verifyUrl);
171
176
  process.stdout.write(`${colors.dim}Waiting for approval…${colors.reset}`);
@@ -186,12 +191,21 @@ async function runLogin(args, baseUrl) {
186
191
  continue;
187
192
  }
188
193
  if (data.status === "approved" && data.apiKey) {
189
- const path = await writeConfig({
190
- url: base,
191
- apiKey: data.apiKey,
192
- org: data.orgSlug ?? undefined,
193
- });
194
- console.log(`\n${colors.green}✓${colors.reset} Logged in as ${data.name ?? "you"}${data.orgSlug ? ` ${colors.dim}(${data.orgSlug})${colors.reset}` : ""} — saved ${path}`);
194
+ const cfg = await readConfig();
195
+ const next = { ...cfg, url: base };
196
+ if (args.bot) {
197
+ // Save under the bot's slot; keep the user's personal key intact.
198
+ next.bots = {
199
+ ...cfg.bots,
200
+ [args.bot]: { apiKey: data.apiKey, org: data.orgSlug ?? undefined },
201
+ };
202
+ }
203
+ else {
204
+ next.apiKey = data.apiKey;
205
+ next.org = data.orgSlug ?? cfg.org;
206
+ }
207
+ const path = await writeConfig(next);
208
+ 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
209
  return;
196
210
  }
197
211
  if (data.status === "expired" || data.status === "unknown") {
@@ -315,7 +329,7 @@ async function main() {
315
329
  return;
316
330
  }
317
331
  const cfg = await readConfig();
318
- const settings = resolveSettings({ url: args.url, apiKey: args.key, org: args.org }, cfg);
332
+ const settings = resolveSettings({ url: args.url, apiKey: args.key, org: args.org, bot: args.bot }, cfg);
319
333
  const { url: baseUrl, apiKey } = settings;
320
334
  if (args.command === "mcp-config") {
321
335
  printMcpConfig(settings);
@@ -326,8 +340,10 @@ async function main() {
326
340
  return;
327
341
  }
328
342
  if (!apiKey) {
329
- process.stderr.write(`${colors.red}error:${colors.reset} no API key found.\n` +
330
- `Run \`sfora login\` to authorize, or \`sfora init\` to set one.\n`);
343
+ process.stderr.write(args.bot
344
+ ? `${colors.red}error:${colors.reset} no key for bot '${args.bot}'. Run \`sfora login --bot ${args.bot}\`.\n`
345
+ : `${colors.red}error:${colors.reset} no API key found.\n` +
346
+ `Run \`sfora login\` to authorize, or \`sfora init\` to set one.\n`);
331
347
  process.exitCode = 1;
332
348
  return;
333
349
  }
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: pick(flags.url, process.env.SFORA_URL, cfg.url) ?? DEFAULT_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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfora-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
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": [