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