Skip to content
659 linesCodeBlameRaw
1/**
2 * Docs: the workspace's written knowledge, kept by the artifacts service
3 * (`services/artifacts`). Spaces hold trees of pages; each page is a CRDT
4 * document (Yjs) edited live over a socket, saved with a Markdown
5 * rendition that search, agents, export and the read view use.
6 *
7 * Wire shapes are snake_case end to end: the site, agents and the live
8 * socket all carry the same objects.
9 *
10 * Access, in one place:
11 *
12 * - A space has a kind: `workspace` (every member gets `default_role`),
13 * `team` (the team's members get `default_role`) or `private` (only the
14 * people, agents and teams listed as its members).
15 * - Members are listed with a role: view < comment < edit < manage. A
16 * person's role is the highest of the space's base role (when it applies
17 * to them) and every listing that names them or one of their teams.
18 * Workspace owners manage every workspace and team space; a private space
19 * is its members' alone.
20 * - An agent never sees or changes more than the person it acts for (the
21 * `viewer` on every agent call). It reads what that person can read,
22 * narrowed further to what every person in the `audience` can read; it
23 * suggests where that person can comment; and it edits directly only
24 * where that person can edit AND the space lets agents edit
25 * (`agent_mode: "edit"`). Otherwise its edit becomes a suggestion.
26 */
27import type { MemberProfile, Principal } from "./chat";
28import type { ServiceBinding } from "./clients";
29import type { User } from "./identity";
30import type { Result } from "./result";
31
32/** What someone may do in a space, weakest first. */
33export type DocRole = "view" | "comment" | "edit" | "manage";
34export const DOC_ROLES: readonly DocRole[] = ["view", "comment", "edit", "manage"];
35
36export const DOC_ROLE_LABELS: Record<DocRole, string> = {
37 view: "Can view",
38 comment: "Can comment",
39 edit: "Can edit",
40 manage: "Full access",
41};
42
43export const DOC_ROLE_SUMMARIES: Record<DocRole, string> = {
44 view: "Read pages and their history.",
45 comment: "Read pages and comment on them.",
46 edit: "Write and organize pages, and accept or reject suggestions.",
47 manage: "Edit, and change who has access and how agents work here.",
48};
49
50/** Who a space is for. */
51export type DocSpaceKind = "workspace" | "team" | "private";
52
53export const DOC_SPACE_KIND_LABELS: Record<DocSpaceKind, string> = {
54 workspace: "Everyone in the workspace",
55 team: "A team",
56 private: "Only its members",
57};
58
59/** How agents change pages in a space: as tracked suggestions (the default), or directly. */
60export type DocAgentMode = "suggest" | "edit";
61
62export const DOC_AGENT_MODE_LABELS: Record<DocAgentMode, string> = {
63 suggest: "Suggest changes",
64 edit: "Edit directly",
65};
66
67/** A key for a space member: `user:<id>`, `agent:<id>` or `team:<slug>`. */
68export type DocMemberKey = string;
69
70export type DocSpace = {
71 id: string;
72 workspace_id: string;
73 /** Unique in the workspace; in the URL: `/<workspace>/-/docs/<slug>`. */
74 slug: string;
75 name: string;
76 description: string | null;
77 /** An emoji, or null for the default book. */
78 icon: string | null;
79 kind: DocSpaceKind;
80 /** The team's slug, for a team space. */
81 team: string | null;
82 /** What every workspace member (workspace) or team member (team) gets; null for a private space. */
83 default_role: DocRole | null;
84 agent_mode: DocAgentMode;
85 /** People with edit access may share what is in it (Artifacts), as people with full access can. Off unless turned on. */
86 editors_can_share: boolean;
87 /**
88 * The workspace's General space, made once when Artifacts is first
89 * opened and shown to every member without joining. It can be archived
90 * and deleted like any other; once deleted it is not made again, and no
91 * other space takes its place.
92 */
93 is_default: boolean;
94 /** Projects (repositories, `owner/name`) the space is about: Docs filters by them. */
95 projects: string[];
96 created_by: Principal;
97 created_at: string;
98 archived_at: string | null;
99 /** The viewer's role in it. */
100 viewer_role: DocRole;
101 /** Pages in it that are not in the trash. */
102 page_count: number;
103};
104
105/** A space member, as the space's settings show it. */
106export type DocSpaceMember = {
107 key: DocMemberKey;
108 kind: "user" | "agent" | "team";
109 /** Username, agent handle or team slug. */
110 name: string;
111 display_name: string;
112 avatar: string | null;
113 avatar_seed?: string | null;
114 role: DocRole;
115};
116
117/** Enough to link to a page. */
118export type DocPageRef = {
119 id: string;
120 space_id: string;
121 space_slug: string;
122 title: string;
123 /** An emoji, or null for the default page icon. */
124 icon: string | null;
125 /** `<title-slug>-<id>`: the last part of the page's URL. */
126 slug: string;
127 /** `/<workspace>/-/docs/<space>/<slug>`. */
128 path: string;
129};
130
131export type DocPage = DocPageRef & {
132 parent_id: string | null;
133 position: number;
134 /** A cover image's URL, or a CSS gradient name (`gradient:<n>`); null for none. */
135 cover: string | null;
136 created_by: MemberProfile;
137 created_at: string;
138 updated_by: MemberProfile | null;
139 updated_at: string;
140 /** When it went to the trash; null when it is not there. */
141 archived_at: string | null;
142 has_children: boolean;
143 /** Projects (`owner/name`) the page is about, besides its space's. */
144 projects: string[];
145 owners: MemberProfile[];
146 /** The first lines of its text, for cards. */
147 excerpt: string;
148 /** Whether code it cites changed since someone last marked it current: possibly out of date. */
149 stale: boolean;
150};
151
152// ── Citations and staleness ───────────────────────────────────────────────
153//
154// A page can cite code: a path (a file, a folder, or a glob like
155// `src/export/**`) in a repository, optionally naming what at that path it
156// describes (a symbol, an endpoint, an environment variable). Citations
157// come from the page's text (the editor's citation chips, and links to
158// files in a repository: `/<owner>/<repo>/blob/<ref>/<path>`) and from
159// the page's header ("Describes"). When a merged pull request or a push
160// to a repository's default branch changes a cited path, the page is
161// marked possibly out of date with that change, until someone with edit
162// access marks it current again (or an agent updates it).
163
164/** What a citation names at its path. */
165export type DocCitationKind = "path" | "symbol" | "endpoint" | "env";
166export const DOC_CITATION_KINDS: readonly DocCitationKind[] = ["path", "symbol", "endpoint", "env"];
167
168export const DOC_CITATION_KIND_LABELS: Record<DocCitationKind, string> = {
169 path: "A file or folder",
170 symbol: "A symbol",
171 endpoint: "An endpoint",
172 env: "An environment variable",
173};
174
175export type DocCitation = {
176 /** `owner/name`, lowercased. */
177 repo: string;
178 /** A file, a folder (everything under it), or a glob (`*`, `**`, `?`). */
179 path: string;
180 kind: DocCitationKind;
181 /** The symbol, endpoint (`POST /v1/export`) or variable (`EXPORT_BUCKET`), for those kinds. */
182 label: string | null;
183 /** The commit it was cited at, when known. */
184 ref: string | null;
185 /** From the page's text, or from its header's "Describes". */
186 source: "body" | "header";
187};
188
189/** One entry of a page's "Describes": a repository and a path in it. */
190export type DocDescribes = { repo: string; path: string };
191
192/** A change that made a page possibly out of date. */
193export type DocStaleChange = {
194 /**
195 * False when the viewer can't read the repository: then `repo`, `pull`,
196 * `commit` and `paths` are blank, and the page only says that a change
197 * they can't see touched code it cites.
198 */
199 visible: boolean;
200 repo: string | null;
201 commit: string | null;
202 /** The merged pull request, when the change came from one. */
203 pull: { number: number; title: string | null } | null;
204 /** The cited paths it changed (the changed files, at most 20). */
205 paths: string[];
206 /** When it was noticed. RFC 3339. */
207 at: string;
208};
209
210/** Why a page is possibly out of date: every change since it was last marked current, newest first. */
211export type DocStaleness = { since: string; changes: DocStaleChange[] };
212
213// ── A project's docs ──────────────────────────────────────────────────────
214//
215// A repository's `docs/` folder (and README.md), shown read-only in Docs
216// next to the workspace's spaces and found by the same search. It is read
217// from the default branch and kept up to date on every push to it. Each
218// reader sees only the repositories they can read. Changes go through the
219// repository: "Edit in Code" opens the file.
220
221export type DocRepoFile = {
222 /** From the repository's root: `docs/guide/setup.md`, `README.md`. */
223 path: string;
224 /** Its first heading, or its file name. */
225 title: string;
226};
227
228export type DocRepoSpace = {
229 id: string;
230 /** `owner/name`, as the repository is named now. */
231 repo: string;
232 default_branch: string;
233 /** The commit it was read at; null until it has been. */
234 commit: string | null;
235 indexed_at: string | null;
236 added_by: MemberProfile;
237 files: DocRepoFile[];
238 /** Whether the viewer may stop showing it (whoever added it, or an owner). */
239 can_remove: boolean;
240};
241
242export type DocRepoPage = {
243 space: DocRepoSpace;
244 file: DocRepoFile & {
245 markdown: string;
246 /** `/<workspace>/-/docs/repo/<owner>/<name>/<path>`. */
247 href: string;
248 /** The file in Code, on the default branch. */
249 code_href: string;
250 };
251};
252
253/** A page as the sidebar's tree lists it: flat, ordered by `position` within each parent. */
254export type DocTreeNode = {
255 id: string;
256 parent_id: string | null;
257 position: number;
258 title: string;
259 icon: string | null;
260 slug: string;
261 /** Possibly out of date (see `DocPage.stale`). */
262 stale?: boolean;
263};
264
265export type DocsSidebarSpace = DocSpace & { pages: DocTreeNode[] };
266
267export type DocsSidebar = {
268 spaces: DocsSidebarSpace[];
269 favorites: DocPageRef[];
270 /** The pages the viewer opened last, newest first. */
271 recent: DocPageRef[];
272 /** Whether the viewer may make spaces (members of the workspace may). */
273 can_create_space: boolean;
274 trash_count: number;
275 /** Pages the viewer can read that are possibly out of date. */
276 stale_count: number;
277 /** Projects' docs folders shown in Docs, those whose repository the viewer can read. */
278 repos: DocRepoSpace[];
279};
280
281export type DocsHome = {
282 /** Recently edited pages the viewer can read, newest first. */
283 recent: DocPage[];
284 /** Pages the viewer made or owns. */
285 mine: DocPage[];
286 /** Pages possibly out of date, most recently flagged first. */
287 stale: DocPage[];
288 spaces: DocSpace[];
289 /** Every project some space or page is linked to, for the filter. */
290 projects: string[];
291 /** The project the lists are filtered to, or null. */
292 project: string | null;
293};
294
295/** A tracked change an agent proposed. `blocks` targets name top-level block ids from `page_markdown`. */
296export type DocEditTarget =
297 /** Add to the end of the page. */
298 | { kind: "append" }
299 /** Replace the whole page. */
300 | { kind: "document" }
301 /** Replace a section: the heading whose text matches (case-insensitive) and everything under it, up to the next heading of the same or a higher level. The new Markdown should include the heading if it is to stay. */
302 | { kind: "section"; heading: string }
303 /** Replace top-level blocks `from_block` through `to_block`, inclusive (with any blocks nested under them). */
304 | { kind: "blocks"; from_block: string; to_block: string };
305
306export type DocSuggestionStatus = "open" | "accepted" | "rejected" | "stale";
307
308export type DocSuggestion = {
309 id: string;
310 page_id: string;
311 /** The agent that suggested it. */
312 author: MemberProfile;
313 /** The person it acted for. */
314 asked_by: MemberProfile | null;
315 target: DocEditTarget;
316 /** The target's Markdown when it was suggested. */
317 before_markdown: string;
318 /** What it proposes instead. */
319 after_markdown: string;
320 /** Why, in a line. */
321 note: string | null;
322 status: DocSuggestionStatus;
323 created_at: string;
324 decided_by: MemberProfile | null;
325 decided_at: string | null;
326 /** The top-level blocks it covers now, so the editor can mark them; empty for an append or when the target is gone. */
327 block_ids: string[];
328};
329
330export type DocVersionKind = "created" | "edit" | "agent" | "suggestion" | "restore";
331
332export type DocVersion = {
333 id: string;
334 page_id: string;
335 created_at: string;
336 /** Everyone whose changes are in it, people and agents. */
337 authors: MemberProfile[];
338 kind: DocVersionKind;
339 /** "Suggested by @inky, accepted by @ana"; "Restored from Oct 3, 14:02". */
340 note: string | null;
341};
342
343export type DocDiffLine = { op: "same" | "add" | "del"; text: string };
344
345export type DocVersionDetail = DocVersion & {
346 markdown: string;
347 /** Against the version before it (or nothing, for the first). */
348 diff: DocDiffLine[];
349};
350
351export type DocPageDetail = {
352 page: DocPage;
353 space: DocSpace;
354 /** The page's ancestors, root first. */
355 breadcrumbs: DocPageRef[];
356 /** As last saved: the read view, and what the editor shows before the socket connects. */
357 markdown: string;
358 role: DocRole;
359 backlinks: DocPageRef[];
360 children: DocPageRef[];
361 favorite: boolean;
362 /** When the viewer last opened it, before now. */
363 last_viewed_at: string | null;
364 suggestions: DocSuggestion[];
365 /** Code the page cites: from its text and its header. */
366 citations: DocCitation[];
367 /** The header's "Describes" list. */
368 describes: DocDescribes[];
369 /** Why it is possibly out of date; null when it isn't. */
370 staleness: DocStaleness | null;
371};
372
373export type DocSearchHit = DocPageRef & {
374 space_name: string;
375 /** Text around the match; `[[` and `]]` mark matched words. */
376 snippet: string;
377 updated_at: string;
378 projects: string[];
379 /** Set for a file from a project's docs folder (then `id` is `repo:<space>:<path>` and `path` its address in Docs). */
380 repo_file?: { repo: string; path: string } | null;
381 /** The heading of the passage that matched, when search found one (`mode: "hybrid"`). */
382 heading?: string | null;
383 /** How it was found: by its words, by meaning (the semantic index), or both. */
384 matched?: "words" | "meaning" | "both" | null;
385};
386
387export type DocTemplate = {
388 id: string;
389 name: string;
390 description: string;
391 icon: string;
392 /** Built in (meeting notes, RFC, ...) rather than saved by the workspace. */
393 builtin: boolean;
394 markdown: string;
395 created_by: Principal | null;
396};
397
398/** One top-level block, as agents see a page. */
399export type DocBlockOutline = {
400 id: string;
401 /** BlockNote's type: `heading`, `paragraph`, `bulletListItem`, `codeBlock`, `callout`, ... */
402 type: string;
403 /** For a heading. */
404 level: number | null;
405 /** The block and anything nested under it. */
406 markdown: string;
407};
408
409/**
410 * What an agent may do on a page, for the person it acts for: read it,
411 * suggest changes, edit its body (only in a space whose agents edit), and
412 * attach a file it made (whenever the person could edit: a file beside the
413 * page changes nothing in it).
414 */
415export type DocAgentAbilities = { read: boolean; suggest: boolean; edit: boolean; attach: boolean };
416
417export type NewDocSpace = {
418 name: string;
419 slug?: string | null;
420 description?: string | null;
421 icon?: string | null;
422 kind: DocSpaceKind;
423 team?: string | null;
424 default_role?: DocRole | null;
425 agent_mode?: DocAgentMode | null;
426 projects?: string[] | null;
427 editors_can_share?: boolean | null;
428};
429
430export type DocSpaceChange = Partial<Omit<NewDocSpace, "kind">> & { kind?: DocSpaceKind; archived?: boolean };
431
432export type NewDocPage = {
433 space_id: string;
434 parent_id?: string | null;
435 title?: string | null;
436 icon?: string | null;
437 /** A built-in template's id (`builtin:<name>`) or a saved one's. */
438 template_id?: string | null;
439 /** Starting Markdown, when there is no template. */
440 markdown?: string | null;
441 projects?: string[] | null;
442};
443
444export type DocPageChange = {
445 title?: string;
446 icon?: string | null;
447 cover?: string | null;
448 projects?: string[];
449 /** Member keys (`user:<id>`, `agent:<id>`). */
450 owners?: string[];
451 /** Replaces the header's "Describes" list (at most 20). */
452 describes?: DocDescribes[];
453};
454
455/** Where a page goes: under `parent_id` (null for the top of the space), before `before_id` (null for the end). */
456export type DocMove = { space_id?: string | null; parent_id: string | null; before_id?: string | null };
457
458export type DocSearchQuery = {
459 query: string;
460 space_id?: string | null;
461 /** `owner/name`: pages linked to it, or in a space linked to it. */
462 project?: string | null;
463 limit?: number | null;
464 /**
465 * `words` (the default): titles and text by their words, as you type.
466 * `hybrid`: words and meaning (the semantic index) together, each hit
467 * with the passage and heading that matched; the Docs search page.
468 */
469 mode?: "words" | "hybrid" | null;
470};
471
472/**
473 * The comment operations of the editor's thread store (BlockNote's
474 * `RESTYjsThreadStore`), applied by the page's room to the `threads` map in
475 * the page's Yjs document, so every open editor sees them at once.
476 */
477export type DocThreadAction =
478 | { op: "create"; body: unknown; metadata?: unknown; page_level?: boolean }
479 /** Anchors a thread to the text between two Yjs relative positions (JSON). */
480 | { op: "anchor"; thread_id: string; anchor: unknown; head: unknown }
481 | { op: "comment"; thread_id: string; body: unknown; metadata?: unknown }
482 | { op: "edit_comment"; thread_id: string; comment_id: string; body: unknown; metadata?: unknown }
483 | { op: "delete_comment"; thread_id: string; comment_id: string; soft?: boolean }
484 | { op: "delete_thread"; thread_id: string }
485 | { op: "resolve" | "unresolve"; thread_id: string }
486 | { op: "react" | "unreact"; thread_id: string; comment_id: string; emoji: string };
487
488/** A comment thread, as agents and the inbox see it. */
489export type DocThread = {
490 id: string;
491 /** The text it is on; null for a comment on the whole page. */
492 quote: string | null;
493 resolved: boolean;
494 comments: { id: string; author: MemberProfile; text: string; created_at: string }[];
495};
496
497/** A file put in a page: served from the usercontent origin. */
498export type DocFile = { id: string; url: string; name: string; content_type: string; bytes: number };
499
500/**
501 * What a page's live socket carries besides the Yjs protocol (binary
502 * frames: y-protocols sync and awareness). These are JSON text frames.
503 */
504export type DocsLiveEvent =
505 | { type: "page.updated"; page: DocPage }
506 | { type: "page.archived"; page_id: string }
507 | { type: "suggestion.created" | "suggestion.updated"; suggestion: DocSuggestion }
508 | { type: "version.created"; version: DocVersion }
509 /** Whether it is possibly out of date changed: ask for the page again (what each reader sees of why depends on what they can read). */
510 | { type: "page.staleness" }
511 /** The viewer's role changed, or their access ended (`role` null). */
512 | { type: "access"; role: DocRole | null };
513
514/**
515 * Header the site sets on a forwarded live socket and on uploads: the
516 * viewer, as JSON. Trusted only because the artifacts service is reachable
517 * through service bindings alone.
518 */
519export const DOCS_VIEWER_HEADER = "x-g1t-docs-viewer";
520
521/** The largest file a page takes, in bytes. */
522export const DOC_MAX_FILE_BYTES = 25 * 1024 * 1024;
523
524/** The name of the Yjs XML fragment that holds a page's blocks. */
525export const DOC_FRAGMENT = "document-store";
526/** The name of the Yjs map that holds a page's comment threads. */
527export const DOC_THREADS = "threads";
528
529export type DocsApi = {
530 // ── The site ─────────────────────────────────────────────────────────
531
532 /** Spaces with their page trees, favorites, recent pages. Makes the General space the first time. */
533 sidebar(workspace: string, viewer: User): Promise<Result<DocsSidebar>>;
534 home(workspace: string, viewer: User, options?: { project?: string | null }): Promise<Result<DocsHome>>;
535 space(workspace: string, spaceSlug: string, viewer: User): Promise<Result<{ space: DocSpace; members: DocSpaceMember[]; pages: DocPage[] }>>;
536 /** Every space the viewer can open, archived ones included (never deleted ones), General first. */
537 spaces(workspace: string, viewer: User): Promise<Result<DocSpace[]>>;
538 createSpace(workspace: string, viewer: User, input: NewDocSpace): Promise<Result<DocSpace>>;
539 /** Manage role only. `archived` true or false archives a space or brings it back, the General space too. */
540 updateSpace(workspace: string, spaceId: string, viewer: User, change: DocSpaceChange): Promise<Result<DocSpace>>;
541 /** Adds, changes (`role`) or removes (`role` null) a member. Manage role only; the last manager stays. */
542 setSpaceMember(workspace: string, spaceId: string, viewer: User, member: DocMemberKey, role: DocRole | null): Promise<Result<DocSpaceMember[]>>;
543
544 /** A page, and the viewer's role in it; records the view. Not found when they can't read it. */
545 page(workspace: string, pageId: string, viewer: User): Promise<Result<DocPageDetail>>;
546 createPage(workspace: string, viewer: User, input: NewDocPage): Promise<Result<DocPage>>;
547 updatePage(workspace: string, pageId: string, viewer: User, change: DocPageChange): Promise<Result<DocPage>>;
548 /** Edit role in both spaces. Refuses moving a page under itself. */
549 movePage(workspace: string, pageId: string, viewer: User, move: DocMove): Promise<Result<DocPage>>;
550 duplicatePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>;
551 /** To the trash, with every page under it. */
552 archivePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>;
553 restorePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>;
554 /** For good: only from the trash, manage role. */
555 deletePage(workspace: string, pageId: string, viewer: User): Promise<Result<boolean>>;
556 trash(workspace: string, viewer: User): Promise<Result<DocPage[]>>;
557 favorite(workspace: string, pageId: string, viewer: User, on: boolean): Promise<Result<boolean>>;
558
559 search(workspace: string, viewer: User, query: DocSearchQuery): Promise<Result<DocSearchHit[]>>;
560
561 versions(workspace: string, pageId: string, viewer: User): Promise<Result<DocVersion[]>>;
562 version(workspace: string, pageId: string, versionId: string, viewer: User): Promise<Result<DocVersionDetail>>;
563 /** Makes the page what it was then, as a new version. Edit role. */
564 restoreVersion(workspace: string, pageId: string, versionId: string, viewer: User): Promise<Result<DocVersion>>;
565
566 templates(workspace: string, viewer: User): Promise<Result<DocTemplate[]>>;
567 /** Saves a page as one of the workspace's templates. */
568 saveTemplate(workspace: string, viewer: User, input: { page_id: string; name: string; description?: string | null }): Promise<Result<DocTemplate>>;
569 deleteTemplate(workspace: string, templateId: string, viewer: User): Promise<Result<boolean>>;
570
571 exportPage(workspace: string, pageId: string, viewer: User): Promise<Result<{ filename: string; markdown: string }>>;
572 /** Every page in a space as Markdown, at paths that follow the tree. */
573 exportSpace(workspace: string, spaceId: string, viewer: User): Promise<Result<{ name: string; files: { path: string; markdown: string }[] }>>;
574
575 suggestions(workspace: string, pageId: string, viewer: User): Promise<Result<DocSuggestion[]>>;
576 /** Accept (applies it to the live document, attributed to both) or reject. Edit role. */
577 decideSuggestion(workspace: string, suggestionId: string, viewer: User, decision: "accept" | "reject"): Promise<Result<DocSuggestion>>;
578 acceptAll(workspace: string, pageId: string, viewer: User): Promise<Result<DocSuggestion[]>>;
579
580 /** A comment operation from the editor; comment role (deleting others' comments or threads needs edit). */
581 thread(workspace: string, pageId: string, viewer: User, action: DocThreadAction): Promise<Result<unknown>>;
582 threads(workspace: string, pageId: string, viewer: User): Promise<Result<DocThread[]>>;
583
584 /** Clears "possibly out of date": the page says what the code does now. Edit role. */
585 markCurrent(workspace: string, pageId: string, viewer: User): Promise<Result<boolean>>;
586 /** Pages the viewer can read that are possibly out of date, most recently flagged first; `repo` (`owner/name`) narrows to changes there. */
587 stalePages(workspace: string, viewer: User, options?: { repo?: string | null }): Promise<Result<DocPage[]>>;
588
589 /**
590 * Shows a repository's `docs/` folder and README.md in Docs, read from
591 * its default branch. Any member who can read the repository may; the
592 * files are read at once and again on every push to the default branch.
593 */
594 addRepoSpace(workspace: string, viewer: User, repo: string): Promise<Result<DocRepoSpace>>;
595 /** Stops showing it. Whoever added it, or a workspace owner. */
596 removeRepoSpace(workspace: string, viewer: User, id: string): Promise<Result<boolean>>;
597 /** One file of a project's docs, for a viewer who can read the repository; not found otherwise. */
598 repoPage(workspace: string, viewer: User, repo: string, path: string): Promise<Result<DocRepoPage>>;
599 /**
600 * Indexes the workspace's pages and projects' docs for agents' recall
601 * again, in batches in the background. Workspace owners. True when a run
602 * started (false: one is already going).
603 */
604 reindexDocs(workspace: string, viewer: User): Promise<Result<boolean>>;
605
606};
607
608async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
609 const response = await service.fetch(`https://service/rpc/${method}`, {
610 method: "POST",
611 headers: { "content-type": "application/json" },
612 body: JSON.stringify(args),
613 });
614 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
615 return (await response.json()) as T;
616}
617
618export function docsClient(service: ServiceBinding): DocsApi {
619 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
620 return {
621 sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }),
622 home: (workspace, viewer, options) => call("home", { workspace, viewer, project: options?.project ?? null }),
623 space: (workspace, spaceSlug, viewer) => call("space", { workspace, space: spaceSlug, viewer }),
624 spaces: (workspace, viewer) => call("spaces", { workspace, viewer }),
625 createSpace: (workspace, viewer, input) => call("create_space", { workspace, viewer, input }),
626 updateSpace: (workspace, spaceId, viewer, change) => call("update_space", { workspace, space_id: spaceId, viewer, change }),
627 setSpaceMember: (workspace, spaceId, viewer, member, role) => call("set_space_member", { workspace, space_id: spaceId, viewer, member, role }),
628 page: (workspace, pageId, viewer) => call("page", { workspace, page_id: pageId, viewer }),
629 createPage: (workspace, viewer, input) => call("create_page", { workspace, viewer, input }),
630 updatePage: (workspace, pageId, viewer, change) => call("update_page", { workspace, page_id: pageId, viewer, change }),
631 movePage: (workspace, pageId, viewer, move) => call("move_page", { workspace, page_id: pageId, viewer, move }),
632 duplicatePage: (workspace, pageId, viewer) => call("duplicate_page", { workspace, page_id: pageId, viewer }),
633 archivePage: (workspace, pageId, viewer) => call("archive_page", { workspace, page_id: pageId, viewer }),
634 restorePage: (workspace, pageId, viewer) => call("restore_page", { workspace, page_id: pageId, viewer }),
635 deletePage: (workspace, pageId, viewer) => call("delete_page", { workspace, page_id: pageId, viewer }),
636 trash: (workspace, viewer) => call("trash", { workspace, viewer }),
637 favorite: (workspace, pageId, viewer, on) => call("favorite", { workspace, page_id: pageId, viewer, on }),
638 search: (workspace, viewer, query) => call("search", { workspace, viewer, query }),
639 versions: (workspace, pageId, viewer) => call("versions", { workspace, page_id: pageId, viewer }),
640 version: (workspace, pageId, versionId, viewer) => call("version", { workspace, page_id: pageId, version_id: versionId, viewer }),
641 restoreVersion: (workspace, pageId, versionId, viewer) => call("restore_version", { workspace, page_id: pageId, version_id: versionId, viewer }),
642 templates: (workspace, viewer) => call("templates", { workspace, viewer }),
643 saveTemplate: (workspace, viewer, input) => call("save_template", { workspace, viewer, input }),
644 deleteTemplate: (workspace, templateId, viewer) => call("delete_template", { workspace, template_id: templateId, viewer }),
645 exportPage: (workspace, pageId, viewer) => call("export_page", { workspace, page_id: pageId, viewer }),
646 exportSpace: (workspace, spaceId, viewer) => call("export_space", { workspace, space_id: spaceId, viewer }),
647 suggestions: (workspace, pageId, viewer) => call("suggestions", { workspace, page_id: pageId, viewer }),
648 decideSuggestion: (workspace, suggestionId, viewer, decision) => call("decide_suggestion", { workspace, suggestion_id: suggestionId, viewer, decision }),
649 acceptAll: (workspace, pageId, viewer) => call("accept_all", { workspace, page_id: pageId, viewer }),
650 thread: (workspace, pageId, viewer, action) => call("thread", { workspace, page_id: pageId, viewer, action }),
651 threads: (workspace, pageId, viewer) => call("threads", { workspace, page_id: pageId, viewer }),
652 markCurrent: (workspace, pageId, viewer) => call("mark_current", { workspace, page_id: pageId, viewer }),
653 stalePages: (workspace, viewer, options) => call("stale_pages", { workspace, viewer, repo: options?.repo ?? null }),
654 addRepoSpace: (workspace, viewer, repo) => call("add_repo_space", { workspace, viewer, repo }),
655 removeRepoSpace: (workspace, viewer, id) => call("remove_repo_space", { workspace, viewer, id }),
656 repoPage: (workspace, viewer, repo, path) => call("repo_page", { workspace, viewer, repo, path }),
657 reindexDocs: (workspace, viewer) => call("reindex_docs", { workspace, viewer }),
658 };
659}