sfora-cli 0.6.0 → 0.8.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
@@ -39,6 +39,32 @@ sfora # interactive shell — explore /projects, cat, grep
39
39
  One CLI for everyone: the default identity is **you**; add `--bot <name>` to any
40
40
  command to run as that bot.
41
41
 
42
+ ## Local mode — no account, no server
43
+
44
+ `sfora init --local` turns the current repo into a workspace: a `.sfora/`
45
+ directory where tasks, posts, and docs are plain markdown files — versioned
46
+ with your code, reviewable in PRs, greppable, offline.
47
+
48
+ ```bash
49
+ sfora init --local # scaffold .sfora/ (board columns, posts/, docs/)
50
+ sfora task plan.md # → .sfora/board/01-todo/0001-<slug>.md
51
+ sfora tasks # board by column (add --json for scripting)
52
+ sfora # shell over the files — and `mv` between column
53
+ # dirs IS a card move
54
+ sfora --mcp # MCP server over the local workspace
55
+ ```
56
+
57
+ ```
58
+ .sfora/
59
+ ├── board/01-todo/0001-fix-login.md # tasks — NNNN-<slug>.md per column
60
+ ├── posts/2026-07-02-standup.md # posts — YYYY-MM-DD-<slug>.md
61
+ └── docs/architecture.md # docs
62
+ ```
63
+
64
+ It's the exact same format the cloud serves, so connecting a team later is just
65
+ `sfora login` — inside a `.sfora/` repo the CLI targets the local files;
66
+ add `--cloud` (or `--org`) to reach your cloud workspace from there.
67
+
42
68
  `sfora init` writes `~/.sfora/config.json` (chmod 600). Resolution precedence is
43
69
  **flags > env (`SFORA_API_KEY` / `SFORA_URL` / `SFORA_ORG`) > config > default**,
44
70
  so a saved config means no env vars on every run.
@@ -48,12 +74,14 @@ so a saved config means no env vars on every run.
48
74
  pnpm -F sfora build && node packages/sfora/dist/cli.js --help
49
75
  ```
50
76
 
51
- ### The `just-bash` dependency
77
+ ### The shell engine
52
78
 
53
- This package depends on `just-bash@^3.0.1` from npm no extra steps needed.
79
+ The sfora shell is a real POSIX-style shell mounted on your workspace. It's
80
+ powered by an embedded interpreter (the `just-bash` package, `^3.0.1`) — a
81
+ normal npm dependency, no extra steps needed.
54
82
 
55
- If you're working against an **unpublished** `just-bash` (e.g. the copy in
56
- `context/just-bash`), point the dependency at it and build it first:
83
+ If you're working against an **unpublished** build of the engine (e.g. the copy
84
+ in `context/just-bash`), point the dependency at it and build it first:
57
85
 
58
86
  ```jsonc
59
87
  // packages/sfora/package.json
@@ -65,11 +93,11 @@ cd context/just-bash && pnpm install && pnpm build # produces dist/bundle/*
65
93
  cd ../../ && pnpm install
66
94
  ```
67
95
 
68
- > Note: `createSforaShell` constructs the `Bash` instance with
69
- > `defenseInDepth: false`. just-bash's in-process hardening (which blocks globals
70
- > like `WeakRef`) defaults on and would break `fetch` (undici uses `WeakRef`
71
- > internally). sfora runs the user's/agent's own commands against their own
72
- > workspace over HTTPS, so that sandbox isn't needed here.
96
+ > Note: `createSforaShell` constructs the shell with `defenseInDepth: false`.
97
+ > The engine's in-process hardening (which blocks globals like `WeakRef`)
98
+ > defaults on and would break `fetch` (undici uses `WeakRef` internally). sfora
99
+ > runs the user's/agent's own commands against their own workspace over HTTPS,
100
+ > so that sandbox isn't needed here.
73
101
 
74
102
  ## Auth
75
103
 
@@ -187,7 +215,7 @@ backend) are the public exports, alongside the lower-level `SforaApiClient`.
187
215
  | `cat`, read | lazy `GET` of the markdown body (fetched only when read) |
188
216
  | `echo >`, write | `PUT` markdown → create/update a post or draft |
189
217
  | `rm`, unlink | `DELETE` → soft-delete (author or org admin/owner) |
190
- | `cd`, `pwd`, `grep`, `find`, pipes, … | all the just-bash builtins/commands |
218
+ | `cd`, `pwd`, `grep`, `find`, pipes, … | all the standard shell builtins/commands |
191
219
  | `mkdir` | no-op on known dirs; denied elsewhere (can't create projects) |
192
220
  | `chmod`, `utimes` | accepted no-ops (permissions/mtime are server-owned) |
193
221
  | symlinks, `cp`, `mv` | denied (`EPERM`) — no backend operation |
package/dist/SforaFs.d.ts CHANGED
@@ -5,6 +5,9 @@
5
5
  * /
6
6
  * ├── projects/<slug>/posts/<YYYY-MM-DD-title>.md (lazy → GET …/posts/…)
7
7
  * │ /drafts/<…>.md (lazy → GET …/drafts/…)
8
+ * │ /docs/<slug>.md (lazy → GET …/docs/…)
9
+ * │ /pulls/<number>.md (lazy → GET …/pulls/…, read-only)
10
+ * │ /public/roadmap.md (rendered from …/board)
8
11
  * ├── inbox/mentions.md (lazy → GET …/inbox)
9
12
  * └── me/api-key (lazy → GET …/me/api-key)
10
13
  *
package/dist/SforaFs.js CHANGED
@@ -5,6 +5,9 @@
5
5
  * /
6
6
  * ├── projects/<slug>/posts/<YYYY-MM-DD-title>.md (lazy → GET …/posts/…)
7
7
  * │ /drafts/<…>.md (lazy → GET …/drafts/…)
8
+ * │ /docs/<slug>.md (lazy → GET …/docs/…)
9
+ * │ /pulls/<number>.md (lazy → GET …/pulls/…, read-only)
10
+ * │ /public/roadmap.md (rendered from …/board)
8
11
  * ├── inbox/mentions.md (lazy → GET …/inbox)
9
12
  * └── me/api-key (lazy → GET …/me/api-key)
10
13
  *
@@ -83,6 +86,28 @@ function classify(path) {
83
86
  return { kind: "postFile", slug, dir, filename: seg[3] };
84
87
  }
85
88
  }
89
+ if (dir === "docs") {
90
+ if (seg.length === 3)
91
+ return { kind: "docsDir", slug };
92
+ if (seg.length === 4)
93
+ return { kind: "docFile", slug, filename: seg[3] };
94
+ }
95
+ if (dir === "pulls") {
96
+ if (seg.length === 3)
97
+ return { kind: "pullsDir", slug };
98
+ if (seg.length === 4) {
99
+ const number = Number.parseInt(seg[3], 10);
100
+ if (Number.isFinite(number))
101
+ return { kind: "pullFile", slug, number };
102
+ }
103
+ }
104
+ if (dir === "public") {
105
+ if (seg.length === 3)
106
+ return { kind: "publicDir", slug };
107
+ if (seg.length === 4 && seg[3] === "roadmap.md") {
108
+ return { kind: "roadmapFile", slug };
109
+ }
110
+ }
86
111
  if (dir === "board") {
87
112
  if (seg.length === 3)
88
113
  return { kind: "boardDir", slug };
@@ -142,6 +167,8 @@ export class SforaFs {
142
167
  // Sync snapshots for getAllPaths() (glob support) — updated as lists resolve.
143
168
  #projectSlugs = [];
144
169
  #entrySnapshots = new Map();
170
+ // Slugs whose board is shared publicly — gates the `public/` projection.
171
+ #sharedSlugs = new Set();
145
172
  constructor(client) {
146
173
  this.#client = client;
147
174
  }
@@ -179,15 +206,50 @@ export class SforaFs {
179
206
  this.#cache.delete(`${dir}:${slug}`);
180
207
  this.#cache.delete("projects"); // postCount changes
181
208
  }
209
+ // ─── notes (docs) cache ──────────────────────────────────────────
210
+ #listNotes(slug) {
211
+ const key = `docs:${slug}`;
212
+ return this.#cached(key, async () => {
213
+ const notes = await this.#client.listNotes(slug);
214
+ this.#entrySnapshots.set(`/projects/${slug}/docs`, notes.map((n) => n.filename));
215
+ return notes;
216
+ });
217
+ }
218
+ // ─── pull requests cache (read-only) ─────────────────────────────
219
+ #listPulls(slug) {
220
+ const key = `pulls:${slug}`;
221
+ return this.#cached(key, async () => {
222
+ const pulls = await this.#client.listPulls(slug);
223
+ this.#entrySnapshots.set(`/projects/${slug}/pulls`, pulls.map((p) => `${p.number}.md`));
224
+ return pulls;
225
+ });
226
+ }
227
+ async #matchPull(slug, number) {
228
+ const pulls = await this.#listPulls(slug);
229
+ return pulls.find((p) => p.number === number);
230
+ }
182
231
  // ─── board caches ────────────────────────────────────────────────
183
- #listColumns(slug) {
232
+ /** Cached `GET …/board`: sharing state (`publicSlug`) + columns, one fetch. */
233
+ #boardMeta(slug) {
184
234
  const key = `columns:${slug}`;
185
235
  return this.#cached(key, async () => {
186
- const columns = await this.#client.listColumns(slug);
187
- this.#entrySnapshots.set(`/projects/${slug}/board`, columns.map((c) => c.dirname));
188
- return columns;
236
+ const meta = await this.#client.getBoardMeta(slug);
237
+ this.#entrySnapshots.set(`/projects/${slug}/board`, meta.columns.map((c) => c.dirname));
238
+ // Keep a sync snapshot of which boards are shared, for getAllPaths().
239
+ if (meta.publicSlug)
240
+ this.#sharedSlugs.add(slug);
241
+ else
242
+ this.#sharedSlugs.delete(slug);
243
+ return meta;
189
244
  });
190
245
  }
246
+ async #listColumns(slug) {
247
+ return (await this.#boardMeta(slug)).columns;
248
+ }
249
+ /** The board's public roadmap slug, or null when it isn't shared. */
250
+ async #publicSlug(slug) {
251
+ return (await this.#boardMeta(slug)).publicSlug;
252
+ }
191
253
  #listCards(slug, column) {
192
254
  const key = `cards:${slug}/${column}`;
193
255
  return this.#cached(key, async () => {
@@ -247,6 +309,53 @@ export class SforaFs {
247
309
  return (entries.find((e) => e.filename === filename) ??
248
310
  entries.find((e) => fileSlug(e.filename) === want));
249
311
  }
312
+ async #matchNote(slug, filename) {
313
+ const notes = await this.#listNotes(slug);
314
+ const want = fileSlug(filename);
315
+ return (notes.find((n) => n.filename === filename) ??
316
+ notes.find((n) => fileSlug(n.filename) === want));
317
+ }
318
+ // ─── public roadmap render ───────────────────────────────────────
319
+ /**
320
+ * Render a shared board as a single markdown roadmap: one `##` section per
321
+ * column (position order), each card a `-` list item, with the live public URL
322
+ * under the heading. Built from the same board fetch the `/board/` tree uses.
323
+ *
324
+ * Only shared boards render — an unshared board (`publicSlug == null`) ENOENTs,
325
+ * matching the gated `public/` projection.
326
+ */
327
+ async #renderRoadmap(slug) {
328
+ const roadmapPath = `/projects/${slug}/public/roadmap.md`;
329
+ const projects = await this.#listProjects();
330
+ const project = projects.find((p) => p.slug === slug);
331
+ if (!project)
332
+ throw enoent("open", roadmapPath);
333
+ const meta = await this.#boardMeta(slug);
334
+ if (meta.publicSlug == null)
335
+ throw enoent("open", roadmapPath);
336
+ const columns = meta.columns
337
+ .slice()
338
+ .sort((a, b) => a.position - b.position);
339
+ const out = [
340
+ `# ${project.name} roadmap`,
341
+ "",
342
+ `> https://www.sfora.ai/roadmap/${meta.publicSlug}`,
343
+ "",
344
+ ];
345
+ for (const col of columns) {
346
+ out.push(`## ${col.name}`, "");
347
+ const cards = await this.#listCards(slug, col.dirname);
348
+ if (cards.length === 0) {
349
+ out.push("_No cards yet._", "");
350
+ }
351
+ else {
352
+ for (const card of cards)
353
+ out.push(`- ${card.title}`);
354
+ out.push("");
355
+ }
356
+ }
357
+ return `${out.join("\n").trimEnd()}\n`;
358
+ }
250
359
  // ─── reads ───────────────────────────────────────────────────────
251
360
  async #readText(path) {
252
361
  const loc = classify(path);
@@ -265,6 +374,27 @@ export class SforaFs {
265
374
  catch (e) {
266
375
  throw fromApi(e, "open", normalize(path));
267
376
  }
377
+ case "docFile":
378
+ try {
379
+ return await this.#client.readNote(loc.slug, loc.filename);
380
+ }
381
+ catch (e) {
382
+ throw fromApi(e, "open", normalize(path));
383
+ }
384
+ case "pullFile":
385
+ try {
386
+ return await this.#client.readPull(loc.slug, loc.number);
387
+ }
388
+ catch (e) {
389
+ throw fromApi(e, "open", normalize(path));
390
+ }
391
+ case "roadmapFile":
392
+ try {
393
+ return await this.#renderRoadmap(loc.slug);
394
+ }
395
+ catch (e) {
396
+ throw fromApi(e, "open", normalize(path));
397
+ }
268
398
  case "inboxFile":
269
399
  try {
270
400
  return await this.#client.readInbox();
@@ -292,8 +422,11 @@ export class SforaFs {
292
422
  case "meDir":
293
423
  case "projectDir":
294
424
  case "postsDir":
425
+ case "docsDir":
426
+ case "pullsDir":
295
427
  case "boardDir":
296
428
  case "boardColumnDir":
429
+ case "publicDir":
297
430
  throw eisdir("read", normalize(path));
298
431
  default:
299
432
  throw enoent("open", normalize(path));
@@ -331,8 +464,11 @@ export class SforaFs {
331
464
  loc.kind === "meDir" ||
332
465
  loc.kind === "projectDir" ||
333
466
  loc.kind === "postsDir" ||
467
+ loc.kind === "docsDir" ||
468
+ loc.kind === "pullsDir" ||
334
469
  loc.kind === "boardDir" ||
335
- loc.kind === "boardColumnDir") {
470
+ loc.kind === "boardColumnDir" ||
471
+ loc.kind === "publicDir") {
336
472
  throw eisdir("open", norm);
337
473
  }
338
474
  if (loc.kind === "postFile") {
@@ -383,14 +519,25 @@ export class SforaFs {
383
519
  return true;
384
520
  case "projectDir":
385
521
  case "postsDir":
522
+ case "docsDir":
523
+ case "pullsDir":
386
524
  case "boardDir":
387
525
  return await this.#projectExists(loc.slug);
526
+ case "publicDir":
527
+ case "roadmapFile":
528
+ // Only present when the board is shared publicly.
529
+ return ((await this.#projectExists(loc.slug)) &&
530
+ (await this.#publicSlug(loc.slug)) != null);
388
531
  case "boardColumnDir":
389
532
  return ((await this.#projectExists(loc.slug)) &&
390
533
  (await this.#columnExists(loc.slug, loc.column)));
391
534
  case "postFile":
392
535
  return (await this.#matchEntry(loc.slug, loc.dir, loc.filename)) !==
393
536
  undefined;
537
+ case "docFile":
538
+ return (await this.#matchNote(loc.slug, loc.filename)) !== undefined;
539
+ case "pullFile":
540
+ return (await this.#matchPull(loc.slug, loc.number)) !== undefined;
394
541
  case "boardCardFile":
395
542
  return ((await this.#matchCard(loc.slug, loc.column, loc.filename)) !==
396
543
  undefined);
@@ -441,10 +588,24 @@ export class SforaFs {
441
588
  return this.#fileStat(new Date());
442
589
  case "projectDir":
443
590
  case "postsDir":
591
+ case "docsDir":
592
+ case "pullsDir":
444
593
  case "boardDir":
445
594
  if (!(await this.#projectExists(loc.slug)))
446
595
  throw enoent("stat", norm);
447
596
  return this.#dirStat();
597
+ case "publicDir":
598
+ if (!(await this.#projectExists(loc.slug)))
599
+ throw enoent("stat", norm);
600
+ if ((await this.#publicSlug(loc.slug)) == null)
601
+ throw enoent("stat", norm);
602
+ return this.#dirStat();
603
+ case "roadmapFile":
604
+ if (!(await this.#projectExists(loc.slug)))
605
+ throw enoent("stat", norm);
606
+ if ((await this.#publicSlug(loc.slug)) == null)
607
+ throw enoent("stat", norm);
608
+ return this.#fileStat(new Date());
448
609
  case "boardColumnDir":
449
610
  if (!(await this.#projectExists(loc.slug)))
450
611
  throw enoent("stat", norm);
@@ -458,6 +619,18 @@ export class SforaFs {
458
619
  throw enoent("stat", norm);
459
620
  return this.#fileStat(new Date(entry.publishedAt || Date.now()));
460
621
  }
622
+ case "docFile": {
623
+ const note = await this.#matchNote(loc.slug, loc.filename);
624
+ if (!note)
625
+ throw enoent("stat", norm);
626
+ return this.#fileStat(new Date(note.lastEditedAt || Date.now()));
627
+ }
628
+ case "pullFile": {
629
+ const pull = await this.#matchPull(loc.slug, loc.number);
630
+ if (!pull)
631
+ throw enoent("stat", norm);
632
+ return this.#fileStat(new Date(pull.updatedAt || Date.now()));
633
+ }
461
634
  case "boardCardFile": {
462
635
  const card = await this.#matchCard(loc.slug, loc.column, loc.filename);
463
636
  if (!card)
@@ -505,20 +678,50 @@ export class SforaFs {
505
678
  return [dirent("mentions.md", false)];
506
679
  case "meDir":
507
680
  return [dirent("api-key", false)];
508
- case "projectDir":
681
+ case "projectDir": {
509
682
  if (!(await this.#projectExists(loc.slug))) {
510
683
  throw enoent("scandir", norm);
511
684
  }
512
- return [
685
+ const entries = [
513
686
  dirent("board", true),
687
+ dirent("docs", true),
514
688
  dirent("drafts", true),
515
689
  dirent("links.md", false),
516
690
  dirent("posts", true),
691
+ dirent("pulls", true),
517
692
  ];
693
+ // `public/` only appears once the board is shared publicly.
694
+ if ((await this.#publicSlug(loc.slug)) != null) {
695
+ entries.push(dirent("public", true));
696
+ }
697
+ return entries;
698
+ }
518
699
  case "postsDir": {
519
700
  const entries = await this.#listEntries(loc.slug, loc.dir);
520
701
  return entries.map((e) => dirent(e.filename, false));
521
702
  }
703
+ case "docsDir": {
704
+ if (!(await this.#projectExists(loc.slug))) {
705
+ throw enoent("scandir", norm);
706
+ }
707
+ const notes = await this.#listNotes(loc.slug);
708
+ return notes.map((n) => dirent(n.filename, false));
709
+ }
710
+ case "pullsDir": {
711
+ if (!(await this.#projectExists(loc.slug))) {
712
+ throw enoent("scandir", norm);
713
+ }
714
+ const pulls = await this.#listPulls(loc.slug);
715
+ return pulls.map((p) => dirent(`${p.number}.md`, false));
716
+ }
717
+ case "publicDir":
718
+ if (!(await this.#projectExists(loc.slug))) {
719
+ throw enoent("scandir", norm);
720
+ }
721
+ if ((await this.#publicSlug(loc.slug)) == null) {
722
+ throw enoent("scandir", norm);
723
+ }
724
+ return [dirent("roadmap.md", false)];
522
725
  case "boardDir": {
523
726
  if (!(await this.#projectExists(loc.slug))) {
524
727
  throw enoent("scandir", norm);
@@ -539,7 +742,10 @@ export class SforaFs {
539
742
  case "inboxFile":
540
743
  case "meFile":
541
744
  case "postFile":
745
+ case "docFile":
746
+ case "pullFile":
542
747
  case "boardCardFile":
748
+ case "roadmapFile":
543
749
  throw enotdir("scandir", norm);
544
750
  default:
545
751
  throw enoent("scandir", norm);
@@ -565,7 +771,10 @@ export class SforaFs {
565
771
  return;
566
772
  case "projectDir":
567
773
  case "postsDir":
774
+ case "docsDir":
775
+ case "pullsDir":
568
776
  case "boardDir":
777
+ case "publicDir":
569
778
  if (await this.#projectExists(loc.slug))
570
779
  return;
571
780
  throw eacces("mkdir", norm);
@@ -649,6 +858,11 @@ export class SforaFs {
649
858
  }
650
859
  case "inboxFile":
651
860
  case "meFile":
861
+ case "docFile":
862
+ case "pullFile":
863
+ case "roadmapFile":
864
+ // Read-only surfaces (notes aren't deletable through the fs yet; the
865
+ // roadmap is a projection; pulls are owned by GitHub).
652
866
  throw eacces("unlink", norm);
653
867
  case "root":
654
868
  case "projectsDir":
@@ -656,7 +870,10 @@ export class SforaFs {
656
870
  case "meDir":
657
871
  case "projectDir":
658
872
  case "postsDir":
873
+ case "docsDir":
874
+ case "pullsDir":
659
875
  case "boardDir":
876
+ case "publicDir":
660
877
  // Top-level structural dirs can't be removed.
661
878
  if (options?.recursive)
662
879
  throw eperm("rm", norm);
@@ -767,7 +984,14 @@ export class SforaFs {
767
984
  paths.add(`/projects/${slug}`);
768
985
  paths.add(`/projects/${slug}/posts`);
769
986
  paths.add(`/projects/${slug}/drafts`);
987
+ paths.add(`/projects/${slug}/docs`);
988
+ paths.add(`/projects/${slug}/pulls`);
770
989
  paths.add(`/projects/${slug}/board`);
990
+ // `public/` is only real for boards known to be shared.
991
+ if (this.#sharedSlugs.has(slug)) {
992
+ paths.add(`/projects/${slug}/public`);
993
+ paths.add(`/projects/${slug}/public/roadmap.md`);
994
+ }
771
995
  }
772
996
  for (const [dirPath, filenames] of this.#entrySnapshots) {
773
997
  for (const name of filenames)
@@ -29,6 +29,33 @@ export interface WriteResult {
29
29
  filename: string;
30
30
  id: string;
31
31
  }
32
+ /**
33
+ * A note (doc) file entry. Mirrors a row of `GET …/docs`. Notes have stable
34
+ * `<slug>.md` filenames (no date prefix) and no draft concept.
35
+ */
36
+ export interface Note {
37
+ filename: string;
38
+ title: string;
39
+ lastEditedAt: number;
40
+ }
41
+ /**
42
+ * A pull request entry. Mirrors a row of `GET …/pulls`. Read-only — the source
43
+ * of truth is GitHub; sfora syncs these so agents can see the code work
44
+ * alongside posts/tasks/docs.
45
+ */
46
+ export interface Pull {
47
+ number: number;
48
+ title: string;
49
+ state: "open" | "closed" | "merged";
50
+ draft: boolean;
51
+ author: string;
52
+ head: string;
53
+ base: string;
54
+ url: string;
55
+ additions?: number;
56
+ deletions?: number;
57
+ updatedAt: number;
58
+ }
32
59
  /** A column directory under `/projects/<slug>/board/`. `dirname` is `NN-<slug>`. */
33
60
  export interface BoardColumn {
34
61
  dirname: string;
@@ -37,6 +64,14 @@ export interface BoardColumn {
37
64
  position: number;
38
65
  cardCount: number;
39
66
  }
67
+ /**
68
+ * Board sharing state + columns. Mirrors `GET …/board`. `publicSlug` is the
69
+ * board's public roadmap slug, or `null` when the board isn't shared publicly.
70
+ */
71
+ export interface BoardMeta {
72
+ publicSlug: string | null;
73
+ columns: BoardColumn[];
74
+ }
40
75
  /** A card file under `/projects/<slug>/board/<col>/`. `filename` is `NNNN-<slug>.md`. */
41
76
  export interface BoardCardEntry {
42
77
  filename: string;
@@ -58,6 +93,13 @@ export interface SforaApiConfig {
58
93
  baseUrl: string;
59
94
  /** Agent API key (`sk_…`). Sent as `Authorization: Bearer <apiKey>`. */
60
95
  apiKey: string;
96
+ /**
97
+ * Act as another agent you own (or any, if you're an org admin), using this
98
+ * key. Sent as `X-Sfora-Act-As: <name-or-id>`. Lets one authorized CLI
99
+ * attribute work to different agents — run several on one device without a
100
+ * key per agent.
101
+ */
102
+ actAs?: string;
61
103
  }
62
104
  /** Non-2xx response from the fs API. Carries the HTTP status + machine code so SforaFs can map it to an errno. */
63
105
  export declare class SforaApiError extends Error {
@@ -90,6 +132,22 @@ export declare class SforaApiClient {
90
132
  writePost(projectSlug: string, kind: PostKind, filename: string, markdown: string): Promise<WriteResult>;
91
133
  /** `DELETE …/(posts|drafts)/:filename.md` — soft-deletes the matched post. */
92
134
  deletePost(projectSlug: string, kind: PostKind, filename: string): Promise<void>;
135
+ /** `GET …/docs` — the project's notes, most-recently-edited first. */
136
+ listNotes(projectSlug: string): Promise<Note[]>;
137
+ /** `GET …/docs/:filename.md`. Returns the raw markdown body. */
138
+ readNote(projectSlug: string, filename: string): Promise<string>;
139
+ /** `GET …/pulls` — the project's synced pull requests, open first. */
140
+ listPulls(projectSlug: string): Promise<Pull[]>;
141
+ /**
142
+ * `GET …/pulls/:number` — one PR as markdown: metadata, the cards it resolves,
143
+ * and the unified diff. Returns the raw markdown body.
144
+ */
145
+ readPull(projectSlug: string, number: number): Promise<string>;
146
+ /**
147
+ * `GET …/board` — board sharing state + columns (auto-creates the board on
148
+ * first call). `publicSlug` is null unless the board is shared publicly.
149
+ */
150
+ getBoardMeta(projectSlug: string): Promise<BoardMeta>;
93
151
  /** `GET …/board` — list the project's columns (auto-creates the board on first call). */
94
152
  listColumns(projectSlug: string): Promise<BoardColumn[]>;
95
153
  /** `GET …/board/:column` — list cards in a column. `column` is the `NN-slug` dirname. */
@@ -120,6 +178,18 @@ export declare class SforaApiClient {
120
178
  * card across columns without changing its body. Used by `mv` in the shell.
121
179
  */
122
180
  moveCard(projectSlug: string, fromCol: string, filename: string, toCol: string): Promise<CardWriteResult>;
181
+ /** `POST /v1/posts/:postId/comments` — add a comment to a post. */
182
+ createComment(postId: string, body: string): Promise<{
183
+ id: string;
184
+ }>;
185
+ /**
186
+ * `POST /v1/posts/:postId/reactions` — toggle an emoji reaction on a post.
187
+ * Returns whether the reaction is now on (`reacted: true`) or off.
188
+ */
189
+ reactToPost(postId: string, emoji: string): Promise<{
190
+ content: string;
191
+ reacted: boolean;
192
+ }>;
123
193
  /** `GET /v1/fs/inbox/mentions.md` — markdown summary of unread mentions. */
124
194
  readInbox(): Promise<string>;
125
195
  /** `GET /v1/fs/me/api-key` — text identity (no key material). */