sfora-cli 0.10.0 → 0.11.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.
Files changed (68) hide show
  1. package/README.md +139 -0
  2. package/dist/SforaFs.js +8 -6
  3. package/dist/api-client.d.ts +243 -4
  4. package/dist/api-client.js +248 -20
  5. package/dist/block-commands.d.ts +84 -0
  6. package/dist/block-commands.js +155 -0
  7. package/dist/cli.js +317 -26
  8. package/dist/format/blockSplice.d.ts +135 -0
  9. package/dist/format/blockSplice.js +330 -0
  10. package/dist/format/blocks/dropClosure.d.ts +10 -1
  11. package/dist/format/blocks/dropClosure.js +11 -1
  12. package/dist/format/callout.d.ts +69 -7
  13. package/dist/format/callout.js +112 -15
  14. package/dist/format/checklist.js +11 -4
  15. package/dist/format/formatAxes.d.ts +228 -0
  16. package/dist/format/formatAxes.js +454 -0
  17. package/dist/format/index.d.ts +1 -0
  18. package/dist/format/index.js +4 -0
  19. package/dist/format/lineGeometry.d.ts +34 -4
  20. package/dist/format/lineGeometry.js +140 -40
  21. package/dist/format/lint/appliesTo.d.ts +92 -0
  22. package/dist/format/lint/appliesTo.js +369 -0
  23. package/dist/format/lint/config.d.ts +106 -0
  24. package/dist/format/lint/config.js +205 -0
  25. package/dist/format/lint/fixAll.d.ts +62 -0
  26. package/dist/format/lint/fixAll.js +107 -0
  27. package/dist/format/lint/frontmatterSchema.d.ts +181 -0
  28. package/dist/format/lint/frontmatterSchema.js +660 -0
  29. package/dist/format/lint/index.d.ts +34 -5
  30. package/dist/format/lint/index.js +34 -5
  31. package/dist/format/lint/lintSource.d.ts +27 -7
  32. package/dist/format/lint/lintSource.js +67 -33
  33. package/dist/format/lint/rules/frontmatter-schema.d.ts +2 -0
  34. package/dist/format/lint/rules/frontmatter-schema.js +92 -0
  35. package/dist/format/lint/rules/index.d.ts +2 -1
  36. package/dist/format/lint/rules/index.js +7 -1
  37. package/dist/format/lint/rules/malformed-callout.js +25 -16
  38. package/dist/format/lint/rules/malformed-checklist.js +8 -3
  39. package/dist/format/lint/rules/malformed-frontmatter.js +6 -1
  40. package/dist/format/lint/severity.d.ts +15 -0
  41. package/dist/format/lint/severity.js +50 -0
  42. package/dist/format/lint/textEdits.d.ts +86 -0
  43. package/dist/format/lint/textEdits.js +162 -0
  44. package/dist/format/lint/types.d.ts +44 -8
  45. package/dist/format/markdown/slug.d.ts +28 -0
  46. package/dist/format/markdown/slug.js +63 -0
  47. package/dist/format/plaintext.js +13 -3
  48. package/dist/format/sheetCellSpans.d.ts +95 -0
  49. package/dist/format/sheetCellSpans.js +223 -0
  50. package/dist/format/sheetSelection.d.ts +136 -0
  51. package/dist/format/sheetSelection.js +282 -0
  52. package/dist/format/textStats.d.ts +23 -0
  53. package/dist/format/textStats.js +80 -0
  54. package/dist/format/wikiLinks.d.ts +60 -1
  55. package/dist/format/wikiLinks.js +195 -9
  56. package/dist/index.d.ts +26 -1
  57. package/dist/index.js +20 -3
  58. package/dist/opener.d.ts +23 -0
  59. package/dist/opener.js +26 -0
  60. package/dist/render.d.ts +132 -0
  61. package/dist/render.js +208 -0
  62. package/dist/shell-commands.d.ts +34 -0
  63. package/dist/shell-commands.js +108 -0
  64. package/dist/watch.d.ts +79 -0
  65. package/dist/watch.js +113 -0
  66. package/dist/web-url.d.ts +39 -0
  67. package/dist/web-url.js +63 -0
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,7 @@ sfora new "Acme" # create a project
11
11
  sfora post plan.md --project acme # push a markdown plan as a post
12
12
  sfora task spec.md --project acme # …or a task on the board
13
13
  sfora cat /projects/acme/board/01-todo/0042-fix-login.md
14
+ sfora url /projects/acme/docs/kickoff.md # where it lives on the web
14
15
  ```
15
16
 
16
17
  Under the hood it maps a Unix view onto sfora's `/v1/fs` HTTP API (a sandboxed
@@ -211,6 +212,144 @@ backend) are the public exports, alongside the lower-level `SforaApiClient`.
211
212
 
212
213
  ## Supported operations
213
214
 
215
+ ## Blocks — write one paragraph, not the file
216
+
217
+ A markdown body is addressable: every top-level block has an id that is a
218
+ fingerprint over its bytes, so `sfora blocks` lists them and
219
+ `sfora put --block <id>` replaces exactly one and leaves every other byte alone.
220
+
221
+ ```bash
222
+ $ sfora blocks /projects/acme/docs/kickoff.md
223
+ kfrontmat yaml id: k17e8c0... read-only
224
+ k7f3a2cx heading # Kickoff read-only
225
+ kq8w1zzp paragraph The hill chart is the one view that a…
226
+ 3 blocks · 1 writable
227
+ https://www.sfora.ai/org/acme/notes/k17e8c0...
228
+
229
+ $ echo "A rewritten paragraph." | sfora put /projects/acme/docs/kickoff.md --block kq8w1zzp
230
+ ✓ Wrote block kq8w1zzp of /v1/fs/projects/acme/library/documents/kickoff.md
231
+ changed · 3 of 4 block ids kept · 1 moved
232
+ https://www.sfora.ai/org/acme/notes/k17e8c0...
233
+ you are visible as editing this document
234
+ ```
235
+
236
+ `read-only` blocks are real lines you can see in the file — the frontmatter
237
+ fence, the title heading — that the write door does not store, so their ids
238
+ address nothing. Aim at a writable one.
239
+
240
+ **Every write prints what it did.** sfora's write door splices: a PUT of bytes
241
+ that parse the same as the stored ones stores nothing, so `no change — the
242
+ stored bytes already matched` is a real and frequent answer to a
243
+ read-edit-write loop that reformatted more than it meant to. When bytes did
244
+ move, the line says how many block ids survived it.
245
+
246
+ **A stale id is not an error, it is a re-aim.** Block ids are derived from
247
+ content, so "this id resolves to nothing" means somebody changed that block
248
+ since you read it. The server sends the document's current blocks with the
249
+ refusal, and the CLI prints them:
250
+
251
+ ```
252
+ that block is gone — somebody changed it since you read it
253
+ Block kq8w1zzp is not in this document any more.
254
+
255
+ the document has these blocks now:
256
+ k7f3a2cx line 1 ## Agenda
257
+ kt2p9lmx line 3 A rewritten paragraph.
258
+ kb91xz4q line 5 ```json
259
+
260
+ re-aim with: sfora put /projects/acme/docs/kickoff.md --block <id>
261
+ ```
262
+
263
+ The columns differ from `sfora blocks` on purpose. That listing describes what
264
+ a READ serves — frontmatter, the title heading, and `writable` to say which of
265
+ those the write door can reach. This one is what the write door FOUND, which is
266
+ the document's stored body: every id here already resolves, so there is no
267
+ read-only row to mark, and `line` is where in the file to look.
268
+
269
+ `blocks`, `put` and `url` are also commands **inside the shell**, so the body
270
+ can come from a pipeline:
271
+
272
+ ```text
273
+ sfora:/$ blocks /projects/acme/docs/kickoff.md | grep paragraph
274
+ sfora:/$ sed 's/hill chart/hill/' draft.md | put /projects/acme/docs/kickoff.md --block kq8w1zzp
275
+ ```
276
+
277
+ Writing a document also makes you **visible in it** — the app's avatar stack
278
+ shows you editing, with the block you aimed at. The CLI says so once per run.
279
+
280
+ ## Watch — a document as a channel
281
+
282
+ `sfora watch` long-polls the workspace and prints every write as it lands. Point
283
+ it at one document, or at a whole project.
284
+
285
+ ```bash
286
+ $ sfora watch /projects/acme/docs/kickoff.md
287
+ watching /projects/acme/docs/kickoff.md (not your own writes) — ^C to stop
288
+ 14:22:07 · Ada Lovelace · Kickoff · 4 of 5 block ids kept · https://www.sfora.ai/org/acme/notes/k17e8c0...
289
+ 14:24:19 · Dogfood Bot · Kickoff · edited · https://www.sfora.ai/org/acme/notes/k17e8c0...
290
+
291
+ $ sfora watch acme --json | jq -r 'select(.docType == "note") | .title'
292
+ ```
293
+
294
+ * `--json` writes **NDJSON** — one event per line (schema below).
295
+ * Your own writes are excluded by default; `--self` includes them.
296
+ * `--wait <secs>` sets the per-request long-poll budget (0–50, default 25).
297
+ * Watching a document makes you **visible in it** as a viewer; ^C aborts the
298
+ open long-poll and retracts you on the way out, rather than leaving a ghost
299
+ in the avatar stack for 90 seconds. It lands immediately — it does not wait
300
+ out the `--wait` budget. A second ^C gives up on the retraction and exits
301
+ now.
302
+ * A dropped connection reconnects with exponential backoff to 30s and resumes
303
+ from the cursor already reached — no replayed pings, no lost ones.
304
+
305
+ ### The NDJSON schema
306
+
307
+ Each line is the server's `/v1/events` object, **verbatim** — the CLI reshapes
308
+ nothing, so one parser reads the CLI's stream and the HTTP door's pages alike.
309
+
310
+ | field | |
311
+ |---|---|
312
+ | `type` | `"doc.write"` or `"doc.delete"` |
313
+ | `ts` | epoch ms — also the cursor value |
314
+ | `docType` | `"note"` · `"post"` · `"card"` |
315
+ | `docId` | the entity id |
316
+ | `projectId` | the project it lives in |
317
+ | `title`, `path` | the document's title and fs path |
318
+ | `url` | the page it can be read on |
319
+ | `author`, `authorId`, `authorType` | who wrote |
320
+ | `changed` | always `true` — a write that changed nothing never pings |
321
+ | `blockIds` | `{ rebound, orphaned, total }` for the version before this write |
322
+ | `deleted` | `doc.delete` only; `blockIds` is then absent |
323
+ | `restricted` | the ping is real, its pointer was withheld — see below |
324
+
325
+ A **restricted** ping has no `title`, `path` or `url`: the document is somebody's
326
+ unpublished draft. The event is still delivered, because a watch that went
327
+ silent would look connected and be deaf.
328
+
329
+ Warnings (reconnects) go to stderr, so `--json` stdout stays parseable.
330
+
331
+ ## Links — `sfora url` · `sfora open`
332
+
333
+ Every workspace thing has a page. The server says where; the CLI prints it.
334
+
335
+ ```bash
336
+ sfora url /projects/acme/library/documents/kickoff.md
337
+ # https://www.sfora.ai/org/acme/notes/k17e8c0...
338
+
339
+ sfora open /projects/acme/board/02-todo/0042-fix-login.md # …and opens it
340
+ sfora url /projects/acme/board --json # { "path": …, "url": … }
341
+ ```
342
+
343
+ The CLI holds **no route table and no hostname**. It asks for the path you gave
344
+ and reads the link off the answer — a markdown read carries it in the
345
+ `X-Sfora-Url` header, a JSON one in a `url` field — so links stay right when the
346
+ app's routes move, and a dev deployment hands out dev links.
347
+
348
+ `ls`, `cat` and the write verbs print the same link as one dim line **on
349
+ stderr**, so `sfora cat x.md > x.md` and `sfora ls | wc -l` stay byte-clean.
350
+ Paths with no page of their own (uploaded files, repository trees, your inbox)
351
+ print nothing, and `sfora url` on one says so.
352
+
214
353
  | op | behaviour |
215
354
  |----|-----------|
216
355
  | `ls`, `readdir` | directory listings via `/v1/fs/projects/…` (cached ~5s) |
package/dist/SforaFs.js CHANGED
@@ -413,12 +413,14 @@ export class SforaFs {
413
413
  const columns = meta.columns
414
414
  .slice()
415
415
  .sort((a, b) => a.position - b.position);
416
- const out = [
417
- `# ${project.name} roadmap`,
418
- "",
419
- `> https://www.sfora.ai/roadmap/${meta.publicSlug}`,
420
- "",
421
- ];
416
+ // The share link is the SERVER'S (`publicUrl` off `GET …/board`), not a
417
+ // hostname glued onto the slug here: which origin a deployment serves its
418
+ // pages from is the deployment's business, and a CLI pointed at a laptop or
419
+ // a preview would otherwise quote the production one. An older server that
420
+ // does not send it leaves the heading and the columns, minus the line.
421
+ const out = [`# ${project.name} roadmap`, ""];
422
+ if (meta.publicUrl)
423
+ out.push(`> ${meta.publicUrl}`, "");
422
424
  for (const col of columns) {
423
425
  out.push(`## ${col.name}`, "");
424
426
  const cards = await this.#listCards(slug, col.dirname);
@@ -24,10 +24,56 @@ export interface Entry {
24
24
  scheduledFor?: number;
25
25
  isDraft: boolean;
26
26
  }
27
+ /**
28
+ * How many block ids survived a write, from the server's rebinding ledger.
29
+ *
30
+ * Absent rather than zeroed when the door computed no correspondence — a
31
+ * create has no previous version to rebind from.
32
+ */
33
+ export interface RebindCounts {
34
+ rebound: number;
35
+ orphaned: number;
36
+ total: number;
37
+ }
38
+ /**
39
+ * The effect report every markdown write returns: did the stored bytes move,
40
+ * and what happened to the block ids if so.
41
+ *
42
+ * The reason a write reports this at all is that sfora's write door SPLICES —
43
+ * a PUT of bytes that parse the same as the stored ones stores nothing, so
44
+ * `changed: false` is a real and common answer, and a bare `201` would read as
45
+ * "saved" for a write that did nothing.
46
+ */
47
+ export interface WriteEffect {
48
+ changed: boolean;
49
+ blockIds?: RebindCounts;
50
+ }
51
+ /** What the last response said about itself. See `takeResponseInfo`. */
52
+ export interface ResponseInfo {
53
+ /** The absolute web page the response named, if any (card #335). */
54
+ url: string | null;
55
+ /** The effect report, when the response was a markdown write (card #333). */
56
+ effect: WriteEffect | null;
57
+ /**
58
+ * The write put the caller on the document's roster.
59
+ *
60
+ * Only document writes declare presence server-side — posts and cards have
61
+ * markdown bodies but nobody has one open in a document editor — so this
62
+ * comes off the response rather than being inferred from the path shape.
63
+ */
64
+ present: boolean;
65
+ }
66
+ /** The effect report in a parsed response body, or null when there is none. */
67
+ export declare function writeEffectFrom(body: unknown): WriteEffect | null;
27
68
  /** Result of a successful `PUT` (create/update). */
28
- export interface WriteResult {
69
+ export interface WriteResult extends Partial<WriteEffect> {
29
70
  filename: string;
30
71
  id: string;
72
+ /** The fs path the write landed at — canonical, which may differ from the URL. */
73
+ path?: string;
74
+ /** The web page it can be read on. */
75
+ url?: string;
76
+ created?: boolean;
31
77
  }
32
78
  /**
33
79
  * A note (doc) file entry. Mirrors a row of `GET …/docs`. Notes have stable
@@ -100,6 +146,14 @@ export interface BoardColumn {
100
146
  export interface BoardMeta {
101
147
  publicSlug: string | null;
102
148
  columns: BoardColumn[];
149
+ /**
150
+ * The shared roadmap's public page, or `null` when the board isn't shared.
151
+ *
152
+ * The slug next to it is the identifier; this is the LINK, and it comes from
153
+ * the server for the same reason every other one does — the hostname is the
154
+ * deployment's, not the CLI's.
155
+ */
156
+ publicUrl?: string | null;
103
157
  }
104
158
  /** A card file under `/projects/<slug>/board/<col>/`. `filename` is `NNNN-<slug>.md`. */
105
159
  export interface BoardCardEntry {
@@ -110,12 +164,52 @@ export interface BoardCardEntry {
110
164
  lastActivityAt: number;
111
165
  }
112
166
  /** Result of `PUT …/board/<col>/<filename>.md`. */
113
- export interface CardWriteResult {
167
+ export interface CardWriteResult extends Partial<WriteEffect> {
114
168
  filename: string;
115
169
  id: string;
116
170
  number: number;
117
171
  /** New column dirname — set when frontmatter `column:` or `status:` moved the card. */
118
172
  movedTo?: string;
173
+ path?: string;
174
+ /** The web page it can be read on. */
175
+ url?: string;
176
+ created?: boolean;
177
+ }
178
+ /** One member in a document right now. Mirrors a row of `here`. */
179
+ export interface DocPresenceMember {
180
+ memberId: string;
181
+ name: string;
182
+ type: "human" | "agent";
183
+ kind: "viewing" | "editing";
184
+ blockId?: string;
185
+ lastSeenAt: number;
186
+ }
187
+ /** `POST <doc>/_presence` — the roster, after your own declaration landed. */
188
+ export interface DocPresence {
189
+ document: string;
190
+ url?: string;
191
+ present: boolean;
192
+ block: string | null;
193
+ blockResolved: boolean | null;
194
+ here: DocPresenceMember[];
195
+ }
196
+ /**
197
+ * One event off `/v1/events`.
198
+ *
199
+ * Deliberately loose beyond the fields the CLI prints: the feed carries room
200
+ * messages and ask state-changes as well as document writes, and a client that
201
+ * enumerated every field would have to be edited each time the server learns a
202
+ * new kind. `--json` forwards the object verbatim for exactly this reason.
203
+ */
204
+ export interface AgentEvent {
205
+ type: string;
206
+ ts: number;
207
+ [key: string]: unknown;
208
+ }
209
+ /** A page of the long-poll: what happened, and where to poll from next. */
210
+ export interface AgentEventsPage {
211
+ events: AgentEvent[];
212
+ cursor: number;
119
213
  }
120
214
  export interface SforaApiConfig {
121
215
  /** e.g. `https://your-sfora.com` or `http://localhost:2222`. Trailing slashes are trimmed. */
@@ -134,11 +228,101 @@ export interface SforaApiConfig {
134
228
  export declare class SforaApiError extends Error {
135
229
  readonly status: number;
136
230
  readonly code: string;
137
- constructor(status: number, code: string, message: string);
231
+ /**
232
+ * The parsed error body, when there was one.
233
+ *
234
+ * Some refusals are USEFUL, not merely informative: a 409 from a `?block=`
235
+ * write carries the document's current blocks so the caller can re-aim
236
+ * without a second round trip. Throwing away everything but the message
237
+ * would make the CLI ask for that page again. Untyped here because the fs
238
+ * error envelope is `{ error, message }` plus whatever the specific refusal
239
+ * adds; {@link blockConflictFrom} is the typed reader for the one shape the
240
+ * CLI acts on.
241
+ */
242
+ readonly data?: unknown;
243
+ constructor(status: number, code: string, message: string, data?: unknown);
244
+ }
245
+ /** One block, as `?view=blocks` describes it. */
246
+ export interface BlockView {
247
+ id: string;
248
+ writable: boolean;
249
+ type: string;
250
+ lines: [number, number];
251
+ text: string;
252
+ depth?: number;
253
+ lang?: string;
254
+ }
255
+ /** `GET …?view=blocks` — the addressable view of a document. */
256
+ export interface BlocksView {
257
+ /** The fs path these blocks are OF — what to GET for the document itself. */
258
+ canonical: string;
259
+ note: string;
260
+ document?: {
261
+ id?: string;
262
+ title?: string;
263
+ lastEditedAt?: string;
264
+ };
265
+ blocks: BlockView[];
266
+ /** Present when block extents could not be determined; `blocks` is then empty. */
267
+ unaddressable?: string;
268
+ /** The web page the document is read on (card #335). */
269
+ url?: string;
270
+ }
271
+ /**
272
+ * One block, as a 409 lists it.
273
+ *
274
+ * A DIFFERENT SHAPE from {@link BlockView}, and the difference is not an
275
+ * oversight on either side. `?view=blocks` projects what the read serves — the
276
+ * frontmatter envelope, the title heading, the mdast type of each node, and
277
+ * `writable` to say which of those the write door can actually resolve. The
278
+ * 409 lists what the write door FOUND, which is the document's stored body:
279
+ * every id in it resolves by construction, so there is no `writable` to report
280
+ * and no envelope block to report it about. Three fields, one line each in the
281
+ * re-aim table: which id, where it is, what it says.
282
+ */
283
+ export interface BlockSummary {
284
+ id: string;
285
+ /** 1-based line the block starts on, in the stored body. */
286
+ line: number;
287
+ /** The block's first non-empty line, clipped by the server. */
288
+ preview: string;
289
+ }
290
+ /**
291
+ * The recovery payload on a 409 from a `?block=` write: the id no longer
292
+ * resolves, and here is what the document has NOW so the caller can re-aim.
293
+ */
294
+ export interface BlockConflict {
295
+ message: string;
296
+ /** The id that was aimed at. */
297
+ block?: string;
298
+ blocks: BlockSummary[];
299
+ }
300
+ /** The 409 recovery payload inside an error, or null for any other failure. */
301
+ export declare function blockConflictFrom(error: unknown): BlockConflict | null;
302
+ /** Options every markdown write door takes. */
303
+ export interface WriteOptions {
304
+ /**
305
+ * Write ONE block instead of the whole file — the id from `?view=blocks`.
306
+ *
307
+ * The body is then that block's markdown, with no frontmatter and no title:
308
+ * a block write edits prose and never a document's state.
309
+ */
310
+ blockId?: string;
138
311
  }
139
312
  export declare class SforaApiClient {
140
313
  #private;
141
314
  constructor(config: SforaApiConfig);
315
+ /** What the last response said about itself, consumed. */
316
+ takeResponseInfo(): ResponseInfo;
317
+ /**
318
+ * The web page an fs path is read on, or null when it has none.
319
+ *
320
+ * One request, and the ONLY route knowledge involved is the server's: this
321
+ * reads the path the caller gave and reports the link the answer carried.
322
+ * A directory listing answers in JSON, a document in markdown with the link
323
+ * in a header, and {@link webUrlFromResponse} covers both.
324
+ */
325
+ resolveWebUrl(fsPath: string): Promise<string | null>;
142
326
  /** `GET /v1/fs/projects` */
143
327
  listProjects(): Promise<Project[]>;
144
328
  /** `POST /v1/fs/projects` — create a project; the caller becomes its lead. */
@@ -168,7 +352,23 @@ export declare class SforaApiClient {
168
352
  /** `GET …/posts/:filename.md` (or `…/drafts/…`). Returns the raw markdown body. */
169
353
  readPost(projectSlug: string, filename: string, kind?: PostKind): Promise<string>;
170
354
  /** Create a post or upsert a mutable draft from a Markdown file. */
171
- writePost(projectSlug: string, kind: PostKind, filename: string, markdown: string): Promise<WriteResult>;
355
+ writePost(projectSlug: string, kind: PostKind, filename: string, markdown: string, options?: WriteOptions): Promise<WriteResult>;
356
+ /**
357
+ * `GET …?view=blocks` on any fs path with a stored markdown body — the
358
+ * addressable view a `?block=` write aims into.
359
+ *
360
+ * The path is the caller's fs path (`/projects/acme/docs/x.md`), not a
361
+ * `/v1/fs` URL: the CLI does not build routes.
362
+ */
363
+ readBlocks(fsPath: string): Promise<BlocksView>;
364
+ /**
365
+ * `PUT <fs path>` — the generic write door, so the CLI can put to any path
366
+ * (and to a single block of it) without a per-entity method.
367
+ *
368
+ * A 409 from a `?block=` write throws a {@link SforaApiError} carrying the
369
+ * server's recovery payload; read it with {@link blockConflictFrom}.
370
+ */
371
+ writePath(fsPath: string, markdown: string, options?: WriteOptions): Promise<WriteResult & Partial<CardWriteResult>>;
172
372
  /** `DELETE …/(posts|drafts)/:filename.md` — soft-deletes the matched post. */
173
373
  deletePost(projectSlug: string, kind: PostKind, filename: string): Promise<void>;
174
374
  /** `GET …/docs` — the project's notes, most-recently-edited first. */
@@ -236,6 +436,45 @@ export declare class SforaApiClient {
236
436
  content: string;
237
437
  reacted: boolean;
238
438
  }>;
439
+ /**
440
+ * `POST <path>/_presence` — say you are in a document.
441
+ *
442
+ * Returns `null` when the path has no roster. The CLI does not decide which
443
+ * paths those are: it asks, and a `422` is the server's named refusal
444
+ * ("posts and board cards have markdown bodies but nobody has one open in a
445
+ * document editor"). Every other failure is thrown as usual.
446
+ */
447
+ declarePresence(fsPath: string, options?: {
448
+ kind?: "viewing" | "editing";
449
+ block?: string;
450
+ leave?: boolean;
451
+ }): Promise<DocPresence | null>;
452
+ /**
453
+ * `GET /v1/events` — the long-poll. Blocks server-side until something
454
+ * happens or the wait budget elapses, then answers with a cursor to poll
455
+ * from next.
456
+ *
457
+ * `signal` is not optional in spirit: this is the ONE request in the client
458
+ * that is designed to hang, so a caller that cannot cancel it cannot stop.
459
+ * `sfora watch` aborts it from its ^C handler, which is the difference
460
+ * between a terminal that says "^C to stop" and one that does.
461
+ */
462
+ pollEvents(params: {
463
+ since: number;
464
+ wait?: number;
465
+ doc?: string;
466
+ project?: string;
467
+ includeSelf?: boolean;
468
+ signal?: AbortSignal;
469
+ }): Promise<AgentEventsPage>;
470
+ /**
471
+ * The entity id behind an fs path — what `/v1/events?doc=` wants.
472
+ *
473
+ * Read off `?view=blocks`, which states the document's identity in its
474
+ * `document` field, rather than parsed out of the file's frontmatter: the
475
+ * projection is the server saying which row these bytes are.
476
+ */
477
+ resolveDocId(fsPath: string): Promise<string | null>;
239
478
  /** `GET /v1/fs/inbox/mentions.md` — markdown summary of unread mentions. */
240
479
  readInbox(): Promise<string>;
241
480
  /** `GET /v1/fs/me/api-key` — text identity (no key material). */