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