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.
@@ -26,14 +26,18 @@ function routeBase(kind) {
26
26
  export class SforaApiClient {
27
27
  #baseUrl;
28
28
  #apiKey;
29
+ #actAs;
29
30
  constructor(config) {
30
31
  this.#baseUrl = config.baseUrl.replace(/\/+$/, "");
31
32
  this.#apiKey = config.apiKey;
33
+ this.#actAs = config.actAs;
32
34
  }
33
35
  // ─── HTTP plumbing ──────────────────────────────────────────────
34
36
  async #request(method, path, body) {
35
37
  const headers = Object.create(null);
36
38
  headers.Authorization = `Bearer ${this.#apiKey}`;
39
+ if (this.#actAs)
40
+ headers["X-Sfora-Act-As"] = this.#actAs;
37
41
  if (body !== undefined)
38
42
  headers["Content-Type"] = "text/markdown";
39
43
  let res;
@@ -135,12 +139,47 @@ export class SforaApiClient {
135
139
  const file = encodeURIComponent(filename);
136
140
  await this.#request("DELETE", `/v1/fs/projects/${slug}/${base}/${file}`);
137
141
  }
142
+ // ─── Notes (docs) ────────────────────────────────────────────────
143
+ /** `GET …/docs` — the project's notes, most-recently-edited first. */
144
+ async listNotes(projectSlug) {
145
+ const slug = encodeURIComponent(projectSlug);
146
+ const data = await this.#json(`/v1/fs/projects/${slug}/docs`);
147
+ return data.docs ?? [];
148
+ }
149
+ /** `GET …/docs/:filename.md`. Returns the raw markdown body. */
150
+ async readNote(projectSlug, filename) {
151
+ const slug = encodeURIComponent(projectSlug);
152
+ const file = encodeURIComponent(filename);
153
+ return this.#text(`/v1/fs/projects/${slug}/docs/${file}`);
154
+ }
155
+ // ─── Pull requests (read-only) ───────────────────────────────────
156
+ /** `GET …/pulls` — the project's synced pull requests, open first. */
157
+ async listPulls(projectSlug) {
158
+ const slug = encodeURIComponent(projectSlug);
159
+ const data = await this.#json(`/v1/fs/projects/${slug}/pulls`);
160
+ return data.pulls ?? [];
161
+ }
162
+ /**
163
+ * `GET …/pulls/:number` — one PR as markdown: metadata, the cards it resolves,
164
+ * and the unified diff. Returns the raw markdown body.
165
+ */
166
+ async readPull(projectSlug, number) {
167
+ const slug = encodeURIComponent(projectSlug);
168
+ return this.#text(`/v1/fs/projects/${slug}/pulls/${number}`);
169
+ }
138
170
  // ─── Kanban board ────────────────────────────────────────────────
139
- /** `GET …/board` — list the project's columns (auto-creates the board on first call). */
140
- async listColumns(projectSlug) {
171
+ /**
172
+ * `GET …/board` — board sharing state + columns (auto-creates the board on
173
+ * first call). `publicSlug` is null unless the board is shared publicly.
174
+ */
175
+ async getBoardMeta(projectSlug) {
141
176
  const slug = encodeURIComponent(projectSlug);
142
177
  const data = await this.#json(`/v1/fs/projects/${slug}/board`);
143
- return data.columns;
178
+ return { publicSlug: data.publicSlug ?? null, columns: data.columns };
179
+ }
180
+ /** `GET …/board` — list the project's columns (auto-creates the board on first call). */
181
+ async listColumns(projectSlug) {
182
+ return (await this.getBoardMeta(projectSlug)).columns;
144
183
  }
145
184
  /** `GET …/board/:column` — list cards in a column. `column` is the `NN-slug` dirname. */
146
185
  async listCards(projectSlug, columnDir) {
@@ -239,6 +278,40 @@ export class SforaApiClient {
239
278
  throw await this.#toError(res);
240
279
  return (await res.json());
241
280
  }
281
+ // ─── Post actions (comment / react) ─────────────────────────────
282
+ /** `POST /v1/posts/:postId/comments` — add a comment to a post. */
283
+ async createComment(postId, body) {
284
+ const res = await fetch(`${this.#baseUrl}/v1/posts/${encodeURIComponent(postId)}/comments`, {
285
+ method: "POST",
286
+ headers: {
287
+ Authorization: `Bearer ${this.#apiKey}`,
288
+ "Content-Type": "application/json",
289
+ },
290
+ body: JSON.stringify({ body }),
291
+ });
292
+ if (!res.ok)
293
+ throw await this.#toError(res);
294
+ const data = (await res.json());
295
+ return { id: data.comment?._id ?? "" };
296
+ }
297
+ /**
298
+ * `POST /v1/posts/:postId/reactions` — toggle an emoji reaction on a post.
299
+ * Returns whether the reaction is now on (`reacted: true`) or off.
300
+ */
301
+ async reactToPost(postId, emoji) {
302
+ const res = await fetch(`${this.#baseUrl}/v1/posts/${encodeURIComponent(postId)}/reactions`, {
303
+ method: "POST",
304
+ headers: {
305
+ Authorization: `Bearer ${this.#apiKey}`,
306
+ "Content-Type": "application/json",
307
+ },
308
+ body: JSON.stringify({ emoji }),
309
+ });
310
+ if (!res.ok)
311
+ throw await this.#toError(res);
312
+ const data = (await res.json());
313
+ return data.reaction ?? { content: emoji, reacted: true };
314
+ }
242
315
  /** `GET /v1/fs/inbox/mentions.md` — markdown summary of unread mentions. */
243
316
  async readInbox() {
244
317
  return this.#text("/v1/fs/inbox/mentions.md");