Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet | 1 | /** |
| 2 | * Folios: what people call artifacts. Artifacts mode is one mode for docs, | |
| 3 | * slides, designs and dashboards, each private, shared with people and | |
| 4 | * agents, in a space or open to the workspace, and edited live together. | |
| 5 | * Kept by the docs service (`services/docs`). Plan and decisions: | |
| 6 | * docs/ARTIFACTS_MODE.md. | |
| 7 | * | |
| 8 | * Naming: code says "folio", people see "artifact" (UI text, URLs | |
| 9 | * `/<ws>/-/artifacts/...`, the `artifact` MCP tool, REST paths, the | |
| 10 | * `artifacts:*` scopes). The Cloudflare Artifacts git store and workflow | |
| 11 | * run artifacts are something else and keep their names. | |
| 12 | * | |
| 13 | * Wire shapes are snake_case end to end. Each kind's own model is in its | |
| 14 | * own file: folios-slides.ts, folios-design.ts, folios-dashboard.ts. | |
| 15 | * `crates/contracts/src/folios.rs` mirrors what the Rust API needs, and a | |
| 16 | * Rust test runs both validators over `folios.fixtures.json`. | |
| 17 | * | |
| 18 | * Access, in one place (section 2.1 of the plan): | |
| 19 | * | |
| 20 | * - Every folio has one owner, always a person, who has `manage`. | |
| 21 | * - It sits in a space, or in its owner's Private section (`space` null). | |
| 22 | * - A person's role is the highest of: owner; explicit grants on it or an | |
| 23 | * ancestor it inherits from; their space role when it inherits up to a | |
| 24 | * folio in a space; and the general access of its access root | |
| 25 | * (`workspace`: every member; `link`: members who opened the link). | |
| 26 | * - `inherit: false` makes a folio its own access root ("Only people | |
| 27 | * invited"). "Private" (the lock) is computed: only the owner can read it. | |
| 28 | * - An agent's role is never higher than its asker's, narrowed to what | |
| 29 | * every person in the audience can read. | |
| 30 | */ | |
| 31 | import type { MemberProfile } from "./chat"; | |
| 32 | import type { ServiceBinding } from "./clients"; | |
| 33 | import type { DatasetQuery, DatasetResult } from "./datasets"; | |
| 34 | import type { | |
| 35 | DocAgentAbilities, | |
| 36 | DocAgentMode, | |
| 37 | DocAudience, | |
| 38 | DocBlockOutline, | |
| 39 | DocDiffLine, | |
| 40 | DocEditTarget, | |
| 41 | DocRepoSpace, | |
| 42 | DocRole, | |
| 43 | DocSpace, | |
| 44 | DocSpaceKind, | |
| 45 | DocSuggestion, | |
| 46 | DocThread, | |
| 47 | DocThreadAction, | |
| 48 | } from "./docs"; | |
| 49 | import type { DashboardOp, DashboardPreview } from "./folios-dashboard"; | |
| 50 | import type { DesignOp, DesignPreview } from "./folios-design"; | |
| 51 | import type { SlidesOp, SlidesPreview } from "./folios-slides"; | |
| 52 | import type { User } from "./identity"; | |
| 53 | import type { Result } from "./result"; | |
| 54 | ||
| 55 | // ── Kinds ───────────────────────────────────────────────────────────── | |
| 56 | ||
| 57 | export type FolioKind = "doc" | "slides" | "design" | "dashboard"; | |
| 58 | export const FOLIO_KINDS: readonly FolioKind[] = ["doc", "slides", "design", "dashboard"]; | |
| 59 | ||
| 60 | /** The kinds as the Artifacts home's tiles name them. */ | |
| 61 | export const FOLIO_KIND_LABELS: Record<FolioKind, string> = { | |
| 62 | doc: "Docs", | |
| 63 | slides: "Slides", | |
| 64 | design: "Design", | |
| 65 | dashboard: "Dashboard", | |
| 66 | }; | |
| 67 | ||
| 68 | /** One of a kind, in a sentence: "a doc", "a deck"... */ | |
| 69 | export const FOLIO_KIND_NOUNS: Record<FolioKind, string> = { | |
| 70 | doc: "doc", | |
| 71 | slides: "deck", | |
| 72 | design: "design", | |
| 73 | dashboard: "dashboard", | |
| 74 | }; | |
| 75 | ||
| 76 | export function isFolioKind(value: unknown): value is FolioKind { | |
| 77 | return typeof value === "string" && (FOLIO_KINDS as readonly string[]).includes(value); | |
| 78 | } | |
| 79 | ||
| 80 | /** Only a doc holds other folios: a doc with children is the folder. */ | |
| 81 | export function folioCanHaveChildren(kind: FolioKind): boolean { | |
| 82 | return kind === "doc"; | |
| 83 | } | |
| 84 | ||
| 85 | // ── Roles and access ────────────────────────────────────────────────── | |
| 86 | ||
| 87 | /** What someone may do with a folio, weakest first. The same roles as a space's. */ | |
| 88 | export type FolioRole = DocRole; | |
| 89 | export const FOLIO_ROLES: readonly FolioRole[] = ["view", "comment", "edit", "manage"]; | |
| 90 | ||
| 91 | /** Who may open a folio besides its owner, grants and space. */ | |
| 92 | export type FolioGeneralAccess = "none" | "workspace" | "link"; | |
| 93 | export const FOLIO_GENERAL_ACCESS: readonly FolioGeneralAccess[] = ["none", "workspace", "link"]; | |
| 94 | ||
| 95 | export const FOLIO_GENERAL_ACCESS_LABELS: Record<FolioGeneralAccess, string> = { | |
| 96 | none: "Restricted", | |
| 97 | workspace: "Everyone in the workspace", | |
| 98 | link: "Anyone in the workspace with the link", | |
| 99 | }; | |
| 100 | ||
| 101 | /** General access never gives `manage`. */ | |
| 102 | export type FolioGeneralRole = Exclude<FolioRole, "manage">; | |
| 103 | export const FOLIO_GENERAL_ROLES: readonly FolioGeneralRole[] = ["view", "comment", "edit"]; | |
| 104 | ||
| 105 | /** Space kinds as Artifacts names them (the database keeps workspace / team / private). */ | |
| 106 | export const FOLIO_SPACE_KIND_LABELS: Record<DocSpaceKind, string> = { | |
| 107 | workspace: "Open", | |
| 108 | team: "Team", | |
| 109 | private: "Members only", | |
| 110 | }; | |
| 111 | ||
| 112 | /** Who a grant names: `user:<id>`, `agent:<id>` or `team:<slug>`. */ | |
| 113 | export type FolioPrincipal = string; | |
| 114 | ||
| 115 | const PRINCIPAL = /^(user|agent|team):[A-Za-z0-9_.-]{1,64}$/; | |
| 116 | ||
| 117 | export function isFolioPrincipal(value: unknown): value is FolioPrincipal { | |
| 118 | return typeof value === "string" && PRINCIPAL.test(value); | |
| 119 | } | |
| 120 | ||
| 121 | /** The deepest a folio tree goes. */ | |
| 122 | export const FOLIO_MAX_DEPTH = 10; | |
| 123 | /** The longest title, in characters. */ | |
| 124 | export const FOLIO_MAX_TITLE = 200; | |
| 125 | /** The most people a folio is shared with in one change. */ | |
| 126 | export const FOLIO_MAX_SHARE = 50; | |
| 127 | /** A subtree larger than this has its access rebuilt by a queue job, not inline. */ | |
| 128 | export const FOLIO_INLINE_REACL = 2_000; | |
| 129 | ||
| 130 | // ── Addresses ───────────────────────────────────────────────────────── | |
| 131 | ||
| 132 | const FOLIO_ID = /(fol_[0-9a-hjkmnp-tv-z]{26})$/; | |
| 133 | ||
| 134 | /** The words of a title as an address: `q4-roadmap`. At most 50 characters. */ | |
| 135 | export function folioTitleSlug(title: string): string { | |
| 136 | return title | |
| 137 | .normalize("NFKD") | |
| 138 | .replace(/[̀-ͯ]/g, "") | |
| 139 | .toLowerCase() | |
| 140 | .replace(/[^a-z0-9]+/g, "-") | |
| 141 | .replace(/^-+|-+$/g, "") | |
| 142 | .slice(0, 50) | |
| 143 | .replace(/-+$/g, ""); | |
| 144 | } | |
| 145 | ||
| 146 | /** A folio's last address segment, `<title-slug>-<id>`: only the id is read, so renames keep links working. */ | |
| 147 | export function folioSlug(title: string, id: string): string { | |
| 148 | const words = folioTitleSlug(title); | |
| 149 | return words ? `${words}-${id}` : id; | |
| 150 | } | |
| 151 | ||
| 152 | /** A folio's address. Flat: moving it between spaces and Private never breaks a link. */ | |
| 153 | export function folioPath(workspace: string, title: string, id: string): string { | |
| 154 | return `/${workspace}/-/artifacts/${folioSlug(title, id)}`; | |
| 155 | } | |
| 156 | ||
| 157 | /** The folio id at the end of an address segment, or null. */ | |
| 158 | export function folioIdFrom(segment: string | null | undefined): string | null { | |
| 159 | return FOLIO_ID.exec(String(segment ?? ""))?.[1] ?? null; | |
| 160 | } | |
| 161 | ||
| 162 | /** Every folio id a text links to, for backlinks. */ | |
| 163 | export function linkedFolioIds(text: string): string[] { | |
| 164 | return [...new Set(text.match(/fol_[0-9a-hjkmnp-tv-z]{26}/g) ?? [])]; | |
| 165 | } | |
| 166 | ||
| 167 | /** Segments the site's routes use under `-/artifacts/`: never a folio. */ | |
| 168 | export const FOLIO_ROUTE_SEGMENTS: readonly string[] = ["live", "api", "query", "threads", "upload", "export", "new", "templates", "trash", "stale", "spaces", "repo"]; | |
| 169 | ||
| 170 | // ── Folios ──────────────────────────────────────────────────────────── | |
| 171 | ||
| 172 | /** Enough to link to a folio. */ | |
| 173 | export type FolioRef = { | |
| 174 | id: string; | |
| 175 | kind: FolioKind; | |
| 176 | title: string; | |
| 177 | /** An emoji, or null for the kind's icon. */ | |
| 178 | icon: string | null; | |
| 179 | /** `<title-slug>-<id>`. */ | |
| 180 | slug: string; | |
| 181 | /** `/<ws>/-/artifacts/<slug>`. */ | |
| 182 | path: string; | |
| 183 | }; | |
| 184 | ||
| 185 | /** The space a folio sits in. */ | |
| 186 | export type FolioSpaceRef = { id: string; slug: string; name: string; kind: DocSpaceKind }; | |
| 187 | ||
| 188 | /** Where a folio's access comes from when it inherits. */ | |
| 189 | export type FolioInheritedFrom = { kind: "space" | "folio"; id: string; name: string }; | |
| 190 | ||
| 191 | /** What a card draws: written when the folio is saved, never data values. */ | |
| 192 | export type FolioPreview = { kind: "doc"; lines: string[] } | SlidesPreview | DesignPreview | DashboardPreview; | |
| 193 | ||
| 194 | /** A folio as lists and its page show it, for one viewer. */ | |
| 195 | export type Folio = FolioRef & { | |
| 196 | workspace_id: string; | |
| 197 | /** Null: its owner's Private section. */ | |
| 198 | space: FolioSpaceRef | null; | |
| 199 | parent_id: string | null; | |
| 200 | position: number; | |
| 201 | owner: MemberProfile; | |
| 202 | created_by: MemberProfile; | |
| 203 | created_at: string; | |
| 204 | /** Any change: rename, move, share. */ | |
| 205 | updated_at: string; | |
| 206 | /** Content changes: "Edited 45m ago". */ | |
| 207 | edited_by: MemberProfile | null; | |
| 208 | edited_at: string; | |
| 209 | trashed_at: string | null; | |
| 210 | viewer_role: FolioRole; | |
| 211 | favorite: boolean; | |
| 212 | /** Only its owner can read it (the lock). */ | |
| 213 | private: boolean; | |
| 214 | /** How many people, agents and teams it is shared with directly. */ | |
| 215 | shared_count: number; | |
| 216 | /** The access root's general access. */ | |
| 217 | general_access: FolioGeneralAccess; | |
| 218 | general_role: FolioGeneralRole | null; | |
| 219 | /** False: "Only people invited", its own access root. */ | |
| 220 | inherit: boolean; | |
| 221 | inherited_from: FolioInheritedFrom | null; | |
| 222 | /** How agents change it: its own, else its space's, else `suggest`. */ | |
| 223 | agent_mode: DocAgentMode; | |
| 224 | excerpt: string; | |
| 225 | preview: FolioPreview | null; | |
| 226 | /** Where it was written up from, such as a chat thread, when it was. */ | |
| 227 | source: { title: string; href: string } | null; | |
| 228 | /** Possibly out of date: code it cites changed. */ | |
| 229 | stale: boolean; | |
| 230 | has_children: boolean; | |
| 231 | }; | |
| 232 | ||
| 233 | /** A row of the sidebar's trees. */ | |
| 234 | export type FolioTreeNode = { | |
| 235 | id: string; | |
| 236 | kind: FolioKind; | |
| 237 | parent_id: string | null; | |
| 238 | position: number; | |
| 239 | title: string; | |
| 240 | icon: string | null; | |
| 241 | slug: string; | |
| 242 | /** It restricts access below where it sits (a lock on the row). */ | |
| 243 | restricted: boolean; | |
| 244 | stale?: boolean; | |
| 245 | }; | |
| 246 | ||
| 247 | export type FolioListTab = "all" | "yours" | "shared"; | |
| 248 | export const FOLIO_LIST_TABS: readonly FolioListTab[] = ["all", "yours", "shared"]; | |
| 249 | ||
| 250 | export type FolioListQuery = { | |
| 251 | tab: FolioListTab; | |
| 252 | kinds?: FolioKind[] | null; | |
| 253 | space_id?: string | null; | |
| 254 | /** A member key, `user:<id>`. */ | |
| 255 | owner?: string | null; | |
| 256 | /** `owner/name`. */ | |
| 257 | project?: string | null; | |
| 258 | /** Words or meaning; hybrid search when given. */ | |
| 259 | q?: string | null; | |
| 260 | cursor?: string | null; | |
| 261 | limit?: number | null; | |
| 262 | }; | |
| 263 | ||
| 264 | /** The most folios a page of a list holds. */ | |
| 265 | export const FOLIO_LIST_MAX = 100; | |
| 266 | ||
| 267 | export type FolioList = { items: Folio[]; next_cursor: string | null }; | |
| 268 | ||
| The docs service answers folio_page with a folio and what its page shows around it, a space can let its editors share what is in it, and someone asking for access to an artifact is told to wait when they already asked for it in the last day. | 269 | /** A folio as its own page opens it: the folio, its saved text, where it sits, what is in it and what links to it. */ |
| 270 | export type FolioPage = { | |
| 271 | folio: Folio; | |
| 272 | /** Its text rendition when last saved (a doc's Markdown): what shows until the live editor loads. */ | |
| 273 | text: string; | |
| 274 | /** The docs it sits under, from the top, that the viewer can read. */ | |
| 275 | breadcrumbs: FolioRef[]; | |
| 276 | /** What sits under it (a doc's sub-pages) that the viewer can read. */ | |
| 277 | children: FolioRef[]; | |
| 278 | /** Folios the viewer can read that link to it. */ | |
| 279 | backlinks: FolioRef[]; | |
| 280 | /** A doc's open suggestions. */ | |
| 281 | suggestions: FolioSuggestion[]; | |
| 282 | }; | |
| 283 | ||
| Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet | 284 | export type FoliosSidebarSpace = DocSpace & { joined: boolean; tree: FolioTreeNode[] }; |
| 285 | ||
| 286 | export type FoliosSidebar = { | |
| 287 | favorites: FolioRef[]; | |
| 288 | /** Joined open spaces, the viewer's team spaces and Members-only spaces. */ | |
| 289 | spaces: FoliosSidebarSpace[]; | |
| 290 | /** The viewer's own folios in no space. */ | |
| 291 | private_tree: FolioTreeNode[]; | |
| 292 | /** The tops of what is shared with the viewer: the highest ancestor they can read. */ | |
| 293 | shared: FolioRef[]; | |
| 294 | /** Projects' docs: repositories' `docs/` folders, read-only. */ | |
| 295 | repos: DocRepoSpace[]; | |
| 296 | can_create_space: boolean; | |
| 297 | trash_count: number; | |
| 298 | stale_count: number; | |
| 299 | }; | |
| 300 | ||
| 301 | /** Content to start a folio with: Markdown for docs and slides, a spec for designs and dashboards. */ | |
| 302 | export type FolioContentInput = { markdown: string } | { spec: unknown }; | |
| 303 | ||
| 304 | export type NewFolio = { | |
| 305 | kind: FolioKind; | |
| 306 | title?: string | null; | |
| 307 | icon?: string | null; | |
| 308 | /** Null or absent: the creator's Private section. */ | |
| 309 | space_id?: string | null; | |
| 310 | /** A doc to sit under. */ | |
| 311 | parent_id?: string | null; | |
| 312 | template_id?: string | null; | |
| 313 | content?: FolioContentInput | null; | |
| 314 | source?: { title: string; href: string } | null; | |
| 315 | share_with?: { principal: FolioPrincipal; role: FolioRole }[] | null; | |
| 316 | }; | |
| 317 | ||
| 318 | export type FolioChange = { | |
| 319 | title?: string; | |
| 320 | icon?: string | null; | |
| 321 | cover?: string | null; | |
| 322 | projects?: string[]; | |
| 323 | }; | |
| 324 | ||
| 325 | /** Where to put a folio: a space (null: Private) and a parent doc, before a sibling or last. */ | |
| 326 | export type FolioMove = { space_id: string | null; parent_id: string | null; before_id?: string | null }; | |
| 327 | ||
| 328 | // ── Sharing ─────────────────────────────────────────────────────────── | |
| 329 | ||
| 330 | /** Where a row of "Who has access" comes from. */ | |
| 331 | export type FolioAccessSource = | |
| 332 | | { kind: "owner" } | |
| 333 | /** A grant on this folio: editable here. */ | |
| 334 | | { kind: "grant" } | |
| 335 | /** A grant on a parent it inherits from: change it there. */ | |
| 336 | | { kind: "folio"; id: string; title: string; path: string } | |
| 337 | /** The space it inherits from. */ | |
| 338 | | { kind: "space"; id: string; name: string }; | |
| 339 | ||
| 340 | export type FolioAccessRow = { | |
| 341 | principal: FolioPrincipal; | |
| 342 | /** A person or agent; a team is shown by name. */ | |
| 343 | profile: MemberProfile | { kind: "team"; id: string; name: string; display_name: string }; | |
| 344 | role: FolioRole; | |
| 345 | source: FolioAccessSource; | |
| 346 | }; | |
| 347 | ||
| 348 | /** The share dialog. */ | |
| 349 | export type FolioAccessList = { | |
| 350 | folio_id: string; | |
| 351 | owner: MemberProfile; | |
| 352 | rows: FolioAccessRow[]; | |
| 353 | general_access: FolioGeneralAccess; | |
| 354 | general_role: FolioGeneralRole | null; | |
| 355 | inherit: boolean; | |
| 356 | inherited_from: FolioInheritedFrom | null; | |
| 357 | /** The folio's own setting; null follows its space's. */ | |
| 358 | agent_mode: DocAgentMode | null; | |
| 359 | /** The viewer may change any of it. */ | |
| 360 | can_share: boolean; | |
| 361 | /** Public links are off until a workspace turns them on (later). */ | |
| 362 | public_link: "off"; | |
| 363 | }; | |
| 364 | ||
| 365 | /** One change from the share dialog. */ | |
| 366 | export type FolioAccessChange = | |
| 367 | /** Share with someone, or change their role; `notify` is an optional message. */ | |
| 368 | | { op: "grant"; principal: FolioPrincipal; role: FolioRole; notify?: string | null } | |
| 369 | | { op: "revoke"; principal: FolioPrincipal } | |
| 370 | /** `none` takes no role; `workspace` and `link` take one, never `manage`. */ | |
| 371 | | { op: "general"; access: FolioGeneralAccess; role: FolioGeneralRole | null } | |
| 372 | /** Follow the space or parent (true), or "Only people invited" (false). */ | |
| 373 | | { op: "inherit"; inherit: boolean } | |
| 374 | | { op: "agent_mode"; agent_mode: DocAgentMode | null }; | |
| 375 | ||
| 376 | // ── History, proposals, templates ───────────────────────────────────── | |
| 377 | ||
| 378 | export type FolioVersionKind = "created" | "edit" | "agent" | "suggestion" | "proposal" | "restore"; | |
| 379 | ||
| 380 | export type FolioVersion = { | |
| 381 | id: string; | |
| 382 | folio_id: string; | |
| 383 | created_at: string; | |
| 384 | authors: MemberProfile[]; | |
| 385 | kind: FolioVersionKind; | |
| 386 | note: string | null; | |
| 387 | }; | |
| 388 | ||
| 389 | export type FolioVersionDetail = FolioVersion & { | |
| 390 | /** The folio's text rendition then. */ | |
| 391 | text: string; | |
| 392 | /** Against the version before it. */ | |
| 393 | diff: DocDiffLine[]; | |
| 394 | }; | |
| 395 | ||
| 396 | export type FolioProposalStatus = "open" | "accepted" | "rejected" | "stale"; | |
| 397 | ||
| 398 | /** An agent's whole change to a slides deck, design or dashboard, which a person previews and applies or rejects. */ | |
| 399 | export type FolioProposal = { | |
| 400 | id: string; | |
| 401 | folio_id: string; | |
| 402 | author: MemberProfile; | |
| 403 | asked_by: MemberProfile | null; | |
| 404 | note: string | null; | |
| 405 | /** "Adds slides 4–6; rewrites the title slide". */ | |
| 406 | summary: string; | |
| 407 | status: FolioProposalStatus; | |
| 408 | created_at: string; | |
| 409 | decided_by: MemberProfile | null; | |
| 410 | decided_at: string | null; | |
| 411 | }; | |
| 412 | ||
| 413 | /** A doc's tracked change, as Docs has them, on a folio. */ | |
| 414 | export type FolioSuggestion = Omit<DocSuggestion, "page_id"> & { folio_id: string }; | |
| 415 | ||
| 416 | export type FolioTemplate = { | |
| 417 | id: string; | |
| 418 | kind: FolioKind; | |
| 419 | name: string; | |
| 420 | description: string; | |
| 421 | icon: string | null; | |
| 422 | builtin: boolean; | |
| 423 | /** Markdown (doc, slides) or a JSON spec (design, dashboard). */ | |
| 424 | body: string; | |
| 425 | created_by: MemberProfile | null; | |
| 426 | }; | |
| 427 | ||
| 428 | // ── Live ────────────────────────────────────────────────────────────── | |
| 429 | ||
| 430 | /** JSON text frames on a folio's socket, besides the Yjs protocol's binary ones. */ | |
| 431 | export type FoliosLiveEvent = | |
| 432 | | { type: "folio.updated"; folio: Folio } | |
| 433 | | { type: "folio.trashed"; folio_id: string } | |
| 434 | | { type: "suggestion.created" | "suggestion.updated"; suggestion: FolioSuggestion } | |
| 435 | | { type: "proposal.created" | "proposal.updated"; proposal: FolioProposal } | |
| 436 | | { type: "version.created"; version: FolioVersion } | |
| 437 | /** Whether it is possibly out of date changed: ask again. */ | |
| 438 | | { type: "folio.staleness" } | |
| 439 | /** Its sharing changed: ask for the access list again to refresh badges. */ | |
| 440 | | { type: "folio.access" } | |
| 441 | /** The viewer's role changed, or their access ended (`role` null; the socket then closes with 4403). */ | |
| 442 | | { type: "access"; role: FolioRole | null }; | |
| 443 | ||
| 444 | // ── Agents ──────────────────────────────────────────────────────────── | |
| 445 | ||
| 446 | /** Who will see what an agent says. More than 20 people reads as the workspace. */ | |
| 447 | export type FolioAudience = DocAudience; | |
| 448 | ||
| 449 | export type FolioAgentAbilities = DocAgentAbilities; | |
| 450 | ||
| 451 | /** Options on any agent edit. */ | |
| 452 | export type FolioEditOptions = { | |
| 453 | note?: string | null; | |
| 454 | /** Never edit directly: file a suggestion (doc) or a proposal (others). */ | |
| 455 | suggest_only?: boolean | null; | |
| 456 | /** The edit brings it up to date with the code it cites. */ | |
| 457 | marks_current?: boolean | null; | |
| 458 | }; | |
| 459 | ||
| 460 | /** An agent's (or a token's) change to a folio, in the kind's own terms. */ | |
| 461 | export type FolioAgentEdit = ( | |
| 462 | | { kind: "doc"; target: DocEditTarget; markdown: string } | |
| 463 | | { kind: "slides"; ops: SlidesOp[] } | |
| 464 | | { kind: "design"; ops: DesignOp[] } | |
| 465 | | { kind: "dashboard"; ops: DashboardOp[] } | |
| 466 | ) & | |
| 467 | FolioEditOptions; | |
| 468 | ||
| 469 | /** The most ops one edit carries. */ | |
| 470 | export const FOLIO_MAX_OPS = 200; | |
| 471 | ||
| 472 | export type FolioAgentEditResult = | |
| 473 | | { mode: "applied"; version_id: string | null; folio: FolioRef; summary: string } | |
| 474 | | { mode: "suggested"; suggestion: FolioSuggestion; folio: FolioRef } | |
| 475 | | { mode: "proposed"; proposal: FolioProposal; folio: FolioRef }; | |
| 476 | ||
| 477 | /** | |
| 478 | * A folio in the form an agent reads and writes: a doc's Markdown with its | |
| 479 | * block ids; a deck's Markdown with slide ids; a design's node spec; a | |
| 480 | * dashboard's spec (tiles and queries, never values). | |
| 481 | */ | |
| 482 | export type FolioAgentRead = { | |
| 483 | folio: FolioRef & { edited_at: string }; | |
| 484 | space: { id: string; slug: string; name: string; agent_mode: DocAgentMode } | null; | |
| 485 | /** Markdown for doc and slides; JSON text for design and dashboard. */ | |
| 486 | content: string; | |
| 487 | /** doc: top-level blocks, for `blocks` targets. */ | |
| 488 | blocks?: DocBlockOutline[]; | |
| 489 | can: FolioAgentAbilities; | |
| 490 | /** False: someone the agent is talking to can't read it, so don't quote it there. */ | |
| 491 | audience_can_read: boolean; | |
| 492 | }; | |
| 493 | ||
| 494 | /** One passage recalled for an agent: part of a folio, or of a repository's docs. */ | |
| 495 | export type FolioPassage = { | |
| 496 | folio: FolioRef | null; | |
| 497 | repo_file: { repo: string; path: string; href: string } | null; | |
| 498 | space_name: string; | |
| 499 | heading: string | null; | |
| 500 | text: string; | |
| 501 | score: number; | |
| 502 | updated_at: string; | |
| 503 | stale: boolean; | |
| 504 | }; | |
| 505 | ||
| 506 | export type FolioSearchHit = FolioRef & { | |
| 507 | space_name: string | null; | |
| 508 | snippet: string; | |
| 509 | edited_at: string; | |
| 510 | heading?: string | null; | |
| 511 | matched?: "words" | "meaning" | "both" | null; | |
| 512 | }; | |
| 513 | ||
| 514 | // ── Validators (pure; mirrored in Rust) ─────────────────────────────── | |
| 515 | ||
| 516 | function isObject(value: unknown): value is Record<string, unknown> { | |
| 517 | return typeof value === "object" && value !== null && !Array.isArray(value); | |
| 518 | } | |
| 519 | ||
| 520 | const isRole = (value: unknown): value is FolioRole => typeof value === "string" && (FOLIO_ROLES as readonly string[]).includes(value); | |
| 521 | ||
| 522 | /** What is wrong with a new folio, or null. Whether its space and parent exist and allow it is the service's to say. */ | |
| 523 | export function newFolioError(input: NewFolio): string | null { | |
| 524 | if (!isFolioKind(input.kind)) return `There is no kind of artifact called ${String(input.kind)}.`; | |
| 525 | const title = input.title ?? ""; | |
| 526 | if ([...title].length > FOLIO_MAX_TITLE) return `A title is at most ${FOLIO_MAX_TITLE} characters.`; | |
| 527 | const content = input.content ?? null; | |
| 528 | if (content !== null) { | |
| 529 | const wantsMarkdown = input.kind === "doc" || input.kind === "slides"; | |
| 530 | if (wantsMarkdown && typeof (content as { markdown?: unknown }).markdown !== "string") return `A ${FOLIO_KIND_NOUNS[input.kind]} starts from markdown.`; | |
| 531 | if (!wantsMarkdown && !isObject((content as { spec?: unknown }).spec)) return `A ${FOLIO_KIND_NOUNS[input.kind]} starts from a spec.`; | |
| 532 | if (input.template_id != null) return "Start from a template or from content, not both."; | |
| 533 | } | |
| 534 | const share = input.share_with ?? []; | |
| 535 | if (share.length > FOLIO_MAX_SHARE) return `Share with at most ${FOLIO_MAX_SHARE} at once.`; | |
| 536 | for (const row of share) { | |
| 537 | if (!isFolioPrincipal(row.principal)) return `${String(row.principal)} is not user:, agent: or team: and an id.`; | |
| 538 | if (!isRole(row.role)) return `There is no role called ${String(row.role)}.`; | |
| 539 | } | |
| 540 | return null; | |
| 541 | } | |
| 542 | ||
| 543 | /** What is wrong with a change from the share dialog, or null. */ | |
| 544 | export function folioAccessChangeError(change: FolioAccessChange): string | null { | |
| 545 | switch (change.op) { | |
| 546 | case "grant": | |
| 547 | if (!isFolioPrincipal(change.principal)) return `${String(change.principal)} is not user:, agent: or team: and an id.`; | |
| 548 | if (!isRole(change.role)) return `There is no role called ${String(change.role)}.`; | |
| 549 | if (change.notify != null && [...change.notify].length > 2_000) return "A message is at most 2000 characters."; | |
| 550 | return null; | |
| 551 | case "revoke": | |
| 552 | return isFolioPrincipal(change.principal) ? null : `${String(change.principal)} is not user:, agent: or team: and an id.`; | |
| 553 | case "general": | |
| 554 | if (!(FOLIO_GENERAL_ACCESS as readonly string[]).includes(change.access)) return `There is no general access called ${String(change.access)}.`; | |
| 555 | if (change.access === "none") return change.role === null ? null : "Restricted takes no role."; | |
| 556 | if (change.role === null) return `${FOLIO_GENERAL_ACCESS_LABELS[change.access]} needs a role.`; | |
| 557 | return (FOLIO_GENERAL_ROLES as readonly string[]).includes(change.role) ? null : "General access gives view, comment or edit, never full access."; | |
| 558 | case "inherit": | |
| 559 | return typeof change.inherit === "boolean" ? null : "inherit is true or false."; | |
| 560 | case "agent_mode": | |
| 561 | return change.agent_mode === null || change.agent_mode === "suggest" || change.agent_mode === "edit" ? null : "agent_mode is suggest, edit or null."; | |
| 562 | default: | |
| 563 | return `There is no access change called ${String((change as { op?: unknown }).op)}.`; | |
| 564 | } | |
| 565 | } | |
| 566 | ||
| 567 | /** | |
| 568 | * What is wrong with an agent edit's envelope, or null: its kind, and a | |
| 569 | * doc's target and Markdown or the other kinds' list of ops. Each op is | |
| 570 | * checked by its kind's validator (`slidesOpError`, `designOpError`, | |
| 571 | * `dashboardOpError`). | |
| 572 | */ | |
| 573 | export function folioAgentEditError(edit: unknown): string | null { | |
| 574 | if (!isObject(edit)) return "An edit is an object."; | |
| 575 | if (!isFolioKind(edit.kind)) return `There is no kind of artifact called ${String(edit.kind)}.`; | |
| 576 | if (edit.note != null && typeof edit.note !== "string") return "note is text."; | |
| 577 | if (edit.kind === "doc") { | |
| 578 | if (typeof edit.markdown !== "string") return "A doc edit has markdown."; | |
| 579 | if (edit.ops !== undefined) return "A doc edit has a target and markdown, not ops."; | |
| 580 | const target = edit.target; | |
| 581 | if (!isObject(target)) return "A doc edit has a target."; | |
| 582 | switch (target.kind) { | |
| 583 | case "append": | |
| 584 | case "document": | |
| 585 | return null; | |
| 586 | case "section": | |
| 587 | return typeof target.heading === "string" && target.heading.length > 0 ? null : "A section target names its heading."; | |
| 588 | case "blocks": | |
| 589 | return typeof target.from_block === "string" && typeof target.to_block === "string" ? null : "A blocks target names from_block and to_block."; | |
| 590 | default: | |
| 591 | return "A doc edit's target is append, document, section or blocks."; | |
| 592 | } | |
| 593 | } | |
| 594 | if (edit.markdown !== undefined || edit.target !== undefined) return `A ${FOLIO_KIND_NOUNS[edit.kind]} edit has ops, not a target and markdown.`; | |
| 595 | if (!Array.isArray(edit.ops) || edit.ops.length === 0) return `A ${FOLIO_KIND_NOUNS[edit.kind]} edit has a list of ops.`; | |
| 596 | if (edit.ops.length > FOLIO_MAX_OPS) return `An edit has at most ${FOLIO_MAX_OPS} ops.`; | |
| 597 | return edit.ops.every((op) => isObject(op) && typeof op.op === "string") ? null : "Each op is an object with an op."; | |
| 598 | } | |
| 599 | ||
| 600 | /** What is wrong with a list query, or null. */ | |
| 601 | export function folioListQueryError(query: FolioListQuery): string | null { | |
| 602 | if (!(FOLIO_LIST_TABS as readonly string[]).includes(query.tab)) return "tab is all, yours or shared."; | |
| 603 | for (const kind of query.kinds ?? []) if (!isFolioKind(kind)) return `There is no kind of artifact called ${String(kind)}.`; | |
| 604 | const limit = query.limit ?? null; | |
| 605 | if (limit !== null && (!Number.isInteger(limit) || limit < 1 || limit > FOLIO_LIST_MAX)) return `limit is between 1 and ${FOLIO_LIST_MAX}.`; | |
| 606 | return null; | |
| 607 | } | |
| 608 | ||
| 609 | // ── The docs service's folio RPC ────────────────────────────────────── | |
| 610 | ||
| 611 | /** Every method the docs service answers for folios at `/rpc/<method>` (plan section 7). */ | |
| 612 | export const FOLIO_RPC_METHODS = [ | |
| 613 | // Lists and navigation. | |
| 614 | "folio_list", | |
| 615 | "folio_sidebar", | |
| 616 | "folio", | |
| The docs service answers folio_page with a folio and what its page shows around it, a space can let its editors share what is in it, and someone asking for access to an artifact is told to wait when they already asked for it in the last day. | 617 | "folio_page", |
| Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet | 618 | // Changing folios. |
| 619 | "create_folio", | |
| 620 | "update_folio", | |
| 621 | "move_folio", | |
| 622 | "duplicate_folio", | |
| 623 | "trash_folio", | |
| 624 | "restore_folio", | |
| 625 | "delete_folio", | |
| 626 | "folio_trash", | |
| 627 | "favorite_folio", | |
| 628 | // A person's (or a token's) own read and edit in the agent form. | |
| 629 | "folio_content", | |
| 630 | "edit_folio", | |
| 631 | // Sharing and spaces. | |
| 632 | "folio_access", | |
| 633 | "set_folio_grant", | |
| 634 | "set_folio_general_access", | |
| 635 | "request_folio_access", | |
| 636 | "join_space", | |
| 637 | "leave_space", | |
| 638 | // Search, history, templates and export. | |
| 639 | "search_folios", | |
| 640 | "folio_versions", | |
| 641 | "folio_version", | |
| 642 | "restore_folio_version", | |
| 643 | "folio_templates", | |
| 644 | "save_folio_template", | |
| 645 | "delete_folio_template", | |
| 646 | "export_folio", | |
| 647 | // Suggestions, proposals and comments. | |
| 648 | "folio_suggestions", | |
| 649 | "decide_folio_suggestion", | |
| 650 | "folio_proposals", | |
| 651 | "decide_folio_proposal", | |
| 652 | "folio_thread", | |
| 653 | "folio_threads", | |
| 654 | // Dashboards. | |
| 655 | "query_tile", | |
| 656 | "query_dataset", | |
| 657 | "query_dataset_for_agent", | |
| 658 | // Agents. | |
| 659 | "folios_for_agent", | |
| 660 | "read_folio_for_agent", | |
| 661 | "create_folio_as_agent", | |
| 662 | "edit_folio_as_agent", | |
| 663 | "share_folio_as_agent", | |
| 664 | "recall_folios_for_agent", | |
| 665 | "stale_folios_for_agent", | |
| 666 | "mark_folio_current", | |
| 667 | "reindex_folios", | |
| 668 | ] as const; | |
| 669 | ||
| 670 | export type FolioRpcMethod = (typeof FOLIO_RPC_METHODS)[number]; | |
| 671 | ||
| 672 | export type FoliosApi = { | |
| 673 | // ── The site, and the API acting for a person ─────────────────────── | |
| 674 | list(workspace: string, viewer: User, query: FolioListQuery): Promise<Result<FolioList>>; | |
| 675 | sidebar(workspace: string, viewer: User): Promise<Result<FoliosSidebar>>; | |
| 676 | /** A folio and the viewer's role in it; records the visit (which is what makes a link folio readable). Not found when they can't read it. */ | |
| 677 | folio(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| The docs service answers folio_page with a folio and what its page shows around it, a space can let its editors share what is in it, and someone asking for access to an artifact is told to wait when they already asked for it in the last day. | 678 | /** As `folio`, with what its page shows around it. Records the visit too. */ |
| 679 | page(workspace: string, viewer: User, folioId: string): Promise<Result<FolioPage>>; | |
| Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet | 680 | create(workspace: string, viewer: User, input: NewFolio): Promise<Result<Folio>>; |
| 681 | update(workspace: string, viewer: User, folioId: string, change: FolioChange): Promise<Result<Folio>>; | |
| 682 | /** Edit role where it is and where it goes. Refuses moving under itself, under a non-doc, or deeper than `FOLIO_MAX_DEPTH`. */ | |
| 683 | move(workspace: string, viewer: User, folioId: string, move: FolioMove): Promise<Result<Folio>>; | |
| 684 | duplicate(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 685 | /** To the trash, with everything under it. */ | |
| 686 | trash(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 687 | restore(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 688 | /** For good: only from the trash, manage role. */ | |
| 689 | delete(workspace: string, viewer: User, folioId: string): Promise<Result<boolean>>; | |
| 690 | trashed(workspace: string, viewer: User): Promise<Result<Folio[]>>; | |
| 691 | favorite(workspace: string, viewer: User, folioId: string, on: boolean): Promise<Result<boolean>>; | |
| 692 | /** The folio in its agent form, for a person or their token. */ | |
| 693 | content(workspace: string, viewer: User, folioId: string): Promise<Result<FolioAgentRead>>; | |
| 694 | /** Applies an edit in the kind's terms as the viewer: edit role, else a suggestion or proposal with comment role. */ | |
| 695 | edit(workspace: string, viewer: User, folioId: string, edit: FolioAgentEdit): Promise<Result<FolioAgentEditResult>>; | |
| 696 | ||
| 697 | access(workspace: string, viewer: User, folioId: string): Promise<Result<FolioAccessList>>; | |
| 698 | /** One change from the share dialog: grants go to `set_folio_grant`, the rest to `set_folio_general_access`. */ | |
| 699 | changeAccess(workspace: string, viewer: User, folioId: string, change: FolioAccessChange): Promise<Result<FolioAccessList>>; | |
| 700 | /** Asks the owner and managers for access; they get an inbox item. */ | |
| 701 | requestAccess(workspace: string, viewer: User, folioId: string, message?: string | null): Promise<Result<boolean>>; | |
| 702 | joinSpace(workspace: string, viewer: User, spaceId: string): Promise<Result<boolean>>; | |
| 703 | leaveSpace(workspace: string, viewer: User, spaceId: string): Promise<Result<boolean>>; | |
| 704 | ||
| 705 | search(workspace: string, viewer: User, query: { q: string; kinds?: FolioKind[] | null; space_id?: string | null; project?: string | null; owner?: string | null; mode?: "words" | "hybrid" | null; limit?: number | null }): Promise<Result<FolioSearchHit[]>>; | |
| 706 | versions(workspace: string, viewer: User, folioId: string): Promise<Result<FolioVersion[]>>; | |
| 707 | version(workspace: string, viewer: User, folioId: string, versionId: string): Promise<Result<FolioVersionDetail>>; | |
| 708 | restoreVersion(workspace: string, viewer: User, folioId: string, versionId: string): Promise<Result<FolioVersion>>; | |
| 709 | templates(workspace: string, viewer: User, kind?: FolioKind | null): Promise<Result<FolioTemplate[]>>; | |
| 710 | saveTemplate(workspace: string, viewer: User, input: { folio_id: string; name: string; description?: string | null }): Promise<Result<FolioTemplate>>; | |
| 711 | deleteTemplate(workspace: string, viewer: User, templateId: string): Promise<Result<boolean>>; | |
| 712 | export(workspace: string, viewer: User, folioId: string, format?: "markdown" | "json" | null): Promise<Result<{ filename: string; content_type: string; body: string }>>; | |
| 713 | ||
| 714 | suggestions(workspace: string, viewer: User, folioId: string): Promise<Result<FolioSuggestion[]>>; | |
| 715 | decideSuggestion(workspace: string, viewer: User, suggestionId: string, decision: "accept" | "reject"): Promise<Result<FolioSuggestion>>; | |
| 716 | proposals(workspace: string, viewer: User, folioId: string): Promise<Result<FolioProposal[]>>; | |
| 717 | decideProposal(workspace: string, viewer: User, proposalId: string, decision: "accept" | "reject"): Promise<Result<FolioProposal>>; | |
| 718 | thread(workspace: string, viewer: User, folioId: string, action: DocThreadAction): Promise<Result<unknown>>; | |
| 719 | threads(workspace: string, viewer: User, folioId: string): Promise<Result<DocThread[]>>; | |
| 720 | ||
| 721 | /** A dashboard tile's numbers for the viewer: the tile's query, read from the room. */ | |
| 722 | queryTile(workspace: string, viewer: User, folioId: string, tileId: string): Promise<Result<DatasetResult>>; | |
| 723 | /** A query as the viewer, for the API. */ | |
| 724 | queryDataset(workspace: string, viewer: User, query: DatasetQuery): Promise<Result<DatasetResult>>; | |
| 725 | ||
| 726 | // ── Agents (services/agents) ──────────────────────────────────────── | |
| 727 | // | |
| 728 | // Each takes the agent and the person it acts for (`viewer`). The agent | |
| 729 | // never reads or changes more than that person can, narrowed to what | |
| 730 | // everyone in `audience` can read. A folio it may not read is not found. | |
| 731 | ||
| 732 | foliosForAgent(workspace: string, agentId: string, viewer: User, query: FolioListQuery, audience?: FolioAudience | null): Promise<Result<FolioList>>; | |
| 733 | readForAgent(workspace: string, agentId: string, viewer: User, folioId: string, audience?: FolioAudience | null): Promise<Result<FolioAgentRead>>; | |
| 734 | /** | |
| 735 | * A new folio: the viewer owns it, the agent made it and gets `edit` on | |
| 736 | * it. `where` is a space the viewer can edit, `private`, or | |
| 737 | * `conversation` (Private plus `view` for these people). | |
| 738 | */ | |
| 739 | createAsAgent( | |
| 740 | workspace: string, | |
| 741 | agentId: string, | |
| 742 | viewer: User, | |
| 743 | input: { kind: FolioKind; title: string; content?: FolioContentInput | null; template_id?: string | null; where: { space_id: string } | "private" | { conversation: string[] }; parent_id?: string | null; source?: { title: string; href: string } | null }, | |
| 744 | ): Promise<Result<FolioRef>>; | |
| 745 | editAsAgent(workspace: string, agentId: string, viewer: User, folioId: string, edit: FolioAgentEdit): Promise<Result<FolioAgentEditResult>>; | |
| 746 | /** `view` or `comment` for people already in the conversation, when the viewer has `manage`. Never general access, `edit` or `manage`. */ | |
| 747 | shareAsAgent(workspace: string, agentId: string, viewer: User, folioId: string, input: { user_ids: string[]; role: "view" | "comment" }, audience: FolioAudience): Promise<Result<FolioAccessList>>; | |
| 748 | recallForAgent(workspace: string, agentId: string, viewer: User, input: { query: string; limit?: number | null; spaces?: string[] | null; kinds?: FolioKind[] | null }, audience?: FolioAudience | null): Promise<Result<FolioPassage[]>>; | |
| 749 | staleForAgent(workspace: string, agentId: string, viewer: User, options?: { repo?: string | null; since?: string | null }, audience?: FolioAudience | null): Promise<Result<Folio[]>>; | |
| 750 | /** As the asker narrowed to repositories every audience member can read; spend only when the audience is the asker alone. */ | |
| 751 | queryDatasetForAgent(workspace: string, agentId: string, viewer: User, input: { query: DatasetQuery } | { folio_id: string; tile_id: string }, audience?: FolioAudience | null): Promise<Result<DatasetResult>>; | |
| 752 | markCurrent(workspace: string, viewer: User, folioId: string): Promise<Result<boolean>>; | |
| 753 | /** Indexes the workspace's folios again in the background. Workspace owners. */ | |
| 754 | reindex(workspace: string, viewer: User): Promise<Result<boolean>>; | |
| 755 | }; | |
| 756 | ||
| 757 | export function foliosClient(service: ServiceBinding): FoliosApi { | |
| 758 | const call = async <T>(method: FolioRpcMethod, args: object): Promise<T> => { | |
| 759 | const response = await service.fetch(`https://service/rpc/${method}`, { | |
| 760 | method: "POST", | |
| 761 | headers: { "content-type": "application/json" }, | |
| 762 | body: JSON.stringify(args), | |
| 763 | }); | |
| 764 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 765 | return (await response.json()) as T; | |
| 766 | }; | |
| 767 | return { | |
| 768 | list: (workspace, viewer, query) => call("folio_list", { workspace, viewer, query }), | |
| 769 | sidebar: (workspace, viewer) => call("folio_sidebar", { workspace, viewer }), | |
| 770 | folio: (workspace, viewer, folioId) => call("folio", { workspace, viewer, folio_id: folioId }), | |
| The docs service answers folio_page with a folio and what its page shows around it, a space can let its editors share what is in it, and someone asking for access to an artifact is told to wait when they already asked for it in the last day. | 771 | page: (workspace, viewer, folioId) => call("folio_page", { workspace, viewer, folio_id: folioId }), |
| Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet | 772 | create: (workspace, viewer, input) => call("create_folio", { workspace, viewer, input }), |
| 773 | update: (workspace, viewer, folioId, change) => call("update_folio", { workspace, viewer, folio_id: folioId, change }), | |
| 774 | move: (workspace, viewer, folioId, move) => call("move_folio", { workspace, viewer, folio_id: folioId, move }), | |
| 775 | duplicate: (workspace, viewer, folioId) => call("duplicate_folio", { workspace, viewer, folio_id: folioId }), | |
| 776 | trash: (workspace, viewer, folioId) => call("trash_folio", { workspace, viewer, folio_id: folioId }), | |
| 777 | restore: (workspace, viewer, folioId) => call("restore_folio", { workspace, viewer, folio_id: folioId }), | |
| 778 | delete: (workspace, viewer, folioId) => call("delete_folio", { workspace, viewer, folio_id: folioId }), | |
| 779 | trashed: (workspace, viewer) => call("folio_trash", { workspace, viewer }), | |
| 780 | favorite: (workspace, viewer, folioId, on) => call("favorite_folio", { workspace, viewer, folio_id: folioId, on }), | |
| 781 | content: (workspace, viewer, folioId) => call("folio_content", { workspace, viewer, folio_id: folioId }), | |
| 782 | edit: (workspace, viewer, folioId, edit) => call("edit_folio", { workspace, viewer, folio_id: folioId, edit }), | |
| 783 | access: (workspace, viewer, folioId) => call("folio_access", { workspace, viewer, folio_id: folioId }), | |
| 784 | changeAccess: (workspace, viewer, folioId, change) => | |
| 785 | call(change.op === "grant" || change.op === "revoke" ? "set_folio_grant" : "set_folio_general_access", { workspace, viewer, folio_id: folioId, change }), | |
| 786 | requestAccess: (workspace, viewer, folioId, message) => call("request_folio_access", { workspace, viewer, folio_id: folioId, message: message ?? null }), | |
| 787 | joinSpace: (workspace, viewer, spaceId) => call("join_space", { workspace, viewer, space_id: spaceId }), | |
| 788 | leaveSpace: (workspace, viewer, spaceId) => call("leave_space", { workspace, viewer, space_id: spaceId }), | |
| 789 | search: (workspace, viewer, query) => call("search_folios", { workspace, viewer, query }), | |
| 790 | versions: (workspace, viewer, folioId) => call("folio_versions", { workspace, viewer, folio_id: folioId }), | |
| 791 | version: (workspace, viewer, folioId, versionId) => call("folio_version", { workspace, viewer, folio_id: folioId, version_id: versionId }), | |
| 792 | restoreVersion: (workspace, viewer, folioId, versionId) => call("restore_folio_version", { workspace, viewer, folio_id: folioId, version_id: versionId }), | |
| 793 | templates: (workspace, viewer, kind) => call("folio_templates", { workspace, viewer, kind: kind ?? null }), | |
| 794 | saveTemplate: (workspace, viewer, input) => call("save_folio_template", { workspace, viewer, input }), | |
| 795 | deleteTemplate: (workspace, viewer, templateId) => call("delete_folio_template", { workspace, viewer, template_id: templateId }), | |
| 796 | export: (workspace, viewer, folioId, format) => call("export_folio", { workspace, viewer, folio_id: folioId, format: format ?? null }), | |
| 797 | suggestions: (workspace, viewer, folioId) => call("folio_suggestions", { workspace, viewer, folio_id: folioId }), | |
| 798 | decideSuggestion: (workspace, viewer, suggestionId, decision) => call("decide_folio_suggestion", { workspace, viewer, suggestion_id: suggestionId, decision }), | |
| 799 | proposals: (workspace, viewer, folioId) => call("folio_proposals", { workspace, viewer, folio_id: folioId }), | |
| 800 | decideProposal: (workspace, viewer, proposalId, decision) => call("decide_folio_proposal", { workspace, viewer, proposal_id: proposalId, decision }), | |
| 801 | thread: (workspace, viewer, folioId, action) => call("folio_thread", { workspace, viewer, folio_id: folioId, action }), | |
| 802 | threads: (workspace, viewer, folioId) => call("folio_threads", { workspace, viewer, folio_id: folioId }), | |
| 803 | queryTile: (workspace, viewer, folioId, tileId) => call("query_tile", { workspace, viewer, folio_id: folioId, tile_id: tileId }), | |
| 804 | queryDataset: (workspace, viewer, query) => call("query_dataset", { workspace, viewer, query }), | |
| 805 | foliosForAgent: (workspace, agentId, viewer, query, audience) => call("folios_for_agent", { workspace, agent_id: agentId, viewer, query, audience: audience ?? null }), | |
| 806 | readForAgent: (workspace, agentId, viewer, folioId, audience) => call("read_folio_for_agent", { workspace, agent_id: agentId, viewer, folio_id: folioId, audience: audience ?? null }), | |
| 807 | createAsAgent: (workspace, agentId, viewer, input) => call("create_folio_as_agent", { workspace, agent_id: agentId, viewer, input }), | |
| 808 | editAsAgent: (workspace, agentId, viewer, folioId, edit) => call("edit_folio_as_agent", { workspace, agent_id: agentId, viewer, folio_id: folioId, edit }), | |
| 809 | shareAsAgent: (workspace, agentId, viewer, folioId, input, audience) => | |
| 810 | call("share_folio_as_agent", { workspace, agent_id: agentId, viewer, folio_id: folioId, ...input, audience }), | |
| 811 | recallForAgent: (workspace, agentId, viewer, input, audience) => | |
| 812 | call("recall_folios_for_agent", { workspace, agent_id: agentId, viewer, ...input, audience: audience ?? null }), | |
| 813 | staleForAgent: (workspace, agentId, viewer, options, audience) => | |
| 814 | call("stale_folios_for_agent", { workspace, agent_id: agentId, viewer, repo: options?.repo ?? null, since: options?.since ?? null, audience: audience ?? null }), | |
| 815 | queryDatasetForAgent: (workspace, agentId, viewer, input, audience) => | |
| 816 | call("query_dataset_for_agent", { workspace, agent_id: agentId, viewer, ...input, audience: audience ?? null }), | |
| 817 | markCurrent: (workspace, viewer, folioId) => call("mark_folio_current", { workspace, viewer, folio_id: folioId }), | |
| 818 | reindex: (workspace, viewer) => call("reindex_folios", { workspace, viewer }), | |
| 819 | }; | |
| 820 | } |