| 1 | /** |
| 2 | * Docs: the workspace's written knowledge, kept by the docs service |
| 3 | * (`services/docs`). 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. Plan and |
| 6 | * decisions: docs/WORKSPACE.md, "Docs". |
| 7 | * |
| 8 | * Wire shapes are snake_case end to end: the site, agents and the live |
| 9 | * socket all carry the same objects. |
| 10 | * |
| 11 | * Access, in one place: |
| 12 | * |
| 13 | * - A space has a kind: `workspace` (every member gets `default_role`), |
| 14 | * `team` (the team's members get `default_role`) or `private` (only the |
| 15 | * people, agents and teams listed as its members). |
| 16 | * - Members are listed with a role: view < comment < edit < manage. A |
| 17 | * person's role is the highest of the space's base role (when it applies |
| 18 | * to them) and every listing that names them or one of their teams. |
| 19 | * Workspace owners manage every workspace and team space; a private space |
| 20 | * is its members' alone. |
| 21 | * - An agent never sees or changes more than the person it acts for (the |
| 22 | * `viewer` on every agent call). It reads what that person can read, |
| 23 | * narrowed further to what every person in the `audience` can read; it |
| 24 | * suggests where that person can comment; and it edits directly only |
| 25 | * where that person can edit AND the space lets agents edit |
| 26 | * (`agent_mode: "edit"`). Otherwise its edit becomes a suggestion. |
| 27 | */ |
| 28 | import type { MemberProfile, Principal } from "./chat"; |
| 29 | import type { ServiceBinding } from "./clients"; |
| 30 | import type { User } from "./identity"; |
| 31 | import type { Result } from "./result"; |
| 32 | |
| 33 | /** What someone may do in a space, weakest first. */ |
| 34 | export type DocRole = "view" | "comment" | "edit" | "manage"; |
| 35 | export const DOC_ROLES: readonly DocRole[] = ["view", "comment", "edit", "manage"]; |
| 36 | |
| 37 | export 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 | |
| 44 | export 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. */ |
| 52 | export type DocSpaceKind = "workspace" | "team" | "private"; |
| 53 | |
| 54 | export 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. */ |
| 61 | export type DocAgentMode = "suggest" | "edit"; |
| 62 | |
| 63 | export 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>`. */ |
| 69 | export type DocMemberKey = string; |
| 70 | |
| 71 | export 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 | /** The workspace's General space, made the first time Docs is opened. Can't be archived. */ |
| 87 | is_default: boolean; |
| 88 | /** Projects (repositories, `owner/name`) the space is about: Docs filters by them. */ |
| 89 | projects: string[]; |
| 90 | created_by: Principal; |
| 91 | created_at: string; |
| 92 | archived_at: string | null; |
| 93 | /** The viewer's role in it. */ |
| 94 | viewer_role: DocRole; |
| 95 | /** Pages in it that are not in the trash. */ |
| 96 | page_count: number; |
| 97 | }; |
| 98 | |
| 99 | /** A space member, as the space's settings show it. */ |
| 100 | export type DocSpaceMember = { |
| 101 | key: DocMemberKey; |
| 102 | kind: "user" | "agent" | "team"; |
| 103 | /** Username, agent handle or team slug. */ |
| 104 | name: string; |
| 105 | display_name: string; |
| 106 | avatar: string | null; |
| 107 | avatar_seed?: string | null; |
| 108 | role: DocRole; |
| 109 | }; |
| 110 | |
| 111 | /** Enough to link to a page. */ |
| 112 | export type DocPageRef = { |
| 113 | id: string; |
| 114 | space_id: string; |
| 115 | space_slug: string; |
| 116 | title: string; |
| 117 | /** An emoji, or null for the default page icon. */ |
| 118 | icon: string | null; |
| 119 | /** `<title-slug>-<id>`: the last part of the page's URL. */ |
| 120 | slug: string; |
| 121 | /** `/<workspace>/-/docs/<space>/<slug>`. */ |
| 122 | path: string; |
| 123 | }; |
| 124 | |
| 125 | export type DocPage = DocPageRef & { |
| 126 | parent_id: string | null; |
| 127 | position: number; |
| 128 | /** A cover image's URL, or a CSS gradient name (`gradient:<n>`); null for none. */ |
| 129 | cover: string | null; |
| 130 | created_by: MemberProfile; |
| 131 | created_at: string; |
| 132 | updated_by: MemberProfile | null; |
| 133 | updated_at: string; |
| 134 | /** When it went to the trash; null when it is not there. */ |
| 135 | archived_at: string | null; |
| 136 | has_children: boolean; |
| 137 | /** Projects (`owner/name`) the page is about, besides its space's. */ |
| 138 | projects: string[]; |
| 139 | owners: MemberProfile[]; |
| 140 | /** The first lines of its text, for cards. */ |
| 141 | excerpt: string; |
| 142 | }; |
| 143 | |
| 144 | /** A page as the sidebar's tree lists it: flat, ordered by `position` within each parent. */ |
| 145 | export type DocTreeNode = { |
| 146 | id: string; |
| 147 | parent_id: string | null; |
| 148 | position: number; |
| 149 | title: string; |
| 150 | icon: string | null; |
| 151 | slug: string; |
| 152 | }; |
| 153 | |
| 154 | export type DocsSidebarSpace = DocSpace & { pages: DocTreeNode[] }; |
| 155 | |
| 156 | export type DocsSidebar = { |
| 157 | spaces: DocsSidebarSpace[]; |
| 158 | favorites: DocPageRef[]; |
| 159 | /** The pages the viewer opened last, newest first. */ |
| 160 | recent: DocPageRef[]; |
| 161 | /** Whether the viewer may make spaces (members of the workspace may). */ |
| 162 | can_create_space: boolean; |
| 163 | trash_count: number; |
| 164 | }; |
| 165 | |
| 166 | export type DocsHome = { |
| 167 | /** Recently edited pages the viewer can read, newest first. */ |
| 168 | recent: DocPage[]; |
| 169 | /** Pages the viewer made or owns. */ |
| 170 | mine: DocPage[]; |
| 171 | spaces: DocSpace[]; |
| 172 | /** Every project some space or page is linked to, for the filter. */ |
| 173 | projects: string[]; |
| 174 | /** The project the lists are filtered to, or null. */ |
| 175 | project: string | null; |
| 176 | }; |
| 177 | |
| 178 | /** A tracked change an agent proposed. `blocks` targets name top-level block ids from `page_markdown`. */ |
| 179 | export type DocEditTarget = |
| 180 | /** Add to the end of the page. */ |
| 181 | | { kind: "append" } |
| 182 | /** Replace the whole page. */ |
| 183 | | { kind: "document" } |
| 184 | /** 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. */ |
| 185 | | { kind: "section"; heading: string } |
| 186 | /** Replace top-level blocks `from_block` through `to_block`, inclusive (with any blocks nested under them). */ |
| 187 | | { kind: "blocks"; from_block: string; to_block: string }; |
| 188 | |
| 189 | export type DocSuggestionStatus = "open" | "accepted" | "rejected" | "stale"; |
| 190 | |
| 191 | export type DocSuggestion = { |
| 192 | id: string; |
| 193 | page_id: string; |
| 194 | /** The agent that suggested it. */ |
| 195 | author: MemberProfile; |
| 196 | /** The person it acted for. */ |
| 197 | asked_by: MemberProfile | null; |
| 198 | target: DocEditTarget; |
| 199 | /** The target's Markdown when it was suggested. */ |
| 200 | before_markdown: string; |
| 201 | /** What it proposes instead. */ |
| 202 | after_markdown: string; |
| 203 | /** Why, in a line. */ |
| 204 | note: string | null; |
| 205 | status: DocSuggestionStatus; |
| 206 | created_at: string; |
| 207 | decided_by: MemberProfile | null; |
| 208 | decided_at: string | null; |
| 209 | /** The top-level blocks it covers now, so the editor can mark them; empty for an append or when the target is gone. */ |
| 210 | block_ids: string[]; |
| 211 | }; |
| 212 | |
| 213 | export type DocVersionKind = "created" | "edit" | "agent" | "suggestion" | "restore"; |
| 214 | |
| 215 | export type DocVersion = { |
| 216 | id: string; |
| 217 | page_id: string; |
| 218 | created_at: string; |
| 219 | /** Everyone whose changes are in it, people and agents. */ |
| 220 | authors: MemberProfile[]; |
| 221 | kind: DocVersionKind; |
| 222 | /** "Suggested by @inky, accepted by @ana"; "Restored from Oct 3, 14:02". */ |
| 223 | note: string | null; |
| 224 | }; |
| 225 | |
| 226 | export type DocDiffLine = { op: "same" | "add" | "del"; text: string }; |
| 227 | |
| 228 | export type DocVersionDetail = DocVersion & { |
| 229 | markdown: string; |
| 230 | /** Against the version before it (or nothing, for the first). */ |
| 231 | diff: DocDiffLine[]; |
| 232 | }; |
| 233 | |
| 234 | export type DocPageDetail = { |
| 235 | page: DocPage; |
| 236 | space: DocSpace; |
| 237 | /** The page's ancestors, root first. */ |
| 238 | breadcrumbs: DocPageRef[]; |
| 239 | /** As last saved: the read view, and what the editor shows before the socket connects. */ |
| 240 | markdown: string; |
| 241 | role: DocRole; |
| 242 | backlinks: DocPageRef[]; |
| 243 | children: DocPageRef[]; |
| 244 | favorite: boolean; |
| 245 | /** When the viewer last opened it, before now. */ |
| 246 | last_viewed_at: string | null; |
| 247 | suggestions: DocSuggestion[]; |
| 248 | }; |
| 249 | |
| 250 | export type DocSearchHit = DocPageRef & { |
| 251 | space_name: string; |
| 252 | /** Text around the match; `[[` and `]]` mark matched words. */ |
| 253 | snippet: string; |
| 254 | updated_at: string; |
| 255 | projects: string[]; |
| 256 | }; |
| 257 | |
| 258 | export type DocTemplate = { |
| 259 | id: string; |
| 260 | name: string; |
| 261 | description: string; |
| 262 | icon: string; |
| 263 | /** Built in (meeting notes, RFC, ...) rather than saved by the workspace. */ |
| 264 | builtin: boolean; |
| 265 | markdown: string; |
| 266 | created_by: Principal | null; |
| 267 | }; |
| 268 | |
| 269 | /** One top-level block, as agents see a page. */ |
| 270 | export type DocBlockOutline = { |
| 271 | id: string; |
| 272 | /** BlockNote's type: `heading`, `paragraph`, `bulletListItem`, `codeBlock`, `callout`, ... */ |
| 273 | type: string; |
| 274 | /** For a heading. */ |
| 275 | level: number | null; |
| 276 | /** The block and anything nested under it. */ |
| 277 | markdown: string; |
| 278 | }; |
| 279 | |
| 280 | /** What an agent may do on a page, for the person it acts for. */ |
| 281 | export type DocAgentAbilities = { read: boolean; suggest: boolean; edit: boolean }; |
| 282 | |
| 283 | export type DocAgentPage = { |
| 284 | page: DocPageRef & { updated_at: string }; |
| 285 | space: { id: string; slug: string; name: string; agent_mode: DocAgentMode }; |
| 286 | markdown: string; |
| 287 | blocks: DocBlockOutline[]; |
| 288 | can: DocAgentAbilities; |
| 289 | }; |
| 290 | |
| 291 | /** Who will see what an agent says; it reads only what they all can. */ |
| 292 | export type DocAudience = |
| 293 | /** These people (by user id), e.g. a DM's or a private channel's members. */ |
| 294 | | { kind: "people"; user_ids: string[] } |
| 295 | /** Everyone in the workspace, e.g. a public channel: only workspace-wide spaces. */ |
| 296 | | { kind: "workspace" }; |
| 297 | |
| 298 | /** What `apply_edit` did: applied it, or filed a suggestion because it may not edit there. */ |
| 299 | export type DocAgentEditResult = |
| 300 | | { mode: "applied"; version_id: string | null; page: DocPageRef } |
| 301 | | { mode: "suggested"; suggestion: DocSuggestion; page: DocPageRef }; |
| 302 | |
| 303 | export type NewDocSpace = { |
| 304 | name: string; |
| 305 | slug?: string | null; |
| 306 | description?: string | null; |
| 307 | icon?: string | null; |
| 308 | kind: DocSpaceKind; |
| 309 | team?: string | null; |
| 310 | default_role?: DocRole | null; |
| 311 | agent_mode?: DocAgentMode | null; |
| 312 | projects?: string[] | null; |
| 313 | }; |
| 314 | |
| 315 | export type DocSpaceChange = Partial<Omit<NewDocSpace, "kind">> & { kind?: DocSpaceKind; archived?: boolean }; |
| 316 | |
| 317 | export type NewDocPage = { |
| 318 | space_id: string; |
| 319 | parent_id?: string | null; |
| 320 | title?: string | null; |
| 321 | icon?: string | null; |
| 322 | /** A built-in template's id (`builtin:<name>`) or a saved one's. */ |
| 323 | template_id?: string | null; |
| 324 | /** Starting Markdown, when there is no template. */ |
| 325 | markdown?: string | null; |
| 326 | projects?: string[] | null; |
| 327 | }; |
| 328 | |
| 329 | export type DocPageChange = { |
| 330 | title?: string; |
| 331 | icon?: string | null; |
| 332 | cover?: string | null; |
| 333 | projects?: string[]; |
| 334 | /** Member keys (`user:<id>`, `agent:<id>`). */ |
| 335 | owners?: string[]; |
| 336 | }; |
| 337 | |
| 338 | /** Where a page goes: under `parent_id` (null for the top of the space), before `before_id` (null for the end). */ |
| 339 | export type DocMove = { space_id?: string | null; parent_id: string | null; before_id?: string | null }; |
| 340 | |
| 341 | export type DocSearchQuery = { |
| 342 | query: string; |
| 343 | space_id?: string | null; |
| 344 | /** `owner/name`: pages linked to it, or in a space linked to it. */ |
| 345 | project?: string | null; |
| 346 | limit?: number | null; |
| 347 | }; |
| 348 | |
| 349 | /** |
| 350 | * The comment operations of the editor's thread store (BlockNote's |
| 351 | * `RESTYjsThreadStore`), applied by the page's room to the `threads` map in |
| 352 | * the page's Yjs document, so every open editor sees them at once. |
| 353 | */ |
| 354 | export type DocThreadAction = |
| 355 | | { op: "create"; body: unknown; metadata?: unknown; page_level?: boolean } |
| 356 | /** Anchors a thread to the text between two Yjs relative positions (JSON). */ |
| 357 | | { op: "anchor"; thread_id: string; anchor: unknown; head: unknown } |
| 358 | | { op: "comment"; thread_id: string; body: unknown; metadata?: unknown } |
| 359 | | { op: "edit_comment"; thread_id: string; comment_id: string; body: unknown; metadata?: unknown } |
| 360 | | { op: "delete_comment"; thread_id: string; comment_id: string; soft?: boolean } |
| 361 | | { op: "delete_thread"; thread_id: string } |
| 362 | | { op: "resolve" | "unresolve"; thread_id: string } |
| 363 | | { op: "react" | "unreact"; thread_id: string; comment_id: string; emoji: string }; |
| 364 | |
| 365 | /** A comment thread, as agents and the inbox see it. */ |
| 366 | export type DocThread = { |
| 367 | id: string; |
| 368 | /** The text it is on; null for a comment on the whole page. */ |
| 369 | quote: string | null; |
| 370 | resolved: boolean; |
| 371 | comments: { id: string; author: MemberProfile; text: string; created_at: string }[]; |
| 372 | }; |
| 373 | |
| 374 | /** A file put in a page: served from the usercontent origin. */ |
| 375 | export type DocFile = { id: string; url: string; name: string; content_type: string; bytes: number }; |
| 376 | |
| 377 | /** |
| 378 | * What a page's live socket carries besides the Yjs protocol (binary |
| 379 | * frames: y-protocols sync and awareness). These are JSON text frames. |
| 380 | */ |
| 381 | export type DocsLiveEvent = |
| 382 | | { type: "page.updated"; page: DocPage } |
| 383 | | { type: "page.archived"; page_id: string } |
| 384 | | { type: "suggestion.created" | "suggestion.updated"; suggestion: DocSuggestion } |
| 385 | | { type: "version.created"; version: DocVersion } |
| 386 | /** The viewer's role changed, or their access ended (`role` null). */ |
| 387 | | { type: "access"; role: DocRole | null }; |
| 388 | |
| 389 | /** |
| 390 | * Header the site sets on a forwarded live socket and on uploads: the |
| 391 | * viewer, as JSON. Trusted only because the docs service is reachable |
| 392 | * through service bindings alone. |
| 393 | */ |
| 394 | export const DOCS_VIEWER_HEADER = "x-g1t-docs-viewer"; |
| 395 | |
| 396 | /** The largest file a page takes, in bytes. */ |
| 397 | export const DOC_MAX_FILE_BYTES = 25 * 1024 * 1024; |
| 398 | |
| 399 | /** The name of the Yjs XML fragment that holds a page's blocks. */ |
| 400 | export const DOC_FRAGMENT = "document-store"; |
| 401 | /** The name of the Yjs map that holds a page's comment threads. */ |
| 402 | export const DOC_THREADS = "threads"; |
| 403 | |
| 404 | export type DocsApi = { |
| 405 | // ── The site ───────────────────────────────────────────────────────── |
| 406 | |
| 407 | /** Spaces with their page trees, favorites, recent pages. Makes the General space the first time. */ |
| 408 | sidebar(workspace: string, viewer: User): Promise<Result<DocsSidebar>>; |
| 409 | home(workspace: string, viewer: User, options?: { project?: string | null }): Promise<Result<DocsHome>>; |
| 410 | space(workspace: string, spaceSlug: string, viewer: User): Promise<Result<{ space: DocSpace; members: DocSpaceMember[]; pages: DocPage[] }>>; |
| 411 | createSpace(workspace: string, viewer: User, input: NewDocSpace): Promise<Result<DocSpace>>; |
| 412 | /** Manage role only. */ |
| 413 | updateSpace(workspace: string, spaceId: string, viewer: User, change: DocSpaceChange): Promise<Result<DocSpace>>; |
| 414 | /** Adds, changes (`role`) or removes (`role` null) a member. Manage role only; the last manager stays. */ |
| 415 | setSpaceMember(workspace: string, spaceId: string, viewer: User, member: DocMemberKey, role: DocRole | null): Promise<Result<DocSpaceMember[]>>; |
| 416 | |
| 417 | /** A page, and the viewer's role in it; records the view. Not found when they can't read it. */ |
| 418 | page(workspace: string, pageId: string, viewer: User): Promise<Result<DocPageDetail>>; |
| 419 | createPage(workspace: string, viewer: User, input: NewDocPage): Promise<Result<DocPage>>; |
| 420 | updatePage(workspace: string, pageId: string, viewer: User, change: DocPageChange): Promise<Result<DocPage>>; |
| 421 | /** Edit role in both spaces. Refuses moving a page under itself. */ |
| 422 | movePage(workspace: string, pageId: string, viewer: User, move: DocMove): Promise<Result<DocPage>>; |
| 423 | duplicatePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>; |
| 424 | /** To the trash, with every page under it. */ |
| 425 | archivePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>; |
| 426 | restorePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>; |
| 427 | /** For good: only from the trash, manage role. */ |
| 428 | deletePage(workspace: string, pageId: string, viewer: User): Promise<Result<boolean>>; |
| 429 | trash(workspace: string, viewer: User): Promise<Result<DocPage[]>>; |
| 430 | favorite(workspace: string, pageId: string, viewer: User, on: boolean): Promise<Result<boolean>>; |
| 431 | |
| 432 | search(workspace: string, viewer: User, query: DocSearchQuery): Promise<Result<DocSearchHit[]>>; |
| 433 | |
| 434 | versions(workspace: string, pageId: string, viewer: User): Promise<Result<DocVersion[]>>; |
| 435 | version(workspace: string, pageId: string, versionId: string, viewer: User): Promise<Result<DocVersionDetail>>; |
| 436 | /** Makes the page what it was then, as a new version. Edit role. */ |
| 437 | restoreVersion(workspace: string, pageId: string, versionId: string, viewer: User): Promise<Result<DocVersion>>; |
| 438 | |
| 439 | templates(workspace: string, viewer: User): Promise<Result<DocTemplate[]>>; |
| 440 | /** Saves a page as one of the workspace's templates. */ |
| 441 | saveTemplate(workspace: string, viewer: User, input: { page_id: string; name: string; description?: string | null }): Promise<Result<DocTemplate>>; |
| 442 | deleteTemplate(workspace: string, templateId: string, viewer: User): Promise<Result<boolean>>; |
| 443 | |
| 444 | exportPage(workspace: string, pageId: string, viewer: User): Promise<Result<{ filename: string; markdown: string }>>; |
| 445 | /** Every page in a space as Markdown, at paths that follow the tree. */ |
| 446 | exportSpace(workspace: string, spaceId: string, viewer: User): Promise<Result<{ name: string; files: { path: string; markdown: string }[] }>>; |
| 447 | |
| 448 | suggestions(workspace: string, pageId: string, viewer: User): Promise<Result<DocSuggestion[]>>; |
| 449 | /** Accept (applies it to the live document, attributed to both) or reject. Edit role. */ |
| 450 | decideSuggestion(workspace: string, suggestionId: string, viewer: User, decision: "accept" | "reject"): Promise<Result<DocSuggestion>>; |
| 451 | acceptAll(workspace: string, pageId: string, viewer: User): Promise<Result<DocSuggestion[]>>; |
| 452 | |
| 453 | /** A comment operation from the editor; comment role (deleting others' comments or threads needs edit). */ |
| 454 | thread(workspace: string, pageId: string, viewer: User, action: DocThreadAction): Promise<Result<unknown>>; |
| 455 | threads(workspace: string, pageId: string, viewer: User): Promise<Result<DocThread[]>>; |
| 456 | |
| 457 | // ── Agents (services/agents) ───────────────────────────────────────── |
| 458 | // |
| 459 | // Each takes the agent and the person it acts for (`viewer`, the asker). |
| 460 | // The service checks the agent is a live agent of the workspace and caps |
| 461 | // everything by the viewer's access; `audience`, when given, narrows |
| 462 | // reads further to what every person in it can read. A page the agent |
| 463 | // may not read is not found, exactly as one that does not exist. |
| 464 | |
| 465 | /** Spaces the agent may read for the viewer (and audience), with what it may do in each. */ |
| 466 | spacesForAgent(workspace: string, agentId: string, viewer: User, audience?: DocAudience | null): Promise<Result<(Pick<DocSpace, "id" | "slug" | "name" | "description" | "kind" | "agent_mode" | "projects"> & { can: DocAgentAbilities })[]>>; |
| 467 | /** A page's Markdown and its top-level blocks (ids for `blocks` targets). */ |
| 468 | pageMarkdown(workspace: string, agentId: string, viewer: User, pageId: string, audience?: DocAudience | null): Promise<Result<DocAgentPage>>; |
| 469 | /** Full-text search over pages every reader can read; at most 20, best first. */ |
| 470 | searchForAgent(workspace: string, agentId: string, viewer: User, query: DocSearchQuery, audience?: DocAudience | null): Promise<Result<DocSearchHit[]>>; |
| 471 | /** Files a tracked suggestion; people with edit access accept or reject it inline. Needs the viewer's comment role. Notifies the page's owners. */ |
| 472 | suggestEdit(workspace: string, agentId: string, viewer: User, pageId: string, edit: { target: DocEditTarget; markdown: string; note?: string | null }): Promise<Result<DocSuggestion>>; |
| 473 | /** |
| 474 | * Applies an edit to the live document when the space lets agents edit |
| 475 | * and the viewer can edit; otherwise files it as a suggestion (and says |
| 476 | * so in `mode`). Attributed to the agent in the page's history. |
| 477 | */ |
| 478 | applyEdit(workspace: string, agentId: string, viewer: User, pageId: string, edit: { target: DocEditTarget; markdown: string; note?: string | null }): Promise<Result<DocAgentEditResult>>; |
| 479 | /** |
| 480 | * A new page, written by the agent: in `space_id` (the General space |
| 481 | * when null), under `parent_id`. Needs the viewer's edit role there |
| 482 | * (whatever the space's agent mode: a new page changes nothing anyone |
| 483 | * wrote). `source` links where it came from ("write this up"). |
| 484 | */ |
| 485 | createPageAsAgent( |
| 486 | workspace: string, |
| 487 | agentId: string, |
| 488 | viewer: User, |
| 489 | input: { space_id?: string | null; parent_id?: string | null; title: string; icon?: string | null; markdown: string; source?: { title: string; href: string } | null }, |
| 490 | ): Promise<Result<DocPageRef>>; |
| 491 | /** A page's comment threads, for an agent asked about them. */ |
| 492 | threadsForAgent(workspace: string, agentId: string, viewer: User, pageId: string, audience?: DocAudience | null): Promise<Result<DocThread[]>>; |
| 493 | }; |
| 494 | |
| 495 | async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> { |
| 496 | const response = await service.fetch(`https://service/rpc/${method}`, { |
| 497 | method: "POST", |
| 498 | headers: { "content-type": "application/json" }, |
| 499 | body: JSON.stringify(args), |
| 500 | }); |
| 501 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); |
| 502 | return (await response.json()) as T; |
| 503 | } |
| 504 | |
| 505 | export function docsClient(service: ServiceBinding): DocsApi { |
| 506 | const call = <T>(method: string, args: object) => rpc<T>(service, method, args); |
| 507 | return { |
| 508 | sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }), |
| 509 | home: (workspace, viewer, options) => call("home", { workspace, viewer, project: options?.project ?? null }), |
| 510 | space: (workspace, spaceSlug, viewer) => call("space", { workspace, space: spaceSlug, viewer }), |
| 511 | createSpace: (workspace, viewer, input) => call("create_space", { workspace, viewer, input }), |
| 512 | updateSpace: (workspace, spaceId, viewer, change) => call("update_space", { workspace, space_id: spaceId, viewer, change }), |
| 513 | setSpaceMember: (workspace, spaceId, viewer, member, role) => call("set_space_member", { workspace, space_id: spaceId, viewer, member, role }), |
| 514 | page: (workspace, pageId, viewer) => call("page", { workspace, page_id: pageId, viewer }), |
| 515 | createPage: (workspace, viewer, input) => call("create_page", { workspace, viewer, input }), |
| 516 | updatePage: (workspace, pageId, viewer, change) => call("update_page", { workspace, page_id: pageId, viewer, change }), |
| 517 | movePage: (workspace, pageId, viewer, move) => call("move_page", { workspace, page_id: pageId, viewer, move }), |
| 518 | duplicatePage: (workspace, pageId, viewer) => call("duplicate_page", { workspace, page_id: pageId, viewer }), |
| 519 | archivePage: (workspace, pageId, viewer) => call("archive_page", { workspace, page_id: pageId, viewer }), |
| 520 | restorePage: (workspace, pageId, viewer) => call("restore_page", { workspace, page_id: pageId, viewer }), |
| 521 | deletePage: (workspace, pageId, viewer) => call("delete_page", { workspace, page_id: pageId, viewer }), |
| 522 | trash: (workspace, viewer) => call("trash", { workspace, viewer }), |
| 523 | favorite: (workspace, pageId, viewer, on) => call("favorite", { workspace, page_id: pageId, viewer, on }), |
| 524 | search: (workspace, viewer, query) => call("search", { workspace, viewer, query }), |
| 525 | versions: (workspace, pageId, viewer) => call("versions", { workspace, page_id: pageId, viewer }), |
| 526 | version: (workspace, pageId, versionId, viewer) => call("version", { workspace, page_id: pageId, version_id: versionId, viewer }), |
| 527 | restoreVersion: (workspace, pageId, versionId, viewer) => call("restore_version", { workspace, page_id: pageId, version_id: versionId, viewer }), |
| 528 | templates: (workspace, viewer) => call("templates", { workspace, viewer }), |
| 529 | saveTemplate: (workspace, viewer, input) => call("save_template", { workspace, viewer, input }), |
| 530 | deleteTemplate: (workspace, templateId, viewer) => call("delete_template", { workspace, template_id: templateId, viewer }), |
| 531 | exportPage: (workspace, pageId, viewer) => call("export_page", { workspace, page_id: pageId, viewer }), |
| 532 | exportSpace: (workspace, spaceId, viewer) => call("export_space", { workspace, space_id: spaceId, viewer }), |
| 533 | suggestions: (workspace, pageId, viewer) => call("suggestions", { workspace, page_id: pageId, viewer }), |
| 534 | decideSuggestion: (workspace, suggestionId, viewer, decision) => call("decide_suggestion", { workspace, suggestion_id: suggestionId, viewer, decision }), |
| 535 | acceptAll: (workspace, pageId, viewer) => call("accept_all", { workspace, page_id: pageId, viewer }), |
| 536 | thread: (workspace, pageId, viewer, action) => call("thread", { workspace, page_id: pageId, viewer, action }), |
| 537 | threads: (workspace, pageId, viewer) => call("threads", { workspace, page_id: pageId, viewer }), |
| 538 | spacesForAgent: (workspace, agentId, viewer, audience) => call("spaces_for_agent", { workspace, agent_id: agentId, viewer, audience: audience ?? null }), |
| 539 | pageMarkdown: (workspace, agentId, viewer, pageId, audience) => |
| 540 | call("page_markdown", { workspace, agent_id: agentId, viewer, page_id: pageId, audience: audience ?? null }), |
| 541 | searchForAgent: (workspace, agentId, viewer, query, audience) => call("search_for_agent", { workspace, agent_id: agentId, viewer, query, audience: audience ?? null }), |
| 542 | suggestEdit: (workspace, agentId, viewer, pageId, edit) => call("suggest_edit", { workspace, agent_id: agentId, viewer, page_id: pageId, edit }), |
| 543 | applyEdit: (workspace, agentId, viewer, pageId, edit) => call("apply_edit", { workspace, agent_id: agentId, viewer, page_id: pageId, edit }), |
| 544 | createPageAsAgent: (workspace, agentId, viewer, input) => call("create_page_as_agent", { workspace, agent_id: agentId, viewer, input }), |
| 545 | threadsForAgent: (workspace, agentId, viewer, pageId, audience) => |
| 546 | call("threads_for_agent", { workspace, agent_id: agentId, viewer, page_id: pageId, audience: audience ?? null }), |
| 547 | }; |
| 548 | } |