sfora-cli 0.9.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 (108) hide show
  1. package/README.md +147 -6
  2. package/dist/SforaFs.js +278 -10
  3. package/dist/api-client.d.ts +290 -5
  4. package/dist/api-client.js +307 -22
  5. package/dist/block-commands.d.ts +84 -0
  6. package/dist/block-commands.js +155 -0
  7. package/dist/cli.js +323 -29
  8. package/dist/format/__tests__/byteStable.d.ts +5 -0
  9. package/dist/format/__tests__/byteStable.js +64 -0
  10. package/dist/format/blockSplice.d.ts +135 -0
  11. package/dist/format/blockSplice.js +330 -0
  12. package/dist/format/blocks/dropClosure.d.ts +81 -0
  13. package/dist/format/blocks/dropClosure.js +196 -0
  14. package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
  15. package/dist/format/blocks/markdown-block-catalog.js +162 -0
  16. package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
  17. package/dist/format/blocks/markdown-block-ids.mjs +25 -0
  18. package/dist/format/blocks/parsers.d.ts +105 -0
  19. package/dist/format/blocks/parsers.js +442 -0
  20. package/dist/format/blocks/structured-block-schema.d.ts +8 -0
  21. package/dist/format/blocks/structured-block-schema.js +30 -0
  22. package/dist/format/callout.d.ts +128 -0
  23. package/dist/format/callout.js +227 -0
  24. package/dist/format/cardMarkdown.d.ts +2 -0
  25. package/dist/format/cardMarkdown.js +10 -0
  26. package/dist/format/checklist.d.ts +34 -0
  27. package/dist/format/checklist.js +158 -0
  28. package/dist/format/formatAxes.d.ts +228 -0
  29. package/dist/format/formatAxes.js +454 -0
  30. package/dist/format/index.d.ts +19 -4
  31. package/dist/format/index.js +28 -4
  32. package/dist/format/lineGeometry.d.ts +100 -0
  33. package/dist/format/lineGeometry.js +424 -0
  34. package/dist/format/lint/appliesTo.d.ts +92 -0
  35. package/dist/format/lint/appliesTo.js +369 -0
  36. package/dist/format/lint/config.d.ts +106 -0
  37. package/dist/format/lint/config.js +205 -0
  38. package/dist/format/lint/fixAll.d.ts +62 -0
  39. package/dist/format/lint/fixAll.js +107 -0
  40. package/dist/format/lint/frontmatterSchema.d.ts +181 -0
  41. package/dist/format/lint/frontmatterSchema.js +660 -0
  42. package/dist/format/lint/index.d.ts +49 -0
  43. package/dist/format/lint/index.js +51 -0
  44. package/dist/format/lint/lintSource.d.ts +56 -0
  45. package/dist/format/lint/lintSource.js +188 -0
  46. package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
  47. package/dist/format/lint/rules/broken-wiki-link.js +45 -0
  48. package/dist/format/lint/rules/frontmatter-schema.d.ts +2 -0
  49. package/dist/format/lint/rules/frontmatter-schema.js +92 -0
  50. package/dist/format/lint/rules/index.d.ts +11 -0
  51. package/dist/format/lint/rules/index.js +32 -0
  52. package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
  53. package/dist/format/lint/rules/malformed-callout.js +88 -0
  54. package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
  55. package/dist/format/lint/rules/malformed-checklist.js +65 -0
  56. package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
  57. package/dist/format/lint/rules/malformed-frontmatter.js +98 -0
  58. package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
  59. package/dist/format/lint/rules/malformed-structured-block.js +134 -0
  60. package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
  61. package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
  62. package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
  63. package/dist/format/lint/rules/orphan-reference.js +87 -0
  64. package/dist/format/lint/severity.d.ts +15 -0
  65. package/dist/format/lint/severity.js +50 -0
  66. package/dist/format/lint/textEdits.d.ts +86 -0
  67. package/dist/format/lint/textEdits.js +162 -0
  68. package/dist/format/lint/types.d.ts +116 -0
  69. package/dist/format/lint/types.js +16 -0
  70. package/dist/format/markdown/dates.js +2 -0
  71. package/dist/format/markdown/document.js +2 -0
  72. package/dist/format/markdown/index.js +2 -0
  73. package/dist/format/markdown/mentions.js +2 -0
  74. package/dist/format/markdown/slug.d.ts +28 -0
  75. package/dist/format/markdown/slug.js +65 -0
  76. package/dist/format/markdown/yaml.js +2 -0
  77. package/dist/format/noteMarkdown.js +2 -0
  78. package/dist/format/parseWithFallback.d.ts +13 -0
  79. package/dist/format/parseWithFallback.js +98 -0
  80. package/dist/format/plaintext.d.ts +5 -0
  81. package/dist/format/plaintext.js +51 -0
  82. package/dist/format/postMarkdown.js +3 -1
  83. package/dist/format/sheetCellSpans.d.ts +95 -0
  84. package/dist/format/sheetCellSpans.js +223 -0
  85. package/dist/format/sheetSelection.d.ts +136 -0
  86. package/dist/format/sheetSelection.js +282 -0
  87. package/dist/format/taskUploadFilename.d.ts +6 -0
  88. package/dist/format/taskUploadFilename.js +13 -0
  89. package/dist/format/textStats.d.ts +23 -0
  90. package/dist/format/textStats.js +80 -0
  91. package/dist/format/wayfinder.d.ts +50 -0
  92. package/dist/format/wayfinder.js +203 -0
  93. package/dist/format/wikiLinks.d.ts +78 -0
  94. package/dist/format/wikiLinks.js +266 -0
  95. package/dist/index.d.ts +26 -1
  96. package/dist/index.js +20 -3
  97. package/dist/mcp-server.js +5 -2
  98. package/dist/opener.d.ts +23 -0
  99. package/dist/opener.js +26 -0
  100. package/dist/render.d.ts +132 -0
  101. package/dist/render.js +208 -0
  102. package/dist/shell-commands.d.ts +34 -0
  103. package/dist/shell-commands.js +108 -0
  104. package/dist/watch.d.ts +79 -0
  105. package/dist/watch.js +113 -0
  106. package/dist/web-url.d.ts +39 -0
  107. package/dist/web-url.js +63 -0
  108. package/package.json +7 -6
@@ -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
@@ -38,6 +84,35 @@ export interface Note {
38
84
  title: string;
39
85
  lastEditedAt: number;
40
86
  }
87
+ /** An uploaded project file under the unified Library. */
88
+ export interface Artifact {
89
+ name: string;
90
+ mimeType: string;
91
+ size: number;
92
+ uploadedBy: string | null;
93
+ uploadedAt: number;
94
+ url: string | null;
95
+ }
96
+ /** A GitHub repository attached to a project. Repository content is read-only. */
97
+ export interface Repository {
98
+ projectRepoId: string;
99
+ dirname: string;
100
+ owner: string;
101
+ repo: string;
102
+ defaultBranch: string;
103
+ }
104
+ export interface RepositoryTreeEntry {
105
+ path: string;
106
+ kind: "file" | "directory" | "submodule";
107
+ size?: number;
108
+ }
109
+ export interface RepositoryTree {
110
+ owner: string;
111
+ repo: string;
112
+ ref: string;
113
+ truncated: boolean;
114
+ entries: RepositoryTreeEntry[];
115
+ }
41
116
  /**
42
117
  * A pull request entry. Mirrors a row of `GET …/pulls`. Read-only — the source
43
118
  * of truth is GitHub; sfora syncs these so agents can see the code work
@@ -71,6 +146,14 @@ export interface BoardColumn {
71
146
  export interface BoardMeta {
72
147
  publicSlug: string | null;
73
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;
74
157
  }
75
158
  /** A card file under `/projects/<slug>/board/<col>/`. `filename` is `NNNN-<slug>.md`. */
76
159
  export interface BoardCardEntry {
@@ -81,12 +164,52 @@ export interface BoardCardEntry {
81
164
  lastActivityAt: number;
82
165
  }
83
166
  /** Result of `PUT …/board/<col>/<filename>.md`. */
84
- export interface CardWriteResult {
167
+ export interface CardWriteResult extends Partial<WriteEffect> {
85
168
  filename: string;
86
169
  id: string;
87
170
  number: number;
88
171
  /** New column dirname — set when frontmatter `column:` or `status:` moved the card. */
89
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;
90
213
  }
91
214
  export interface SforaApiConfig {
92
215
  /** e.g. `https://your-sfora.com` or `http://localhost:2222`. Trailing slashes are trimmed. */
@@ -105,11 +228,101 @@ export interface SforaApiConfig {
105
228
  export declare class SforaApiError extends Error {
106
229
  readonly status: number;
107
230
  readonly code: string;
108
- 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;
109
311
  }
110
312
  export declare class SforaApiClient {
111
313
  #private;
112
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>;
113
326
  /** `GET /v1/fs/projects` */
114
327
  listProjects(): Promise<Project[]>;
115
328
  /** `POST /v1/fs/projects` — create a project; the caller becomes its lead. */
@@ -121,6 +334,16 @@ export declare class SforaApiClient {
121
334
  getProjectLinks(slug: string): Promise<string>;
122
335
  /** `PUT …/links.md` — replace the project's links from a markdown list. */
123
336
  setProjectLinks(slug: string, markdown: string): Promise<void>;
337
+ /** `GET …/plan.md` — the project's plan (goal + question buckets). */
338
+ readMap(slug: string): Promise<string>;
339
+ readPlan(slug: string): Promise<string>;
340
+ /**
341
+ * `PUT …/plan.md` — set the goal. Only the `## the goal` section is
342
+ * honored; the server names everything it ignored in `ignoredSections`.
343
+ */
344
+ writePlan(slug: string, markdown: string): Promise<void>;
345
+ /** `GET …/asks.md` — coordination asks (read-only projection). */
346
+ readAsks(slug: string): Promise<string>;
124
347
  /**
125
348
  * `GET …/posts` or `…/drafts`. `scheduled` lists drafts that have a
126
349
  * (future) `scheduledFor` set.
@@ -128,14 +351,37 @@ export declare class SforaApiClient {
128
351
  listPosts(projectSlug: string, kind?: PostKind): Promise<Entry[]>;
129
352
  /** `GET …/posts/:filename.md` (or `…/drafts/…`). Returns the raw markdown body. */
130
353
  readPost(projectSlug: string, filename: string, kind?: PostKind): Promise<string>;
131
- /** `PUT …/(posts|drafts)/:filename.md` — create or update from a markdown file. */
132
- writePost(projectSlug: string, kind: PostKind, filename: string, markdown: string): Promise<WriteResult>;
354
+ /** Create a post or upsert a mutable draft from a Markdown file. */
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>>;
133
372
  /** `DELETE …/(posts|drafts)/:filename.md` — soft-deletes the matched post. */
134
373
  deletePost(projectSlug: string, kind: PostKind, filename: string): Promise<void>;
135
374
  /** `GET …/docs` — the project's notes, most-recently-edited first. */
136
375
  listNotes(projectSlug: string): Promise<Note[]>;
137
376
  /** `GET …/docs/:filename.md`. Returns the raw markdown body. */
138
377
  readNote(projectSlug: string, filename: string): Promise<string>;
378
+ writeNote(projectSlug: string, filename: string, markdown: string): Promise<WriteResult>;
379
+ deleteNote(projectSlug: string, filename: string): Promise<void>;
380
+ listArtifacts(projectSlug: string): Promise<Artifact[]>;
381
+ readArtifact(projectSlug: string, filename: string): Promise<string>;
382
+ listRepositories(projectSlug: string): Promise<Repository[]>;
383
+ getRepositoryTree(projectSlug: string, dirname: string): Promise<RepositoryTree>;
384
+ readRepositoryFile(projectSlug: string, dirname: string, path: string): Promise<string>;
139
385
  /** `GET …/pulls` — the project's synced pull requests, open first. */
140
386
  listPulls(projectSlug: string): Promise<Pull[]>;
141
387
  /**
@@ -190,6 +436,45 @@ export declare class SforaApiClient {
190
436
  content: string;
191
437
  reacted: boolean;
192
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>;
193
478
  /** `GET /v1/fs/inbox/mentions.md` — markdown summary of unread mentions. */
194
479
  readInbox(): Promise<string>;
195
480
  /** `GET /v1/fs/me/api-key` — text identity (no key material). */